HeatmapChart

A matrix of cells coloured by magnitude on a single-hue sequential ramp, for spotting concentration across two categorical dimensions. Ships a ramp legend, since a cell's colour can't be read back to a number without one.

html
<UiHeatmapChart :data="..." :xKey="..." :yKey="..." :valueKey="..." :ariaLabel="..." />

Needs the charts module

This component ships in @jgastager/bundy-ui/charts, a separate module so projects that never draw a chart don't carry the paint classes these need. Nothing extra to install — just add the module entry. See Charts.


Data

required

data is one object per cell, not one per row of the matrix — the grid is reconstructed from the xKey/yKey pairs you supply. A combination you leave out becomes a gap rather than an empty cell, which is the honest rendering of “not measured”; don't fill it with a zero, since that claims a measurement you don't have. Wednesday morning is missing below.

24 sessions180 sessions

A cell you don't supply a row for is simply absent. That's the honest rendering of "not measured" — filling the gap with a zero claims a measurement you don't have.

X key

required

xKey names the field holding each cell's column. It's a plain key into your objects, so the chart never has to know the shape of your data beyond the three fields you point it at.

24 sessions180 sessions

Y key

required

yKey names the field holding each cell's row. Rows read top to bottom in the order they first appear in data, so sort the array if the order matters — a time-of-day axis wants morning at the top whatever order the rows arrived in.

24 sessions180 sessions

Value key

required

valueKey names the numeric field that picks each cell's shade. It has to be a number — the ramp is a quantitative encoding, so a string that happens to look numeric has nowhere to sit on the scale.

24 sessions180 sessions

ARIA label

required

ariaLabel is required rather than optional: a matrix of coloured rectangles is completely opaque to a screen reader, so this is the only description of the chart that exists. Describe what the shape shows — where the concentration is — rather than naming the chart type.

24 sessions180 sessions

ARIA description

ariaDescription carries the detail the label shouldn't — the range, the sampling, an anomaly worth naming. It's read after the label, so write the two as a headline and a paragraph rather than repeating one inside the other.

Counts run from 24 sessions on Monday morning to 160 on Friday evening. Evenings are consistently the busiest slot on every day of the week.
24 sessions180 sessions

Color

color is the family the ramp is built from. One semantic name goes in and the shades are stepped out of it for you, so the ramp stays a single hue in lightness order by construction — which is what makes a cell readable as “more” rather than merely “different”. A multi-hue ramp would only say the latter.

Colors are the library's own semantic ones — primary, secondary, success, warning, error, info, pending, neutral — and any shade of them, primary-400 through primary-950, exactly as the bg-* utilities spell them. Set color per series (or per item) to pin one; leave it unset to take the next slot of the default sequence. neutral and secondary are never handed out automatically, so an unlabelled series never lands on them.

color is one semantic name, stepped into a ramp from a dim tint up to the full color. Building it that way rather than taking a list of hex steps keeps it a single hue in lightness order by construction — which is what makes a cell readable as "more" rather than merely "different".

Steps

steps sets how many shades the ramp is quantized into. Fewer steps make each band easy to name back to a range; more make the gradient smoother but the individual cells harder to read off the legend. Five is usually the ceiling for a legend somebody actually uses.

3 steps
24180
5 steps
24180
9 steps
24180

Ramp

ramp spells the shades out exactly, replacing the span generated from color and steps. Reach for it when that span is the wrong part of the family — a contribution graph wants its darkest step close to the page, which color alone can't express. List them dim to bright: the ordering *is* the encoding, and a list out of lightness order silently stops meaning “more”.

24 sessions180 sessions

ramp overrides the shades generated from color and steps, for when the generated span is the wrong part of the family — color="success" starts at 800, which is too light for a cell meaning "one" on a dark page. List them dim to bright; the ordering is the encoding, and a list out of lightness order stops meaning "more" without erroring.

Domain

domain is the pair of bounds the ramp spans, defaulting to the data's own extent. Pin it when several heatmaps need to be comparable with each other, or when zero should anchor the dim end so a quiet cell reads as quiet rather than as “lowest here”.

0 sessions200 sessions

domain pins the scale's ends. Without it each heatmap normalizes to its own extent, so two of them side by side are not comparable even when they look it.

Zero color

zeroColor paints cells that are exactly zero apart from the ramp entirely. Absence is a different kind of thing from “the least of something”: a day with no commits isn't a slow day, and giving it the dimmest step says it was. Setting it also re-spans the ramp over the non-zero values, so the counts that do exist get every step available to them.

zero-color takes cells at exactly zero out of the ramp. Absence and "the least of something" are different claims, and a calendar heatmap is where that bites: a day with no commits isn't a slow day. Setting it also re-spans the ramp over the non-zero values, so the counts that exist use every step rather than surrendering one to a shade nothing reaches.

Radius

radius rounds each cell's corners in pixels. Small cells want a small value or they turn into dots — a calendar heatmap sits around two, a five-by-three matrix like this one can take the default six.

X label

xLabel titles the horizontal axis, naming the dimension the columns cross. Worth adding whenever the tick labels alone are ambiguous — “Mon, Tue, Wed” could be a schedule or a history.

24 sessions180 sessions

Y label

yLabel titles the vertical axis. Together the two titles say what the matrix crosses, which is the one thing a heatmap can't show on its own — the cells encode magnitude, not what the axes mean.

24 sessions180 sessions

Value format

valueFormat turns a cell's number into the text shown in the tooltip and on the legend's two ends. It's where the unit belongs — “124 sessions” rather than a bare 124 — since nothing else on a heatmap says what's being counted.

24 sessions180 sessions

X format

xFormat takes a column key and returns its tick label — and returning an empty string drops that tick entirely. That escape hatch is what keeps a long axis readable: a calendar heatmap has a column per week and no room to name them all, so labelling only the weeks that open a month is the usual answer.

24 sessions180 sessions

x-format and y-format may return an empty string to drop a tick label, and the margins are reserved from the formatted text — so blanking one reclaims its room. That's how a year-long calendar heatmap labels twelve months across fifty-three columns instead of smearing all fifty-three.

Y format

yFormat is the same for the row labels — shorten them, or return an empty string to drop a tick on a matrix with more rows than the axis has room to name.

24 sessions180 sessions

Tooltip

tooltip is the panel that follows the pointer, on by default, and it's the only place a cell's exact number appears. Turn it off for a decorative tile where the shape is the whole point and there's nothing to look up.

24180

Hovering dims every cell except the one under the pointer to 0.7, rather than drawing a ring on it — a ring is a dot, which reads as a stray mark sitting on the cell.

The hover fade is a CSS transition over the library's own 250ms duration, so it costs nothing and honours prefers-reduced-motion. There's no animate prop and nothing tweens on a data change — the marks are plain SVG elements Vue patches directly.

Legend

legend is the ramp strip under the plot, on by default and worth keeping: a cell's shade can't be read back to a number from anything else on the chart. Drop it only alongside tooltip, when you've decided the chart is a texture rather than a reading.

24 sessions180 sessions

The ramp legend is on by default and worth keeping. A bar's length can be read back to a number from the axis; a cell's colour can't be read back from anything else.

Legend position

legendPosition moves the ramp legend above the plot instead of below it. Top works well when the legend is what a reader needs before the cells mean anything — an unfamiliar unit, or a ramp that isn't the obvious dim-to-bright.

24 sessions180 sessions

Guides

guides draws the axis labels and reserves the margins they need, and is on by default. Turning it off leaves a bare matrix filling the whole box — the shape for a dashboard tile, where the surrounding card already says what the axes are.

guides="false" drops the axes, ticks, labels and grid *and* the margins they reserve, so the marks fill the whole box. That plus a small height is the inline-sparkline shape — there's no separate component for it.

Height

height is the plot's height in pixels — width always comes from the container. It's ignored once aspectRatio is set, and it's kept through the loading and empty states, so a dashboard doesn't reflow as its panels resolve.

Aspect ratio

aspectRatio sizes the chart from its container's width instead of a fixed height — useful for a heatmap in a responsive grid, where a fixed height leaves cells squat on wide screens and cramped on narrow ones.

24 sessions180 sessions

Loading

loading replaces the plot with a skeleton of exactly the same height, so the page doesn't jump when the data lands. An empty data array, by contrast, gets the empty state — the two are separate conditions and worth driving separately.

00
No data to display
00

A commit history

The calendar heatmap, laid out the way a contribution graph is: one column per week, one row per weekday, a year across. A quiet day here is a real measured zero, not a gap — the opposite of the case below — so every day gets a cell. zero-color paints those apart from the ramp: a day with no commits isn't a slow day, and it shouldn't read as the palest green. The ramp then re-spans the days that do have commits, so the counts use every step available to them. ramp spells the shades out — dark green to bright — where the span generated from color alone would start too light for a cell that means "one commit". x-format returns an empty string for most columns so only the weeks that open a month are labelled; fifty-three labels would be a smear. Small cells want a small radius.

0 commits16 commits

API Reference

Generated from the component's source — props, slots and emits as the component actually declares them.

Props

Prop
Type
Default
Description
datarequired
Record<string, unknown>[]
One object per cell. A row you don't supply leaves a gap rather than an empty cell — the honest rendering of "not measured". Don't fill it with a zero.
xKeyrequired
string
Field holding each cell's column.
yKeyrequired
string
Field holding each cell's row.
valueKeyrequired
string
Field holding the numeric value that picks the cell's shade.
ariaLabelrequired
string
What the chart shows, for screen readers. Required: an unlabelled chart is unreadable.
string
A longer accessible description, for detail the label shouldn't carry.
ChartColorFamily
'primary'
The family the ramp is built from. Cells step through real shades of it, so the ordering lives in the lightness — which is what makes a cell readable as "more" rather than merely "different".
number
5
How many steps the ramp is quantized into.
ChartColorName[]
The exact shades to step through, replacing the one generated from `color` and `steps`. Reach for it when the generated span is the wrong part of the family — a contribution graph wants its darkest step close to the page, which `color` alone can't express. List them dim to bright: the ordering *is* the encoding, and a list out of lightness order silently stops meaning "more".
[number, number]
Lower and upper bound of the scale. Defaults to the data's own extent.
ChartColorName
Paint for cells that are exactly zero, taking them out of the ramp entirely. Absence is a different kind of thing from "the least of something", and a calendar heatmap is the case where that matters: a day with no commits isn't a slow day, and giving it the dimmest step of the ramp says it was. Set this to a `neutral` shade and the ramp re-spans the non-zero values, so the counts that do exist use every step available to them.
number
6
Corner radius on each cell, in pixels.
string
Title for the x axis.
string
Title for the y axis.
(value: number) => string
Formats the value in the tooltip and on the legend's ends.
(value: string) => string
Formats the column tick labels. Return an empty string to drop one. A calendar heatmap has a column per week and no room to name them all, so labelling only the weeks that open a month is the usual answer — the axis still says where you are without turning into a smear of text.
(value: string) => string
Formats the row tick labels. Same escape hatch for a matrix with many rows.
boolean
true
Shows the tooltip on hover.
boolean
true
Shows the ramp legend. A cell's shade can't be read back to a number without it.
"top" | "bottom"
'bottom'
Where the legend sits relative to the plot.
boolean
true
Draws the axis labels and reserves their margins.
number
320
Height of the plot in pixels. Ignored when `aspectRatio` is set.
number
Width-to-height ratio, sizing the chart from its container's width.
boolean
false
Replaces the plot with a skeleton of the same height.

Types

ts
interface ChartPlot {
    x: number;
    y: number;
    width: number;
    height: number;
    /** Right edge, since marks need it more often than the width. */
    right: number;
    /** Bottom edge, which is also the x axis' baseline. */
    bottom: number;
}
ts
type ChartColorFamily = "primary" | "secondary" | "success" | "warning" | "error" | "info" | "pending" | "neutral";
ts
type ChartColorName = ChartColorFamily | `${ChartColorFamily}-${ChartColorShade}`;