Calendar heatmap (GitHub-style)
52 weeks × 7 days of activity, encoded as opacity on a single brand colour. Density at a glance — busy Mondays in spring, the December lull, the spike around month-end.
Overview
A 52×7 grid of small squares, each shaded by activity for that day. Reads as "habit + cadence + anomalies" in a single view.
The calendar heatmap is GitHub's contribution graph generalised: every cell is a day, opacity encodes a count, and the eye picks up patterns no line chart can — weekday-vs-weekend rhythm, monthly cycles, vacation gaps. It works because the calendar layout is already in everyone's head; you don't need axis labels.
Reach for it when you have one numeric value per day across roughly a year, and the question is about pattern, not exact totals — "when do operators log most entries?", "which weeks were dead?". Skip it if the data is hourly (use a small multiples line) or if you have fewer than 30 days (use bars).
The chart
A year of entries logged into the system — Mon–Sun rows, week columns. Darker = more activity that day.
Required pieces
One named export. The legend is a child component.
Chart.Heatmap ships in v0.2 with built-in week alignment and locale-aware month labels. Until then, derive cell positions from (weekIndex, dayOfWeek) as below.React snippet
Pass a flat array of {date, value}; the component handles the week/day grid.
Customising
Three forks.
Threshold buckets
Snap to discrete bins (0, 1–3, 4–9, 10+). Easier to read than a continuous scale; matches GitHub.
<Chart.Heatmap
data={entries}
buckets={[0, 1, 4, 10]}
colorScale="brand"
/>
Day-of-week heatmap
24 columns (hour) × 7 rows (day) — perfect for "when do operators sign in?" analyses.
<Chart.Heatmap
layout="hourly"
data={signIns}
rows={7}
cols={24}
/>
Diverging scale
For data with a meaningful zero — gains vs. losses — use a red-to-green diverging scale anchored at 0.
<Chart.Heatmap
data={netCashflow}
colorScale="diverging"
midpoint={0}
/>
In context
Inside an operator's profile card. The heatmap answers "is this operator engaged?" in less space than a count would.
Accessibility
365 cells is a lot of data and zero ways for a screen reader to navigate sensibly. The fix is the fallback table.
- The chart is one
role="img". Screen readers don't try to recite every cell — they get the summary fromaria-label. - Aria-label summarises the year. "Activity heatmap, 1,284 entries across 52 weeks, busiest in October" — not "365 cells".
- A hidden table holds the data. Rows are weeks, columns are days.
sr-onlywraps it so sighted users don't see two copies. - Tooltips are pointer-and-keyboard. Cells are focusable with
tabindex="0"; arrow keys move between adjacent cells; Enter opens the day's detail. - Colour scale has a non-colour cue. The legend shows "Less … More" text bookending the gradient so monochrome screens see the order.
- Diverging scales label both ends. "Loss … Gain" text accompanies the red-to-green so colour-blind users see the polarity.