How to Create Subplots in Plotly
Subplots are essential when you need to compare multiple datasets, show different perspectives of the same data, or build comprehensive dashboards. Instead of generating separate charts and manually...
Key Insights
- Plotly’s
make_subplots()function enables complex multi-chart dashboards by organizing visualizations into customizable grid layouts with support for mixed chart types, shared axes, and precise positioning control. - Reference individual subplots using
rowandcolparameters when adding traces, and use thespecsparameter to create advanced layouts including merged cells, secondary axes, and different subplot types. - Proper spacing, shared axes, and consistent styling transform basic subplot grids into professional dashboards—set
horizontal_spacingandvertical_spacingto prevent overlapping labels and useupdate_xaxes()with row/col selectors for targeted formatting.
Introduction to Plotly Subplots
Subplots are essential when you need to compare multiple datasets, show different perspectives of the same data, or build comprehensive dashboards. Instead of generating separate charts and manually arranging them, Plotly’s make_subplots() function from the plotly.subplots module handles layout management programmatically.
The function creates a figure with a predefined grid structure where you add traces by specifying their row and column positions. This approach beats creating individual figures because it maintains consistent sizing, enables shared axes for meaningful comparisons, and produces a single interactive visualization that users can export or embed as one unit.
Use subplots when you’re analyzing time series across categories, comparing metrics side-by-side, or building executive dashboards that need multiple chart types in one view.
Basic Subplot Grid Layout
Creating a basic grid is straightforward—specify the number of rows and columns, then add traces to specific positions. Here’s a practical example showing quarterly sales data across four regions:
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
# Sample data
quarters = ['Q1', 'Q2', 'Q3', 'Q4']
regions = {
'North': [45000, 52000, 48000, 61000],
'South': [38000, 41000, 43000, 47000],
'East': [51000, 49000, 55000, 58000],
'West': [42000, 46000, 50000, 54000]
}
# Create 2x2 subplot grid
fig = make_subplots(
rows=2, cols=2,
subplot_titles=('North Region', 'South Region', 'East Region', 'West Region')
)
# Add traces to specific positions
positions = [(1,1), (1,2), (2,1), (2,2)]
for (row, col), (region, sales) in zip(positions, regions.items()):
fig.add_trace(
go.Scatter(x=quarters, y=sales, mode='lines+markers', name=region),
row=row, col=col
)
fig.update_layout(height=600, showlegend=False, title_text="Quarterly Sales by Region")
fig.show()
Notice how add_trace() takes row and col parameters. Without these, traces would overlay on the first subplot. The subplot_titles parameter adds headers automatically, saving you from manual annotation positioning.
Customizing Subplot Types and Positions
Real dashboards rarely use identical chart types. The specs parameter lets you define different subplot types and merge cells for custom layouts. Here’s a dashboard with a full-width line chart on top and two bar charts below:
from plotly.subplots import make_subplots
import plotly.graph_objects as go
# Create custom layout with specs
fig = make_subplots(
rows=2, cols=2,
specs=[
[{"type": "scatter", "colspan": 2}, None], # Full width on row 1
[{"type": "bar"}, {"type": "bar"}] # Two bars on row 2
],
subplot_titles=('Monthly Revenue Trend', 'Product A Sales', 'Product B Sales'),
vertical_spacing=0.15
)
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
revenue = [125000, 132000, 128000, 145000, 151000, 148000]
product_a = [3200, 3400, 3100, 3800, 3900, 3700]
product_b = [2800, 2900, 3100, 3300, 3500, 3400]
# Add line chart spanning full width
fig.add_trace(
go.Scatter(x=months, y=revenue, mode='lines+markers',
name='Revenue', line=dict(color='#2E86AB', width=3)),
row=1, col=1
)
# Add bar charts side by side
fig.add_trace(
go.Bar(x=months, y=product_a, name='Product A', marker_color='#A23B72'),
row=2, col=1
)
fig.add_trace(
go.Bar(x=months, y=product_b, name='Product B', marker_color='#F18F01'),
row=2, col=2
)
fig.update_layout(height=700, showlegend=True, title_text="Sales Dashboard")
fig.show()
The specs parameter is a list of lists matching your grid structure. Use {"colspan": 2} to merge cells horizontally, and set the merged cells to None. You can also use {"rowspan": 2} for vertical merging. The type key ensures Plotly configures appropriate axes for each chart type.
Shared Axes and Subplot Titles
Shared axes are critical for meaningful comparisons. When comparing time series or categories across subplots, shared axes ensure identical scales. Here’s a temperature comparison across cities:
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import pandas as pd
# Sample temperature data
dates = pd.date_range('2024-01-01', periods=30, freq='D')
temps = {
'New York': 32 + np.random.randn(30) * 5,
'Los Angeles': 65 + np.random.randn(30) * 4,
'Chicago': 28 + np.random.randn(30) * 6,
'Miami': 75 + np.random.randn(30) * 3
}
fig = make_subplots(
rows=2, cols=2,
shared_xaxes=True, # All subplots share same x-axis
shared_yaxes=True, # All subplots share same y-axis
subplot_titles=list(temps.keys()),
vertical_spacing=0.1,
horizontal_spacing=0.08
)
positions = [(1,1), (1,2), (2,1), (2,2)]
for (row, col), (city, temp_data) in zip(positions, temps.items()):
fig.add_trace(
go.Scatter(x=dates, y=temp_data, mode='lines',
name=city, line=dict(width=2)),
row=row, col=col
)
fig.update_xaxes(title_text="Date", row=2, col=1)
fig.update_xaxes(title_text="Date", row=2, col=2)
fig.update_yaxes(title_text="Temperature (°F)", row=1, col=1)
fig.update_yaxes(title_text="Temperature (°F)", row=2, col=1)
fig.update_layout(height=600, showlegend=False, title_text="January Temperature Comparison")
fig.show()
With shared_xaxes=True, zooming or panning on one subplot affects all subplots sharing that axis. This is invaluable for synchronized exploration. Set to 'columns' or 'rows' for partial sharing within specific grid sections.
Advanced Layouts: Secondary Y-Axes and Insets
Financial charts often need secondary y-axes to display metrics with different scales. Here’s a stock price chart with volume bars:
from plotly.subplots import make_subplots
import plotly.graph_objects as go
# Sample financial data
dates = pd.date_range('2024-01-01', periods=60, freq='D')
prices = 150 + np.cumsum(np.random.randn(60) * 2)
volumes = np.random.randint(1000000, 5000000, 60)
# Create subplot with secondary y-axis
fig = make_subplots(
rows=2, cols=1,
shared_xaxes=True,
vertical_spacing=0.03,
subplot_titles=('Stock Price', 'Volume'),
row_heights=[0.7, 0.3] # Price chart gets 70% of height
)
# Add price line
fig.add_trace(
go.Scatter(x=dates, y=prices, mode='lines',
name='Price', line=dict(color='#1f77b4', width=2)),
row=1, col=1
)
# Add volume bars
fig.add_trace(
go.Bar(x=dates, y=volumes, name='Volume',
marker_color='#7f7f7f', opacity=0.5),
row=2, col=1
)
fig.update_xaxes(title_text="Date", row=2, col=1)
fig.update_yaxes(title_text="Price ($)", row=1, col=1)
fig.update_yaxes(title_text="Volume", row=2, col=1)
fig.update_layout(
height=600,
showlegend=True,
title_text="Stock Performance Analysis",
hovermode='x unified'
)
fig.show()
The row_heights parameter controls relative heights—[0.7, 0.3] allocates 70% to the price chart and 30% to volume. For true secondary y-axes on the same subplot, use specs=[{"secondary_y": True}] and the secondary_y parameter when adding traces.
Styling and Formatting Subplots
Professional dashboards require careful styling. Control spacing, colors, and formatting for each subplot individually:
from plotly.subplots import make_subplots
import plotly.graph_objects as go
fig = make_subplots(
rows=1, cols=3,
subplot_titles=('Revenue', 'Expenses', 'Profit Margin'),
horizontal_spacing=0.12
)
categories = ['Q1', 'Q2', 'Q3', 'Q4']
revenue = [450, 520, 480, 610]
expenses = [320, 360, 340, 410]
margin = [28.9, 30.8, 29.2, 32.8]
fig.add_trace(
go.Bar(x=categories, y=revenue, marker_color='#2E86AB', name='Revenue'),
row=1, col=1
)
fig.add_trace(
go.Bar(x=categories, y=expenses, marker_color='#A23B72', name='Expenses'),
row=1, col=2
)
fig.add_trace(
go.Scatter(x=categories, y=margin, mode='lines+markers',
marker=dict(size=10, color='#F18F01'),
line=dict(width=3, color='#F18F01'),
name='Margin %'),
row=1, col=3
)
# Update individual subplot axes
fig.update_yaxes(title_text="Amount ($K)", row=1, col=1)
fig.update_yaxes(title_text="Amount ($K)", row=1, col=2)
fig.update_yaxes(title_text="Margin (%)", row=1, col=3)
# Global layout settings
fig.update_layout(
height=400,
showlegend=False,
title_text="Financial Performance Dashboard",
title_x=0.5,
font=dict(family="Arial, sans-serif", size=12),
plot_bgcolor='#F8F9FA',
margin=dict(l=60, r=40, t=80, b=60)
)
# Add gridlines to all subplots
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='#E5E5E5')
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='#E5E5E5')
fig.show()
Key styling parameters: horizontal_spacing and vertical_spacing prevent label collisions (default 0.2 for horizontal, 0.3 for vertical). Use update_xaxes() and update_yaxes() with row and col parameters to target specific subplots. The margin dict controls whitespace around the entire figure.
Best Practices and Common Pitfalls
Choose appropriate layouts: Don’t cram more than 6-9 subplots in one figure. Beyond that, consider separate figures or interactive filtering. Use row_heights and column_widths to emphasize important charts.
Performance considerations: Each trace adds rendering overhead. For large datasets, downsample before plotting or use scattergl instead of scatter for WebGL acceleration. Avoid adding hundreds of traces across many subplots.
Common errors: Forgetting to set row and col when adding traces stacks everything on subplot (1,1). When using colspan or rowspan, remember to set merged cells to None in the specs array. If axis titles disappear, you’re likely updating the wrong row/col combination—double-check your indices.
Axis range synchronization: When using shared_xaxes=False but wanting similar ranges, manually set ranges with update_xaxes(range=[min_val, max_val]) for consistency.
Export considerations: High-resolution exports of complex subplots can be large. Set width and height explicitly before exporting to control output dimensions. Use fig.write_html() for interactive exports or fig.write_image() for static formats (requires kaleido package).
The make_subplots() function provides the foundation for sophisticated visualizations. Master the specs parameter, understand axis sharing implications, and invest time in styling—these skills transform basic grids into compelling analytical dashboards.