Матричная диаграмма связей в D3.js строится на основе преобразования графа в квадратную матрицу, где строки и столбцы соответствуют узлам, а значения в ячейках отражают наличие, силу или тип связи. Такой подход особенно эффективен при визуализации плотных графов, где классическое представление через узлы и рёбра становится перегруженным.
Ключевая идея заключается в переходе от списка связей:
к двумерной структуре matrix[i][j], где каждая ячейка
кодирует отношение между узлом i и узлом j.
Исходные данные обычно представляют собой JSON-структуру:
const graph = {
nodes: [
{ id: "A" },
{ id: "B" },
{ id: "C" }
],
links: [
{ source: "A", target: "B", value: 2 },
{ source: "B", target: "C", value: 5 }
]
};
Построение матрицы требует предварительного индексирования узлов:
const indexByName = new Map();
graph.nodes.forEach((node, i) => {
indexByName.set(node.id, i);
});
const matrix = graph.nodes.map(() =>
graph.nodes.map(() => ({ value: 0 }))
);
Далее выполняется заполнение связей:
graph.links.forEach(link => {
const sourceIndex = indexByName.get(link.source);
const targetIndex = indexByName.get(link.target);
matrix[sourceIndex][targetIndex].value = link.value;
matrix[targetIndex][sourceIndex].value = link.value;
});
Так формируется симметричная матрица для неориентированных графов.
Каждая ячейка матрицы представляется прямоугольником
<rect>. Позиционирование определяется индексами:
const cellSize = 20;
const svg = d3.select("svg")
.attr("width", cellSize * graph.nodes.length)
.attr("height", cellSize * graph.nodes.length);
const rows = svg.selectAll("g")
.data(matrix)
.enter()
.append("g")
.attr("transform", (d, i) => `translate(0, ${i * cellSize})`);
Далее добавляются ячейки:
rows.selectAll("rect")
.data(d => d)
.enter()
.append("rect")
.attr("x", (d, i) => i * cellSize)
.attr("width", cellSize)
.attr("height", cellSize);
Для отображения интенсивности связей используется цветовое
кодирование. В D3 применяется scaleSequential или
scaleLinear.
const color = d3.scaleSequential()
.domain([0, d3.max(graph.links, d => d.value)])
.interpolator(d3.interpolateBlues);
Применение цвета к ячейкам:
.attr("fill", d => d.value ? color(d.value) : "#f0f0f0")
Отсутствие связи кодируется нейтральным цветом.
Качество восприятия матрицы напрямую зависит от порядка узлов. Хаотичное расположение создаёт визуальный шум, поэтому применяются стратегии сортировки:
Простейший вариант — сортировка по количеству связей:
const degree = new Map();
graph.nodes.forEach(n => degree.set(n.id, 0));
graph.links.forEach(l => {
degree.set(l.source, degree.get(l.source) + 1);
degree.set(l.target, degree.get(l.target) + 1);
});
graph.nodes.sort((a, b) => degree.get(b.id) - degree.get(a.id));
После изменения порядка необходимо пересобрать матрицу, так как индексы изменяются.
Подписи строк и столбцов добавляются отдельно, так как ячейки не содержат текстовой информации.
const labels = svg.append("g")
.selectAll("text")
.data(graph.nodes)
.enter()
.append("text")
.attr("x", (d, i) => i * cellSize + cellSize / 2)
.attr("y", -5)
.attr("text-anchor", "middle")
.text(d => d.id);
Аналогично создаются вертикальные подписи с поворотом:
.attr("transform", (d, i) =>
`translate(-5, ${i * cellSize + cellSize / 2}) rotate(-90)`
)
Матричная структура особенно удобна для интерактивного подсвечивания строк и столбцов.
rect.on("mouseover", function(event, d, i, nodes) {
const row = d3.select(this.parentNode);
const index = row.datum().index;
svg.selectAll("rect")
.classed("faded", true);
svg.selectAll("g")
.filter((_, r) => r === index)
.selectAll("rect")
.classed("faded", false);
});
Класс faded снижает непрозрачность:
.faded {
opacity: 0.2;
}
Для ориентированных графов матрица перестаёт быть симметричной.
Значение в ячейке (i, j) не обязано совпадать с
(j, i).
matrix[sourceIndex][targetIndex].value = link.value;
Обратная ячейка остаётся пустой или нулевой. Это позволяет визуализировать направление потоков, зависимостей или иерархий.
При больших наборах данных значения связей могут агрегироваться:
matrix[i][j].value += link.value;
matrix[i][j].count += 1;
Затем вычисляется производное значение:
matrix[i][j].value = matrix[i][j].value / matrix[i][j].count;
При росте количества узлов до сотен или тысяч матричная диаграмма становится тяжёлой для DOM. Основные оптимизации:
canvasПример переключения на Canvas:
const context = canvas.getContext("2d");
for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[i].length; j++) {
context.fillStyle = color(matrix[i][j].value);
context.fillRect(j * cellSize, i * cellSize, cellSize, cellSize);
}
}
При группировке узлов матрица приобретает блочную структуру, где плотные связи концентрируются вдоль диагонали.
Используются алгоритмы:
После кластеризации узлы переупорядочиваются так, чтобы группы располагались рядом, создавая визуальные «квадраты плотности».
Матрицы часто применяются вместе с:
Связка позволяет переключаться между структурным и матричным представлением одной и той же модели данных без изменения источника.