Force-directed графы в Vega представляют собой визуализацию сетевых структур, в которых расположение узлов определяется моделью физических сил. Такой подход позволяет естественным образом отображать сложные взаимосвязи: узлы отталкиваются друг от друга, рёбра действуют как пружины, а вся система стремится к состоянию минимальной энергии.
Основой служит система имитации физических взаимодействий:
Формально положение узла обновляется итеративно:
В Vega используется адаптация алгоритма D3-force, интегрированная в декларативную модель визуализации.
Force-directed граф в Vega строится на двух массивах:
Пример структуры данных:
{
"nodes": [
{ "id": "A", "group": 1 },
{ "id": "B", "group": 1 },
{ "id": "C", "group": 2 }
],
"links": [
{ "source": "A", "target": "B" },
{ "source": "A", "target": "C" }
]
}
Ключевой момент заключается в том, что Vega требует согласованности
идентификаторов: source и target должны
ссылаться на id узлов.
Force-directed граф реализуется через комбинацию:
data)transform)signals)marks)forces через transforms или
signal-driven simulation)Минимальная конфигурация:
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"width": 600,
"height": 400,
"data": [
{
"name": "nodes",
"values": [
{ "id": "A" },
{ "id": "B" },
{ "id": "C" }
]
},
{
"name": "links",
"values": [
{ "source": "A", "target": "B" },
{ "source": "A", "target": "C" }
]
}
]
}
Далее добавляется force-система через трансформации и сигналы.
Force-directed layout в Vega реализуется через набор сил:
Каждый узел получает заряд, создающий взаимное отталкивание:
Конфигурационно:
"forces": [
{
"force": "charge",
"strength": -120
}
]
Отрицательное значение усиливает отталкивание.
Рёбра моделируются как пружины, стремящиеся сохранить расстояние между узлами:
{
"force": "link",
"links": "links",
"id": "id",
"distance": 50,
"strength": 1
}
Параметры:
distance — целевая длина ребраstrength — жёсткость связиid — ключ сопоставления узловСистема удерживается в пределах области визуализации:
{
"force": "center",
"x": 300,
"y": 200
}
Без этой силы граф может “уплывать” за границы холста.
Используется для предотвращения перекрытия узлов:
{
"force": "collide",
"radius": 10
}
Эта сила особенно важна при плотных графах.
В Vega данные обновляются реактивно через трансформации:
"transform": [
{
"type": "force",
"iterations": 300,
"static": false,
"forces": [
{
"force": "charge",
"strength": -80
},
{
"force": "link",
"links": "links",
"id": "id",
"distance": 60
},
{
"force": "center",
"x": 300,
"y": 200
}
]
}
]
Параметр iterations определяет количество шагов
симуляции при инициализации.
Узлы обычно отображаются как символы (symbol mark):
"marks": [
{
"type": "symbol",
"from": { "data": "nodes" },
"encode": {
"enter": {
"size": { "value": 100 },
"fill": { "value": "#4c78a8" }
},
"update": {
"x": { "field": "x" },
"y": { "field": "y" }
}
}
}
]
Ключевой момент: координаты x и y
автоматически вычисляются force-системой.
Рёбра отображаются через линии:
{
"type": "line",
"from": { "data": "links" },
"encode": {
"update": {
"x": { "field": "source.x" },
"y": { "field": "source.y" },
"x2": { "field": "target.x" },
"y2": { "field": "target.y" },
"stroke": { "value": "#999" }
}
}
}
Связь координат узлов и рёбер критична: Vega автоматически связывает объекты через ссылки.
Signals позволяют управлять параметрами симуляции в реальном времени.
Добавляется сигнал drag:
"signals": [
{
"name": "dragged",
"value": null,
"on": [
{
"events": "@node:mousedown",
"update": "datum"
},
{
"events": "mouseup",
"update": "null"
}
]
}
]
При закреплении узла его координаты фиксируются:
"fx": { "signal": "dragged.id ? dragged.x : null" },
"fy": { "signal": "dragged.id ? dragged.y : null" }
Для больших графов добавляется zoom:
{
"name": "view",
"value": { "x": 0, "y": 0, "scale": 1 },
"on": [
{
"events": "wheel",
"update": "clamp(scale * pow(1.001, event.deltaY), 0.5, 5)"
}
]
}
Marks затем трансформируются через x, y,
scale.
Некоторые узлы необходимо закрепить:
В Vega это достигается через fx и fy:
{
"type": "symbol",
"encode": {
"update": {
"fx": { "signal": "datum.fixed ? datum.x : null" },
"fy": { "signal": "datum.fixed ? datum.y : null" }
}
}
}
Стабильность графа зависит от:
Пример настройки:
{
"force": "charge",
"strength": -60,
"theta": 0.9
}
Параметр theta влияет на аппроксимацию расчёта дальних
сил.
Force-directed графы чувствительны к числу узлов.
Основные оптимизации:
Слишком большое значение iterations приводит к:
Сильное отталкивание между всеми парами узлов даёт O(n²). Vega использует оптимизации, но при больших графах нагрузка остаётся значительной.
Группировка узлов до симуляции уменьшает сложность:
Обновление координат только при изменениях данных или взаимодействии пользователя.
При слишком слабых link-силах граф теряет форму и превращается в облако.
При сильной центровке узлы слипаются в середине.
При отсутствии collision force узлы перекрывают друг друга, делая визуализацию нечитаемой.
При несбалансированных силах граф не достигает устойчивого состояния и постоянно “дрожит”.
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"width": 600,
"height": 400,
"data": [
{
"name": "nodes",
"values": [
{ "id": "A" },
{ "id": "B" },
{ "id": "C" },
{ "id": "D" }
]
},
{
"name": "links",
"values": [
{ "source": "A", "target": "B" },
{ "source": "A", "target": "C" },
{ "source": "B", "target": "D" }
]
}
],
"transform": [
{
"type": "force",
"iterations": 200,
"static": false,
"forces": [
{
"force": "charge",
"strength": -90
},
{
"force": "link",
"links": "links",
"id": "id",
"distance": 70
},
{
"force": "center",
"x": 300,
"y": 200
},
{
"force": "collide",
"radius": 12
}
]
}
],
"marks": [
{
"type": "line",
"from": { "data": "links" },
"encode": {
"update": {
"x": { "field": "source.x" },
"y": { "field": "source.y" },
"x2": { "field": "target.x" },
"y2": { "field": "target.y" },
"stroke": { "value": "#bbb" }
}
}
},
{
"type": "symbol",
"from": { "data": "nodes" },
"encode": {
"update": {
"x": { "field": "x" },
"y": { "field": "y" },
"size": { "value": 120 },
"fill": { "value": "#4c78a8" }
}
}
}
]
}