remybroun
infographic
A Claude Code skill that turns a document or a topic into a designed visual explainer. 52 block forms, an enforced word budget, and guards that fail the build.
- Stars
- 3
- Language
- Python
- Created
- Aug 13, 2026
- Updated
- Aug 14, 2026
Introduction
infographic
A Claude Code skill that turns a document, a dataset or a topic into a designed visual explainer: a print-ready PDF, or a continuous scrolling page whose HTML is the deliverable.
It is not a chart library. It is a set of constraints that make a model produce a graphic document instead of an essay with figures stapled to it, and most of those constraints fail the build rather than advise.
/infographic <a topic, a path, or a pasted document>
What it produces

The word budget in scripts/lib/density.py runs before anything renders, and a
breach is a build error. That number is not arbitrary: version 1 of this skill
shipped an eight-page explainer carrying 2,086 words and one chart, and
every paragraph in it was individually defensible. The budget exists because
taste under time pressure always chooses "one more clarifying sentence".
The vocabulary it draws with

52 block types across six families, plus 49 aliases so a spec can be written in
ordinary words (pie → donut, waffle → unit, 2x2 → quadrant,
flow → process).
That figure is a map. Below it are the blocks themselves, rendered: three sheets
out of nine in the specimen gallery, which draws all 36 forms
this repository has honest data for. Every number in it is read out of the repo
at build time, from the registry, the word budget, the five shipped fixtures,
the linter's own checks and git log, so it cannot drift from the code it
documents and nothing in it is invented to complete a shape.


When the catalog has no shape for an idea, you draw it. A figure block takes
authored SVG and keeps every guarantee the built-in blocks make: required alt,
a required data twin, refused colour literals, and its labels charged against the
budget. Capped at three per document, because without a cap "draw the shape the
catalog lacks" becomes "hand-draw everything" and the consistency is gone.

The top-right panel there is a figure. Three spines over one set of facts is
not a tree (they partition nothing), not a process (they are alternatives, not
steps) and not a quadrant (there are no axes). Naming the closest block type
needs a "well, sort of", which is the test for authoring the shape instead.
How a document gets made
Steps 1–6 are judgement and cannot be automated. 7–11 mostly are.
- Source: read it, or
ig.py extract source.pdf - Reader and claim: who reads this, and which words are they missing?
- Three spines: three arguments over the same facts, then choose one
- Target: paper, poster, slide, or a continuous scrolling page
- Scenes: which images does this live or die by, before opening the catalog
- Forms: one per remaining claim
- Spec:
ig.py new out/spec.json - Theme: validated, never hand-picked
- Render:
ig.py render out/spec.json --out-dir out - Look at it:
ig.py shoot out/doc.html. The linter never has. - Hand off: the spec, the document, and how it was made
Two orderings are load-bearing. Three spines before one is chosen, because the first argument to arrive is nearly always the mechanism. That is the shape the source is already in, and it is rarely the one the reader has a stake in. And scenes named before the catalog is opened, because once it is open the question silently changes from what does this look like? to which of the 52 shapes is closest?
What the build refuses to render

Three failures stop the build outright: a breach of the word budget, more than three hand-drawn figures, and a colour literal inside a drawing. The rest warn.
Every one of these guards is a specific document that shipped and should not have:
| Guard | The failure that produced it |
|---|---|
| Word budget, enforced in code | Version 1 shipped 2,086 words across eight pages carrying one chart |
| At most three hand-drawn figures | Version 2 fixed the word count and hand-drew everything, losing all consistency |
| Colour literals refused in a drawing | Hand-drawn figures are where computed colour slips first |
| Authored tables count toward the budget | A section retyped as three tables passed as clean; the cells were exempt |
| Identifiers counted, definitions required | A page carried 30 identifiers and no definitions block, and passed |
| Graphic forms compared against the last version | A regeneration came back 93% identical, with every step performed honestly |
A kpi row is measured against the document | Four numbers led a page, and all four were explained better further down |
invert without bleed | It flips the ink but paints no ground, so a drawing renders light-on-light |
The pattern is worth stating plainly, because it recurs:
When the output is wrong, look for the incentive that made the wrong thing cheapest, not for the missing rule.
The linter rewarded prose, so it got prose. The catalog framed every idea, so
ideas came out catalog-shaped. Table cells were exempt from the budget, so
paragraphs became tables. A six-word label cap makes skipped_bucket cheaper
than "the recipient switched that group off", so pages came out labelled in
identifiers only their author could read.
Themes
default (neutral editorial) · rentos (olive editorial, Instrument Serif) ·
mono (greyscale, print-safe).
Every image on this page is rentos, so what you are looking at is a theme
doing its whole job: the olive lead, terracotta as the warm secondary, blue and
plum as separable companions, and Instrument Serif on the block titles over
Inter everywhere else. The serif is opt-in per theme (type.block_title), since
a theme whose display face is its sans gains nothing from it.
All three pass the computable colour checks: contrast, categorical separation,
and colour-vision-deficiency distance. A new brand theme is a JSON file, not
code, and its slot order is found by enumerating orderings and keeping the ones
that clear the gates, never by picking what looks nice. Brand olive and
terracotta are deliberately not adjacent slots: that pair measures a CVD ΔE of
4.5, so a protanope cannot separate them. The scorecard in the sheet above counts
the checks per theme; mono runs fewer of them because greyscale has fewer
categorical slots to separate, not because it scores worse.
python3 scripts/ig.py validate --all
python3 scripts/ig.py catalog --sheet out/sheet.pdf --theme mono
Layout
SKILL.md what Claude loads first
references/ one file per decision; load the one that owns it
pipeline.md the eleven steps
graphic-first.md the word budget, and why it is code
scenes.md deciding what gets drawn by hand
anti-patterns.md check every document against this before shipping
catalog/ the 52 block types, by family
scripts/
ig.py the CLI
build.py spec → HTML
check_document.py the linter
lib/density.py the word budget
lib/derivation.py did a regeneration change anything
lib/leading_numbers.py is that stat row carrying its weight
fixtures/specs/ five complete, rendering worked examples
GALLERY.md every form the skill draws, drawn
assets/
gen_readme.py the three slides on this page
gen_gallery.py the nine specimen sheets, from this repo's own data
build_gallery.sh rebuilds every image here in one command
measure_blocks.py how tall does each block lay out, so rows can be paired
trim_png.py crops the blank tail off a rasterised page
python3 scripts/ig.py selftest # 276 assertions
python3 scripts/ig.py selftest --render # also builds all five fixtures
Requirements
Python 3.9+ standard library, and a Chromium-family browser for rendering and
screenshots (set CHROME_PATH if it is not found). poppler is optional for
building documents, where it reads PDF sources and measures per-page ink
coverage, and required for rebuilding the images on this page, which rasterise
through pdftoppm. No pip installs, no npm, no matplotlib.
Notes on this page
Every image here was produced by the skill. sh assets/build_gallery.sh rebuilds
all twelve from source in one command.
The three wide ones are 16:9 slides from gen_readme.py,
built as a paginated target because a README image is a fixed frame and a
paginated page fills it rather than leaving the dead column a scrolling layout
leaves. The specimen sheets are A4 portrait, and that is deliberate too: a text
label is a fixed size in millimetres, so the page width is what decides how
large it lands in GitHub's column. A4 portrait puts an 8pt label at roughly 12px;
the 338mm slide puts the same label at roughly 7px.
The sheets set meta.spacing: "tight", which scales the gutter, the row gap and
the padding inside a framed block together. Scaling only the gap does not read as
tighter: two framed charts are held apart by pad + gap + pad, and at the default
that is 62px of which the gap is 26. tight is not the default, because in a
document that argues, the gap is what tells a reader one idea has finished.
The eleven pipeline steps and the guard table are not images. An ordered list and a table render natively, are searchable and copyable, and follow your theme. An image earns its place when the idea is spatial.
The three slides are the only place in this repo where tables: false is set.
The accessibility twin is a <details> element, and inside a raster image that
is a control nobody can operate, so the values it would carry sit in the prose
beside each figure instead. The specimen sheets keep their twins in the HTML.
Two things are still wrong, and neither is faked:
- The images are light, so they glare in dark mode. Fixing that properly needs a validated dark theme, which does not exist yet.
- The gallery build reports one warning.
near-empty-pagefires on the alias sheet at 8% ink. It is measuring correctly and measuring the wrong thing: that sheet is 94% full by area and 8% full by ink, because a grid of outlined chips is mostly the page showing through. The check is tuned for documents rather than specimen sheets, and it is reported rather than suppressed. - The
rentostheme's fonts live outside this repo. Itsfonts_dirpoints at a sibling brand directory, so a fresh clone renders it in Georgia and Helvetica instead of Instrument Serif and Inter. The build now warns when a declared font file is missing rather than falling back in silence, but the assets are still not vendored here.
The one thing the tooling cannot do
The linter checks structure. It has never once looked at a document, and it cannot tell you that a label collided, that an arrow points at nothing, that the drawing was the wrong drawing, or that the argument does not land.
ig.py shoot exists so that you can.