Key Takeaways — Chapter 13: Subplots and GridSpec
1. Layout Is Code
The threshold concept: every multi-panel design decision maps to a specific matplotlib API call. Panel sizes, alignment, spacing, shared axes, inset panels — each has a direct translation into plt.subplots, GridSpec, sharex/sharey, or inset_axes. Practice the translation, and complex layouts become approachable.
2. plt.subplots for Regular Grids
plt.subplots(nrows, ncols, figsize=(W, H)) is the default for regular grids with equal-sized panels. The returned axes is a numpy array — 2D if both nrows and ncols are ≥ 2, 1D otherwise. Iterate over panels with axes.flat when you have more panels than easy indices. This is the simplest pattern for small multiples.
3. GridSpec for Unequal and Spanning Layouts
fig.add_gridspec(nrows, ncols, width_ratios=[...], height_ratios=[...]) creates a grid where rows and columns can have different sizes. Use slice notation (gs[0, :], gs[0:2, 0:2]) to create Axes that span multiple cells. This is how you build hero-plus-supporting layouts, scatter-with-marginals, and dashboards with asymmetric panels.
4. Shared Axes Enable Comparison
sharex=True, sharey=True, sharex="col", sharex="row" control axis sharing across panels. Shared axes align the limits and tick positions, hiding redundant tick labels on all but the bottom row (for x) or leftmost column (for y). Use shared axes whenever direct comparison across panels is the point. Use free axes when the panels have different units or wildly different ranges.
5. constrained_layout Is the Modern Default
plt.subplots(..., constrained_layout=True) or mpl.rcParams["figure.constrained_layout.use"] = True applies matplotlib's modern constraint-based layout solver. It handles spacing automatically, including around titles, colorbars, legends, and suptitles. Use it as the default for multi-panel figures. tight_layout still works but is being superseded.
6. Inset Axes for Zooms and Detail Panels
ax.inset_axes([x, y, width, height]) creates a new Axes inside a parent Axes. The coordinates are axes-fraction relative. Combined with ax.indicate_inset_zoom(ax_inset) to draw a rectangle on the parent marking the zoom region, this is the standard way to add a zoom panel to a line or scatter chart.
7. Nested GridSpecs and Subfigures
For very complex compositions, use GridSpecFromSubplotSpec to nest GridSpecs inside cells of an outer grid, or use fig.subfigures() (matplotlib 3.4+) to compose a figure from logical groups, each with its own internal layout. Both approaches let you build elaborate multi-level structures that would be unwieldy as a single flat GridSpec.
8. Colorbars and Legends at the Figure Level
When multiple panels share a color scale, use fig.colorbar(im, ax=axes) with a list of Axes to create a single shared colorbar. When multiple panels share a legend, pull handles, labels from one representative Axes and pass them to fig.legend(...) to create a figure-level legend. These patterns avoid redundant per-panel colorbars or legends.
9. Secondary Axes Only for Unit Conversions
ax.twinx() creates a dual-y-axis chart. Use it only when the two axes are unit conversions of the same variable (e.g., Celsius and Fahrenheit) — this is the narrow exception to Chapter 4's anti-dual-axis rule. For two different variables, use small multiples instead. Dual-axis charts with different variables manufacture visual correlations and should be avoided.
10. The Five-Step Design-to-Code Translation
When you have a layout sketch and need to write the code: (1) count rows and columns, (2) identify unequal sizes and set width_ratios/height_ratios, (3) identify spanning cells and use slice notation, (4) identify shared axes and set sharex/sharey, (5) set a reasonable figsize. With practice, this decomposition becomes automatic, and complex sketches translate into GridSpec code in minutes.