Publication-quality matplotlib figures for astronomy journals.
One pip install gives you journal-matched styles for MNRAS, RASTI,
A&A, ApJ/ApJL, the Open Journal of Astrophysics, PRD/PRL (and
the other Physical Review journals), JCAP and Nature Astronomy,
as well as the chemistry journals of the RSC and the ACS — figures
at exactly the right physical size, accessible colours and colormaps from
CMasher, and helpers that make the tedious parts (sizing, panel labels,
accessibility checks, saving) one-liners.
pip install plotastro # or: conda install -c conda-forge plotastroSimplest usage — no new API to learn. Importing plotastro registers the
styles with matplotlib itself; after one plt.style.use line you write
ordinary matplotlib, and the default figure size is already the journal's
column width:
import matplotlib.pyplot as plt
import plotastro # just to register the styles
plt.style.use("mnras") # or "aanda", "apj", "oja", "prd", ...
fig, ax = plt.subplots() # plain matplotlib from here onWith the helpers (optional, but they make the tedious parts one-liners):
import plotastro as pa
pa.set_style("mnras")
fig, ax = pa.subplots() # one-column figure, golden-ratio height
ax.plot(x, y, label="model")
ax.set_xlabel("$x$")
ax.legend()
pa.savefig("myplot") # -> myplot.pdf, ready for \includegraphics| One column | Full width |
|---|---|
![]() |
![]() |
Full documentation: plotastro.readthedocs.io — or start with the tutorial notebook, which walks through every feature with runnable examples.
Two problems ruin most paper figures:
- Wrong physical size. If you hand LaTeX a 6-inch figure and it squeezes
it into an 84 mm column, every label shrinks by ~50 % and becomes
unreadable. The fix: build the figure at its final printed width, then
include it with a plain
\includegraphics{fig.pdf}— no[width=...]. - Inaccessible colours. ~5 % of male readers have a colour-vision
deficiency, and MNRAS's
author guidelines
explicitly ask for colour-blind-friendly figures, and many readers print
in greyscale. Hand-picked colour cycles, matplotlib's default among them,
have colours that merge for these readers. Colours taken from
CMasher's maps stay distinct:
pa.set_style("mnras", palette="cmr.rainforest"). Thenpa.check_figure()lets you verify the finished figure.
The styles share one visual language — Times-like serif fonts at ~9 pt with
~8 pt tick lettering, inward ticks on all four sides with minors, a subtle
grid, legends on a translucent white background — and differ only in figure width (plus the
sans-serif fonts Nature requires), so your plots stay consistent between
papers no matter where you submit. The chemistry styles (rsc, acs) keep
the ticks, grid, legends and colours but follow their publishers' rules:
sans-serif lettering, all of it at 8 pt, and no line thinner than 0.5 pt.
pa.set_style(...), pa.figsize(...) and plt.style.use(...) accept
(aliases in parentheses):
| key | journal | one column | full width |
|---|---|---|---|
mnras |
Monthly Notices of the RAS | 240.0 pt = 3.32 in | 504.0 pt = 6.97 in |
rasti |
RAS Techniques & Instruments | 240.0 pt = 3.32 in | 504.0 pt = 6.97 in |
aanda (a&a, aa) |
Astronomy & Astrophysics | 250.4 pt = 3.46 in (88 mm) | 512.2 pt = 7.09 in (180 mm) |
apj (apjl, aastex) |
The Astrophysical Journal | 242.3 pt = 3.35 in | 513.1 pt = 7.10 in |
oja |
Open Journal of Astrophysics | ≈245.3 pt = 3.39 in | ≈508 pt = 7.03 in |
prd (prl, revtex) |
Physical Review D, and the other Physical Review journals (PRL, PRA, PRB, PRC, PRE, PRX, ...), which share its REVTeX layout | 246.0 pt = 3.40 in | 510.0 pt = 7.06 in |
jcap |
J. Cosmology & Astroparticle Phys. | single-column ≈455 pt = 6.30 in | — |
natastro (nature) |
Nature Astronomy (sans-serif!) | 253.2 pt = 3.50 in (89 mm) | 520.7 pt = 7.20 in (183 mm) |
rsc |
All Royal Society of Chemistry journals (sans-serif) | 236.2 pt = 3.27 in (8.3 cm) | 486.5 pt = 6.73 in (17.1 cm) |
acs (jacs, achemso) |
All American Chemical Society journals (sans-serif) | 240.9 pt = 3.33 in | 505.9 pt = 7.00 in |
thesis |
A4 thesis text width | 426.8 pt = 5.91 in | — |
beamer |
Beamer slide text width | 307.3 pt = 4.25 in | — |
Widths come from each journal's LaTeX class / author guide. For a custom
document, put \the\columnwidth or \the\textwidth in your .tex body,
compile, read the value off the page, and pass it directly:
pa.figsize(width=345.0).
Each chemistry publisher has one figure guide for all its journals, so
there is one style per publisher. rsc covers every Royal Society of
Chemistry journal (Chemical Science, ChemComm, PCCP, RSC Advances,
...), at the RSC's 8.3 cm and 17.1 cm column widths. acs covers every
American Chemical Society journal (JACS, ACS Nano, Langmuir, ...), at
3.33 in and 7 in. ACS recommends Helvetica or Arial lettering, and some
ACS journals ask for no text smaller than 8 pt, so both styles use
sans-serif fonts with all text at 8 pt. ACS also asks for no line thinner
than 0.5 pt, so the minor ticks and grid lines are 0.5 pt. RSC sets no
font rules, so rsc uses the same lettering as acs. There is no
author-list format for these journals yet (use journal="generic").
The only hard dependency is matplotlib; plotastro works with both NumPy 1.x
and 2.x (CI tests each). CMasher colours
and colormaps are an optional extra (pip install "plotastro[cmasher]"; see
below). Running the examples from a clone?
pip install -r requirements-dev.txt.
pa.figsize("column") # one column, golden-ratio height
pa.figsize("full") # full text width
pa.figsize("column", fraction=0.5) # half a column
pa.figsize("column", aspect=1) # square panel (aspect = height/width)
pa.figsize("column", journal="aanda") # size for a specific journal
pa.figsize(345.0) # any width in LaTeX pointspa.subplots() takes the same arguments plus everything plt.subplots
accepts, and scales the height with the grid so each panel keeps its aspect:
fig, ax = pa.subplots() # 1 panel, one column
fig, axes = pa.subplots(2, 2, width="full") # 2x2 grid, full width
fig, axes = pa.subplots(1, 2, width="full", aspect=0.75, sharey=True)For colours, plotastro recommends CMasher
(van der Velden 2020, JOSS 5, 2004):
perceptually uniform scientific colormaps (sequential, diverging and
cyclic), most of them colour-vision-deficiency friendly. In its sequential
maps the lightness rises steadily from one end to the other, so colours
sampled from them stay distinct for readers with a colour-vision deficiency
and in greyscale print. CMasher is not a dependency. Install it only
if you want it (pip install cmasher, or pip install "plotastro[cmasher]");
plotastro imports it only when you ask for a CMasher colour. Names start
with cmr., as in CMasher itself:
pa.set_style("mnras", palette="cmr.rainforest") # 8-colour cycle from a CMasher map
pa.set_style("mnras", cmap="cmr.ocean") # default colormap for imshow etc.
ax.set_prop_cycle(color=pa.cmasher_colors("torch", n=5)) # n discrete colours
ax.imshow(img, cmap=pa.cmasher_cmap("rainforest")) # the colormap
ax.contourf(x, y, z, levels=6, cmap=pa.cmasher_cmap("iceburn", n=6)) # 6 levelsDiscrete colours are sampled from cmap_range=(0.15, 0.85) by default.
This follows CMasher's advice, since most of its sequential maps run from
black to white and those ends vanish on the page. For lines that must be
easy to tell apart, CMasher suggests apple, chroma, neon,
rainforest or torch; for steps of one quantity, a single-hue map such
as flamingo, freeze, gothic, jungle or ocean. To span the whole
map with a fixed number of lines, sample exactly that many:
pa.cmasher_colors("rainforest", n=len(models)). Please cite CMasher if
you use it (cmasher.get_bibtex()).
Matched shades without transparency (better for print and EPS than alpha=):
color = pa.cmasher_colors("torch", n=3, cmap_range=(0.3, 0.8))[0]
ax.plot(x, y, color=color)
ax.fill_between(x, lo, hi, color=pa.lighten(color, 0.6))
pa.darken(color, 0.3) # the other directionThe colours page of the documentation has more: which map suits which kind of data, lines coloured by a parameter with a colour bar, diverging and cyclic data, and common errors. The tutorial notebook runs the same examples.
Don't take any palette's word for it — simulate it
(Machado et al. 2009 model, no extra dependencies). The figure shows the 8
colours of palette="cmr.rainforest": they stay distinct under each
simulation, because each colour is lighter than the one before.
colors = pa.cmasher_colors("rainforest")
pa.check_colors(colors) # under deuteranopia, protanopia and greyscale
pa.check_figure(fig) # simulate a whole rendered figure — the
# final check before submission
pa.simulate_cvd(colors, "deuteranopia") # the raw transformIf two lines merge in any panel, add markers or dash patterns (below), or sample fewer colours so they are further apart. MNRAS recommends Color Oracle and ColorBrewer for exactly this; now it's built in.
Without a palette=, the styles use plotastro's default cycle, pa.COLORS
(matplotlib's "C0"…"C11"): a reordering of ColorBrewer Set1 with
three light colours from Tableau's Color Blind 10. Also shipped:
pa.OKABE_ITO (Okabe & Ito 2008),
pa.PETROFF10 and pa.PETROFF8 (Petroff 2021),
pa.TOL_VIBRANT (Paul Tol) and
pa.PAIRED (light/dark pairs); any of them can be the cycle:
pa.set_style("mnras", palette="okabe_ito"). These palettes pick colours by
hue, so they are not reliably accessible: every one of them has pairs that
print as the same grey, and in the default cycle brown and red are also
hard to tell apart with protanopia. If you use them, pair the colours with
markers or dash patterns (below) and check with pa.check_figure(fig).
The styles' default colormap is viridis (perceptually uniform, CVD-safe).
Good matplotlib picks: viridis/magma/cividis for sequential data,
RdBu_r or coolwarm for diverging data (red–blue, not red–green). Avoid
jet/rainbow. Change the default with pa.set_style("mnras", cmap="cividis"),
or see cmocean.
pa.MARKERS = ["o", "s", "^", "D", "v", "p", "*", "X"] — filled shapes that
survive shrinking to 4 pt. Conventions worth knowing:
| marker | typical use in astro figures |
|---|---|
"o" "s" "D" |
primary data series |
"^" / "v" |
lower / upper limits (readers expect this) |
"*" "p" |
highlight special objects (the Sun, a best-fit point) |
"x" "+" |
thin crosses — dense scatter plots, since they don't occlude |
"." |
huge point clouds (use ms=1–2, or better, rasterized hexbin) |
Useful tricks: markevery=7 thins markers on dense curves;
mfc="none" (hollow markers) keeps overlapping datasets readable;
ms= and mew= control size and edge width.
Beyond matplotlib's "-", "--", ":", "-.", the dict pa.LINESTYLES
provides named dash tuples of the form (offset, (on, off, ...)) in points:
ax.plot(x, y, ls=pa.LINESTYLES["long dash"]) # (0, (9, 3))
ax.plot(x, y, ls=(0, (4, 1, 1, 1))) # or roll your ownGuidelines: keep to ≤ 4 distinct dash patterns per panel (more becomes noise); use solid for data / the headline result and dashes/dots for models and references; MNRAS explicitly warns against triple-dot-dashed lines.
Colour should never be the only difference between curves. pa.style_cycler
advances colour, marker and/or line style in step, so every series is
unique in two or three channels at once (and survives greyscale printing):
ax.set_prop_cycle(pa.style_cycler(markers=True)) # one axes
ax.set_prop_cycle(pa.style_cycler(linestyles=True, markers=True))
plt.rc("axes", prop_cycle=pa.style_cycler(markers=True)) # everywhereJournals want multi-panel figures labelled (a), (b), (c)…:
fig, axes = pa.subplots(2, 2, width="full")
pa.label_panels(axes) # (a) (b) (c) (d)
pa.label_panels(axes, loc="outside", fmt="{}", fontweight="bold") # Nature style
pa.label_panels(axes, uppercase=True, loc="lower right") # (A) ... bottom-rightBy default the styles use matplotlib mathtext with STIX fonts: Times-compatible maths, zero dependencies. For pixel-perfect agreement with your manuscript (custom macros, real kerning):
pa.set_style("mnras", usetex=True) # needs latex + dvipng + ghostscriptThis loads the newtx Times fonts (matching the MNRAS/A&A house font), or
Helvetica for Nature Astronomy and the chemistry styles. Develop with usetex=False, flip it on for the
final version — LaTeX rendering is slow.
The styles bake in submission-friendly defaults: PDF output, 450 dpi for
rasterised elements (journals want ≥ 300–400), tight bounding box, and
TrueType font embedding (pdf.fonttype: 42, so no Type-3 font rejections).
pa.savefig("figure1") # figure1.pdf
pa.savefig("figure1", formats=("pdf", "png")) # + a PNG for slides/Slack
pa.savefig("figure1", fig=fig, dpi=600) # extra options pass throughIf a journal insists on EPS, note EPS has no transparency — replace
alpha= with pa.lighten() shades (a good habit anyway).
Assembling the author/affiliation block by hand is error-prone on long collaborations. Feed plotastro the author CSV your collaboration already maintains — it works with real-world lists exactly as they are (this is examples/authors_example.csv):
Lastname,Firstname,Authorname,Email,JoinedAsBuilder,Affiliation,ORCID,
Bandi,Behnood,Behnood Bandi, b.bandi@sussex.ac.uk, False,"Astronomy Centre, University of Sussex, Falmer, Brighton BN1 9QH, UK",0000-0001-5838-3903,
Rocher,Antoine,Antoine Rocher,antoine.rocher@epfl.ch,False,"EPFL, \'{E}cole polytechnique f\'{e}d\'{e}rale de Lausanne, Chemin des Maillettes, 51, 1290 Versoix, Switzerland",0000-0003-4349-6424,
Verdier,Aur\'{e}lien,Aur\'{e}lien Verdier,aurelien.verdier@epfl.ch,False,"EPFL, \'{E}cole polytechnique f\'{e}d\'{e}rale de Lausanne, Chemin des Maillettes, 51, 1290 Versoix, Switzerland",,
Richard,Johan,Johan Richard,johan.richard@univ-lyon1.fr,False,"CRAL, Centre de Recherche Astrophysique de Lyon, Universit\'{e} de Lyon, 9 avenue Charles Andr\'{e}, 69230 Saint-Genis-Laval, France",0000-0001-5492-1049,
Loveday,Jon ,Jon Loveday, j.loveday@sussex.ac.uk, False,"Astronomy Centre, University of Sussex, Falmer, Brighton BN1 9QH, UK",0000-0001-5290-8940,
Brown,Michael,Michael Brown,michael.brown@monash.edu,False,"Monash, School of Physics and Astronomy, Monash University, Wellington Road, Clayton, VIC 3800, Australia",0000-0002-1207-9137,It recognises Authorname (or name, or Firstname+Lastname),
Affiliation/affiliations (several separated by ;, or one row per
affiliation — repeated author rows are merged), and optional ORCID and
Email; every other column is ignored (JoinedAsBuilder, ...), stray
spaces are stripped, and LaTeX already in the file (accents like \'{e})
passes through untouched. Affiliations are numbered in order of first
appearance and shared between authors automatically; the first author with
an email becomes the corresponding author.
print(pa.authorlist("authors_example.csv", journal="mnras"))\author[B. Bandi et al.]{
Behnood Bandi,$^{1}$\thanks{E-mail: b.bandi@sussex.ac.uk}
Antoine Rocher,$^{2}$
Aur\'{e}lien Verdier,$^{2}$
Johan Richard,$^{3}$
Jon Loveday$^{1}$
and Michael Brown$^{4}$
\\
% List of institutions
$^{1}$Astronomy Centre, University of Sussex, Falmer, Brighton BN1 9QH, UK\\
$^{2}$EPFL, \'{E}cole polytechnique f\'{e}d\'{e}rale de Lausanne, Chemin des Maillettes, 51, 1290 Versoix, Switzerland\\
$^{3}$CRAL, Centre de Recherche Astrophysique de Lyon, Universit\'{e} de Lyon, 9 avenue Charles Andr\'{e}, 69230 Saint-Genis-Laval, France\\
$^{4}$Monash, School of Physics and Astronomy, Monash University, Wellington Road, Clayton, VIC 3800, Australia
}The same CSV works for every astronomy and physics journal: mnras/rasti,
aanda (\inst/\institute), apj/oja (AASTeX \author/\affiliation
with ORCIDs), prd (REVTeX), jcap (lettered \affiliation[a]), or
generic for a plain numbered block (the chemistry journals have no
format of their own yet). A command-line tool ships with the package, so
co-authors who don't use Python can run it too:
plotastro-authors authors.csv --journal aanda
plotastro-authors authors.csv -j apj -o authors.texSee examples/authors_example.csv for a complete example.
set_style(journal, usetex=, grid=, palette=, cmap=, **rc) |
activate a journal's style (alias: use) |
authorlist(csv, journal=) |
LaTeX author/affiliation block from a CSV (CLI: plotastro-authors) |
figsize(width, journal=, fraction=, aspect=, ...) |
journal-correct figure dimensions |
subplots(...) |
plt.subplots with the size computed for you |
savefig(name, formats=("pdf",)) |
save one figure in several formats |
label_panels(axes, ...) |
(a), (b), (c) panel labels |
style_cycler(markers=, linestyles=) |
redundant-encoding property cycle |
cmasher_colors(cmap, n=, cmap_range=), cmasher_cmap(cmap, cmap_range=, n=) |
CMasher colours / colormaps (optional cmasher package) |
COLORS, CYCLE, OKABE_ITO, PETROFF8, PETROFF10, TOL_VIBRANT, PAIRED |
built-in palettes |
lighten(c, f), darken(c, f) |
matched shades without transparency |
simulate_cvd, check_colors, check_figure |
colour-vision-deficiency checks |
MARKERS, LINESTYLES |
curated marker / dash-pattern sequences |
show_colors(), show_markers(), show_linestyles() |
reference charts |
current_journal(), JOURNALS, GOLDEN |
introspection |
set_size(...) |
deprecated alias for the original myfigsize API |
- Turn the grid off:
pa.set_style("mnras", grid=False), or per-axesax.grid(False). - Override anything:
pa.set_style("mnras", **{"font.size": 10}), orplt.rcParams[...] = ...afterset_style. - "Times New Roman not found" warning: the font list falls back through
Times → Nimbus Roman → STIX → DejaVu automatically; install
mscorefonts/STIX to silence it, or ignore it. - Labels getting cut off? They shouldn't be — the styles enable
constrained_layout. If you manage layout manually, disable it withplt.rcParams["figure.constrained_layout.use"] = False. - Astronomical images: use
origin="lower"inimshow(or uncommentimage.origin: lowerin the style file), andax.grid(False). - Figures look huge/small on screen: that's just
figure.dpi: 150for display; the saved size is exact. - Styles without Python helpers: after
import plotastroonce,plt.style.use("mnras")works in any code; or copy the.mplstylefiles fromsrc/plotastro/styles/intomatplotlib.get_configdir()/stylelib/. - Old API:
plotastro.set_size(...)reproduces the originalmyfigsize.set_size(); the oldMNRAS_Style.mplstyleis nowplt.style.use("mnras").
git clone <this repo> && cd <repo>
pip install -e ".[dev]"
pytest # run the test suite
python tools/generate_styles.py # regenerate styles/ after editing the template
python examples/make_reference_figures.py # regenerate README figuresThe .mplstyle files are generated from the templates in
tools/generate_styles.py — edit that, not the
files (CI checks they stay in sync). Releases: bump the version in
pyproject.toml and CHANGELOG.md, then push a v* tag — the
publish workflow builds and uploads to PyPI
(see the one-time trusted-publishing setup notes in that file).
- Original MNRAS style this grew from: M. Knabenhans' mplstyle_for_MNRAS
- Set1 ordering of the default cycle: Thøger Rivera-Thorsen; light colours from Tableau Color Blind 10
- Palettes: Okabe & Ito, Petroff (2021), Paul Tol, ColorBrewer Paired
- Optional colormaps: CMasher (E. van der Velden 2020, JOSS 5, 2004; BSD-3-Clause), used as an optional dependency, not bundled
- CVD model: Machado, Oliveira & Fernandes (2009), IEEE TVCG 15(6)
- Figure-size approach after Jack Walton's guide
- Journal guidelines: MNRAS · A&A · AAS Journals · OJA · APS · Nature · RSC · ACS
MIT licensed — see LICENSE.






