pythonforbio.
[WASM idle]
Plotting07/14

Matplotlib Artists

Starting sandbox…
Beginnerlesson

Matplotlib Artists

In one line: everything drawn is an object with getters and setters — which is why matplotlib never dead-ends.

The idea

Every visible element is an Artist. Plotting functions return the Artists they created, so you can keep them and modify them later.

lines = ax.plot(x, y)                 # returns a LIST of Line2D
line = lines[0]
line.set_color("crimson")
line.set_linewidth(2)
line.set_linestyle("--")
line.set_alpha(0.7)
line.set_label("treated")
line.set_zorder(10)                   # draw on top

ax.plot returning a list trips people up — it can plot multiple lines at once. The idiom is line, = ax.plot(...) (note the comma) to unpack a single one.

The Artist families

Class What it is Created by
Line2D a line, or markers ax.plot
PathCollection many markers as one object ax.scatter
Rectangle / Patch bars, spans, shapes ax.bar, ax.axvspan
Text any text ax.set_title, ax.annotate
AxesImage a raster image ax.imshow
QuadMesh a mesh ax.pcolormesh, sns.heatmap
PolyCollection filled regions ax.fill_between
Legend the legend box ax.legend()
Spine the panel borders automatic

Note scatter returns one PathCollection for all points, not one Artist per point. That is why scatter is fast, and why you set properties on the collection as a whole (or with per-point arrays).

Get and set

Every property has a matched pair:

line.get_color(); line.set_color("red")
ax.get_xlim();    ax.set_xlim(0, 10)
text.get_fontsize(); text.set_fontsize(8)

line.set(color="red", lw=2, alpha=0.5)     # several at once
plt.getp(line)                              # list every property
plt.setp(lines, color="grey", alpha=0.3)    # set on many Artists at once

plt.getp(artist) is the discovery tool — it prints everything you can change on that object.

Finding Artists after the fact

ax.lines            # all Line2D
ax.collections      # scatter, fill_between
ax.patches          # bars, shapes
ax.texts            # annotations
ax.images
ax.get_children()   # everything
ax.get_legend()
ax.get_xticklabels()

This is how you modify a plot someone else drew — including one seaborn drew:

ax = sns.boxplot(data=df, x="condition", y="value")
for patch in ax.patches:
    patch.set_alpha(0.6)
    patch.set_edgecolor("black")

Any seaborn plot is a bag of Artists you can post-process. That is the escape hatch for "seaborn almost does what I want".

zorder — draw order

ax.scatter(x, y, zorder=3)
ax.axhline(0, zorder=1)          # behind the points
ax.grid(True, zorder=0)

Higher zorder draws on top. Defaults: images 0, patches 1, lines 2, text 3. The usual need is pushing gridlines behind data — which requires ax.set_axisbelow(True) for gridlines specifically, because they are a special case.

Text and annotation

ax.text(x, y, "TP53", fontsize=8, ha="center", va="bottom")

ax.annotate(
    "TP53",
    xy=(x, y),                              # the point
    xytext=(x + 0.5, y + 1),                # where the label goes
    arrowprops=dict(arrowstyle="->", lw=0.8, color="grey"),
    fontsize=8,
)

# coordinates relative to the Axes rather than the data
ax.text(0.02, 0.98, "A", transform=ax.transAxes,
        fontsize=12, fontweight="bold", va="top")

transform=ax.transAxes uses (0,0)=bottom-left, (1,1)=top-right of the panel, independent of the data range. This is how you place panel labels (A, B, C) consistently across a multi-panel figure.

Matplotlib renders LaTeX-style maths in any text:

ax.set_xlabel(r"log$_2$ fold change")
ax.set_ylabel(r"$-$log$_{10}$ $p_{\mathrm{adj}}$")
ax.set_title(r"$\Delta$ expression")

Use a raw string (r"...") so backslashes survive.

Labelling selected points

The archetypal bioinformatics annotation task:

top = df.nlargest(10, "neglog10p")
for _, row in top.iterrows():
    ax.annotate(row["gene"], (row["log2fc"], row["neglog10p"]),
                fontsize=7, xytext=(3, 3), textcoords="offset points")

Labels will overlap. The adjustText package repels them automatically and is worth installing:

from adjustText import adjust_text
texts = [ax.text(r.log2fc, r.neglog10p, r.gene, fontsize=7)
         for r in top.itertuples()]
adjust_text(texts, ax=ax, arrowprops=dict(arrowstyle="-", lw=0.5, color="grey"))

Legend control

ax.legend(frameon=False, loc="upper right", fontsize=8, ncol=2)
ax.legend(bbox_to_anchor=(1.02, 1), loc="upper left")     # outside the panel
ax.legend(title="Condition")

handles, labels = ax.get_legend_handles_labels()
ax.legend(handles[:3], labels[:3])                         # subset

from matplotlib.lines import Line2D
custom = [Line2D([0], [0], marker="o", color="w",
                 markerfacecolor="crimson", label="significant")]
ax.legend(handles=custom)

Building custom handles is how you produce a legend for something matplotlib did not label automatically — e.g. a scatter coloured by a continuous array.

Only artists with a label that does not start with _ appear. ax.plot(x, y, label="_nolegend_") hides one deliberately.

Rasterizing for large scatters

ax.scatter(x, y, s=1, rasterized=True)
fig.savefig("out.pdf", dpi=300)

A vector PDF with 500,000 scatter points is enormous and crashes viewers. rasterized=True renders just that Artist as pixels while keeping text and axes as vectors — small file, editable labels, no crash. This is essential for volcano plots, Manhattan plots and single-cell embeddings.

Common mistakes

  • Forgetting ax.plot returns a list. Use line, = ax.plot(...).
  • Modifying an Artist after savefig and expecting the file to change.
  • Losing the return value and then trying to find the Artist by digging through ax.get_children().
  • Text in data coordinates when you meant Axes coordinates.
  • Overlapping labels. Use adjustText or label fewer points.
  • Non-rasterized huge scatters in a vector output.
  • zorder with gridlines — needs ax.set_axisbelow(True).
  • Non-raw strings with LaTeX, so \D becomes an escape sequence.

See also

Figure and Axes · Axes Styling and Annotation · Saving Figures · Colormaps and Color · Applied - Differential Expression Volcano Plot

scratch

No output yet — run the code to populate this drawer.