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,
lruns 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.
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
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
Reverse a ramp
In the second example call YlGnBu as
(t) => d3.interpolateYlGnBu(1 - t). The lightness row reverses to13 26 40 55 68 77 88 95 99and the ramp is dark-to-light. Use that when dark should mean few (on a dark page). - 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
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' ]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
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 downIn 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
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 58Try 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.
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.scaleSequentialis not a normal scale: it has a domain but no range. The interpolator is the range, and it receives(value - min) / (max - min).scaleSequentialQuantileandscaleSequentialLogchange 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)")returnsnullbecause D3 cannot resolve a CSS variable. A fill attribute can usevar()(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
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
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
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).lis the lightness from 0 to 100. Viridis and YlGnBu go in one direction. Turbo andinterpolateRainbowgo up and down, so two different values can get the same lightness and a similar-looking colour. - 4
D3 has the ramps.
d3-scale-chromatic(version 3.1.0 here) exports interpolators such asinterpolateViridis(a function from 0 to 1 to a colour) andscheme…arrays such asschemeTableau10. You wrap an interpolator ind3.scaleSequential(interpolator).domain([min, max]), ord3.scaleDiverging(...).domain([min, middle, max]), and a name list ind3.scaleOrdinal(names, colours). - 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.interpolateLabblends in Lab, which is built so that equal steps look equal.d3.interpolateHclblends 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
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
Which kind of colour scale would you use for rides per hour in the June heatmap, and why?
Why is a rainbow a poor choice for a number that runs from low to high?
What does d3.scaleOrdinal return for a name that is not in its domain, and why can that mislead?
Practice
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.
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.laband 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.