Command Palette

Search for a command to run...

Hectal
PHASE 7Intermediate ~22 min· topic 5 of 7

Topic 7.5

Heatmaps and Small Multiples

In one line

A heatmap colours a grid of cells by a number, which shows a pattern in two directions at once, like day by hour. Small multiples repeat one small chart for many groups, with the same scale so the groups can be compared.

Think of it like this

A school timetable that you colour in. Draw the days across and the hours down, and paint the busy slots dark. In one look you see that every Monday morning is dark and every Friday afternoon is pale. A heatmap is that timetable, with the paint chosen by a number.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

heatmap
A grid of cells whose colour shows a number.
sequential palette
Colours that go from light to dark in one direction, for numbers that go from low to high.
small multiples
Many small copies of one chart, one for each group, laid out in a grid.
shared scale
The same axis range in every small chart, so a tall line means the same in all of them.
free scale
Each small chart has its own axis range, so all of them fill their box.

Step by step

01Load the hours and make two band scales

The first example reads hourly-june.csv: 720 rows, one per hour, for 30 days by 24 hours. It uses datetime.slice(8, 10) for the day. The file's datetime has no time zone, and d3.autoType would read it as a Date in the viewer's zone (Break it).

02Paint the cells and read the pattern

Each row becomes a <rect>; its colour comes from the sequential scale. The darkest cell is Thursday 26 June at 17:00 with 599 rides. Weekdays have two dark stripes (around 8 and from 17 to 18); the weekends (1, 7, 8, 14, 15, 21, 22, 28 and 29 June) have one dark blob at midday. Zero cells (42) are grey and mostly at night.

03Change the colour scale

The same data on a linear and a square-root scale: 337 cells fall in the palest tones with linear, 164 with square root. Both are honest if labelled. Pick the one that answers the question: linear for "where is the peak", square root for "is this quiet hour really empty".

04Repeat a small chart 25 times

The second example draws one line per station. Left, one scale for all: the heights compare. Right, a scale for each: every line fills its box. S25 is 12 pixels tall on the left and 46 on the right, with the same 1,833 peak.

Try it yourself

  1. 1

    Use the square-root scale

    In the first example replace d3.scaleSequential( by d3.scaleSequentialSqrt( in the color line (the first one, before const svg). The whole grid turns darker: a square root lifts small numbers, so moderate hours look nearly as dark as the rush hours, and only the quietest hours stay pale. The palest tones line keeps printing its two counts (337 and 164) because it builds both scales itself.

  2. 2

    Squash the colour range

    Change .domain([0, max]) to .domain([0, 300]). All 118 cells with more than 300 rides now have the same darkest colour, so the busiest hour (599) can no longer be told from an ordinary rush hour. A clipped scale hides the peak.

Code & diagrams

June by day and hour New tab

The rows are the 30 days (1 to 30 June) and the columns the 24 hours. Hover a cell in your own page to see the `<title>` text: it is the cheapest tooltip (Topic 8.2 builds a better one).

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

cells: 720 | days: 30 | hours: 24
busiest cell: Thu 26 June at 17:00 with 599 rides
cells with no rides: 42 | median cell: 78.5
cells in the palest tones: linear 337 sqrt 164
25 stations, one small chart each New tab

Same data, same lines, two scales. Left: S25 is a low line, which is true. Right: it fills its box and looks as busy as S01, which is not.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

charts per grid: 25 | tallest peak: 7260 | S25 peak: 1833
S25 line height: shared 12 px of 46 | free 46 px
S22 months with no rides: Jan, Feb, Mar, Apr

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Let autoType read the date

Change the load to const rows = await d3.csv("/data/lumen/hourly-june.csv", d3.autoType) and print rows[0].datetime.

terminal
$ Console, on a computer set to UTC+5:30
── what you'll see ──
2025-05-31T18:30:00.000Z

Break #2

Forget to convert numbers in the small multiples

In the second example remove , d3.autoType from the d3.csv call.

terminal
$ Console
── what you'll see ──
charts per grid: 25 | tallest peak: 937 | S25 peak: 937
S25 line height: shared 46 px of 46 | free 46 px
S22 months with no rides:

Myth vs fact

Myth

A heatmap needs a rainbow so that every value has its own colour.

Fact

A rainbow has no order: people cannot say whether green is more than yellow. One hue going from light to dark is read correctly by almost everyone, including people with colour blindness.

Myth

Small multiples should each fill their box.

Fact

Only if the aim is to compare shapes. To compare size, they need a shared scale, and then small values look small, which is the truth.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    d3.scaleSequential maps a domain to an interpolator function, not to a range. That is why it takes d3.interpolateYlGnBu and has no .range(). Use d3.scaleQuantize(domain, d3.schemeBlues[5]) for a few discrete classes, the best choice when the reader must look up a value in a legend.

Remember this

  1. 1

    The problem this solves: Priya wants to know when the rush happens. June has 720 hours of rides. A line of 720 points is a tangle. A grid of 30 days by 24 hours, coloured by rides, shows the pattern in one picture: the weekday peaks at 8 and at 17 and 18 o'clock, and the weekend blob at midday.

  2. 2

    A heatmap is two band scales and a colour scale. The x band is the hour, the y band is the day (recall Topic 6.4: a band scale gives each category one slot), and each cell is a <rect> whose fill comes from d3.scaleSequential(d3.interpolateYlGnBu).domain([0, max]). A sequential palette goes from light (few) to dark (many). Never use a rainbow: it has no order and cannot be read by someone with colour blindness (Topic 2.5).

  3. 3

    The scale decides what you see. On a linear scale, 337 of the 720 cells end up in the palest tones, so most quiet hours look the same. A square-root scale (d3.scaleSequentialSqrt) spreads them: only 164 cells stay in the palest tones (measured as a lightness above 0.85). Neither is wrong, but you must say which you used.

  4. 4

    Zero is not the same as missing. 42 cells have zero rides (night hours). A zero cell gets its own light grey in the example, and a really missing value would need a hatch or a different colour. If both are the lightest colour, the reader cannot tell a quiet hour from a broken sensor.

  5. 5

    Small multiples repeat the same chart. One small line chart per station, in a grid, lets the eye compare shapes quickly, with no legend to look up. The big rule is the scale. With one shared vertical scale, S25 (peak 1,833 rides) is a low line, only 12 pixels of 46, because S06 has 7,260. With each chart on its own scale, S25 looks as tall as S01 and a reader thinks they are as busy. Use a shared scale to compare size, and free scales only to compare shape, and say so on the page.

  6. 6

    Limits. A heatmap of 30 by 24 is 720 <rect> elements, which is fine. A year by hour is 8,760: use Canvas (Phase 11). Twenty-five small charts is fine. Two hundred is a long scroll: filter or group first. Cells need a colour legend (Topic 7.6) and a text alternative for a screen reader (Phase 15).

Explain it without notes

01

What are the three scales of a heatmap?

02

Why is a rainbow a poor palette for a heatmap?

03

What is the difference between a shared and a free scale in small multiples?

Practice

01

Colour the weekend rows' labels differently: in the heatmap, add a second y axis tick label colour for days 1, 7, 8, 14, 15, 21, 22, 28 and 29.

02

Draw the first example as weekday by hour (7 rows) using the mean rides per hour. Hint: d3.rollup(rows, (v) => d3.mean(v, (d) => d.rides), (d) => d.weekday, (d) => d.hour).

Trade-offs

  • ↔

    A heatmap hides exact numbers for the sake of the pattern. Give the exact value in a tooltip or a table.

Done when you can

  • I can draw a heatmap with two band scales and a sequential colour scale.

  • I can say what changes between a linear and a square-root colour scale.

  • I can make small multiples with a shared and with a free scale, and say what each is for.

  • I can show zero and missing differently.