Skip to content

Saving figures

Figure.save picks the format from the file extension (case-insensitive):

fig.save("figure.pdf")
fig.save("figure.svg")
fig.save("figure.png", dpi=300)
fig.save("figure.html")
Extension Output
.pdf Vector PDF with real embedded/subset fonts — editable text
.svg Vector SVG with the font embedded (@font-face)
.png Raster at dpi (default 200), with physical-size metadata
.html / .htm A single self-contained page (see below)

Anything else raises ValueError rather than guessing.

PDF: editable, selectable, accessible text

This is pyplotrs' headline feature. Text in the PDF is genuine embedded, subsetted font data — not outlines — so:

  • opening the PDF in Illustrator/Inkscape lets you select, re-type and restyle every label;
  • pdftotext / copy-paste extracts the text (math included, as Unicode);
  • pdffonts shows the embedded subsets.
pdftotext figure.pdf - | head
pdffonts figure.pdf

Pass tagged=True to write a tagged, accessible PDF: the whole chart becomes one Figure structure element with alt text (auto-derived from the titles/labels, or set explicitly), plus a document title and language, so screen readers can announce it.

fig.save("figure.pdf", tagged=True, title="Figure 1", alt="Response vs time")

Every text face in use is embedded as its own subset, so a figure mixing regular, bold, italic and math carries four subsets and every one of them stays selectable.

Resolution

dpi sets the resolution of raster (.png) output. The default of 200 dpi is publication-quality; bump it for print:

fig.save("figure.png", dpi=600)

The value is also written into the PNG's pHYs chunk, so the file knows its own physical size and lands at the right size in a document rather than at whatever the importing application assumes.

A PDF or SVG page is resolution-independent, but an image inside one — imshow, pcolormesh, contourf — is not, and dpi sets its resolution too:

fig.save("figure.pdf", dpi=600)   # the heatmap inside is 600 ppi, not 200

That matters when a journal states a minimum for figures containing raster content, which is usually 300 dpi and often 600 for anything with fine detail.

Line widths thinner than half a point

A stroke below about 0.5 pt is not WYSIWYG in a PDF viewer. Viewers enforce a minimum rendered width of one device pixel, so a 0.2 pt rule that measures 0.2 pt in the PNG is drawn as a full pixel on screen — visibly heavier, and heavier by a different amount at each zoom level. The line is correct in the file, and a press rendering at its own resolution will print it at 0.2 pt; only the preview lies.

Most journals set a minimum line width anyway (commonly 0.25 pt, sometimes 0.5 pt), so the practical advice is the same either way: keep rules at 0.5 pt or above unless you have a reason not to.

mine = pp.themes.default.with_(line_width=0.8, spine_width=0.6)

Transparent backgrounds

transparent=True drops the white page fill from .png output in favor of an alpha channel — useful for dropping a figure onto a colored slide or webpage background:

fig.save("figure.png", transparent=True)

.pdf/.svg/.html paint no page background to begin with, so they are already "transparent" and ignore the flag.

HTML

.html writes a single portable page with nothing fetched at view time:

  • 2D figures are inlined as vector SVG with real selectable text and embedded fonts. If any label contains $...$ math, it is re-rendered by an inlined copy of MathJax (so the math is selectable and copyable as LaTeX/MathML), fully offline.
  • 3D figures become a dependency-free Canvas2D viewer you can orbit, zoom and pan.
fig.save("figure.html")

title= and alt= label the page and its inline SVG (role="img") here too, auto-derived from the figure's own titles and labels when omitted.

Cross-machine consistency

Whichever body font is resolved, it is embedded into every saved file, so a figure looks identical wherever it's opened — independent of the fonts installed on the viewer's machine. See styling & themes.

Saving many figures at once

Rendering releases the GIL, so a thread pool over figures actually runs in parallel — and with no global state there is nothing for the threads to fight over:

from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor() as pool:
    pool.map(lambda i: build(i).save(f"panel{i}.pdf"), range(64))

See performance.

Animated output

An animation is saved through its own object, not Figure.save, and writes .gif or .apng:

pp.animate(render, frames=60).save("wave.gif")