The matplotlib operations you look up every time — from basic line plots to subplots, custom styles, annotations, and saving publication-quality figures. This reference covers the full plotting workflow.
Quick reference
The 25 patterns that cover 90% of daily matplotlib work.
| Pattern | Code |
|---|---|
| Basic line plot | plt.plot(x, y) |
| Scatter plot | plt.scatter(x, y) |
| Bar chart | plt.bar(x, height) |
| Histogram | plt.hist(data, bins=20) |
| Box plot | plt.boxplot(data) |
| Set title | plt.title("My Title") |
| Axis labels | plt.xlabel("X") ; plt.ylabel("Y") |
| Legend | plt.legend(["Series A", "Series B"]) |
| Show figure | plt.show() |
| Save figure | plt.savefig("fig.png", dpi=150, bbox_inches="tight") |
| Figure size | plt.figure(figsize=(10, 6)) |
| Subplots | fig, axes = plt.subplots(2, 2, figsize=(10, 8)) |
| Plot on axis | ax.plot(x, y) |
| Set x limits | ax.set_xlim(0, 100) |
| Set y limits | ax.set_ylim(-1, 1) |
| Grid | ax.grid(True, alpha=0.3) |
| Tick labels | ax.set_xticklabels(labels, rotation=45) |
| Twin axis | ax2 = ax.twinx() |
| Colormap | plt.scatter(x, y, c=values, cmap="viridis") |
| Colorbar | plt.colorbar(label="Value") |
| Tight layout | plt.tight_layout() |
| Close figure | plt.close() |
| Style sheet | plt.style.use("seaborn-v0_8") |
| Annotate point | ax.annotate("label", xy=(x, y), xytext=(x+1, y+1), arrowprops=dict(arrowstyle="->")) |
| Fill between | ax.fill_between(x, y1, y2, alpha=0.3) |
Setup and imports
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
# Check version
print(mpl.__version__) # 3.8+
# Inline in Jupyter
%matplotlib inline
# Or for interactive plots in Jupyter:
%matplotlib widget
Basic plot types
Line plot
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)
plt.figure(figsize=(8, 4))
plt.plot(x, y, color="steelblue", linewidth=2, linestyle="--", label="sin(x)")
plt.plot(x, np.cos(x), color="coral", linewidth=2, label="cos(x)")
plt.title("Trigonometric Functions")
plt.xlabel("x (radians)")
plt.ylabel("Amplitude")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("trig.png", dpi=150, bbox_inches="tight")
plt.show()
Scatter plot
np.random.seed(42)
x = np.random.randn(200)
y = 0.7 * x + np.random.randn(200) * 0.5
plt.figure(figsize=(6, 6))
plt.scatter(x, y, c=np.abs(x), cmap="plasma", alpha=0.7, edgecolors="white", linewidths=0.5, s=50)
plt.colorbar(label="|x|")
plt.title("Scatter Plot with Colormap")
plt.xlabel("x")
plt.ylabel("y")
plt.tight_layout()
plt.show()
Bar chart
categories = ["A", "B", "C", "D", "E"]
values = [23, 45, 12, 67, 34]
errors = [2, 4, 1, 5, 3]
fig, ax = plt.subplots(figsize=(7, 4))
bars = ax.bar(categories, values, yerr=errors, capsize=5,
color="steelblue", alpha=0.8, edgecolor="white")
# Add value labels on bars
for bar, val in zip(bars, values):
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.5,
str(val), ha="center", va="bottom", fontsize=10)
ax.set_title("Bar Chart with Error Bars")
ax.set_ylabel("Value")
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
Grouped bar chart
x = np.arange(4)
width = 0.35
fig, ax = plt.subplots(figsize=(8, 5))
ax.bar(x - width / 2, [20, 35, 30, 25], width, label="Group A", color="steelblue")
ax.bar(x + width / 2, [25, 32, 34, 20], width, label="Group B", color="coral")
ax.set_xticks(x)
ax.set_xticklabels(["Q1", "Q2", "Q3", "Q4"])
ax.legend()
ax.set_title("Grouped Bar Chart")
plt.tight_layout()
plt.show()
Histogram
data = np.random.normal(50, 10, 1000)
fig, ax = plt.subplots(figsize=(7, 4))
n, bins, patches = ax.hist(data, bins=30, color="steelblue", alpha=0.7, edgecolor="white", density=True)
# Overlay normal distribution curve
from scipy.stats import norm
mu, sigma = norm.fit(data)
x = np.linspace(data.min(), data.max(), 200)
ax.plot(x, norm.pdf(x, mu, sigma), "r-", linewidth=2, label=f"Normal fit (μ={mu:.1f}, σ={sigma:.1f})")
ax.set_title("Histogram with Fit")
ax.set_xlabel("Value")
ax.set_ylabel("Density")
ax.legend()
plt.tight_layout()
plt.show()
Box plot
data = [np.random.normal(0, 1, 100),
np.random.normal(1, 1.5, 100),
np.random.normal(-1, 0.5, 100)]
fig, ax = plt.subplots(figsize=(6, 5))
bp = ax.boxplot(data, labels=["A", "B", "C"], patch_artist=True,
medianprops=dict(color="white", linewidth=2))
colors = ["steelblue", "coral", "seagreen"]
for patch, color in zip(bp["boxes"], colors):
patch.set_facecolor(color)
patch.set_alpha(0.7)
ax.set_title("Box Plot")
ax.set_ylabel("Value")
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
Pie chart
sizes = [35, 25, 20, 15, 5]
labels = ["Python", "JavaScript", "Java", "C++", "Other"]
explode = (0.05, 0, 0, 0, 0) # emphasize first slice
fig, ax = plt.subplots(figsize=(7, 7))
wedges, texts, autotexts = ax.pie(
sizes, labels=labels, explode=explode, autopct="%1.1f%%",
startangle=140, colors=["steelblue", "coral", "seagreen", "gold", "orchid"]
)
for text in autotexts:
text.set_fontsize(9)
ax.set_title("Language Distribution")
plt.tight_layout()
plt.show()
Heatmap
data = np.random.rand(8, 8)
fig, ax = plt.subplots(figsize=(7, 6))
im = ax.imshow(data, cmap="YlOrRd", aspect="auto")
plt.colorbar(im, ax=ax, label="Value")
# Add text annotations
for i in range(data.shape[0]):
for j in range(data.shape[1]):
ax.text(j, i, f"{data[i, j]:.2f}", ha="center", va="center", fontsize=7)
ax.set_title("Heatmap")
ax.set_xticks(range(8))
ax.set_yticks(range(8))
plt.tight_layout()
plt.show()
Subplots
Grid of subplots
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
x = np.linspace(0, 10, 100)
axes[0, 0].plot(x, np.sin(x), color="steelblue")
axes[0, 0].set_title("Sine")
axes[0, 1].plot(x, np.cos(x), color="coral")
axes[0, 1].set_title("Cosine")
axes[1, 0].plot(x, np.exp(-x / 5) * np.sin(x), color="seagreen")
axes[1, 0].set_title("Damped Sine")
axes[1, 1].scatter(np.random.randn(100), np.random.randn(100), alpha=0.5, color="orchid")
axes[1, 1].set_title("Scatter")
for ax in axes.flat:
ax.grid(True, alpha=0.3)
plt.suptitle("2×2 Subplot Grid", fontsize=14, y=1.02)
plt.tight_layout()
plt.show()
Unequal subplot sizes with GridSpec
from matplotlib.gridspec import GridSpec
fig = plt.figure(figsize=(10, 6))
gs = GridSpec(2, 3, figure=fig, hspace=0.4, wspace=0.3)
ax1 = fig.add_subplot(gs[0, :]) # top row, all columns
ax2 = fig.add_subplot(gs[1, 0]) # bottom-left
ax3 = fig.add_subplot(gs[1, 1]) # bottom-middle
ax4 = fig.add_subplot(gs[1, 2]) # bottom-right
x = np.linspace(0, 10, 100)
ax1.plot(x, np.sin(x))
ax1.set_title("Wide plot (full row)")
for ax, title in zip([ax2, ax3, ax4], ["A", "B", "C"]):
ax.plot(np.random.randn(50).cumsum())
ax.set_title(title)
plt.suptitle("GridSpec Layout")
plt.show()
Twin axes (dual y-axis)
fig, ax1 = plt.subplots(figsize=(8, 5))
x = np.arange(12)
revenue = [100, 110, 130, 120, 150, 160, 145, 170, 185, 200, 190, 210]
growth_rate = [0, 10, 18, -8, 25, 7, -9, 17, 9, 8, -5, 10]
color1, color2 = "steelblue", "coral"
ax1.bar(x, revenue, color=color1, alpha=0.7, label="Revenue ($k)")
ax1.set_xlabel("Month")
ax1.set_ylabel("Revenue ($k)", color=color1)
ax1.tick_params(axis="y", labelcolor=color1)
ax2 = ax1.twinx()
ax2.plot(x, growth_rate, color=color2, marker="o", linewidth=2, label="Growth (%)")
ax2.set_ylabel("Growth Rate (%)", color=color2)
ax2.tick_params(axis="y", labelcolor=color2)
ax2.axhline(0, color="gray", linestyle="--", alpha=0.5)
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper left")
plt.title("Revenue and Growth Rate")
plt.tight_layout()
plt.show()
Styling and customization
Line styles, markers, and colors
fig, ax = plt.subplots(figsize=(8, 5))
x = np.linspace(0, 4 * np.pi, 100)
# line style + marker + color in format string: 'color marker linestyle'
ax.plot(x, np.sin(x), "b-", linewidth=2, label="sin (solid)")
ax.plot(x, np.sin(x + 1), "r--", linewidth=2, label="sin+1 (dashed)")
ax.plot(x, np.sin(x + 2), "g-.", linewidth=2, label="sin+2 (dash-dot)")
ax.plot(x, np.sin(x + 3), "m:", linewidth=2, label="sin+3 (dotted)")
ax.plot(x[::10], np.sin(x + 4)[::10], "ko", markersize=8, label="sin+4 (dots only)")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Line styles: - solid, -- dashed, -. dash-dot, : dotted
Markers: o circle, s square, ^ triangle, D diamond, * star, + plus, x cross, . point
Colors: named ("steelblue"), hex ("#2196F3"), RGB tuple ((0.13, 0.59, 0.95)), single-letter ("b")
Style sheets
# List available styles
print(plt.style.available)
# Apply a style
plt.style.use("seaborn-v0_8-whitegrid")
# Other popular styles:
# "ggplot", "fivethirtyeight", "bmh", "dark_background", "tableau-colorblind10"
# Temporarily use a style
with plt.style.context("dark_background"):
plt.plot([1, 2, 3], [4, 5, 6])
plt.show()
Colormaps
# Sequential: "viridis", "plasma", "inferno", "magma", "cividis", "Blues", "Greens"
# Diverging: "RdBu", "seismic", "coolwarm", "bwr", "PiYG"
# Qualitative: "Set1", "Set2", "tab10", "tab20", "Paired"
# Cyclic: "hsv", "twilight"
# Usage in scatter/imshow
sc = plt.scatter(x, y, c=values, cmap="viridis", vmin=0, vmax=1)
plt.colorbar(sc, label="Value")
# Get colors from a colormap for bar/line plots
cmap = plt.get_cmap("tab10")
colors = [cmap(i) for i in range(5)]
Font and text customization
import matplotlib as mpl
# Global font settings
mpl.rcParams.update({
"font.family": "sans-serif",
"font.size": 12,
"axes.titlesize": 14,
"axes.labelsize": 12,
"xtick.labelsize": 10,
"ytick.labelsize": 10,
"legend.fontsize": 10,
"figure.titlesize": 16,
})
fig, ax = plt.subplots()
ax.set_title("Title", fontsize=16, fontweight="bold", pad=12)
ax.set_xlabel("X axis", fontsize=13)
ax.text(0.5, 0.5, "Center text", transform=ax.transAxes,
ha="center", va="center", fontsize=14, color="gray")
Annotations
Annotate a data point
fig, ax = plt.subplots(figsize=(7, 5))
x = np.linspace(0, 10, 100)
y = np.sin(x) * np.exp(-x / 10)
ax.plot(x, y, color="steelblue", linewidth=2)
# Find maximum
idx = np.argmax(y)
ax.annotate(
f"Max ({x[idx]:.1f}, {y[idx]:.2f})",
xy=(x[idx], y[idx]),
xytext=(x[idx] + 2, y[idx] + 0.1),
arrowprops=dict(arrowstyle="->", color="black"),
fontsize=10,
bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", edgecolor="gray"),
)
ax.set_title("Annotation Example")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Shapes and spans
fig, ax = plt.subplots(figsize=(8, 4))
x = np.linspace(0, 10, 100)
ax.plot(x, np.sin(x), "steelblue")
# Horizontal/vertical lines
ax.axhline(0, color="black", linewidth=0.8)
ax.axvline(np.pi, color="red", linestyle="--", label="x=π")
# Shaded region
ax.axvspan(np.pi, 2 * np.pi, alpha=0.15, color="green", label="[π, 2π]")
# Rectangle patch
from matplotlib.patches import Rectangle
ax.add_patch(Rectangle((6, -0.5), 2, 1, facecolor="yellow", alpha=0.3, edgecolor="orange"))
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Axes customization
fig, ax = plt.subplots(figsize=(8, 5))
x = np.linspace(0, 100, 50)
ax.plot(x, np.sqrt(x), "steelblue", linewidth=2)
# Limits
ax.set_xlim(0, 100)
ax.set_ylim(0, 12)
# Custom ticks
ax.set_xticks([0, 25, 50, 75, 100])
ax.set_xticklabels(["0%", "25%", "50%", "75%", "100%"])
# Minor ticks
from matplotlib.ticker import MultipleLocator, FormatStrFormatter
ax.xaxis.set_minor_locator(MultipleLocator(5))
ax.yaxis.set_major_formatter(FormatStrFormatter("%.1f"))
# Grid (major and minor)
ax.grid(which="major", alpha=0.4)
ax.grid(which="minor", alpha=0.15, linestyle=":")
# Logarithmic scale
# ax.set_xscale("log")
# ax.set_yscale("log")
# Remove spines
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.set_title("Axes Customization")
ax.set_xlabel("Percentage")
ax.set_ylabel("Square Root")
plt.tight_layout()
plt.show()
Saving figures
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot([1, 2, 3], [4, 5, 6])
# PNG — raster, good for web
fig.savefig("chart.png", dpi=150, bbox_inches="tight", transparent=False)
# SVG — vector, perfect for print/slides
fig.savefig("chart.svg", bbox_inches="tight")
# PDF — vector, for LaTeX/reports
fig.savefig("chart.pdf", bbox_inches="tight")
# High-res PNG for print
fig.savefig("chart_print.png", dpi=300, bbox_inches="tight")
# Tight layout removes extra whitespace
plt.tight_layout()
plt.savefig("chart_tight.png", dpi=150, bbox_inches="tight")
plt.close(fig) # free memory after saving
bbox_inches="tight" — trims whitespace around the figure. Always use it.
dpi — screen: 72–100; web: 150; print: 300.
Object-oriented vs pyplot API
Matplotlib has two styles. Always prefer the OO API for non-trivial plots.
# Pyplot API (OK for quick one-liners)
plt.figure(figsize=(6, 4))
plt.plot(x, y)
plt.title("Pyplot style")
plt.show()
# Object-oriented API (recommended)
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x, y)
ax.set_title("OO style")
plt.show()
# OO with multiple subplots — each axis is explicit
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
ax1.plot(x, y)
ax2.scatter(x, y)
plt.tight_layout()
plt.show()
Common patterns
Time series
import pandas as pd
dates = pd.date_range("2024-01-01", periods=90, freq="D")
values = np.cumsum(np.random.randn(90)) + 100
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(dates, values, color="steelblue", linewidth=1.5)
ax.fill_between(dates, values, values.min(), alpha=0.15, color="steelblue")
# Rotate date labels
fig.autofmt_xdate(rotation=45)
ax.set_title("Time Series")
ax.set_ylabel("Price")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Error bars
x = np.arange(5)
y = np.array([2.1, 3.4, 2.8, 4.1, 3.7])
yerr = np.array([0.3, 0.4, 0.2, 0.5, 0.3])
fig, ax = plt.subplots(figsize=(6, 4))
ax.errorbar(x, y, yerr=yerr, fmt="o-", capsize=5,
color="steelblue", ecolor="gray", linewidth=2, markersize=8)
ax.set_title("Error Bar Plot")
ax.set_xticks(x)
ax.set_xticklabels(["A", "B", "C", "D", "E"])
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Step plot (for digital signals, ECDF)
fig, ax = plt.subplots(figsize=(7, 4))
x = np.arange(10)
y = np.array([3, 5, 4, 7, 6, 8, 7, 9, 8, 10])
ax.step(x, y, where="post", color="steelblue", linewidth=2, label="Step (post)")
ax.plot(x, y, "o", color="steelblue", markersize=6)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Saving multiple figures to one PDF
from matplotlib.backends.backend_pdf import PdfPages
with PdfPages("report.pdf") as pdf:
for i in range(4):
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(np.random.randn(50).cumsum(), label=f"Series {i}")
ax.legend()
ax.set_title(f"Figure {i + 1}")
pdf.savefig(fig, bbox_inches="tight")
plt.close(fig)
print("Saved report.pdf")
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
plt.plot() after plt.show() |
show() resets state — new blank figure |
Call show() only at the very end |
| Mixing pyplot and OO on the same figure | Confusing; pyplot modifies the current axes | Pick one style per figure |
plt.savefig() after plt.show() |
show() clears the figure first |
Save before show() |
Missing plt.close() in loops |
Memory leak — figures accumulate | Always plt.close(fig) in loops |
figsize in inches, not pixels |
Figure comes out wrong size | Use figsize=(w, h) in inches; set dpi for pixel size |
| Mutating data after plotting | Plot doesn't update | Re-plot or use set_data() on the line object |
plt.xticks(labels) without positions |
Labels misaligned | plt.xticks(ticks, labels) — positions first |
ax.set_title vs plt.title on subplots |
plt.title targets the current axes only |
Use ax.set_title(...) for subplots; plt.suptitle(...) for the whole figure |
Matplotlib vs alternatives
| Library | When to use | Strength |
|---|---|---|
| Matplotlib | Full control, publication figures, custom layouts | Most flexible, widest ecosystem |
| Seaborn | Statistical plots with nice defaults | Prettier defaults, DataFrame-native |
| Plotly | Interactive web charts | HTML/JS output, zoom/hover |
| Bokeh | Interactive data apps | Streaming, dashboards |
| Altair | Declarative grammar-of-graphics | Clean API, Vega-Lite backend |
Pandas .plot() |
Quick EDA | Wraps matplotlib, one-liners |
FAQ
Should I use plt.xxx() or fig, ax = plt.subplots(); ax.xxx()?
Use fig, ax = plt.subplots() (OO API) for everything except throwaway one-liners. The OO API is explicit — you always know which axis you're modifying, and it scales to multiple subplots without confusion.
How do I make the figure bigger/smaller?
plt.figure(figsize=(width_inches, height_inches)) or fig, ax = plt.subplots(figsize=(10, 6)). At 96 DPI, figsize=(10, 6) gives a 960×576 px image. For a different pixel size, also set dpi: savefig("out.png", dpi=150, bbox_inches="tight").
Why does plt.savefig() produce a blank image?
You called plt.show() before plt.savefig(). show() renders and then clears the figure. Always save first: plt.savefig("out.png"); plt.show().
How do I plot a pandas DataFrame column?
df["col"].plot(ax=ax) or ax.plot(df.index, df["col"]). For bar charts: df.plot(kind="bar", ax=ax). Pandas .plot() wraps matplotlib and passes ax directly.
How do I change the color cycle (default colors)?
plt.rcParams["axes.prop_cycle"] = plt.cycler(color=["#E41A1C", "#377EB8", "#4DAF4A", "#984EA3"]). Or use a named color cycle: mpl.style.use("tableau-colorblind10").
How do I add a second x-axis or y-axis?
For a second y-axis: ax2 = ax.twinx(). For a second x-axis: ax2 = ax.twiny(). Then plot on ax2 normally. Use ax.tick_params(axis="y", labelcolor=color) to colour each axis's tick labels to match its line.