Colormaps, colour mapping and accessibility
Normalisation, colorbars and the choice of palette — the part of a chart that decides whether the numbers are readable.
Normalisation and ScalarMappable
A colour map alone does nothing: you also need a norm that maps data values to the 0-1 range the map expects. Scatter, imshow and pcolormesh all do this internally, and accepting the default is how charts end up with a misleading colourbar.
import matplotlib as mpl
import matplotlib.pyplot as plt
norm = mpl.colors.Normalize(vmin=0, vmax=100)
cmap = plt.get_cmap("viridis")
sc = ax.scatter(x, y, c=values, cmap=cmap, norm=norm, s=20)
fig.colorbar(sc, ax=ax, label="Response time (ms)", extend="max")
mpl.colors.LogNorm(vmin=1, vmax=10_000) # wide dynamic range
mpl.colors.BoundaryNorm([0, 10, 50, 100], cmap.N) # discrete classes
mpl.colors.TwoSlopeNorm(vcenter=0, vmin=-3, vmax=8) # diverging data
# reuse the same mapping for a second plot
ax2.scatter(x2, y2, c=values2, cmap=cmap, norm=norm)| Family | Examples | Use for |
|---|---|---|
| Sequential | viridis, magma, cividis | Magnitude from low to high |
| Diverging | coolwarm, RdBu | Deviation around a real midpoint |
| Cyclic | twilight, hsv | Phase and angles that wrap around |
| Qualitative | tab10, Set2 | Unordered categories |
| Rainbow | jet | Avoid: false boundaries and poor for colour blindness |
Choosing a palette
plt.get_cmap("cividis") # designed for colour-blind readers
plt.get_cmap("RdBu_r") # reversed, so high values read as cold or hot
plt.colormaps() # everything available
# a cyclic map needs both ends to meet
plt.get_cmap("twilight")
# qualitative palettes for categories, never for magnitude
cmap = plt.get_cmap("tab10")
colors = cmap(np.linspace(0, 1, 10))- Sequential for magnitude, diverging only with a meaningful midpoint such as zero.
- Reverse a diverging map when the direction of good and bad is not what the default implies.
- Perceptually uniform maps keep equal steps looking equal;
jetdoes not. - Never encode categories with a sequential map — the reader will infer an order that does not exist.
Colourbar and accessibility
cb = fig.colorbar(sc, ax=ax, orientation="vertical", pad=0.02)
cb.set_label("Response time (ms)")
cb.set_ticks([0, 25, 50, 75, 100])
cb.ax.tick_params(labelsize=8)
# centre the diverging scale on the real midpoint
norm = mpl.colors.TwoSlopeNorm(vcenter=0.0, vmin=-3.0, vmax=3.0)
ax2.scatter(x2, y2, c=values2, cmap="coolwarm", norm=norm)⚠️
Colour alone is not an accessible encoding. Roughly one in twelve men has a colour vision deficiency, and charts get printed in greyscale — pair colour with marker shape, line style or a direct label.
FAQ
How do I make the colour scale comparable across panels?
Create one
Normalize instance and pass the same norm and cmap to every panel, then add a single shared colorbar. Per-panel autoscaling makes the same value look different.Why does my diverging map look wrong?
The default range is the data minimum to maximum, so the midpoint lands wherever the data happens to sit. Set
TwoSlopeNorm(vcenter=0) or equivalent so the neutral colour marks the neutral value.Related
Images and 3D: imshow, contour and mplot3d Chart types: bar, scatter, histogram, pie and box
Last refreshed 2026-09-18.