Heatmap Chart
ProRow × column heatmap with 4 variants (default, calendar, blocks, glow), 9 color schemes (viridis, magma and cividis sampled from the published colormaps, plus six single-hue ramps), a no-jump tooltip and ambient hover glow. Pure CSS.
import { SvHeatmapChartComponent } from '@styloviz-pro/charts';Demo & usage
Basic usage
ProUsage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Scheme + values
ProCell interactions
ProWhen to use
- Showing intensity across two categorical dimensions (hour × day, region × metric).
- You want to spot hot/cold patterns in a matrix at a glance.
- For date-based daily activity, use the Heatmap Calendar.
Async states
- loading
- empty
- error
- retry
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| cells | readonly HeatmapCell[] | [] | Matrix cells: { row, col, value }. |
| rowLabels | readonly string[] | ['Mon','Tue','Wed','Thu','Fri','Sat','S… | Row (Y-axis) labels. |
| colLabels | readonly string[] | Array.from({ length: 24 }, (_, i) => (i… | Column (X-axis) labels. |
| colorScheme | HeatmapColorScheme | 'blue' | Colour ramp. `viridis`, `magma` and `cividis` are perceptually uniform — sampled at eleven stops from the matplotlib tables, so equal steps in value are equal steps in CAM02-UCS lightness; they stay monotonic in greyscale, and `cividis` is built to read the same way with deuteranopia. The six named hues are single-hue lightness ramps: better than a rainbow, but their steps are not evenly spaced perceptually, so the middle of the scale compresses. The default stays `blue` rather than moving to `viridis`, because a default is the one thing a host cannot opt out of having already shipped: changing it would restyle every existing dashboard without anybody asking. Reach for `viridis` on anything where the reader has to judge magnitude from colour rather than just spot the hot corner. |
| variant | HeatmapVariant | 'default' | Visual variant: default, calendar, blocks or glow. |
| cellSize | number | 22 | Cell size in pixels. |
| gap | number | 2 | Gap between cells in pixels. |
| showValues | boolean | false | Render the numeric value inside each cell. |
| title | string | '' | Chart title. |
| minLabel | string | 'Low' | Label for the low end of the scale. |
| maxLabel | string | 'High' | Label for the high end of the scale. |
| ariaLabel | string | '' | Accessible name override for the grid. Falls back to a generated summary. |
| customClass | string | '' | Extra CSS classes applied to the root wrapper. |
| loading | boolean | false | True while the matrix is being fetched. |
| error | SvDataError | string | null | null | Load failure. A bare string is accepted; use `toSvDataError()` to normalize an `HttpErrorResponse` and get sensible retry behaviour. |
| emptyMessage | string | 'No data to plot' | Copy for the empty state. |
| filtered | boolean | false | Distinguishes "no data at all" from "nothing matched the current filters". |
| valuePolicy | SvValuePolicy | 'render' | What to do with a square whose reading this chart cannot plot — `NaN` from a blank source cell, `Infinity` from a division by zero. A negative is NOT one of these: this is a colour ramp, so −12 °C sits on it perfectly well. - `render` (default) — draw the square as missing: a hatch, a focusable stop that announces "no reading", and a key for the hatch in the legend. This is the honest answer here, and the one a pie cannot offer, because a matrix already has a way to say "this square has no colour". - `omit` — leave the square blank, as though the matrix had a hole there, and disclose how many. - `reject` — refuse the matrix and show the error state. `clamp` is not available: a missing reading pushed to 0 lands on a real point of the ramp and looks like a measurement. |
| cellCeiling | number | 2500 | How many squares this chart will draw before it starts telling the reader it is past what an SVG matrix does well. 2,500 is a 50 × 50 grid. It is a judgement, not a measurement of any one machine, and it is an input precisely because the right number depends on the host's cell size, its device and how much else is on the page. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| cellHover | HeatmapCellEvent | Emitted when a cell is hovered or keyboard-focused. |
| cellSelect | HeatmapCellEvent | Emitted when a cell is clicked or activated via keyboard (Enter/Space). |
| retry | void | The host should re-run the failed load. |
Wire it to your API
This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.
Template
ProBound to the names this component actually exposes.
Host class
ProDrive the three async inputs from your own fetch.
- This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `cells`, `loading` and `error`.
- Set `filtered` when a search or filter is active. It is the difference between "you have no orders" and "no orders match this filter" — telling a customer with 4,000 orders the first one reads as a bug.
- Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.
Full walkthrough: Connecting to a backend.
Customization & theming
- Four variants and nine colour schemes: three perceptually uniform ramps (viridis, magma, cividis) and six single-hue ones. The uniform three run dark-to-bright already, so they are not re-picked for the dark glow variant — doing that would break the very uniformity they are chosen for.
- Configurable cell shape and scale.
- Ambient glow on hover.
- `valuePolicy` decides what happens to a square whose reading this chart cannot plot — `NaN` from a blank source cell, `Infinity` from a division by zero. A negative is not one of these: this is a colour ramp, so −12 °C sits on it perfectly well. `render` (default) draws the square as missing — a hatch, plus a key for the hatch in the legend — which is the honest answer a pie cannot offer, because a matrix already has a way to say "this square has no colour". `omit` leaves a hole and discloses the count; `reject` refuses the matrix. `clamp` is not offered: a missing reading pushed to 0 lands on a real point of the ramp and looks like a measurement.
Best practices
- Do: Pick a perceptually uniform colorScheme — viridis, magma or cividis — so intensity reads linearly. Each is eleven stops sampled from the published matplotlib tables, so every tenth of the ramp is an even step (within ±8%) in CAM02-UCS lightness, the space they were designed in; the spec recomputes it. They used to be three stops joined in sRGB, which kept the ends right and bent the middle — viridis's dark end rose a quarter as fast as its centre. This advice used to have nothing to point at: the six named hues are single-hue lightness ramps, better than a rainbow but not evenly spaced in perceptual terms, so an equal difference in value did not produce an equal difference in appearance and the middle of the scale compressed.
- Do: Reach for cividis when the reader may have a colour-vision deficiency, and for viridis on anything where magnitude has to be judged from colour rather than the hot corner merely spotted. All three stay monotonic in greyscale, so they survive being printed — a ramp that is not stops encoding magnitude the moment it leaves the screen.
- Tip: The default stays blue rather than moving to viridis. A default is the one thing a host cannot opt out of having already shipped, and changing it would restyle every existing dashboard without anybody asking.
- Do: Provide min/max labels or a legend for the color scale.
- Tip: Enable `showValues` for small matrices where exact numbers matter.
Accessibility
- Each cell is a keyboard-focusable `role="button"` with a row/column/value `aria-label`; the grid is a labelled `role="group"`.
- Tooltip content is available on focus and Enter/Space emits `cellSelect`. Cells expose row, column and value.
- A missing square announces "no reading" rather than a number, and is a focus stop like any other — the hatch is the visual half of the same fact, and the legend key names it in words so it is not colour-only.
- Whatever `valuePolicy` did is stated under the grid, not only in `console.warn`. One `NaN` used to reach the ramp interpolator and come back as the literal `#NaNNaNNaN`, which browsers discard — blanking the entire matrix with no error anywhere.
- Every square is its own tab stop, so a large matrix is a long walk by keyboard. Past `cellCeiling` squares (2,500 — a 50 × 50 grid) a note under the grid names the real count and that cost; the chart still draws every square. Aggregate or split a matrix that has to be navigable.
Feedback on Heatmap Chart
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.