Subplots & Figure Layout

Master subplots and figure layout in Matplotlib with Python. Learn to create and arrange multiple plots, customize grids, and troubleshoot common issues.

Focus: subplots and figure layout

Sponsored

You’ve built beautiful single charts in Matplotlib — but the moment you need to compare two trends, zoom into an outlier, or show a correlation matrix next to a time series, one lonely plt.plot() starts to feel like trying to tell a story with only one photograph. Data science is all about relationships, and relationships demand multiple views side by side. That’s exactly the pain this lesson solves: how to stop stitched-together separate figures and start using subplots and figure layout to create professional, publication-ready multi-panel graphics with just a few lines of Python.

The problem this lesson solves

Real-world analysis rarely fits in a single chart. You might need to show the distribution of a feature, its time trend, and its correlation with another variable — all in one glance. Without a solid grasp of subplots and figure layout, you’ll fall into these traps:

  • Overlapping labels — axis titles and tick labels collide when panels are too close.
  • Inconsistent scales — comparing plots with different y-axis ranges fools the eye.
  • Cluttered code — manually creating and repositioning axes becomes a maintenance nightmare.
  • Rigid grids — you can’t place a large heatmap next to two small line charts.

The cost? Wasted hours, confusing presentations, and charts that mislead rather than clarify. By mastering subplots(), subplot_mosaic(), and layout functions like tight_layout() and constrained_layout, you’ll turn these headaches into a smooth, repeatable workflow.

Core concept / mental model

Think of a figure as a blank canvas, and axes as individual painting areas on that canvas. Subplots are simply a way to carve that canvas into a grid — rows and columns — where each cell holds its own plot.

Mental model: A figure is the page of a comic book. Each subplot is a panel. The layout decides how the panels are arranged — 2×2, 3×1, or even a custom mosaic where one panel is twice as wide as its neighbors.

Here’s the foundational concept:

  • Figure: The entire window or image (the canvas).
  • Axes: An individual plot area (the panel) with its own x/y axis.
  • Grid: The arrangement of rows and columns (e.g., 2×2 for four panels).
  • Indexing: Panels are numbered row-by-row from the top-left, starting at 1 (when using add_subplot()).

Once you internalize this, you’ll see that every multi-panel chart is just a matter of choosing a grid and placing data into the right cells.

How it works step by step

Let’s break down the process of creating a multi-panel figure using the most common function, plt.subplots().

Step 1: Create the figure and axes grid

import matplotlib.pyplot as plt

fig, axes = plt.subplots(nrows=2, ncols=2)
  • fig is the whole figure.
  • axes is a 2×2 NumPy array of Axes objects.

You access individual panels via indices like axes[0, 0] (top-left) or axes[1, 1] (bottom-right).

Step 2: Plot into each panel

axes[0, 0].plot([1, 2, 3], [1, 4, 9])          # line plot
axes[0, 1].scatter([1, 2, 3], [4, 5, 6])       # scatter
axes[1, 0].hist([1, 2, 2, 3, 3, 3])            # histogram
axes[1, 1].bar(['A', 'B', 'C'], [3, 5, 1])     # bar chart

Step 3: Customize each panel

Every axes object has methods like set_title(), set_xlabel(), and set_ylabel().

axes[0, 0].set_title('Quadratic')
axes[0, 1].set_xlabel('X values')

Step 4: Adjust layout for readability

Use fig.tight_layout() to automatically prevent overlapping labels, or set constrained_layout=True when creating the figure.

fig.tight_layout()

Step 5: Display or save

plt.show()
# or fig.savefig('my_multi_panel_figure.png', dpi=150)

This flow works for most standard grids. For irregular layouts, you’ll upgrade to subplot_mosaic (see Hands-on).

Hands-on walkthrough

Let’s practice with a realistic dataset — sales data against marketing spend. We’ll create a 2×2 grid showing: time series, scatter, histogram, and bar chart.

Example 1: Basic 2×2 subplots

import matplotlib.pyplot as plt
import numpy as np

# Sample data
x = np.linspace(0, 10, 50)
sales = np.sin(x) * 50 + 100
spend = np.random.default_rng(0).uniform(20, 80, 50)

# Create a 2x2 grid
fig, axes = plt.subplots(2, 2, figsize=(10, 8))

# Top-left: line plot
axes[0, 0].plot(x, sales, color='tab:blue')
axes[0, 0].set_title('Sales over time')

# Top-right: scatter
axes[0, 1].scatter(spend, sales, alpha=0.6, color='tab:green')
axes[0, 1].set_title('Sales vs Spend')
axes[0, 1].set_xlabel('Marketing spend')

# Bottom-left: histogram
axes[1, 0].hist(sales, bins=15, color='tab:orange')
axes[1, 0].set_title('Sales distribution')

# Bottom-right: bar chart
categories = ['Q1', 'Q2', 'Q3', 'Q4']
avg_sales = [95, 110, 120, 105]
axes[1, 1].bar(categories, avg_sales, color='tab:red')
axes[1, 1].set_title('Quarterly averages')

fig.tight_layout()
plt.show()

Expected output: A clean 2×2 grid with four distinct charts, each with its own title, and no overlapping labels.

Example 2: Custom layout with subplot_mosaic

Sometimes you need a big plot on top and two small ones below. subplot_mosaic lets you name your panels.

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
y = np.sin(x)

fig, axes = plt.subplot_mosaic(
    [['time_series', 'time_series'],
     ['distribution', 'scatter']],
    figsize=(10, 6)
)

axes['time_series'].plot(x, y, label='sin(x)')
axes['time_series'].set_title('One large time series')
axes['time_series'].legend()

axes['distribution'].hist(y, bins=20, color='purple')
axes['distribution'].set_title('Sine histogram')

axes['scatter'].scatter(x, y, s=5, alpha=0.5)
axes['scatter'].set_title('Sine scatter')

fig.tight_layout()
plt.show()

Expected output: The top row is a single plot spanning the full width. The bottom row splits into two panels — a histogram on the left and a scatter on the right.

Example 3: Sharing axes to compare consistently

When panels share the same x-axis (e.g., time series from different sensors), use sharex=True to align scales and reduce redundancy.

import matplotlib.pyplot as plt
import numpy as np

t = np.arange(0, 100, 1)
signal1 = np.random.default_rng(1).normal(0, 1, 100).cumsum()
signal2 = np.random.default_rng(2).normal(0, 1, 100).cumsum()

fig, axes = plt.subplots(2, 1, sharex=True, figsize=(8, 6))

axes[0].plot(t, signal1, color='navy')
axes[0].set_ylabel('Sensor A')
axes[1].plot(t, signal2, color='crimson')
axes[1].set_ylabel('Sensor B')
axes[1].set_xlabel('Time (s)')

fig.align_ylabels(axes)
fig.tight_layout()
plt.show()

Expected output: Two time series stacked vertically with aligned y-labels, sharing the same x-axis automatically.

Compare options / when to choose what

Not every situation calls for plt.subplots(). Here’s a quick guide to help you decide.

Method Best for Pros Cons
plt.subplots(nrows, ncols) Standard rectangular grids Simple, quick, familiar Not flexible for irregular layouts
plt.subplot_mosaic() Irregular / named layouts Clear names, flexible spans Slightly more verbose
plt.subplot2grid() Older-style irregular grids Fine-grained control More verbose, harder to read
plt.GridSpec() Advanced custom layouts Ultimate control Steeper learning curve

Pro tip: For 90% of your needs, stick with subplots(). Only when you need a panel that spans multiple rows or columns should you reach for subplot_mosaic() or GridSpec.

Variations in layout tuning

  • figsize: Always set it — default size often leads to cramped panels.
  • sharex / sharey: Use when axes should have identical scales for fair comparison.
  • constrained_layout=True: A modern alternative to tight_layout() that handles complex layouts better.

Troubleshooting & edge cases

Even with clean code, things can go wrong. Here are the most common issues and how to fix them.

1. “I get a list instead of an image” — flattening axes

When you create a 1×N grid, axes is a 1D array, but for 2×2 you get a 2D array. Accessing axes[0] might return a row, not an axes object.

Fix: Flatten the array: for ax in axes.flatten(): or use subplot_mosaic to get named axes.

2. Overlapping labels and titles

Panels crowd each other, and labels overlap.

Fix: Call fig.tight_layout() after plotting, or set constrained_layout=True in plt.subplots().

3. Different y-axis scales make comparison misleading

Two time series with different ranges (e.g., 0–10 vs 0–1000) plotted on the same scale will hide the smaller one.

Fix: Use sharey=False (default) and add appropriate axis labels, or use twin axes (ax.twinx()) when appropriate.

4. ValueError: The number of columns must be a positive integer

Attempting to pass ncols=0 or a non-integer.

Fix: Always use positive integers. Also, ensure figsize values are positive.

5. Deleting a panel — how to remove an unused subplot

Sometimes a grid has an empty cell.

Fix: Use fig.delaxes(axes[1, 1]) to remove it, or design a mosaic that doesn’t include it.

What you learned & what's next

You now understand the core idea behind subplots and figure layout — that a figure is a canvas, and subplots are its panels. You can create standard grids with plt.subplots(), custom layouts with subplot_mosaic(), and you know how to fine-tune spacing and scale sharing. You’ve completed a practical exercise covering multiple chart types in a single figure, which is a daily task in data science.

This skill connects directly to the next lesson in the track, where you’ll explore customizing visual aesthetics — colors, styles, and annotations — to make your multi-panel figures not only clear but also publication-ready. From here, you’ll be able to compose entire visual stories from the same data, the hallmark of professional data communication.

Practice recap

Now try this on your own: load any dataset you like (or use numpy.random), and create a 2×2 subplot figure that includes a histogram, a scatter plot, a line chart, and a bar chart. Experiment with sharex=True for two of the plots, then switch to subplot_mosaic to make one panel span the full width. Save your figure as a PNG and check the layout quality — you're ready for the next lesson on visual aesthetics.

Common mistakes

  • Forgetting to call fig.tight_layout() after plotting, causing overlapping labels.
  • Accessing axes incorrectly for a 1xN grid — axes[0] gives a single axes, but for 2x2 it gives a row array; flatten with axes.flatten().
  • Using sharex=True when panels should have independent x-axis scales, leading to misleading comparisons.
  • Not setting figsize, resulting in cramped panels that are hard to read.
  • Using plt.subplot() inside a manually created figure without a clear layout, making the code harder to maintain.

Variations

  1. Use plt.subplot_mosaic() for named, irregular layouts that are easier to read and maintain.
  2. Leverage constrained_layout=True instead of tight_layout() for more robust automatic spacing.
  3. For complex grids, use plt.GridSpec() to achieve total control over row and column spans.

Real-world use cases

  • A data analyst creates a 2x2 dashboard showing sales trends, distribution, and correlation for a quarterly business review.
  • A research scientist plots raw signals, their FFT, and a spectrogram side-by-side to diagnose sensor anomalies.
  • A machine learning engineer compares model training/validation loss curves across multiple runs in a single figure for quick iteration.

Key takeaways

  • A figure is the canvas; subplots (axes) are the individual panels arranged in a grid.
  • plt.subplots(nrows, ncols) is the go-to for standard grids; subplot_mosaic handles irregular layouts.
  • Access each panel via axes indexing — axes[row, col] — and customize with methods like set_title().
  • Share axes with sharex/sharey only when consistent scales are needed for fair comparison.
  • Always finalize with tight_layout() or constrained_layout to prevent overlapping labels.
  • Save multi-panel figures with savefig() for reproducible, shareable output.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.