Sankey-диаграммы в D3.js реализуются через отдельный модуль d3-sankey, предназначенный для построения потоковых графов, где ширина связей пропорциональна величине потока между узлами. Основная область применения — визуализация распределений энергии, денег, трафика, ресурсов и любых систем с направленными взвешенными переходами.
В основе лежит граф, состоящий из двух сущностей:
Базовая структура данных:
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:
import * as d3 from "d3";
import { sankey, sankeyLinkHorizontal } from "d3-sankey";
Ключевые сущности:
sankey() — генератор раскладкиsankeyLinkHorizontal() — генератор пути для связейSankey-алгоритм решает задачу минимизации пересечений и распределения узлов по уровням:
Основные параметры алгоритма:
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, y1graph.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);
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(30)
Большие значения уменьшают визуальную перегруженность, но увеличивают высоту диаграммы.
Sankey-алгоритм рассчитан на ацикличные графы. При наличии циклов:
При больших данных узким местом становится итеративная оптимизация позиций:
Оптимизации:
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-диаграмма формирует восприятие через три основных измерения:
Такая модель делает её особенно эффективной для анализа систем распределения и преобразования ресурсов, где важен переход между состояниями, а не только статические значения.