Heatmap
A value for each row and column, such as orders by day and hour, as a grid of cells on a five-step ramp with a "Fewer … More" scale. Drawn on the server, with no client JavaScript, and never the only view of the data.
Import
import { Heatmap } from '@nizivo/charts/heatmap';Static or interactive
Server. Heatmap has no 'use client' and renders no island. The grid is hidden from assistive
technology, which reads the data table instead: one row per row of the grid, one column per column,
every value as text. Figma asks for focusable cells or a table alternative; the table is it, and
tableView="details" shows it to everyone. How to choose.
Examples
Basic
| Day | 00 | 02 | 04 | 06 | 08 | 10 | 12 | 14 | 16 | 18 | 20 | 22 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Mon | 2 | 1 | 1 | 9 | 18 | 21 | 24 | 52 | 38 | 22 | 12 | 9 |
| Tue | 3 | 2 | 8 | 11 | 19 | 22 | 36 | 40 | 37 | 23 | 20 | 10 |
| Wed | 2 | 1 | 6 | 12 | 20 | 24 | 35 | 44 | 41 | 25 | 18 | 9 |
| Thu | 3 | 2 | 7 | 10 | 21 | 23 | 37 | 47 | 39 | 26 | 19 | 11 |
| Fri | 4 | 2 | 6 | 12 | 22 | 26 | 39 | 50 | 46 | 31 | 24 | 14 |
| Sat | 11 | 3 | 10 | 21 | 24 | 36 | 38 | 52 | 51 | 37 | 22 | 4 |
| Sun | 9 | 4 | 3 | 8 | 16 | 29 | 34 | 45 | 40 | 30 | 21 | 6 |
import type { JSX } from 'react';
import { Heatmap } from '@nizivo/charts/heatmap';
interface Slot {
day: string;
hour: string;
orders: number | null;
}
const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const HOURS = ['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22'];
// Figma 03's heatmap: orders per two hours, a week, busiest at midday and in the evening.
const ORDERS: number[][] = [
[2, 1, 1, 9, 18, 21, 24, 52, 38, 22, 12, 9],
[3, 2, 8, 11, 19, 22, 36, 40, 37, 23, 20, 10],
[2, 1, 6, 12, 20, 24, 35, 44, 41, 25, 18, 9],
[3, 2, 7, 10, 21, 23, 37, 47, 39, 26, 19, 11],
[4, 2, 6, 12, 22, 26, 39, 50, 46, 31, 24, 14],
[11, 3, 10, 21, 24, 36, 38, 52, 51, 37, 22, 4],
[9, 4, 3, 8, 16, 29, 34, 45, 40, 30, 21, 6],
];
const WEEK: Slot[] = DAYS.flatMap((day, d) =>
HOURS.map((hour, h) => ({ day, hour, orders: ORDERS[d]?.[h] ?? null })),
);
export function HeatmapExample(): JSX.Element {
return (
<Heatmap
title="Orders by hour"
summary="Orders per two hours, Oct 5–11"
data={WEEK}
x="hour"
y="day"
value="orders"
labels={{ row: 'Day' }}
/>
);
}
Missing values and the table
A store closed on Sunday mornings (no value), with the values open under the grid.
Show data table
| Day | 00 | 02 | 04 | 06 | 08 | 10 | 12 | 14 | 16 | 18 | 20 | 22 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Mon | 2 | 1 | 1 | 9 | 18 | 21 | 24 | 52 | 38 | 22 | 12 | 9 |
| Tue | 3 | 2 | 8 | 11 | 19 | 22 | 36 | 40 | 37 | 23 | 20 | 10 |
| Wed | 2 | 1 | 6 | 12 | 20 | 24 | 35 | 44 | 41 | 25 | 18 | 9 |
| Thu | 3 | 2 | 7 | 10 | 21 | 23 | 37 | 47 | 39 | 26 | 19 | 11 |
| Fri | 4 | 2 | 6 | 12 | 22 | 26 | 39 | 50 | 46 | 31 | 24 | 14 |
| Sat | 11 | 3 | 10 | 21 | 24 | 36 | 38 | 52 | 51 | 37 | 22 | 4 |
| Sun | Closed | Closed | Closed | Closed | Closed | 29 | 34 | 45 | 40 | 30 | 21 | 6 |
import type { JSX } from 'react';
import { Heatmap } from '@nizivo/charts/heatmap';
interface Slot {
day: string;
hour: string;
orders: number | null;
}
const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const HOURS = ['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22'];
// Figma 03's heatmap: orders per two hours, a week, busiest at midday and in the evening.
const ORDERS: number[][] = [
[2, 1, 1, 9, 18, 21, 24, 52, 38, 22, 12, 9],
[3, 2, 8, 11, 19, 22, 36, 40, 37, 23, 20, 10],
[2, 1, 6, 12, 20, 24, 35, 44, 41, 25, 18, 9],
[3, 2, 7, 10, 21, 23, 37, 47, 39, 26, 19, 11],
[4, 2, 6, 12, 22, 26, 39, 50, 46, 31, 24, 14],
[11, 3, 10, 21, 24, 36, 38, 52, 51, 37, 22, 4],
[9, 4, 3, 8, 16, 29, 34, 45, 40, 30, 21, 6],
];
const WEEK: Slot[] = DAYS.flatMap((day, d) =>
HOURS.map((hour, h) => ({ day, hour, orders: ORDERS[d]?.[h] ?? null })),
);
export function HeatmapDetailsExample(): JSX.Element {
return (
<Heatmap
title="Orders by hour"
data={WEEK.map((slot) =>
slot.day === 'Sun' && Number(slot.hour) < 10 ? { ...slot, orders: null } : slot,
)}
x="hour"
y="day"
value="orders"
tableView="details"
labels={{ row: 'Day', missing: 'Closed' }}
/>
);
}
Status
Loading, empty and failed: the same box, Figma's seven-day height.
import type { JSX } from 'react';
import { Heatmap } from '@nizivo/charts/heatmap';
interface Slot {
day: string;
hour: string;
orders: number | null;
}
const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const HOURS = ['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22'];
// Figma 03's heatmap: orders per two hours, a week, busiest at midday and in the evening.
const ORDERS: number[][] = [
[2, 1, 1, 9, 18, 21, 24, 52, 38, 22, 12, 9],
[3, 2, 8, 11, 19, 22, 36, 40, 37, 23, 20, 10],
[2, 1, 6, 12, 20, 24, 35, 44, 41, 25, 18, 9],
[3, 2, 7, 10, 21, 23, 37, 47, 39, 26, 19, 11],
[4, 2, 6, 12, 22, 26, 39, 50, 46, 31, 24, 14],
[11, 3, 10, 21, 24, 36, 38, 52, 51, 37, 22, 4],
[9, 4, 3, 8, 16, 29, 34, 45, 40, 30, 21, 6],
];
const WEEK: Slot[] = DAYS.flatMap((day, d) =>
HOURS.map((hour, h) => ({ day, hour, orders: ORDERS[d]?.[h] ?? null })),
);
export function HeatmapStatusExample(): JSX.Element {
return (
<div>
<Heatmap
title="Orders by hour"
status="loading"
data={[] as Slot[]}
x="hour"
y="day"
value="orders"
/>
<Heatmap title="Orders by hour" data={[] as Slot[]} x="hour" y="day" value="orders" />
<Heatmap title="Orders by hour" status="error" data={WEEK} x="hour" y="day" value="orders" />
</div>
);
}
The ramp
The colours are Figma's: chart/primary at 8, 25, 45, 70 and 100 %, over the surface in light and
dark. heatmapStep(value, max) is the rule, exported so a server can bucket the same way:
- Five equal widths over 0 to the largest value: 0 to 20 % of it is step 1, up to 40 % step 2, and so on, the largest value step 5.
- 0 and negative values are step 1; when the largest value is 0, every value is step 1.
- A value that is not a finite number is a cell with no value: drawn as a hairline only, and
labels.missing("—") in the table.
Rows and columns come in the order they first appear in data (heatmapGrid).
Props
Server component. Props: HeatmapProps<T>. Source: src/heatmap/heatmap.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
datarequired | readonly T[] | – | One datum per cell; rows and columns come in the order they first appear. |
empty | ReactNode | – | Replaces the default empty message. From |
error | ReactNode | – | Replaces the default error message, for example an InlineError with a retry action. From |
format | Intl.NumberFormatOptions | Intl.NumberFormat's own ({}) | Values in the data table. |
labels | Partial<HeatmapLabels> | – | |
locale | string | 'en-US' | BCP 47 locale every number is formatted in, once, on the server (§16.4). |
pending | boolean | – | A refresh is in flight while the chart shows data (§16.5): From |
render | RenderProp<ChartFrameState> | – | From |
status | DataStatus"ready""loading""empty""error" | 'ready'; a ready chart with no table rows renders as `'empty'`. | From |
summary | ReactNode | – | One line under the title, for example "Daily sales, Oct 1–30, vs previous period". From |
tableView | ChartTableView"hidden""details" | 'hidden' | From |
titlerequired | ReactNode | – | The figure's caption and its accessible name. From |
titleVisibility | ChartTitleVisibility"visible""hidden" | 'visible' |
From |
valuerequired | Accessor<T, number | null | undefined> | – | Its value; one that is not a finite number is a cell with no value. |
xrequired | Accessor<T, string> | – | The cell's column (for example an hour): a key of the datum, or a |
xTicks | ChartXTicks | { max: 6 } | The column labels under the grid: |
yrequired | Accessor<T, string> | – | The cell's row (for example a day). |
- Also takes every
<figure>attribute exceptchildren,title.
render takes an element, or, in client code, a function of the props and this state (ChartFrameState):
| Name | Type | Default | Description |
|---|---|---|---|
interactionrequired | ChartInteractionMode"nizivo""none" | – | As passed: whether the child claims keyboard access to its points. |
pendingrequired | boolean | – | |
statusrequired | DataStatus"ready""loading""empty""error" | – | The status as rendered: |
x (the column, such as an hour), y (the row, such as a day) and value are an Accessor, as in
every chart. xTicks labels every every-th column, or at most max of them (default 6: every
other column of twelve).
labels takes Partial<HeatmapLabels>, on top of ChartFrame's:
| Key | Default |
|---|---|
row | Row |
missing | — |
fewer | Fewer |
more | More |
Other exports
HEATMAP_STEPSconstantimport { HEATMAP_STEPS } from '@nizivo/charts/heatmap'The ramp's steps: chart/primary at 8, 25, 45, 70 and 100 % (Figma).
const HEATMAP_STEPS: numberHeatmapLabelstypeimport type { HeatmapLabels } from '@nizivo/charts/heatmap'Heatmap's strings (§16.4), with ChartFrame's. Never concatenated: each is a whole label.
Name Type Default Description fewerrequiredstring– Before the scale's lightest swatch.
missingrequiredstring– In the table for a cell with no value.
morerequiredstring– After its strongest swatch.
rowrequiredstring– The data table's corner header, over the row names (for example "Day").
HeatmapPropstypeimport type { HeatmapProps } from '@nizivo/charts/heatmap'Heatmap's props: its data and formats in the charts' one shape (
_core/api.ts) and ChartFrame's, the<figure>'s attributes included. It draws its own labels and scale, is never mirrored and makes no keyboard claim, soaxes,legend,xDirectionandinteractionare its own.The props of
Heatmap, listed above.heatmapGridfunctionimport { heatmapGrid } from '@nizivo/charts/heatmap'The rows and columns in the order they first appear, and each cell's value.
function heatmapGrid(cells: ReadonlyArray<{ row: string; column: string; value: number | null; }>): { rows: string[]; columns: string[]; values: Map<string, number | null>; max: number; }heatmapLabelsconstantimport { heatmapLabels } from '@nizivo/charts/heatmap'English defaults (§16.4); pass
labelsto translate per instance.const heatmapLabels: HeatmapLabelsheatmapStepfunctionimport { heatmapStep } from '@nizivo/charts/heatmap'A value's step on the ramp, 1 to 5, or
nullfor no value: five equal widths over 0 tomax. 0 and below are step 1,maxis step 5; withmaxat 0 or below every value is step 1.function heatmapStep(value: number | null, max: number): number | null
Data attributes and slots
Inside the ChartFrame root, which carries
nz-heatmap (with axes="none" and interaction="none"):
| Element | Attributes |
|---|---|
div.nz-heatmap-grid | aria-hidden: the rows, the column labels and the scale |
span.nz-heatmap-cell | data-slot="heatmap-cell", data-step="1"…"5"; no data-step when the cell has no value |
div.nz-heatmap-scale | data-slot="heatmap-scale": "Fewer", five swatches (data-step), "More" |
CSS hooks
| Hook | Default | Read as |
|---|---|---|
--nz-heatmap-color | --nz-chart-primary |
|
Layers: nz.components. Classes: .nz-chart-frame-body, .nz-heatmap, .nz-heatmap-cell, .nz-heatmap-column, .nz-heatmap-grid, .nz-heatmap-label, .nz-heatmap-row, .nz-heatmap-scale, .nz-heatmap-swatch.
Tokens this stylesheet reads (11)
--nz-border-subtle, --nz-border-width-default, --nz-chart-primary, --nz-font-label-m, --nz-font-mono-m, --nz-radius-xs, --nz-space-1-5, --nz-space-2, --nz-space-4, --nz-text-tertiary, --nz-tracking-label-m
Cells are square, at most 32 px, and shrink together in a narrow chart. While loading, empty or failed the body keeps Figma's seven-row height.
FunnelChart
Steps in order, as bars of each step's share of the first, with the counts and the share of the step before as text. Drawn on the server, with no client JavaScript.
Sparkline
An inline trend line, 112 × 36, drawn on the server. Decorative by default, one named image with label; its colour follows the trend the data shows, and down is neutral grey, not red.