Skip to content

Heatmap

ChartsSince 0.4.0
import { Heatmap } from '@absoluteui/components/heatmap';
import '@absoluteui/components/heatmap/style.css';

Using Angular? See Heatmap for Angular.

An accessible SVG heatmap: a matrix of values over two categorical axes (columns along x, rows along y). Each cell’s fill encodes its value on a scale of discrete steps:

  • sequential (default): one palette slot, light to dark;
  • diverging: two palette slots on either side of a midpoint.

Fill lightness alone is a weak cue, so it is never the only one:

  • cells print their values when the formatted value fits (cellLabels);
  • the legend lists every step’s value range;
  • every cell is keyboard-reachable in two dimensions (one tab stop, arrow keys) with a tooltip, and its accessible name carries the exact value;
  • the data table holds the exact matrix, and a generated summary names the size and the extremes.

Nothing animates.

  • Showing patterns across two categorical dimensions (weekday by hour, region by month, feature by segment).
  • Diverging data around a meaningful midpoint (change from baseline, correlation): use colorScale.type ‘diverging’.
  • When exact comparison of a few values matters: use Bar Chart (fill lightness is a weak cue).
  • Continuous x and y values: use Scatter Chart.
  • Very large matrices (thousands of cells): aggregate first; every cell is a DOM element.
import { Heatmap } from '@absoluteui/components/heatmap';
const host = document.createElement('div');
document.body.append(host);
Heatmap.create(host, {
label: 'Support tickets by weekday and hour',
columns: ['9:00', '12:00', '15:00', '18:00'],
rows: ['Mon', 'Tue', 'Wed'],
values: [
[4, 12, 9, 3],
[6, 15, null, 2],
[5, 11, 8, 4],
],
colorScale: { color: 3, steps: 4 },
xAxis: { label: 'Hour' },
yAxis: { label: 'Weekday' },
height: 240,
});

Diverging scale around zero with percent values

Section titled “Diverging scale around zero with percent values”
import { Heatmap } from '@absoluteui/components/heatmap';
const host = document.querySelector<HTMLElement>('#change');
if (host) {
const chart = Heatmap.create(host, {
label: 'Change from last year',
columns: ['Q1', 'Q2', 'Q3', 'Q4'],
rows: ['North', 'South', 'East'],
values: [
[0.12, 0.05, -0.03, 0.08],
[-0.1, -0.04, 0.02, 0.01],
[0.03, 0.09, 0.15, -0.06],
],
colorScale: { type: 'diverging', midpoint: 0, negative: 2, positive: 1, steps: 3 },
valueFormat: { style: 'percent', maximumFractionDigits: 0 },
dataTable: 'visible',
});
chart.update({ cellLabels: 'none' });
}
import { Heatmap } from '@absoluteui/components/heatmap';
import '@absoluteui/components/heatmap/style.css';
const host = document.querySelector<HTMLElement>('#load');
if (host) {
Heatmap.create(host, {
label: 'Server load by day and hour',
columns: ['00', '06', '12', '18'],
rows: ['Sat', 'Sun'],
values: [[0.2, 0.35, 0.8, 0.6], [0.15, 0.3, 0.7, 0.55]],
valueFormat: { style: 'percent' },
colorScale: { min: 0, max: 1 },
});
}
import { Heatmap } from '@absoluteui/components/heatmap';
const host = document.createElement('div');
document.body.append(host);
Heatmap.create(host, {
label: 'Tickets by weekday and hour',
columns: ['9', '12', '15'],
rows: ['Mon', 'Tue'],
values: [[1, 5, 9], [null, 4, 2]],
});
<div class="aui-chart aui-heatmap [aui-chart--patterns]">
<div class="aui-chart__header">title, description</div>
<ul class="aui-chart__legend" aria-label="Legend"><li><span class="aui-chart__legend-item">swatch, 0 to 2</span></li>...</ul>
<div class="aui-chart__body">
<svg class="aui-chart__svg" role="group" aria-roledescription="heatmap" aria-labelledby="T" aria-describedby="S">
<g class="aui-chart__axis" aria-hidden="true">column and row labels</g>
<g class="aui-heatmap__cells">
<rect class="aui-heatmap__cell aui-chart__series--c1 aui-heatmap__level--60 aui-chart__mark" role="img" aria-label="12, Tue: 4" tabindex="0"/>
<rect class="aui-heatmap__cell aui-heatmap__cell--empty aui-chart__mark" role="img" aria-label="9, Tue: No value"/>
</g>
<g class="aui-heatmap__labels" aria-hidden="true"><text class="aui-chart__value-label">4</text>...</g>
</svg>
<div class="aui-chart__tooltip" aria-hidden="true" hidden/>
</div>
<p class="aui-chart__summary" hidden/> <div class="aui-chart__data">Show data table, table</div>
</div>
Heatmap.create(host: HTMLElement, options: HeatmapOptions): Heatmap

Global configuration key: heatmap (see Configuration).

Option Type Default Description
label string Chart title: visible text and the accessible name of the plot (aria-labelledby); also the data table caption. Required.
hideLabel boolean false Visually hide the title; it stays the accessible name.
description string Visible description below the title, also referenced by aria-describedby (text only).
summary string Replaces the generated accessible summary (matrix size, lowest and highest value with their cells).
valueFormat Intl.NumberFormatOptions Number format for cell labels, legend step ranges, tooltips, the data table, and the summary.
formatValue (value: number) => string Custom value text; overrides valueFormat everywhere.
hiddenSeries readonly string[] Shared chart option; a heatmap has no series, so it has no effect.
columns readonly string[] Column labels (x axis), in order (mirrored in RTL). Required.
rows readonly string[] Row labels (y axis), top to bottom. Required.
values readonly (readonly (number | null)[])[] values[row][column]: exactly one array per row and one value per column. null, NaN, and infinities are empty cells (“No value”). Required.
colorScale HeatmapColorScale { type?: ‘sequential’ | ‘diverging’ (default sequential), color?: 1-8 (sequential slot, default 1), negative?: 1-8 (default 2), positive?: 1-8 (default 1), midpoint?: number (diverging center, default 0), min?, max? (fixed domain; default the nice-rounded data extent), steps?: 3-9 (per side for diverging, default 5) }. Values outside a fixed domain use the first or last step.
xAxis { label?: string } Column axis title (drawn below the plot).
yAxis { label?: string } Row axis title (drawn beside the row labels); also the header of the data table’s first column (default “Row”).
legend 'auto' | 'show' | 'hide' "auto" Legend listing every color step with its value range; auto and show both show it, hide removes it. Entries are not toggle buttons. Globally configurable.
dataTable 'toggle' | 'visible' | 'hidden' | 'none' "toggle" Data table fallback with the exact matrix: behind a Show data table button (built on first open), always visible, visually hidden (screen readers only), or none. Globally configurable.
tooltip boolean true Tooltip on pointer hover and keyboard focus (cell name and value). Globally configurable.
patterns boolean false Shared chart option (toggles aui-chart–patterns); heatmap cells and step swatches are not textured, so it has no visible effect. Globally configurable.
height number 300 Plot height in CSS px (integer 120-2000); the width follows the container. Cells share the plot evenly. Globally configurable.
cellLabels 'auto' | 'always' | 'none' "auto" Values printed inside cells: auto and always print them where the formatted value fits (always skips cells that are too small); none prints no values. Globally configurable.
locale string "en-US" Number and message locale. Globally configurable. Inherited from common configuration.
direction 'ltr' | 'rtl' "ltr" rtl mirrors the columns (first column at the right), puts the row labels on the right, and swaps ArrowLeft/ArrowRight. Globally configurable. Inherited from common configuration.
Property Type Description
host HTMLElement The host element.
element SVGSVGElement The plot (svg role=“group”), named by the title and described by the summary.
summary string The generated (or custom) accessible summary.
hiddenSeries string[] Shared chart property; always empty for a heatmap.
Method Description
update(options: Partial<HeatmapOptions>): void Merge, validate, and re-render. Invalid options throw and keep the previous state. Keyboard focus stays on the same cell.
setSeriesVisible(id: string, visible: boolean): void Shared chart method; a heatmap has no series, so it only re-renders.
destroy(): void Remove rendering, host classes, listeners, the resize observer, and the configuration subscription.

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-chart Host (shared chart chrome).
aui-heatmap Host modifier for this chart type.
aui-chart--patterns patterns option on (no visible effect on heatmap cells).
aui-chart__header / __title / __title--hidden / __description Title and description.
aui-chart__legend / __legend-entry / __legend-item / __legend-label Legend of color steps (static entries, not buttons).
aui-chart__swatch aria-hidden legend and tooltip key (step fill).
aui-chart__body / __svg / __layer Plot container (height from –aui-chart-height), SVG, and drawing layer.
aui-chart__axis / __tick-label / __axis-title Column and row labels and axis titles (aria-hidden).
aui-chart__series--c1 ... --c8 Palette slot classes; set –aui-chart-color from color-chart-<n>.
aui-heatmap__level--15 ... --100 Fill strength in steps of 5: the palette color mixed into color-surface at that percentage (sets –aui-heatmap-fill).
aui-heatmap__cells / __cell / __cell--empty Cell group, focusable cells (also aui-chart__mark), and empty (null) cells.
aui-heatmap__labels aria-hidden group of printed cell values (aui-chart__value-label).
aui-chart__mark / __mark--active / __mark--hover Focusable cell and its states (thicker color-text outline).
aui-chart__value-label Printed values with a surface-colored halo.
aui-chart__tooltip / __tooltip-title / __tooltip-rows / __tooltip-row / __tooltip-label / __tooltip-value Tooltip (aria-hidden; the focused cell carries the same text).
aui-chart__empty Empty state text (No data).
aui-chart__summary Hidden summary referenced by aria-describedby.
aui-chart__data / __data--hidden / __table-toggle / __table-scroll / __table / __cell--numeric Data table fallback.

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-color-border | --aui-color-border-strong | --aui-color-chart-1 | --aui-color-chart-2 | --aui-color-chart-3 | --aui-color-chart-4 | --aui-color-chart-5 | --aui-color-chart-6 | --aui-color-chart-7 | --aui-color-chart-8 | --aui-color-chart-axis | --aui-color-chart-grid | --aui-color-focus-ring | --aui-color-hover | --aui-color-link | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-muted | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-font-size-sm | --aui-font-size-xs | --aui-font-weight-medium | --aui-font-weight-regular | --aui-font-weight-semibold | --aui-line-height-normal | --aui-line-height-tight | --aui-radius-md | --aui-radius-sm | --aui-shadow-md | --aui-space-1 | --aui-space-2 | --aui-space-3 | --aui-tooltip-max-width | --aui-touch-target-min

type HeatmapCellLabels = 'auto' | 'always' | 'none';
interface HeatmapConfig extends ChartBaseConfig { cellLabels: HeatmapCellLabels }
interface HeatmapColorScale {
type?: 'sequential' | 'diverging'; // default 'sequential'
color?: ChartColor; // sequential slot, default 1
negative?: ChartColor; // diverging, below midpoint, default 2
positive?: ChartColor; // diverging, above midpoint, default 1
midpoint?: number; // default 0
min?: number; // default: data extent (nice-rounded)
max?: number;
steps?: number; // 3-9 (per side for diverging), default 5
}
interface HeatmapOptions extends ChartCommonOptions, Partial<HeatmapConfig> {
columns: readonly string[];
rows: readonly string[];
values: readonly (readonly (number | null)[])[];
colorScale?: HeatmapColorScale;
xAxis?: { label?: string };
yAxis?: { label?: string };
}

Pattern: SVG matrix chart as a named, described group (role=“group” with aria-roledescription “heatmap”) of role=“img” cells with two-dimensional roving focus; a legend listing every color step with its value range; data table fallback with the exact matrix.

Key Context Action
ArrowLeft / ArrowRight Cell focused Previous / next column in the same row (swapped in RTL).
ArrowUp / ArrowDown Cell focused Same column in the previous / next row.
Home / End Cell focused First / last cell of the row.
PageUp / PageDown Cell focused Ten columns back / forward in the row (clamped to the ends).
Escape Tooltip visible Hides the tooltip without moving focus (WCAG 1.4.13); it returns on the next move.
Tab / Shift+Tab Any One tab stop for the plot (the active cell), then the Show data table button and table region. Legend entries are not focusable.
Element Role
svg.aui-chart__svg group with aria-roledescription “heatmap” (localized), named by the title
cells (rect.aui-heatmap__cell) img with an aria-label: column, row, formatted value (or “No value”)
legend list (ul) named “Legend”; entries are static text (step ranges), not buttons
data table table (native) with caption, column headers (row axis title, then the columns), and row headers; wide tables scroll in a focusable region
row and column labels, axis titles, printed cell values, tooltip aria-hidden (decorative duplicates of the accessible data)
Attribute Element Value
aria-labelledby svg id of the visible (or visually hidden) title
aria-describedby svg description id (when provided) and the generated summary id
aria-roledescription svg Localized “heatmap”
aria-label each cell Localized “{column}, {row}: {value}”, for example “12, Tue: 4”; empty cells read “No value”
tabindex active cell 0 on exactly one cell (roving); other cells have none
aria-expanded / aria-controls Show data table button Table open state and the table region id

Roving focus: one cell is in the tab sequence and remembers the last visited cell, also across re-renders (resize, update, configuration changes). Every cell, including empty ones, is reachable. Toggling the table keeps focus on the table button.

Criterion Level Note
1.1.1 Non-text Content A
1.3.1 Info and Relationships A
1.4.1 Use of Color A
1.4.3 Contrast (Minimum) AA
1.4.10 Reflow AA
1.4.11 Non-text Contrast AA
1.4.13 Content on Hover or Focus AA
2.1.1 Keyboard A
2.4.3 Focus Order A
2.4.7 Focus Visible AA
4.1.2 Name, Role, Value A

Automated test results: Accessibility and compliance.

  • Fill lightness is a weak cue (and sequential steps of one hue are hard to tell apart), so it is never the only one: cells print their values when they fit (cellLabels), the legend lists every step’s value range, each cell’s accessible name and tooltip carry its exact value, and the data table holds the whole matrix.
  • The generated summary names the matrix size and the lowest and highest values with their cells, for example “heatmap, 2 rows by 3 columns. Lowest 1 at 9, Mon; highest 9 at 15, Mon.”
  • Cells are separated by a color-surface stroke, so neighboring fills need no mutual contrast; the hovered or focused cell gets a 2px color-text outline.
  • Forced colors: fills are not reliable, so cells become outlined boxes (Canvas fill, CanvasText stroke; Highlight for hover and focus); values remain available through the printed cell labels, accessible names, and the data table.
  • No animation.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver, TalkBack) is pending.
  • aria-roledescription support varies; screen readers without it announce “group”.
  • Large matrices mean many arrow presses; browse-mode users can read the data table instead.

default (sequential), diverging, empty (“No data”: no rows, no columns, or no finite values), empty cell, cell focused (tooltip), cell hovered, table open.

From Trigger To
plot not focused Tab active cell focused, tooltip shown
cell focused arrow keys, Home, End, PageUp, PageDown another cell focused
tooltip visible Escape tooltip hidden
any update() / configuration change re-rendered; focus kept on the same cell
  • Trade-off: lightness steps of one hue are hard to tell apart, and color alone would fail WCAG 1.4.1. The heatmap therefore prints values in cells that fit them, lists each step’s range in the legend, exposes every value through the cell names and tooltips, and offers the exact matrix in the data table. Keep at least cellLabels or dataTable on.
  • Every cell (including empty ones) is keyboard-reachable in two dimensions.
  • The summary names the size and extremes: “heatmap, 2 rows by 3 columns. Lowest 1 at 9, Mon; highest 9 at 15, Mon.”
  • Cells are separated by a color-surface stroke, so neighboring fills need no mutual contrast.
  • Forced colors: fills are not reliable, so cells become outlined boxes (Canvas / CanvasText, Highlight on hover and focus); values stay available through the printed labels, accessible names, and the data table.
  • No animation. Manual screen-reader verification is pending.

The width follows the container (ResizeObserver); the height is height. Cells share the plot evenly. Row labels take at most 30% of the width and are truncated; column labels are truncated or thinned when crowded; cell values are printed only where they fit. Reflows at 320 CSS px.

direction: 'rtl' puts the first column at the right and the row labels on the right, and swaps ArrowLeft/ArrowRight. The legend, tooltip, and table follow the document direction.

Step fills mix color-chart-<slot> into color-surface at 15-100% (color-mix), so they adapt to every theme; empty cells use color-surface-sunken. Lighter steps are intentionally low-contrast: meaning is carried by the printed values, legend, and table, not by the fill.

configureAbsoluteUI({ components: { heatmap: { cellLabels: 'none', dataTable: 'visible' } } });

Changes apply to existing instances immediately.

Instance options override global and theme defaults. All options are dynamic.

All text uses textContent; SVG uses createElementNS; colors are classes. No innerHTML or style attributes.

No persistence, network access, or telemetry.

Budget: Heatmap.create with 24 x 30 cells < 60 ms in headless Chromium (e2e performance test), including the first render. One number formatter per render; the data table is built lazily.

Up to about 1,000 cells; every cell is a focusable rect.

  • null, NaN, and infinities are empty cells (color-surface-sunken, “No value”); they are still focusable.
  • All cells empty, or no rows or columns: “No data”.
  • All values equal: every cell uses the same step.
  • Values outside a fixed colorScale.min/max use the first or last step; the legend then reads “Below …” / “Above …” at the ends.
  • Diverging scales are symmetric around midpoint (the larger distance sets both sides); the legend has 2 x steps entries.
Code Category Cause
HEATMAP_OPTIONS configuration Missing label; non-string columns or rows; values not one array per row with one number-or-null per column; invalid colorScale (type, slots, non-finite numbers, max <= min, steps outside 3-9); invalid axis titles or formatters.
CFG_INVALID_VALUE / CFG_UNKNOWN_OPTION configuration Invalid or unknown configuration.

A failed construction leaves the host untouched.

  • Discrete steps only; one slot (sequential) or two (diverging).
  • cellLabels: 'always' prints values only where they fit, like 'auto'.
  • Manual screen-reader verification is pending.
  • Numeric or time axes, uneven cells, calendar layouts.
  • Clustering, sorting, selection.