pythonforbio.
[WASM idle]
Plotting08/14

Saving Figures

Starting sandbox…
Beginnerlesson

Saving Figures

In one line: vector (PDF/SVG) for publication, PNG at 300+ DPI for everything else, fonttype=42 so the text stays editable, and rasterized=True on huge scatters.

The call

fig.savefig("figure.pdf", bbox_inches="tight")
fig.savefig("figure.png", dpi=300, bbox_inches="tight")
fig.savefig("figure.svg")
fig.savefig("figure.pdf", bbox_inches="tight", transparent=True)
plt.close(fig)

bbox_inches="tight" crops whitespace and — importantly — rescues labels that would otherwise be clipped off the edge.

fig.savefig, not plt.savefig, so you know which figure you saved. See pyplot vs Object-Oriented API.

Format choice

Format Use for Notes
PDF journals, LaTeX vector, embeds fonts, universal
SVG web, editing in Illustrator/Inkscape vector, XML
EPS legacy journals vector, no transparency
PNG slides, notebooks, GitHub raster; set dpi
TIFF some journals demand it raster, large
JPG never for plots lossy compression creates artefacts on sharp lines

Most journals want vector for line art (300–600 DPI equivalent) and accept 300 DPI raster for photographs and dense scatters.

The font problem

import matplotlib as mpl
mpl.rcParams["pdf.fonttype"] = 42      # TrueType — text stays text
mpl.rcParams["ps.fonttype"] = 42
mpl.rcParams["svg.fonttype"] = "none"  # SVG: don't convert text to paths

By default matplotlib embeds Type 3 fonts, which many PDF editors cannot edit and some journals reject. With fonttype = 42 your labels remain selectable, searchable, and editable in Illustrator — which matters because the last round of figure revision almost always happens there.

Set this once at the top of every plotting script. It costs nothing and saves a resubmission.

Rasterizing selected elements

ax.scatter(x, y, s=1, alpha=0.3, rasterized=True)
fig.savefig("volcano.pdf", dpi=300)

A vector PDF stores every one of your 500,000 scatter points as a separate object. The file becomes hundreds of megabytes and crashes Illustrator and some PDF viewers.

rasterized=True renders that Artist as pixels while keeping axes, ticks and text as vectors. You get a small file with sharp, editable text and a picture-quality point cloud. Essential for volcano plots, Manhattan plots, and single-cell embeddings.

fig.savefig(..., dpi=300) controls the resolution of the rasterized parts even in a vector file.

Sizing for publication

Get this right at creation, not afterwards:

fig, ax = plt.subplots(figsize=(3.5, 2.6))     # single column
fig, axes = plt.subplots(1, 2, figsize=(7.2, 3))  # double column
Target Width (inches) Width (mm)
Single column (most journals) 3.3–3.5 85–89
1.5 column 5.0–5.5 127–140
Double column 6.9–7.2 175–183
Nature single 3.5 89
Nature double 7.2 183

Never scale a figure in the manuscript. If you draw at 10 inches wide with 10 pt fonts and the journal shrinks it to 3.5 inches, your fonts become 3.5 pt and are illegible. Draw at final size and set fonts to what you want in print (7–9 pt for labels is typical).

Check by printing at 100% and reading it.

A reusable save function

from pathlib import Path
import matplotlib as mpl

mpl.rcParams.update({
    "pdf.fonttype": 42, "ps.fonttype": 42, "svg.fonttype": "none",
    "savefig.bbox": "tight", "savefig.dpi": 300,
    "figure.dpi": 100,
})

def save(fig, name, outdir="figures", formats=("pdf", "png")):
    outdir = Path(outdir); outdir.mkdir(parents=True, exist_ok=True)
    for fmt in formats:
        fig.savefig(outdir / f"{name}.{fmt}", format=fmt)
    plt.close(fig)

Saving both PDF (for the manuscript) and PNG (for the slide deck, the Slack message, the lab meeting) every time removes a whole category of "can you send me that as a PNG" round-trips.

Memory in loops

for gene in genes:
    fig, ax = plt.subplots()
    ax.plot(...)
    fig.savefig(f"plots/{gene}.png", dpi=150)
    plt.close(fig)            # ← without this, memory grows until the job dies

Matplotlib keeps every unclosed figure alive. Past 20 you get a warning; past a few thousand you get an OOM kill. plt.close(fig) is not optional in a loop.

Reproducible metadata

fig.savefig("fig.pdf", metadata={
    "Creator": "analysis.py",
    "Title": "Figure 2",
    "Subject": f"commit {git_sha}, seed {SEED}",
})

Embedding the git commit and random seed in the file means that in eighteen months, when a reviewer asks how a figure was made, the figure itself tells you.

Transparent backgrounds

fig.savefig("fig.png", transparent=True, dpi=300)

Good for slides over a coloured background. Bad if you also drew white text or white-edged markers — they vanish.

Common mistakes

  • Default fonttype producing uneditable Type 3 fonts.
  • No bbox_inches="tight", clipping the y-label.
  • Not rasterizing a 500k-point scatter, producing a 200 MB PDF.
  • Not closing figures in a loop.
  • Drawing at the wrong size and scaling later.
  • JPEG for plots.
  • PNG at default 100 DPI for a journal.
  • Saving before drawing is finishedsavefig captures the current state.
  • plt.savefig() with several figures open, saving the wrong one.
  • Forgetting the PNG version and being asked for it later.

See also

Figure and Axes · rcParams and Style Sheets · Matplotlib Artists · Subplots and Layout · Applied - Reproducible Environment

lesson example

No output yet — run the code to populate this drawer.