Масштабируемая упаковка кругов
Расширенная версия примера упаковки кругов, созданная @giammaria. Таймер помогает анимировать изменение масштаба и прозрачности, что удобно для подробного изучения иерархии. Для простоты при увеличении показываются лишь несколько текстовых примитивов, относящихся к выбранному узлу. Для более подробного представления можно выводить другие типы примитивов, оси и легенды. Автор также подготовил более сложный вариант во внешнем редакторе Vega.
Спецификация Vega в JSON <>
{
"$schema": "https://vega.github.io/schema/vega/v6.json",
"description": "Упаковка кругов для иерархических данных с масштабированием.",
"width": 600,
"height": 600,
"padding": 5,
"signals": [
{
"name": "duration",
"init": "750",
"description": "Длительность перехода при масштабировании. Появление длится столько же, но начинается с заданной здесь задержкой.",
"on": [
{
"events": {"type": "click", "marknames": ["circles", "background"]},
"update": "(event.metaKey || event.ctrlKey ? 4 : 1) *750"
}
]
},
{
"name": "k",
"value": 1,
"description": "Масштаб увеличения относительно выбранного узла.",
"on": [
{
"events": [{"signal": "focus"}],
"update": "focus ? width/(focus.r*2) : 1"
}
]
},
{
"name": "root",
"update": "{'id': data('tree')[0]['id'], 'x': data('tree')[0]['x'], 'y': data('tree')[0]['y'], 'r': data('tree')[0]['r'], 'k': 1, 'children': data('tree')[0]['children']}",
"description": "Корневой узел иерархии."
},
{
"name": "focus",
"init": "root",
"description": "Увеличенный узел иерархии.",
"on": [
{
"events": {"type": "click", "markname": "background"},
"update": "{id: root['id'], 'x': root['x'], 'y': root['y'], 'r': root['r'], 'k': 1,'children': root['children']}"
},
{
"events": {"type": "click", "markname": "circles"},
"update": "(focus['x'] === datum['x'] && focus['y'] === datum['y'] && focus['r'] === datum['r'] && focus['r'] !== root['r']) ? {'id': root['id'], 'x': root['x'], 'y': root['y'], 'r': root['r'], 'k': 1, 'children': root['children']} : {'id': datum['id'], 'x': datum['x'], 'y': datum['y'], 'r': datum['r'], 'k': k, 'children': datum['children']}"
}
]
},
{
"name": "focus0",
"update": "data('focus0') && length(data('focus0'))>0 ? data('focus0')[0] : focus",
"description": "Ранее увеличенный узел иерархии."
},
{
"name": "timer",
"description": "Таймер переходов, включая масштабирование и плавное появление.",
"on": [{"events": "timer", "update": "now()"}]
},
{
"name": "interpolateTime",
"description": "Время начала и конца интерполяции анимации в миллисекундах.",
"on": [
{
"events": {
"type": "click",
"marknames": ["circles", "background"]
},
"update": "{'start': timer, 'end': timer+duration}"
}
]
},
{
"name": "t",
"description": "Нормализованное время для функции плавности.",
"update": "interpolateTime ? clamp((timer-interpolateTime.start)/(interpolateTime.end-interpolateTime.start), 0, 1): null"
},
{
"name": "tEase",
"description": "Расчёт плавности; сейчас используется easeInOutCubic.",
"update": "t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1"
},
{
"name": "interpolateTimeDelayed",
"description": "Время плавного перехода с задержкой.",
"on": [
{
"events": {"signal": "interpolateTime"},
"update": "{'start': interpolateTime['end'], 'end': interpolateTime['end']+duration}"
}
]
},
{
"name": "tDelayed",
"description": "Нормализованное время плавного перехода с задержкой.",
"update": "interpolateTimeDelayed ? clamp((timer-interpolateTimeDelayed.start)/(interpolateTimeDelayed.end-interpolateTimeDelayed.start), 0, 1): null"
},
{
"name": "tEaseDelayed",
"description": "Расчёт плавности с задержкой; сейчас используется easeInOutCubic.",
"update": "tDelayed < 0.5 ? 4 * tDelayed * tDelayed * tDelayed : (tDelayed - 1) * (2 * tDelayed - 2) * (2 * tDelayed - 2) + 1"
},
{
"name": "showDetails",
"description": "Логический признак показа сведений об узле.",
"value": false,
"on": [
{
"events": {
"type": "click",
"marknames": ["circles", "background"],
"filter": [
"!event.altKey && !event.shiftKey",
"event.button === 0"
],
"markname": "circles"
},
"update": "focus['children'] > 0 ? false : datum['id'] === root['id'] || focus0['id'] !== root['id'] && focus['id'] === root['id'] ? false : true"
},
{
"events": {
"type": "click",
"marknames": ["circles", "background"],
"filter": ["event.altKey || event.shiftKey", "event.button === 0"]
},
"update": "focus0['id'] === focus['id'] ? !showDetails : true"
}
]
}
],
"data": [
{
"name": "source",
"url": "data/flare.json",
"transform": [
{
"type": "formula",
"expr": "isValid(datum['parent']) ? datum['parent'] : null",
"as": "parent"
},
{
"type": "formula",
"expr": "isValid(datum['size']) ? datum['size'] : null",
"as": "size"
}
]
},
{
"name": "tree",
"source": "source",
"transform": [
{"type": "stratify", "key": "id", "parentKey": "parent"},
{
"type": "pack",
"field": "size",
"sort": {"field": "value"},
"size": [{"signal": "width"}, {"signal": "height"}]
}
]
},
{
"name": "focus0",
"on": [{"trigger": "focus", "insert": "focus"}],
"transform": [
{"type": "formula", "expr": "now()", "as": "now"},
{
"type": "window",
"ops": ["row_number"],
"as": ["row"],
"sort": {"field": "now", "order": "descending"}
},
{"type": "filter", "expr": "datum['row'] ? datum['row'] == 2 : true "},
{"type": "project", "fields": ["id", "x", "y", "r", "children"]},
{"type": "formula", "expr": "width/(datum['r']*2)", "as": "k"}
]
},
{
"name": "details_data",
"source": "tree",
"transform": [
{
"type": "filter",
"expr": "datum['id'] === focus['id'] && showDetails"
},
{
"type": "formula",
"expr": "['глубина иерархии: ' + datum['depth'], 'число потомков: ' + datum['children'],isValid( datum['size']) ? 'размер: ' + datum['size'] + ' байт' : '']",
"as": "details"
}
]
}
],
"scales": [
{
"name": "color",
"type": "ordinal",
"domain": {"data": "tree", "field": "depth"},
"range": {"scheme": "magma"}
}
],
"marks": [
{
"name": "background",
"description": "Невидимый прямоугольник позади всех элементов, покрывающий весь холст.",
"type": "rect",
"encode": {
"enter": {
"x": {"signal": "-padding['left']"},
"y": {"signal": "-padding['top']"},
"width": {"signal": "width+padding['left']+padding['right']"},
"height": {"signal": "height+padding['top']+padding['bottom']"},
"fillOpacity": {"value": 0}
}
}
},
{
"name": "circles",
"description": "Упакованные круги с масштабированием.",
"type": "symbol",
"from": {"data": "tree"},
"encode": {
"enter": {
"shape": {"value": "circle"},
"fill": {"scale": "color", "field": "depth"},
"cursor": {"value": "pointer"},
"tooltip": {"field": "name"}
},
"update": {
"x": {
"signal": "lerp([root['x']+ (datum['x'] - focus0['x']) * focus0['k'], root['x'] + (datum['x'] - focus['x']) * k], tEase)"
},
"y": {
"signal": "lerp([ root['y'] + (datum['y'] - focus0['y']) * focus0['k'], root['y'] + (datum['y'] - focus['y']) * k], tEase)"
},
"size": {
"signal": "pow(2*(datum['r'] * lerp([focus0['k'], k],tEase)),2)"
},
"fill": {
"signal": "showDetails && focus['id'] === datum['id'] ? '#fff' : scale('color',datum['depth'])"
},
"zindex": {
"signal": "!showDetails ? 1 : (focus['id'] === root['id'] && isValid(datum['parent'])) ? -99 : indexof(pluck(treeAncestors('tree', datum['id']), 'id'), focus['id']) > 0 ? -99 : 1"
},
"stroke": {
"signal": "showDetails ? scale('color', datum['depth']) : luminance(scale('color', datum['depth'])) > 0.5 ? 'black' : 'white'"
},
"strokeWidth": {
"signal": "focus['id'] === datum['id'] && showDetails ? 20 : 0.5"
},
"strokeOpacity": {
"signal": "!showDetails ? 0.5 : focus['id'] === root['id'] ? min(tEase, 0.35) : min(tEaseDelayed, 0.35)"
}
},
"hover": {
"color": {
"signal": "showDetails ? scale('color', datum['depth']) : luminance(scale('color', datum['depth'])) > 0.5 ? 'black' : 'white'"
},
"strokeWidth": {"value": 2}
}
}
},
{
"name": "details_title",
"details": "the name of the node (appears on zoom)",
"type": "text",
"from": {"data": "details_data"},
"interactive": false,
"encode": {
"enter": {
"text": {"signal": "datum['name']"},
"fill": {"scale": "color", "field": "depth"},
"fontSize": {"signal": "0.055*width"},
"align": {"value": "center"},
"x": {"signal": "width/2"},
"y": {"signal": "height/4"},
"opacity": {"value": 0}
},
"update": {
"opacity": {
"signal": "!showDetails ? 0 : focus['id'] === root['id'] ? tEase : tEaseDelayed"
}
}
}
},
{
"name": "details",
"description": "Дополнительные сведения об узле, появляющиеся при увеличении.",
"type": "text",
"from": {"data": "details_data"},
"interactive": false,
"encode": {
"enter": {
"text": {"signal": "datum['details']"},
"fontSize": {"signal": "0.045*width"},
"align": {"value": "center"},
"x": {"signal": "width/2"},
"y": {"signal": "height/3"},
"fill": {"value": "gray"},
"opacity": {"value": 0}
},
"update": {
"opacity": {
"signal": "!showDetails ? 0 : focus['id'] === root['id'] ? tEase : tEaseDelayed"
}
}
}
},
{
"name": "helper_text",
"interactive": false,
"description": "Подсказка по управлению в левом нижнем углу.",
"type": "text",
"encode": {
"enter": {
"fontSize": {"value": 14},
"text": {
"signal": "['Управление:', '• нажмите на узел, чтобы приблизить его','• для узла с потомками нажмите Shift и щёлкните, чтобы увидеть подробности', '• чтобы замедлить анимацию, нажмите ⌘ и щёлкните (Mac) или Ctrl и щёлкните (Windows)']"
},
"y": {"signal": "height+5"}
},
"update": {
"opacity": {
"signal": "ceil(k) === 1 ? isValid(t) ? tEaseDelayed : 1 : 0"
}
}
}
}
]
}