Publication-Ready Figures in Python: matplotlib Settings for Journal Submission

April 27, 2026

Reviewer comment three rounds in: “Figure 3 is unreadable at single-column width.” The axis labels were 12 pt in your draft, which collapsed to about 5 pt after the journal scaled the figure to 89 mm. The fix is not better matplotlib defaults — it is treating figure size and font size as a coupled decision from the start.

This guide covers the matplotlib configuration that actually produces journal-acceptable figures the first time: explicit physical sizes in inches, fonts pinned to the journal’s requirements, vector output for line art, and a single rcParams block you can paste into a project. Skip the generic “use 300 DPI” advice — the real constraints are tighter and more specific.

Set Figure Size in Inches, Not Pixels

Most journals specify figure dimensions in millimeters at the final printed size. The standard widths:

  • Single column: 89 mm (~3.5 in) — Nature, Science, Cell, JAMA, PLOS ONE all converge here
  • 1.5 column: 120 mm (~4.7 in) — Nature, Science (used for medium-complexity figures)
  • Full / two-column: 183 mm (~7.2 in) — the maximum width for any figure

Pass these directly to plt.figure(figsize=(width_in, height_in)). Do not rely on the default 6.4×4.8 in figure and rescale at submission — everything you put on that figure (fonts, line widths, marker sizes) was sized against the wrong canvas.

Width conversion $\text{width (in)} = \frac{\text{width (mm)}}{25.4}$

Single column: 89 / 25.4 = 3.504 in. Full width: 183 / 25.4 = 7.205 in.

Set the height by aspect ratio, not by guess. A square panel uses height = width. A wide-format trend plot is often width × 0.6 to 0.7. For multi-panel figures, set the figure size to the final composite size and let plt.subplots(nrows, ncols) divide it.

Pin the Fonts

Most journals require sans-serif Helvetica or Arial at 6–8 pt for axis labels and tick labels. Nature specifies 5–7 pt; Cell uses 6–8 pt depending on figure type; JAMA prefers Arial at 8 pt minimum. The matplotlib default is DejaVu Sans at 10 pt — too large at single-column width and not on most journals’ accepted-fonts list.

import matplotlib as mpl

mpl.rcParams.update({
    'font.family': 'sans-serif',
    'font.sans-serif': ['Helvetica', 'Arial', 'DejaVu Sans'],
    'font.size': 7,
    'axes.labelsize': 8,
    'axes.titlesize': 8,
    'xtick.labelsize': 7,
    'ytick.labelsize': 7,
    'legend.fontsize': 7,
    'figure.dpi': 150,
})

If Helvetica is not installed, matplotlib silently falls back to DejaVu Sans — the fallback chain above prevents that. On Linux, install ttf-mscorefonts-installer to get Arial; macOS ships with Helvetica.

Use Vector Output for Anything With Lines or Text

The PNG-vs-PDF question is not about DPI. Vector formats (PDF, SVG, EPS) store paths and text as instructions, not pixels — they remain crisp at any zoom level and pass journal print workflows without rasterization artifacts. PNG is appropriate only for figures dominated by raster content (microscopy images, photographs, pixel-based heatmaps with thousands of cells).

ContentFormatWhy
Line plots, bar charts, box plotsPDF or SVGVector preserves crisp lines at print size
Scatter plots with < ~5,000 pointsPDFVector; file stays under 5–10 MB
Scatter plots with > 10,000 pointsPNG at 600 DPIVector files become huge and slow to render
Heatmaps, microscopy, photosTIFF or PNG at 300–600 DPINative raster content

Save with fig.savefig('fig3.pdf', bbox_inches='tight', pad_inches=0.02). The bbox_inches='tight' argument trims whitespace; pad_inches leaves a small margin. Avoid bbox_inches='tight' when you have set an exact figure size for journal compliance — it will silently change the dimensions.

Color Choices Matter More Than Most People Realize

Nature and Science explicitly require color schemes that work for readers with color vision deficiency (~8% of men, ~0.5% of women). The default matplotlib color cycle (the “tab10” palette) fails this test — red and green are indistinguishable for the most common deficiency type.

Use a palette that is colorblind-safe by construction:

# Wong colorblind-safe palette (Nature Methods, 2011)
WONG = ['#000000', '#E69F00', '#56B4E9', '#009E73',
        '#F0E442', '#0072B2', '#D55E00', '#CC79A7']

mpl.rcParams['axes.prop_cycle'] = mpl.cycler(color=WONG)

For continuous colormaps (heatmaps), use viridis or cividis — both are perceptually uniform and colorblind-safe. Avoid jet entirely; it is perceptually nonlinear and obscures the data’s actual range. The matplotlib documentation calls this out, but the default for older code is still jet, so check your inheritance.

Common Mistake

Using plt.tight_layout() after setting an exact figure size. tight_layout resizes the axes inside the figure to fit labels, which can change the plotted area’s proportions. For journal compliance, use constrained_layout=True when creating the figure, or manually adjust spacing with fig.subplots_adjust(). Both preserve the outer figure dimensions you set.

Tick Marks and Axis Spines

Default matplotlib draws ticks pointing outward and includes all four spines (top, right, bottom, left). Most journal aesthetics prefer ticks inward and only the bottom and left spines visible. This matches the conventions in matplotlib’s style customization documentation:

mpl.rcParams.update({
    'axes.spines.top': False,
    'axes.spines.right': False,
    'xtick.direction': 'out',
    'ytick.direction': 'out',
    'xtick.major.size': 3,
    'xtick.minor.size': 1.5,
    'ytick.major.size': 3,
    'ytick.minor.size': 1.5,
    'axes.linewidth': 0.5,
})

Line widths of 0.5 pt for spines and 0.75–1.0 pt for data lines reproduce well at print size. Heavier lines (the matplotlib default is 1.0–1.5 pt for spines and 1.5 pt for lines) look muddy when scaled down.

Error Bars and Markers

Error bars in publication figures need three settings most defaults get wrong: cap width, line width, and marker overlap. Use capsize=2 (the default capsize of 0 hides the cap entirely; 5 is too large at print size), elinewidth=0.5 (matches the spine weight), and markersize=4 for scatter points or markersize=3 if you have many overlapping points.

Which error bar to plot is a separate decision — SEM and SD show different things, and using the wrong one is a common reviewer note. SEM vs SD vs 95% CI covers when each is appropriate; pick the right type before you tune the rendering.

Reproducible Style: Save the rcParams

The rcParams approach above belongs in a single style file you reuse across every figure in a paper. Create journal.mplstyle in your project root and load it with plt.style.use('./journal.mplstyle'). Submitting figures with consistent typography across the paper is the single highest-leverage thing you can do for the reviewer experience.

For seaborn users, set the matplotlib rcParams first, then call sns.set_theme(rc=mpl.rcParams) to inherit them. Calling sns.set_theme() alone overrides your settings with seaborn’s defaults — check the seaborn set_theme documentation for parameter precedence.

Where Matplotlib Stops Being Worth the Effort

For multi-panel figures with non-trivial layouts, matplotlib’s gridspec works but is verbose. For statistical figures with annotations (significance bars, sample sizes per group, post-hoc letters), the manual annotation code dominates the figure-generation time. At that point, exporting from a stats environment that has a journal preset built in — or using a tool that auto-formats based on a target journal — is faster than tuning matplotlib by hand. The principles here still apply: explicit physical size, pinned fonts, vector output, colorblind-safe palettes. The implementation is just a different layer.

Ready to analyze your data?

Join the beta waitlist and be the first to try GraphHelix.