Figure and Axes
Figure and Axes
Core Principle: Matplotlib architectures separate the top-level container canvas (
Figure) from individual plotting areas (Axes), which contain coordinate systems, ticks, labels, and graphical artists.
The container hierarchy
Figure the whole image; owns size, DPI, saving
├─ Axes one plotting panel
│ ├─ XAxis / YAxis the actual axes: ticks, labels, scale
│ ├─ Line2D plotted lines
│ ├─ Collection scatter points, patches in bulk
│ ├─ Text titles, labels, annotations
│ └─ Spines the four border lines
├─ Axes another panel
├─ Legend
└─ SuptitleText
The naming is genuinely unfortunate. Axes = a panel. A 2×2 grid contains four Axes. The x-axis of one panel is ax.xaxis. Once you accept it, everything else follows.
Creating them
This code completed without producing output.
plt.subplots() returns (Figure, Axes) for a single panel and (Figure, ndarray of Axes) for a grid. axes.flat iterates a grid regardless of its shape:
for ax, gene in zip(axes.flat, genes):
ax.plot(...)
ax.set_title(gene)
Figure size and DPI
This code completed without producing output.
figsize is in inches. Pixel dimensions are figsize × dpi. For publication, set the figure to the journal's column width and let the DPI handle resolution:
| Target | figsize |
|---|---|
| Single journal column | (3.5, 2.5) |
| Double column | (7, 4) |
| Full page | (7, 9) |
| Slide | (10, 5.6): 16:9 |
Sizing the figure correctly at creation time is the difference between crisp 8 pt labels and a figure that was shrunk to 40% in the manuscript and is now illegible. Never resize a matplotlib figure in a word processor. Set figsize and font sizes so it is correct at final print size.
The Axes method surface
# plotting
ax.plot(x, y); ax.scatter(x, y); ax.bar(x, h); ax.hist(v, bins=50)
ax.errorbar(x, y, yerr=e); ax.fill_between(x, lo, hi)
ax.imshow(matrix); ax.boxplot(data); ax.violinplot(data)
# labels and limits
ax.set_xlabel("..."); ax.set_ylabel("..."); ax.set_title("...")
ax.set_xlim(0, 100); ax.set_ylim(bottom=0)
ax.set(xlabel="...", ylabel="...", title="...", xlim=(0, 100)) # all at once
# scales
ax.set_xscale("log"); ax.set_yscale("log")
ax.set_yscale("symlog", linthresh=1) # log that handles zero and negatives
# reference lines
ax.axhline(0, ls="--", lw=1, c="grey")
ax.axvline(x=1); ax.axvspan(10, 20, alpha=0.2)
# ticks
ax.set_xticks([0, 1, 2]); ax.set_xticklabels(["a", "b", "c"])
ax.tick_params(axis="x", rotation=45, labelsize=8)
# appearance
ax.grid(True, alpha=0.3)
ax.legend(frameon=False, loc="upper right")
ax.spines[["top", "right"]].set_visible(False) # the "despine" idiom
ax.set(...) is worth adopting: one call instead of four.
Figure-level methods
fig.suptitle("Overall title", fontsize=14)
fig.savefig("out.pdf", bbox_inches="tight", dpi=300)
fig.tight_layout() # legacy layout fixer
fig.colorbar(mappable, ax=ax, label="log2 expression")
fig.legend(...) # one legend for the whole figure
fig.align_ylabels(axes) # line up y-labels across panels
plt.close(fig) # free memory: essential in loops
Sharing axes
This code completed without producing output.
Shared axes mean the same scale in every panel, which is what makes small multiples comparable. It also removes the redundant inner tick labels automatically.
If panels do not share a scale, say so prominently: a reader will assume they do, and a 10× difference in y-range between adjacent panels is genuinely misleading.
Reaching into a seaborn plot
Axes-level seaborn functions return the Axes; figure-level ones return a grid object:
ax = sns.scatterplot(data=df, x="a", y="b") # ax is a matplotlib Axes
ax.axhline(0)
g = sns.relplot(data=df, x="a", y="b", col="gene")
g.figure # the Figure
g.axes # ndarray of Axes
g.axes_dict["TP53"] # by facet value
g.axes.flat[0].axhline(0)
Everything in this note applies to seaborn output. See Figure-level vs Axes-level.
Twin axes
This code completed without producing output.
Useful for plotting coverage and GC content on one panel. Also a well-known way to mislead: two arbitrary y-scales can make any two series look correlated. Use it sparingly and label both axes unambiguously.
Common mistakes
- Confusing Axes with axis.
- Resizing after saving: the save captured the state at that moment.
- Not closing figures in a loop → "More than 20 figures have been opened" and growing memory. Always
plt.close(fig). - Setting
figsizeafter creation.fig.set_size_inches()works but changes relative font sizes. - Assuming panels share a scale when they do not.
axesbeing a 2-D array and indexing it as 1-D. Useaxes.flat.plt.subplots(2)vsplt.subplots(2, 2): the first gives 2 stacked panels, not 4.
See also
pyplot vs Object-Oriented API · Subplots and Layout · Matplotlib Artists · Saving Figures · Axes Styling and Annotation · Figure-level vs Axes-level
Exercise
Write panel_geometry(n_panels, ncol, dpi), reading the three integers from a
single input line. Never request more columns than there are panels: clamp
ncol to at least one and at most n_panels, then take the row count by
ceiling division. Build the grid with plt.subplots, allocating 3.0 inches of
width and 2.5 inches of height per panel, passing the requested dpi, and
passing squeeze=False so a one-panel figure still yields a two-dimensional
array rather than a bare Axes. Hide any panel beyond n_panels.
Return three lines: rows=<n> cols=<n> axes=<n> used=<n>, where axes counts
every Axes in the grid and used counts the visible ones; figsize=<w>x<h>in
with both numbers read back from fig.get_size_inches() and rounded to three
decimals; and pixels=<w>x<h>, the inch dimensions multiplied by
fig.get_dpi() and rounded to the nearest integer. A single panel is the case
that catches a careless implementation, because without squeeze=False
plt.subplots returns an Axes with no .flat attribute.
Test cases · 4
| # | via | input | expected stdout |
|---|---|---|---|
| 1 | stdin | 7 4 100 | rows=2 cols=4 axes=8 used=7 figsize=12.0x5.0in pixels=1200x500 |
| 2 | stdin | 6 3 150 | rows=2 cols=3 axes=6 used=6 figsize=9.0x5.0in pixels=1350x750 |
| 3 | stdin | 1 1 300 | rows=1 cols=1 axes=1 used=1 figsize=3.0x2.5in pixels=900x750 |
| 4 | stdin | 2 5 100 | rows=1 cols=2 axes=2 used=2 figsize=6.0x2.5in pixels=600x250 |
Hints · 3
01Hint
`fig.get_size_inches()` returns a two-element array of numpy floats — convert them before formatting.
02Hint
Pixel dimensions are inches times DPI; that is the whole relationship between `figsize` and `dpi`.
03Hint
Clamp the column count before the ceiling division, or four panels in six columns claims two rows.