Иерархические структуры данных в визуализации формируются через
преобразование вложенных объектов в набор узлов и связей, где каждый
элемент графа имеет родителя и набор потомков. В контексте D3.js
ключевым механизмом работы с такими структурами является модуль
d3-hierarchy, который предоставляет инструменты для
построения деревьев, кластеров и производных компоновок.
Деревья в D3.js строятся на основе исходного JSON-объекта, где вложенность определяет структуру связей:
const data = {
name: "root",
children: [
{
name: "A",
children: [
{ name: "A1" },
{ name: "A2" }
]
},
{
name: "B",
children: [
{ name: "B1" },
{ name: "B2" }
]
}
]
};
Для преобразования этого объекта в иерархическую модель используется
d3.hierarchy:
const root = d3.hierarchy(data);
Каждый узел получает дополнительные поля:
depth — уровень вложенностиheight — расстояние до самого глубокого потомкаparent — ссылка на родителяchildren — массив дочерних узловПосле этого структура может быть передана в компоновщик
tree или cluster.
Горизонтальное дерево строится с использованием стандартного layout
d3.tree(), где координаты узлов рассчитываются в декартовой
системе, а затем оси интерпретируются как «глубина» и «позиция по
вертикали».
const width = 800;
const height = 400;
const treeLayout = d3.tree()
.size([height, width - 160]);
Важно, что в горизонтальной ориентации обычно меняются местами оси X и Y для достижения читаемой структуры слева направо.
treeLayout(root);
После выполнения treeLayout каждый узел получает
координаты:
d.x — вертикальная позицияd.y — горизонтальная позиция (глубина)При отрисовке горизонтального дерева часто применяется трансформация:
const node = svg.selectAll("g.node")
.data(root.descendants())
.enter()
.append("g")
.attr("transform", d => `translate(${d.y},${d.x})`);
Такое преобразование делает дерево направленным слева направо.
Для линий между узлами используется генератор
d3.linkHorizontal():
const link = d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x);
svg.selectAll("path.link")
.data(root.links())
.enter()
.append("path")
.attr("d", link);
Функция root.links() возвращает массив связей вида
{source, target}, что упрощает построение рёбер графа.
Радиальное дерево представляет иерархию в полярных координатах, где расстояние от центра соответствует глубине, а угол — позиции в поддереве.
const radius = Math.min(width, height) / 2;
const treeLayout = d3.tree()
.size([2 * Math.PI, radius]);
Здесь:
После расчета layout узлы имеют:
d.x — угол в радианахd.y — радиальное расстояниеДля перевода в декартову систему используется преобразование:
function radialPoint(x, y) {
return [
y * Math.cos(x - Math.PI / 2),
y * Math.sin(x - Math.PI / 2)
];
}
Смещение -Math.PI / 2 поворачивает дерево так, чтобы
корень находился сверху.
const node = svg.selectAll("g.node")
.data(root.descendants())
.enter()
.append("g")
.attr("transform", d => {
const [x, y] = radialPoint(d.x, d.y);
return `translate(${x},${y})`;
});
Для соединений используется d3.linkRadial():
const link = d3.linkRadial()
.angle(d => d.x)
.radius(d => d.y);
svg.selectAll("path.link")
.data(root.links())
.enter()
.append("path")
.attr("d", link);
Этот генератор автоматически строит дуги в полярной системе координат.
Горизонтальная компоновка основана на прямоугольной сетке координат, где структура читается как древовидный список с уровнями. Радиальная компоновка распределяет узлы по окружности, обеспечивая компактное размещение больших деревьев.
Ключевые различия:
Горизонтальная модель:
Радиальная модель:
При увеличении глубины дерева возникает необходимость контролировать расстояния между уровнями.
treeLayout.nodeSize([20, 100]);
Для радиальных деревьев аналогично:
treeLayout.separation((a, b) => (a.parent === b.parent ? 1 : 2));
Функция separation регулирует плотность расположения
соседних узлов.
Узлы обычно представлены окружностями и текстовыми метками:
node.append("circle")
.attr("r", 4)
.attr("fill", "#555");
node.append("text")
.attr("dy", 3)
.attr("x", d => d.children ? -8 : 8)
.style("text-anchor", d => d.children ? "end" : "start")
.text(d => d.data.name);
В радиальной версии ориентация текста требует дополнительного поворота:
.attr("transform", d => {
const angle = (d.x * 180 / Math.PI) - 90;
return `rotate(${angle})`;
});
Для улучшения визуального восприятия применяются кривые Безье. В горизонтальной компоновке:
d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x);
Для радиальной:
d3.linkRadial()
.angle(d => d.x)
.radius(d => d.y);
В обоих случаях формируются плавные траектории, минимизирующие пересечения.
Деревья часто дополняются механизмом сворачивания поддеревьев. Узлы
могут переключать состояние children и
"_children":
function toggle(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
}
После изменения структуры вызывается повторный пересчёт layout:
treeLayout(root);
Для радиальных деревьев критично корректное центрирование:
svg.attr("transform", `translate(${width / 2},${height / 2})`);
В горизонтальных деревьях часто применяется сдвиг для корневого узла:
svg.attr("transform", "translate(80,0)");
При работе с большим количеством узлов возникают ограничения производительности SVG. Основные подходы:
Layout D3 пересчитывается полностью при каждом изменении структуры,
поэтому важна минимизация операций над root.
Поведение масштабирования реализуется через d3.zoom:
svg.call(
d3.zoom()
.scaleExtent([0.5, 5])
.on("zoom", (event) => {
g.attr("transform", event.transform);
})
);
Это позволяет сохранять читаемость как горизонтальных, так и радиальных деревьев при увеличении сложности структуры.
Радиальная компоновка основана на полярной системе координат:
Такое представление минимизирует пересечения линий на больших деревьях, но усложняет восприятие линейной иерархии.
Для управления угловым распределением используется порядок обхода дерева (обычно DFS), который влияет на расположение поддеревьев по окружности.
d3.tree():
d3.cluster():
Оба алгоритма используют один и тот же механизм
hierarchy, различаясь стратегией распределения
координат.
Горизонтальное дерево опирается на прямоугольную систему координат, где каждая ось имеет семантический смысл: глубина и порядок.
Радиальное дерево переводит ту же иерархию в угловую метрику, что приводит к равномерному распределению элементов по окружности и снижению визуальной перегрузки при росте ширины дерева.
Такое преобразование не изменяет структуру данных, но полностью меняет восприятие и способы анализа иерархии.