figure-generator
Skill0 starsChanged 9 days agoMITView on GitHub
Ship deterministic hand-authored SVG generators for print-quality figures backed by real published datasets. Ships the .mjs generator pattern reading from data/<slug>.json, the data-sha256 audit hash embed for provenance, the dataset-first rule with contract tests pinning headline numbers, the dataset-inversion procedure for reverse-engineering data from an approved sample SVG, the fix-in-generator-never-in-SVG rule, and the figure-count hoist to one JSON. Use when authoring reproducible book or report figures, when a figure's numbers might drift silently, or when reverse-engineering data from a graphic whose dataset is missing.
In this repository66 other files it gives its agents.
- AGENTS.mdAGENTS.md
- skills/bootstrap-project/resources/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- skills/bootstrap-project/resources/CLAUDE.mdCLAUDE.md
- act-tenetsSkill
- adversarial-reviewSkill
- annotate-screenshotSkill
- anti-hallucinationSkill
- ascii-chartSkill
- assess-brainSkill
- big-ideaSkill
- bootstrap-coreSkill
- bootstrap-projectSkill
- browser-toolsSkill
- chart-big-ideaSkill
- chart-interpretationSkill
- chart-vocabularySkill
- code-reviewSkill
- communication-craftSkill
- compile-brainSkill
- convertSkill
- corpus-qa-sweepSkill
- critical-thinkingSkill
- deep-reviewSkill
- doc-hygieneSkill
- docs-shellSkill
- docx-to-mdSkill
- ethical-reasoningSkill
- evaluate-before-adoptingSkill
- flint-chartSkill
- flint-interactionsSkill
- flint-themeSkill
- git-workflowSkill
- html-to-mdSkill
- humanizerSkill
- install-visual-companionsSkill
- lint-clean-markdownSkill
- markdown-mermaidSkill
- markdown-sanitization-chainSkill
- md-to-emlSkill
- md-to-htmlSkill
- md-to-txtSkill
- md-to-wordSkill
- meditationSkill
- mutation-testingSkill
- planSkill
- platform-awarenessSkill
- print-svg-style-guideSkill
- proactive-awarenessSkill
- problem-framing-auditSkill
- project-capability-authoringSkill
- project-statusSkill
- render-chartSkill
- render-verifySkill
- rich-emailSkill
- risk-analysisSkill
- security-and-hardeningSkill
- setup-dependenciesSkill
- setup-enterprise-stackSkill
- spikeSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 2 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 23
Every shipped figure is emitted by a deterministic .mjs script that:
- Very specific: line 60
Debugging is easier: When a figure looks wrong, console.log() inside the generator shows exactly the numbers it saw.
- Distinctive: line 66
Every figure must be backed by a REAL, PUBLISHED dataset.
- Very specific: line 170
Look for <text> elements near axis lines with numeric content.
- Distinctive: line 192
When a project runs multiple gates that all depend on "how many figures are expected," hoist the count into one JSON file.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Figure generator discipline
Deterministic figure production for books, reports, and any artifact where a figure's numbers have to survive re-generation and stay auditable. Distilled from a shipped book: 53 figures across 14 chapters, every generator hand-authored, every dataset published.
The engineering side of the illustration workflow. print-svg-style-guide governs how figures LOOK; this skill governs how they get MADE, VERSIONED, and AUDITED.
When to invoke
- Authoring a new figure for a book, report, or exec-facing document whose numbers should stay traceable
- Re-authoring an existing figure whose data has drifted or whose script is missing
- Reverse-engineering the underlying dataset from an approved sample SVG
- Setting up a contract test for a new figure's headline numbers
- Diagnosing why a figure's numbers do not match the surrounding prose
The generator pattern
Every shipped figure is emitted by a deterministic .mjs script that:
- Reads from
data/<slug>.json— never inlines the numbers - Computes a SHA-256 hash of the source data
- Embeds the hash in the SVG as an XML comment (
<!-- data-sha256: ... -->) - Emits to
assets/figures/<NN>-<slug>.svg(or the project's chosen path)
Skeleton:
// scripts/generate-<slug>.mjs
import { readFileSync, writeFileSync } from "node:fs";
import { createHash } from "node:crypto";
const DATA_PATH = "data/<slug>.json";
const OUT_PATH = "assets/figures/<NN>-<slug>.svg";
const raw = readFileSync(DATA_PATH, "utf8");
const data = JSON.parse(raw);
const dataHash = createHash("sha256").update(raw).digest("hex");
// ... layout math using data ...
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480" font-family="Inter, system-ui, sans-serif">
<!-- data-sha256: ${dataHash} -->
<title>${title}</title>
<!-- ... marks, labels, axes ... -->
</svg>`;
writeFileSync(OUT_PATH, svg);
console.log(`Wrote ${OUT_PATH} (data-sha256: ${dataHash.slice(0, 12)}...)`);Why hand-authored and not a rendering library
- Layout math is exact. Statistical-chart libraries (Vega-Lite, ECharts, Chart.js via the
flint-chartMCP server) own final geometry — they stretch within abaseSizeceiling and return their owncomputedSize. Print-legibility gates and bounds gates need the generator to own layout, not the renderer. - Regen is byte-stable. Given the same dataset, a hand-authored generator produces byte-identical output. That is what makes
data-sha256a useful audit hash. - Debugging is easier. When a figure looks wrong,
console.log()inside the generator shows exactly the numbers it saw. A renderer failure hides behind opaque layout errors.
flint-chart still has a role — as an EXPLORATION tool during the design pass (Step 5a of the [chart-big-idea → flint-chart → hand-author] pipeline). Flint helps pick a chart family faster than reasoning from scratch. The shipped artifact, though, is always a hand-authored generator when the target is print.
Dataset-first rule
Every figure must be backed by a REAL, PUBLISHED dataset. Never inline composite numbers. The rule holds for character-anchored figures and generic figures alike; the reader must be able to reproduce the scenario.
The publication set for each dataset:
data/<slug>.json(source of truth)data/<slug>.csv(human-readable copy for readers who prefer tables)data/<slug>.md(README describing schema, provenance, units)data/<slug>.schema.json(JSON Schema so downstream consumers can validate)
For a companion-site workflow (static-site publication):
- Publish to
datasets/<slug>/on the site (schema page + CSV + JSON + README) - Publication is a PREREQUISITE for shipping the figure, not a follow-up
Naming convention: <slug> describes the figure content, not the character or the chapter. q3-region-revenue is right; chapter-5-figure-4 is not. Readers look up datasets by concept, not by figure number, so the concept has to be in the name.
Contract tests (pin the headline numbers)
For every figure, write a contract test that pins the values the surrounding prose cites. Runs via Node's built-in test runner.
Skeleton:
// scripts/<slug>-contract.test.mjs
import { readFileSync } from "node:fs";
import { test } from "node:test";
import assert from "node:assert/strict";
const data = JSON.parse(readFileSync("data/<slug>.json", "utf8"));
test("Q4 aggregate matches the chapter total", () => {
const q4Total = data.regions.reduce((s, r) => s + r.q4, 0);
assert.equal(q4Total, 28400000); // "$28.4M quarter" in chapter prose
});
test("EMEA variance matches the prose claim", () => {
const emea = data.regions.find(r => r.name === "EMEA");
const variance = emea.actual - emea.plan;
assert.equal(variance, 1700000); // "$1.7M variance" in chapter prose
});
test("Cohort 6 gain is 2.18x Cohort 1", () => {
const c1 = data.cohorts[0].gain;
const c6 = data.cohorts[5].gain;
assert.equal(Math.round((c6 / c1) * 100) / 100, 2.18); // ordinal claim in prose
});Run: node --test scripts/<slug>-contract.test.mjs
What to pin
- Aggregate totals cited in prose (
$28.4M quarter) - Per-segment values the prose names (
$1.7M variance in EMEA) - Ordinal claims (
2.18× larger,third-lowest,only region falling)
What NOT to pin
- The full dataset shape (schema tests belong in a separate
<slug>.schema.test.mjs) - Numbers that appear ONLY in the figure and never in prose (over-constrained; makes future dataset edits noisy)
Register the test
Many projects use an explicit test list (e.g., dd:contract in the DDA workflow). Dropping a new <slug>-contract.test.mjs into scripts/ does nothing if the harness enumerates its tests explicitly. Add the test to whatever list the project maintains. The tell is the test count: if it did not rise by the number of tests you wrote, you are not wired in.
Typical package.json edit:
{
"scripts": {
"dd:<slug>": "node scripts/generate-<slug>.mjs && node --test scripts/<slug>-contract.test.mjs",
"dd:contract": "node --test scripts/<slug1>-contract.test.mjs scripts/<slug2>-contract.test.mjs ..."
}
}Both edits ship together — the per-figure script and the aggregate list.
Fix in the generator, never in the SVG
Manual SVG edits get clobbered on the next regen. The rule is absolute:
| Symptom | Fix in |
| Number wrong in the SVG | The generator's layout math, or the dataset |
| Text truncated | The generator's layout — see print-svg-style-guide § "when text won't fit" (cut / reflow / abbreviate / grow) |
| Color miscoded | The generator's palette lookup, not the SVG's fill attribute |
XML-invalid comment (-- inside, bare &) | The generator's comment emission — see render-verify § SVG XML invalid |
| Regenerating clobbers your patch | That is the rule working, not a bug |
The one legitimate exception: a one-off, throwaway figure that will never regenerate. If you are SURE, hand-edit the SVG. But then treat that SVG as the source of truth and delete the generator, so the next contributor is not misled.
Dataset inversion (reverse-engineer the data from an approved sample SVG)
Sometimes an approved sample SVG exists but its underlying dataset is missing. Do not reconstruct the data from prose descriptions — prose fabricates. Invert the sample.
Procedure
Extract axis tick coordinates and their labeled values from the sample.
Select-String -Path <sample>.svg -Pattern '<text|<line' | Select-Object -First 40Look for
<text>elements near axis lines with numeric content; those are tick labels. Match each label to the<line>coordinate it labels.Solve two ticks for the linear scale with this skill's bundled script, never by hand:
node scripts/invert-svg.mjs <sample>.svg --tick-a 0 --tick-b 50The script selects tick labels aligned on the requested axis, pairs them with nearby tick-mark lines, and solves
pxPerUnit = (y1 - y2) / (v2 - v1). It supports flat SVGs with explicit, double-quoted coordinates, not arbitrary rendered SVG geometry. Ambiguous axes, transforms, and missing supported points exit with code 2; isolate or flatten the intended chart before retrying. Example from a shipped case: Fig 6.6 solved to2.25 px per point.- Invert every data-point coordinate through the scale. The same script run inverts every circle centre and polyline / polygon point it finds (
dataValue = vA + (pA - p) / pxPerUnit, with the scale's sign carrying the axis direction) and reports each derived value. Pass--grid <step>(or--precision <decimals>) for the domain's natural precision, and--write <path>to save the derived values as a JSON array ready to becomedata/<slug>.json. - A necessary check on the derivation: every derived value resolves to an EXACT value on the natural grid. The script exits with code 1 and does not write a dataset if a point is off-grid. Fig 6.6's 24 data points all landed on integers, with no fractional pixels and no rounding fudge. Passing this check does not establish that the selected axis, marks, or units are the right ones; those remain semantic decisions.
- Feed the derived values into the generator and verify byte-identity. The output polylines should be byte-identical to the sample. That is the strongest evidence the dataset faithfully encodes the approved design.
Why inversion beats prose reconstruction
Prose fabricates. In a real case, prose said "EMEA is the only region falling" — the sample proved three regions decline and the two lines diverge (gaps 10 → 15 → 20 → 30). A generator authored from the prose would have shipped one wrong region trajectory. The sample doesn't lie, and the byte-identity check is the strongest possible verification.
Figure-count hoist (one JSON, not scattered constants)
When a project runs multiple gates that all depend on "how many figures are expected," hoist the count into one JSON file. Every gate reads from it. Adding a figure touches ONE file.
{ "expectedFigures": 62, "expectedPages": 439 }Consumers read the same file:
check-placeholders.cjs— counts![...]()declarations in chapter markdownbuild-pdf.js— counts figures embedded as data-URIs during the buildcheck-rendered-figures.cjs— counts/ImageXObjects in the built PDFcheck-placeholders.test.cjs— the contract test for the above
JSON, not a .js module, is deliberate: if the root package.json declares "type": "module", the .js build script is ESM and cannot require(), while the gate scripts are .cjs. JSON is the only format both read without a bridge (CJS require, ESM readFileSync + JSON.parse).
Before hoisting, the figure count typically lives scattered across three or four files. Three of the four go silently stale on the next figure. Bump one number, hope the rest catch up, discover in a slow build that one did not. Hoist eliminates the class of drift.
Page count: measure, do not predict
expectedPages must be read off the PDF-build output, never assumed. A figure does not reliably add a page. In the DDA workflow, Fig 5.4 added a page; Fig 6.6 did not (439 → 439). Set the value AFTER the build, not before.
Anti-patterns
| Don't | Do |
Inline numbers in the generator (const q3Total = 28400000) | Read from data/<slug>.json and hash the raw text into the SVG |
| Ship a figure without a contract test | Numeric truth is the argument's substrate; drift without a test is silent |
| Hand-edit the emitted SVG to fix a defect | Grep scripts/generate-* for the filename first — fix the generator, then re-run it |
| Reconstruct a dataset from prose descriptions | Prose fabricates. Invert the sample instead |
| Bump the figure count without hoisting | Every gate that reads a figure count separately is a future stale-count bug |
Predict expectedPages from figure count | Measure it off the built PDF |
| Skip test-registration ("the test file exists, it must run") | Explicit test lists silently skip unregistered files. The tell is the test count did not rise |
| Ship a figure whose SVG passes every gate but is XML-invalid | See render-verify § SVG XML invalid. Inline HTML is lenient; <img> is strict |
Related skills
print-svg-style-guide— how figures LOOK. Governs the SVG this skill emits.chart-big-idea— Step 0.5 (earn-a-figure) and Step 4.5 (focus discipline) run BEFORE this skill fires. Decides whether the figure should exist.flint-chart— exploration tool during figure design. Not the shipping path for print-quality figures.render-verify— post-render check. § SVG XML invalid and § Prose-coupling check fire on the artifacts this skill produces.
Would revise if
- A generated figure drifts from its data or design contract despite passing the prescribed checks
- Dataset inversion fails on a sample whose derivation is correct, or the regenerated geometry fails the byte-identity check
- A manual SVG correction cannot be reproduced by the generator
The generator pattern, contract-test discipline, and dataset-inversion procedure are book-tested across 53 shipped figures.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

