Subplots & Styling

Data Science · Lesson 23 of 32 6 min read Updated

One chart answers one question. A small dashboard of charts, side by side, answers several at once and invites comparison: five cities' temperatures, or sales, orders and average order value for the same months. matplotlib builds these with subplots.

In this lesson you will arrange several axes in one figure, share axes so comparisons are fair, and style charts with colours, line styles, style sheets and readable number formats.

A grid of charts

plt.subplots(nrows, ncols) returns a figure and an array of axes. You draw on each axes exactly as before, choosing it by position.

PythonRun it yourself
import pandas as pd
import matplotlib.pyplot as plt

sales = pd.read_csv("sales.csv", parse_dates=["order_date"]).drop_duplicates()
sales["revenue"] = sales["quantity"] * sales["unit_price"] * (1 - sales["discount"] / 100)
monthly = sales.groupby(sales["order_date"].dt.month).agg(
    orders=("order_id", "count"), revenue=("revenue", "sum"))
monthly["avg_order"] = monthly["revenue"] / monthly["orders"]

fig, axes = plt.subplots(1, 3, figsize=(12, 3.5), layout="constrained")
print(axes.shape)

axes[0].bar(monthly.index, monthly["orders"], color="#64748b")
axes[0].set_title("Orders")
axes[1].bar(monthly.index, monthly["revenue"] / 1000, color="#c2410c")
axes[1].set_title("Revenue (₹ thousand)")
axes[2].plot(monthly.index, monthly["avg_order"], marker="o", color="#2563eb")
axes[2].set_title("Average order value (₹)")

for ax in axes:
    ax.set_xticks(range(1, 13))
    ax.set_xlabel("Month")
fig.suptitle("Sales in 2025", fontsize=14)
plt.show()
Output
(3,)

Three charts sit in one row. The first two have a similar shape, low for most of the year and much higher in October and November. The third, average order value, is bumpier: it is highest in November but not in October, so October's jump came mainly from more orders, while November also had bigger baskets. Each measure has its own y-axis scale in its own panel; that is the honest way to show measures with different units, instead of squeezing two scales onto one chart. layout="constrained" makes matplotlib space the panels so titles and labels do not overlap. fig.suptitle() adds a title for the whole figure.

Small multiples with shared axes

When panels show the same measure for different groups, give them the same scale with sharex and sharey. This design is called small multiples, and it is often clearer than one crowded chart.

PythonRun it yourself
import pandas as pd
import matplotlib.pyplot as plt

weather = pd.read_csv("weather.csv", parse_dates=["date"])
weather = weather[weather["temp_max"] < 60]
cities = ["Delhi", "Mumbai", "Chennai", "Jaipur", "Kochi"]

fig, axes = plt.subplots(2, 3, figsize=(11, 5.5), sharex=True, sharey=True, layout="constrained")
for ax, city in zip(axes.flat, cities):
    d = weather[weather["city"] == city].set_index("date")["temp_max"]
    ax.plot(d.index, d.rolling(7, center=True, min_periods=1).mean(), color="#c2410c")
    ax.set_title(city)
    ax.grid(alpha=0.3)

axes.flat[-1].set_visible(False)
fig.suptitle("Daily maximum temperature, 7-day mean (°C)")
fig.autofmt_xdate()
plt.show()

print(axes.flat[0].get_ylim() == axes.flat[4].get_ylim())
Output
True

Five panels share one temperature scale, so the shapes can be compared directly: Delhi and Jaipur have tall summer peaks and deep winter valleys, Chennai is hot from April to June, and Mumbai and Kochi are flat lines that dip slightly in the monsoon. axes.flat lets you loop over a 2-D grid of axes as one list, and the unused sixth panel is hidden. The final print proves that the y-limits really are shared. fig.autofmt_xdate() tilts the date labels so they do not collide.

Colours, line styles and markers

Every plotting function accepts styling arguments. These are the most useful:

ArgumentExample valuesChanges
color"crimson", "#2563eb", "C0"Colour (named, hex, or the n-th default colour)
linestyle"-", "--", ":", "-."Solid, dashed, dotted, dash-dot lines
linewidth1, 2.5Line thickness
marker"o", "s", "^", "x"Point symbols
alpha0.3Transparency
label"Delhi"Text for the legend
PythonRun it yourself
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter

months = list(range(1, 13))
target = [3.0] * 12
actual = [2.8, 2.9, 3.6, 2.9, 2.7, 2.4, 4.2, 4.0, 4.8, 7.0, 6.9, 3.3]

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(months, actual, color="#c2410c", linewidth=2.5, marker="o", label="Actual")
ax.plot(months, target, color="gray", linestyle="--", linewidth=1.5, label="Target")
ax.fill_between(months, target, actual, where=[a > t for a, t in zip(actual, target)],
                color="#c2410c", alpha=0.15)
ax.yaxis.set_major_formatter(FuncFormatter(lambda v, pos: f"₹{v:.0f}L"))
ax.set_xticks(months, ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"])
ax.set_title("Monthly revenue against target")
ax.legend(frameon=False)
for side in ["top", "right"]:
    ax.spines[side].set_visible(False)
plt.show()

The chart shows a thick orange line for actual revenue and a thin grey dashed line for the target at ₹3 lakh. Where actual is above target, the gap is shaded in pale orange, most strongly in October and November. A FuncFormatter turns plain tick numbers into labels such as ₹4L. set_xticks() with a second list replaces the tick labels with month initials. Hiding the top and right spines (the frame lines) removes clutter.

Style sheets and rcParams

A style sheet changes the default look of every chart at once. plt.style.available lists them; plt.style.context() applies one temporarily. For individual defaults, change plt.rcParams, matplotlib's dictionary of settings.

PythonRun it yourself
import matplotlib.pyplot as plt

print(len(plt.style.available), "styles, for example:", [s for s in plt.style.available if "colorblind" in s or s == "ggplot"])

values = [5.3, 5.1, 4.5, 1.8, 1.3, 1.1]
names = ["Fashion", "Electronics", "Home", "Groceries", "Sports", "Books"]

with plt.style.context("tableau-colorblind10"):
    plt.rcParams["font.size"] = 11
    fig, ax = plt.subplots(figsize=(7, 3.5))
    ax.bar(names, values, color=["C0", "C1", "C2", "C3", "C4", "C5"])
    ax.set_title("Revenue by category (₹ lakh)")
    plt.show()
Output
29 styles, for example: ['ggplot', 'seaborn-v0_8-colorblind', 'tableau-colorblind10']

The tableau-colorblind10 style uses a set of colours chosen to stay distinguishable for people with the most common forms of colour blindness, and C0, C1 and so on pick colours from the active style in order. Inside the with block the style applies; after it, the defaults return. (In practice, all six bars here could be one colour, since the names already identify them; colour per bar is shown only to display the palette.)

Common mistakes

  • Panels with different scales that look comparable. Use sharey=True when panels show the same measure.
  • Two different measures on two y-axes of one chart. Readers cannot tell which line belongs to which scale, and the crossing points mean nothing. Use separate panels instead.
  • Overlapping titles and labels. Use layout="constrained" (or fig.tight_layout()).
  • Too much decoration. Heavy grids, frames, shadows and 3-D effects make charts harder to read.

Key points

  • fig, axes = plt.subplots(rows, cols) creates a grid; pick each axes by position or loop with axes.flat.
  • sharex and sharey give panels the same scale; layout="constrained" prevents overlaps.
  • Style lines and markers with color, linestyle, linewidth, marker and alpha.
  • Format ticks with FuncFormatter, hide unneeded spines, and use fig.suptitle() for a figure title.
  • Style sheets (plt.style.context("tableau-colorblind10")) and plt.rcParams change defaults.

Exercise

Make a 1 × 2 figure from students.csv. On the left, draw a histogram of english marks; on the right, a histogram of hindi marks (use na_values=["AB"]). Use the same bins (range(10, 101, 10)) and sharey=True, give each panel a title, and add an overall title. Print the mean of both subjects.

Show solution
Python
import pandas as pd
import matplotlib.pyplot as plt

students = pd.read_csv("students.csv", na_values=["AB"]).drop_duplicates()
bins = range(10, 101, 10)

fig, axes = plt.subplots(1, 2, figsize=(9, 3.5), sharey=True, layout="constrained")
for ax, subject in zip(axes, ["english", "hindi"]):
    ax.hist(students[subject].dropna(), bins=bins, color="#2563eb", edgecolor="white")
    ax.set_title(subject.title())
    ax.set_xlabel("Marks")
axes[0].set_ylabel("Students")
fig.suptitle("English and Hindi marks, Class 10")
plt.show()

print(students[["english", "hindi"]].mean().round(1))
Output
english    62.1
hindi      66.1
dtype: float64

With shared y-axes and identical bins, you can see at once that the Hindi histogram sits further to the right than English, matching its higher mean.

Quick Quiz

1. What does plt.subplots(2, 3) return as its second value?

2. When should panels share the y-axis?

3. Which option stops titles and labels of subplots from overlapping?

4. You want to show revenue in ₹ and order count on the same months. What is the clearer design?

Learn Data Science with a mentor

Free lessons get you started. Our Data Science & Machine Learning training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call