Heatmap

A matrix or calendar heatmap with a stepped or diverging scale and empty outlines for missing cells. Plain SVG: no chart engine.

Install

Pick one engine. Both builds take the same props, so switching later changes only the import path.

Plain SVG (no chart engine)

npx shadcn@latest add https://beautifulcharts.dev/r/instrument-heatmap.json

import { Heatmap } from "@/components/beautiful-charts/themes/instrument/heatmap";

Apache ECharts

npx shadcn@latest add https://beautifulcharts.dev/r/instrument-heatmap-echarts.json

import { Heatmap } from "@/components/beautiful-charts/themes/instrument/echarts/heatmap-chart";
npm dependencies: echarts@^6.1.0

Example

A complete client module. Paste it into your app after installing the Recharts build; for ECharts, change recharts to echarts in the import.

"use client";

import { Heatmap } from "@/components/beautiful-charts/themes/instrument/heatmap";

// One row per day, as "YYYY-MM-DD". Days with no row are drawn as empty outlines, never as zero.
const days = Array.from({ length: 120 }, (_, i) => ({
  day: new Date(Date.UTC(2026, 5, 1) + i * 86_400_000).toISOString().slice(0, 10),
  deploys: (i * 7) % 5,
}));

export function DeployCalendar() {
  return (
    <Heatmap
      data={days}
      layout="calendar"
      dateKey="day"
      valueKey="deploys"
      config={{ deploys: { label: "Deploys", color: "var(--chart-2)" } }}
      title="Deploys per day"
      onSelectedChange={(selection) => console.log(selection?.datum.day)}
    />
  );
}

Heatmap props

Generated from HeatmapChartProps in the installed source, so this table and your editor's hints always agree. * marks a required prop.

Heatmap props
PropTypeDescription
valueKey*keyof TDatum & stringThe field holding each cell's value. Its entry in config sets the colour and label.
layout"matrix" | "calendar""matrix" (default) places cells by xKey and yKey; "calendar" places them by dateKey, one per day.
xKeykeyof TDatum & stringMatrix: the field for columns.
yKeykeyof TDatum & stringMatrix: the field for rows.
xOrderstring[]Matrix: column order. Defaults to first appearance in the data.
yOrderstring[]Matrix: row order. Defaults to first appearance in the data.
dateKeykeyof TDatum & stringCalendar: the field holding each day, as "YYYY-MM-DD" (read in UTC).
fromstringCalendar: the first day shown. Defaults to the earliest day in the data.
tostringCalendar: the last day shown. Defaults to the latest day in the data.
weekStartnumberCalendar: first day of the week (0 = Sunday). Defaults to the locale's.
scale"sequential" | "diverging""sequential" (default) ramps one colour; "diverging" uses two colours either side of midpoint.
midpointnumberDiverging: the neutral value. Default 0.
domain[number, number]Fix the scale's range. Values outside it take the end colour and are flagged. Defaults to a nice range around the data.
stepsnumberColour steps, 2–9 (default 5), shown in the legend as ranges. 0 draws a continuous ramp.
showValuesbooleanWrite each value in its cell when it fits.
formatX(value: string) => stringFormats column labels (matrix), in the axis, tooltip and table.
formatY(value: string) => stringFormats row labels (matrix), in the axis, tooltip and table.
cellGapnumberSpace between cells in px (default 2).
cellRadiusnumberCell corner radius in px (default 3).

Apache ECharts build only

Escape hatches into the engine. Your option is deep-merged over the generated one, so you can change one nested setting without replacing a whole section.

Apache ECharts build props
PropTypeDescription
rendererEChartsRenderer"canvas" (default, best for large grids) or "svg".
optionOptionOverrideDeep-merged over the generated option, or (generated) => option. Cells are one custom series (id "cells", one item per cell).

Series config

One entry per series key in config. shadcn's ChartConfig entries work as they are. Give a series at most one of color, theme or colors; TypeScript rejects more than one. This table lists only the options this chart reads.

Heatmap series config
PropTypeDescription
labelReactNodeName shown in the legend, tooltip and table. Defaults to the key, humanised.
ariaLabelstringPlain text for tables, summaries and announcements when label is not a string.
format(value: number) => stringFormats this series' values everywhere: tooltip, legend, table, readout.
colorColorValueOne colour: any CSS colour or var(--chart-1), or { light, dark }.
themenevershadcn's per-theme colours, { light, dark }.
colorsneverGradient stops from base to value end, per theme. The last stop is the series' single colour.

Events

Every callback, and the payload types it receives. Indexes always point into the data you passed, even when a brush or grouping shows a subset.

Heatmap events
PropTypeDescription
onSelectedChange(selection: Selection<TDatum> | null) => voidCalled when a row is selected or cleared, by pointer, keyboard or the data table.
onActiveChange(index: number | null) => voidCalled as the hovered or focused row changes, or null when it leaves.
onReady(chart: EChartsType) => voidCalled once with the ECharts instance, e.g. to add event listeners.
export type Selection<TDatum> = {
  /** Index into the data you passed in, even when a brush shows a subset. */
  dataIndex: number;
  datum: TDatum;
  seriesKey: string | null;
  source: "pointer" | "keyboard" | "table";
  /** For a grouped segment (a donut's "Other"): the indexes of every row it stands for. `dataIndex` is the largest. */
  members?: number[] | undefined;
};

export type TooltipContext<TDatum> = {
  datum: TDatum;
  /** Index into the data you passed in. */
  dataIndex: number;
  /** Visible series at this index. `value` is your raw value; `share` is its part of the row in 100% mode. */
  series: { key: string; label: ReactNode; value: number | null; share?: number | null | undefined; color: string; selected: boolean }[];
};

Parts and styling hooks

Each part carries a data-slot attribute. Theme rules inside the chart are one attribute selector deep, so a selector such as .my-chart [data-slot="chart-title"] overrides them. These are the slots in the files this chart installs; parts a chart doesn't use are simply absent from its markup.

chartchart-bodychart-descriptionchart-eyebrowchart-footerchart-headerchart-headingchart-legendchart-legend-itemchart-legend-labelchart-legend-swatchchart-legend-valuechart-livechart-mainchart-plotchart-readoutchart-statchart-stat-notechart-stat-valuechart-statechart-state-boxchart-state-messagechart-statschart-tablechart-table-scrollchart-takeawaychart-titlechart-tooltipodometerodometer-digitodometer-glyphsodometer-reelodometer-signpartial-notereadout-deltareadout-unitreadout-valuesr-onlytooltip-indicatortooltip-rowtooltip-titletooltip-value

Props every chart shares

Header, states, legend, tooltip, formatting, motion, sound and controlled interaction state. They work the same on every Instrument chart and both engines. The guide explains colours, controlled state and accessibility.

Show all 29 shared props
Props every chart shares
PropTypeDescription
data*TDatum[]Your rows, as they are. Missing values (null, undefined, NaN) stay missing and are never drawn as zero.
localestringBCP 47 locale. Defaults to "en-US" so server and client render the same text.
timeZonestringIANA time zone for dates. Defaults to "UTC" so server and client agree.
configChartConfigPer-series label, colour, icon, format and style, keyed by series key. shadcn ChartConfig works as it is.
eyebrowReactNodeSmall label above the title.
titleReactNodeCard title. A string also names the chart for screen readers.
descriptionReactNodeA line under the title.
takeawayReactNode | ((summary: string) => ReactNode) | falseThe sentence under the title. Computed from the data by default; pass text, a function of the computed sentence, or false to hide it.
ariaDescriptionstringAccessible summary for screen readers. Computed from the data by default.
motionMotionPreference"full" (default), "subtle" or "off". Reduced-motion settings always win.
motionOptionsMotionOptionsWhen the entrance plays.
soundbooleanSound cues on hover, scrub and select. Plays only if the visitor has also turned sound on for the page.
densityDensity"comfortable" (default) or "compact" for dashboards.
tooltipTooltipOptions<TDatum> | falseTooltip position, indicator, cursor and custom content, or false to turn it off.
valueFormatter(value: number) => stringValues formatted for display. Series formatters in config win.
loadingbooleanShow the loading state. Over existing data, the old marks stay dimmed instead of vanishing.
errorstring | nullAn error message. Shows the error state in place of the plot.
emptyStateReactNodeShown instead of the plot when there is no data.
loadingStateReactNodeShown while loading is true and there is no data yet.
errorStateReactNodeShown when error is set.
selectednumber | nullControlled and uncontrolled interaction state. Point selection (selected) pins one row by its index in data. Series selection (selectedSeries) picks one series and dims the others. They are separate: picking a row doesn't pick a series.
defaultSelectednumber | nullInitial selected row when uncontrolled.
activeIndexnumber | nullControlled hover or focus index, for syncing several charts on one cursor.
palette"theme" | "shadcn"Series colours: "theme" (default) uses the theme's designed palette; "shadcn" uses your --chart-1..5. --bc-series-N always wins. You can also opt a whole section in with the class bc-use-shadcn-colors.
tablebooleanShow the "Show data table" disclosure. On by default.
statsChartStat[]A row of figures under the title, for dashboard cards and blocks.
classNamestringClasses on the card, for spacing and layout.
heightnumberHeight of the plot area in px. The frame sizes around it.