Command Palette

Search for a command to run...

Hectal
PHASE 10Intermediate ~20 min· topic 1 of 6

Topic 10.1

Colour Scales

In one line

Colour carries data in three ways: an ordered ramp (sequential), a ramp with a meaningful middle (diverging), or unrelated names (categorical). Each job needs its own scale, and a rainbow is wrong for order because its lightness goes up and down.

Think of it like this

The colours on a weather map. Pale yellow to deep red says "warmer" without a word, because you read lighter-to-darker as less-to-more. A map that went red, then green, then purple, then red again for rising temperature would make you stop and check the key for every patch. Colour on a chart is read the same way: the eye must be able to tell which way is more.

Words you'll meet

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

sequential scale
Colours that change in one direction, light to dark, for values that run from low to high.
diverging scale
Two ramps that meet at a meaningful middle such as zero, so both directions are readable.
categorical scale
One distinct colour per name. The colours have no order.
interpolator
A function that takes a number from 0 to 1 and returns a colour.
lightness
How light or dark a colour is. In Lab, l runs from 0 (black) to 100 (white).

Step by step

01Pick the job first

Ask what the colour stands for. A number from low to high is sequential. A number with a middle is diverging. A name is categorical. The rest follows from that answer.

02Make one scale of each kind

The first example builds all three and prints what they return for a few values, then for odd ones: past the domain, null, undefined, NaN, a domain of one value, and a fourth name for a three-colour scale.

03Measure the lightness of four ramps

The second example samples nine steps of each ramp and prints the Lab lightness. One-direction ramps pass. Two ramps go up and down.

Measure the lightness of four rampsdiagram
Rendering diagram…

04Blend in three colour spaces

The third example blends the dashboard's orange (#e76f51) and teal (#2a9d8f) in RGB, Lab and HCL and prints the middle colour of each. Then use the palette lab to see colours and their contrast.

Try it yourself

  1. 1

    Go outside the domain

    In the first example add console.log(seq(-5), seq(200000));. It prints #440154 #fde725: Viridis returns its end colours for input outside the domain, so a very busy day is never drawn in an unexpected colour. .clamp(true) makes that explicit for any interpolator.

  2. 2

    Reverse a ramp

    In the second example call YlGnBu as (t) => d3.interpolateYlGnBu(1 - t). The lightness row reverses to 13 26 40 55 68 77 88 95 99 and the ramp is dark-to-light. Use that when dark should mean few (on a dark page).

  3. 3

    Give unknown names one grey

    At the end of the first example add const grey = d3.scaleOrdinal(["Harbour", "Old Town", "University"], ["#0072B2", "#D55E00", "#009E73"]).unknown("#999999"); console.log(grey("Riverside"), grey(undefined), grey.domain().length);. It prints #999999 #999999 3: with .unknown() a name that is not in the domain gets the grey, and the domain stays at 3 names instead of growing.

Code & diagrams

Three scales, and what they return for odd input New tab

Edge cases in one place. A missing value gives undefined, so a fill set from it is not set and the shape turns black. An ordinal scale gives new names the colours it already has, in order, and adds them to its domain. (Viridis returns its end colour past the domain; call .clamp(true) to be sure for other interpolators.)

Console

(nothing was printed)

Expected console output

sequential 0, 60000, 120000: [ '#440154', '#21918c', '#fde725' ]
diverging -6, 0, 6: [ 'rgb(103, 0, 31)', 'rgb(242, 239, 238)', 'rgb(5, 48, 97)' ]
categorical: #0072B2 #D55E00 #009E73
sequential beyond domain 200000: #fde725
sequential null: undefined undefined: undefined NaN: undefined
all values equal, domain [5, 5]: #21918c
a fourth name, no fourth colour: #0072B2 #D55E00
cat.domain(): [ 'Harbour', 'Old Town', 'University', 'Riverside', 'Hillcrest' ]
Four ramps: which ones change lightness in one direction? New tab

In the picture: Viridis runs dark purple to green to yellow, YlGnBu pale yellow to deep blue, Turbo near-black through blue, green, yellow and orange to dark red, and Rainbow purple through pink, orange, green and blue back to purple. The numbers say what the eye cannot: Turbo and Rainbow rise to about 90 and fall again.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Viridis  15 25 35 45 54 63 73 82 91 one direction
YlGnBu   99 95 88 77 68 55 40 26 13 one direction
Turbo    9 48 70 86 90 84 68 46 31 goes up and down
Rainbow  37 52 63 74 88 85 73 52 37 goes up and down
The same two colours blended in RGB, Lab and HCL New tab

In the picture the RGB and Lab rows pass through a dull grey-brown in the middle. The HCL row passes through olive and green and stays vivid. The numbers agree: chroma (colourfulness) in the middle is 12, 19 and 48.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

RGB  midpoint #898670 lightness 56 chroma 12
Lab  midpoint #a38c70 lightness 60 chroma 19
HCL  midpoint #83983c lightness 60 chroma 48
end colours: lightness 61 and 58

Try it: interactive

Palette lab

Pick a kind of scale, then look at how its colours look to different kinds of colour vision and what contrast they have on white. Try five classes of a sequential palette, then switch to categorical.

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

Colour a missing value with an ordinal scale

In the first example add the line console.log(cat(undefined), cat(null), cat.domain().length); at the end.

terminal
$ Console
── what you'll see ──
#009E73 #0072B2 7

Myth vs fact

Myth

A rainbow scale is best because it has the most colours.

Fact

More hues do not make more differences visible. The second example shows that Rainbow and Turbo rise and fall in lightness, so unequal values can look alike, and readers cannot say which end is more without the legend.

Myth

Any two colours can be blended to make a good ramp.

Fact

A ramp needs a lightness change in one direction. A blend between two colours of equal lightness (the third example's ends are 61 and 58) looks flat in greyscale print or to a reader who cannot tell the hues apart.

Pro corner

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

  • ▸

    d3.scaleSequential is not a normal scale: it has a domain but no range. The interpolator is the range, and it receives (value - min) / (max - min). scaleSequentialQuantile and scaleSequentialLog change how values map to that 0–1 number, which helps when a few large days stretch the ramp.

  • ▸

    A diverging scale's middle must be a real value (zero, a target). If the data runs from 3 to 9 with no meaningful middle, a sequential ramp is the honest choice: forcing a midpoint invents a threshold.

  • ▸

    d3.color("var(--chart-accent)") returns null because D3 cannot resolve a CSS variable. A fill attribute can use var() (the browser resolves it), but a D3 interpolator cannot blend it. Blend real colours, or blend in JavaScript after reading the token (Topic 10.4).

Remember this

  1. 1

    The problem this solves: a chart that uses colour for a number (rides per hour in the June heatmap) or for a name (six districts in the stack) must pick colours that match the job. The wrong choice does not print an error. It just makes readers see differences that are not in the data, or miss ones that are.

  2. 2

    Three jobs, three scales. A sequential scale goes from low to high in one direction, such as a light-to-dark ramp for rides. A diverging scale has a meaningful middle (zero, an average) and two directions, such as warmer or colder than normal. A categorical scale gives each name its own colour, with no order. Using the wrong one is the most common colour mistake.

  3. 3

    Lightness carries order. People read lighter-to-darker as more or less before they read hue. So a sequential ramp must change lightness in one direction. In the second example, d3.lab(colour).l is the lightness from 0 to 100. Viridis and YlGnBu go in one direction. Turbo and interpolateRainbow go up and down, so two different values can get the same lightness and a similar-looking colour.

  4. 4

    D3 has the ramps. d3-scale-chromatic (version 3.1.0 here) exports interpolators such as interpolateViridis (a function from 0 to 1 to a colour) and scheme… arrays such as schemeTableau10. You wrap an interpolator in d3.scaleSequential(interpolator).domain([min, max]), or d3.scaleDiverging(...).domain([min, middle, max]), and a name list in d3.scaleOrdinal(names, colours).

  5. 5

    Interpolate in a perceptual colour space. Blending two colours in RGB averages the red, green and blue numbers, and the middle can turn grey and dull. d3.interpolateLab blends in Lab, which is built so that equal steps look equal. d3.interpolateHcl blends hue, chroma and lightness, and keeps the colour vivid but walks through other hues on the way (orange to teal passes through green). The third example measures all three.

  6. 6

    Categories: few, and far apart. Visualisation guidance puts the limit at roughly six or seven colours that a reader can match to a legend before it becomes a lookup chore (a rule of thumb, not a standard). With 25 stations, colour is the wrong channel: sort and label the bars instead (Topic 2.2). An ordinal scale with fewer colours than names reuses colours from the start, silently, so two names can share one colour.

Explain it without notes

01

Which kind of colour scale would you use for rides per hour in the June heatmap, and why?

02

Why is a rainbow a poor choice for a number that runs from low to high?

03

What does d3.scaleOrdinal return for a name that is not in its domain, and why can that mislead?

Practice

01

Write a function rampLightness(interpolator) that returns true if the Lab lightness of 9 samples changes in one direction only. Test it with d3.interpolateViridis and d3.interpolateRainbow.

02

Make a diverging scale for the temperature gap temp_c - 17 (days from daily.csv, range -7.1 to +7.9) and print the colour for the two ends. Which value is the middle?

Trade-offs

  • ↔

    Stepped (classed) colours are easier to read against a legend but hide small differences. Continuous ramps show every difference but cannot be matched to exact numbers by eye.

Done when you can

  • I can say whether a given chart needs a sequential, diverging or categorical scale.

  • I can measure a ramp's lightness with d3.lab and say if it is one-directional.

  • I can say what a scale returns for null, undefined, NaN and an unknown name.

  • I can say why I would not use more than about six colours for names.