reporting/figure-export/SKILL.md
Exports publication-ready figures with the correct vector/raster split, embedded editable fonts, color-space-robust palettes, and journal-correct sizing and resolution in matplotlib and ggplot2. Use when preparing figures for journal submission, exporting a dense single-cell or GWAS plot without producing an unopenable vector file, or fixing fonts and colors that break in print.
npx skillsauth add GPTomics/bioSkills bio-reporting-figure-exportInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference examples tested with: matplotlib 3.8+, numpy 1.26+, ggplot2 3.5+, ggrastr 1.0+, ragg 1.2+
Before using code patterns, verify installed versions match. If versions differ:
pip show matplotlib then help(matplotlib.figure.Figure.savefig); introspect matplotlib.rcParams if a key is renamedpackageVersion('ggplot2') then ?ggsaveIf code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
"Export this figure for the journal" -> Save the plot so each part of it survives production: vector structure stays crisp, the dense data layer stays a reasonable file size, text stays editable, and color survives the print conversion.
fig.savefig('fig.pdf', dpi=600) with publication rcParams setggsave('fig.pdf', width=89, units='mm', device=cairo_pdf)A publication figure is not one object; it is four superimposed layers, and export means giving each the representation it needs:
The expert move is the hybrid figure: rasterize only layer 2, keep layers 1 and 3 vector, embed editable fonts, and pick a colormap that survives CMYK and grayscale. Everything below serves that.
A reproducibility framing: a figure is a pure function of (data, code, theme, font availability). If any of those is unpinned, the figure is not reproducible.
The single most important export skill for single-cell (UMAP/tSNE) and GWAS (Manhattan) figures. A fully-vector million-point scatter is unopenable and gets rejected by the typesetter's RIP; rasterizing just the data layer keeps file size sane while axes and text stay crisp at any zoom.
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(3.5, 3.0)) # physical inches = the real size control
ax.scatter(x, y, s=2, rasterized=True, zorder=0) # 10^6 points -> embedded raster
ax.plot(xfit, yfit, color='black', zorder=2) # stays vector
ax.set_xlabel('UMAP1') # stays vector text
fig.savefig('fig.pdf', dpi=600) # dpi governs ONLY the rasterized layer
In a vector container, savefig(dpi=...) sets the resolution of the embedded raster patch and does nothing to the vector parts. ax.set_rasterization_zorder(0) rasterizes every artist below a z-order cleanly. In R use ggrastr::rasterise(geom_point(size=0.3), dpi=600) (wraps any geom since 0.2.0), keeping theme_* vector.
The most common typesetter complaint about matplotlib PDFs is un-editable text. matplotlib's default pdf.fonttype is 3 (Type 3), which embeds glyphs as PostScript procedures that import into Illustrator as ungrouped paths and cannot be re-selected as text. Set 42 (TrueType, wrapped) so text stays selectable and editable. Fix it once, globally:
import matplotlib as mpl
mpl.rcParams['pdf.fonttype'] = 42 # TrueType, editable text in PDF
mpl.rcParams['ps.fonttype'] = 42 # same for EPS/PS
mpl.rcParams['svg.fonttype'] = 'none' # SVG: emit real <text>, reference the font by name
Type 42/TrueType/Type 3 fonts are subsetted (only used glyphs embedded); Type 1 are not. With large glyph sets (CJK) Type 42 can bloat the PDF - a real tradeoff. svg.fonttype='none' keeps words editable in Inkscape/Illustrator but the viewer must have the font (else it substitutes); the default 'path' outlines every glyph (portable, uneditable). Default to editable text; only outline if a specific production desk asks.
In R, the base pdf() device has weak font handling and inconsistent cross-OS rendering; use cairo_pdf (embeds fonts and supports alpha). For raster, ragg::agg_png()/agg_tiff() render anti-aliased text better than cairo/base devices. Rule of thumb: showtext for vector devices, ragg for raster.
Screens are additive RGB; offset print is subtractive CMYK. matplotlib and ggplot2 author in RGB only - there is no honest path to a true CMYK figure from them. The journal's pipeline converts RGB->CMYK, and because the CMYK gamut is smaller than sRGB, saturated out-of-gamut colors shift: pure RGB blue (#0000FF) and vivid green/cyan come back muddier and darker on paper. The neon scatter that pops on screen can print gray.
What to do: pull colors slightly off full saturation (they survive conversion better); soft-proof downstream in Illustrator/Photoshop with a CMYK profile if it matters; and submit RGB - Nature, Science, Cell, and PLOS all explicitly want RGB, not CMYK, because their pipeline does the conversion and online is RGB anyway. If a legacy desk demands CMYK, convert downstream with an explicit profile and re-check that nothing shifted; do not fake it with a colorspace flag.
EPS/PostScript do not support alpha - matplotlib's PS backend renders partially-transparent artists as opaque, so an alpha-blended overplotted scatter loses its density information on EPS export. PDF and SVG support alpha natively; prefer them when transparency carries meaning. If a journal forces EPS and transparency is needed, rasterize that layer (rasterized=True) or re-encode density as hexbin/2D-KDE. savefig(transparent=True) makes the background transparent for slide overlays, not for print.
A vector PDF has no inherent resolution; it renders sharp at any zoom. DPI governs only raster formats (PNG/TIFF) and the rasterized data layer inside a vector file. The real size control is the physical figure size in inches/mm - design at the journal's exact column width from the start; rescaling a 300-dpi raster to 200% halves its effective resolution. Font sizes are in points (1 pt = 1/72 in) independent of DPI.
The DPI tiers follow the IMAGE CLASS, because print reproduces tone via halftone dots (follow the target journal's own numbers; these are the common production convention):
| Image class | DPI | Why | |-------------|-----|-----| | Halftone / grayscale / color photo | 300 | continuous tone matches typical screen rulings | | Combination (halftone + line/text) | 500-600 | thin lines and small type must not jag against toned background | | Line art (pure black/white) | 1000-1200 | hard edges alias badly at low DPI - or keep it vector and DPI is moot |
savefig.dpi is the file resolution; figure.dpi is the on-screen resolution. Independently of DPI, very thin strokes (below ~0.25 pt / 0.1 mm) can drop out or thicken unpredictably at the printer's RIP even in a vector file - keep hairlines at or above the journal's minimum line weight.
| Format | Type | Use for | Avoid for | |--------|------|---------|-----------| | PDF | vector(+raster) | default for most journals; hybrid figures; alpha works | - | | EPS | vector(+raster) | legacy journal requirement | anything with alpha (flattened opaque) | | SVG | vector | web; handoff to Illustrator/Inkscape for editing | final print at some desks (support varies) | | TIFF (LZW) | raster, lossless | print production when a journal demands raster (Cell, PLOS) | large vector-friendly line figures | | PNG | raster, lossless | online, previews, slides, README figures | print where vector is accepted | | JPEG | raster, LOSSY | photographs only | any figure with text/lines/edges (DCT ringing) |
Never JPEG for line/text figures - block compression rings along high-contrast edges (gray halos on text, fringing on thin lines). For TIFF, use LZW (near-universal reader support) for 8-bit figures; use ZIP for 16-bit (LZW can inflate 16-bit files).
Perceptually-uniform maps (viridis, cividis, magma, inferno, plasma) are constructed in CAM02-UCS so equal data steps map to equal perceived steps with monotonically increasing lightness - which is exactly why they survive grayscale and avoid inventing false gradients. cividis is additionally optimized so viewers with and without red-green color-vision deficiency see nearly the same image. By contrast, jet/rainbow has non-monotonic luminance that invents bright/dark bands the data does not have (false edges at yellow/cyan) and collapses to mush in grayscale - a correctness failure, not an aesthetic one.
CreationDate into every PDF, so two identical runs differ byte-for-byte (noisy git diffs). Pass metadata={'CreationDate': None} to savefig, or set the SOURCE_DATE_EPOCH env var, for deterministic output.bbox_inches='tight' breaks exact widths. It recomputes the bounding box from drawn content, so output dimensions depend on tick-label lengths and the renderer's font metrics - the same script on two machines (different fonts) yields different-sized PDFs, and it can clip annotations. For camera-ready figures at an exact 89 mm, design to size with constrained_layout=True and save without bbox_inches='tight'; if cropping is unavoidable, pair it with explicit pad_inches.matplotlib.use('Agg') before importing pyplot on a cluster/CI box, or just use the file backends, so no display is required.Specs change and vary by sub-journal; re-pull the target's author-guideline page. Snapshot, June 2026:
| Journal | Widths | Min DPI | Formats | Color | |---------|--------|---------|---------|-------| | Nature | 89 mm single / 183 mm double; <=170 mm tall | 300 photo, 600+ line | vector AI/EPS/PDF preferred; TIFF raster | RGB | | Science | 5.7 / 12.1 / 18.4 cm | >=300 at final size; vector preferred | Illustrator-openable vector; no PowerPoint | RGB (not CMYK) | | Cell Press | 85 / 114 / 174 mm | 300 color, 500 grayscale, 1000 line | TIFF (LZW) or vector | RGB | | PLOS | 789-2250 px wide; <=2625 px tall | 300-600 (do not exceed 600) | TIFF or EPS only; flattened, LZW, no alpha/layers | RGB or grayscale, 8-bit; no CMYK |
PLOS is strictest (8-bit RGB/grayscale TIFF, no alpha channel, no layers). Cell's grayscale (500) and line (1000) tiers exceed the generic numbers - follow the journal's own.
| Symptom | Cause | Fix |
|---------|-------|-----|
| Typesetter: "supply editable text" | default Type 3 fonts | pdf.fonttype=42, ps.fonttype=42, svg.fonttype='none' |
| PDF won't open / hundreds of MB | fully-vector dense scatter | rasterize the data layer (rasterized=True / ggrastr::rasterise) |
| Colors muddy in print | saturated RGB out of CMYK gamut | desaturate slightly; soft-proof; submit RGB |
| Transparency gone on EPS | EPS has no alpha | rasterize that layer, or use PDF/SVG, or hexbin |
| Figure not exactly 89 mm | bbox_inches='tight' non-deterministic size | design to size + constrained_layout, drop tight bbox |
| Heatmap shows false bands | jet/rainbow non-monotonic luminance | viridis/cividis (sequential), diverging map for signed data |
| Noisy git diff on identical figure | PDF CreationDate timestamp | metadata={'CreationDate': None} or SOURCE_DATE_EPOCH |
tools
End-to-end CLIP-seq pipeline from FASTQ to ENCODE-compliant binding sites, single-nucleotide crosslink maps, annotation, motifs, and (optionally) differential binding. Use when running the full Yeo lab eCLIP / iCLIP / iCLIP2 / iCLIP3 / irCLIP / PAR-CLIP analysis with SMInput control, protocol-specific UMI extraction, ENCODE STAR parameters, CLIPper or Skipper peak calling with stringent log2 FC and -log10 p thresholds, IDR rescue and self-consistency QC, and downstream motif registration with mCross or PEKA.
development
Detect, date, and contextualize whole-genome duplication (WGD / paleopolyploidy) events using wgd v2 (Chen et al 2024), KsRates (Sensalari 2022 substitution-rate-corrected Ks dating), DupGen_finder (Qiao 2019), MAPS (Li 2018 phylogenomic), POInT (Conant 2008 ordered-block), SLEDGe (2024 ML-based), Whale.jl (Bayesian DL+WGD), and synteny-anchored paranome construction. Use when identifying ancient polyploidy from Ks distributions and synteny block analysis, positioning WGD events relative to speciation, distinguishing tandem from segmental from WGD duplications, dating the 2R/3R vertebrate / fish / salmonid WGDs, building paranome and Ks-age mixture models, applying KsRates substitution-rate correction across lineages, or testing alternative biased-fractionation / dosage-balance models post-WGD.
tools
Build whole-genome alignments using Progressive Cactus (Armstrong 2020 reference-free clade-level WGA), Minigraph-Cactus (Hickey 2024 pangenome-aware), LASTZ chain/net (UCSC pipeline), MUMmer4 (Marçais 2018 pairwise), minimap2 -x asm5/10/20 (Li 2018 fast pairwise), AnchorWave (Song 2022 WGD-aware), and Mauve / progressiveMauve (bacterial). Operates the HAL toolkit (Hickey 2013) for downstream extraction including halSynteny, halLiftover, halBranchMutations, and hal2maf. Use when constructing multi-species alignments for comparative-annotation projection (TOGA), synteny detection, conservation analyses (phyloP / PhastCons), or pangenome graph construction; selecting between reference-free (Cactus) and reference-anchored (LASTZ chains/nets) approaches; tuning sensitivity for closely vs distantly related genomes; or producing HAL files for genome-wide downstream tools.
development
Detect syntenic blocks and structural rearrangements between genomes using MCScanX (Wang 2012), JCVI/MCScan (Tang 2008 Python), GENESPACE (Lovell 2022) for orthology-anchored riparian visualization, SyRI for structural variation, AnchorWave for sequence-level synteny, i-ADHoRe 3.0 for highly diverged species, SynNet for synteny networks, and ntSynt for multi-genome macrosynteny. Use when identifying collinear gene blocks across species, distinguishing macrosynteny from microsynteny, detecting inversions/translocations/duplications, anchoring orthology in WGD lineages, producing publication riparian plots, computing synteny block age via Ks (cross-references whole-genome-duplication), or running synteny-aware ortholog inference in polyploids.