Plotting02/14
Starting sandbox…
Beginner·4 graded tests

Colormaps and Color

Colormaps and Color

Core Principle: Colormap selection must reflect data topology (sequential, diverging, or categorical), ensuring perceptual uniformity and accessibility across grayscale printing and color-vision deficiencies.

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:

  1. 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.
  2. Not monotonic in lightness. Printed in greyscale it becomes unreadable: the yellow and cyan map to the same grey.
  3. 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

[ ]python
out1ms

This code completed without producing output.

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

Exercise

Write normalise(values), which takes a list of floats read from a single whitespace-separated line and decides which normalization the data calls for. Apply the rules in order: if the values straddle zero use TwoSlopeNorm(vcenter=0.0, vmin=lo, vmax=hi) with a diverging colormap; if they are all strictly positive and hi / lo is at least 100 use LogNorm(vmin=lo, vmax=hi); otherwise use Normalize(vmin=lo, vmax=hi). Attach the chosen norm and colormap to an image with ax.imshow([values], ...) and read every value's position back through im.norm, so the reported numbers come from the artist rather than from arithmetic you repeated by hand.

Return a list of lines: the normalised position of each value in input order, formatted to three decimal places and space separated; the rule name, one of twoslope, log, linear or degenerate; and above=<n>, the count of positions strictly greater than 0.5. When every value is identical no norm is well defined, so return degenerate with every position reported as 0.500 and above=0. When the input is empty return the single line none.

exerciseEvery submission runs independently in a fresh namespace.

Test cases · 4

#viainputexpected stdout
1stdin2.61 0.98 0.94 -1.02 -0.04 0.01 1.000 0.688 0.680 0.000 0.480 0.502 twoslope above=4
2stdin1.0 19.0 120.0 143.0 88.0 15260.0 0.000 0.306 0.497 0.515 0.465 1.000 log above=2
3stdin1.2 1.5 0.9 2.0 0.273 0.545 0.000 1.000 linear above=2
4stdin3.5 3.5 3.5 0.500 0.500 0.500 degenerate above=0

Hints · 3

01Hint

`TwoSlopeNorm` requires vmin < vcenter < vmax strictly, so test `lo < 0 < hi` before constructing it.

02Hint

`LogNorm` is undefined at or below zero — guard the branch with `lo > 0`.

03Hint

A norm called on a single scalar returns a scalar; wrap it in `float()` before rounding.