Subplots

Several axes on one figure - the grid, the shared axes, and the layout that stops them colliding.

Overview

The grid

A single figure with multiple panels is called a subplot layout. Each panel is its own Axes object with its own data, limits, labels and legend. The panels share the figure's canvas and can share axes to make comparisons easier.

Subplots are the right approach when you want to compare the same kind of chart across groups, show related variables side by side, or combine different chart types into one display. A grid of small multiples -- the same chart repeated for each category -- is one of the most effective visual patterns, and subplots are how you build it in matplotlib.

plt.subplots(rows, cols) creates the grid and returns the figure and an array of Axes objects. From there, you index into the array to draw on each panel. The array follows NumPy conventions: axes[row, col] for a 2-D grid, axes[i] for a single row or column.

This page covers the basic grid, the shape surprise when the grid is one-dimensional, shared axes, unequal panel sizes with gridspec, the mosaic layout for named panels, insets, removing unused panels, and the spacing controls that keep the layout clean.

The most common issue with subplots is overlapping labels. Titles, axis labels and tick labels can collide when the panels are small. fig.tight_layout() adjusts the spacing automatically and fixes most cases. For finer control, gridspec parameters and constrained_layout=True give you explicit padding between panels.

Shared axes are worth knowing about early. When panels show the same variable, sharing the y axis gives them the same scale. Without it, the reader might compare bar heights across panels that use different ranges. sharey=True in the subplots call handles this. It also removes the redundant tick labels from inner panels, which reduces clutter.

The mosaic layout, available through plt.subplot_mosaic, lets you name each panel and arrange them in a text grid. That makes it easier to build layouts where one panel is wider or taller than the others. The editors on this page show both the regular grid and the mosaic approach.

fig, axes = plt.subplots(2, 3) creates a figure with six axes and returns them as a NumPy array.

axes[0, 2] is the top-right panel. axes.flat iterates in reading order. This Is what you want when the panels match to a flat list of groups:

for ax, name in zip(axes.flat, names):
    ...

zip stops at the shorter of the two. A grid with more cells than data leaves the extras blank. Visible. Often worth dropping with ax.drop() or hiding with ax.axis("off").

Worth knowing

subplots(r, c) returns an array of axes; axes.flat iterates in reading order.
The array is 2-D only for a true grid — 1xN gives 1-D and 1x1 gives a bare Axes. squeeze=False makes it always 2-D.
sharex/sharey are what make a grid a comparison; without them each panel scales to its own data.
add_gridspec with height_ratios handles panels of different sizes; gs[0, :] spans a row.
subplot_mosaic takes a picture of the layout as a string and returns a dict keyed by the letters.
tight_layout() or constrained_layout=True stop labels colliding — a figure cropped when saved is usually missing one.

Subplots

a few axes on one figure.

A grid of axes

subplots(rows, cols) returns an array you index or iterate.

example_01.pymatplotlib
Output

squeeze, and the shape surprise

A 1xN grid gives a 1-D array, which breaks code written for 2-D.

example_02.pymatplotlib
Output

Sharing axes

So panels can be compared, and the labels stop repeating.

example_03.pymatplotlib
Output

Panels of different sizes

gridspec when the grid is not uniform.

example_04.pymatplotlib
Output

subplot_mosaic reads better

A picture of the layout, in a string.

example_05.pymatplotlib
Output

Making room

tight_layout and constrained_layout both stop labels colliding.

example_06.pymatplotlib
Output

The shape surprise

The returned array's shape isn't always what code expects.

subplots(2, 3) gives a (2, 3) array. subplots(1, 3) gives a 1-D array of three. subplots(1, 1) gives a single Axes, not an array at all. Which is what makes fig, ax = plt.subplots() work.

That is convenient interactively and awkward in a function that takes the grid size as a param. axes[0, 1] fails on the 1-D case.

squeeze=False forces a 2-D array always. In reusable code it's the right default, and it costs one arg.

Sharing

sharex=True and sharey=True make the panels use the same limits.

This isn't a cosmetic setting. Without it, each panel autoscales to its own data. A bar that reaches the top of one panel may represent a smaller number than a shorter bar in the panel beside it. The grid looks like a compare and isn't one.

Sharing also hides the inner tick labels. This Drops a great deal of repetition from a grid.

sharex="col" and sharey="row" share within columns or rows. This Suits a mat where each row is a other quantity.

The trade-off: with shared axes, a panel whose data occupies a small part of the shared range is compressed. When the panels truly have other scales, small multiples with split axes and clearly labelled ranges are more honest. But then the reader must be told not to compare heights right.

Uneven grids

fig.add_gridspec(rows, cols) creates a grid you then slice:

gs = fig.add_gridspec(2, 3, height_ratios=[2, 1])
big = fig.add_subplot(gs[0, :])     # whole top row
small = fig.add_subplot(gs[1, 0])   # bottom left

height_ratios and width_ratios make rows and columns other sizes. This Is how you give a main chart more space than the supporting ones.

subplot_mosaic

plt.subplot_mosaic takes a picture of the layout:

fig, axd = plt.subplot_mosaic("""
    AAB
    CCB
""")

Repeated letters span cells, and the result is a dict keyed by the letters. axd["A"] is far easier to follow than gs[0, :2], and the layout is visible in the source rather than coded in slice math.

A . in the string leaves a cell empty.

It's the most readable way to build an uneven layout. Worth preferring wherever it fits.

Layout

Labels, titles and tick text are drawn outside the axes. Matplotlib doesn't account for them when placing panels. The result is shared labels, or a title running off the top.

Two methods fix it.

fig.tight_layout() is called after all is drawn and adjusts spacing once. It's simple and works for most cases. It can be confused by artists added later. It doesn't handle colorbars above all well.

constrained_layout=True is set when the figure is created and keeps adjusting as things are added. It handles colorbars, suptitles and legends outside the axes better. It's the one to reach for on a complex figure.

Neither is on by default. fig.subplots_adjust(hspace=..., wspace=...) sets spacing manually when you want precise control.

The symptom of missing layout management is a figure that looks acceptable on screen and comes out cropped when saved. Because savefig uses the figure's declared size, not what the screen happened to show. bbox_inches="tight" on savefig is the other half of that fix.

Removing unused panels

A 3×4 grid holding ten charts leaves two empty boxes with ticks and spines. This Look like a mistake.

ax.drop() deletes the axes fully. ax.axis("off") keeps it but hides all. This Preserves the spacing. handy when the grid should stay rectangular.

for ax in axes.flat[len(items):]:
    ax.remove()

The empty cell is also a fair place for a legend or a note, using ax.axis("off") and ax.legend(...) with handles gathered from the other panels.

Titles and labels across a grid

With shared axes, per-panel axis labels are repetition. fig.supxlabel and fig.supylabel label the grid once.

Panel titles remain handy and should be small. They're finding the panel, not heading a chart. fontsize=9 and loc="left" reads well.

For a formal figure, lettering the panels is standard:

for letter, ax in zip("abcdef", axes.flat):
    ax.text(0.02, 0.95, letter, transform=ax.transAxes,
            fontweight="bold", va="top")

Axes coords, so the letter stays in the corner even so of the data.

Iterating in the right order

axes.flat iterates row by row. This Matches how the panels will be read.

zip(axes.flat, items) pairs them and stops at the shorter. This Is convenient and silently drops items if the grid is too small. Asserting the sizes match is one line and blocks a chart quietly missing a group:

assert len(items) <= axes.size

For a grid indexed by two vars, axes[i, j] with clear loops is clearer than flattening. The spot then carries meaning.

When not to use subplots

Small multiples are great for checking the same reading across groups.

They're poor when the panels show other quantities on other scales. The shared layout implies a comparability that the axes don't support. Four panels showing revenue, headcount, delay and satisfaction are four charts that happen to be adjacent. Separating them. Or at least not sharing axes and labelling each scale clearly — is more honest.

They're also poor past about twelve panels. Here Each turns too small to read. At that point the answer is often to reduce what is being shown, not to shrink the panels further.

Figure size for a grid

The figure size should scale with the grid. A 2×3 grid in the same six inches as a single chart gives panels a third the size. Text that was comfortable turns proportionally huge.

A fair rule is to keep the per-panel size roughly constant:

fig, axes = plt.subplots(nrows, ncols, figsize=(3.2 * ncols, 2.4 * nrows))

Then adding a row makes the figure taller rather than making all smaller.

Shared axes in detail

sharex=True links the axes objects. So Changing the limits on one changes all of them. Which is convenient interactively and at times surprising in a script. Here An autoscale on one panel silently rescales the rest.

ax.label_outer() hides the tick labels on inner panels. This sharex/sharey do on its own but which is needed when you build the grid by hand.

sharex="col" links within columns only. This Suits a grid where columns are other quantities and rows are groups.

To share later rather than at creation, ax2.sharex(ax1) exists. The older ax2.get_shared_x_axes().join(ax1, ax2) appears in existing code.

A grid that reads

The details that make a grid of panels look seen as:

steady panel titles, small and left-aligned.

One axis label per grid, via supxlabel/supylabel.

Shared limits, so the compare is valid.

A steady colour across panels for the same series, or a single colour if each panel is one thing.

Empty cells removed, not left as empty boxes.

A common annotation style — if one panel has a link line, they all should.

On their own trivial. Together they're the gap between a grid that reads as one figure and one that reads as a few charts stuck together.

Building a grid programmatically

Most real grids are made from data rather than written out. A few patterns make that solid.

Find the grid size from the number of items:

n = len(items)
ncols = 3
nrows = math.ceil(n / ncols)
fig, axes = plt.subplots(nrows, ncols, figsize=(3.2 * ncols, 2.4 * nrows),
                         sharex=True, sharey=True, squeeze=False)

squeeze=False promises a 2-D array what the numbers work out to, so axes.flat always behaves.

Then pair, draw, and clean up the remainder:

for ax, item in zip(axes.flat, items):
    draw(ax, item)
for ax in axes.flat[n:]:
    ax.remove()

The figure size scaling with the grid is what keeps the text a constant size scaled to each panel, rather than shrinking as rows are added.

And because zip stops at the shorter arg, asserting n <= axes.size is worth the line. Else a grid that is too small silently drops groups.

Subplots, in summary

subplots(r, c) returns an array. axes.flat iterates in reading order. squeeze=False makes the shape predictable.

sharex and sharey are what turn a grid into a compare. Without them each panel scales to its own data and the heights mean other things.

gridspec and subplot_mosaic handle uneven layouts. The mosaic string is far easier to read than slice math.

tight_layout or constrained_layout stop labels colliding. A figure that saves cropped is often missing one.

Label the grid once with supxlabel rather than each panel, drop unused cells. Scale the figure size with the number of panels.

And small multiples are the right answer far more often than one crowded chart. They're the display that makes many checks possible at once.

Grids in practice

Three layouts cover nearly all real grids.

A row of two or three, for a before-and-after or a small set of related views. Wide, shared y, one shared axis label.

A square-ish grid, for small multiples over a cat var. Shared both ways, panel titles small, figure size scaled by the grid.

A main chart plus supporting ones, built with subplot_mosaic or gridspec and unequal ratios. The main panel gets two-thirds of the height and the supporting row the rest.

The fourth layout — a grid of unrelated charts — is common and often a mistake. Panels in a grid are read as close. Four charts of other quantities on other scales are four figures that happen to be adjacent. Either give them split figures, or make the gap clear with clearly labelled split scales and no shared axes.

The question that decides it: would a reader be right to compare the panels to each other? If yes, share the axes. If no, they probably shouldn't be in a grid.

Sharing a legend

A grid where each panel repeats the same legend wastes space and focus.

One figure-level legend is the fix:

handles, labels = axes.flat[0].get_legend_handles_labels()
fig.legend(handles, labels, loc="lower center", ncol=len(labels))
fig.subplots_adjust(bottom=0.15)

Handles are taken from any one panel. They're all the same series. fig.legend places it scaled to the figure rather than an axes.

Room has to be made for it. This constrained_layout does on its own and subplots_adjust does by name.

The same applies to a colorbar: one bar serving the whole grid. This Also enforces the shared scale the panels need to be close.

One more thing

fig.align_ylabels() lines up the y-axis labels across a column of panels. This Else sit at other distances from the axes based on how wide each panel's tick labels are.

It's a small alignment that is very visible once noticed. align_xlabels does the same flatly. On a grid where the panels have other value ranges, it's the gap between a column of labels that reads as one and one that looks ragged.

Subplots: the short version

A grid is a claim that the panels belong together. Sharing the axes is what makes the claim true. Labelling the grid once rather than each panel is what makes it read as one figure.

The three things that most often go wrong are an unshared y axis making incomparable panels look close, a figure size that didn't grow with the grid so all is cramped. Empty cells left as bare boxes. All three are one line each.

Questions people ask about Subplots

Why does plt.subplots return two things? It returns the figure and the axes. Unpack as fig, ax = plt.subplots().

Why can I not index my axes with ax[0]? With one row and one column you get a single Axes. With a row or column you get a 1D array. A grid gives a 2D array. Pass squeeze=False to always get 2D.

How do I share an axis between panels? Pass sharex=True or sharey=True to plt.subplots. Inner tick labels are hidden automatically.

How do I make panels of different sizes? Use subplot_mosaic or GridSpec with width_ratios and height_ratios.

How do I hide unused panels? Call ax.set_visible(False) or ax.axis('off') on each spare axes.

What is the difference between plt.subplot and plt.subplots? subplot adds one axes to the current figure. subplots creates the figure and the whole grid at once.

Reading the code back

A grid is created in one call and finished in three. Share the axes, label the grid once. Drop what is unused. The figure size should be found from the grid rather than fixed. Adding a row makes the figure taller instead of making each panel smaller. Those four choices are what split a grid that reads as one figure from a few charts that happen to be adjacent.

Check yourself

0 of 4

Answer without scrolling back up.

  1. What does `plt.subplots(1, 3)` return for the axes?

  2. Why does `sharey=True` matter for a grid of histograms?

  3. What does the string in `subplot_mosaic` represent?

  4. A figure looks fine on screen but is cropped when saved. What is missing?

Cheat sheet

Subplots

Create grids of subplots with plt.subplots and GridSpec in matplotlib. Place multiple axes on one figure with controlled sizes and spacing.

MATPLOTLIB · vizlearn.in/matplotlib/subplots.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.