Scientific & Technical Figures
Overview
For new projects, build figures on xy.pyplot (import xy.pyplot as plt). XY is a near-drop-in for the matplotlib object-oriented API, scales to billions of points for the same code, and exports to SVG / PDF / PNG / HTML out of the box. If the project already uses matplotlib directly, keep that — the two imports are interchangeable for any code that doesn't depend on matplotlib-only extension APIs. Use the helper scripts in scripts/ for journal sizing, palette selection, format-correct export, and accessibility checks. Treat every other library (seaborn, plotly, bokeh, altair) as a special-case — they all cost you something in either typographic control, vector fidelity, or reproducibility.
When to use this skill
- Creating a figure for a paper, thesis, technical report, or grant.
- Re-styling a chart to meet a specific journal's spec (size, DPI, font).
- Picking a colorblind-safe palette or verifying one.
- Exporting one source figure to multiple formats (SVG first, PDF for journal, PNG only when the consumer demands raster).
- Building a multi-panel figure with consistent typography across panels.
- Migrating from pure-matplotlib to xy (or back).
If the user just wants to eyeball a dataframe (df.plot() in a notebook), this skill is overkill — use plain matplotlib.
Workflow
Pick the figure type → see
references/figure-types.mdfor the canonical recipe per type (line, scatter, bar, heatmap, contour, errorbar, multi-panel, 3D).Pick a sizing target → single column / 1.5 column / double column / poster. Defaults:
figure-size --target single|one5|double|poster.Pick a color palette → see
references/color-and-accessibility.md. Validate it:colorblind-check --palette NAME.Build the figure with the matplotlib OO API:
fig, ax = plt.subplots(figsize=(3.5, 2.6), constrained_layout=True) # ...Apply a base style:
from style_presets import apply_style; apply_style("nature").Export:
uv run scripts/export_figure.py demo --out figure1 --formats svg pdf --also png --dpi 300. SVG is the preferred default; PDF is what most journals actually accept; PNG at 300 DPI is a compatibility fallback for downstream tools that cannot consume SVG.Verify →
accessibility-check --image figure.pngsimulates the three common CVD types and reports which colors collapse.
Run every script via uv run scripts/<name>.py ... — never assume matplotlib is already installed.
Hard rules (apply before doing anything else)
- Object-oriented API.
fig, ax = plt.subplots(...), neverplt.plotfor non-trivial work. Same in matplotlib and xy. - Sans-serif at print size. 7–9 pt for axis labels, 6–8 pt for ticks, ≥8 pt for panel labels (A, B, C).
- No
jet. Useviridis,plasma, orcividisfor sequential;coolwarmorRdBufor diverging. - Heatmaps: prefer
shading="gouraud". When the row count is small (e.g. 50 bark-rate bands on a log-frequency axis)shading="nearest"/"flat"draws flat rectangles and the SVG renderer leaves anti-aliasing hairlines at every boundary. Gouraud interpolation smooths across cell corners and eliminates those raster-like seams. Seereferences/figure-types.md§ "Heatmap". - No JPEG for data. PNG, TIFF, or vector (PDF/EPS/SVG).
- SVG first, PDF for journal, PNG only as compatibility fallback. SVG is the most flexible vector format (lossless, browser-renderable, designer-friendly, edit-friendly in Inkscape/Illustrator). PDF is what most journals demand; PDF is also the format that doesn't drop nicely into Word/PowerPoint, which is why SVG is the better default for figures that need to flow back into office documents. PNG is bitmap and does not scale — only emit PNG when the consumer can't ingest SVG; if you must, use 300 DPI (not the matplotlib default of 100 or 200).
- Vector for plots, raster for images. Plots → SVG → PDF. Photographs/microscopy → TIFF 300–600 DPI.
- Color = redundant encoding. Color-blind safety is mandatory; mapping hue + shape or hue + linestyle is the gold standard.
- No titles inside the figure. The caption belongs in LaTeX/Word. The figure shows data.
- Log axes: replace
10ⁿwith domain values. Default log ticks show powers of ten (10²,10³). For domain-specific ranges (audio: 20, 50, 100, 200, 500, 1k, 2k, 5k, 10k, 20k Hz; time: 1 ms, 2 ms, 5 ms, 10 ms …), set manualtick_valuesandtick_labelsso the axis reads like a professional in that field expects. Seereferences/figure-types.md§ "Logarithmic axes — tick formatting". - Cite, don't show, matplotlib defaults. Nothing screams "AI-generated" louder than
plt.title("My Plot").
Library guidance
| Library | Use when | Avoid when |
|---|---|---|
reflex-xy (xy.pyplot) |
Preferred default for new projects. Drop-in matplotlib.pyplot syntax; scales to 10⁹ points; exports SVG / PDF / PNG / HTML from the same source. |
You depend on a matplotlib-only extension API (rare). |
matplotlib (matplotlib.pyplot) |
Project already uses it; you need a matplotlib-only extension API; you're submitting to a venue with strict EPS/PDF font rules. | New project with no prior commitment — prefer xy. |
| seaborn | Quick statistical aggregates (box/violin/pair). Built on matplotlib. | Custom projection or non-statistical figure (use xy/mpl directly). |
| plotly | Interactive figures for HTML supplements / dashboards. | Static journal submission (use xy/mpl). |
| bokeh / altair | Web apps, declarative grammar fans. | Static publication submission. |
xy.pyplot and matplotlib.pyplot are interchangeable for the vast majority of plotting code. Pick xy for new work and switch the import only when you hit a method xy doesn't yet implement — see the compatibility table in references/xy.md for what's covered.
Resources
scripts/ (run via uv run scripts/<name>.py ...)
style_presets.py— apply named journal/preset styles (nature,science,plos,ieee,poster).figure_size.py— compute(width, height)in inches for any column target.color_palettes.py— colorblind-safe palettes (Okabe-Ito, Wong, Tol-bright, Tol-muted).export_figure.py— save a figure to one or more formats at journal-correct DPI; embeds fonts in PDF.colorblind_check.py— simulate protan/deutan/tritan CVD on a PNG and report collisions.accessibility_check.py— font-size, contrast, and palette checks on a finished PNG.figure_demo.py— one-shot demo that renders + exports a canonical line/scatter/bar/multi-panel example.
references/
color-and-accessibility.md— palette catalog, CVD types, contrast guidance, colormap selection.figure-types.md— recipes for line, scatter, bar, heatmap, contour, errorbar, multi-panel, 3D.journal-specs.md— Nature, Science, PLOS, IEEE, Elsevier size/DPI/font tables.xy.md— reflex-xy quick-start andxy.pyplotmatplotlib compatibility notes.mistakes.md— recurring anti-patterns (titles in figures, default styles, color-only encoding).release-checklist.md— pre-submission figure QA list.
assets/
style_nature.mplstyle,style_science.mplstyle,style_plos.mplstyle,style_ieee.mplstyle,style_poster.mplstyle— drop-in styles per target venue. Load viaapply_style("nature")(the Python API), notplt.style.use(...)directly — matplotlib's style loader silently ignoresaxes.prop_cycle, so the Python helper is what actually installs the Okabe-Ito palette.
Verification
Before declaring a figure done, run uv run scripts/accessibility_check.py <png> and confirm:
- No font < 6 pt at print size.
- No two CVD simulations collapse the same line pair to the same color.
- No axis labels truncated by figure bounds.
- File size < 10 MB (most journals cap at 10–20 MB per figure).