# Heatmap

A calendar or matrix heatmap built from dither cells, where every square steps cleanly from a few dots to solid.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The dithercn agent skill, for building any dither on the engine, is at [/.well-known/agent-skills/dithercn/SKILL.md](/.well-known/agent-skills/dithercn/SKILL.md).

<ComponentPreview src="examples/heatmap-demo.tsx" size="lg" controls="heatmapControls">
  <HeatmapDemo />
</ComponentPreview>

## Installation [#installation]

<InstallBlock name="heatmap" />

## Usage [#usage]

```tsx
import { Heatmap } from "@/components/dithercn/heatmap";

const data = [
  { date: "2026-10-10", value: 3 },
  { date: "2026-10-11", value: 0 },
  { date: "2026-10-12", value: 7 },
];

export function Deploys() {
  return <Heatmap data={data} unit={{ one: "deploy", other: "deploys" }} />;
}
```

Each column is a week and each row a weekday. The grid ends with the latest day in `data` and goes back `weeks` weeks, 53 by default. Days missing from `data` count as 0, and entries for the same day add up. Dates are read in UTC, so a square never moves with the visitor's time zone.

Every square is 4×4 dither cells. With the default four `steps` a square is dotted, a checkerboard, a hollow frame or solid, and an empty day is a faint backing square. Each level is a fixed mask rather than a raw threshold, so every square keeps a crisp outline and the levels stay distinct even at 2px cells. The scale goes up to six steps.

The grid fills its container. Cells shrink from `cell` down to 2px to fit every week; below that the oldest weeks drop off, so a phone shows the most recent months.

Hover a square, or focus the grid and use the arrow keys, for a readout above it. Hover a level in the scale to spotlight every square at that level, and click to keep it lit.

## Examples [#examples]

### A year of activity [#a-year-of-activity]

A full year with weeks starting on Monday, in `color="purple"`. On a narrow screen it keeps the latest weeks that fit.

<ComponentPreview src="examples/heatmap-year.tsx" size="lg">
  <HeatmapYear />
</ComponentPreview>

### Weekday by hour [#weekday-by-hour]

`mode="matrix"` takes a grid of values with row and column labels instead of dates. Here pushes by weekday and hour, with `cell={4}` for larger squares and `steps={5}`. Column labels thin out when they would collide.

<ComponentPreview src="examples/heatmap-matrix.tsx" size="lg">
  <HeatmapMatrix />
</ComponentPreview>

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | `"calendar" \| "matrix"` | `"calendar"` | A day per square, or any grid of values. |
| `data` (required) | `{ date: string \| Date; value: number }[] \| number[][]` |  | Calendar: values by day, date as "YYYY-MM-DD" or a Date read in UTC. Matrix: data[row][column]. |
| `weeks` | `number` | `53` | Calendar only: weeks shown, ending with the latest day. |
| `weekStart` | `0 \| 1` | `0` | Calendar only: the first row, 0 for Sunday or 1 for Monday. |
| `rows` | `string[]` |  | Matrix only, required: row labels, top to bottom. |
| `columns` | `string[]` |  | Matrix only, required: column labels, left to right. |
| `steps` | `number` | `4` | Densities above empty, 2 to 6. The last is solid. |
| `max` | `number` | `The largest value` | The value drawn solid. Larger values draw solid too, so one outlier doesn't wash out the rest. |
| `unit` | `string \| { one: string; other: string }` |  | What a value counts, for the readout and summary: "min" or { one: "deploy", other: "deploys" }. |
| `color` | `DitherColor` | `"ink"` | Ink colour, a palette name or any CSS colour. |
| `bloom` | `"off" \| "low" \| "high"` | `"off"` | Soft glow behind the two densest levels. |
| `cell` | `number` | `--dither-cell` | Largest dither cell in CSS pixels. Shrinks to 2 to fit the container. |
| `legend` | `boolean` | `true` | Show the Less to More scale. |
| `interactive` | `boolean` | `true` | Readout, keyboard and the scale spotlight. false draws a static image. |
| `animate` | `boolean` | `true` | Squares ripple in from the top left on first view. |

## Notes [#notes]

* The grid is a focusable group labelled with a summary: the range, the total and the busiest square. Arrow keys move a square at a time (left and right move a week in calendar mode), Home and End jump to the ends, and a live region reads each square. Each step in the scale is a toggle button with its range as its name.
* The ripple plays once when the grid scrolls into view. With reduced motion the grid appears finished and the readout moves without easing.
* It is one SVG: a backing path and one path per level, built from whole cells snapped to device pixels. Hover only moves an outline and the readout by transform.