Иерархические структуры данных являются базовой моделью для
визуализации отношений «родитель—потомок» в графах, файловых системах,
организационных схемах и многих других предметных областях. В
визуализации таких структур ключевую роль играют алгоритмы компоновки
деревьев, среди которых в D3.js используются два основных подхода:
d3.tree() и d3.cluster().
Обе функции строят иерархические макеты на основе данных,
предварительно преобразованных в формат d3.hierarchy.
Несмотря на схожесть математической модели, поведение и геометрия
результирующих структур существенно различаются.
Перед применением любого иерархического layout данные приводятся к
структуре HierarchyNode. Это ключевой этап, без которого
tree и cluster не могут работать.
Основная форма исходных данных:
const data = {
name: "root",
children: [
{ name: "A" },
{
name: "B",
children: [
{ name: "B1" },
{ name: "B2" }
]
}
]
};
Преобразование:
const root = d3.hierarchy(data);
После этого каждый узел получает дополнительные свойства:
depth — уровень вложенностиheight — высота поддереваparent — ссылка на родителяchildren — массив дочерних узловЭти данные становятся основой для вычисления координат.
И d3.tree(), и d3.cluster() используют один
и тот же принцип: разделение пространства по уровням глубины (depth) и
распределение узлов по оси, перпендикулярной направлению роста
дерева.
Основная идея:
Различие заключается в стратегии распределения ширины.
d3.tree() строит классическое «дерево» с равномерным
распределением узлов по горизонтали (или окружности в радиальной
версии).
const treeLayout = d3.tree()
.size([height, width]);
или для фиксированной глубины:
const treeLayout = d3.tree()
.nodeSize([dx, dy]);
tree() стремится к компактному
размещению, где расстояние между листьями может варьироваться в
зависимости от структуры поддерева.
После вызова:
treeLayout(root);
каждый узел получает координаты:
x — позиция по вертикали (или углу в радиальном
режиме)y — позиция по горизонтали (или радиусу)Пример использования:
root.descendants().forEach(d => {
console.log(d.x, d.y);
});
В классическом вертикальном дереве:
x — вертикальная координатаy — горизонтальная глубинаd3.cluster() является модификацией дерева, в которой
все листья выравниваются по одной линии.
В отличие от tree(), где пространство распределяется с
учётом поддеревьев, cluster():
const clusterLayout = d3.cluster()
.size([height, width]);
или:
const clusterLayout = d3.cluster()
.nodeSize([dx, dy]);
После вычисления:
clusterLayout(root);
структура координат аналогична tree():
x — позиция по основной осиy — глубинаОднако распределение по оси X отличается принципиально: листья образуют равномерную «линейку».
d3.tree() Учитывает структуру поддеревьев, создавая
адаптивную плотность
d3.cluster() Выравнивает листья независимо от
структуры
tree():
cluster():
d3.tree()
d3.cluster()
Обе функции поддерживают одинаковый набор базовых методов настройки.
Определяет область распределения:
d3.tree().size([height, width]);
d3.cluster().size([height, width]);
Используется при необходимости полного заполнения контейнера.
Альтернатива фиксированного шага между узлами:
d3.tree().nodeSize([dx, dy]);
Особенности:
dx — вертикальный шагdy — горизонтальный шагФункция управления расстоянием между узлами:
treeLayout.separation((a, b) => {
return a.parent === b.parent ? 1 : 2;
});
Логика:
В cluster() эта функция особенно важна, так как
структура выравнивания жестче.
Обе модели могут быть развернуты в полярную систему координат.
После расчёта координат:
const radialPoint = (d) => {
const angle = d.x;
const radius = d.y;
return [
radius * Math.cos(angle - Math.PI / 2),
radius * Math.sin(angle - Math.PI / 2)
];
};
Применение:
tree() даёт неравномерное радиальное деревоcluster() формирует аккуратные круговые уровниПосле расчёта layout строятся рёбра:
const links = root.links();
Каждая связь:
{
source: Node,
target: Node
}
Генерация пути:
d3.linkHorizontal()
или для радиального дерева:
d3.linkRadial()
Даже при одинаковых настройках tree() и
cluster() реагируют на форму входных данных по-разному.
A → B → C → D
A
├── B1
├── B2
├── B3
├── B4
tree() сжимает ветви с учётом структурыcluster() выравнивает B1–B4 на одной линииA
├── B
│ └── C
│ └── D
└── E
tree() визуально отражает глубину цепочкиcluster() выравнивает E и D по одной горизонталиВ больших иерархиях выбор между tree() и
cluster() напрямую влияет на читаемость.
При изменении размеров контейнера:
size() требует перерасчёта layoutnodeSize() сохраняет стабильную геометриюВ интерактивных интерфейсах чаще используется nodeSize,
так как:
Алгоритм внутри обеих функций основан на рекурсивном обходе дерева с вычислением:
Различие заключается в постобработке:
tree() корректирует позиции с учётом «веса»
поддеревьевcluster() игнорирует веса и фиксирует шаг между
листьямиОбе функции часто используются совместно с:
d3.scaleLinear — масштабирование координатd3.drag — интерактивное перемещение узловd3.zoom — навигация по деревуd3.transition — анимация перестроения layoutПример анимации:
node.transition()
.duration(500)
.attr("transform", d => `translate(${d.y},${d.x})`);
tree()cluster()cluster()tree()