Figure and Axes
In one line: a Figure is the canvas, an Axes is one panel on it — and "Axes" does not mean "axis".
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
fig, ax = plt.subplots() # one panel
fig, ax = plt.subplots(figsize=(6, 4)) # size in INCHES
fig, axes = plt.subplots(2, 3, figsize=(12, 6)) # a 2×3 grid → ndarray
fig, axes = plt.subplots(2, 2, sharex=True, sharey=True)
fig, (ax1, ax2) = plt.subplots(1, 2) # unpack
fig, axes = plt.subplots(2, 2, layout="constrained") # auto-fix overlaps
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
fig, ax = plt.subplots(figsize=(6, 4), dpi=100)
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
fig, axes = plt.subplots(2, 2, sharex=True, sharey=True)
fig, axes = plt.subplots(1, 3, sharey="row")
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
ax2 = ax.twinx() # shares x, independent y
ax2 = ax.twiny()
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