GrChartHeatmap
Overview
When to take it
- a retention matrix by cohort — the rows are cohorts, the columns offsets: it is immediately visible on which month the curve flattens out;
- two categorical dimensions and one quantity — hours × days of the week, service × version, region × channel;
- the shape matters rather than the exact numbers — a patch and a gradient read faster than twenty cells with figures;
- the matrix is sparse —
nullleaves a cell empty, and “has not happened yet” is visible as an absence rather than as a zero; - a deviation from the norm in both directions — a diverging scale colours a shortfall and an excess with different roles around the middle.
When to take something else
| Need | Take |
|---|---|
| Compare the quantities of one dimension | GrChartBar |
| Show the course of a quantity over time | GrChartLine |
| Show exact numbers that will be read and sorted | GrDataTable |
| Show a profile across several axes | GrChartRadar |
Examples 4 each with a live demo and its code
Retention by monthly cohort
Rows are cohorts, columns are months since sign-up: the eye finds the month where the curve flattens without comparing thirty numbers. Months that have not come yet are
null— the cell stays empty instead of being painted as zero retention.stepsbuckets the colour so the legend reads, thetooltipslot adds the cohort size, andcell-clickopens the cohort.Retention by monthly cohortShare still paying N months after signing up · click a cohort 40100Chart data Row M0 M1 M2 M3 M4 M5 Apr 100 71 62 58 55 54 May 100 74 66 61 59 — Jun 100 69 60 57 — — Jul 100 77 70 — — — Aug 100 79 — — — — Sep 100 — — — — — Click a cell to open its cohort.
Above and below plan
scale="diverging"with amidpointat zero gives a miss and an overshoot different roles and puts «on plan» in the neutral middle. A sequential scale would paint the worst miss the same pale tone as a small overshoot.domainkeeps the scale symmetric, and the quarter that has not started isnull.Revenue against planDeviation by region and quarter -1515Chart data Row Q1 Q2 Q3 Q4 North 4 9 10 — South -6 -3 -8 — East 2 5 5 — West -12 -9 -12 — Central 7 3 9 — Feature adoption by plan, in words
Four
stepsturn shares into buckets, and thelegendslot names them — «rare», «some», «many», «most» — with swatches fromcolorAt, instead of printing bare thresholds.high-colortakes any theme role.Feature adoption by planShare of accounts that used it in September Rare · 0–25% Some · 25–50% Many · 50–75% Most · 75–100%Chart data Row Starter Team Business Recurring invoices 38 64 81 Payment links 52 71 88 Multi-currency 6 22 57 API access 2 15 63 Audit log 0 9 41 When customers write to support
Weekdays by hour, with no numbers in the cells — the shape is the point: the weekend is a cold band, Monday lunchtime the hottest spot.
steps="0"keeps the scale continuous for a smooth daily curve, andv-model:active-cellputs the cell under the cursor or the keyboard into the line above.When customers write to supportPeak: Monday around 13:00553Chart data Row 8:00 9:00 10:00 11:00 12:00 13:00 14:00 15:00 16:00 17:00 18:00 19:00 Mon 30 37 43 48 51 53 53 51 48 43 37 30 Tue 21 28 34 39 42 44 44 42 39 34 28 21 Wed 21 28 34 39 42 44 44 42 39 34 28 21 Thu 21 28 34 39 42 44 44 42 39 34 28 21 Fri 21 28 34 39 42 44 44 42 39 34 28 21 Sat 5 7 8 10 11 11 11 11 10 8 7 5 Sun 5 7 8 10 11 11 11 11 10 8 7 5
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
height | number | undefined | undefined | The canvas height in pixels; 256 by default. The data, the loading state and the empty state all take up the same height. |
width | number | undefined | 640 | Declared width: the first render goes from it, after that the width is measured. |
valueFormat | GrChartNumberFormat | undefined | undefined | The value format in the tooltip, the hidden table and announcements: decimal places and separators. |
showLegend | boolean | undefined | undefined | The scale bar with the domain bounds; `true` by default — without it there is nothing to read the colour by. |
tooltip | boolean | undefined | undefined | The panel with the values under the cursor; `true` by default. `false` hides only the panel: the cursor, the active marks, `update:activeIndex` and `pointHover` keep working while the chart is interactive. |
loading | boolean | undefined | false | Loading: a skeleton of the same height stands in place of the plot area, and the space for the axes is already reserved. |
empty | boolean | undefined | undefined | The empty state, set explicitly. Unset — the chart is empty when there is nothing to draw. |
emptyText | string | undefined | undefined | The placeholder text of an empty chart. Unset — "No data" from the package translations. |
dataTable | "hidden" | "visible" | "off" | undefined | undefined | The data table: `hidden` (the default) — for the screen reader only, `visible` — under the chart, `off` — no table. |
interactive | boolean | undefined | true | `false` — the chart becomes a picture: no focus, no tooltip, no keyboard. |
size | "md" | "xs" | "sm" | "lg" | undefined | undefined | The type size of the row, column and cell labels on the core control scale; by default — the size from `GrConfigProvider`. |
locale | string | undefined | undefined | The BCP-47 locale for numbers and dates. Unset — the language of the application translations, otherwise `en`. |
ariaLabel | string | undefined | undefined | The accessible name of the chart. Unset — a summary the component assembles: the chart type and the amount of data. |
ariaDescription | string | undefined | undefined | A description for the screen reader — the conclusion the chart is shown for; it adds to the summary rather than replacing it. |
dataTableMaxRows | number | "auto" | undefined | undefined | The row ceiling of the hidden data table. `'auto'` (the default) — as many rows as can be read; above the ceiling the table goes by an even sample and says so with a note, while point-by-point completeness stays with the keyboard. A number sets its own ceiling, `Infinity` removes it altogether, `dataTable: 'off'` removes the table entirely — which of these is needed is up to the application. |
domain | readonly [number, number] | undefined | undefined | Scale bounds. Not set — computed from the data. |
scale | HeatmapScaleKind | undefined | "sequential" | Sequential (one role) or diverging (two roles around a midpoint). |
midpoint | number | undefined | 0 | The midpoint of the diverging scale. |
lowColor | string | undefined | HEATMAP_LOW_COLOR | The colour of the low end of a diverging scale; `--gr-danger` by default. |
highColor | string | undefined | HEATMAP_HIGH_COLOR | The colour of the high end of the scale; for a sequential scale it is the only colour. `--gr-chart-1` by default. |
midColor | string | undefined | HEATMAP_MID_COLOR | The colour of the midpoint of a diverging scale; `--gr-muted` by default. |
steps | number | undefined | undefined | Number of scale steps. `0` — continuous. |
cellGap | number | undefined | undefined | The gap between cells in pixels. |
showValues | boolean | "auto" | undefined | undefined | Values in the cells. `'auto'` — only if they fit. |
activeCellv-model | { x: number; y: number; } | null | undefined | undefined | Cursor — `v-model:activeCell`. |
valuesrequired | readonly (readonly (number | null)[])[] | — | Values row by row: `values[y][x]`. `null` — there is no cell, not a "zero". |
xLabelsrequired | readonly string[] | — | Column labels — one per column of `values`. If they do not fit the cell, they are truncated or thinned out. |
yLabelsrequired | readonly string[] | — | Row labels — one per row of `values`. The column for them takes up to 40% of the width. |
Slots
| Slot | Type | Description |
|---|---|---|
tooltip | { active: GrChartActivePoint; cell: GrChartHeatmapCell | null; } | Custom content for the tooltip panel instead of the standard one; the data of the active position comes in the slot props. |
legend | { thresholds: readonly number[]; colorAt: (fraction: number) => string; } | A custom legend instead of the standard one. It is not drawn for an empty chart. |
empty | any | A custom placeholder for an empty chart instead of "No data"; it takes up the same `height`. |
header | any | A header above the chart, inside its root — above a legend placed on top. |
Events
| Event | Signature | Description |
|---|---|---|
update:activeCell | (event: "update:activeCell", value: { x: number; y: number; } | null): void | — |
cellClick | (event: "cellClick", value: GrChartHeatmapCell): void | — |
cellHover | (event: "cellHover", value: GrChartHeatmapCell | null): void | — |
Methods
| Methods | Signature | Description |
|---|---|---|
element | HTMLElement | null | The root element — for measurements and scrolling in the consumer. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrChartHeatmapConfigurableProps, GrChartSize, GrChartHeatmapCell } from '@feugene/granularity-charts/components/GrChartHeatmap'Filedist/types/components/GrChartHeatmap/config.d.ts
const grChartHeatmapConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrChartHeatmap">;Filedist/types/components/GrChartHeatmap/defaults.d.ts
interface GrChartHeatmapConfigurableProps {
size: GrChartSize;
height: number;
cellGap: number;
steps: number;
showLegend: boolean;
showValues: boolean | 'auto';
tooltip: boolean;
dataTable: 'hidden' | 'visible' | 'off';
dataTableMaxRows: number | 'auto';
}Filedist/types/components/GrChartFrame/chartFrameStyles.d.ts
type GrChartSize = GrComponentSize;Filedist/types/components/GrChartHeatmap/safelist.d.ts
const grChartHeatmapSafelist: string[];Filedist/types/components/GrChartHeatmap/GrChartHeatmap.vue.d.ts
interface GrChartHeatmapCell {
x: number;
y: number;
xLabel: string;
yLabel: string;
value: number | null;
}interface GrChartHeatmapProps {
values: readonly (readonly (number | null)[])[];
xLabels: readonly string[];
yLabels: readonly string[];
domain?: readonly [number, number];
scale?: HeatmapScaleKind;
midpoint?: number;
lowColor?: string;
highColor?: string;
midColor?: string;
steps?: number;
cellGap?: number;
showLegend?: boolean;
showValues?: boolean | 'auto';
valueFormat?: GrChartNumberFormat;
height?: number;
width?: number;
tooltip?: boolean;
activeCell?: {
x: number;
y: number;
} | null;
loading?: boolean;
empty?: boolean;
emptyText?: string;
dataTable?: 'hidden' | 'visible' | 'off';
interactive?: boolean;
size?: GrChartSize;
locale?: string;
ariaLabel?: string;
ariaDescription?: string;
dataTableMaxRows?: number | 'auto';
}interface GrChartHeatmapEmits {
(e: 'update:activeCell', value: {
x: number;
y: number;
} | null): void;
(e: 'cellClick', value: GrChartHeatmapCell): void;
(e: 'cellHover', value: GrChartHeatmapCell | null): void;
}Usage from the library prose
null is neither a zero nor the minimum of the scale
A missing cell is not filled at all, gets a dash in the hidden table and does not enter the domain of the scale. For cohorts that matters fundamentally: “the month has not come yet” and “retention is zero per cent” are different statements, and colouring the first as the second would mean drawing a complete washout where there is simply no data.
Rows of different lengths are padded with null on the right rather than with zeros: a cohort matrix
is sparse by construction.
The scale is one role of the theme rather than five colours
The colour of a cell is computed with color-mix from a role (highColor, and in a diverging scale
also lowColor and midColor). Five hand-picked colours would have to be picked again for the dark
theme and again for the second heat map on a neighbouring page; a role adjusts itself.
steps quantises the share: 5 (the default) gives five steps, 0 a continuous gradient. The edges
of the scale coincide in both modes, and the middles differ.
A diverging scale is normalised to the larger of the offsets from the middle — that way it is symmetric by construction rather than by a coincidence in the data. The minimum meanwhile keeps a noticeable admixture of paint: without it, it would be indistinguishable from an empty cell.
The legend decodes the scale rather than enumerating the categories
A bar with the labels of the bounds of the domain. Without it there is nothing to read the colour by: a matrix has no value axis, and “more saturated” on its own means nothing.
The keyboard is two-dimensional
←→ change the column, ↑↓ the row, Home/End take you to the edge of a row, and
PageUp/PageDown to the edge of a column. One Tab stop for the whole map.
Neither axis wraps around. A jump from the end of a row to the beginning of the next one disorients: the reader loses track of which row they are in, and there is nothing to tell them — only the content of a cell is announced.
Row and column labels do not overlap
The column of row names grows to fit the longest name — up to 40% of the chart width, but no
narrower than 96px; anything longer gets an ellipsis and the full name in <title>. Column labels
stand centred on the cells while they fit. In a narrow cell a label is truncated as long as at least
three characters of it remain, and beyond that the row is thinned out — keeping the first and the
last column — and each remaining label takes up the space up to halfway to its neighbours. The
labels have no way to overlap, and the name of any column stays in the tooltip and the hidden
table.
Limits
The component does not cluster, does not sort the rows and the columns and does not draw a dendrogram: the order is set by the consumer, because only they know what “similar” means here. There is no non-uniform grid either — the cells are of one size, otherwise the area would start encoding a second metric the reader was not told about.
Props in this example