Matplotlib cheat sheet
A scannable Matplotlib reference: 22 short snippets across 11 topics, each linking back to the lesson it came from.
At a glance
| Topic | What it covers | |
|---|---|---|
| Figures, axes and your first plot | A Figure is the whole image or window; an Axes is one plotting area inside it. Confusing the two is why people fight | lesson |
| Styling, subplots and saving | Share axes (sharex, sharey) whenever panels use the same units — it removes duplicated tick labels and makes comparison | lesson |
| Backends and environment setup | Matplotlib separates the plot description from the thing that draws it. The backend is the renderer: an interactive one | lesson |
| Chart types: bar, scatter, histogram, pie and box | The everyday chart functions, the arguments that matter, and the specific ways each chart can mislead a reader | lesson |
| Titles, legends, annotations and text | Direct labelling beats a legend for two or three series: the reader never has to match a colour to an entry. A legend | lesson |
| Scales, ticks and date axes | Log and symmetric-log scales, tick locators and formatters, and getting date and category axes to read the way you | lesson |
| Colormaps, colour mapping and accessibility | A colour map alone does nothing: you also need a norm that maps data values to the 0-1 range the map expects. Scatter | lesson |
| Images and 3D: imshow, contour and mplot3d | imshow treats an array as pixels and is the fastest way to view a matrix. pcolormesh accepts explicit coordinate arrays | lesson |
| Animations and interactive figures | FuncAnimation calls your update function once per frame. With blit=True it redraws only the artists you return, which | lesson |
| Publication quality: DPI, vector formats and layout | figsize is in inches and dpi is dots per inch, so pixel dimensions are simply the product. A single-column journal | lesson |
| Debugging plots: empty axes, missing data and overlap | Most empty-plot problems have one of four causes: NaN in the data, values outside the limits, a log scale rejecting | lesson |
Quick snippets
Figures, axes and your first plot
Figure versus Axes
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(7, 4)) # the explicit (object) style
ax.plot([1, 2, 3, 4], [1, 4, 9, 16], marker="o", label="y = x²")
ax.set_title("Growth")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.legend()
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig("plot.png", dpi=150)
The four plots that cover most work
ax.plot(x, y) # line: trends over a continuous axis
ax.scatter(x, y, s=20, alpha=0.6) # scatter: relationship between two measures
ax.bar(labels, values) # bar: comparison across categories
ax.hist(values, bins=30) # histogram: distribution of one variable
ax.barh(labels, values) # horizontal bar - better for long labels
ax.boxplot(values) # spread and outliers per group
ax.imshow(matrix) # heatmap-style matrix
Making a chart readable
ax.set_xlim(0, 10)
ax.set_ylim(bottom=0)
ax.tick_params(axis="x", rotation=45)
ax.annotate("peak", xy=(4, 16), xytext=(5, 12),
arrowprops={"arrowstyle": "->"})
ax.legend(loc="upper left", frameon=False)
fig.suptitle("Quarterly revenue")Full lesson: Figures, axes and your first plot →
Styling, subplots and saving
Themes and colours
plt.style.available[:5]
plt.style.use("seaborn-v0_8-whitegrid") # process-wide default
fig, ax = plt.subplots()
ax.plot(x, y, color="#4f46e5", linewidth=2, linestyle="--")
ax.plot(x, y2, marker="s", markersize=5, alpha=0.8)
cmap = plt.get_cmap("viridis") # perceptually uniform
Multiple panels
fig, axes = plt.subplots(1, 2, figsize=(10, 4), sharey=True)
axes[0].plot(x, y)
axes[0].set_title("A")
axes[1].scatter(x, y2)
axes[1].set_title("B")
fig.tight_layout()
# grids with mixed sizes
fig = plt.figure(figsize=(10, 6))
ax1 = fig.add_subplot(2, 2, 1)
ax2 = fig.add_subplot(2, 2, (2, 3)) # spans two cells
ax3 = fig.add_subplot(2, 2, 4)
Saving and embedding
fig.savefig("chart.png", dpi=150, bbox_inches="tight")
fig.savefig("chart.svg") # vector: scales cleanly
fig.savefig("chart.pdf", transparent=True) # for LaTeX / print
# close when generating many figures in a loop, or memory grows
plt.close(fig)Full lesson: Styling, subplots and saving →
Backends and environment setup
What a backend is
import matplotlib
matplotlib.use("Agg") # must run BEFORE pyplot is imported
import matplotlib.pyplot as plt
matplotlib.get_backend() # what is active right now
# inside a Jupyter notebook, instead of a backend call
# %matplotlib inline # static PNG in the output cell
# %matplotlib widget # interactive canvas, needs ipympl installed
Choosing a backend per environment
# headless machine: force a file backend before Python starts
export MPLBACKEND=Agg
# one-off script
MPLBACKEND=Agg python make_charts.py
# a GUI toolkit must be installed for interactive backends
pip install matplotlib PyQt6 # or use tkinter from the standard library
pip install ipympl # for the widget backend in Jupyter
Configuration and matplotlibrc
matplotlib.matplotlib_fname() # the config file actually in use
matplotlib.get_configdir() # where your user config lives
# a matplotlibrc file, applied to every script on this machine
# backend: Agg
# figure.figsize: 7, 4.5
# figure.dpi: 120
# savefig.dpi: 200
# font.size: 10Full lesson: Backends and environment setup →
Chart types: bar, scatter, histogram, pie and box
When a chart misleads
# a pie with ten slices communicates nothing
ax.pie(np.array([30, 20, 12, 9, 8, 7, 6, 4, 3, 1]),
labels=[f"cat{i}" for i in range(10)], autopct="%1.1f%%")
# three bars and a line on one axis: two different scales, zero information
ax.bar(labels, [3, 4, 5, 6]) # counts in hundreds
ax2 = ax.twinx()
ax2.plot(labels, [0.31, 0.42, 0.55, 0.61]) # a rate between 0 and 1
# the honest version: two panels with separate axes
fig, (top, bottom) = plt.subplots(2, 1, sharex=True, figsize=(7, 5))Full lesson: Chart types: bar, scatter, histogram, pie and box →
Titles, legends, annotations and text
Titles and axis labels
fig, ax = plt.subplots(figsize=(7, 4))
ax.set_title("Revenue by quarter", loc="left", fontsize=13)
ax.set_xlabel("Quarter")
ax.set_ylabel("Revenue (GBP millions)")
ax.set_xticks(range(4), ["Q1", "Q2", "Q3", "Q4"])
fig.suptitle("Annual report", fontsize=15) # the whole figure
fig.supxlabel("Fiscal year 2026", fontsize=9)
fig.supylabel("All units in GBP", fontsize=9)
Annotations and text coordinates
ax.annotate("launch", xy=(3, 40), xytext=(4, 25),
arrowprops=dict(arrowstyle="->", color="grey", lw=0.8))
# transData: the default, positions follow the data
ax.text(2, 30, "peak", fontsize=9)
# transAxes: 0-1 across the panel, ignores the data range
ax.text(0.02, 0.95, "provisional", transform=ax.transAxes, va="top")
# transFigure: 0-1 across the whole figure
fig.text(0.5, 0.01, "Source: internal data", ha="center", fontsize=8)Full lesson: Titles, legends, annotations and text →
Scales, ticks and date axes
Scales and limits
ax.set_yscale("log") # orders of magnitude
ax.set_xscale("symlog", linthresh=1.0) # log scale that tolerates zero
ax.set_xscale("logit") # proportions between 0 and 1
ax.set_xlim(0, 100)
ax.set_ylim(bottom=0) # keep the top automatic
ax.invert_yaxis() # depth, rank and lat/lon charts
ax.margins(x=0.02, y=0.05) # breathing room around the data
ax.autoscale(enable=True, axis="y", tight=False)
Tick locators and formatters
from matplotlib.ticker import (MultipleLocator, MaxNLocator,
PercentFormatter, FuncFormatter)
ax.xaxis.set_major_locator(MultipleLocator(10))
ax.yaxis.set_major_locator(MaxNLocator(nbins=6))
ax.yaxis.set_major_formatter(PercentFormatter(decimals=0))
ax.yaxis.set_major_formatter(FuncFormatter(lambda v, pos: f"{v/1000:.0f}k"))
ax.tick_params(axis="x", rotation=45, labelsize=9, length=3)
ax.minorticks_on()
ax.grid(True, which="major", alpha=0.25)Full lesson: Scales, ticks and date axes →
Colormaps, colour mapping and accessibility
Choosing a palette
plt.get_cmap("cividis") # designed for colour-blind readers
plt.get_cmap("RdBu_r") # reversed, so high values read as cold or hot
plt.colormaps() # everything available
# a cyclic map needs both ends to meet
plt.get_cmap("twilight")
# qualitative palettes for categories, never for magnitude
cmap = plt.get_cmap("tab10")
colors = cmap(np.linspace(0, 1, 10))
Colourbar and accessibility
cb = fig.colorbar(sc, ax=ax, orientation="vertical", pad=0.02)
cb.set_label("Response time (ms)")
cb.set_ticks([0, 25, 50, 75, 100])
cb.ax.tick_params(labelsize=8)
# centre the diverging scale on the real midpoint
norm = mpl.colors.TwoSlopeNorm(vcenter=0.0, vmin=-3.0, vmax=3.0)
ax2.scatter(x2, y2, c=values2, cmap="coolwarm", norm=norm)Full lesson: Colormaps, colour mapping and accessibility →
Images and 3D: imshow, contour and mplot3d
Contour lines
x = np.linspace(-3, 3, 200)
y = np.linspace(-3, 3, 200)
X, Y = np.meshgrid(x, y)
Z = np.exp(-(X ** 2 + Y ** 2)) + 0.4 * np.exp(-((X - 1.5) ** 2 + Y ** 2))
cs = ax.contourf(X, Y, Z, levels=20, cmap="viridis")
ax.contour(X, Y, Z, levels=8, colors="white", linewidths=0.5, alpha=0.5)
ax.clabel(cs, inline=True, fontsize=7, fmt="%.2f")
fig.colorbar(cs, ax=ax, label="Density")
A first mplot3d surface
fig = plt.figure(figsize=(7, 5))
ax3 = fig.add_subplot(projection="3d")
ax3.plot_surface(X, Y, Z, cmap="viridis", linewidth=0, antialiased=True)
ax3.contour(X, Y, Z, zdir="z", offset=Z.min(), cmap="viridis", alpha=0.6)
ax3.set_xlabel("x")
ax3.set_zlabel("density")
ax3.view_init(elev=30, azim=-60) # camera angles in degrees
# sometimes the 2-D view is simply better
fig, ax = plt.subplots()
ax.contourf(X, Y, Z, levels=20, cmap="viridis")Full lesson: Images and 3D: imshow, contour and mplot3d →
Animations and interactive figures
Saving to video or GIF
anim.save("wave.mp4", fps=30, dpi=150) # requires ffmpeg on PATH
anim.save("wave.gif", writer="pillow", fps=15) # no ffmpeg needed
from matplotlib.animation import PillowWriter, FFMpegWriter
anim.save("wave.mp4", writer=FFMpegWriter(fps=30, bitrate=1800))
anim.save("wave.gif", writer=PillowWriter(fps=12))
# in a notebook, render a playable clip inline
from matplotlib.animation import HTMLWriter
anim.save("wave.html", writer=HTMLWriter(fps=30))Full lesson: Animations and interactive figures →
Publication quality: DPI, vector formats and layout
Size and DPI arithmetic
fig, ax = plt.subplots(figsize=(3.5, 2.5)) # inches
fig.savefig("fig1.pdf") # vector: resolution is irrelevant
fig.savefig("fig1.png", dpi=600) # 2100 x 1500 pixels
fig.savefig("fig1.tiff", dpi=300, pil_kwargs={"compression": "tiff_lzw"})
print(fig.get_size_inches()) # confirm the physical size
# rule of thumb: line charts 300 dpi, raster images 600 dpi, always prefer vector
Multi-panel figures with labels
fig, axes = plt.subplots(1, 2, figsize=(7, 3), constrained_layout=True)
for label, ax in zip("ab", axes):
ax.plot(x, np.sin(x))
ax.text(-0.16, 1.06, label, transform=ax.transAxes,
fontsize=11, fontweight="bold", va="top", ha="left")
fig.supxlabel("Time (s)", fontsize=9)
fig.savefig("figure1.pdf", bbox_inches="tight", pad_inches=0.02)Full lesson: Publication quality: DPI, vector formats and layout →
Debugging plots: empty axes, missing data and overlap
Overlapping labels and layout
fig, ax = plt.subplots(layout="constrained") # modern replacement for tight_layout
ax.tick_params(axis="x", rotation=45, labelsize=9)
fig.align_labels() # line up shared axis labels
ax.legend(loc="upper left", bbox_to_anchor=(1.0, 1.0)) # outside the data
fig.savefig("chart.png", bbox_inches="tight") # include what overflows
# long tick labels: shorten the strings instead of rotating further
ax.set_xticks(range(len(names)), [n[:18] for n in names], ha="right", rotation=30)Full lesson: Debugging plots: empty axes, missing data and overlap →
FAQ
Is this Matplotlib cheat sheet free to use?
Where do the examples come from?
How do I go deeper than a cheat sheet?
Related cheat sheets
Python 3 NumPy pandas Jupyter Notebook Flask FastAPI
Last refreshed 2026-09-27.