How to Create Subplots in Matplotlib
Subplots allow you to display multiple plots within a single figure, making it easy to compare related datasets or show different perspectives of the same data. Rather than generating separate...
Key Insights
- Use
plt.subplots()to create subplot grids in one line—it returns a figure and axes array that makes accessing individual plots straightforward through indexing - Share axes between subplots with
sharexandshareyparameters to maintain consistent scales across related visualizations and save space by eliminating redundant labels - GridSpec provides fine-grained control over subplot layouts when you need asymmetric arrangements or plots that span multiple grid cells
Introduction to Subplots
Subplots allow you to display multiple plots within a single figure, making it easy to compare related datasets or show different perspectives of the same data. Rather than generating separate figures that users must view independently, subplots present a cohesive visual narrative.
Common use cases include comparing model predictions across different parameters, showing data distributions alongside time series, displaying multiple features of a dataset simultaneously, or creating dashboard-style visualizations. The key advantage is context—when related plots share the same figure, viewers can immediately spot patterns, correlations, and anomalies across visualizations.
Basic Subplot Creation with plt.subplots()
The plt.subplots() function is your primary tool for creating subplot grids. It returns two objects: a figure and an axes array (or single axes object for one subplot).
The basic syntax is fig, axes = plt.subplots(nrows, ncols, figsize). The nrows and ncols parameters define your grid dimensions, while figsize controls the overall figure size in inches as a tuple (width, height).
import matplotlib.pyplot as plt
import numpy as np
# Create a 2x2 grid of subplots
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# Generate x values
x = np.linspace(0, 2*np.pi, 100)
# Plot different mathematical functions
axes[0, 0].plot(x, np.sin(x), color='blue')
axes[0, 0].set_title('Sine Function')
axes[0, 0].grid(True, alpha=0.3)
axes[0, 1].plot(x, np.cos(x), color='red')
axes[0, 1].set_title('Cosine Function')
axes[0, 1].grid(True, alpha=0.3)
axes[1, 0].plot(x, np.tan(x), color='green')
axes[1, 0].set_title('Tangent Function')
axes[1, 0].set_ylim(-5, 5) # Limit y-axis for visibility
axes[1, 0].grid(True, alpha=0.3)
axes[1, 1].plot(x, np.exp(x/2), color='purple')
axes[1, 1].set_title('Exponential Function')
axes[1, 1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
When you create a single row or column of subplots, the axes object is a 1D array. For a grid, it’s a 2D array. For a single subplot, it’s just an axes object without array indexing. Always check the shape of your axes to avoid indexing errors.
Accessing and Customizing Individual Subplots
Each subplot is an independent axes object that you can customize with standard Matplotlib methods. Use array indexing to access specific subplots and apply different plot types and styling.
import matplotlib.pyplot as plt
import numpy as np
# Create 1x3 subplot layout
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# Generate sample data
categories = ['A', 'B', 'C', 'D', 'E']
values = [23, 45, 56, 78, 32]
x_scatter = np.random.randn(100)
y_scatter = 2 * x_scatter + np.random.randn(100)
data_hist = np.random.normal(100, 15, 1000)
# Bar chart
axes[0].bar(categories, values, color='steelblue', edgecolor='black', linewidth=1.2)
axes[0].set_title('Sales by Category', fontsize=14, fontweight='bold')
axes[0].set_xlabel('Category', fontsize=12)
axes[0].set_ylabel('Sales ($1000s)', fontsize=12)
axes[0].grid(axis='y', alpha=0.3)
# Scatter plot
axes[1].scatter(x_scatter, y_scatter, alpha=0.6, c=y_scatter, cmap='viridis', s=50)
axes[1].set_title('Correlation Analysis', fontsize=14, fontweight='bold')
axes[1].set_xlabel('Variable X', fontsize=12)
axes[1].set_ylabel('Variable Y', fontsize=12)
axes[1].grid(True, alpha=0.3)
# Histogram
axes[2].hist(data_hist, bins=30, color='coral', edgecolor='black', alpha=0.7)
axes[2].set_title('Distribution of Measurements', fontsize=14, fontweight='bold')
axes[2].set_xlabel('Value', fontsize=12)
axes[2].set_ylabel('Frequency', fontsize=12)
axes[2].axvline(data_hist.mean(), color='red', linestyle='--', linewidth=2, label=f'Mean: {data_hist.mean():.1f}')
axes[2].legend()
axes[2].grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
This approach gives you complete control over each subplot’s appearance. You can mix plot types, apply different color schemes, and customize labels independently. The key is treating each axes object as a separate canvas.
Advanced Layout Options
When you need layouts beyond simple grids, Matplotlib provides several advanced options. GridSpec offers the most flexibility for creating complex, asymmetric layouts.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
# Create figure and GridSpec
fig = plt.figure(figsize=(12, 10))
gs = gridspec.GridSpec(3, 2, figure=fig, hspace=0.3, wspace=0.3)
# Large plot spanning top row
ax_main = fig.add_subplot(gs[0, :])
x = np.linspace(0, 10, 100)
y = np.sin(x) * np.exp(-x/10)
ax_main.plot(x, y, linewidth=2, color='darkblue')
ax_main.fill_between(x, y, alpha=0.3, color='lightblue')
ax_main.set_title('Main Analysis: Damped Oscillation', fontsize=16, fontweight='bold')
ax_main.set_xlabel('Time (s)')
ax_main.set_ylabel('Amplitude')
ax_main.grid(True, alpha=0.3)
# Two medium plots in middle row
ax_left = fig.add_subplot(gs[1, 0])
ax_left.plot(x, np.sin(x), color='green')
ax_left.set_title('Component 1: Oscillation')
ax_left.grid(True, alpha=0.3)
ax_right = fig.add_subplot(gs[1, 1])
ax_right.plot(x, np.exp(-x/10), color='red')
ax_right.set_title('Component 2: Decay')
ax_right.grid(True, alpha=0.3)
# Two smaller plots in bottom row
ax_bottom_left = fig.add_subplot(gs[2, 0])
ax_bottom_left.hist(y, bins=20, color='purple', alpha=0.7, edgecolor='black')
ax_bottom_left.set_title('Amplitude Distribution')
ax_bottom_left.set_xlabel('Amplitude')
ax_bottom_right = fig.add_subplot(gs[2, 1])
spectrum = np.fft.fft(y)
freq = np.fft.fftfreq(len(y))
ax_bottom_right.plot(freq[:50], np.abs(spectrum[:50]), color='orange')
ax_bottom_right.set_title('Frequency Spectrum')
ax_bottom_right.set_xlabel('Frequency')
plt.show()
GridSpec allows you to define a grid and then assign subplots to span multiple cells using slice notation. The hspace and wspace parameters control vertical and horizontal spacing between subplots. This approach is ideal for creating dashboard-style visualizations where you want to emphasize certain plots over others.
Sharing Axes and Adjusting Spacing
Shared axes are crucial when comparing data across the same scale. They eliminate redundant axis labels and ensure consistent scaling across subplots.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
# Generate sample time series data
dates = pd.date_range('2023-01-01', periods=365, freq='D')
np.random.seed(42)
# Simulate three related metrics
metric1 = np.cumsum(np.random.randn(365)) + 100
metric2 = np.cumsum(np.random.randn(365)) + 150
metric3 = np.cumsum(np.random.randn(365)) + 200
# Create subplots with shared x-axis
fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True)
# Plot each metric
axes[0].plot(dates, metric1, color='blue', linewidth=1.5)
axes[0].set_ylabel('Revenue ($M)', fontsize=11, fontweight='bold')
axes[0].set_title('Business Metrics Dashboard', fontsize=14, fontweight='bold', pad=20)
axes[0].grid(True, alpha=0.3)
axes[0].axhline(metric1.mean(), color='red', linestyle='--', alpha=0.5, label='Average')
axes[0].legend(loc='upper left')
axes[1].plot(dates, metric2, color='green', linewidth=1.5)
axes[1].set_ylabel('Users (K)', fontsize=11, fontweight='bold')
axes[1].grid(True, alpha=0.3)
axes[1].axhline(metric2.mean(), color='red', linestyle='--', alpha=0.5, label='Average')
axes[1].legend(loc='upper left')
axes[2].plot(dates, metric3, color='purple', linewidth=1.5)
axes[2].set_ylabel('Engagement', fontsize=11, fontweight='bold')
axes[2].set_xlabel('Date', fontsize=11, fontweight='bold')
axes[2].grid(True, alpha=0.3)
axes[2].axhline(metric3.mean(), color='red', linestyle='--', alpha=0.5, label='Average')
axes[2].legend(loc='upper left')
# Adjust spacing to prevent overlap
plt.tight_layout()
plt.show()
The sharex=True parameter ensures all subplots use the same x-axis range. When you zoom or pan one subplot, all shared subplots update together. The tight_layout() function automatically adjusts spacing to prevent label overlap—use it as your default unless you need manual control with subplots_adjust().
Best Practices and Common Pitfalls
Choose your layout based on the story you’re telling. Vertical stacking works well for time series comparisons where you want to align temporal patterns. Grids suit categorical comparisons. Asymmetric layouts using GridSpec emphasize hierarchy—use larger plots for primary insights and smaller ones for supporting details.
Avoid overcrowding. More than 9 subplots in a single figure usually reduces readability. If you need more, consider creating multiple figures or using interactive visualizations. Each subplot should have enough space for labels, legends, and data points to be clearly visible.
import matplotlib.pyplot as plt
import numpy as np
# Generate sample data
np.random.seed(42)
data = [np.random.randn(100) + i for i in range(4)]
# POOR DESIGN: Cramped, inconsistent, hard to read
fig_bad, axes_bad = plt.subplots(2, 2, figsize=(8, 6))
for idx, ax in enumerate(axes_bad.flat):
ax.hist(data[idx], bins=20)
ax.set_title(f'Dataset {idx+1}')
fig_bad.suptitle('Poor Subplot Design', fontsize=14)
plt.tight_layout()
# GOOD DESIGN: Spacious, consistent, clear hierarchy
fig_good, axes_good = plt.subplots(2, 2, figsize=(14, 10))
colors = ['steelblue', 'coral', 'green', 'purple']
for idx, (ax, color) in enumerate(zip(axes_good.flat, colors)):
ax.hist(data[idx], bins=20, color=color, edgecolor='black', alpha=0.7)
ax.set_title(f'Distribution of Dataset {idx+1}', fontsize=13, fontweight='bold', pad=10)
ax.set_xlabel('Value', fontsize=11)
ax.set_ylabel('Frequency', fontsize=11)
ax.grid(axis='y', alpha=0.3)
# Add statistical annotations
mean_val = np.mean(data[idx])
ax.axvline(mean_val, color='red', linestyle='--', linewidth=2, label=f'Mean: {mean_val:.2f}')
ax.legend()
fig_good.suptitle('Well-Designed Subplot Layout', fontsize=16, fontweight='bold', y=0.995)
plt.tight_layout()
plt.show()
The improved version uses consistent styling, adequate figure size, meaningful colors, and statistical annotations. Each subplot is self-explanatory with proper labels and grid lines for easier value reading.
Performance matters when creating many subplots. Each axes object consumes memory, and rendering time increases linearly with subplot count. For exploratory analysis with dozens of plots, consider creating them in batches or using interactive tools like Plotly.
Master these subplot techniques and you’ll create professional, informative visualizations that effectively communicate complex data relationships. The key is balancing information density with clarity—show enough to tell your story without overwhelming your audience.