5.4.15 · D4Scientific Computing (Python)

Exercises — Matplotlib — figure - axes architecture

2,703 words12 min readBack to topic

We will lean on this one picture the whole way down. Glance at it now:

Figure — Matplotlib — figure - axes architecture

The outer cream rectangle is the Figure (fig). The teal boxes inside are Axes (ax), each a self-contained plot with its own rulers. The orange squiggle and plum dot are Artists living inside one Axes. Keep pointing at this picture as you solve.


Level 1 — Recognition (name the box)

Recall Solution L1·Q1

WHAT we are doing: counting containers of each kind.

  • Figure: the whole canvas → becomes the one saved image → there is exactly 1.
  • Axes: each little plot with its own rulers → there are 4 (a 2×2 grid).
  • Artist: everything drawable; here each plot has (at least) one line → the lines are Line2D Artists, so ≥ 4 Artists just for the lines, plus every tick, spine, and label is also an Artist. Outermost first: Figure ⊃ Axes ⊃ Artist. Look at the picture: one cream sheet, four teal frames, drawings inside the frames.
Recall Solution L1·Q2
  • The label is set through the Axes method set_xlabel. (The label is conceptually attached to the x-Axis, but you reach it via the Axes.)
  • The object that manages tick positions on the horizontal ruler is ax.**xaxis**. Mnemonic (from parent): Axe-S = a Single subplot; Axi-S = a Single ruler.
Recall Solution L1·Q3

plt.subplots() returns two things in order: the Figure first (fig, the canvas) and the Axes (ax, the plot region) second. It also creates them for you — see Matplotlib — pyplot vs object-oriented API.


Level 2 — Application (write the right call)

Recall Solution L2·Q1

A title spanning the whole canvas is a Figure-level action → fig.suptitle("..."). Contrast: ax.set_title("...") titles one Axes only. Because the title is figure-wide, it must live on fig, not on any single ax.

Recall Solution L2·Q2

The grid is indexed axes[row, col], rows top→bottom, cols left→right, both starting at 0. Bottom-right of a 2×2 is row 1, col 1:

axes[1, 1].scatter([1, 2], [3, 1])

WHY index instead of plt.scatter? With 4 Axes there is no sensible "current" one, so plt.scatter would gamble on which box. Indexing removes the guess — see Matplotlib — subplots and GridSpec layout.

Recall Solution L2·Q3
call second return
(1,1) a single Axes object (not an array)
(1,3) a 1-D array of length 3 → axes[j]
(3,1) a 1-D array of length 3 → axes[i]
(2,4) a 2-D array shape (2,4)axes[i, j]
Rule: if nrows*ncols == 1 you get one Axes; if exactly one of them is 1 you get a 1-D array; otherwise a 2-D array. Total Axes count : here .

Level 3 — Analysis (why did it break?)

Recall Solution L3·Q1

WHAT happened: plt.plot is shorthand for gca().plot(...)get current Axes (create one if none exists). The first plt.plot created a Figure + one Axes and added a Line2D. The second plt.plot found that same current Axes and added a second Line2D to it. Result: ONE Figure, ONE Axes, TWO lines → one image with both lines overlaid. There is only one saved file because there is only one Figure. Fix for two images: make a new Figure each time — e.g. plt.figure() before the second plot, or explicitly fig1, ax1 = plt.subplots(); fig2, ax2 = plt.subplots() and save each.

Recall Solution L3·Q2

Subtle point: here plt.plot calls gca(), and because subplots() just made that Axes the current one, the line usually does land on ax in this simple case — so this particular snippet often works. The mistake is relying on that coincidence. If any other Axes had been created after (or in another call/thread), gca() could point elsewhere and fig would save empty. Robust version — never touch the hidden state:

fig, ax = plt.subplots()
ax.plot([0,1,2],[0,1,4])   # explicit: draw on THIS Axes
fig.savefig("out.png")

WHY: ax.plot guarantees the Line2D Artist enters the Axes you saved. See the blank-figure mistake in the parent note.

Recall Solution L3·Q3

WHAT a legend does: ax.legend() scans the labelled Artists in THAT Axes only and builds a box from their label= values. You labelled a line in axes[0], but asked axes[1] for a legend — a different box with no labelled Artists → empty (Matplotlib may even warn "No artists with labels found"). Fix: call axes[0].legend() (same box as the labelled line), or a figure-level fig.legend() to gather labels across all Axes.


Level 4 — Synthesis (build it correctly)

Recall Solution L4·Q1
import numpy as np, matplotlib.pyplot as plt
x = np.linspace(0, 2*np.pi, 200)
fig, ax = plt.subplots(figsize=(5,3))       # 1 Figure, 1 Axes
ax.plot(x, np.sin(x), label="sin")          # Line2D → ax
ax.plot(x, np.cos(x), label="cos")          # 2nd Line2D → same ax
ax.set_xlabel("x [rad]")                     # label on THIS Axes
ax.set_title("waves")                        # title on THIS Axes
ax.legend()                                  # reads labels in THIS Axes
fig.savefig("w.png", dpi=150)                # saving is Figure-level

WHY each object: lines/labels/legend are per-Axes → call on ax. Saving produces the whole image → call on fig. Because both lines belong to the same ax, they share coordinates and the one legend lists both. See Saving figures — dpi, formats, bbox for the dpi choice.

Recall Solution L4·Q2
fig, axes = plt.subplots(2, 3, figsize=(9, 5))
for i, ax in enumerate(axes.flat):          # .flat iterates the 2-D array row-major
    ax.plot(data[i])                         # each ax is one subplot
    ax.set_title(f"panel {i}")
fig.tight_layout()                           # Figure relayouts all 6 to avoid overlap

WHY axes.flat? plt.subplots(2,3) returns a 2-D array shape (2,3). .flat gives a flat iterator in row-major order (panel 0 = top-left, panel 5 = bottom-right), so enumerate numbering matches the grid reading order. tight_layout is fig-level because relayout affects all Axes at once.

Recall Solution L4·Q3
fig, axes = plt.subplots(3, 1, figsize=(4, 7))
fig.suptitle("shared title")                 # Figure-wide → fig
ylabels = ["volts", "amps", "watts"]
for ax, yl in zip(axes, ylabels):            # 1-D array (ncols==1)
    ax.set_ylabel(yl)                         # per-Axes → ax
fig.tight_layout()

WHY: "spanning all plots" ⇒ Figure method (suptitle, tight_layout); "belongs to one plot" ⇒ Axes method (set_ylabel). A 3×1 grid returns a 1-D array, so a plain for ax in axes loop works — no double indexing needed.


Level 5 — Mastery (design a robust routine)

Recall Solution L5·Q1
import matplotlib.pyplot as plt
 
def plot_series(ax, x, y, *, label, xlabel, ylabel):
    line, = ax.plot(x, y, label=label)   # Line2D added to the GIVEN ax
    ax.set_xlabel(xlabel)
    ax.set_ylabel(ylabel)
    ax.legend()
    return line                          # hand back the Artist for later styling
 
fig, axes = plt.subplots(1, 2, figsize=(8, 3))
plot_series(axes[0], [0,1,2], [0,1,4], label="sq",  xlabel="t", ylabel="p")
plot_series(axes[1], [0,1,2], [2,1,0], label="lin", xlabel="t", ylabel="q")
fig.suptitle("two panels")               # Figure-wide title
fig.tight_layout()
fig.savefig("panels.png", dpi=150)

WHY pass ax in (never call plt. inside): the function never touches hidden global state, so the caller decides which box gets drawn on. The same function works for a single plot, a 2×2 grid cell, or a GridSpec panel — no ambiguity, no "current Axes" surprises. This is the professional pattern used throughout Matplotlib — pyplot vs object-oriented API and Matplotlib — Artists, Line2D and styling (returning the line lets callers restyle it later). Count check: 1 Figure, 2 Axes, 2 Line2D from our function (+ ticks/spines Artists auto-created). One saved image.

Recall Solution L5·Q2
  • (a) 1 Figure.
  • (b) Axes.
  • (c) one ax.plot per Axes → 12 Line2D Artists added (plus many auto Artists — ticks, spines — you did not add).
  • (d) axes shape = (3, 4), a 2-D array.
  • (e) row-major: (0,0), then (0,1), then (0,2). WHY row-major: .flat walks the last index fastest, i.e. along columns within a row before dropping to the next row — matching normal reading order.

Recall Feynman check — the whole page in one breath

Every bug here was the same bug wearing a different hat: you drew in a box you didn't name, or you named the wrong box. Name your fig and your ax, draw only on ax, save only through fig, and put figure-wide things (suptitle, tight_layout, savefig) on fig. Do that and the "current Axes" ghost can never haunt you.


Connections

  • Parent: figure/axes architecture
  • Matplotlib — pyplot vs object-oriented API
  • Matplotlib — subplots and GridSpec layout
  • Matplotlib — Artists, Line2D and styling
  • NumPy — arrays as plot data
  • Saving figures — dpi, formats, bbox
  • Scientific Computing (Python)