Skip to content
Nizivo UI

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

Orders by hourOrders per two hours, Oct 5–11
Day000204060810121416182022
Mon2119182124523822129
Tue328111922364037232010
Wed21612202435444125189
Thu327102123374739261911
Fri426122226395046312414
Sat1131021243638525137224
Sun9438162934454030216
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.

Orders by hour
Show data table
Day000204060810121416182022
Mon2119182124523822129
Tue328111922364037232010
Wed21612202435444125189
Thu327102123374739261911
Fri426122226395046312414
Sat1131021243638525137224
SunClosedClosedClosedClosedClosed2934454030216
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.

Orders by hour
Loading chart
Orders by hour

No data to show

Orders by hour

This chart could not be loaded

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:

  1. 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.
  2. 0 and negative values are step 1; when the largest value is 0, every value is step 1.
  3. 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.

NameTypeDefaultDescription
datarequiredreadonly T[]–

One datum per cell; rows and columns come in the order they first appear.

emptyReactNode–

Replaces the default empty message.

From ChartFrameProps.

errorReactNode–

Replaces the default error message, for example an InlineError with a retry action.

From ChartFrameProps.

formatIntl.NumberFormatOptionsIntl.NumberFormat's own ({})

Values in the data table.

labelsPartial<HeatmapLabels>–
localestring'en-US'

BCP 47 locale every number is formatted in, once, on the server (§16.4).

pendingboolean–

A refresh is in flight while the chart shows data (§16.5): aria-busy and data-pending.

From ChartFrameProps.

renderRenderProp<ChartFrameState>–

From ChartFrameAttributes.

statusDataStatus
"ready""loading""empty""error"
'ready'; a ready chart with no table rows renders as `'empty'`.

From ChartFrameProps.

summaryReactNode–

One line under the title, for example "Daily sales, Oct 1–30, vs previous period".

From ChartFrameProps.

tableViewChartTableView
"hidden""details"
'hidden'

From ChartFrameProps.

titlerequiredReactNode–

The figure's caption and its accessible name.

From ChartFrameProps.

titleVisibilityChartTitleVisibility
"visible""hidden"
'visible'

'hidden' hides the title and summary visually; they still name the figure.

From ChartFrameProps.

valuerequiredAccessor<T, number | null | undefined>–

Its value; one that is not a finite number is a cell with no value.

xrequiredAccessor<T, string>–

The cell's column (for example an hour): a key of the datum, or a (datum, index) function.

xTicksChartXTicks{ max: 6 }

The column labels under the grid: every columns, or at most max labels.

yrequiredAccessor<T, string>–

The cell's row (for example a day).

  • Also takes every <figure> attribute except children, title.

render takes an element, or, in client code, a function of the props and this state (ChartFrameState):

NameTypeDefaultDescription
interactionrequiredChartInteractionMode
"nizivo""none"
–

As passed: whether the child claims keyboard access to its points.

pendingrequiredboolean–
statusrequiredDataStatus
"ready""loading""empty""error"
–

The status as rendered: 'empty' when a ready chart has no rows.

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:

KeyDefault
rowRow
missing—
fewerFewer
moreMore

Other exports

HEATMAP_STEPS constant

import { HEATMAP_STEPS } from '@nizivo/charts/heatmap'

The ramp's steps: chart/primary at 8, 25, 45, 70 and 100 % (Figma).

const HEATMAP_STEPS: number

HeatmapLabels type

import type { HeatmapLabels } from '@nizivo/charts/heatmap'

Heatmap's strings (§16.4), with ChartFrame's. Never concatenated: each is a whole label.

NameTypeDefaultDescription
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").

HeatmapProps type

import 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, so axes, legend, xDirection and interaction are its own.

The props of Heatmap, listed above.

heatmapGrid function

import { 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; }

heatmapLabels constant

import { heatmapLabels } from '@nizivo/charts/heatmap'

English defaults (§16.4); pass labels to translate per instance.

const heatmapLabels: HeatmapLabels

heatmapStep function

import { heatmapStep } from '@nizivo/charts/heatmap'

A value's step on the ramp, 1 to 5, or null for no value: five equal widths over 0 to max. 0 and below are step 1, max is step 5; with max at 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"):

ElementAttributes
div.nz-heatmap-gridaria-hidden: the rows, the column labels and the scale
span.nz-heatmap-celldata-slot="heatmap-cell", data-step="1"…"5"; no data-step when the cell has no value
div.nz-heatmap-scaledata-slot="heatmap-scale": "Fewer", five swatches (data-step), "More"

CSS hooks

HookDefaultRead as
--nz-heatmap-color--nz-chart-primary
  • background on .nz-heatmap-cell[data-step]
  • background on .nz-heatmap-swatch[data-step]

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.

On this page