Colormaps and Color
In one line: match the colormap type to the data type, never use jet, and check it works in greyscale and for colourblind readers.
The three colormap types
| Type | Use for | Examples |
|---|---|---|
| Sequential | ordered data from a baseline: counts, coverage, expression, density | viridis, magma, Blues, rocket |
| Diverging | signed data with a meaningful midpoint: log fold change, correlation, z-score | vlag, RdBu_r, coolwarm, icefire |
| Qualitative | unordered categories: sample groups, chromosomes, cell types | tab10, Set2, colorblind |
Using the wrong type is a real error, not a style preference. A sequential colormap on a log fold change puts its midpoint at an arbitrary value; a qualitative palette on ordered data destroys the ordering.
Always center diverging maps
sns.heatmap(logfc_matrix, cmap="vlag", center=0)
sns.heatmap(corr, cmap="vlag", center=0, vmin=-1, vmax=1)
from matplotlib.colors import TwoSlopeNorm
ax.imshow(m, cmap="RdBu_r", norm=TwoSlopeNorm(vcenter=0, vmin=-3, vmax=5))
Without center=0, if your data runs −1 to +5 the colormap's white point lands at +2 and every genuinely unchanged gene is drawn as up-regulated. This is one of the most common quietly-misleading things in genomics figures.
Why not jet / rainbow
The jet colormap has three documented problems:
- Perceptually non-uniform. Equal steps in data are not equal steps in apparent colour. Its sharp cyan/yellow bands create visual edges where the data is smooth — inventing boundaries that do not exist.
- Not monotonic in lightness. Printed in greyscale it becomes unreadable — the yellow and cyan map to the same grey.
- Bad for colour vision deficiency. Roughly 8% of men with northern European ancestry have red-green CVD; jet's red and green extremes are its most important values.
viridis was designed to fix all three: perceptually uniform, monotonically increasing in lightness, and CVD-safe. It is the default for a reason.
Colourblind-safe choices
sns.set_palette("colorblind")
sns.color_palette("colorblind") # 10 distinguishable colours
# sequential and diverging maps that are already safe
"viridis", "cividis", "magma", "rocket" # sequential
"vlag", "icefire", "RdBu_r" # diverging
cividis is specifically designed to appear near-identical to viewers with and without CVD.
Never encode information in colour alone. Redundantly encode with marker shape, line style, direct labels, or position:
sns.scatterplot(data=df, x="pc1", y="pc2", hue="batch", style="condition")
ax.plot(x, y1, c="C0", ls="-", marker="o", label="control")
ax.plot(x, y2, c="C1", ls="--", marker="s", label="treated")
Check your figure: convert it to greyscale, or use a CVD simulator (daltonlens, or the Coblis web tool). If two series become indistinguishable, fix it before submitting.
Specifying colours
c="crimson" # named
c="#4C72B0" # hex
c=(0.3, 0.5, 0.7) # RGB floats
c=(0.3, 0.5, 0.7, 0.5) # RGBA
c="C0" # the current cycle's first colour
c="0.5" # greyscale string
Using "C0", "C1", ... rather than literal colours means your plots automatically follow whatever palette you set with sns.set_theme(). Change the theme once, every figure updates.
Palettes
sns.color_palette() # current default
sns.color_palette("colorblind", 8)
sns.color_palette("Set2", 5)
sns.color_palette("viridis", as_cmap=True)
sns.cubehelix_palette(8, start=.5, rot=-.75) # sequential, greyscale-safe
sns.diverging_palette(240, 10, as_cmap=True)
sns.light_palette("seagreen", as_cmap=True)
sns.set_palette("colorblind") # set globally
Explicit category → colour mapping is the habit that keeps a multi-panel figure coherent:
CONDITION_COLORS = {"control": "#4C72B0", "treated": "#DD8452", "drug": "#55A868"}
sns.scatterplot(data=df, x="a", y="b", hue="condition", palette=CONDITION_COLORS)
sns.boxplot(data=long, x="condition", y="expr", palette=CONDITION_COLORS)
Without this, seaborn assigns colours in the order categories appear — so "treated" can be blue in panel A and orange in panel B. Define the dict once at the top of your script.
Normalization
from matplotlib.colors import LogNorm, TwoSlopeNorm, BoundaryNorm
ax.imshow(counts, norm=LogNorm(vmin=1, vmax=1e4), cmap="viridis")
ax.imshow(logfc, norm=TwoSlopeNorm(vcenter=0), cmap="RdBu_r")
ax.imshow(m, norm=BoundaryNorm([0, 1, 5, 10, 50], ncolors=256), cmap="viridis")
ax.imshow(m, vmin=0, vmax=10) # clip the range
sns.heatmap(m, robust=True) # use 2nd/98th percentiles
LogNorm for count data. Expression and coverage span orders of magnitude; on a linear colour scale one outlier makes everything else uniformly dark.
robust=True in seaborn clips to the 2nd–98th percentile, which stops a single extreme value from consuming the entire colour range. Extremely useful, and worth disclosing in the caption.
Colorbars
im = ax.imshow(m, cmap="viridis")
cb = fig.colorbar(im, ax=ax, label="log$_2$ CPM", shrink=0.8)
cb.ax.tick_params(labelsize=8)
cb.set_ticks([0, 5, 10])
Always label the colorbar. An unlabelled colour scale is an unlabelled axis.
Transparency for overplotting
ax.scatter(x, y, alpha=0.1, s=2, rasterized=True)
Alpha stops helping past roughly 50,000 points — everything saturates to solid. At that scale use hexbin or a 2-D histogram. See Common Plot Types.
Bioinformatics conventions
# expression heatmaps: z-scored per gene, diverging, centred
sns.clustermap(mat, z_score=0, cmap="vlag", center=0)
# raw expression: sequential, log-normed
sns.heatmap(cpm, cmap="rocket", norm=LogNorm())
# correlation: diverging, full [-1, 1]
sns.heatmap(corr, cmap="vlag", center=0, vmin=-1, vmax=1, square=True)
# volcano: grey background, one accent for hits
colors = np.where(sig, "crimson", "lightgrey")
# Manhattan: two alternating neutral colours by chromosome
alt = ["#4C72B0", "#A9BCD8"]
# nucleotides: a widely used convention
BASE_COLORS = {"A": "#3DA853", "C": "#4285F4", "G": "#F9AB00", "T": "#EA4335"}
Grey for the bulk, colour for the point. A volcano plot where all 20,000 genes are coloured is 20,000 things competing for attention. Grey out the non-significant ones and the eye goes exactly where you want it.
Common mistakes
- jet / rainbow.
- Diverging colormap without
center=. - Sequential colormap on signed data.
- Qualitative palette on ordered data.
- Colour as the only encoding.
- Inconsistent category colours across panels of one figure.
- Unlabelled colorbar.
- Linear colour scale on count data spanning orders of magnitude.
- Too many categories — beyond ~8, colours stop being distinguishable. Group, facet, or label directly.
- Never checking in greyscale.
See also
Common Plot Types · seaborn Themes and Palettes · Applied - Heatmaps and Clustermaps · Axes Styling and Annotation · Saving Figures