Тепловая карта (heatmap)

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

Типичная структура данных для тепловой карты представляет собой массив объектов, каждый из которых описывает координаты и значение:

const data = [
  { x: 0, y: 0, value: 12 },
  { x: 1, y: 0, value: 34 },
  { x: 2, y: 0, value: 23 },
  { x: 0, y: 1, value: 45 }
];

Альтернативный формат — матрица:

const matrix = [
  [12, 34, 23],
  [45, 56, 10],
  [5,  18, 39]
];

При работе с D3.js чаще используется первый вариант, так как он проще для масштабирования и привязки осей.

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

Основой визуализации служит SVG-элемент, внутри которого размещаются прямоугольники:

const width = 600;
const height = 400;

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

SVG выступает как координатная система, в которой каждая ячейка тепловой карты позиционируется по осям X и Y.

Шкалы координат

Для корректного размещения элементов применяются линейные или порядковые шкалы. В случае дискретных индексов удобно использовать scaleBand, так как она автоматически распределяет пространство между ячейками.

const xScale = d3.scaleBand()
  .domain([0, 1, 2])
  .range([0, width])
  .padding(0.05);

const yScale = d3.scaleBand()
  .domain([0, 1, 2])
  .range([0, height])
  .padding(0.05);

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

Цветовая шкала

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

const colorScale = d3.scaleSequential()
  .domain([0, 100])
  .interpolator(d3.interpolateViridis);

Диапазон домена задаёт минимальное и максимальное значение данных, а интерполятор определяет переход цвета. Часто применяются палитры interpolateViridis, interpolateInferno, interpolatePlasma, так как они обеспечивают равномерное восприятие градиента.

Привязка данных к элементам

Связывание данных с DOM-элементами осуществляется через механизм data join:

svg.selectAll("rect")
  .data(data)
  .enter()
  .append("rect")
  .attr("x", d => xScale(d.x))
  .attr("y", d => yScale(d.y))
  .attr("width", xScale.bandwidth())
  .attr("height", yScale.bandwidth())
  .attr("fill", d => colorScale(d.value));

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

Оси координат

Для улучшения читаемости добавляются оси X и Y. В случае scaleBand используются соответствующие генераторы осей:

const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);

svg.append("g")
  .attr("transform", `translate(0, ${height})`)
  .call(xAxis);

svg.append("g")
  .call(yAxis);

Оси отображают индексы или категории, связанные с данными.

Обработка двумерных матриц

Если данные представлены матрицей, их необходимо преобразовать:

const flatData = matrix.flatMap((row, y) =>
  row.map((value, x) => ({ x, y, value }))
);

Такой подход упрощает дальнейшее использование стандартного D3-пайплайна без специальных условий.

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

Тепловые карты часто дополняются интерактивными элементами. Один из базовых вариантов — tooltip:

const tooltip = d3.select("body")
  .append("div")
  .style("position", "absolute")
  .style("background", "#fff")
  .style("padding", "5px")
  .style("border", "1px solid #ccc")
  .style("opacity", 0);

svg.selectAll("rect")
  .on("mouseover", (event, d) => {
    tooltip.style("opacity", 1)
      .html(`Значение: ${d.value}`);
  })
  .on("mousemove", (event) => {
    tooltip.style("left", event.pageX + 10 + "px")
      .style("top", event.pageY + 10 + "px");
  })
  .on("mouseout", () => {
    tooltip.style("opacity", 0);
  });

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

Анимация изменений

D3.js поддерживает плавные переходы при обновлении данных:

svg.selectAll("rect")
  .data(data)
  .transition()
  .duration(800)
  .attr("fill", d => colorScale(d.value));

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

Масштабирование и производительность

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

  • уменьшение числа DOM-элементов за счёт агрегации;
  • использование виртуализации сетки;
  • отказ от сложных теней и фильтров SVG;
  • минимизация перерисовок через join-паттерн:
const cells = svg.selectAll("rect")
  .data(data, d => `${d.x}-${d.y}`);

cells.join(
  enter => enter.append("rect")
    .attr("x", d => xScale(d.x))
    .attr("y", d => yScale(d.y))
    .attr("width", xScale.bandwidth())
    .attr("height", yScale.bandwidth())
    .attr("fill", d => colorScale(d.value)),
  update => update.transition().attr("fill", d => colorScale(d.value)),
  exit => exit.remove()
);

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

Нормализация данных

Для корректного цветового отображения часто требуется нормализация значений:

const extent = d3.extent(data, d => d.value);

const colorScale = d3.scaleSequential()
  .domain(extent)
  .interpolator(d3.interpolateInferno);

Использование extent автоматически подстраивает шкалу под текущий диапазон данных, повышая информативность визуализации.

Группировка и агрегирование

При высокой плотности данных применяется агрегация:

const grouped = d3.rollups(
  data,
  v => d3.mean(v, d => d.value),
  d => d.x,
  d => d.y
);

Такой метод позволяет снизить шум и выявить общие закономерности распределения значений.

Стилизация элементов

SVG-элементы могут дополнительно оформляться для повышения читаемости:

svg.selectAll("rect")
  .attr("stroke", "#eee")
  .attr("stroke-width", 1);

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

Использование логарифмической шкалы

При сильной вариативности данных линейная шкала становится неэффективной. В таких случаях применяется логарифмическое преобразование:

const colorScale = d3.scaleSequential()
  .domain([1, 1000])
  .interpolator(d3.interpolateMagma)
  .clamp(true);

Предварительная трансформация значений может выполняться отдельно:

d.value = Math.log(d.value + 1);

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

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

Связь между компонентами реализуется через общие масштабы или события взаимодействия, например:

rect.on("click", (event, d) => {
  updateLineChart(d.x);
});

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