Sankey-диаграммы через плагин d3-sankey

Sankey-диаграммы в D3.js реализуются через отдельный модуль d3-sankey, предназначенный для построения потоковых графов, где ширина связей пропорциональна величине потока между узлами. Основная область применения — визуализация распределений энергии, денег, трафика, ресурсов и любых систем с направленными взвешенными переходами.


В основе лежит граф, состоящий из двух сущностей:

  • nodes — вершины системы (источники, промежуточные узлы, стоки)
  • links — направленные связи между узлами с числовым весом

Базовая структура данных:

const data = {
  nodes: [
    { name: "Источник A" },
    { name: "Источник B" },
    { name: "Промежуточный" },
    { name: "Приёмник" }
  ],
  links: [
    { source: 0, target: 2, value: 10 },
    { source: 1, target: 2, value: 5 },
    { source: 2, target: 3, value: 15 }
  ]
};

Связи могут ссылаться на узлы как по индексу, так и по объекту, если данные предварительно нормализованы.


Подключение d3-sankey

Модуль подключается отдельно от ядра D3:

import * as d3 from "d3";
import { sankey, sankeyLinkHorizontal } from "d3-sankey";

Ключевые сущности:

  • sankey() — генератор раскладки
  • sankeyLinkHorizontal() — генератор пути для связей

Алгоритм раскладки

Sankey-алгоритм решает задачу минимизации пересечений и распределения узлов по уровням:

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

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

  • nodeWidth — ширина узла
  • nodePadding — расстояние между узлами
  • extent — область компоновки
  • nodeAlign — выравнивание (left, right, justify, center)

Базовая инициализация схемы

const width = 900;
const height = 500;

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

const sankeyGenerator = sankey()
  .nodeWidth(20)
  .nodePadding(15)
  .extent([[1, 1], [width - 1, height - 6]]);

Преобразование данных

Перед отрисовкой данные проходят через генератор:

const graph = sankeyGenerator(data);

После этого:

  • graph.nodes содержит рассчитанные координаты x0, x1, y0, y1
  • graph.links содержит точки y0, y1, используемые для построения кривых

Отрисовка узлов

svg.append("g")
  .selectAll("rect")
  .data(graph.nodes)
  .enter()
  .append("rect")
  .attr("x", d => d.x0)
  .attr("y", d => d.y0)
  .attr("height", d => d.y1 - d.y0)
  .attr("width", d => d.x1 - d.x0)
  .style("fill", "#4e79a7");

Высота узла отражает суммарный поток, проходящий через него.


Отрисовка связей

Связи визуализируются как плавные кривые Безье:

svg.append("g")
  .attr("fill", "none")
  .selectAll("path")
  .data(graph.links)
  .enter()
  .append("path")
  .attr("d", sankeyLinkHorizontal())
  .attr("stroke", "#888")
  .attr("stroke-width", d => Math.max(1, d.width))
  .attr("opacity", 0.5);

Ширина линии напрямую связана с value.


Цветовое кодирование потоков

Цвета часто привязываются к источнику:

const color = d3.scaleOrdinal(d3.schemeCategory10);

svg.selectAll("path")
  .style("stroke", d => color(d.source.name));

Альтернативный подход — градиенты от источника к цели:

const defs = svg.append("defs");

defs.selectAll("linearGradient")
  .data(graph.links)
  .enter()
  .append("linearGradient")
  .attr("id", (d, i) => "grad" + i)
  .html(d => `
    <stop offset="0%" stop-color="${color(d.source.name)}"/>
    <stop offset="100%" stop-color="${color(d.target.name)}"/>
  `);

Подписи узлов

svg.append("g")
  .selectAll("text")
  .data(graph.nodes)
  .enter()
  .append("text")
  .attr("x", d => d.x0 - 6)
  .attr("y", d => (d.y1 + d.y0) / 2)
  .attr("dy", "0.35em")
  .attr("text-anchor", "end")
  .text(d => d.name);

Для узлов справа используется противоположное выравнивание:

.filter(d => d.x0 < width / 2)
  .attr("text-anchor", "start")
  .attr("x", d => d.x1 + 6);

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

Hover-подсветка потоков

link.on("mouseover", function() {
  d3.select(this)
    .attr("opacity", 0.9);
});

Подсветка связанных узлов

function highlight(node) {
  svg.selectAll("rect")
    .attr("opacity", d => d === node ? 1 : 0.3);

  svg.selectAll("path")
    .attr("opacity", d =>
      d.source === node || d.target === node ? 0.8 : 0.1
    );
}

Обновление данных

Sankey-диаграммы чувствительны к пересчёту layout:

function update(newData) {
  const graph = sankeyGenerator(newData);

  svg.selectAll("rect")
    .data(graph.nodes)
    .join("rect")
    .attr("x", d => d.x0)
    .attr("y", d => d.y0)
    .attr("height", d => d.y1 - d.y0)
    .attr("width", d => d.x1 - d.x0);

  svg.selectAll("path")
    .data(graph.links)
    .join("path")
    .attr("d", sankeyLinkHorizontal())
    .attr("stroke-width", d => d.width);
}

Выравнивание узлов

Параметр nodeAlign влияет на структуру графа:

  • sankeyLeft — источники слева
  • sankeyRight — стоки справа
  • sankeyCenter — центрирование
  • sankeyJustify — равномерное распределение
sankey()
  .nodeAlign(d3.sankeyJustify);

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

nodePadding

Регулирует вертикальные промежутки:

.nodePadding(30)

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


Обработка сложных графов

Циклические зависимости

Sankey-алгоритм рассчитан на ацикличные графы. При наличии циклов:

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

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

При больших данных узким местом становится итеративная оптимизация позиций:

  • число узлов > 500 резко увеличивает время расчёта
  • число связей > 2000 требует упрощения визуализации

Оптимизации:

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

Формирование читаемой структуры

Sankey-диаграммы требуют строгой логики уровней:

  • источники слева
  • промежуточные узлы в центре
  • стоки справа

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


Расширенные техники стилизации

Скругление потоков

.attr("d", sankeyLinkHorizontal())

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


Прозрачность по значению

.attr("opacity", d => Math.max(0.2, d.value / 100))

Анимация потоков

path.transition()
  .duration(800)
  .attr("stroke-width", d => d.width);

Типичные ошибки построения

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

Модель восприятия данных

Sankey-диаграмма формирует восприятие через три основных измерения:

  • ширина потока как количественная характеристика
  • положение узлов как структурная иерархия
  • направление как причинно-следственная связь

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