paperchart
A six-primitive chart toolkit for rendering quiet, print-weight charts to PNG.
The surface is a shell CLI: feed it a JSON file, get a 2×-DPR figure.
Trigger on any of: "make a chart for this blog post", "render a before / after
benchmark as an image", "I need a figure for my paper / release notes /
announcement", "design a minimal chart in the style of Anthropic or OpenAI
research posts", "draw a binary layout / critical path / latency comparison /
recall parity chart".
Do not use this skill when the user asks for an interactive dashboard, a
real-time graph, or a chart with brand colours other than the paperchart
palette. This skill is opinionated on purpose.
The CLI
paperchart <type> -i data.json -o out.png [--width 1600] [--dpr 2]
paperchart <type> --defaults -o out.png # render the built-in sample
Types: table, latency, bytes, stacked-bar, grouped-bar, funnel, ranking, dumbbell, slope, line, area, small-multiples, timeline, scatter, heatmap, calendar-heatmap, histogram, box-plot, cdf, radar, treemap, sankey, waterfall, critical-path, recall, pack-layout, delivery, flowchart, architecture, sequence, state-diagram, er-diagram.
Themes: paper (warm off-white, rust), ink (near-white, black), slate (cool grey, deep blue), forest (off-white, forest green), mono (pure monochrome), dusk (dark charcoal, warm orange). Pass --theme <name> on the CLI or put "theme": "<name>" in the JSON envelope.
Invoke it with npx github:shuakami/paperchart <type> -i data.json -o out.png from any directory. On first run npm clones the repo, runs prepare to build the Vite bundle, and playwright-chromium's postinstall downloads headless Chromium once (~200 MB). Subsequent runs are near-instant.
If the skill has already been installed via npx skills add shuakami/paperchart, the full repo is available under .claude/skills/paperchart/ (or .agents/skills/paperchart/ on non-Claude agents). You can also just cd there and node bin/paperchart.mjs <type> -i data.json -o out.png.
Authoring recipe
When the user asks for a chart:
- Pick the primitive whose shape fits the data. One primitive per figure. If none fit, decline and explain why — do not invent a new chart kind inline.
- Write the JSON file with the shape documented below for that primitive. Keep captions short — 6 to 12 words. Use the accent colour on exactly one row / segment / panel. Leave the rest neutral.
- Run the CLI. Show the user the output path. If they want to iterate, edit the JSON and re-run — the file is the single source of truth.
- Never embed chart titles inside the chart. Titles belong in the blog prose next to the image.
The input envelope
Every chart accepts the same shape:
{
"theme": "ink",
"layout": { "width": 1800, "fontScale": 1.05, "xAxisCaption": "..." },
"style": { "accent": "#1f6feb" },
"data": <chart-specific payload>
}
theme, layout, and style are optional. If you pass just data (or wrap your data as a bare array), the chart uses good defaults.
Primitive schemas
Short form. Full schemas are in the project README and in the per-type skills/<type>/SKILL.md files.
table — { columns: [{ key, label, align?, unit?, group? }], rows: [{ label, caption?, highlight?, values: { [key]: number | string } }] }. Model / config / product comparison. Exactly one highlight: true row.
latency — Row[]. Each row: { group, caption, color, bars: [{ level, ms }] }.
bytes — Row[]. Each row: { group, caption, accent, segments: [{ kb, fill, tag }], firstLoadKB, deferredKB }.
stacked-bar — Row[] where each row is { label, caption?, segments: [{ key, value, color? }] }. Composition per row.
grouped-bar — { series: [{ key, label, color? }], groups: [{ label, caption?, values: { [key]: number } }] }.
ranking — Row[]. Each row: { label, caption?, value, accent? }. Sorted leaderboard, one accented row.
dumbbell — Row[]. Each row: { label, before, after }.
slope — { startLabel, endLabel, unit?, series: [{ label, start, end, accent? }] }.
line — { xLabels, series: [{ label, values, accent? }] }.
area — { xLabels, series: [{ label, values }] } (stacked).
small-multiples — { xLabels?, unit?, panels: [{ label, caption?, values, accent? }] }.
scatter — Point[] with optional regression line.
heatmap — { rowLabels, colLabels, values: number[][] }.
histogram — { bins: [{ x0, x1, count }] }.
cdf — { points: [{ value, cumulative }] }.
waterfall — { steps: [{ label, delta, subtotal? }] }.
timeline — Row[] where each row is { label, caption?, spans: [{ start, end, label, accent? }], milestones?: [{ at, label }] }.
funnel — Stage[]. Each stage: { label, caption?, count, accent? }. Drop-off auto-computed between stages.
sankey — { sources: Node[], targets: Node[], flows: [{ from, to, value }] }. Two-column flow diagram.
treemap — Item[] where each item is { label, value, caption?, accent?, children?: [{ label, value }] }. Two-level squarified.
radar — { axes: [{ key, label, caption?, max? }], series: [{ label, caption?, accent?, values: { [axisKey]: number } }] }.
box-plot — Row[]. Each row: { label, caption?, min, q1, median, q3, max, outliers?, accent? }.
calendar-heatmap — { start?: "YYYY-MM-DD", days: [{ date, value }] } or just [{ date, value }].
critical-path — Row[]. Each row: { label, detail, startMs, endMs, kb, critical: boolean }.
recall — Query[]. Each query: { query, hits, sets: "equal" }.
pack-layout — Segment[]. Each segment: { label, detail, bytes, accent: boolean }.
delivery — { header, subheader, panels: PanelSpec[] } with up to 3 panels.
flowchart — { direction?: "TD" | "LR", nodes: [{ id, label, caption?, shape?: "rect" | "round" | "stadium" | "diamond" | "cylinder", accent? }], edges: [{ from, to, label?, dashed?, accent? }] }. Layered DAG, back-edges detected and drawn as loop-backs.
architecture — { groups: [{ id, label, caption?, services: [{ id, label, caption?, accent? }] }], connections: [{ from, to, label?, dashed?, accent?, bidirectional? }] }. Groups stack left-to-right; services stack top-to-bottom inside each group.
sequence — { actors: [{ id, label, caption? }], steps: [{ kind: "msg", from, to, label?, reply?, async?, accent? } | { kind: "note", over: id | [id, id], text }] }. Time flows downward.
state-diagram — { direction?: "TD" | "LR", states: [{ id, label, caption?, start?, end?, accent? }], transitions: [{ from, to, label?, dashed?, accent? }] }. Start / end pseudostates render as • / ⦾.
er-diagram — { entities: [{ id, label, caption?, accent?, fields: [{ name, type?, pk?, fk?, note? }] }], relationships: [{ from, to, fromCard, toCard, label?, accent? }] } where cardinality codes are "1", "0..1", "M", "1..M", "0..M".
The palette — five values only
| role |
hex |
| page / card background |
#F6F1EA |
| accent (one element per chart) |
#C75F3C |
| secondary neutral |
#D6B99B |
| ink |
#2B2A27 |
| rule / hairline |
#E6DCCE |
No gradients, no shadows, no rounded corners, no emojis.
Typography
Inter, weights 400 / 500 / 600, sizes 13 to 26 px. Tabular numerals for any numeric value. Group title: 22 px / 600. Caption: 14 px / 400 at 62 % opacity. Axis caption: 15 px / 400 at 62 % opacity. Inline value: 16–17 px / 500.
Composition rules
- No chart titles. The blog prose explains what the chart shows.
- Inline labels over legends. Put axis names on the axis, group names next to groups, values next to bars.
- Single accent. Only one colour stands out.
- Generous whitespace. Give every row at least 200 px vertical room if it has a caption beneath the group name.
- Native-size PNG. Never ask the user to scale the chart down in the final layout — fonts get compressed horizontally and everything looks cheap.
Deeper customisation
If the user needs something the CLI does not cover (different sizes, extra annotations, a different accent placement), point them at the repository. Every chart lives in its own file under src/charts/ with the default data array at the top; edit, save, run npm run snap.
Repository
https://github.com/shuakami/paperchart
1---2name: paperchart3description: Generate clean, calm chart PNGs in the spirit of Anthropic and OpenAI blog posts. Trigger when the user asks to produce a chart, model comparison table, diagram or infographic for a technical blog post, release notes, a research write-up or a slide deck and wants a single restrained palette, minimal chrome, heavy inline labelling, and PNG output rendered from React at native resolution. The primary surface is a shell CLI named `paperchart` that takes a JSON file and writes a PNG. Thirty-two primitives covering charts (line, area, scatter, heatmap, calendar heatmap, histogram, cdf, box-plot, ranking, dumbbell, slope, waterfall, small-multiples, timeline, funnel, sankey, treemap, radar, table, stacked-bar, grouped-bar, critical-path, pack-layout, latency, bytes, recall) and Mermaid-style structural diagrams (flowchart, architecture, sequence, state-diagram, er-diagram, delivery). Six themes, layout + style overrides per chart.4license: MIT5---67# paperchart89A six-primitive chart toolkit for rendering quiet, print-weight charts to PNG.10The surface is a shell CLI: feed it a JSON file, get a 2×-DPR figure.1112Trigger on any of: "make a chart for this blog post", "render a before / after13benchmark as an image", "I need a figure for my paper / release notes /14announcement", "design a minimal chart in the style of Anthropic or OpenAI15research posts", "draw a binary layout / critical path / latency comparison /16recall parity chart".1718Do **not** use this skill when the user asks for an interactive dashboard, a19real-time graph, or a chart with brand colours other than the paperchart20palette. This skill is opinionated on purpose.2122## The CLI2324```bash25paperchart <type> -i data.json -o out.png [--width 1600] [--dpr 2]26paperchart <type> --defaults -o out.png # render the built-in sample27```2829Types: `table`, `latency`, `bytes`, `stacked-bar`, `grouped-bar`, `funnel`, `ranking`, `dumbbell`, `slope`, `line`, `area`, `small-multiples`, `timeline`, `scatter`, `heatmap`, `calendar-heatmap`, `histogram`, `box-plot`, `cdf`, `radar`, `treemap`, `sankey`, `waterfall`, `critical-path`, `recall`, `pack-layout`, `delivery`, `flowchart`, `architecture`, `sequence`, `state-diagram`, `er-diagram`.3031Themes: `paper` (warm off-white, rust), `ink` (near-white, black), `slate` (cool grey, deep blue), `forest` (off-white, forest green), `mono` (pure monochrome), `dusk` (dark charcoal, warm orange). Pass `--theme <name>` on the CLI or put `"theme": "<name>"` in the JSON envelope.3233Invoke it with `npx github:shuakami/paperchart <type> -i data.json -o out.png` from any directory. On first run npm clones the repo, runs `prepare` to build the Vite bundle, and `playwright-chromium`'s postinstall downloads headless Chromium once (~200 MB). Subsequent runs are near-instant.3435If the skill has already been installed via `npx skills add shuakami/paperchart`, the full repo is available under `.claude/skills/paperchart/` (or `.agents/skills/paperchart/` on non-Claude agents). You can also just `cd` there and `node bin/paperchart.mjs <type> -i data.json -o out.png`.3637## Authoring recipe3839When the user asks for a chart:40411. Pick the primitive whose shape fits the data. One primitive per figure. If none fit, decline and explain why — do not invent a new chart kind inline.422. Write the JSON file with the shape documented below for that primitive. Keep captions short — 6 to 12 words. Use the accent colour on **exactly one** row / segment / panel. Leave the rest neutral.433. Run the CLI. Show the user the output path. If they want to iterate, edit the JSON and re-run — the file is the single source of truth.444. Never embed chart titles inside the chart. Titles belong in the blog prose next to the image.4546## The input envelope4748Every chart accepts the same shape:4950```json51{52 "theme": "ink",53 "layout": { "width": 1800, "fontScale": 1.05, "xAxisCaption": "..." },54 "style": { "accent": "#1f6feb" },55 "data": <chart-specific payload>56}57```5859`theme`, `layout`, and `style` are optional. If you pass just `data` (or wrap your data as a bare array), the chart uses good defaults.6061## Primitive schemas6263Short form. Full schemas are in the project README and in the per-type `skills/<type>/SKILL.md` files.6465- `table` — `{ columns: [{ key, label, align?, unit?, group? }], rows: [{ label, caption?, highlight?, values: { [key]: number | string } }] }`. Model / config / product comparison. Exactly one `highlight: true` row.66- `latency` — `Row[]`. Each row: `{ group, caption, color, bars: [{ level, ms }] }`.67- `bytes` — `Row[]`. Each row: `{ group, caption, accent, segments: [{ kb, fill, tag }], firstLoadKB, deferredKB }`.68- `stacked-bar` — `Row[]` where each row is `{ label, caption?, segments: [{ key, value, color? }] }`. Composition per row.69- `grouped-bar` — `{ series: [{ key, label, color? }], groups: [{ label, caption?, values: { [key]: number } }] }`.70- `ranking` — `Row[]`. Each row: `{ label, caption?, value, accent? }`. Sorted leaderboard, one accented row.71- `dumbbell` — `Row[]`. Each row: `{ label, before, after }`.72- `slope` — `{ startLabel, endLabel, unit?, series: [{ label, start, end, accent? }] }`.73- `line` — `{ xLabels, series: [{ label, values, accent? }] }`.74- `area` — `{ xLabels, series: [{ label, values }] }` (stacked).75- `small-multiples` — `{ xLabels?, unit?, panels: [{ label, caption?, values, accent? }] }`.76- `scatter` — `Point[]` with optional regression line.77- `heatmap` — `{ rowLabels, colLabels, values: number[][] }`.78- `histogram` — `{ bins: [{ x0, x1, count }] }`.79- `cdf` — `{ points: [{ value, cumulative }] }`.80- `waterfall` — `{ steps: [{ label, delta, subtotal? }] }`.81- `timeline` — `Row[]` where each row is `{ label, caption?, spans: [{ start, end, label, accent? }], milestones?: [{ at, label }] }`.82- `funnel` — `Stage[]`. Each stage: `{ label, caption?, count, accent? }`. Drop-off auto-computed between stages.83- `sankey` — `{ sources: Node[], targets: Node[], flows: [{ from, to, value }] }`. Two-column flow diagram.84- `treemap` — `Item[]` where each item is `{ label, value, caption?, accent?, children?: [{ label, value }] }`. Two-level squarified.85- `radar` — `{ axes: [{ key, label, caption?, max? }], series: [{ label, caption?, accent?, values: { [axisKey]: number } }] }`.86- `box-plot` — `Row[]`. Each row: `{ label, caption?, min, q1, median, q3, max, outliers?, accent? }`.87- `calendar-heatmap` — `{ start?: "YYYY-MM-DD", days: [{ date, value }] }` or just `[{ date, value }]`.88- `critical-path` — `Row[]`. Each row: `{ label, detail, startMs, endMs, kb, critical: boolean }`.89- `recall` — `Query[]`. Each query: `{ query, hits, sets: "equal" }`.90- `pack-layout` — `Segment[]`. Each segment: `{ label, detail, bytes, accent: boolean }`.91- `delivery` — `{ header, subheader, panels: PanelSpec[] }` with up to 3 panels.92- `flowchart` — `{ direction?: "TD" | "LR", nodes: [{ id, label, caption?, shape?: "rect" | "round" | "stadium" | "diamond" | "cylinder", accent? }], edges: [{ from, to, label?, dashed?, accent? }] }`. Layered DAG, back-edges detected and drawn as loop-backs.93- `architecture` — `{ groups: [{ id, label, caption?, services: [{ id, label, caption?, accent? }] }], connections: [{ from, to, label?, dashed?, accent?, bidirectional? }] }`. Groups stack left-to-right; services stack top-to-bottom inside each group.94- `sequence` — `{ actors: [{ id, label, caption? }], steps: [{ kind: "msg", from, to, label?, reply?, async?, accent? } | { kind: "note", over: id | [id, id], text }] }`. Time flows downward.95- `state-diagram` — `{ direction?: "TD" | "LR", states: [{ id, label, caption?, start?, end?, accent? }], transitions: [{ from, to, label?, dashed?, accent? }] }`. Start / end pseudostates render as • / ⦾.96- `er-diagram` — `{ entities: [{ id, label, caption?, accent?, fields: [{ name, type?, pk?, fk?, note? }] }], relationships: [{ from, to, fromCard, toCard, label?, accent? }] }` where cardinality codes are `"1"`, `"0..1"`, `"M"`, `"1..M"`, `"0..M"`.9798## The palette — five values only99100| role | hex |101| --- | --- |102| page / card background | `#F6F1EA` |103| accent (one element per chart) | `#C75F3C` |104| secondary neutral | `#D6B99B` |105| ink | `#2B2A27` |106| rule / hairline | `#E6DCCE` |107108No gradients, no shadows, no rounded corners, no emojis.109110## Typography111112Inter, weights 400 / 500 / 600, sizes 13 to 26 px. Tabular numerals for any numeric value. Group title: 22 px / 600. Caption: 14 px / 400 at 62 % opacity. Axis caption: 15 px / 400 at 62 % opacity. Inline value: 16–17 px / 500.113114## Composition rules1151161. **No chart titles.** The blog prose explains what the chart shows.1172. **Inline labels over legends.** Put axis names on the axis, group names next to groups, values next to bars.1183. **Single accent.** Only one colour stands out.1194. **Generous whitespace.** Give every row at least 200 px vertical room if it has a caption beneath the group name.1205. **Native-size PNG.** Never ask the user to scale the chart down in the final layout — fonts get compressed horizontally and everything looks cheap.121122## Deeper customisation123124If the user needs something the CLI does not cover (different sizes, extra annotations, a different accent placement), point them at the repository. Every chart lives in its own file under `src/charts/` with the default data array at the top; edit, save, run `npm run snap`.125126## Repository127128`https://github.com/shuakami/paperchart`