Plotting05/14
Starting sandbox…
Beginner·4 graded tests

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

[1]python
out1ms

This code completed without producing output.

Figure 1Figure 2Figure 3Figure 4Figure 5Figure 6

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

[2]python
out1ms

This code completed without producing output.

Figure 1

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

[3]python
out1ms

This code completed without producing output.

Figure 1Figure 2

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

[ ]python
out1ms

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 figsize after creation. fig.set_size_inches() works but changes relative font sizes.
  • Assuming panels share a scale when they do not.
  • axes being a 2-D array and indexing it as 1-D. Use axes.flat.
  • plt.subplots(2) vs plt.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.

exerciseEvery submission runs independently in a fresh namespace.

Test cases · 4

#viainputexpected stdout
1stdin7 4 100 rows=2 cols=4 axes=8 used=7 figsize=12.0x5.0in pixels=1200x500
2stdin6 3 150 rows=2 cols=3 axes=6 used=6 figsize=9.0x5.0in pixels=1350x750
3stdin1 1 300 rows=1 cols=1 axes=1 used=1 figsize=3.0x2.5in pixels=900x750
4stdin2 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.