Home/ Cookbook/ Charts/ Calendar heatmap

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.

Charts · 13 of 20 1 primitive · Chart.Heatmap ~15 min React · @corelithzw/react

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 opinion: always anchor the colour scale at the cell's own data, not at zero. If your busiest day shipped 3 commits, the brightest cell should be the 3. Zero-anchored scales hide all the signal.

The chart

A year of entries logged into the system — Mon–Sun rows, week columns. Darker = more activity that day.

Operator entries · last 52 weeks A 52-week by 7-day heatmap of operator entries. October shows the densest activity; the last two weeks of December are sparse. Mon Wed Fri Sun JanFebMarAprMayJunJulAugSepOctNovDec

Required pieces

One named export. The legend is a child component.

Roadmap: 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.

Less More
Chart.Heatmap@corelithzw/react

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.

TM
Tendai Moyo
Gold mine clerk · joined Jan 2024
1,284 entries this year

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 from aria-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-only wraps 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.