GrChartHeatmap

  • charts
  • charts
  • API · 28 props
import { GrChartHeatmap } from '@feugene/granularity-charts/components/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 — null leaves 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

NeedTake
Compare the quantities of one dimensionGrChartBar
Show the course of a quantity over timeGrChartLine
Show exact numbers that will be read and sortedGrDataTable
Show a profile across several axesGrChartRadar

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. steps buckets the colour so the legend reads, the tooltip slot adds the cohort size, and cell-click opens the cohort.

    Retention by monthly cohortShare still paying N months after signing up · click a cohort
    40100
    Chart data
    RowM0M1M2M3M4M5
    Apr1007162585554
    May10074666159—
    Jun100696057——
    Jul1007770———
    Aug10079————
    Sep100—————

    Click a cell to open its cohort.

  • Above and below plan

    scale="diverging" with a midpoint at 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. domain keeps the scale symmetric, and the quarter that has not started is null.

    Revenue against planDeviation by region and quarter
    -1515
    Chart data
    RowQ1Q2Q3Q4
    North4910—
    South-6-3-8—
    East255—
    West-12-9-12—
    Central739—
  • Feature adoption by plan, in words

    Four steps turn shares into buckets, and the legend slot names them — «rare», «some», «many», «most» — with swatches from colorAt, instead of printing bare thresholds. high-color takes 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
    RowStarterTeamBusiness
    Recurring invoices386481
    Payment links527188
    Multi-currency62257
    API access21563
    Audit log0941
  • 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, and v-model:active-cell puts the cell under the cursor or the keyboard into the line above.

    When customers write to supportPeak: Monday around 13:00
    553
    Chart data
    Row8:009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:00
    Mon303743485153535148433730
    Tue212834394244444239342821
    Wed212834394244444239342821
    Thu212834394244444239342821
    Fri212834394244444239342821
    Sat578101111111110875
    Sun578101111111110875

API generated from the package · cannot drift

Props

PropTypedefaultDescription
heightnumber | undefinedundefinedThe canvas height in pixels; 256 by default. The data, the loading state and the empty state all take up the same height.
widthnumber | undefined640Declared width: the first render goes from it, after that the width is measured.
valueFormatGrChartNumberFormat | undefinedundefinedThe value format in the tooltip, the hidden table and announcements: decimal places and separators.
showLegendboolean | undefinedundefinedThe scale bar with the domain bounds; `true` by default — without it there is nothing to read the colour by.
tooltipboolean | undefinedundefinedThe 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.
loadingboolean | undefinedfalseLoading: a skeleton of the same height stands in place of the plot area, and the space for the axes is already reserved.
emptyboolean | undefinedundefinedThe empty state, set explicitly. Unset — the chart is empty when there is nothing to draw.
emptyTextstring | undefinedundefinedThe placeholder text of an empty chart. Unset — "No data" from the package translations.
dataTable"hidden" | "visible" | "off" | undefinedundefinedThe data table: `hidden` (the default) — for the screen reader only, `visible` — under the chart, `off` — no table.
interactiveboolean | undefinedtrue`false` — the chart becomes a picture: no focus, no tooltip, no keyboard.
size"md" | "xs" | "sm" | "lg" | undefinedundefinedThe type size of the row, column and cell labels on the core control scale; by default — the size from `GrConfigProvider`.
localestring | undefinedundefinedThe BCP-47 locale for numbers and dates. Unset — the language of the application translations, otherwise `en`.
ariaLabelstring | undefinedundefinedThe accessible name of the chart. Unset — a summary the component assembles: the chart type and the amount of data.
ariaDescriptionstring | undefinedundefinedA description for the screen reader — the conclusion the chart is shown for; it adds to the summary rather than replacing it.

Slots

SlotTypeDescription
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.
emptyanyA custom placeholder for an empty chart instead of "No data"; it takes up the same `height`.
headeranyA header above the chart, inside its root — above a legend placed on top.

Events

EventSignatureDescription
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

MethodsSignatureDescription
elementHTMLElement | nullThe 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
import type { GrChartHeatmapConfigurableProps, GrChartSize, GrChartHeatmapCell } from '@feugene/granularity-charts/components/GrChartHeatmap'

Filedist/types/components/GrChartHeatmap/config.d.ts

grChartHeatmapConfigconst
const grChartHeatmapConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrChartHeatmap">;

Filedist/types/components/GrChartHeatmap/defaults.d.ts

GrChartHeatmapConfigurablePropsinterface
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

GrChartSizetype
type GrChartSize = GrComponentSize;

Filedist/types/components/GrChartHeatmap/safelist.d.ts

grChartHeatmapSafelistconst
const grChartHeatmapSafelist: string[];

Filedist/types/components/GrChartHeatmap/GrChartHeatmap.vue.d.ts

GrChartHeatmapCellinterface
interface GrChartHeatmapCell {
    x: number;
    y: number;
    xLabel: string;
    yLabel: string;
    value: number | null;
}
GrChartHeatmapPropsinterface
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';
}
GrChartHeatmapEmitsinterface
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.

The hidden table is the most valuable thing here

It has real headings for the rows and the columns, and that is not a formality: a visual heat map without a table is unreadable entirely rather than merely less convenient. Colour tells a blind user nothing, and there may be no values in the drawing — the labels in the cells appear only where they fit.

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.