Визуализация данных в виде тепловой карты в 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-элемент, внутри которого размещаются прямоугольники:
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));
Переходы позволяют визуально отслеживать изменения состояний системы, особенно при обновлении временных рядов.
При работе с большими наборами данных важным фактором становится оптимизация:
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);
});
Такой подход позволяет строить многослойные аналитические панели, где тепловая карта выступает в роли обзорного слоя.