Multi-Panel Figures and Layouts

  • ID: VISPY-007
  • Type: Lesson
  • Audience: Public
  • Theme: Multi-panel layouts for disciplined comparison

Single plots are useful, but many real questions require structured comparison:

In this lesson, you will build multi-panel figures using:

All figures are exported to figures/ using the shared CDI counter.


Setup

import warnings
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

from cdi_viz.theme import (
    cdi_notebook_init,
    show_and_save_mpl,
    show_and_save_plotly,
    cdi_theme
)

warnings.filterwarnings("ignore")

# Chapter init: resets the shared counter and ensures figures/ exists
cdi_notebook_init(chapter="07")

df = pd.read_csv("data/cdi-student-outcomes.csv")
print(df.head())
     group  test_prep  study_hours  math_score  reading_score  writing_score
0  Group B  completed          3.9          58             64             51
1  Group A       none          7.7          67             85             61
2  Group A       none          9.3          83             65             73
3  Group A       none          3.9          60             67             48
4  Group A       none          8.3          68             63             47

Matplotlib: a clean 2-panel comparison

This layout answers two questions at once:

  1. Relationship: study time vs math score
  2. Distribution: how math scores are spread
fig, axes = plt.subplots(ncols=2, figsize=(10, 4.2))

# Panel 1: scatter
axes[0].scatter(df["study_hours"], df["math_score"], alpha=0.6)
axes[0].set_title("Study hours vs math score")
axes[0].set_xlabel("Study hours per week")
axes[0].set_ylabel("Math score")

# Panel 2: histogram
axes[1].hist(df["math_score"], bins=22, alpha=0.85)
axes[1].set_title("Distribution of math scores")
axes[1].set_xlabel("Math score")
axes[1].set_ylabel("Count")

fig.suptitle("Two views of the same outcome", y=1.02)
fig.tight_layout()

show_and_save_mpl(fig)  # figures/07_001.png
Saved PNG → figures/07_001.png

Why this works

  • The left panel shows structure (trend and outliers).
  • The right panel shows distribution (spread and skew).
  • Together they prevent over-interpreting a single view.

Matplotlib: small multiples with shared scales

Small multiples are disciplined comparison: same axes, same scales, repeated across groups.

Here we repeat the same scatter plot for each group.

groups = sorted(df["group"].unique())

fig, axes = plt.subplots(ncols=len(groups), figsize=(11, 4), sharey=True)

for ax, g in zip(axes, groups):
    sub = df[df["group"] == g]
    ax.scatter(sub["study_hours"], sub["math_score"], alpha=0.6)
    ax.set_title(f"Group {g}")
    ax.set_xlabel("Study hours")

axes[0].set_ylabel("Math score")
fig.suptitle("Study hours vs math score by group", y=1.02)
fig.tight_layout()

show_and_save_mpl(fig)  # figures/07_002.png
Saved PNG → figures/07_002.png

Tip: sharey=True is important. If each panel auto-scales independently, comparisons become unreliable.


Seaborn: FacetGrid for quick small multiples

Seaborn can create small multiples quickly. It is useful for exploration when you want repeat the same plot across categories with minimal code.

g = sns.FacetGrid(df, col="group", height=3.6, aspect=1.0, sharey=True)
g.map_dataframe(sns.scatterplot, x="study_hours", y="math_score", alpha=0.65)
g.set_axis_labels("Study hours per week", "Math score")
g.set_titles("Group {col_name}")
g.fig.suptitle("FacetGrid: disciplined comparison by group", y=1.02)

show_and_save_mpl(g.fig)  # figures/07_003.png
Saved PNG → figures/07_003.png


Seaborn: multi-panel categorical comparison

This uses a panel-per-group layout for a box plot.

g = sns.catplot(
    data=df,
    x="test_prep",
    y="math_score",
    col="group",
    kind="box",
    height=3.6,
    aspect=1.0,
    sharey=True,
)
g.set_axis_labels("Test preparation", "Math score")
g.set_titles("Group {col_name}")
g.fig.suptitle("Math score by test prep, repeated by group", y=1.02)

show_and_save_mpl(g.fig)  # figures/07_004.png
Saved PNG → figures/07_004.png


Plotly: facets for interactive multi-panel comparison

Plotly facets are great when you want the learner to: - hover points - zoom into a panel - toggle legend groups

In CDI publishing, we export a static PNG snapshot for consistency.

import plotly.express as px

fig = px.scatter(
    df,
    x="study_hours",
    y="math_score",
    color="test_prep",
    facet_col="group",
    title="Study Hours vs Math Score (Faceted by Group)",
)

# Center title by default; keep export stable
cdi_theme(fig)

show_and_save_plotly(fig, show=False)  # figures/07_005.png
Saved PNG → figures/07_005.png


A simple layout checklist

When building multi-panel figures, check these every time:

  • Shared scales when comparing groups
  • Consistent labels across panels
  • Short panel titles (avoid full sentences)
  • One main title (figure-level), then small panel titles
  • Whitespace: use tight_layout() or controlled margins
  • Avoid legends inside panels if they block data

Key Takeaways

  • Multi-panel figures support disciplined comparison.
  • Matplotlib gives full control over layout and publication style.
  • Seaborn makes small multiples quick for exploration.
  • Plotly facets add interactivity during development and sharing.
  • Export all figures to PNG for stable Quarto book builds.

Exercises

  1. Create a 2×2 Matplotlib grid showing: math, reading, writing distributions, plus one scatter plot.
  2. Repeat a scatter plot by test_prep using Seaborn facets (row= or col=).
  3. Build a Plotly faceted histogram of math_score by group and export the PNG.
  4. In a small multiple figure, intentionally remove sharey=True and observe how it changes interpretation.