Matplotlib Essentials¶
The foundation of Python plotting. Matplotlib gives you total control over every line, tick, and pixel — the go-to for publication figures and the engine under seaborn and pandas'
.plot().
Table of Contents¶
- Installation / Setup
- Core Syntax
- Common Operations
- Practical Examples
- Common Patterns
- Best Practices
- Common Mistakes
- Troubleshooting
- Related Cheat Sheets
- Official Documentation
Installation / Setup¶
For headless scripts (servers, CI), pick the Agg backend before importing pyplot:
import matplotlib
matplotlib.use("Agg") # render to files only, no window
import matplotlib.pyplot as plt
Core Syntax¶
The mental model: Figure → Axes¶
fig, ax = plt.subplots(figsize=(8, 5)) # one axes
fig, axes = plt.subplots(2, 2, figsize=(10, 8)) # 2x2 grid -> axes is (2,2) array
ax.plot(x, y) # draw on a specific axes (recommended)
plt.plot(x, y) # pyplot stateful shorthand (fine for quick EDA)
Two styles coexist: the object-oriented API (fig, ax) is preferred for control and multi-panel figures; plt.* functions act on "the current axes".
Basic chart types¶
| Chart | Function | Typical data |
|---|---|---|
| Line | ax.plot(x, y, label=...) |
Time series, trends |
| Bar | ax.bar(x, heights), ax.barh(x, heights) |
Category totals |
| Histogram | ax.hist(values, bins=30, density=True) |
Distribution of one variable |
| Scatter | ax.scatter(x, y, s=, c=, alpha=) |
Two numeric variables |
| Box | ax.boxplot(data, labels=...) |
Distribution by group |
| Violin | ax.violinplot(data) |
Distribution shape by group |
| Heatmap | ax.imshow(matrix, cmap="viridis", aspect="auto") |
2-D matrix (correlations) |
| Text/annotations | ax.text(x, y, s), ax.annotate(...) |
Labels and callouts |
x = np.arange(5)
y = np.array([3, 7, 2, 8, 5])
fig, ax = plt.subplots()
ax.plot(x, y, marker="o", linestyle="--", linewidth=2, color="#1f77b4", label="score")
ax.bar(x + 0.2, y, width=0.4, alpha=0.6, label="bars")
ax.set_xlabel("x"); ax.set_ylabel("y"); ax.set_title("Example")
ax.legend()
Subplots¶
fig, axes = plt.subplots(2, 2, figsize=(10, 7), sharex=True)
axes[0, 0].hist(df["a"], bins=30)
axes[0, 1].scatter(df["a"], df["b"])
axes[1, 0].boxplot([df["a"], df["b"]], labels=["a", "b"])
axes[1, 1].plot(x, y)
fig.suptitle("Big title", fontsize=16)
fig.tight_layout() # fix overlapping labels
# or pass constrained_layout=True to plt.subplots()
axes from plt.subplots(nrows, ncols) is a NumPy array — flatten before looping: for ax_i in axes.flat:.
Axes, ticks, legends¶
ax.set_xlabel("time (s)"); ax.set_ylabel("score")
ax.set_title("Title"); ax.set_xlim(0, 10); ax.set_ylim(0, 100)
ax.set_xticks([0, 2, 4]); ax.set_xticklabels(["a", "b", "c"], rotation=45)
ax.tick_params(axis="x", labelsize=9)
ax.grid(True, linestyle=":", alpha=0.6)
ax.legend(loc="best", title="groups") # loc: "upper left", "lower right", ...
ax.axhline(y=df["score"].mean(), color="r", ls="--") # reference line
ax.axvline(x=threshold, color="k", ls=":")
ax.twinx() # second y-axis (right side)
Annotations¶
ax.annotate("peak", xy=(x_best, y_best), xytext=(x_best + 0.5, y_best - 10),
arrowprops=dict(arrowstyle="->", color="red"))
ax.text(0.02, 0.98, "n=1000", transform=ax.transAxes, va="top", fontsize=9)
transform=ax.transAxes positions text relative to the axes (0–1) so it stays put on resize.
Styles & colors¶
plt.style.available # list installed styles
plt.style.use("seaborn-v0_8-whitegrid") # popular: ggplot, fivethirtyeight, default
with plt.style.context("ggplot"): # style scoped to a block
fig, ax = plt.subplots(); ax.plot(x, y)
ax.plot(x, y, color="#2ca02c") # hex, named, or RGB(A)
ax.scatter(x, y, c=values, cmap="viridis") # colormaps: viridis, plasma, coolwarm, RdYlBu
fig.colorbar(im, ax=ax, label="density")
Sequential data → sequential maps (viridis, cividis); diverging data → coolwarm/RdBu; avoid the legacy jet.
Exporting figures¶
fig.savefig("figures/result.png", dpi=300, bbox_inches="tight")
fig.savefig("paper/fig1.pdf") # vector — best for papers
fig.savefig("fig1.svg", transparent=True)
# common options: dpi, bbox_inches="tight", facecolor, transparent, format
Always savefig from the fig object (not plt) after drawing but ideally before plt.show().
Common Operations¶
# Color points by a third variable
sc = ax.scatter(df["x"], df["y"], c=df["z"], cmap="viridis", s=20, alpha=0.7)
fig.colorbar(sc, label="z")
# Histogram + KDE-ish overlay
ax.hist(values, bins=30, density=True, alpha=0.6, label="data")
# smooth density: use seaborn.histplot(kde=True) instead
# Grouped box/violin from arrays
groups = [df[df["g"] == g]["v"].values for g in ["a", "b", "c"]]
ax.boxplot(groups, labels=["a", "b", "c"], patch_artist=True)
# Two lines, two scales
ax.plot(t, temp, color="tab:red")
ax2 = ax.twinx()
ax2.plot(t, humidity, color="tab:blue")
Practical Examples¶
1. Annotated bar chart of model scores¶
import matplotlib.pyplot as plt
import numpy as np
models = ["baseline", "logistic", "xgboost"]
scores = np.array([0.72, 0.84, 0.91])
fig, ax = plt.subplots(figsize=(7, 4))
bars = ax.bar(models, scores, color=["#7f8c8d", "#3498db", "#2ecc71"])
ax.set_ylim(0, 1); ax.set_ylabel("accuracy"); ax.set_title("Model comparison")
ax.axhline(0.8, color="k", ls=":", lw=1)
for bar, s in zip(bars, scores): # annotate each bar
ax.text(bar.get_x() + bar.get_width() / 2, s + 0.01, f"{s:.2f}",
ha="center", fontsize=10)
fig.savefig("model_comparison.png", dpi=300, bbox_inches="tight")
2. EDA dashboard with subplots¶
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
axes[0, 0].hist(df["age"], bins=30, edgecolor="white"); axes[0, 0].set_title("Age")
axes[0, 1].scatter(df["age"], df["income"], s=8, alpha=0.5)
axes[1, 0].boxplot([df[df["churn"] == 1]["age"], df[df["churn"] == 0]["age"]],
labels=["churn", "no churn"])
axes[1, 1].bar(*np.unique(df["plan"], return_counts=True))
fig.tight_layout()
3. Trend lines with shaded uncertainty¶
rng = np.random.default_rng(0)
weeks = np.arange(12)
mean = np.linspace(60, 90, 12) + rng.normal(0, 2, 12)
std = np.full(12, 5.0)
fig, ax = plt.subplots()
ax.plot(weeks, mean, marker="o", label="weekly accuracy")
ax.fill_between(weeks, mean - std, mean + std, alpha=0.2, label="±1 std")
ax.set_xlabel("week"); ax.set_ylabel("accuracy (%)")
ax.legend(); ax.grid(ls=":", alpha=0.5)
Common Patterns¶
# One plot per numeric column
numeric = df.select_dtypes("number").columns
fig, axes = plt.subplots(2, (len(numeric) + 1) // 2, figsize=(12, 6))
for ax_i, col in zip(axes.flat, numeric):
ax_i.hist(df[col], bins=40); ax_i.set_title(col)
fig.tight_layout()
# Share axis on purpose
fig, axes = plt.subplots(1, 3, sharey=True) # comparable heights
# Loop drawing + legend once
for grp, gdf in df.groupby("region"):
ax.plot(gdf["date"], gdf["sales"], label=grp)
ax.legend()
# Consistent style for a whole notebook
plt.rcParams.update({"font.size": 11, "axes.grid": True, "figure.dpi": 110})
Best Practices¶
- Use the object-oriented API (
fig, ax) — needed for subplots and reusable code. - Label every axis with units, title every figure; add legends when >1 series.
- Export at
dpi=300(print) or vector PDF/SVG for papers;bbox_inches="tight"to avoid clipped labels. - Keep defaults sensible: start y-axis at 0 for bar charts, not necessarily for line/scatter.
- Prefer colormaps that are perceptually uniform (
viridis,cividis) and colorblind-safe. - Small multiples beat clutter — split many series into subplots with shared axes.
- Build figures from tidy data (one row per observation) with pandas/seaborn, then polish with Matplotlib.
Common Mistakes¶
| Mistake | Consequence | Correct approach |
|---|---|---|
axes[0,0] on a 1-D subplot |
IndexError | Check shape; use .flat or index [0] for 1-D |
Calling plt.title() with multiple axes |
Title lands on the last axes only | ax.set_title(...) per axes |
savefig after show() |
Saves a blank figure | Save before show(), or fig.savefig after drawing |
Not calling fig.tight_layout() |
Overlapping labels/titles | tight_layout() or constrained_layout=True |
| Plotting in a loop in Jupyter | 100 figures spam the notebook | Draw on one fig, axes grid |
| 15 series, default color cycle | Unreadable rainbow | Small multiples + shared legend, or categorical palettes |
| Strings passed where numbers needed | TypeError / alphabetical x-order | Parse dtypes first |
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
TclError: no display name (headless) |
No GUI on server | matplotlib.use("Agg") before import |
| Blank plot in Jupyter | %matplotlib inline missing / wrong backend |
Add the magic or restart kernel |
| Chinese/emoji shown as boxes | Missing glyph in default font | Set font.family to a font with the glyphs |
| Legend overlaps data | Default placement | bbox_to_anchor=(1.02, 1) outside, or loc="upper left" |
| Dates on x-axis as raw numbers | Datetime not parsed | pd.to_datetime; use ax.xaxis.set_major_formatter if needed |
MatplotlibDeprecationWarning about styles |
Old seaborn style name | Use current names: seaborn-v0_8-whitegrid |
Related Cheat Sheets¶
- seaborn.md — statistical charts on top of Matplotlib
- plotly.md — interactive alternative for exploration and dashboards
- pandas.md —
.plot()shortcuts delegate to Matplotlib - Data Visualization category index · Home · Index
Official Documentation¶
Last Updated¶
2026-09-06 — Initial version.