Diagram Design: Editorial Diagram Taste as 45 Visual Types and 32 Machine Verifiers
cathrynlavery/diagram-design
Cathryn Lavery's diagram-design (MIT, 54,318 stars) turns one sentence of natural language into a self-contained HTML+SVG editorial diagram: 45 visual types each with its own reference, semantic patterns chosen before layout; six mandatory connector rules, a 4px grid and a per-diagram budget of at most 9 nodes make taste decidable; .drawio / Mermaid / Excalidraw sources are extracted, never rendered, then redrawn through four dials (format, size, detail, audience) with a fidelity ledger; every diagram's HTML carries a data-* metadata contract that 32 verify-* scripts audit number by number and line by line.
Editor's takeWhat is most worth stealing is that taste is decomposed into three decidable machine contracts: color as semantic roles plus a contrast contract (verify-contrast.py computes WCAG for every role against the background it actually sits on, compositing translucency first), geometry as a 4px grid plus per-diagram budgets of at most 9 nodes, 12 arrows and 2 accent elements, and numbers as data-value / data-carry / data-status declarations in the HTML that 32 verify-* scripts recompute. Measured locally: change one delta in the shipped waterfall example from −38 to −36 and verify-waterfall.py immediately reports that bar 6 'FY26 plan' declares 236 while the walk arrives at 238; mermaid_extract.py on the 9-node fixture prints a node and edge census, hub degrees, entry and terminal nodes, collapsible groups and budget flags; the adversarial Excalidraw fixture's 'IGNORE ALL PREVIOUS INSTRUCTIONS' survives only as an escaped inert label while its two links and one embed land in the discarded list. The limits are stated just as honestly: the README concedes output quality depends on the model, the same prompt can lay out differently twice, and the result is not a diffable source format — for docs that live in Git and change every sprint, Mermaid is still the better fit; PNG export needs a one-time Playwright plus Chromium install; the 9-node budget means complex systems must be split into overview plus detail; and the 210 pre-baked examples were built under an earlier skin, with regeneration an open v5.1 task.
npx skills add cathrynlavery/diagram-designModels understand content; what they lack is taste
diagram-design (MIT, 54,318 stars / 3,442 forks, by Cathryn Lavery, homepage diagramdesign.dev, plugin version 2.6.98) is a cross-runtime Agent Skill for Claude Code, Codex, GitHub Copilot, Factory Droid, Pi, Cursor, Gemini CLI, Kiro and OpenCode. Given one sentence of intent, the agent produces a single self-contained HTML file — inline SVG, embedded CSS, no JavaScript by default — that opens on double-click and exports to SVG or PNG. The repo's own tagline is the whole pitch: 45 diagram types. No shadows. No Mermaid slop.
The origin story in the README is specific: every time the author needed a figure for a blog post, asking Claude produced "a generic rounded-box thing that looked nothing like the rest of the site", so it was either thirty minutes fighting Figma or no diagram at all. "The model already understood the content. What it was missing was taste, so I wrote the taste down": one accent color for the one or two things that matter, 1px hairlines, no shadows, every coordinate on a 4px grid. Everything else in the repository — 63 references, 210 examples, 87 scripts — is that sentence made executable.

type-architecture example: exactly one focal node (accent stroke on accent-tint fill), rounded orthogonal connectors only, arrow labels sitting on opaque masks with visible gaps, and the legend as a bottom strip instead of a box floating inside the diagram — none of the §4 anti-patterns survive here.45 visual types plus 9 semantic patterns: behavior first, layout second
The taxonomy is two-layered. Semantic patterns describe what a system does; visual types describe how information is arranged. When behavior, state, enforcement or risk carries the meaning, the agent loads semantic-patterns.md, picks one primary pattern, then picks the nearest visual type as the layout grammar; otherwise it chooses a type directly. Each of the 45 types has its own references/type-*.md, and CI pins the count (VISUAL_TYPE_COUNT = 45 in verify-docs-sync.py): the selection table, the frontmatter description, the gallery ordinals and the README count must all agree. The catalog runs from architecture, architecture delta and IT current-state through flowchart, sequence, state, ER, timeline, swimlane, quadrant, radar, polar, loop/flywheel, nested, tree, org chart, layer stack, exploded axonometric, axonometric plan, Venn, pyramid/funnel, treemap/marimekko, heatmap, bar/dumbbell, waterfall, line variants (slopegraph, ridgeline, streamgraph, bump), Gantt, scatter (bubble, beeswarm), high-level, process, medallion, data flow, DP integration, DP security matrix, Sankey, fishbone, Wardley map, kanban, user journey, service blueprint, deployment, dependency graph, UML class, story map and database schema.
| The reader must understand… | Semantic pattern | Nearest visual type |
|---|---|---|
| Many arrivals competing for finite service capacity | Fan-in queue / bottleneck | Data flow |
| Repeated question / input / governance / output slots across stages | Stage framework with semantic slots | Process |
| A loose conversation becoming a durable structured record | Unstructured input → structured artifact | Data flow |
| Why two policy decisions differ and where they first diverge | Paired policy-evaluation traces | Flowchart |
| Which routes cross a trust boundary and which are blocked | Secure paved road | Architecture |
| Which controls apply at each enforcement surface | Governance / control catalog | Layer stack |
| How defenses reduce risk and what risk remains | Compensating security layers | Layer stack |
| Which sub-elements a system decomposes into, each independently citable | Traceable block decomposition | Tree |
| How one subject progresses through phases, waits, retries and terminal outcomes | Lifecycle phase map | State machine |
Selection discipline is written down too: if a three-column table communicates the same thing, pick the table; if two types both look useful, pick the dominant axis and let a semantic pattern add at most one supporting primitive, never a second layout grammar; past the complexity budget, split into overview plus detail. Before drawing, the agent states its plan — type, pattern, size preset, planned cuts — and lets the user redirect when reachable.
The design system: semantic roles, one focal point, a 4px grid, hard budgets
style-guide.md is the single source of truth for color and type; every other reference names semantic roles (accent, never a hex). The default skin is a cool editorial palette: white-smoke paper #f5f5f5, jet-black ink #2d3142, blue-slate muted #4f5d75, burnt-tangerine accent #bf4520. Note that the accent is not the raw brand tangerine — raw atomic-tangerine measures 2.86:1 on paper, too faint for a label, so the skin uses the same hue darkened until it clears 4.5:1 (measured 4.71:1). Contrast is a contract, not a suggestion: verify-contrast.py computes the WCAG ratio for every role against the background it actually sits on, compositing translucent colors first, 4.5:1 for text and 3:1 for essential marks, on both skins, and fails CI when a row falls short.
| Role | Purpose | light | dark |
|---|---|---|---|
paper / paper-2 | page bg / container bg | #f5f5f5 / #ececec | #2d3142 / #393e53 |
ink | primary text and stroke | #2d3142 | #f5f5f5 |
muted / soft | secondary text, default arrows / sublabels | #4f5d75 / #5a6580 | #bfc0c0 / #949eb2 |
accent / accent-tint | focal, 1–2 per diagram / focal fill | #bf4520 / 8% alpha | #f08a59 / 10% alpha |
link | HTTP/API calls, external arrows | #2e5aa8 | #739fdf |
Three type families, each with one job: Instrument Serif for the H1 and italic callouts, Geist sans 600 for node names, Geist Mono for technical sublabels only (ports, commands, URLs) — "mono is for technical content, not a blanket dev aesthetic", with JetBrains Mono explicitly banned. Structural geometry lives on a 4px grid: node widths and heights are drawn from 80/96/112/120/128/140/144/160/180/200/240/320, gaps from 20/24/32/40/48, padding from 8/12/16, radii from 4/6/8. Font sizes are exempt from the grid; they follow the per-preset role ramp in output-spec.md.
The complexity budget is where the taste gets teeth: at most 9 nodes, 12 arrows or transitions, 2 accent elements and 2 annotation callouts per diagram; exceed it and the diagram becomes two. Every type adds its own row in layout-budget.md: sequence allows 5 lifelines, 1 combined fragment (2 only if each is a single-region opt/loop) and 2 alt regions; ER allows 8 entities; Sankey 3 stages / 8 nodes / 12 flows; waterfall 8 bars including totals with 1 subtotal; db schema 5 tables / 8 shown columns / 6 FKs; scatter 30 points; Venn 3 circles; radar 5 axes and 5 series with exactly 1 focal. A semantic pattern's own budget may only tighten these.

data-* declarations in the HTML and verify-sankey.py re-walks the flows and ribbon geometry. The accent carries exactly one idea here — the flaky reruns.Six mandatory connector rules and the taste gate
§6 states six connector rules as automatic failures: orthogonal routing only, with r=8 rounded elbows between off-axis nodes and diagonals failing outright; every arrow label (≤14 characters, all caps, centered) on an opaque mask with a visible 6–10px gap from its stroke, beside vertical segments and never on the line; no overlapping or stacked strokes, parallel routes offset by ≥12px with a bridge/hop at a single crossing; distinct attach points per connector on a shared edge at L·k/(N+1), ≥12px apart; no transit behind a non-endpoint box except the geometrically unavoidable case (dashed, label at the visible end, no marker on the intervening box); and no label mask overlapped by a node painted after it. Paint order is fixed too — background, zones, arrows, labels, nodes — so lines sit behind boxes.
The §4 anti-pattern table is a wanted list for "AI slop": dark mode with cyan/purple glow, JetBrains Mono as a blanket dev font, identical boxes for every node, a legend floating inside the diagram, unmasked arrow labels, vertical writing-mode text, three equal-width summary cards, any shadow, rounded-2xl, coral on every "important" node, and reproducing Mermaid's renderer layout. The §9 pre-output checklist runs before any diagram ships: type fit, would-a-table-do, the right type reference loaded, all six connector rules, the 4px grid, the accessible-SVG contract, and self_check.py printing OK.
Import is extract-then-redraw, not conversion: four dials and a fidelity ledger
For existing .drawio, Mermaid and Excalidraw sources the stance is extract, don't render; redraw, never convert. Three dependency-free extractors (drawio_extract.py 33.8KB, mermaid_extract.py 56.1KB, excalidraw_extract.py 26.4KB) parse sources into one digest shape: node/edge census, containers and depth, shape distribution, hub degrees as focal candidates, entry points and terminals, unconnected nodes, collapsible groups and budget flags. Every source label, link, directive and metadata field is treated as untrusted data. Source coordinates, palette, fonts, draw.io's diagonal spaghetti and Mermaid's automatic layout are discarded; components, relationships, grouping and direction survive.
| Dial | Options | What it changes |
|---|---|---|
| Format | html · svg · png · html+png | the deliverable: SVG for Figma, PNG for slides, HTML for the web |
| Size | doc-inline 960×600 · doc-wide / slide-16x9 1280×720 · slide-4x3 · social-og 1200×632 · social-square · print-a4/a3/letter · fit | the viewBox and the type ramp: a projected slide gets 16px node names, not 12px |
| Detail | faithful (≤24 nodes, zoned) · balanced (≤12, default) · simplified (≤7) | how much of the source survives, via a fixed degrade ladder: decorations, duplicates, leaf clusters, infrastructure |
| Audience | engineer · mixed · executive | wording, not count: Auth Service / JWT · RS256 · :8443 → token check → Sign-in |
Every import ends with a fidelity ledger of what was merged, collapsed or dropped. The README's sample is admirably plain: "Collapsed: 'Token valid?' decision → edge label on Gateway → Auth. Dropped: 1 sticky note ('legacy path, to be retired'), unconnected in source." The rationale is that the user knows the source and would notice anyway.

balanced detail: the source's six pastel fills became one accent and its hand-dragged coordinates became a 4px grid. The extractor reports budget: nodes OVER (max 9) first, so the degrade decision happens before any drawing.The output is a single HTML file — and a machine-readable contract
This is the hardest layer of the design: each type's output HTML carries a data-* metadata contract (data-diagram, data-snapshot, data-grid, data-object-id, data-status, data-signature, waterfall's data-value/data-carry, the block decomposition's data-block-*). The figure declares what it drew, and 32 verify-*.py scripts recompute those declarations: waterfall running-total conservation, Sankey flow conservation, treemap and marimekko areas matching their labelled values, slopegraph endpoints on one shared scale equal to their printed values, beeswarm dots exactly at their values with no overprint, ridgeline shared amplitude, heatmap monotone opacity ramp plus a complete N×M grid and ≤1 focal cell, bubble areas encoding every declared size, architecture-delta grids and change ledgers agreeing across snapshots. Each verifier ships with a test-verify-*.py of adversarial cases (41 of them), so the checkers themselves get checked.
I ran several of them against a fresh checkout — zero dependencies, straight Python 3.13:
| Command | Measured result |
|---|---|
self_check.py assets/example-architecture.html | OK (accessible-SVG contract, single-file safety, content inside the viewBox) |
verify-geometry.py assets/example-architecture.html | 0 findings (no mask clipped by a later node, no diagonals, stacked strokes or corner ports) |
verify-waterfall.py assets/example-waterfall.html | OK … conserve the running total and draw it where they claim |
verify-waterfall.py fixtures/waterfall-plausible-wrong.html | FAIL: bar 6 "FY26 plan" declares 236 but the walk arrives at 238 — the fixture differs from the shipped example by a single delta (−38 changed to −36) |
mermaid_extract.py fixtures/sample-flowchart.mmd | 9n/7e digest: shape census, hubs (API Gateway degree 6), entry/terminal nodes, the unconnected note, two collapsible groups, budget ok |
drawio_extract.py fixtures/sample-architecture.drawio | 12n/8e with budget: nodes OVER (max 9) — the degrade decision is forced before drawing |
excalidraw_extract.py fixtures/sample-adversarial.excalidraw | injection text escaped into an inert label; discarded: 1 image payloads, 2 links, 1 embeds, 1 deleted, 1 unknown — links are never followed |
export_svg.py assets/example-flowchart.html out.svg | 8.7KB standalone SVG: role="img" + aria-labelledby, class CSS carried in CDATA, font @import, namespaced defs IDs |

data-carry and the verifier re-walks the arithmetic. Perturb one delta by 2 and CI names the bar, the declared value and the walked value.The rendered gates are equally unsentimental. lint-render.py (1,679 lines) opens every example in headless Chromium, screenshots it as authored, screenshots again with overflow released in stages, and pixel-diffs the two — new ink is paint that was being clipped. lint-skin.py reads source for skin conformance (palette, fonts, single-file safety, script allowlist). verify-docs-sync.py (1,765 lines) watches sixteen drift classes, including "the frontmatter description must keep a lexical hook for every visual type" and "SKILL.md must stay under 40,000 bytes" (ADR 0004: the description is the only text an agent sees before deciding to load the skill, so routing vocabulary is never traded for body prose).
Progressive disclosure: SKILL.md is a router
At startup an installed agent sees only the skill's name and description; a matching request loads SKILL.md (30,778 bytes against a 40,000 cap), then exactly one type reference; behavior-heavy figures add semantic-patterns.md, animation adds animation.md, exact markup adds primitives-core.md, a per-type budget row adds layout-budget.md. The README's "What loads when" table makes this a contract: "Make me a flowchart" is SKILL.md plus type-flowchart.md and nothing else. "Add a new type tomorrow and nothing else changes." 210 example HTML files and 47 canonical screenshots (rendered deterministically by render-canonical-screenshots.py, with verify-screenshot-freshness.py watching for staleness) give every type an openable reference.
Branding runs through onboarding: "onboard diagram-design to https://yoursite.com" makes the agent fetch the homepage, extract the dominant palette and font stack, map detected values onto semantic roles, show a proposed diff and write the tokens into style-guide.md, with a fidelity receipt naming sampled URLs, exact color roles, font families and weights, font source URLs and any fallback. WCAG AA is verified before tokens are written, and a site color that fails contrast at 9–12px diagram sizes gets a proposed adjustment with the reason. Multi-client work uses named profiles in ~/.diagram-design/profiles/<slug>.md plus a per-project .diagram-design marker (ADR 0006), so parallel workspaces keep different brands without overwriting a shared install. A first-run gate refuses to ship default-skinned diagrams into a branded project silently: with the style guide still at factory defaults, the skill pauses and asks whether to customize now, paste tokens, or keep the default.

verify-axonometric-plan.py checks all of it, and build-axonometric-plan-examples.py --check keeps the shipped examples identical to what the builder produces.Distribution and CI: six host manifests, 83 local gates
The repository maintains three native manifests — .claude-plugin/ (marketplace plus plugin), .codex-plugin/ and .factory-plugin/ — six slash commands in commands/ (export-diagram, import-drawio, import-mermaid, import-excalidraw, profile, doctor), five Pi prompt templates in prompts/, and the cross-host npx skills add cathrynlavery/diagram-design. Manifest versions are bumped on main after each merge by the Auto Version Bump workflow (ADR 0009); a PR that edits them is rejected by CI, which is what keeps open PRs from conflicting on every merge. .maintainer-policy.json encodes 83 local gate commands, squash merging and a five-open-PRs-per-contributor limit as machine-readable policy, with test-maintainer-policy.py keeping it honest against the manifests and CI. The CI matrix is three operating systems × Python 3.11/3.12; pixel comparisons run only on ubuntu + 3.12 (six oracles comparing against each other would be noise), while every other leg runs all source-level gates.
The privacy posture is equally clean: PRIVACY.md states there is no server, no accounts, no analytics and no telemetry. The only network requests are the Google Fonts stylesheet when a browser opens a diagram (switchable to system fonts for zero requests) and the pages the agent itself fetches when a user asks for brand onboarding. /doctor is a one-shot, read-only environment diagnostic: it resolves python3 (a Windows Microsoft Store alias stub does not count), requires ≥3.10, and checks Playwright and command routing without touching user files.

Verdict and honest limits
What makes this repo worth stealing is not the 45 layouts but that taste is decomposed into three decidable artifacts: semantic roles plus a contrast contract (color), a 4px grid plus complexity budgets (geometry), and data-* declarations plus 32 verifiers (numbers). Most design-flavored skills stop at adjectives; this one stops at CI. The import pipeline's stance is unusually correct as well: never render the source, never reuse its coordinates, treat its text as untrusted data, and hand over a fidelity ledger — the adversarial fixture's "IGNORE ALL PREVIOUS INSTRUCTIONS" survives only as an escaped, inert node label while its links and embeds land in the discarded list.
The limits are stated with the same clarity. The README's comparison table concedes that output quality depends on the model you run, that the same prompt can lay out differently twice, and that the result is not an editable source format — no text to diff in a pull request, no canvas to drag: "for docs that live in Git and change every sprint, Mermaid is still the better fit. Diagram Design is for the diagram you are going to publish." PNG export needs a one-time Playwright plus Chromium install; the 9-node budget means a genuinely large system must be split into overview plus detail; the 210 pre-baked examples were built under an earlier skin and regenerating them against the current style guide is an open v5.1 task. Motion is an optional presentation layer (none/reveal/step/loop, with ADR 0003 allowing only reveal as autoplay) and must render its complete static meaning with no JavaScript and under prefers-reduced-motion.
Install
# Cross-host (Cursor / Claude Code / Codex / Copilot / Gemini CLI / Cline / Windsurf / Amp…)
npx skills add cathrynlavery/diagram-design
# Claude Code (marketplace: slash commands + auto-update)
/plugin marketplace add cathrynlavery/diagram-design
/plugin install diagram-design@diagram-design
# Codex
codex plugin marketplace add cathrynlavery/diagram-design
codex plugin add diagram-design@diagram-design
# Post-install self check on any generated file
python3 <skill-dir>/scripts/self_check.py my-diagram.html