Publication-correct matplotlib figures, journal by journal.
Most matplotlib styling focuses on the look. paperplot does that too — but it
also handles the parts that decide whether a journal accepts your figure:
- sizes it to the journal's real column width (8.6 cm, 17.8 cm, …), not a generic default;
- embeds fonts correctly (Type-42, no Type-3);
- preflights it for the rules figures actually get rejected over — font type, line weight, lettering height, and grayscale legibility.
Ships Physical Review (APS) (incl. PRL/PRX/PRB), Nature, and IEEE, plus a presentation target for slides. matplotlib-only core; seaborn/IPython are optional extras.
Just want the look?
pp.register_mplstyles()thenplt.style.use("paperplot-aps")— no API to learn. You give up only the parts a style sheet can't do (true column sizing, font embedding, preflight);pp.figure()/pp.save()add those back. See Drop-in style sheets.
pip install paperplot-quantum # core (matplotlib + numpy)
pip install "paperplot-quantum[notebook]" # + IPython for pp.show()The PyPI package is paperplot-quantum, but you still import paperplot.
Python ≥ 3.10. Latest main from GitHub:
pip install "git+https://github.com/zlatko-minev/paperplot.git". Full matrix on
the Install page.
import numpy as np
import paperplot as pp
pp.use("aps") # or "nature" / "ieee" / "prl" / "talk" — set once
fig, ax = pp.figure(width="single") # 8.6 cm wide, golden ratio, styled
ax.plot(np.linspace(0, 10, 200), np.sin(np.linspace(0, 10, 200)))
ax.set_xlabel(r"delay $\tau$ (ns)")
ax.set_ylabel(r"population $\langle n \rangle$")
pp.save(fig, "fig1.pdf") # embeds fonts, runs preflight()Same three-curve plot, same data — stock matplotlib versus one pp.use("aps").
paperplot fixes the column width, type scale, color cycle, ticks, and line weights
in a single call.
matplotlib defaults![]() |
paperplot — pp.use("aps")![]() |
![]() |
![]() |
![]() |
A custom GridSpec, four panel letters: (a)
The part most styling packages skip: pp.preview_in_page(fig) drops your figure
into a true-to-scale mock journal page with real body text — so you see exactly
how big it lands in the column and whether the lettering still reads, before you
submit. More in the gallery.
![]() single column |
![]() histograms, in context |
![]() double column, across the page |
Every image is generated from examples/showcase.py by
docs/generate_gallery.py and regenerated by CI on each
docs build (published to GitHub Pages). Build locally:
pip install -e ".[docs,notebook]"
python docs/generate_gallery.py && mkdocs serve| True journal sizing | width="single"/"onehalf"/"double"/"full_page" → exact column widths in inches. |
| Correct export | PDF default, EPS first-class; fonts embedded as Type-42, no Type-3; RGB; revision stamped in metadata. |
| Preflight linter | pp.preflight(fig) → structured Report: flags sub-spec fonts/lines and grayscale-ambiguous colors. Warn, never block. |
| Colorblind-safe by default | Okabe-Ito cycle; sequential/diverging maps kept separate (pp.cmap("BuGn")); custom via pp.register_palette. Fill/stroke convention: pp.fills() (muted) under pp.strokes() (bright). pp.show_palettes() shows every palette tagged for colorblind-safety. |
| Plot helpers | pp.hist_outline (translucent fill + crisp staircase outline), pp.hist_filled, pp.data_fit_band (markers + fit + CI band), pp.swatches. |
| See it on the page | pp.preview_in_page(fig) drops the figure into a true-to-scale mock journal page with real body text — catch sizing/legibility before you submit (a paperplot original). Plus pp.show(fig, zoom=2) and pp.grayscale_proof(fig). |
| Helpers | pp.panel_labels(axes), pp.despine(ax), pp.clean_shared_axes(fig). |
# multi-panel, double column
fig, axes = pp.figure(width="double", nrows=2, ncols=2, sharex=True)
pp.panel_labels(axes, fmt="({})") # (a) (b) (c) (d)
pp.clean_shared_axes(fig)
# sans-serif labels with Computer Modern math is the default (the physics look);
# math="sans" pairs Arial-style math instead, font_scale nudges every size
pp.use("aps", math="cm", font_scale=1.1) # sticky: pp.figure() inherits both
# scoped style (doesn't leak), serif to match REVTeX Times
with pp.style("aps", serif=True):
fig, ax = pp.figure(width="full_page")
# per-figure overrides
fig, ax = pp.figure("single", journal="prl", palette="mylab")
# overlapping histograms, fig-3 style: muted fills, crisp outlines
fig, ax = pp.figure("single")
for s, c in zip((sample_a, sample_b), pp.fills()):
pp.hist_outline(s, ax, bins=60, rescale=True, color=c)
ax.axvline(ideal, color=pp.strokes()[8], ls="--", lw=1.2, zorder=100)
# data + fit + ±1σ band (e.g. lmfit result.eval / eval_uncertainty)
pp.data_fit_band(ax, x, y, yerr=yerr, x_fit=xf, y_fit=yf, y_fit_err=yf_err)
# pre-submission check
report = pp.preflight(fig)
if not report: # truthy == clean
print(report) # aligned table of issuespp.use(...) / pp.figure(journal=...) accept any of:
| key | target | single / double width |
|---|---|---|
aps (+ prl prx prb) |
Physical Review | 8.6 cm / 17.8 cm |
nature |
Nature | 8.9 cm / 18.3 cm |
ieee |
IEEE Transactions / conference | 8.9 cm (3.5″) / 18.2 cm (7.16″) |
talk |
slides / presentation | larger type, thicker lines (scale with the target) |
pp.available() lists them all. New journals are pure data — add a table to
data/journals.toml; no code.
The product is pp.figure()/pp.save() — they size to the real column, embed
fonts, and preflight, which a style sheet cannot. But the look is just
rcParams, and rcParams travel as .mplstyle files, so you can adopt it with zero
buy-in and graduate later:
import paperplot as pp, matplotlib.pyplot as plt
pp.register_mplstyles() # registers paperplot-aps/-nature/-ieee/-talk
plt.style.use("paperplot-aps") # now use plain matplotlib
# composable, matplotlib-native: layer a built-in modifier on top
plt.style.use(["paperplot-nature", "ggplot"])
# or write a file to commit / share / drop into ~/.config/matplotlib/stylelib/
pp.export_mplstyle("ieee", "ieee.mplstyle", serif=True)paperplot never touches matplotlib's global style library on import — registration
is an explicit opt-in. A style sheet carries the target's default (single-column)
size; reach for pp.figure(width=...) when you need the column-true figure.
Journals want figure lettering in Helvetica or Arial (Nature requires it), but
those are proprietary and can't be redistributed. So paperplot ships TeX Gyre
Heros — a free, metric-compatible Helvetica clone — and registers it
automatically. The font preference is Arial → Helvetica → TeX Gyre Heros → DejaVu Sans: if you have the real thing installed it's used, otherwise the
bundled clone gives the same Helvetica look on every machine (and embeds a
proper Type-42 font in the PDF, instead of silently falling back to matplotlib's
DejaVu Sans). This is why the figures look identical on your laptop, a
collaborator's box, and CI. Bundled under the free GUST Font License
(paperplot/data/fonts/LICENSE.md).
See paperplot_DESIGN.md for the full design — journal specs
as data (data/journals.toml), the rcParams mapping, and the registry/versioning
model.
paperplot/
journals.py registry.py data/journals.toml layout.py
style.py lint.py palettes.py preview.py
fonts.py save.py core.py plots.py
mplstyle.py
pip install -e ".[dev]"
pytest # tests
python examples/run_all.py # run every example -> examples/out/render/*.pngPublished to PyPI as paperplot-quantum via Trusted Publishing (OIDC — no
stored tokens). To cut a release: bump version in pyproject.toml, push, then
create a GitHub Release with tag vX.Y.Z. The
release.yml workflow builds, runs twine check,
and publishes. (One-time: register the trusted publisher on PyPI — repo
zlatko-minev/paperplot, workflow release.yml, environment pypi.)








