Упаковка кругами: d3.pack()

Алгоритм d3.pack() реализует плотную упаковку вложенных элементов в виде набора окружностей, где каждый узел иерархии представлен кругом, а дочерние элементы располагаются внутри родительского с минимальными пересечениями и максимально эффективным использованием пространства. Такой подход используется для визуализации древовидных структур, где важны не только связи, но и относительные размеры элементов.

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


Формирование иерархии данных

Перед применением упаковки требуется корректная структура данных. Обычно используется объект с вложенными children, который преобразуется в иерархию с помощью d3.hierarchy().

const data = {
  name: "root",
  children: [
    { name: "A", value: 100 },
    {
      name: "B",
      children: [
        { name: "B1", value: 40 },
        { name: "B2", value: 60 }
      ]
    }
  ]
};

const root = d3.hierarchy(data)
  .sum(d => d.value);

Метод .sum() выполняет агрегацию значений снизу вверх, определяя вес каждого узла.


Создание layout через d3.pack()

После подготовки иерархии применяется layout:

const pack = d3.pack()
  .size([500, 500])
  .padding(5);

Параметр size задаёт область размещения, а padding определяет расстояние между окружностями.

Применение layout:

pack(root);

После выполнения у каждого узла появляются координаты x, y и радиус r.


Структура вычисленных данных

После вызова алгоритма каждый узел иерархии получает расширенные свойства:

  • x — координата центра по горизонтали
  • y — координата центра по вертикали
  • r — радиус окружности
  • depth — уровень вложенности
  • parent — ссылка на родительский узел
  • children — массив дочерних элементов

Листовые узлы получают радиус, пропорциональный их значению, а внутренние узлы — агрегированный радиус, достаточный для размещения всех потомков.


Рендеринг с использованием SVG

Для визуализации чаще всего используется SVG.

const svg = d3.select("svg")
  .attr("width", 500)
  .attr("height", 500);

const nodes = root.descendants();

svg.selectAll("circle")
  .data(nodes)
  .enter()
  .append("circle")
  .attr("cx", d => d.x)
  .attr("cy", d => d.y)
  .attr("r", d => d.r)
  .attr("fill", d => d.children ? "#ccc" : "#69b3a2")
  .attr("stroke", "#333");

Внутренние узлы обычно визуально отделяются от листьев через цветовую дифференциацию.


Листовые узлы и фильтрация

Часто требуется отображать только конечные элементы или, наоборот, только агрегаты:

const leafNodes = root.leaves();

svg.selectAll("circle")
  .data(leafNodes)
  .enter()
  .append("circle")
  .attr("cx", d => d.x)
  .attr("cy", d => d.y)
  .attr("r", d => d.r);

Метод leaves() возвращает только узлы без потомков, что удобно для аналитических визуализаций.


Иерархическая структура упаковки

d3.pack() использует рекурсивный алгоритм, где каждый уровень обрабатывается отдельно. Сначала вычисляются размеры листьев, затем они группируются в минимальные окружности, после чего группы снова упаковываются на более высоком уровне.

Основные свойства алгоритма:

  • сохранение иерархии
  • минимизация пустого пространства
  • отсутствие пересечений окружностей
  • адаптивное масштабирование

Масштабирование и центрирование

Для корректного отображения часто требуется центрировать визуализацию:

svg.selectAll("circle")
  .attr("transform", `translate(${0}, ${0})`);

Либо используется смещение относительно контейнера:

const g = svg.append("g")
  .attr("transform", "translate(50,50)");

Использование scale для адаптации

При необходимости масштабирования под экран используется d3.scaleLinear() или d3.scaleSqrt().

const scale = d3.scaleSqrt()
  .domain([0, d3.max(root.leaves(), d => d.value)])
  .range([5, 50]);

svg.selectAll("circle")
  .attr("r", d => scale(d.value));

Хотя d3.pack() уже вычисляет радиусы, масштабирование полезно при дополнительной стилизации или комбинированных визуализациях.


Сравнение с treemap

В отличие от d3.treemap(), где элементы представлены прямоугольниками, упаковка кругами обладает иной визуальной логикой:

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

Круговая упаковка особенно эффективна для отображения вложенных категорий и распределений.


Поддержка интерактивности

Интерактивные элементы добавляются через обработчики событий:

svg.selectAll("circle")
  .on("mouseover", function(event, d) {
    d3.select(this).attr("stroke-width", 3);
  })
  .on("mouseout", function(event, d) {
    d3.select(this).attr("stroke-width", 1);
  });

Часто используется зумирование через d3.zoom():

const zoom = d3.zoom()
  .scaleExtent([1, 10])
  .on("zoom", (event) => {
    g.attr("transform", event.transform);
  });

svg.call(zoom);

Иерархические ограничения и плотность

Плотность упаковки зависит от параметра padding и структуры данных. При большом количестве узлов на одном уровне возможны следующие эффекты:

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

Оптимизация достигается через предварительную группировку данных или ограничение глубины иерархии.


Работа с подписями

Добавление текста требует учёта радиуса окружности:

svg.selectAll("text")
  .data(nodes)
  .enter()
  .append("text")
  .attr("x", d => d.x)
  .attr("y", d => d.y)
  .attr("text-anchor", "middle")
  .text(d => d.data.name)
  .style("font-size", d => Math.min(d.r / 3, 12));

Для мелких кругов подписи обычно скрываются:

.style("display", d => d.r < 10 ? "none" : "block");

Производительность вычислений

Алгоритм упаковки имеет сложность, зависящую от количества узлов и глубины дерева. На больших наборах данных наблюдается рост времени вычисления.

Практические приёмы оптимизации:

  • предварительная агрегация
  • ограничение глубины hierarchy
  • кеширование результатов layout
  • использование трансформаций вместо пересчёта координат

Комбинированные визуализации

d3.pack() часто используется как часть гибридных схем визуализации:

  • упаковка внутри partition-структур
  • комбинирование с forceSimulation
  • вложение в интерактивные дашборды
  • переходы между treemap и circle packing

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


Геометрическая интерпретация результата

Каждый уровень иерархии формирует набор касающихся окружностей, которые в совокупности образуют более крупную окружность родителя. Это приближённая модель плотной упаковки в двумерном пространстве, где оптимизация достигается итеративным сглаживанием радиусов и позиций.

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