Heatmap
Compare measures across categories through color intensity.
Chart ID: heatmap. Independent import: @revolist/charts/heatmap.
Live example
Section titled “Live example”Loading chart…
Reading the chart
Section titled “Reading the chart”Compare a category-by-measure matrix through color intensity. Values remain in their matrix cells; color encodes magnitude rather than geometric area.
Data requirements
Section titled “Data requirements”Use one category per row and one or more numeric series. Values are finite numbers or explicit nulls; the row mapper does not group or aggregate repeated keys.
The roles below describe the chart's data contract. Generic category/value roles resolve from the dataset; explicitly bind specialized numeric roles as shown in the complete example.
| Data role | Field count |
|---|---|
category | 1 or more |
value | 1 or more |
series | 1 or more |
Download the complete sample data and settings (JSON). See Data & Configuration for mapping, null values, updates and lifecycle rules.
Create this chart
Section titled “Create this chart”Install the package using the installation guide, give the host an explicit height, and call createHeatmapChart after the host mounts. This factory fixes the chart type to heatmap.
<div id="chart" style="width: 100%; height: 450px;"></div>Complete runnable TypeScript example with sample data
import { createHeatmapChart } from '@revolist/charts/heatmap';
const container = document.getElementById('chart')!;const chart = await createHeatmapChart({ container, "dataset": { "revision": 0, "source": "client", "categories": [ { "id": "preview-category-0", "label": "Q1", "path": [ "Q1" ], "labels": [ "Q1" ], "depth": 1, "kind": "leaf" }, { "id": "preview-category-1", "label": "Q2", "path": [ "Q2" ], "labels": [ "Q2" ], "depth": 1, "kind": "leaf" }, { "id": "preview-category-2", "label": "Q3", "path": [ "Q3" ], "labels": [ "Q3" ], "depth": 1, "kind": "leaf" } ], "series": [ { "id": "preview-series-0", "label": "Revenue", "columnPath": [ "Revenue" ], "labels": [ "Revenue" ], "kind": "leaf", "prop": "preview-0" }, { "id": "preview-series-1", "label": "Margin", "columnPath": [ "Margin" ], "labels": [ "Margin" ], "kind": "leaf", "prop": "preview-1" }, { "id": "preview-series-2", "label": "Units", "columnPath": [ "Units" ], "labels": [ "Units" ], "kind": "leaf", "prop": "preview-2" } ], "values": [ [ 42, 24, 34 ], [ 66, 39, 52 ], [ 54, 46, 63 ] ], "analyticalFrame": { "fields": [ { "id": "pivot:category", "label": "pivot:category", "role": "dimension", "semanticType": "category" }, { "id": "pivot:series", "label": "pivot:series", "role": "dimension", "semanticType": "category" }, { "id": "preview-series-0", "label": "Revenue", "role": "measure", "semanticType": "number", "sourceField": "preview-0" }, { "id": "preview-series-1", "label": "Margin", "role": "measure", "semanticType": "number", "sourceField": "preview-1" }, { "id": "preview-series-2", "label": "Units", "role": "measure", "semanticType": "number", "sourceField": "preview-2" } ], "observations": [ { "id": "[\"preview-category-0\",\"preview-series-0\"]", "dimensions": { "pivot:category": { "value": "Q1", "label": "Q1", "path": [ "Q1" ], "labels": [ "Q1" ] }, "pivot:series": { "value": "Revenue", "label": "Revenue", "path": [ "Revenue" ], "labels": [ "Revenue" ] } }, "measures": { "preview-series-0": 42 } }, { "id": "[\"preview-category-0\",\"preview-series-1\"]", "dimensions": { "pivot:category": { "value": "Q1", "label": "Q1", "path": [ "Q1" ], "labels": [ "Q1" ] }, "pivot:series": { "value": "Margin", "label": "Margin", "path": [ "Margin" ], "labels": [ "Margin" ] } }, "measures": { "preview-series-1": 24 } }, { "id": "[\"preview-category-0\",\"preview-series-2\"]", "dimensions": { "pivot:category": { "value": "Q1", "label": "Q1", "path": [ "Q1" ], "labels": [ "Q1" ] }, "pivot:series": { "value": "Units", "label": "Units", "path": [ "Units" ], "labels": [ "Units" ] } }, "measures": { "preview-series-2": 34 } }, { "id": "[\"preview-category-1\",\"preview-series-0\"]", "dimensions": { "pivot:category": { "value": "Q2", "label": "Q2", "path": [ "Q2" ], "labels": [ "Q2" ] }, "pivot:series": { "value": "Revenue", "label": "Revenue", "path": [ "Revenue" ], "labels": [ "Revenue" ] } }, "measures": { "preview-series-0": 66 } }, { "id": "[\"preview-category-1\",\"preview-series-1\"]", "dimensions": { "pivot:category": { "value": "Q2", "label": "Q2", "path": [ "Q2" ], "labels": [ "Q2" ] }, "pivot:series": { "value": "Margin", "label": "Margin", "path": [ "Margin" ], "labels": [ "Margin" ] } }, "measures": { "preview-series-1": 39 } }, { "id": "[\"preview-category-1\",\"preview-series-2\"]", "dimensions": { "pivot:category": { "value": "Q2", "label": "Q2", "path": [ "Q2" ], "labels": [ "Q2" ] }, "pivot:series": { "value": "Units", "label": "Units", "path": [ "Units" ], "labels": [ "Units" ] } }, "measures": { "preview-series-2": 52 } }, { "id": "[\"preview-category-2\",\"preview-series-0\"]", "dimensions": { "pivot:category": { "value": "Q3", "label": "Q3", "path": [ "Q3" ], "labels": [ "Q3" ] }, "pivot:series": { "value": "Revenue", "label": "Revenue", "path": [ "Revenue" ], "labels": [ "Revenue" ] } }, "measures": { "preview-series-0": 54 } }, { "id": "[\"preview-category-2\",\"preview-series-1\"]", "dimensions": { "pivot:category": { "value": "Q3", "label": "Q3", "path": [ "Q3" ], "labels": [ "Q3" ] }, "pivot:series": { "value": "Margin", "label": "Margin", "path": [ "Margin" ], "labels": [ "Margin" ] } }, "measures": { "preview-series-1": 46 } }, { "id": "[\"preview-category-2\",\"preview-series-2\"]", "dimensions": { "pivot:category": { "value": "Q3", "label": "Q3", "path": [ "Q3" ], "labels": [ "Q3" ] }, "pivot:series": { "value": "Units", "label": "Units", "path": [ "Units" ], "labels": [ "Units" ] } }, "measures": { "preview-series-2": 63 } } ], "matrixCells": [ { "categoryId": "preview-category-0", "seriesId": "preview-series-0", "observationId": "[\"preview-category-0\",\"preview-series-0\"]", "measureFieldId": "preview-series-0" }, { "categoryId": "preview-category-0", "seriesId": "preview-series-1", "observationId": "[\"preview-category-0\",\"preview-series-1\"]", "measureFieldId": "preview-series-1" }, { "categoryId": "preview-category-0", "seriesId": "preview-series-2", "observationId": "[\"preview-category-0\",\"preview-series-2\"]", "measureFieldId": "preview-series-2" }, { "categoryId": "preview-category-1", "seriesId": "preview-series-0", "observationId": "[\"preview-category-1\",\"preview-series-0\"]", "measureFieldId": "preview-series-0" }, { "categoryId": "preview-category-1", "seriesId": "preview-series-1", "observationId": "[\"preview-category-1\",\"preview-series-1\"]", "measureFieldId": "preview-series-1" }, { "categoryId": "preview-category-1", "seriesId": "preview-series-2", "observationId": "[\"preview-category-1\",\"preview-series-2\"]", "measureFieldId": "preview-series-2" }, { "categoryId": "preview-category-2", "seriesId": "preview-series-0", "observationId": "[\"preview-category-2\",\"preview-series-0\"]", "measureFieldId": "preview-series-0" }, { "categoryId": "preview-category-2", "seriesId": "preview-series-1", "observationId": "[\"preview-category-2\",\"preview-series-1\"]", "measureFieldId": "preview-series-1" }, { "categoryId": "preview-category-2", "seriesId": "preview-series-2", "observationId": "[\"preview-category-2\",\"preview-series-2\"]", "measureFieldId": "preview-series-2" } ] } }, "bindings": {}, "presentation": { "dataLabels": false }});
// When the host unmounts:// chart.destroy();Await updates before reading a snapshot or exporting. Call chart.destroy() when the host unmounts. See the live catalog to interact with this type.
Supported options
Section titled “Supported options”Set these fields in chartOptions. The option schema validates supported keys, ranges and choices; updating chartOptions replaces that authored object.
| Option | Default | Accepted values |
|---|---|---|
showGridLines | true | true or false |
markOpacity | 1 | 0.2 to 1; step 0.05 |
rendererPolicy | svg | svg, webgl |
Configure categorical columns/rows through axes.matrix.columns and axes.matrix.rows: direction, physical side, titles, persisted or runtime grouped row/column bands and axis/label/title colors. Labels use measured gutters and retain canonical source identity; numeric cell-value colors remain separate. See all matrix axis fields and sizing rules.
Titles, legends, theme and labels belong to presentation or the corresponding top-level setting. Axis, color, navigation and interaction support depends on the selected chart; see Data & Configuration.
Related charts
Section titled “Related charts”- Calendar heatmap — Show one daily measure across calendar weeks.
Charts overview · Installation · Data & Configuration
Two-dimensional navigation
Section titled “Two-dimensional navigation”Heatmap navigation is shared by standalone Charts and Pivot. Public windows use stable IDs rather than matrix offsets. X refers to series (columns); Y refers to categories (rows). Both endpoints are inclusive.
const chart = await createChart({ container, chartType: 'heatmap', dataset, navigation: { enabled: true, navigator: true }, viewport: { x: { scale: 'category', startId: dataset.series[1].id, endId: dataset.series[4].id, }, y: { scale: 'category', startId: dataset.categories[10].id, endId: dataset.categories[19].id, }, },});Wheel and pinch gestures zoom both dimensions. Drag pans; Shift-drag zooms to a rectangle. Focus the plot to use arrow keys to pan, +/− to zoom, and Home to reset. Keyboard and button pans always move at least one cell. Escape cancels a gesture. Existing action and gesture policies apply.
The navigator shows the full matrix with separate column and row tracks. Drag or keyboard-adjust their boundary handles independently. Arrow keys move one row/column; Shift increases the step. A heatmap navigator object supports height (default 96px) and selectionStyle fields fillColor, fillOpacity, borderColor, borderWidth, activeColor, handleColor, handleBorderColor, handleBorderWidth, handleWidth, handleHeight and handleRadius. Line overview types, subset series, custom overview colors, opacity veils and handle grips are unsupported for matrix navigators and are rejected rather than silently ignored.
Navigation never filters or renumbers the authoritative dataset. Tooltips and click events keep canonical row/series identity. The visible window is bucketed only when it exceeds rendering limits; zooming reveals individual cells. Empty rows and columns retain their positions, and implicit intensity uses the full matrix’s value extent so colors stay stable while zooming. Shared tooltips show the visible columns for the inspected row; crosshairs follow the inspected cell.
chart.getCSV() exports the current rows and columns. Use chart.getCSV({ dataScope: 'all' }) for the complete matrix. SVG export and server rendering use the visible window. Model save/restore retains both ID ranges. chart.update({ viewport: {} }) resets both dimensions.
If an endpoint disappears after a data update, that axis renders its available full range while retaining the saved IDs. The window reactivates when those IDs return. Endpoint order follows the current dataset order. Charts sync groups with viewport: 'xy' can link compatible heatmap windows.
Independent semantic range selection is not yet supported for heatmaps; navigation rectangles change the viewport. General XY brushing remains separate roadmap work.
Heatmap matrix axis directions
Section titled “Heatmap matrix axis directions”Heatmap X denotes series/columns and Y denotes categories/rows. Their categorical direction settings live in axes.matrix, independently of numeric value-axis definitions:
axes: { matrix: { columns: { reverse: true }, rows: { reverse: true }, },},ChartMatrixAxes has optional columns and rows objects. Each is a ChartMatrixAxis with optional boolean reverse, default false. Columns normally run left to right and rows top to bottom; each true mirrors that dimension’s projection. The settings apply to cells, category/series labels, aggregate bucket extents, hover/crosshair points, pointer gestures and the full-matrix navigator. Dense/partial buckets keep their original memberships, counts, means and source provenance.
Viewport x/y still uses inclusive canonical series/category IDs with scale: 'category'. Reversal never reorders matrix cells, renumbers IDs, changes CSV/data-table order or reverses saved endpoints. Missing endpoints remain dormant and reactivate when their IDs return. Sync groups retain canonical identities, so compatible peers may use different directions. Navigator start/end handles retain their semantic endpoint roles as physical positions mirror; their keyboard increments follow canonical row/column order. Pointer dragging uses the reversed projection to resolve those same IDs.
Standalone Charts and Pivot share the parser and renderer. Save/restore, SVG/image export and navigator previews preserve directions. Pivot’s Customize → Axes and scales exposes Reverse heatmap columns and Reverse heatmap rows. Locale strings are localeText.chartOptions.matrixColumnsReverse and localeText.chartOptions.matrixRowsReverse; UI policy keys are axes.matrix.columns.reverse and axes.matrix.rows.reverse. Gallery changes to another chart family clear matrix-only settings. Pivot exports PivotChartMatrixAxes and PivotChartMatrixAxis aliases through its root and charts facade.
This matrix configuration is currently heatmap-only. It also supports physical placement, titles, colors and measured gutters as described below. Numeric row/column scales/bounds, additional matrix axes remain separate axis work. The cell-value color scale remains independent of row/column direction. Calendar heatmaps retain their calendar layout. General semantic XY brushing also remains separate from viewport navigation.
Heatmap axis appearance and sizing
Section titled “Heatmap axis appearance and sizing”Configure physical heatmap axes through axes.matrix.columns and axes.matrix.rows. Each ChartMatrixAxis supports these fields, also exposed through the equivalent Pivot types:
| Field | Meaning and default |
|---|---|
reverse | Independent categorical direction; default false. |
groups | Optional inner-to-outer categorical group bands; omitted/empty retains leaf captions. See grouped heatmap axes. |
position | Columns: 'top' (default), 'bottom' or 'none'. Rows: 'left' (default), 'right' or 'none'. Physical placement is independent of reversal. |
title | Optional string, at most 4096 characters. Overrides the corresponding legacy presentation.xAxisTitle/presentation.yAxisTitle fallback. An explicit empty string suppresses that fallback. |
lineColor | Shared authored hex-color vocabulary; defaults to the theme’s muted axis color. |
labelColor | Row/column label color in the same hex vocabulary; defaults to theme muted text. |
titleColor | Title color in the same hex vocabulary; defaults to theme foreground text. |
axes: { matrix: { columns: { position: 'bottom', title: 'Sample columns', lineColor: '#445566' }, rows: { position: 'right', title: 'Source categories', labelColor: '#334455' }, },},position: 'none' removes that axis’s line, labels, title and measured gutter. Matrix cell geometry, source identity, reversal, hover, windows and navigator remain active. Titles and colors affect decorations, independently of the numeric cell-value color mapping. Source data and CSV order stay canonical.
Heatmaps use the shared measured-axis layout. It measures final label/title text, renderer font styles and formatter results before laying out the cells; hidden axes release their space. Row/column label regions fit their allocated gutters/slots, preserving full accessible captions when text is truncated. Gutters are bounded to keep a usable plot on narrow hosts. Browser measurements use SVG text metrics; server-rendered SVG uses deterministic font-metric fallback and retains full captions in accessible labels/titles. Navigator and pointer geometry uses the resulting measured plot bounds. Overview previews retain categorical direction with decorations omitted.
Existing renderer label/element overrides retain precedence over axis colors. Matrix row labels have kind: 'category', axis: 'y', their genuine category and original categoryIndex. Column labels have kind: 'category', axis: 'x', genuine series and seriesIndex. Titles use kind: 'axisTitle'. Formatter results are reused between measurement and painting per semantic source identity, including when several source labels have identical text. No synthetic numeric row/column values enter formatter provenance.
Pivot reuses its physical axis forms for Heatmap columns and Heatmap rows, exposing position and title alongside the existing reversal controls. Numeric scale, bound, tick and unit controls are omitted for these categorical axes. Configure colors through the API or existing renderer styles. Localize the group captions with localeText.chartOptions.matrixColumnsAxis and localeText.chartOptions.matrixRowsAxis; common axisPosition, axisTop, axisBottom, axisLeft, axisRight, axisHidden, axisTitle and axisField locale hooks still apply. UI policy keys are axes.matrix.columns.position, axes.matrix.columns.title, axes.matrix.rows.position and axes.matrix.rows.title. Their axes.matrix.columns/axes.matrix.rows parents and the existing axes policy also apply.
Save/restore and SVG/image exports retain these settings. Queued Pivot updates own matrix appearance before validating the resulting chart type; gallery transitions to another family clear matrix-only settings. Numeric matrix row/column scales/bounds, additional matrix axes remain separate work. Calendar heatmaps continue to use their calendar layout.
Grouped heatmap axes
Section titled “Grouped heatmap axes”axes.matrix.columns.groups and axes.matrix.rows.groups accept up to eight ChartCategoryAxisGroup objects, ordered from the innermost band next to the cells to the outermost band. A nonempty list replaces that dimension’s ordinary leaf captions; include the leaf path level as the first group when both leaf and parent labels are needed. An omitted or empty list retains ordinary captions.
axes: { matrix: { columns: { position: 'bottom', groups: [ { values: { revenue: 'Revenue', costs: 'Costs' }, size: 32 }, { values: { revenue: 'Financial', costs: 'Financial' }, size: 32 }, ], }, rows: { position: 'right', groups: [{ level: 1 }, { level: 0 }] }, },},The lookup keys in the example are stable series IDs, not labels or row indices. Row groups use stable category IDs. Each group supports:
| Field | Behavior |
|---|---|
level | Zero-based path index, an integer from 0 through 127. Defaults to the final member of the row category’s path or column series’ columnPath. Corresponding labels supply display text. |
values | Optional lookup of stable IDs to string, finite number, boolean or null, at most 100,000 entries. Missing IDs fall back to the chosen path level. Authored values supply their own text; null uses the localized missing-value caption. |
size | Band thickness, 16 through 160 SVG pixels. Defaults to 32 for columns and 80 for rows. Shared measured layout bounds total gutters to retain a usable plot; bands can shrink and captions can truncate or be omitted on small hosts. |
tickSize | Boundary tick length, 0 through 24 pixels; default 6, capped by the fitted band thickness. |
lineColor, labelColor | Existing authored hex-color vocabulary. Each band overrides the matrix axis color; absent overrides use the axis color and then the renderer theme. Renderer label styles/formatters retain their existing final precedence. |
Groups merge only adjacent members with the same typed value and outer-group identities. Numeric 1 and string '1' remain separate even when their captions match. Repeated nonadjacent groups remain separate. Windowing clips groups to visible canonical members; reversal mirrors their physical projection without changing IDs, cell values or CSV. Group boundaries use source row/column positions, independently of dense cell bucket aggregation. Navigator previews omit decorations and preserve the full-matrix overview.
Both physical sides, titles, hidden axes, save/restore, server SVG and image export use the shared axis pipeline. position: 'none' hides all bands and releases their gutter. Grouping does not aggregate, reorder or add data. Persisted group definitions contain data, without callback functions. Runtime matrix grouping callbacks are described below; additional matrix axes remain separate work.
Pivot Customize → Axes and scales reuses the group editor under Heatmap columns and Heatmap rows. Enable Group category labels to initialize up to eight levels from that dimension’s path depth, then edit levels, add or remove bands. When columns contain measures without a column dimension path, enabling grouping initializes an explicit stable series-ID-to-label lookup so each measure retains its caption. Path-only API groups on an empty column path have a missing group value and no invented path caption. Explicit values lookups are API-configured and disable the corresponding path-level editor. These controls use axes.matrix.columns.groups and axes.matrix.rows.groups UI policy keys plus existing parents. Existing chartOptions.categoryGroups, categoryGroupLevel, categoryGroupAdd, categoryGroupRemove and axisField locale hooks apply; accessible field labels include the heatmap dimension name. No new locale strings are required.
Runtime heatmap grouping callbacks
Section titled “Runtime heatmap grouping callbacks”Use ChartsRendererOptions.matrixAxes.rows.groups and matrixAxes.columns.groups to derive group values or captions from source metadata. Each dimension independently overrides its persisted axes.matrix group list. An explicit empty array disables grouping for that dimension and restores ordinary leaf captions; an omitted groups keeps the persisted list. Runtime definitions replace the whole list, including its sizes/styles, without merging by index.
import { createChartsRenderer } from '@revolist/charts';
const renderer = createChartsRenderer({ matrixAxes: { rows: { groups: [ { getValue: category => ({ value: category.id, label: category.label }) }, { getValue: category => category.path[0] ?? null }, ], }, columns: { groups: [ { size: 70, getValue: (series, seriesIndex) => ({ value: series.measure?.id ?? series.id, label: `${seriesIndex + 1}: ${series.measure?.label ?? series.label}`, }), }, ], }, },});ChartCategoryAxisRuntimeGroup is the row type: getValue(category, categoryIndex) receives a read-only ChartCategory and its original index in the canonical accepted dataset. ChartSeriesAxisRuntimeGroup is the column type: getValue(series, seriesIndex) receives a read-only ChartSeries with columnPath, labels and optional measure metadata, plus its original index in the canonical accepted dataset. Columns are genuine series, without fabricated category paths. Windows and physical reversal preserve these indexes and IDs. For remote sources, indexes refer to the accepted frame, without inventing positions or members for unloaded records.
Each runtime group accepts all persisted group fields (level, values, size, tickSize, lineColor, labelColor) plus optional getValue. Without a callback, that group’s own stable-ID lookup/path configuration supplies values and labels. A callback returns a string, finite number, boolean, null, or { value, label } with a scalar value and string label. Scalar results display their string form; scalar null uses localeText.renderer.missingValue. A value/label pair separates typed grouping identity from its caption. Adjacent values merge only when both their type and all outer group identities match. undefined, nonfinite numbers and malformed pairs are invalid; invalid configuration/results throw TypeError, and callback exceptions propagate. The eight-band and existing size/tick/color limits still apply.
The renderer caches each visible member’s result by stable ID per group, per render. Measurement and painting share those results; it does not call a row callback once for every matrix cell. A new render after data, viewport, resize or chart updates starts a fresh cache, including when IDs remain the same. Keep callback input read-only. Hidden axes and navigator previews do not evaluate callbacks. Runtime matrix options are used only by heatmaps; other chart families ignore them. Hidden-axis group configuration is still validated.
All band projection, measured gutters, physical sides, reversal, caption fitting and renderer style/label precedence remain shared with persisted grouping. Callbacks do not aggregate/reorder source cells or affect CSV. Runtime functions and derived captions are not serialized into the model; configure the same renderer when restoring a chart. Standalone Charts accepts the options through rendererOptions or createChartsRenderer. Pivot uses createPivotChartsRenderer({ matrixAxes: ... }) and its public renderer option. Pivot exports PivotChartCategoryAxisRuntimeGroup and PivotChartSeriesAxisRuntimeGroup through both its root and charts facade. The existing Pivot group forms edit persisted groups; renderer runtime overrides keep their normal precedence.
Server rendering accepts the same configuration through renderChartToSVG({ model, width, height, rendererOptions: { matrixAxes: ... } }). It evaluates callbacks using the server model’s real metadata and deterministic text fitting. SVG/image output includes the resulting labels; saved models still contain only persisted settings. Reapply matching renderer options for client hydration. No additional locale or UI policy keys are introduced by these renderer callbacks.
Explicit renderer policy
Section titled “Explicit renderer policy”chartOptions.rendererPolicy accepts 'svg' | 'webgl'. Omission keeps native SVG. See the WebGL capability, inspection, fallback and export contract and measured budgets. GPU selection does not raise existing source limits or imply SVG styling equivalence.
Viewports and dense-cell inspection
Section titled “Viewports and dense-cell inspection”Heatmaps have independent row and column windows. Use the matrix navigation contract for scrolling, selected-series projections and canonical window identities. Dense geometry can combine adjacent source cells into bounded mean-valued buckets. A one-source bucket retains raw activation; a multi-source bucket is an aggregate inspection target. A bucket mean never becomes a fabricated source cell or a raw CSV value. The optional GPU backend paints this existing bounded geometry; it does not turn every input cell into a GPU datum or raise source limits. See WebGL limitations and exports.