Алгоритм 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:
const pack = d3.pack()
.size([500, 500])
.padding(5);
Параметр size задаёт область размещения, а
padding определяет расстояние между окружностями.
Применение layout:
pack(root);
После выполнения у каждого узла появляются координаты x,
y и радиус r.
После вызова алгоритма каждый узел иерархии получает расширенные свойства:
x — координата центра по горизонталиy — координата центра по вертикалиr — радиус окружностиdepth — уровень вложенностиparent — ссылка на родительский узелchildren — массив дочерних элементовЛистовые узлы получают радиус, пропорциональный их значению, а внутренние узлы — агрегированный радиус, достаточный для размещения всех потомков.
Для визуализации чаще всего используется 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)");
При необходимости масштабирования под экран используется
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() уже вычисляет радиусы, масштабирование
полезно при дополнительной стилизации или комбинированных
визуализациях.
В отличие от 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");
Алгоритм упаковки имеет сложность, зависящую от количества узлов и глубины дерева. На больших наборах данных наблюдается рост времени вычисления.
Практические приёмы оптимизации:
hierarchyd3.pack() часто используется как часть гибридных схем
визуализации:
partition-структурforceSimulationТакие подходы позволяют переключать представления без изменения исходных данных, используя общую иерархическую модель.
Каждый уровень иерархии формирует набор касающихся окружностей, которые в совокупности образуют более крупную окружность родителя. Это приближённая модель плотной упаковки в двумерном пространстве, где оптимизация достигается итеративным сглаживанием радиусов и позиций.
Геометрически структура стремится к минимизации пустых областей при сохранении вложенности, что делает результат визуально компактным и стабильным при масштабировании.