$npx -y skills add neuromechanist/research-skills --skill scientific-figureThis skill should be used when the user asks to "create a figure", "make a scientific figure", "create a paper figure", "make a figure for my paper", "make a figure for my manuscript", "compose figure panels", "assemble figure panels", "combine figure panels", "make a multi-panel
| 1 | # Scientific Figure |
| 2 | |
| 3 | Compose publication-quality multi-panel figures at exact journal dimensions (mm/pt), validate font sizes against journal minima before export, and write PDF/PNG that the journal will accept without resize. |
| 4 | |
| 5 | ## Why this skill exists |
| 6 | |
| 7 | The previous react-pdf workflow had two structural failures: |
| 8 | |
| 9 | 1. **Composition did not respect physical size.** Flexbox layout shifted dimensions in subtle ways during render. |
| 10 | 2. **Fonts shrank below readable thresholds.** When content overflowed, react-pdf uniformly scaled the entire figure, sometimes pushing axis labels under 4 pt. |
| 11 | |
| 12 | This skill replaces that workflow. Panels are placed at exact mm coordinates, text is preserved as SVG `<text>` elements so font sizes are inspectable before export, and the validator rescales individual panels rather than the whole figure when a font minimum is violated. |
| 13 | |
| 14 | ## Pipeline |
| 15 | |
| 16 | ``` |
| 17 | 1. Plan 2. Build elements 3. Compose 4. Validate fonts 5. Export |
| 18 | (journal (matplotlib/seaborn (svgutils, at (per-element pt vs (Inkscape if |
| 19 | size, panel per panel; SVG out; exact mm/pt; journal minimum; present; |
| 20 | grid) optional icons) text preserved) rescale panel if cairosvg |
| 21 | below minimum) otherwise) |
| 22 | ``` |
| 23 | |
| 24 | Every step uses on-the-fly execution via `uv run --with` so no permanent installs are required for the skill itself. |
| 25 | |
| 26 | ## Step 1: Plan the figure |
| 27 | |
| 28 | Before generating anything, fix the following: |
| 29 | |
| 30 | - **Target journal** — sets the canvas width. See `references/journal-specs.md` for the full table; the four most common are summarized below. |
| 31 | - **Panel grid** — 1x1, 1x2, 2x2, wide-top + sub-panels, or freeform mm coordinates. |
| 32 | - **Color palette** — pick from `references/color-palettes.md` and reuse across all panels. |
| 33 | |
| 34 | ### Journal width / font minimum cheat sheet |
| 35 | |
| 36 | | Journal | 1 column | 2 column | Min body font | Notes | |
| 37 | |---|---|---|---|---| |
| 38 | | Nature | 89 mm | 183 mm | 5 pt | 8 pt for panel labels | |
| 39 | | Science | 55 mm | 120 mm | 6 pt | Myriad/Helvetica preferred | |
| 40 | | Cell | 85 or 112 mm | 174 mm | 6 pt | Max 225 mm tall | |
| 41 | | PNAS | 87 mm | 180 mm | 6 pt | No label below 2 mm tall | |
| 42 | |
| 43 | The validator in step 4 enforces these. Pick the journal early so the validator can warn early. |
| 44 | |
| 45 | ## Step 2: Build the elements |
| 46 | |
| 47 | Each panel is generated independently as an SVG. Common sources: |
| 48 | |
| 49 | - **Plots:** matplotlib/seaborn, saved with `bbox_inches='tight', transparent=True, format='svg'`. See the `[[plot-styling]]` skill for the library decision tree and SciencePlots recipes. |
| 50 | - **Icons:** transparent PNGs from the `[[transparent-icons]]` skill. |
| 51 | - **Schematics:** for new Python-driven work use `[[svg-primitives]]` (auto-fit boxes, validated arrows, layered z-order); for hand-authored SVG patterns see `[[svg-figure]]`. |
| 52 | |
| 53 | Save each element to a working directory (typically `panels/`), then compose them in step 3. |
| 54 | |
| 55 | ## Step 3: Compose the figure |
| 56 | |
| 57 | The composer is built around [svgutils](https://svgutils.readthedocs.io/) (MIT license; `uv run --with svgutils`). It places panels at exact mm coordinates and preserves text as inspectable SVG `<text>` elements. |
| 58 | |
| 59 | Two ways to compose: the `Figure` helper in `scripts/compose.py` (most cases) and direct `svgutils.compose` for full control. |
| 60 | |
| 61 | ### Recipe A: helper (recommended) |
| 62 | |
| 63 | ```bash |
| 64 | uv run --with svgutils --with lxml python scripts/compose.py panels-config.json -o figure.svg |
| 65 | ``` |
| 66 | |
| 67 | `panels-config.json` schema: |
| 68 | |
| 69 | ```json |
| 70 | { |
| 71 | "width_mm": 183, |
| 72 | "height_mm": 120, |
| 73 | "journal": "nature", |
| 74 | "panels": [ |
| 75 | {"id": "A", "src": "panels/spectrum.svg", "x_mm": 0, "y_mm": 0, "scale": 0.5, "label": "A"}, |
| 76 | {"id": "B", "src": "panels/topomap.svg", "x_mm": 92, "y_mm": 0, "scale": 0.5, "label": "B"}, |
| 77 | {"id": "C", "src": "panels/timecourse.svg", "x_mm": 0, "y_mm": 60, "scale": 1.0, "label": "C", "width_mm": 183} |
| 78 | ] |
| 79 | } |
| 80 | ``` |
| 81 | |
| 82 | Panel labels (A, B, C) are placed at top-left of each panel in 12 pt bold sans-serif. |
| 83 | |
| 84 | ### Recipe B: direct svgutils (full control) |
| 85 | |
| 86 | ```python |
| 87 | from svgutils.compose import Figure, SVG, Panel, Text |
| 88 | |
| 89 | fig = Figure( |
| 90 | "183mm", "120mm", |
| 91 | Panel( |
| 92 | SVG(" |