Making it fit
constrained layout, figure-level labels, and one legend for the lot
layout="constrained"fig.tight_layoutfig.suptitlefig.supxlabelfig.legendsubplots_adjustWatch it happen
Play it through, or step back and forth yourself.
fig, axes = plt.subplots(2, 2) # the default. it will overlap.
Put four panels in a figure and matplotlib's default spacing lets the y label of one collide with the ticks of its neighbour. Nothing is broken — the default was chosen long before anyone put labels on everything.
The idea
Put four labelled panels in one figure with matplotlib's default spacing and the y label of one will collide with the tick labels of its neighbour. Nothing is broken — the defaults predate the habit of labelling everything — but it does have to be fixed every single time.
constrained layout
fig, axes = plt.subplots(2, 2, layout="constrained")That's the whole recommendation. It measures every label, title, tick and colourbar and solves for spacing that fits, and it keeps solving as the figure changes. Pass it at creation and stop thinking about spacing.
Why not tight_layout
fig.tight_layout() is the older answer and it does something subtly different: it adjusts once, at the moment you call it. Add a suptitle afterwards, or a colourbar, and the spacing you computed is stale — which is exactly the situation where people conclude matplotlib's layout is broken.
Concretely:
- tight_layout: one-shot, must be called last, often misses colourbars, doesn't support outside legends.
- constrained: continuous, set once at creation, handles colourbars and outside legends.
You can also set it after the fact with fig.set_layout_engine("constrained"), which is handy when a library handed you the figure.
Say it once
Four panels with the same y label means four copies of the same word. Promote shared meaning to the figure:
fig.suptitle("Chai sales, March", fontsize=14)
fig.supxlabel("day")
fig.supylabel("cups")suptitle is a figure title, distinct from each Axes' own set_title — that difference from lesson 1 finally earns its keep here. Keep the per-panel titles for what varies (the category), and put what's shared at the top.
One legend for all
handles, labels = axes.flat[0].get_legend_handles_labels()
fig.legend(handles, labels, loc="outside upper right")If every panel plots the same two series, one key serves all of them. The "outside …" positions place the legend beyond the panels rather than on top of the data — and they only work with constrained layout, which is one more reason to have it on.
get_legend_handles_labels() is the piece people miss: it hands you the artists and their labels from one Axes so you can hand them to the figure.
Manual spacing
fig.subplots_adjust(hspace=0.4, wspace=0.3, top=0.9, left=0.1)Still there when you need exact control — a figure that has to match a template, say. Note that it conflicts with the layout engines: use both and one silently overrides the other, which is a confusing hour to spend. Pick one, and almost always pick constrained.
A checklist for a multi-panel figure
layout="constrained"at creation.figsizescaled to the grid — roughly 3×2.2 inches per panel.- Share the axis you're comparing on.
- Shared labels to the figure, distinguishing labels to the panels.
- One legend, outside.
bbox_inches="tight"when you save, as a belt-and-braces crop.
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.
tryremoving layout="constrained" and watching the labels collide again.
Your turn
3 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Make a 2×2 grid using constrained layout, and return the engine's class name: fig.get_layout_engine().__class__.__name__.
Give a 1×2 figure the figure-level title "Chai sales" and the figure-level x label "day". Return the two texts as a list, in that order.
Plot a labelled line on the first panel of a 1×2 grid, then collect its handles with get_legend_handles_labels() and put one legend on the figure. Return len(fig.legends).
