Skip to main content

Heatmap

Info:Metrics
27

A 2D intensity grid (the activity-calendar / punch-card shape), colored on a theme-derived ramp, with a per-cell readout and opt-in clickable cells.

heat map activity calendar punch card contribution graph intensity grid

Live demo

live · @xtyle/astro

Heatmap

Activity grid

Runs per hour across a week on the default accent ramp, the contribution-graph shape. The scale key reads a cell's shade as a magnitude at a glance; hover or focus a cell for its exact value.

Activity by day and hour
036912151821
Mon1623231710814212453464246535855484244172323169
Tue231710814212419124246535855484244515723169915
Wed10814212419128125358554842445157575099152224
Thu14212419128121924554842445157575043431522241811
Fri2419128121924211442445157575043434956241811813
Sat8481520171046131937302323293638322522273438
Sun815201710461319191223232936383225222734383427

Thermal scale with values

scheme="thermal" runs cells cool to hot, and showValues prints each number in its cell.

Load by host and resource
CPUMemNetDisk
web-14230189
web-226552214
db-11240861

Selectable cells

selectable makes each cell a button that fires select on click or Enter/Space, carrying { row, col, value, … } for a drill-in.

Runs per bucket
AMPMNight
Mon381
Tue520
Wed064

Selected: (click a cell)

Two-channel glow

A second glow matrix carries a second metric as a per-cell halo: the fill reads run count while the glow reads total runtime, so a bucket can be dim-but-glowing (few long runs) or bright-but-quiet (many short ones).

Runs (fill) and total runtime (glow) by day and bucket
MorningMiddayEveningNight
Mon616117
Tue111431
Wed5727
Thu1111011
Fri81794

Current-cell marker and per-cell titles

current rings the live cell (currentPulse breathes it), the "you are here" on a time grid, and titles gives each cell a fuller hover readout that doubles as its accessible name. Hover the grid to read a cell's detail; the pulsing ring marks Wed / Midday as now.

Runs per bucket, this week (Wed Midday is now)
MorningMiddayEveningNight
Mon616117
Tue111431
Wed5727
Thu1111011
Fri81794

Categorical heat-grid

categorical colors each column its own hue from a SeriesScheme (skittles here) and washes it up by value, so a grid whose columns are unrelated categories reads by hue rather than one shared ramp. It shares the palette the Bar and Pie charts use; scale keys the hues in a legend.

Weekly work by category
BugsFeaturesDocsChores
Week 18315
Week 22643
Week 35172
Week 43926

Empty state

An empty matrix shows a muted No data message in place of the grid, so a no-activity window reads as intentional rather than a broken render.

Heatmap renders a matrix of values as a grid of colored cells, each cell's fill scaled by its value on an intensity ramp resolved off the live theme, the activity-calendar / punch-card shape (runs per hour over a week, load by host and resource, a GitHub-style contribution grid). Feed it a dense values matrix (row-major) plus optional rows and cols labels; the default accent ramp washes from a faint surface up to the accent, and thermal or status (or an explicit color array) swap the scale.

Set categorical to color by category instead of intensity: each column (or each row, with categoryAxis="row") takes a distinct hue from a categorical SeriesScheme (accents / skittles / statuses), and the cell value washes that hue from the surface up. One grid, then, reads its categories by hue and their magnitude by fill: the categorical heat-grid. Turn scale on and a legend keys the hues. Every cell's intensity is normalized against the data's own maximum, or an explicit max so several grids share one scale. It's interactive: hovering or focusing a cell floats a value readout, and the whole grid is mirrored into a visually-hidden data table so assistive tech reads the numbers, not the pixels. Set selectable to make cells a drill-in surface, each firing a select event with its row, column, and value. showValues prints the number in each cell. Feed an optional second glow matrix (the same shape as values) to carry a second signal in one grid: the fill reads one metric while a per-cell halo reads another (run count vs total runtime, requests vs error rate). An empty matrix shows a muted No data message in place of the grid.

When to use

How this component composes with the rest of the set.

The cell colors derive off the same register as the rest of the UI, so a grid matches its surrounding chrome without hand-picking a scale. accent is the neutral default (the contribution-graph wash); reach for thermal when the data is a true low-to-high magnitude and the extra hue helps read it.
Reach for categorical when the grid's axis is a set of unrelated categories rather than one low-to-high magnitude: each column (or categoryAxis="row" each row) takes its own hue from a SeriesScheme, and the cell value washes it from the surface up, so a viewer reads which category by hue and how much by fill in one glance. It shares the same seriesPalette the Bar and Pie charts color with, so a categorical grid stays coherent with the rest of a dashboard; a continuous ramp (accent / thermal) is the wrong tool for distinct categories and vice versa.
Pin max when two grids should be read against each other, so a cell of the same value takes the same color in both.
Turn on scale for a color-scale key so a cell's shade reads as a magnitude at a glance, not only on hover, the way a contribution graph carries its own low-to-high legend. The swatches sample the same ramp as the cells, so the key can't drift from what the grid shows.
Carry two metrics in one grid with glow: the fill reads the primary metric (say, run count) while a per-cell halo reads a second (total runtime), so a cell can be dim-but-glowing (few long runs) or bright-but-quiet (many short ones). The halo takes the ramp's hot-end color, so it stays theme-coherent; pin glowMax to share one glow scale across grids.
Make a grid a drill-in with selectable and a select listener: click a cell to open that bucket's detail (an hour's runs, a region's orders). The event's detail names the exact row, column, and value clicked, so the handler needs no lookup. It composes with the same select seam Bar carries.
Mark "now" on a time grid with current: a [rowIndex, colIndex] list rings those cells (the live hour, today's column), currentTone tints the ring, and currentPulse breathes it so a live position reads as live. It's a decorative overlay on top of the value fill, so it composes with any scheme, glow, or selectable.
Give cells richer hover detail with titles, a matrix parallel to values: a titles[row][col] becomes that cell's readout and accessible name, so a cell can spell out a full timestamp and breakdown ("Wed PM: 6 runs, 2 failed") instead of a bare number, without a bespoke tooltip layer.

Props

20 props, straight from the manifest.

PropTypeDefaultBindingsDescription
values number[][]
html svelte astro
The data as a dense row-major matrix: `values[row][col]`. JS property in html/svelte, JSON attribute or prop in astro.
rows string[]
html svelte astro
Row (y-axis) labels, one per matrix row; omit for an unlabeled grid.
cols string[]
html svelte astro
Column (x-axis) labels, one per matrix column; omit for an unlabeled grid.
scheme RampScheme | string[]
accent thermal status
accent
html svelte astro
The intensity ramp: `accent` (a faint-surface-to-accent wash), `thermal` (cool to hot), `status` (success to danger), or an explicit array of stop colors interpolated in OKLCH.
reverse boolean false
html svelte astro
Flips the ramp end for end (a high value takes the low color and vice versa).
max number
html svelte astro
The intensity ceiling a full-strength cell maps to; defaults to the data's own maximum. Set it so several heatmaps share one scale.
glow number[][]
html svelte astro
An optional second intensity matrix (same shape as `values`) driving a per-cell glow halo, so one grid carries two signals: the fill reads one metric and the halo strength reads another (run count vs total runtime, requests vs error rate). Each cell's glow normalizes against the glow data's own maximum (or `glowMax`); a cell with zero glow gets no halo. The halo takes the ramp's hot-end color off the live theme. JS property in html/svelte, JSON attribute or prop in astro.
glowMax number
html svelte astro
The glow-channel ceiling a full-strength halo maps to; defaults to the glow data's own maximum. Set it so several grids share one glow scale. Kebab `glow-max` on the element.
glowBlur number 7
html svelte astro
The px blur a full-strength glow halo reaches. The default suits a roomy grid; lower it on a dense (contribution-graph) grid so neighboring halos don't merge, raise it on a sparse one. Kebab `glow-blur` on the element.
glowLabel string "glow"
html svelte astro
Names the glow metric so its value reaches each cell's accessible name and its hover readout (e.g. `glowLabel="runtime"` reads "Mon, Midday: 8, runtime 42"), keeping the second channel from being visual-only. Kebab `glow-label` on the element.
current [number, number][]
html svelte astro
Cells to ring as the current / "now" marker, each a `[rowIndex, colIndex]` pair, the "you are here" on a time grid (the live hour on an activity strip, today on a contribution graph). JS property in html/svelte, JSON attribute or prop in astro.
currentTone "success" | "danger" | "warn" | "info" | "neutral"
success danger warn info neutral
html svelte astro
Tints the current-cell ring with a semantic tone; defaults to the accent. Kebab `current-tone` on the element.
currentPulse boolean false
html svelte astro
Pulses the current-cell ring so "now" reads as live, not just marked; honors `prefers-reduced-motion`. Kebab `current-pulse` on the element.
titles string[][]
html svelte astro
Per-cell hover text, the same shape as `values`: a non-empty `titles[row][col]` becomes that cell's floating readout and its accessible name, so a cell can carry richer detail (a full timestamp, a breakdown) than its bare value. Cells left empty fall back to the default row/column/value readout. JS property in html/svelte, JSON attribute or prop in astro.
scale boolean false
html svelte astro
Shows a key below the grid: an intensity mode grid gets five ramp swatches between the low (`0`) and high (`max`) value endpoints, and a `categorical` grid gets a legend of one swatch per category hue. The key derives off the same colors as the cells, so it always matches them.
categorical boolean false
html svelte astro
Colors by category instead of intensity: each column (or, with `categoryAxis="row"`, each row) takes a distinct hue from a categorical `SeriesScheme` read off `scheme` (`accents` / `skittles` / `statuses`; defaults to `accents`), and each cell washes that hue from the surface up by its value. The categorical heat-grid, for a matrix whose axis is a set of unrelated categories rather than one low-to-high magnitude. Turn on `scale` for a hue legend.
categoryAxis "col" | "row"
col row
"col"
html svelte astro
Which axis carries the categories in `categorical` mode: `col` (default) gives each column its own hue, `row` each row. Kebab `category-axis` on the element. Ignored unless `categorical` is set.
showValues boolean false
html svelte astro
Prints each cell's value inside it. Kebab `show-values` on the element.
selectable boolean false
html svelte astro
Makes cells actionable: each cell becomes a `role="button"` with a pointer cursor, and clicking one (or pressing Enter/Space while it's focused) fires a `select` `CustomEvent` whose `detail` carries `{ row, col, value, rowIndex, colIndex }`. `@xtyle/svelte` surfaces it as an `onselect` callback. Leave it off for a read-only grid.
label string
html svelte astro
An accessible name for the grid, used as the data table's caption.

Appearance

States

cell-hover

.xtyle-heatmap--hovering .xtyle-heatmap__cell.is-active

Pointer over or keyboard focus on a cell: its border brightens (the rest are never dimmed, so a dense grid never strobes) and a value readout floats above it.

cell-focus

.xtyle-heatmap__cell:focus-visible

Keyboard focus on a cell draws a token ring; each cell is a tab stop that announces its row, column, and value.

current

.xtyle-heatmap__cell--current

A cell flagged via current: a thicker accent (or toned) ring marks it as "now", pulsing when currentPulse is set (and reduced-motion is not requested).

Anatomy

The named parts that make up the component, with their selectors.

chart

.xtyle-heatmap

The <figure> root holding the SVG grid, tooltip, and the accessible data table.

--font-sans

cell

.xtyle-heatmap__cell

A single grid cell, filled by its value's intensity color; the focus/hover target.

--bg-0 --ring --border-thick --duration-fast --ease-standard

current-cell

.xtyle-heatmap__cell--current

A cell marked as the current / "now" position: ringed in the accent (or a chosen tone), optionally pulsing.

--accent --success --danger --warn --info --neutral --ease-standard

tooltip

.xtyle-heatmap__tooltip

The floating value readout shown on hover or focus of a cell.

--surface-overlay --surface-overlay-border --elevation-3 --radius-md --fg-0

scale

.xtyle-heatmap__scale

The optional color-scale key below the grid: five ramp swatches between the low and high value endpoints.

--space-2 --text-xs --fg-3 --bg-0 --border-thin

legend

.xtyle-heatmap__legend

The categorical key below the grid: one swatch per category hue, labeled off the category axis.

--space-1 --space-3 --text-xs --fg-2 --fg-3 --bg-0 --border-thin

Tokens & coverage

What the component consumes, checked live against what the algorithm produces.

Success:fully covered 27/27 consumed tokens produced default register: 299 tokens

Live coverage check against the xtyle-default register (derive(xtyleDefault, { anchors })coverComponent(manifest, register)). Every token this component consumes must be a key the algorithm produces.

--accent --bg-0 --border-thick --border-thin --danger --duration-fast --ease-standard --elevation-3 --fg-0 --fg-2 --fg-3 --font-sans --info --leading-normal --neutral --radius-md --ring --space-1 --space-2 --space-3 --success --surface-overlay --surface-overlay-border --text-sm --text-xs --warn --weight-semibold

Accessibility

The SVG is decorative (aria-hidden); the grid's data is mirrored into a visually-hidden <table> so assistive tech reads the actual numbers. label becomes the table's caption.
Each cell is a focusable tab stop with an aria-label naming its row, column, and value, so the grid is navigable by keyboard and the value readout appears on focus as well as hover.
With selectable, each cell becomes a role="button" (announced as actionable) and activates on Enter/Space as well as click; a read-only grid keeps its cells as role="img" data points.
Color is never the only channel: the value readout, the optional in-cell showValues, the optional scale key's numeric endpoints, and the data table all repeat the number.
In categorical mode the category never rests on hue alone: each cell's aria-label and the data table already name its row and column, so the category a hue stands for is read from the labels; the legend itself is decorative (aria-hidden), a visual shortcut over the axis labels that carry the meaning.
A titles[row][col] becomes that cell's aria-label, so the richer hover text is the cell's accessible name too, never a mouse-only affordance; the current-cell ring is decorative, so it never carries meaning on its own.
The glow halo is a visual channel, so it never carries its metric alone: when a glow matrix is present, each cell's glow value folds into its accessible name and its hover readout (named by glowLabel), so the second metric has a text path for assistive tech even though the halo itself is decorative. The summary data table still lists the primary (fill) metric.

Code

Activity grid

A week-by-hour activity grid on the default accent ramp, the contribution-graph shape.

<xtyle-heatmap
	rows='["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]'
	scale
	label="Activity by day and hour"></xtyle-heatmap>
<script>
	const grid = document.querySelector("xtyle-heatmap");
	grid.cols = Array.from({ length: 24 }, (_, h) => String(h));
	grid.values = weekByHour; // number[7][24]
</script>
<script lang="ts">
	import { Heatmap } from "@xtyle/svelte";

	const rows = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
	const cols = Array.from({ length: 24 }, (_, h) => String(h));
	const values = weekByHour; // number[7][24]
</script>

<Heatmap {rows} {cols} {values} scale label="Activity by day and hour" />
---
import { Heatmap } from "@xtyle/astro";
const rows = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const cols = Array.from({ length: 24 }, (_, h) => String(h));
const values = weekByHour; // number[7][24]
---

<Heatmap rows={rows} cols={cols} values={values} scale label="Activity by day and hour" />

Thermal scale with values

scheme="thermal" runs the cells cool to hot, and showValues prints each number in its cell.

<xtyle-heatmap
	scheme="thermal"
	show-values
	label="Load by host and resource"></xtyle-heatmap>
<script>
	const grid = document.querySelector("xtyle-heatmap");
	grid.rows = ["web-1", "web-2", "db-1"];
	grid.cols = ["CPU", "Mem", "Net", "Disk"];
	grid.values = [[42, 30, 18, 9], [26, 55, 22, 14], [12, 40, 8, 61]];
</script>
<script lang="ts">
	import { Heatmap } from "@xtyle/svelte";

	const rows = ["web-1", "web-2", "db-1"];
	const cols = ["CPU", "Mem", "Net", "Disk"];
	const values = [[42, 30, 18, 9], [26, 55, 22, 14], [12, 40, 8, 61]];
</script>

<Heatmap {rows} {cols} {values} scheme="thermal" showValues label="Load by host and resource" />
---
import { Heatmap } from "@xtyle/astro";
const rows = ["web-1", "web-2", "db-1"];
const cols = ["CPU", "Mem", "Net", "Disk"];
const values = [[42, 30, 18, 9], [26, 55, 22, 14], [12, 40, 8, 61]];
---

<Heatmap rows={rows} cols={cols} values={values} scheme="thermal" showValues label="Load by host and resource" />

Selectable cells

selectable makes each cell a button that fires select on click or Enter/Space, so a grid can drive a drill-in. The detail carries the row, column, value, and both indices.

<xtyle-heatmap
	scheme="accent"
	selectable
	label="Runs per hour"></xtyle-heatmap>
<script>
	const grid = document.querySelector("xtyle-heatmap");
	grid.rows = ["Mon", "Tue", "Wed"];
	grid.cols = ["AM", "PM", "Night"];
	grid.values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
	grid.addEventListener("select", (e) => {
		// { row, col, value, rowIndex, colIndex }
		openRuns(e.detail.row, e.detail.col);
	});
</script>
<script lang="ts">
	import { Heatmap } from "@xtyle/svelte";

	const rows = ["Mon", "Tue", "Wed"];
	const cols = ["AM", "PM", "Night"];
	const values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
</script>

<Heatmap
	{rows}
	{cols}
	{values}
	scheme="accent"
	selectable
	label="Runs per hour"
	onselect={(e) => openRuns(e.detail.row, e.detail.col)}
/>
---
import { Heatmap } from "@xtyle/astro";
const rows = ["Mon", "Tue", "Wed"];
const cols = ["AM", "PM", "Night"];
const values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
---

<Heatmap rows={rows} cols={cols} values={values} scheme="accent" selectable label="Runs per hour" />
<script>
	document.querySelector("xtyle-heatmap").addEventListener("select", (e) => {
		openRuns(e.detail.row, e.detail.col);
	});
</script>

Two-channel glow

A second glow matrix carries a second metric as a per-cell halo, so the fill reads run count while the glow reads total runtime, two signals in one grid.

<xtyle-heatmap
	scheme="accent"
	glow-label="runtime"
	label="Runs (fill) and total runtime (glow) by day and bucket"></xtyle-heatmap>
<script>
	const grid = document.querySelector("xtyle-heatmap");
	grid.rows = ["Mon", "Tue", "Wed"];
	grid.cols = ["AM", "PM", "Night"];
	grid.values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
	grid.glow = [[40, 12, 90], [8, 30, 0], [0, 15, 70]];
</script>
<script lang="ts">
	import { Heatmap } from "@xtyle/svelte";

	const rows = ["Mon", "Tue", "Wed"];
	const cols = ["AM", "PM", "Night"];
	const values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
	const glow = [[40, 12, 90], [8, 30, 0], [0, 15, 70]];
</script>

<Heatmap {rows} {cols} {values} {glow} glowLabel="runtime" scheme="accent" label="Runs (fill) and total runtime (glow)" />
---
import { Heatmap } from "@xtyle/astro";
const rows = ["Mon", "Tue", "Wed"];
const cols = ["AM", "PM", "Night"];
const values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
const glow = [[40, 12, 90], [8, 30, 0], [0, 15, 70]];
---

<Heatmap rows={rows} cols={cols} values={values} glow={glow} glowLabel="runtime" scheme="accent" label="Runs (fill) and total runtime (glow)" />

Current-cell marker and per-cell titles

current rings the live cell (with currentPulse to breathe it and currentTone to tint it), the "you are here" on a time grid, and titles gives a cell a fuller hover readout that doubles as its accessible name.

<xtyle-heatmap
	rows='["Mon","Tue","Wed"]'
	current-pulse
	label="Runs per bucket, this week"></xtyle-heatmap>
<script>
	const grid = document.querySelector("xtyle-heatmap");
	grid.cols = ["AM", "PM", "Night"];
	grid.values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
	// ring + pulse the live cell (Wed / PM), and give it a fuller hover readout
	grid.current = [[2, 1]];
	grid.titles = [[], [], [, "Wed PM: 6 runs · 2 failed · click to view"]];
</script>
<script lang="ts">
	import { Heatmap } from "@xtyle/svelte";

	const rows = ["Mon", "Tue", "Wed"];
	const cols = ["AM", "PM", "Night"];
	const values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
	const current = [[2, 1]];
	const titles = [[], [], [, "Wed PM: 6 runs · 2 failed · click to view"]];
</script>

<Heatmap {rows} {cols} {values} {current} {titles} currentPulse label="Runs per bucket, this week" />
---
import { Heatmap } from "@xtyle/astro";
const rows = ["Mon", "Tue", "Wed"];
const cols = ["AM", "PM", "Night"];
const values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
const current = [[2, 1]];
const titles = [[], [], [, "Wed PM: 6 runs · 2 failed · click to view"]];
---

<Heatmap rows={rows} cols={cols} values={values} current={current} titles={titles} currentPulse label="Runs per bucket, this week" />

Categorical heat-grid

categorical colors each column a distinct hue from a SeriesScheme (skittles here) and washes it up by value, so a grid whose columns are unrelated categories reads by hue, not one shared ramp. scale keys the hues in a legend.

<xtyle-heatmap
	scheme="skittles"
	categorical
	scale
	label="Weekly totals by category"></xtyle-heatmap>
<script>
	const grid = document.querySelector("xtyle-heatmap");
	grid.rows = ["Week 1", "Week 2", "Week 3"];
	grid.cols = ["Bugs", "Features", "Docs", "Chores"];
	grid.values = [[8, 3, 1, 5], [2, 6, 4, 3], [5, 1, 7, 2]];
</script>
<script lang="ts">
	import { Heatmap } from "@xtyle/svelte";

	const rows = ["Week 1", "Week 2", "Week 3"];
	const cols = ["Bugs", "Features", "Docs", "Chores"];
	const values = [[8, 3, 1, 5], [2, 6, 4, 3], [5, 1, 7, 2]];
</script>

<Heatmap {rows} {cols} {values} scheme="skittles" categorical scale label="Weekly totals by category" />
---
import { Heatmap } from "@xtyle/astro";
const rows = ["Week 1", "Week 2", "Week 3"];
const cols = ["Bugs", "Features", "Docs", "Chores"];
const values = [[8, 3, 1, 5], [2, 6, 4, 3], [5, 1, 7, 2]];
---

<Heatmap rows={rows} cols={cols} values={values} scheme="skittles" categorical scale label="Weekly totals by category" />