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 factoriesWatch 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 DelhiEvery 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 axFour 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()orsavefig()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 pageThe 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 gradedAlready 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.
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].
Extend it with **kwargs forwarded to ax.plot, then call it with linestyle="--". Return the line's linestyle.
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.
