Heatmap
A grid of rows by columns, each cell shaded by its value: when something happens by weekday and hour, or how often it fails by suite and platform. The rows and the columns come from the app.
import { Heatmap } from '@adecore/charts';By weekday and hour
import { useState } from 'react';
import { Heatmap, type HeatmapCell } from '@adecore/charts';
import { formatClock, formatHour, formatNumber, formatWeekday, useFormatLocale } from '@adecore/ui/format';
import { eventsPerWeekdayAndHour } from '../shared/heatmap-data.ts';
const counts = eventsPerWeekdayAndHour();
const total = counts.flat().reduce((sum, count) => sum + count, 0);
export default function EventsByWeekdayDemo() {
useFormatLocale();
const [active, setActive] = useState<HeatmapCell | null>(null);
// 19 October 2026 is a Monday, so the rows start on Monday.
const rows = Array.from({ length: 7 }, (_, day) => {
const at = new Date(2026, 9, 19 + day);
return { key: day, label: formatWeekday(at, 'long'), short: formatWeekday(at) };
});
const columns = Array.from({ length: 24 }, (_, hour) => {
const at = new Date(2026, 9, 19, hour);
return { key: hour, label: formatClock(at), short: hour % 6 === 0 ? formatHour(at) : null };
});
return (
<div className="flex w-full max-w-2xl flex-col gap-2">
<Heatmap rows={rows} columns={columns} values={counts} label="Error events per weekday and hour" valueLabel="Events" onActiveChange={setActive} />
<p className="text-xs text-text-muted tabular-nums">
{active === null
? `${formatNumber(total)} events over 28 days, in your time zone`
: `${formatNumber(counts[active.row]![active.column]!)} events on ${rows[active.row]!.label} at ${columns[active.column]!.label}`}
</p>
</div>
);
}const rows = days.map((at, day) => ({ key: day, label: formatWeekday(at, 'long'), short: formatWeekday(at) }));
const columns = hours.map((at, hour) => ({ key: hour, label: formatClock(at), short: hour % 6 === 0 ? formatHour(at) : null }));
<Heatmap rows={rows} columns={columns} values={counts} label="Error events per weekday and hour" valueLabel="Events" />values holds a list per row, a value per column. Each row and column has a key, a label that the tooltip, the live region and the table read, and a short the axis writes in its place: Mon for Monday, or null to leave the axis blank there, as for every hour but each sixth. Write both with formatWeekday, formatHour and formatClock, so they follow the language and the region. The heatmap draws the days in the order given, so the app decides whether a week starts on Monday or on Sunday, and adds up its own counts in the reader's time zone.
Any grid
Failed checks per suite and platform, in the error tone, four steps, taller cells and a "Show values" toggle. One suite does not run on one platform, so its cell has no value.
import { Heatmap } from '@adecore/charts';
import { failuresPerSuiteAndPlatform, PLATFORMS, SUITES } from '../shared/heatmap-data.ts';
const rows = SUITES.map((suite) => ({ key: suite, label: suite }));
const columns = PLATFORMS.map((platform) => ({ key: platform, label: platform }));
const values = failuresPerSuiteAndPlatform();
export default function FailuresBySuiteDemo() {
return (
<div className="w-full max-w-xl">
<Heatmap
rows={rows}
columns={columns}
values={values}
label="Failed checks per suite and platform, last 50 runs"
valueLabel="Failed checks"
tone="error"
steps={4}
cellHeight={28}
table
/>
</div>
);
}<Heatmap
rows={suites}
columns={platforms}
values={failures}
label="Failed checks per suite and platform, last 50 runs"
valueLabel="Failed checks"
tone="error"
steps={4}
cellHeight={28}
table
/>Shades
A cell is the color of tone mixed over the surface by the share of its value in max, the highest value unless the app gives one: 15% plus that share of the other 85%, so the smallest count still stands apart from an empty cell and the highest is the full color. With steps the values fall into that many shades instead, from 15% for the first to 100% for the last, which reads better when a few levels matter more than the exact value. An empty cell, for a zero, a value below zero or a missing value (null), is --surface-sunken. The card and the live region write a missing value as "No data". Give two heatmaps the same max to compare them.
The legend under the grid shows the empty cell and a strip that runs from the lightest to the full color between "Less" and "More", or every shade when steps is set. legend={false} leaves it out.
Keyboard and screen reader
The grid is one tab stop, a group with the role description "heatmap" and label as its name. The cell under the pointer or the keyboard gets an outline and a card with its row, its column and its exact value.
| Key | Moves to |
|---|---|
| Arrow keys | The next cell in that direction |
| Home, End | The first or the last cell of the row |
| Control with Home or End | The first or the last cell of the grid |
| Escape | Nowhere: it clears the cell |
A polite live region reads each cell the keyboard moves to: "Monday, 09:00, Events 41". The values are also in a table for a screen reader; table adds a "Show values" toggle that shows it to everyone.
onActiveChange hears the cell under the pointer or the keyboard as a HeatmapCell, its row and column as indexes into rows and columns, and null once it leaves the grid. The first demo uses it to let the line under the grid follow the cell.
Props
| Prop | Type | Default | |
|---|---|---|---|
rows, columns | readonly HeatmapAxisItem[] | Required. { key, label, short? }. | |
values | readonly (readonly (number | null)[])[] | Required. By row, then by column. | |
label | string | Required. The name of the heatmap. | |
valueLabel | string | Required. What a value counts, such as "Events". | |
tone | ChartColor | 'accent' | |
steps | number | Shades besides the empty cell; without it the shade follows the value. | |
max | number | the highest value | The value of the full color. |
format | (value: number) => string | formatNumber | Writes a value in the card, the live region and the table. |
cellHeight | number | 16 | Pixels. The width of a cell follows the container. |
legend | boolean | true | |
table | boolean | false | Shows the "Show values" toggle. |
onActiveChange | (cell: HeatmapCell | null) => void | Hears the cell under the pointer or the keyboard. | |
className, ref | On the outer <div>. |
The props are HeatmapProps.