Heatmap (тепловая карта) — способ визуализации двумерных данных, при котором интенсивность цвета отражает числовое значение в определённой точке сетки. Такой тип графика особенно эффективен при анализе:
В Vega-Lite heatmap чаще всего строится с помощью mark
rect, где:
x определяет колонку;y определяет строку;color определяет интенсивность.Базовая структура:
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {
"values": [
{"day": "Mon", "hour": 9, "value": 12},
{"day": "Mon", "hour": 10, "value": 18},
{"day": "Tue", "hour": 9, "value": 7}
]
},
"mark": "rect",
"encoding": {
"x": {"field": "hour", "type": "ordinal"},
"y": {"field": "day", "type": "ordinal"},
"color": {"field": "value", "type": "quantitative"}
}
}
Heatmap в Vega-Lite состоит из нескольких компонентов:
| Компонент | Назначение |
|---|---|
mark: rect |
Рисование ячеек |
x |
Горизонтальная координата |
y |
Вертикальная координата |
color |
Интенсивность |
scale |
Настройка цветовой шкалы |
transform |
Подготовка и агрегация данных |
tooltip |
Интерактивные подсказки |
На практике данные редко приходят в готовом виде. Vega-Lite предоставляет набор трансформаций:
aggregatebincalculatefilterfoldpivotjoinaggregatewindowimputeИменно трансформации превращают сырые данные в структуру, пригодную для heatmap.
Heatmap часто отображает агрегированные значения.
Например:
Исходные данные:
[
{"hour": 9, "day": "Mon", "sales": 100},
{"hour": 9, "day": "Mon", "sales": 200},
{"hour": 10, "day": "Mon", "sales": 150}
]
Трансформация:
{
"transform": [
{
"aggregate": [
{
"op": "sum",
"field": "sales",
"as": "totalSales"
}
],
"groupby": ["hour", "day"]
}
]
}
Результат:
[
{"hour": 9, "day": "Mon", "totalSales": 300},
{"hour": 10, "day": "Mon", "totalSales": 150}
]
Использование:
"color": {
"field": "totalSales",
"type": "quantitative"
}
Heatmap часто используется как визуализация плотности распределения.
Для этого применяются:
binaggregate{
"transform": [
{
"bin": true,
"field": "x",
"as": "x_bin"
},
{
"bin": true,
"field": "y",
"as": "y_bin"
}
]
}
После бинирования значения группируются по диапазонам.
Например:
| Исходное значение | Bin |
|---|---|
| 12 | 10–20 |
| 17 | 10–20 |
| 29 | 20–30 |
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {
"url": "data/points.json"
},
"transform": [
{
"bin": {"maxbins": 20},
"field": "x",
"as": "xbin"
},
{
"bin": {"maxbins": 20},
"field": "y",
"as": "ybin"
},
{
"aggregate": [
{
"op": "count",
"as": "count"
}
],
"groupby": ["xbin", "ybin"]
}
],
"mark": "rect",
"encoding": {
"x": {
"field": "xbin",
"type": "ordinal"
},
"y": {
"field": "ybin",
"type": "ordinal"
},
"color": {
"field": "count",
"type": "quantitative"
}
}
}
Трансформация calculate создаёт вычисляемые поля.
Это особенно важно при:
{
"transform": [
{
"calculate": "datum.hour < 12 ? 'Morning' : 'Evening'",
"as": "period"
}
]
}
Теперь можно строить heatmap:
"encoding": {
"x": {"field": "period"},
"y": {"field": "day"},
"color": {"aggregate": "count"}
}
Heatmap визуально напоминает таблицу.
Иногда данные хранятся в “длинном” формате:
[
{"month": "Jan", "metric": "Sales", "value": 100},
{"month": "Jan", "metric": "Profit", "value": 40}
]
Для матричного представления используется pivot.
{
"transform": [
{
"pivot": "metric",
"value": "value",
"groupby": ["month"]
}
]
}
Результат:
[
{
"month": "Jan",
"Sales": 100,
"Profit": 40
}
]
Обратная операция — fold.
Она превращает широкую таблицу в длинную.
Исходная структура:
[
{
"month": "Jan",
"Sales": 100,
"Profit": 40
}
]
Fold:
{
"transform": [
{
"fold": ["Sales", "Profit"]
}
]
}
Результат:
[
{
"month": "Jan",
"key": "Sales",
"value": 100
},
{
"month": "Jan",
"key": "Profit",
"value": 40
}
]
Это удобно для построения универсальных heatmap.
Heatmap должен отображать полную сетку.
Если некоторые комбинации отсутствуют, возникают “дыры”.
Трансформация impute создаёт недостающие значения.
{
"transform": [
{
"impute": "value",
"key": "hour",
"groupby": ["day"],
"value": 0
}
]
}
Теперь отсутствующие часы получат значение 0.
Цвет — центральный элемент heatmap.
"color": {
"field": "value",
"type": "quantitative",
"scale": {
"scheme": "viridis"
}
}
| Scheme | Особенности |
|---|---|
viridis |
Универсальная |
magma |
Тёмная палитра |
inferno |
Контрастная |
plasma |
Яркая |
blues |
Монохром |
reds |
Тепловая |
greenblue |
Аналитическая |
Если данные содержат отрицательные и положительные значения:
"scale": {
"scheme": "redblue",
"domainMid": 0
}
Такая heatmap показывает:
Часто значения нужно привести к диапазону.
{
"transform": [
{
"joinaggregate": [
{
"op": "max",
"field": "value",
"as": "maxValue"
}
]
},
{
"calculate": "datum.value / datum.maxValue",
"as": "normalized"
}
]
}
Один из наиболее распространённых вариантов — временная heatmap.
Например:
{
"transform": [
{
"timeUnit": "hours",
"field": "timestamp",
"as": "hour"
},
{
"timeUnit": "day",
"field": "timestamp",
"as": "day"
}
]
}
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {
"url": "logs.json"
},
"transform": [
{
"timeUnit": "hours",
"field": "timestamp",
"as": "hour"
},
{
"timeUnit": "day",
"field": "timestamp",
"as": "day"
},
{
"aggregate": [
{
"op": "count",
"as": "events"
}
],
"groupby": ["hour", "day"]
}
],
"mark": "rect",
"encoding": {
"x": {
"field": "hour",
"type": "ordinal"
},
"y": {
"field": "day",
"type": "ordinal"
},
"color": {
"field": "events",
"type": "quantitative"
}
}
}
Heatmap можно комбинировать со слоями.
Например:
{
"layer": [
{
"mark": "rect"
},
{
"mark": {
"type": "text",
"baseline": "middle"
},
"encoding": {
"text": {
"field": "value"
}
}
}
]
}
Heatmap поддерживает condition.
"color": {
"condition": {
"test": "datum.value > 80",
"value": "red"
},
"value": "lightgray"
}
Heatmap может отображать ранги и скользящие показатели.
{
"transform": [
{
"window": [
{
"op": "rank",
"as": "rank"
}
],
"sort": [
{
"field": "value",
"order": "descending"
}
]
}
]
}
Один из классических вариантов — матрица корреляций.
Данные:
| A | B | C | |
|---|---|---|---|
| A | 1 | 0.4 | -0.2 |
| B | 0.4 | 1 | 0.7 |
| C | -0.2 | 0.7 | 1 |
Преобразование в long format:
[
{"x": "A", "y": "A", "corr": 1},
{"x": "A", "y": "B", "corr": 0.4},
{"x": "A", "y": "C", "corr": -0.2}
]
Heatmap:
{
"mark": "rect",
"encoding": {
"x": {"field": "x"},
"y": {"field": "y"},
"color": {
"field": "corr",
"type": "quantitative",
"scale": {
"scheme": "redblue",
"domain": [-1, 1]
}
}
}
}
При больших объёмах данных возникают проблемы:
Лучше агрегировать данные заранее.
Плохо:
aggregate count over millions rows
Хорошо:
pre-aggregated bins
{
"bin": {
"maxbins": 50
}
}
vegaEmbed("#view", spec, {
renderer: "canvas"
});
Canvas быстрее SVG при большом количестве ячеек.
Vega-Lite поддерживает selections и parameters.
{
"params": [
{
"name": "hover",
"select": {
"type": "point",
"on": "mouseover"
}
}
]
}
"opacity": {
"condition": {
"param": "hover",
"value": 1
},
"value": 0.5
}
"tooltip": [
{"field": "hour"},
{"field": "day"},
{"field": "value"}
]
Vega предоставляет более низкоуровневый контроль.
В отличие от Vega-Lite:
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"data": [
{
"name": "table",
"values": [
{"x": 1, "y": 1, "value": 10},
{"x": 2, "y": 1, "value": 20}
]
}
],
"scales": [
{
"name": "xscale",
"type": "band",
"domain": {"data": "table", "field": "x"},
"range": "width"
},
{
"name": "yscale",
"type": "band",
"domain": {"data": "table", "field": "y"},
"range": "height"
},
{
"name": "color",
"type": "linear",
"domain": {"data": "table", "field": "value"},
"range": {"scheme": "viridis"}
}
],
"marks": [
{
"type": "rect",
"from": {
"data": "table"
},
"encode": {
"enter": {
"x": {"scale": "xscale", "field": "x"},
"y": {"scale": "yscale", "field": "y"},
"width": {"scale": "xscale", "band": 1},
"height": {"scale": "yscale", "band": 1},
"fill": {
"scale": "color",
"field": "value"
}
}
}
}
]
}
| Возможность | Vega-Lite | Vega |
|---|---|---|
| Простота | Высокая | Средняя |
| Автоматизация | Есть | Нет |
| Контроль | Ограниченный | Полный |
| Signals | Частично | Полностью |
| Сложная интерактивность | Ограничена | Максимальная |
Плохо:
"type": "nominal"
для числового color.
Правильно:
"type": "quantitative"
maxbins: 500
приводит к перегрузке.
Heatmap с пропущенными значениями визуально ломается.
Например:
"scheme": "reds"
для отрицательных значений.
Лучше:
"scheme": "redblue"
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {
"url": "activity.json"
},
"transform": [
{
"timeUnit": "hours",
"field": "timestamp",
"as": "hour"
},
{
"timeUnit": "day",
"field": "timestamp",
"as": "day"
},
{
"aggregate": [
{
"op": "count",
"as": "events"
}
],
"groupby": [
"hour",
"day"
]
},
{
"impute": "events",
"key": "hour",
"groupby": ["day"],
"value": 0
}
],
"mark": "rect",
"encoding": {
"x": {
"field": "hour",
"type": "ordinal"
},
"y": {
"field": "day",
"type": "ordinal"
},
"color": {
"field": "events",
"type": "quantitative",
"scale": {
"scheme": "viridis"
}
},
"tooltip": [
{"field": "day"},
{"field": "hour"},
{"field": "events"}
]
},
"config": {
"view": {
"stroke": null
}
}
}