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
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)
figis the whole figure.axesis 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 forsubplot_mosaic()orGridSpec.
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 totight_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 withaxes.flatten(). - Using
sharex=Truewhen 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
- Use
plt.subplot_mosaic()for named, irregular layouts that are easier to read and maintain. - Leverage
constrained_layout=Trueinstead oftight_layout()for more robust automatic spacing. - 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_mosaichandles irregular layouts.- Access each panel via axes indexing —
axes[row, col]— and customize with methods likeset_title(). - Share axes with
sharex/shareyonly when consistent scales are needed for fair comparison. - Always finalize with
tight_layout()orconstrained_layoutto prevent overlapping labels. - Save multi-panel figures with
savefig()for reproducible, shareable output.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.