Матричные диаграммы связей

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

Ключевая идея заключается в переходе от списка связей:

  • nodes: набор объектов
  • links: пары источника и цели

к двумерной структуре 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;
});

Так формируется симметричная матрица для неориентированных графов.


Геометрия матричной диаграммы в SVG

Каждая ячейка матрицы представляется прямоугольником <rect>. Позиционирование определяется индексами:

  • x = column * cellSize
  • y = row * cellSize
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. Основные оптимизации:

  • минимизация количества SVG-элементов через canvas
  • использование виртуализации строк
  • предварительная агрегация данных
  • отключение transition-анимаций

Пример переключения на 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);
  }
}

Кластеризация и блоковая структура

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

Используются алгоритмы:

  • hierarchical clustering
  • community detection (Louvain)
  • спектральная сортировка

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


Комбинирование с другими типами визуализаций

Матрицы часто применяются вместе с:

  • списком узлов
  • диаграммой связей (force-directed)
  • chord-диаграммами

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