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)
FamilyExamplesUse for
Sequentialviridis, magma, cividisMagnitude from low to high
Divergingcoolwarm, RdBuDeviation around a real midpoint
Cyclictwilight, hsvPhase and angles that wrap around
Qualitativetab10, Set2Unordered categories
RainbowjetAvoid: 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; jet does 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.

Images and 3D: imshow, contour and mplot3d Chart types: bar, scatter, histogram, pie and box

Last refreshed 2026-09-18.