Sunburst-диаграмма через d3.partition()

Иерархические данные в визуализации часто требуют компактного, но выразительного представления структуры вложенности. Для этого используется радиальная форма диаграмм, где каждый уровень иерархии раскладывается по концентрическим окружностям, а сегменты распределяются по углу пропорционально значению. Такой подход реализуется через разбиение пространства с помощью алгоритма partition, который формирует координаты узлов дерева в полярной системе.

Работа начинается с подготовки структуры данных, пригодной для обработки в иерархическом формате. D3 ожидает вложенный объект, где каждый узел может содержать дочерние элементы и числовое значение на листьях.

const data = {
  name: "root",
  children: [
    {
      name: "A",
      children: [
        { name: "A1", value: 100 },
        { name: "A2", value: 300 }
      ]
    },
    {
      name: "B",
      children: [
        { name: "B1", value: 200 },
        { name: "B2", value: 150 }
      ]
    }
  ]
};

Для дальнейшей обработки структура преобразуется в иерархический формат:

const root = d3.hierarchy(data)
  .sum(d => d.value)
  .sort((a, b) => b.value - a.value);

Метод sum агрегирует значения снизу вверх, формируя вес внутренних узлов. Сортировка обеспечивает стабильное расположение элементов.

Алгоритм partition и его роль

Функция d3.partition() преобразует иерархию в набор прямоугольных координат, которые затем интерпретируются в полярной системе.

const partition = d3.partition()
  .size([2 * Math.PI, radius]);

Параметр size задаёт область распределения:

  • первый элемент — угол в радианах (полный круг 2π),
  • второй — радиальная глубина.

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

  • x0, x1 — границы углового сегмента,
  • y0, y1 — радиальные границы.
root = partition(root);

Эта операция полностью подготавливает данные для построения sunburst-диаграммы.

Преобразование координат в радиальную систему

Sunburst-диаграмма опирается на преобразование декартовых координат partition в полярные.

Основные параметры:

  • угол (θ) — определяется значениями x0 и x1,
  • радиус (r) — определяется y0 и y1.

Для отрисовки используется генератор дуг:

const arc = d3.arc()
  .startAngle(d => d.x0)
  .endAngle(d => d.x1)
  .innerRadius(d => d.y0)
  .outerRadius(d => d.y1);

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

Создание SVG-контейнера

Визуализация размещается в SVG-элементе с центровкой координатной системы.

const width = 800;
const radius = width / 2;

const svg = d3.select("body")
  .append("svg")
  .attr("width", width)
  .attr("height", width)
  .append("g")
  .attr("transform", `translate(${radius},${radius})`);

Перенос центра координат позволяет строить диаграмму от центра наружу.

Привязка данных и отрисовка сегментов

После подготовки иерархии выполняется привязка узлов к SVG-путям.

svg.selectAll("path")
  .data(root.descendants())
  .enter()
  .append("path")
  .attr("d", arc)
  .style("fill", d => color((d.children ? d : d.parent).data.name))
  .style("stroke", "#fff");

Ключевые моменты:

  • descendants() возвращает все узлы дерева,
  • цвет часто определяется по верхнему родителю,
  • белые границы отделяют сегменты визуально.

Цветовая шкала и группировка

Для различения ветвей используется категориальная шкала.

const color = d3.scaleOrdinal()
  .domain(root.children.map(d => d.data.name))
  .range(d3.schemeCategory10);

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

Масштабирование глубины

Радиальная ось определяется значениями глубины. Для управления плотностью используется масштабирование:

const radiusScale = d3.scaleLinear()
  .domain([0, root.height])
  .range([0, radius]);

Хотя partition уже распределяет узлы, дополнительное масштабирование позволяет адаптировать диаграмму под разные размеры данных.

Подписи сегментов

Добавление текста требует вычисления середины дуги и радиуса.

svg.selectAll("text")
  .data(root.descendants().filter(d => d.depth))
  .enter()
  .append("text")
  .attr("transform", d => {
    const angle = (d.x0 + d.x1) / 2;
    const r = (d.y0 + d.y1) / 2;
    return `
      rotate(${angle * 180 / Math.PI - 90})
      translate(${r},0)
    `;
  })
  .text(d => d.data.name)
  .style("font-size", "10px");

Преобразование включает:

  • перевод радиан в градусы,
  • поворот относительно центра,
  • перенос вдоль радиуса.

Управление читаемостью текста

В плотных диаграммах подписи часто перекрываются. Применяются фильтры:

.filter(d => d.x1 - d.x0 > 0.03)

или условие по глубине:

.filter(d => d.depth < 4)

Это ограничивает отображение мелких сегментов.

Интерактивность и события

Sunburst-диаграммы часто используются в аналитике, поэтому важна интерактивность.

svg.selectAll("path")
  .on("mouseover", function(event, d) {
    d3.select(this)
      .style("opacity", 0.7);
  })
  .on("mouseout", function(event, d) {
    d3.select(this)
      .style("opacity", 1);
  });

Дополнительно можно выделять ветвь целиком:

.on("click", (event, d) => {
  svg.selectAll("path")
    .style("opacity", node => node.ancestors().includes(d) ? 1 : 0.2);
});

Анимация переходов

Изменение данных или состояния диаграммы сопровождается плавными переходами.

svg.selectAll("path")
  .transition()
  .duration(750)
  .attrTween("d", d => {
    const interpolate = d3.interpolate(d.current, d);
    return t => arc(interpolate(t));
  });

Для корректной анимации требуется сохранение текущего состояния узлов.

Работа с обновлением данных

При динамических изменениях структура пересчитывается:

function update(newData) {
  const newRoot = d3.hierarchy(newData)
    .sum(d => d.value);

  const newPartition = d3.partition().size([2 * Math.PI, radius]);
  newPartition(newRoot);

  svg.selectAll("path")
    .data(newRoot.descendants())
    .join("path")
    .transition()
    .attr("d", arc);
}

Метод join упрощает синхронизацию DOM и данных.

Оптимизация больших иерархий

При больших объёмах данных возникают проблемы производительности:

  • количество узлов растёт экспоненциально,
  • DOM-элементы перегружают браузер,
  • пересчёт layout становится дорогим.

Используются подходы:

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

Особенности интерпретации partition в sunburst

Алгоритм partition сам по себе не создаёт радиальную структуру. Он формирует прямоугольные координаты, которые затем интерпретируются через arc-генератор. Это разделение логики позволяет:

  • менять визуализацию без изменения layout,
  • переиспользовать иерархию для treemap и sunburst,
  • управлять геометрией на этапе рендеринга.

Сопоставление уровней и радиусов

Глубина узла напрямую влияет на радиус:

  • depth = 0 — центр,
  • увеличение depth — движение к внешним кольцам.

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

Геометрическая интерпретация сегментов

Каждый сегмент представляет собой кольцевой сектор, ограниченный:

  • двумя радиусами,
  • двумя углами.

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

Использование в аналитических системах

Sunburst-структуры применяются для:

  • анализа файловых систем,
  • финансовой иерархии,
  • категорий товаров,
  • телеметрических данных.

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

Взаимодействие с масштабами и фильтрацией

D3 позволяет комбинировать partition с масштабами:

const x = d3.scaleLinear().range([0, 2 * Math.PI]);
const y = d3.scaleSqrt().range([0, radius]);

Такая комбинация изменяет восприятие пропорций, усиливая или сглаживая различия между сегментами.

Связь с другими иерархическими диаграммами

Sunburst-диаграмма является радиальным аналогом treemap. Обе используют d3.hierarchy, но различаются способом отображения:

  • treemap — прямоугольная упаковка,
  • sunburst — круговая сегментация.

Partition выступает общей основой для обоих подходов, обеспечивая единый механизм разбиения иерархии.