matplotlib-prefab

Scientific figures (single column, single panel)

Turns tabular data into an 85 mm wide, single-panel line or bar figure: a PDF at a fixed physical size, a PNG preview, a snapshot of the input, the resolved style profile and a check report. Then, where the toolchain allows, sets that figure into a real conference body page so its final printed size can be judged rather than assumed.

The style numbers are provisional. They are the project’s initial choices, not a journal or conference requirement. Never present them as a venue standard.

Scope

Handled: line charts (one shared numeric x, one or more named y series) and vertical bar charts (one numeric value per named category, negatives allowed).

Not handled — say so plainly rather than improvising: scatter, histogram, heatmap, 3D, box/violin, stacked or grouped bars, twin axes, error bars, log axes, multi-panel and two-column layouts. A bar chart here compares named categories; it does not bin raw samples, so a request for a distribution is a histogram request and is out of scope.

Workflow

1. Write the spec

Put the user’s data into one JSON file. Read references/input-schema.md for the exact fields, and read it before writing the first spec of a session — the validator rejects unknown fields, so guessing costs a round trip.

Restructuring data (a CSV column into x, a column per series) is fine. Reordering, smoothing, resampling, rounding or dropping points is not: the figure must show what the user gave you. If the data genuinely cannot be plotted — gaps, NaN, mismatched lengths — say so and ask, rather than patching it silently.

Keep the spec somewhere durable (next to the output, or in the user’s project), not in a temporary directory — it is how the figure gets regenerated later.

2. Render

python skills/scientific-figures/scripts/render.py \
    --spec <your-spec>.json \
    --output-dir <where-the-figure-should-go>

Worked examples of the input live in ../../tests/data/, and what a render produces is committed under ../../tests/output/.

Add --profile <path> to use an edited style profile. Paths are relative to wherever you run the command; the script itself works from any directory.

Exit codes: 0 the figure rendered (possibly with warnings), 1 a check failed, 2 the spec or profile was rejected and nothing was written.

If it exits 2, the message names the field and the reason. Fix the spec — do not work around the validator by changing the data.

3. Read the report

report.json and the terminal summary carry one entry per check, each pass, warn, fail or not_checked. Read the failures and warnings; do not skim to the overall status.

Common ones and what they mean:

Check If it complains
glyph_coverage The font lacks characters in the labels (e.g. CJK in DejaVu Sans). Set fonts.family/fonts.fallbacks in a profile copy to a font that covers them. Do not ship a figure with box glyphs.
font_resolution The requested font is not installed and matplotlib substituted one. Install it or change the profile.
text_within_canvas Labels are being clipped. Shorten the label, rotate bar categories via bar.category_label_rotation_deg, or widen canvas.width_mm.
tick_label_overlap Tick labels collide. Lower axes.max_xticks, rotate, shorten, or widen.
panel_area Decorations are crowding out the plot. Usually too many series or overlong labels.
line_data_integrity, bar_category_order A real bug — the drawing does not match the input. Stop and investigate; do not ship it.

4. Actually look at the PNG

visual_review always comes back not_checked, and nothing fills it in automatically. Open figure.png and judge what no script can:

If the environment cannot display images, say explicitly that the visual review did not happen. Never report it as passed.

4b. Or let the user choose the numbers themselves

There is a browser tool that shows a single-panel figure at its printed size on a calibrated page of any of the three venues, with sliders for the type sizes, weights, line widths, tick counts and bar settings. It runs this repository’s own figure_core.py in the browser through Pyodide, so the preview is not an imitation of the figure — it is the figure.

python scripts/build_and_verify_web.py --only fetch-runtime --only fonts --only assets
python web/serve.py            # then open http://127.0.0.1:8765/index.html

Point the user at it when they want to decide how the figure should look rather than have you decide. Its “Copy for an agent” button produces a complete hand-off: the resolved profile, the spec, the physical size, the versions and the exact render.py command.

When a user hands you that text, load it and use it. Do not “improve” the values they chose. Render with the profile as given, and report the profile, the size, the fonts and the environment you actually used. If something in it cannot work, say which field and why — do not quietly substitute your own number.

Three things about that preview are worth being straight about, because a user may assume otherwise:

Step 5 below is still the check for a real page. The browser tool exists so that day-to-day previewing does not need LaTeX; it does not replace the compile.

5. Check it in a real article page

A figure is read at whatever width the template gives it, not at 85 mm on white. Half of ICLR’s text block is 69.85 mm; one ICML column is 82.55 mm. An 85 mm figure dropped into the ICLR slot shrinks by 0.82, and its 9 pt labels print at 7.4 pt. report.json cannot see any of that.

python tests/render_conference_context.py --venue iclr2026 --case line-multi

That compiles the venue’s own style around a page of real prose, places the figure, and writes page.pdf, page.png and context-report.json under tests/output/conference-context/<venue>/<case>/. Read references/article-context-evaluation.md before running it or reporting what it found.

When to do this:

The default path re-renders the figure at the template’s width, so it lands at scale 1 and the type sizes are the ones you set. Placing an already-rendered figure (--figure-dir) reports the real reduction instead.

Then look at page.png with the prose beside it: are the tick labels readable next to 10 pt body text, is the legend clear of the data at this width, are the series still separable? A high-DPI preview stretched to your screen is not the printed size — the physical numbers are in the report, and page.pdf at 100 % is the real thing.

Needs LaTeX and poppler; render.py does not. python tests/render_conference_context.py --check-dependencies says whether they are installed. If they are missing, or the fixtures are not there, say the context evaluation did not run. The figure itself is still deliverable — but do not report the article context as verified when nothing compiled.

The context output goes in its own directory and never touches the five files from step 2; report.json keeps the standalone check results.

6. Fix and re-render

Fix what the report and your own look at the PNG turned up, then re-render. At most two correction rounds, then report what is still wrong. Never edit the user’s data to make a check pass, and never loop.

Style fixes go in a copy of the profile passed with --profile, or — if the user wants the change to be the new default — in references/profiles/single-column.json itself, with their agreement. Never hard-code a size or colour in a template.

Changing the figure changes the page, so re-run step 5 as well as step 2.

7. Hand over

Report where the files are, which checks warned, whether you looked at the PNG, and the reproduce command from report.json.

If step 5 ran, say which venue’s page you produced, the width it was placed at, the scale factor, and that the style values are provisional project choices — not that venue’s requirement. If step 5 did not run, say so and why.

Output

The output directory gets exactly five files. Anything else already there is left alone; nothing is deleted.

File What it is
figure.pdf The deliverable, at the profile’s physical size (85 mm wide by default)
figure.png Raster preview at output.png_dpi (300)
spec.json The validated input, exactly as drawn
profile.resolved.json The style profile that produced this figure
report.json Check results, environment (matplotlib version, resolved fonts), reproduce command

spec.json + profile.resolved.json reproduce the figure; report.json carries the command that does it.

Style

All style lives in references/profiles/single-column.json — sizes, fonts, colours, tick counts, bar width, output settings. Templates read it and hold no defaults of their own.

Most type settings have an optional per-axis override — fonts.size_x_label_pt, fonts.size_ytick_pt, fonts.weight_x_label and so on — which falls back to the shared field (size_axis_label_pt, size_tick_pt, weight) when absent. A profile written before those existed renders exactly what it always did. Ask figure_core.font_size(profile, role) / font_weight(profile, role) rather than reading a field directly; those are what the renderer uses.

Point sizes are physical: a 9 pt label is 9 pt on the page, and png_dpi only changes preview resolution. Read references/design-rules.md before changing a value, arguing for one, or explaining the visual choices — it covers the palette, the marker/dash strategy, the layout engine constraint, and exactly what the checks can and cannot prove.

Requirements

Python with matplotlib and numpy. No LaTeX installation is needed to draw a figure — maths labels use matplotlib mathtext ("$\\alpha_i$"). See references/input-schema.md for the maths and non-ASCII text rules.

The step 5 article-context check is the one exception: it needs latexmk, pdflatex, bibtex and pdftoppm (sudo apt install texlive-latex-recommended texlive-fonts-recommended texlive-latex-extra latexmk poppler-utils). Without them the figure still renders; the context check reports that it did not run.

Files

Outside the skill, web/ holds the browser style lab described in step 4b — its paper pages are generated from compiled LaTeX by scripts/calibrate_conference_pages.py and the evidence that they match is under tests/web/calibration/. tests/data/ holds prefab specs and tests/output/ holds their rendered results — a proof sheet of the current profile, regenerated by python tests/render_gallery.py. Look there when you need a worked example of the input format or want to see what the preset values actually produce.

tests/output/conference-context/ holds the same figures set into real ICLR, NeurIPS and ICML pages, regenerated by python tests/render_conference_context.py --all. That is where you see what the preset values look like at the size a reader gets them. The venue templates, the body-text excerpt and the wrappers live in tests/fixtures/ and tests/conference_context/.