matplotlib-prefab

Design rules and where the numbers come from

Read this when you need to change a style value, explain one, or judge whether a rendered figure is acceptable.

The values are provisional

references/profiles/single-column.json carries "status": "provisional". Every number in it is an initial project choice made to get a working pipeline, not a journal or conference requirement. Do not describe them to anyone as a publication standard, and do not cite them as one. The repository owner is expected to review and finalise them by hand.

One source of truth

The profile is the only place style numbers live. Templates (assets/templates/*.py) decide which artists to draw; every size, colour, dash pattern, tick count and export setting is read from the profile at draw time. There are no fallback defaults hidden in the templates and no rcParams edits outside figure_core.style_context.

The practical consequence, which the test suite enforces: edit a number in the profile, re-render, and both the line and the bar figure use the new value.

To see a change rather than reason about it, run python tests/render_gallery.py and open tests/output/index.md. It renders every prefab spec in tests/data/ with the current profile and prints the values in force next to the pictures, so the sizes, palette, marker shapes and dash fallback can be judged by eye. The committed sheet is checked against the profile, so it can never quietly go stale.

Sizes are physical

A matplotlib point is 1/72 inch of the final figure. Because the canvas is created at a fixed size in inches and saved without bbox_inches="tight", a 9 pt axis label is 9 pt on the printed page.

This is also why bbox_inches="tight" is avoided: it re-crops the canvas, so the saved file would no longer be the size the profile promises. If you ever add cropping, you must prove the final size still matches the profile.

Type

Role Provisional value
Axis labels 9 pt
Tick labels, legend 8 pt
Title (optional) 10 pt

Each of those has an optional per-axis override, so the two axes need not share a size or a weight:

Shared field Overrides that refine it
fonts.size_axis_label_pt fonts.size_x_label_pt, fonts.size_y_label_pt
fonts.size_tick_pt fonts.size_xtick_pt, fonts.size_ytick_pt
fonts.weight fonts.weight_x_label, fonts.weight_y_label, fonts.weight_xtick, fonts.weight_ytick, fonts.weight_legend, fonts.weight_title

An override that is absent or null falls back to the shared field, so a profile written before these existed renders exactly what it did before — that is a test, not an intention. Resolution is figure_core.font_size(profile, role) and font_weight(profile, role) for the roles x_label, y_label, xtick, ytick, legend, title; resolved_type(profile) returns all of them at once and is what goes into the report and the browser tool’s hand-off.

Weights are validated against the set matplotlib actually applies. An unrecognised weight is rejected rather than silently ignored: a style control that appears to do something and does not is worse than one that refuses.

Tick-label weight cannot be set through rcParams, so it is applied to the label artists after the template has drawn and re-applied on every draw. The font_sizes check verifies the weight as well as the size on every text artist, which is what turns “the field is in the profile” into “the drawing took it”.

Family is DejaVu Sans at normal weight — chosen because it ships with matplotlib, so a fresh checkout renders identically without font installation. Ordinary text is upright; italics are reserved for maths variables, which mathtext handles. mathtext.fontset is dejavusans so maths matches the body text.

No LaTeX installation is required. pdf.fonttype is 42, so text in the PDF stays selectable, searchable and editable rather than being converted to outlines.

If a requested font is missing, matplotlib silently substitutes one and missing characters export as empty boxes. The report refuses to hide that: font_resolution warns on substitution, and glyph_coverage fails when a character is absent from the resolved font.

Lines, markers and colour

Role Provisional value
Data lines 1.2 pt
Axes spines and tick marks 0.6 pt
Markers 4 pt

Colour is the primary way to tell series apart, using the Paul Tol bright qualitative palette (#4477AA, #EE6677, #228833, #CCBB44, #66CCEE, #AA3377), chosen for reasonable separation for common colour-vision deficiencies. Marker shape is bound to the same series slot as colour, so shape reinforces colour rather than encoding something else — the figure stays readable in greyscale and for readers who cannot separate the hues.

Dash pattern is the overflow channel. Series 1..6 are solid in distinct colours; series 7 reuses colour 1 with a dashed line, and so on. Past len(colours) × len(dash_patterns) combinations the render is an error, not a silent recycle.

Markers are thinned with markevery so at most lines.max_markers_per_series (12) appear per series. Every data point is still plotted — the check line_data_integrity verifies the drawn point count equals the input count — the markers just stop turning a dense curve into a solid band.

A single-series bar chart uses one colour for every bar. Giving each category its own colour would encode a grouping that is not in the data.

Axes and layout

Bar charts get a fixed x limit of ±0.5 beyond the outermost bar so the first and last bars are not glued to the spines, and a y range that always contains zero: padding is added only on the non-zero side, so all-positive bars sit exactly on the baseline.

Legends

legend.show_for_single_series is false: a one-series line chart names its data in the y label and the caption, so a legend would only take space. Two or more series always get a legend — the legend_present check fails otherwise.

Placement is "best", which minimises overlap with the data but does not guarantee it. No automatic check can prove a legend is clear of the data, which is one of the reasons the visual review is a required, human/agent step.

What the checks can and cannot tell you

Exact and trustworthy:

Heuristic — reported as warnings, never as proof:

Not attempted at all:

visual_review is therefore always not_checked. Nothing in the pipeline fills it in. Only a person or an agent that actually opens figure.png can close it, and if that is impossible in the environment, the honest report is that the visual review did not happen.

Deliberately out of scope in this version

Two-column and multi-panel figures, scatter, histogram, heatmap and 3D charts, stacked or grouped bars, twin axes, error bars, log axes, venue-specific profiles, CSV input inference, interactive output. Each of those arrives as a new profile or a new template, not as options bolted onto these two.

Background reading

These informed the approach; none is a dependency.