plotting
dataframes
subplots
data visualization
Python

How to plot multiple dataframes in subplots

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Plotting several pandas DataFrames in subplots is a common reporting pattern when you need side-by-side trend comparison. The main challenges are layout consistency, axis alignment, and avoiding repetitive plotting code. A small reusable structure with dynamic subplot creation keeps chart code clean and scalable.

Build a Reusable Plot Specification

Instead of writing one hardcoded block per DataFrame, define a list of plotting specs. Each spec carries the DataFrame, selected columns, title, and optional style settings.

python
1import pandas as pd
2
3idx = pd.date_range("2026-01-01", periods=7, freq="D")
4
5sales_df = pd.DataFrame({"sales": [120, 134, 129, 150, 168, 174, 171]}, index=idx)
6cost_df = pd.DataFrame({"cost": [80, 82, 84, 88, 90, 95, 96]}, index=idx)
7margin_df = pd.DataFrame({"margin": [40, 52, 45, 62, 78, 79, 75]}, index=idx)
8
9plot_specs = [
10    {"df": sales_df, "cols": ["sales"], "title": "Daily Sales", "ylabel": "USD", "color": "tab:blue"},
11    {"df": cost_df, "cols": ["cost"], "title": "Daily Cost", "ylabel": "USD", "color": "tab:red"},
12    {"df": margin_df, "cols": ["margin"], "title": "Daily Margin", "ylabel": "USD", "color": "tab:green"},
13]

This approach makes it easy to add or remove panels without rewriting layout logic.

Create Subplots Dynamically

Use matplotlib.pyplot.subplots with nrows=len(plot_specs) so the figure adapts automatically. Also normalize the single-axis case, because matplotlib returns a scalar axis when only one subplot exists.

python
1import matplotlib.pyplot as plt
2
3fig, axes = plt.subplots(
4    nrows=len(plot_specs),
5    ncols=1,
6    figsize=(11, 3.5 * len(plot_specs)),
7    sharex=True,
8    constrained_layout=True,
9)
10
11if len(plot_specs) == 1:
12    axes = [axes]
13
14for ax, spec in zip(axes, plot_specs):
15    spec["df"][spec["cols"]].plot(ax=ax, color=spec["color"], linewidth=2)
16    ax.set_title(spec["title"])
17    ax.set_ylabel(spec["ylabel"])
18    ax.grid(alpha=0.25)
19
20axes[-1].set_xlabel("Date")
21plt.show()

sharex=True is particularly useful for time series because all panels align naturally.

Plot Multiple Columns Per Subplot

Some DataFrames contain related metrics that belong in one panel. You can pass multiple columns per spec and let pandas build the legend.

python
1traffic_df = pd.DataFrame(
2    {
3        "organic": [230, 245, 252, 270, 289, 295, 310],
4        "paid": [90, 85, 95, 110, 108, 120, 125],
5        "referral": [35, 38, 36, 41, 43, 47, 49],
6    },
7    index=idx,
8)
9
10fig, ax = plt.subplots(figsize=(11, 4))
11traffic_df[["organic", "paid", "referral"]].plot(ax=ax)
12ax.set_title("Traffic by Source")
13ax.set_ylabel("Visits")
14ax.grid(alpha=0.25)
15plt.show()

This works well when the series share units and scale.

Two-Column Grid Layout

When you have many DataFrames, a vertical stack can become too tall. A grid layout improves readability in dashboards.

python
1import math
2import matplotlib.pyplot as plt
3
4cols = 2
5rows = math.ceil(len(plot_specs) / cols)
6
7fig, axes = plt.subplots(rows, cols, figsize=(13, 4 * rows), sharex=False)
8axes_flat = axes.ravel() if hasattr(axes, "ravel") else [axes]
9
10for ax, spec in zip(axes_flat, plot_specs):
11    spec["df"][spec["cols"]].plot(ax=ax, color=spec["color"], legend=False)
12    ax.set_title(spec["title"])
13    ax.set_ylabel(spec["ylabel"])
14    ax.grid(alpha=0.2)
15
16for ax in axes_flat[len(plot_specs):]:
17    ax.set_visible(False)
18
19plt.tight_layout()
20plt.show()

Always hide unused axes in incomplete last rows so the final chart looks intentional.

Keep Axes Comparable Across Panels

For fair visual comparison, align y-axis limits when metrics share units. If units differ, separate scales are fine, but label axes clearly.

python
1y_min = min(spec["df"][spec["cols"]].min().min() for spec in plot_specs)
2y_max = max(spec["df"][spec["cols"]].max().max() for spec in plot_specs)
3
4for ax in axes:
5    ax.set_ylim(y_min * 0.95, y_max * 1.05)

Use this only when it improves interpretation. Forced shared limits on very different ranges can flatten small but meaningful variation.

Encapsulate as a Helper Function

For repeated usage in notebooks or services, wrap plotting behavior in a function.

python
1def plot_dataframe_subplots(specs, sharex=True):
2    import matplotlib.pyplot as plt
3
4    fig, axes = plt.subplots(len(specs), 1, figsize=(11, 3.5 * len(specs)), sharex=sharex)
5    if len(specs) == 1:
6        axes = [axes]
7
8    for ax, spec in zip(axes, specs):
9        spec["df"][spec["cols"]].plot(ax=ax)
10        ax.set_title(spec["title"])
11        ax.grid(alpha=0.2)
12
13    return fig, axes

Now chart creation is a one-line call from data pipelines and report scripts.

Common Pitfalls

  • Assuming axes is always iterable, which breaks when only one subplot exists.
  • Plotting all columns by default and creating unreadable legends with noisy lines.
  • Forgetting shared x-axis for time series, which makes cross-panel comparison difficult.
  • Leaving unused grid cells visible in multi-column layouts.
  • Mixing metrics with very different units on shared y-axis without clear labeling.

Summary

  • Use a plot specification list to remove repetitive plotting logic.
  • Generate subplot layouts dynamically from dataset count.
  • Normalize single-subplot behavior so code remains robust.
  • Choose stacked or grid layout based on number of panels and report size.
  • Align axes intentionally and wrap the final pattern in a reusable helper.

Course illustration
Course illustration

All Rights Reserved.