Matplotlib·Lesson 18·11 min·0/3 exercises

Charts you can reuse

The ax=None convention, and sizing a figure for where it is going

ax=Noneplt.gca**kwargsfig.set_size_inchesplt.closefigure factories

Watch it happen

Play it through, or step back and forth yourself.

fig, ax = plt.subplots(
    figsize=(7, 4))
ax.plot(days, delhi)
ax.set_ylabel("cups")
ax.set_title("Delhi")
ax.grid(alpha=0.3)
ax.spines["top"].set_visible(
    False)
fig, ax = plt.subplots(
    figsize=(7, 4))
ax.plot(days, mumbai)
ax.set_ylabel("cups")
ax.set_title("Mumbai")
ax.grid(alpha=0.3)
ax.spines["top"].set_visible(
    False)
fig, ax = plt.subplots(
    figsize=(7, 4))
ax.plot(days, pune)
ax.set_ylabel("cups")
ax.set_title("Delhi")
ax.grid(alpha=0.3)
ax.spines["top"].set_visible(
    False)← still says Delhi

Every analysis notebook has the same twelve lines of chart code, pasted six times with small differences. The differences are the bugs — one of them still says "Delhi" in the title.

The idea

Every analysis notebook eventually contains the same twelve lines of chart code, pasted six times with small differences — and the differences are where the bugs live. One of them still says "Delhi" in the title.

The convention

matplotlib's own gallery uses a pattern that almost no tutorial mentions, and it is the whole answer:

def trend(values, label, ax=None, **kwargs):
    if ax is None:
        ax = plt.gca()

    ax.plot(days, values, label=label, **kwargs)
    ax.set_ylabel("cups")
    ax.set_title(label, loc="left")
    return ax

Four rules, and each one is load-bearing:

  • Take an ax. The caller decides where the chart goes.
  • Default it to plt.gca(). So it still works as a one-liner when nobody cares.
  • Return it. So the caller can keep customising afterwards.
  • Never call plt.figure(), show() or savefig() inside. Those are the caller's decisions, and a function that makes them for you composes with nothing.

Why it composes

trend(delhi, "Delhi")                          # standalone — gca() makes one

fig, axd = plt.subplot_mosaic([["a", "b"]])
trend(delhi, "Delhi", ax=axd["a"])              # into a layout

fig, axes = plt.subplots(2, 3, sharey=True, layout="constrained")
for ax, item in zip(axes.flat, items):          # once per group
    trend(orders[orders["item"] == item]["cups"], item, ax=ax)

Same function, three quite different figures, no branching inside it. That is the entire return on those three extra lines.

**kwargs forwarded to the plotting call keeps it flexible without you having to predict every option — trend(delhi, "Delhi", color="C1", linestyle="--") just works.

Size for where it is going

A figure isn't "right" in the abstract; it's right for a destination. The same chart needs genuinely different settings for a slide and for a paper:

# a slide      figsize=(10, 5.6), font.size=14   big text, few elements, PNG 150dpi
# a paper      figsize=(3.5, 2.6), font.size=8   one column wide, PDF or SVG
# a README     figsize=(8, 4),   font.size=11    PNG — and check it on a phone
# a dashboard  figsize=(6, 3),   font.size=10    SVG, scales with the page

The counter-intuitive one is the paper figure. It's small, so it needs fewer elements and relatively larger text — the instinct to shrink the font to fit more in is exactly backwards. Whatever the destination, look at the exported file at its real size, not the notebook preview.

A save helper

SIZES = {"slide": (10, 5.6), "paper": (3.5, 2.6), "web": (8, 4)}

def save(fig, name, dest="web"):
    fig.set_size_inches(*SIZES[dest])
    fig.savefig(f"out/{name}-{dest}.png", dpi=150, bbox_inches="tight")

fig.set_size_inches resizes after the fact, so one figure can be exported for several destinations without rebuilding it.

Generating a report

for item in items:
    with plt.style.context("chai.mplstyle"):
        fig, ax = plt.subplots(layout="constrained")
        trend(orders[orders["item"] == item]["cups"], item, ax=ax)
        save(fig, item, "slide")
        plt.close(fig)

plt.close(fig) is not optional in a loop. pyplot keeps every figure you create in a global registry; twenty charts is twenty figures held in memory, and matplotlib warns you at twenty. Close as you go.

The habit worth keeping

When you catch yourself editing a chart for the third time, stop and make it a function. Every chart in the report then looks the same, every change is made once, and re-running it after the data updates costs nothing — which is what makes it worth improving at all.

See it run

The lesson's code, ready to run and to fiddle with.

Putting the kettle on…

Starting up…

Worked example

not graded

Already written and ready to go — press Run to see what it does, then change a number, a column name, anything, and run it again.

trycalling trend(delhi, "Delhi") with no ax= at all and seeing gca() step in.

Press Run — the output appears here.

Your turn

3 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.

Write a function trend(values, ax=None) that follows the convention: fall back to plt.gca(), plot values against days, and return the Axes. Call it with an Axes you made, and return [type(out).__name__, out is ax].

your answer

Extend it with **kwargs forwarded to ax.plot, then call it with linestyle="--". Return the line's linestyle.

your answer

Make a figure, then resize it for a slide with fig.set_size_inches(10, 5.6) and return its pixel size at 150 dpi as a list of two ints.

your answer