HeatmapChart
A row × column grid encoding magnitude as fill intensity.
Quick Preview
Listing inquiries
By neighbourhood · Jan–Jun 2026
Usage
<HeatmapChart
title="Listing inquiries"
description="By neighbourhood · Jan–Jun 2026"
data={[
{ row: "Lekki", column: "Jan", value: 42 },
{ row: "Lekki", column: "Feb", value: 58 },
{ row: "Yaba", column: "Jan", value: 21 },
{ row: "Yaba", column: "Feb", value: 26 },
]}
/>Unlike the recharts-backed charts in this library, HeatmapChart is hand-rolled
— recharts has no heatmap/matrix primitive. It is one component (ChartCard
owned internally, like FunnelChart/TreeMap/BulletChart), not a set of
composable wrappers: pass data as a flat array of { row, column, value }
cells, and the grid, hover/focus tooltip, and sequential legend come for free.
Rows and columns default to first-seen order in data; pass explicit rows/
columns arrays to control ordering or to include categories with no data
(rendered as a dashed "no data" placeholder). Wide grids (many columns) scroll
horizontally inside the card rather than shrinking cells below a readable size.
Color encodes magnitude, so cells are --color-surface-brand at a
magnitude-scaled opacity — 8%, 22%, 44%, 70%, 100% across the five buckets —
rather than a distinct hue per step. surface-brand is already theme-aware,
so the ramp stays correct in both themes with no separate token set; never
the categorical chart-1…3 tokens, which encode identity, not amount.
Examples
Default HeatmapChart
A neighbourhood × month grid. Hover or focus a cell for the exact value; the legend under the grid anchors the color scale to the data's range.
Listing inquiries
By neighbourhood · Jan–Jun 2026
In-cell Values
Pass showValues to print the formatted value inside every cell — useful for
a small, dense matrix (like a collection-rate scorecard) where the reader
wants every figure at a glance, not just on hover. Pin domain so a scale
tied to real thresholds (0–100%) doesn't get stretched by whatever range the
current data happens to cover.
Repayment collection rate
By branch and quarter · % of scheduled repayments collected
Customizing the Grid
Pass explicit rows/columns to fix category order and coverage, pin
domain to a scale that stays comparable across datasets, and turn off
legend when the card's own caption already states the scale. A wide grid
(24 columns here) scrolls horizontally instead of shrinking.
Site visits
By day and hour · sessions, last 30 days
Props
| Prop/API | Type | Default | Description |
|---|---|---|---|
data | { row: string; column: string; value: number }[] | - | One entry per row/column pair that has a value. |
rows | string[] | first-seen order | Row order, top to bottom. |
columns | string[] | first-seen order | Column order, left to right. |
domain | [number, number] | data's [min, max] | Fixed bounds for the color scale. |
size | "sm" | "md" | "lg" | inherited | Cell size and label/legend scale. |
title | ReactNode | - | Chart heading. Omit to hide. |
description | ReactNode | - | Supporting metadata (period, source). Omit to hide. |
legend | boolean | true | Show the sequential-scale legend under the grid. |
showValues | boolean | false | Print the formatted value inside every cell. |
valueFormatter | (value: number) => string | grouped integer | Formats the tooltip, in-cell label, and legend bounds. |
className | string | - | Applied to the ChartCard. |
Accessibility
- Every valued cell is a focusable
buttonwith anaria-labelcarrying the row, column, and formatted value — screen readers get the exact figure without relying on the visual hover tooltip. - "No data" cells (a row/column pair absent from
data) are non-interactive dashed placeholders, skipped from tab order — there is nothing to announce. - Do not communicate state with color alone; the legend and every cell's
aria-label/tooltip carry the same figures the fill encodes. - Keep
legendon (the default) whenever the color scale's bounds aren't already stated elsewhere on the page.