Горизонтальные и радиальные деревья

Иерархические структуры данных в визуализации формируются через преобразование вложенных объектов в набор узлов и связей, где каждый элемент графа имеет родителя и набор потомков. В контексте 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(), где координаты узлов рассчитываются в декартовой системе, а затем оси интерпретируются как «глубина» и «позиция по вертикали».

Базовая инициализация layout

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]);

Здесь:

  • первый параметр — угол (θ)
  • второй параметр — радиус (r)

Преобразование полярных координат

После расчета 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), который влияет на расположение поддеревьев по окружности.

Сравнение стратегий layout

d3.tree():

  • фиксированная структура уровней
  • одинаковая глубина узлов на одной оси
  • подходит для организационных структур

d3.cluster():

  • более компактное расположение
  • одинаковые листья выравниваются по одному радиусу
  • часто используется в радиальных схемах

Оба алгоритма используют один и тот же механизм hierarchy, различаясь стратегией распределения координат.

Связь координатных систем с визуальным результатом

Горизонтальное дерево опирается на прямоугольную систему координат, где каждая ось имеет семантический смысл: глубина и порядок.

Радиальное дерево переводит ту же иерархию в угловую метрику, что приводит к равномерному распределению элементов по окружности и снижению визуальной перегрузки при росте ширины дерева.

Такое преобразование не изменяет структуру данных, но полностью меняет восприятие и способы анализа иерархии.