Colors and Colormaps
Color in matplotlib has three jobs: distinguish series, encode a data value, and keep the figure legible. The API treats them separately - a series gets one color from the palette, a continuous value gets a position in a colormap, and a categorical value gets a slot in a categorical map. Getting these three mechanisms straight covers nearly every styling question a figure raises.
Specifying Colors
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot([0, 1, 2], [1, 3, 2], color="#2563eb") # hex
ax.plot([0, 1, 2], [2, 2.5, 1], color="tab:red") # palette name
ax.plot([0, 1, 2], [0.5, 1, 0.8], color=(0.1, 0.5, 0.3, 0.7)) # RGBA
plt.show()
Accepted forms: CSS hex, CSS names, the palette entries ("tab:blue"), matplotlib's extended names ("C0" through "C9" = the active cycle), and RGBA tuples. C0-style names stay in sync with whatever cycle is active - useful when the palette changes via a style sheet.
The Color Cycle
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
for i in range(4):
ax.plot([0, 1, 2], [i, i + 0.5, i], label=f"s{i}")
ax.legend()
plt.show()
# inspect or override the active cycle
print(plt.rcParams["axes.prop_cycle"])
plt.rcParams["axes.prop_cycle"] = plt.cycler(
color=["#2563eb", "#f59e0b", "#10b981", "#ef4444"])
Series without an explicit color draw from the cycle in order; the default (tab10) is designed for distinguishability and color-blind safety. Override the cycle globally for brand palettes, or set colors per call for deliberate emphasis - "hero series dark, context series gray" is the most common professional move.
Colormaps for Continuous Values
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(9)
x = rng.normal(size=400)
y = rng.normal(size=400) + 0.4 * x
z = rng.gamma(2, 1, size=400)
fig, ax = plt.subplots()
sc = ax.scatter(x, y, c=z, cmap="viridis", s=25, alpha=0.8)
fig.colorbar(sc, ax=ax, label="value")
plt.show()
Choose the map by the data's structure: sequential (viridis, plasma, Blues) for magnitude, diverging (RdYlBu, coolwarm) for values around a meaningful center like zero, categorical (tab10, Set2) for discrete groups. Viridis is the default for good reason - perceptually uniform and color-blind safe. Avoid rainbow/jet for scientific data: its brightness wiggles create false contours.
Custom Colormaps
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
# a 4-step sequential brand palette
cmap = ListedColormap(["#dbeafe", "#93c5fd", "#3b82f6", "#1e3a8a"])
import numpy as np
data = np.random.default_rng(10).random((8, 12))
fig, ax = plt.subplots()
ax.imshow(data, cmap=cmap, vmin=0, vmax=1)
plt.show()
ListedColormap takes an ordered list of colors; BoundaryNorm or vmin/vmax control where the data lands on it. Three to six steps is the practical range for a discrete-step map.
Working Rules
- Series count above ~8: collapse, facet, or filter - no palette survives that many lines.
- Grayscale printing: verify the figure in black and white before shipping; the tab palette is designed to survive it.
- Colorblind check: use the Coblis or similar simulator on the final figure; viridis + tab10 cover the common cases.
- One color, one meaning: never let red mean "danger" in one panel and just "series 4" in another without a legend.
Summary
Give series explicit or cyclic colors from a safe palette, map continuous values with viridis (sequential) or RdYlBu (diverging) plus a colorbar, use categorical maps for groups, and build brand palettes with ListedColormap. Test the result in grayscale and a colorblind simulator - the figure should survive both.