Subplots & Styling
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.
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()(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.
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())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:
| Argument | Example values | Changes |
|---|---|---|
color | "crimson", "#2563eb", "C0" | Colour (named, hex, or the n-th default colour) |
linestyle | "-", "--", ":", "-." | Solid, dashed, dotted, dash-dot lines |
linewidth | 1, 2.5 | Line thickness |
marker | "o", "s", "^", "x" | Point symbols |
alpha | 0.3 | Transparency |
label | "Delhi" | Text for the legend |
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.
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()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=Truewhen 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"(orfig.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 withaxes.flat.sharexandshareygive panels the same scale;layout="constrained"prevents overlaps.- Style lines and markers with
color,linestyle,linewidth,markerandalpha. - Format ticks with
FuncFormatter, hide unneeded spines, and usefig.suptitle()for a figure title. - Style sheets (
plt.style.context("tableau-colorblind10")) andplt.rcParamschange 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
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))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?
It returns the figure and a 2 × 3 NumPy array of axes.
2. When should panels share the y-axis?
Shared scales make same-measure panels directly comparable.
3. Which option stops titles and labels of subplots from overlapping?
Constrained layout adjusts spacing automatically.
4. You want to show revenue in ₹ and order count on the same months. What is the clearer design?
Separate panels avoid confusing dual axes while keeping the months aligned.
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
