Chart dashboard
Turn whatever information the user gives — a table, pasted numbers, a set of
metrics, notes, or just a topic and some facts — into a single self-contained
HTML page of SVG charts rendered with charts-lib: a dashboard, a report, or a
slide deck.
Workflow
Extract the data. Pull every number, category, and time series out of the
user's input into a short plan: for each planned panel note title, chart
type, categories, series. If the user gave a topic with no numbers, say
plainly that figures are illustrative and label them as such on the page.
Never silently invent numbers that read as real measurements.
Three things count as invention, and the last two are easy to miss:
- Filling a gap. A missing week is a gap (
null), not a zero — a zero
draws a collapse that never happened. And a gap only reads as one if the
series is unsmoothed: Charts.line defaults to spline, which drops nulls
and draws an unbroken curve through the hole. Set type: 'line' on any
series with an interior gap.
- Estimating onto a real chart. If you interpolate or model a value, it
does not belong as another point on the primary trend, however carefully you
dash the line or footnote it. Readers remember the shape, not the caveat.
Put estimates in their own panel, or leave the hole visible.
- Rescaling stale numbers. When you're updating an existing page and the
user gave you new figures for only some panels, label the rest as carried
forward. Nudging last quarter's numbers so they look current is fabrication
even though every individual figure came from somewhere real.
Pick the format by what the data has to say (see references/layout.md).
The question is whether the page states a conclusion or lets the reader draw
their own:
- Dashboard — a monitoring surface. Panels stand on their own, the reader
scans for what changed, and no prose tells them what to think. Use
templates/dashboard.html. This is the right default when the user hands
you metrics without an argument attached.
- Report — an argument with evidence. Reach for it when the user is trying
to convince someone ("write up", "for the board", "retrospective",
"analysis"), or when they told you the conclusion themselves and the page
exists to support it. Use
templates/report.html.
- Deck — an argument delivered by someone, one claim per slide, read
from across a room or clicked through in a tab. Reach for it when the user
says presentation, slides, deck, "present this", "walk them through it", or
names a meeting the page has to survive. Use
templates/slides.html.
When it's genuinely ambiguous, ask yourself who reads it and whether you will
be in the room. Nobody presents a bento grid to a board, and nobody watches a
five-section narrative to see if last night's numbers moved. The deck is the
one format that assumes a presenter: if the page has to stand alone with no
one narrating, it is a report, however much the user said "slides".
Copy the template. It lives in this skill's own directory — resolve
templates/ relative to the directory containing this SKILL.md, never from a
hard-coded home path:
<skill-dir>/templates/dashboard.html → ./index.html
<skill-dir>/templates/report.html → ./index.html
<skill-dir>/templates/slides.html → ./index.html
Do not copy assets/charts-lib/ next to the output. The template's three
charts-lib/… tags are placeholders; leave them exactly as written while you
build the page, and fold the library in as the last step (step 9). Their
order matters and the inliner preserves it — theme must load before charts.
Derive the structure from the findings, not from the template. The
dashboard template deliberately ships with a placeholder two-cell grid,
because any arrangement shipped there would end up on every page this skill
produces. (Building a deck? The same rule, applied to slide order rather
than grid cells: references/layout.md § Deck. The template shows one
example of each of its eighteen layouts so the markup is visible in one
place — that sequence is a catalogue, never a running order. Delete what the
argument does not need.)
Before writing markup, answer: what is the dominant shape of this analysis
(one trend / a head-to-head comparison / a ranking / a funnel / a
distribution / parallel equal measures / geography)? Is there genuinely one
panel that is the reason the page exists? The answers pick the opening row —
worked derivations for each shape are in references/layout.md § Compose the
grid from the findings.
Two checks before you move on. A hero must be earned: w8 h2 goes to a
panel only when one finding dominates; co-equal measures get equal cells, and
promoting one is a claim the data doesn't make. And if your top row came out
as a wide line chart plus a donut plus a small panel, verify that it came from
the data — that is the shape this skill falls into by reflex, and it is
right only when a single trend really does lead and composition really is the
second thing the reader needs.
Choose a chart per panel using references/chart-selection.md, then write
the config against references/chart-api.md (the full charts-lib API: every
factory, option, and theme token). Read that file before writing chart code —
don't guess option names.
For a single fact about one engine — what it refuses, whether it self-sizes,
how many grid tracks it wants, its minimum readable size — read
assets/charts-lib/charts.manifest.json instead of the whole API file. It is
the same index the library carries as Charts.meta, kept in step with the
code by the build, and it also holds the shared plotBox and grid rules.
Check the chart's input contract first (chart-selection.md § Input
contract). Each engine accepts a particular kind of x and y, and a mismatch
is a broken panel rather than a style choice. The one that bites most often:
a line chart needs an ordered x — dates, numbers, or labels that carry
their own rising sequence ('Jan'…'Dec', 'Q1'…'Q4', 'Week 1'…'Week 12').
Named categories (regions, browsers, departments) render an error panel
instead of a chart; use Charts.column when x is a name.
Two more references come in at this step when they apply: write the panel
titles against references/narrative.md (where a finding goes, and how to
state it without editorializing), and if the page has a filter or dropdown,
read references/controls.md before wiring it.
When a panel marks something up — an intervention, a projection, a
target, a labelled anomaly — read references/annotation.md for the cue and
its mechanics. Colour and stroke are per-series on a line, so actual-vs-
forecast is two series, not one styled midway.
If the user pointed at a brand — their site, a stylesheet, a screenshot, a
set of hex codes — recolor to match, and change nothing else. Read
references/theming.md and run the bundled extractor:
node <skill-dir>/scripts/extract-theme.js <their-css-or-html>
It harvests the design's canvas, series hue, and any color reserved for a
utility role, then runs the same OKLCH recipe described in theming.md:
paper, greyscale ink, a seven-step series ramp, and accent/annotation/
counter — taken from the design where it has a color that fits the role,
derived by hue rotation where it doesn't. Apply its Charts.applyPalette
block once, before the first chart call — hand it the palette and let
theme.js re-derive the roles; assigning roles one by one leaves tiles,
tooltips and dimmed legend keys on the old colours. Fix anything it marks FAIL rather than
shipping it. Check the series hue it picked against what you know the
brand's colour to be: the script ranks by how the CSS uses a colour, which
usually finds the brand colour and occasionally promotes a heavily-used
secondary instead. The report also names the design's typeface — reported,
not applied, unless the page can genuinely load the face (see § One design
system, only the colors change).
If all you have is one brand color — a single hex, no CSS to harvest —
run the same recipe with nothing observed:
node <skill-dir>/scripts/generate-theme.js '#2323FF'
Verify before reporting done. The page still has its charts-lib/…
placeholders at this point, so to run it you need the library beside it
temporarily. Stage it — step 8 removes it again:
node <skill-dir>/scripts/finalize.js index.html --stage
Then use the strongest check your environment supports:
If you built a deck, check it on paper too. It is made to be handed
round as a PDF, and that path has failures the screen never shows: print to
PDF (or open the print preview) and confirm one slide per sheet, nothing
crossing a page edge, and the dark slides still dark. A slide whose content
outgrew its frame is silently cropped there rather than scrolled.
If the page has any control, test it. Change each dropdown to a
non-default value and confirm — with a screenshot or by reading the rendered
text back — that the affected charts redraw and that any action title
recomputed with them. An untested filter is usually a broken filter.
Fold the library into the page, and ship one file. A dashboard outlives
the folder it was written in — it gets emailed, dropped in Slack, committed
to a wiki, opened from Downloads. A page that loads charts-lib/charts.js
from a sibling folder renders as an empty grid the moment it travels alone,
and it fails silently: the markup, the headings and the KPI numbers are all
there, so it looks like a styling bug rather than a missing dependency. So
the last build step replaces the three placeholder tags with the library's
own contents. One command does the whole ending — checks the page as built,
inlines the library, removes the staged copy, then re-checks with --final,
which this time insists the page is standalone:
node <skill-dir>/scripts/finalize.js index.html
It stops before inlining if the build checks fail, since inlining a broken
page only makes it a bigger broken page, and its exit code is the gate: a
non-zero exit means you are not done. It removes a sibling charts-lib/
only when that folder holds exactly the three files it staged, so a folder
of your own that happens to share the name survives.
The result is one ~590 KB HTML file that opens over file:// with no server,
no network, and no sibling folder. Some rules that follow from that:
- Never reintroduce a
<script src> or <link href> to anything. No CDN
for a chart library, a font, an icon set, or a CSS reset — an offline
reader, a locked-down laptop, or an air-gapped review gets a broken page.
Web fonts are the common slip: name the family in the CSS stack and let it
fall back to the system face, which is what the template already does.
- Images go in as
data: URIs or not at all.
- The library is inlined verbatim. Don't minify it, don't strip the parts
you think a page doesn't use, and don't hand-edit the inlined copy — a
bug fixed in the page instead of in
assets/charts-lib/ is lost on the
next build. Rerunning finalize.js on an already-inlined file is a
harmless no-op, so it is safe to re-run after edits.
- If the user explicitly asks for the split form — they're checking the
page into a repo beside other pages that share the library, say — skip
finalize.js and keep the staged charts-lib/ next to the output, with
the placeholder tags as the real references. Check that page with
scripts/check-page.js index.html (no --final, which would fail it for
the references it is supposed to have). That is the exception, not the
default.
Rules that keep output good
- One idea per panel. A panel whose title needs "and" is two panels.
- Lead with the finding that matters most: if one trend is the reason the page
exists, give it the wide top-left cell (
w8 h2). If nothing dominates — three
equally important measures, say — don't manufacture a hero; equal panels are
the honest layout. The rest of the grid follows the same logic: the shape of
the analysis picks the rows, and a layout reused from the last page is a layout
that describes the last page's data (references/layout.md).
- Every panel gets a
title and a subtitle that states units and scope
("USD thousands · Q4 2025"). Put units in yAxis.suffix and
tooltip.valueSuffix too.
- A projection is never drawn in the same stroke as a measurement: dash the
forecast (or
scenario:'forecast' on bars) and name the notation in the
subtitle. See references/annotation.md.
- Match the chart to what the data is, not to what looks good: a line only
where x is time or a number, a donut/waffle only where the parts are
non-negative and sum to one whole, a scatter only where both axes are
measures. See
chart-selection.md § Input contract.
- Order categorical bars by value, not alphabetically. Keep time on the x-axis
left-to-right.
- Donuts stop being readable somewhere around six wedges — below a few percent
the angles are indistinguishable and the reader is just reading the legend.
Roll the tail into "Other", or use a ranked bar list if the tail is the point.
- Don't restate a series in two panels unless the second adds a new cut.
- Annotate what matters:
callouts: [{ x, text }] on line charts for spikes,
launches, and anomalies mentioned by the user.
How many charts? One per finding — no quota, no padding
The page is not a container to fill up. Each panel should answer a question the
reader actually has, and the count falls out of the data rather than out of a
target. Ask of every panel: what would the reader do differently after seeing
this? If the answer is nothing, it isn't a panel.
That cuts both ways, and both failures are common:
- Padding. Given four numbers from an A/B test, the honest page is two or
three panels and a plain statement of the lift. Filling a twelve-cell grid
means inventing a donut of two nearly-identical sample sizes, a fabricated
daily time series, a "by segment" split nobody measured. The moment you are
reaching for something to chart, you have run past the end of the data — stop
there. A small page that answers the question is a better deliverable than a
full grid that pads it, even though the full grid looks more impressive at a
glance.
- Compression. Given twenty measures that each carry a finding, don't force
them into eight panels by stacking unrelated series onto shared axes. Let the
page be long.
When there are only a few panels, widen them (w6/w8/w12) so the grid still
reads as a designed page rather than a half-empty one — a two-panel dashboard is
two big panels, not two small panels marooned top-left.
Reports work the same way: a figure exists because a claim in the prose needs
evidence. A section that states no claim needs no chart, and a claim the reader
will accept without proof doesn't need one either.
Fit the page to how it will be read
The dashboard and report templates are tuned for someone reading at a desk.
When the user tells you otherwise — "I'm presenting this", "send it round",
"print it" — the same page fails badly in that other context. Presenting is the
case with its own template: a deck (templates/slides.html) is the right answer
to "I'm presenting this", not a dashboard with the type scaled up. For the other
two the fix is how much you put on the page and at what size, never a different
design system. All three cases are in references/layout.md § Fit the page to
how it will be read.
Make the chart show the finding, not just the data
Three defaults in charts-lib are deliberately plain, and taking them as-is is
how a page ends up technically correct and useless. Override them on purpose:
Emphasis. Read the title you just wrote. If it names specific categories,
a specific series, or a specific moment — "the top two account for 90%",
"Direct outperformed Partner", "the drop came after the March update" — the
chart leaves categorical mode and enters emphasis mode: the subject takes the
accent, everything else takes Charts.theme.muted. A finding stated in words
above eight identical bars is a finding the reader has to re-derive.
The mechanism differs by what the subject is — per-point color for
categories, series color + lineWidth for one line among many, plotBands
and callouts for a moment, a tinted centerText for a donut's focal wedge.
All of them, plus the grouped-chart series-vs-cluster split, are in
references/chart-selection.md § Emphasis. Three constraints hold across all
of them: two colors, not a rainbow (multi-hue and an accent reads as no
emphasis at all), at most 2–3 accented items (past that, go back to the
full categorical palette), and muted still has to be readable — context
bars are data too, which is why muted is derived at 3:1 against the canvas
rather than picked for how quiet it looks.
In a deck, emphasis mode is the exception and the plain ramp is the
default. Not a different rule — the same one, biting harder: a deck is where
the urge to make the point is strongest, and a grey supporting bar stops
saying anything by the third slide that has one. Write no color unless the
chart's own title names the points it means.
Three standing rules that apply whether or not the chart is in emphasis mode:
residual categories ("Other", "Don't know", a rolled-up tail) always take
muted, since they can never be the finding; several context series take one
identical mute rather than a ramp, so they read as a single band; and one
emphasis per chart — an accented bar and a callout on that same bar is two
competing signals, not double the emphasis.
Status vs. emphasis. When a chart mixes measured, planned, and projected
numbers, don't spend a palette color on the distinction — encode it in the
fill with scenario: 'plan' | 'forecast' (outlined / hatched) and keep color
for the finding. A projection drawn identically to a measurement is the same
failure as inventing the number.
Data labels. The library draws them in every chart type by default. Opt
out with dataLabels: false where they'd collide — dense lines, many bars,
grouped columns with 3+ series. See references/chart-selection.md § Data labels.
Geofacet variant. 'bar' is what you get by typing nothing, which is not
a reason to use it three times on one page. 'heat' when the spatial pattern
is the point, 'gauge' when regions are measured against a shared target.
See references/chart-api.md § Geofacet.
If you add a control, wire it
A dropdown that doesn't change the charts is worse than no dropdown: it reads as
a broken page, and the reader stops trusting the numbers that are correct. So
either add no controls at all — a static page is a perfectly good deliverable —
or wire them completely, which means the data is filtered rather than the label,
every dependent panel re-renders, and any action title recomputes from the same
filtered rows. If the page has a control, read references/controls.md: it
has the render(state) pattern in full, the guards to apply before shipping
one, and the cases where small multiples beat a filter.
Legends go in one place
charts-lib puts the legend at the top, under the subtitle, and shows it
automatically once a chart has two or more series or wedges. Leave it there. A
reader scanning a grid of panels learns the legend's location once; a page where
it sits above one chart, beside another and below a third makes them re-hunt for
it every time, and that hunting is the entire cost of an inconsistent layout.
So: don't pass legend position options per chart, don't build legends in HTML
next to the chart, and don't hand-place colored dots in a panel's corner. If a
legend genuinely doesn't earn its space — single-series panels, or a donut whose
wedges are already labelled by callouts — turn it off with
legend: { enabled: false } for every panel in that situation, not just the
cramped one. The only sanctioned alternative is charts-lib's own
lineLabels: 'inline', and if you use it on one line chart, use it on all of
them.
Put the finding in the title
A chart titled "Weekly throughput by site" makes the reader do the work of
finding the point. A chart titled "Throughput fell 12% the week of the WMS
cutover" hands it to them and then proves it underneath — the insight lives in
the chart's own hierarchy, so it travels with the figure into a screenshot, a
slide, or an email. The finding goes in title, the units and scope stay in
subtitle.
The line that keeps this from becoming editorializing is whether the chart
proves the sentence: "Billing drives 27% of all tickets" is measurable off the
chart, "Billing is a serious problem" is a verdict the reader should reach
themselves. When no single finding dominates, a plain descriptive title is the
honest choice — don't manufacture a headline.
Read references/narrative.md before writing the titles. It has the
write-this/not-this table, the length budget per cell, and the three-way choice
between an action title, Charts.barInsightTable, and a soft surface card —
including the rule that stops the same sentence appearing in two of them.
Nothing on the page that isn't data or its labels
- No invented narrative furniture that repeats what a chart already says: no
"Key insight" banners, no "Executive summary" you wrote yourself, no
highlighted takeaway strip across the top, no emoji, no "🚀".
- The header is title, one line of scope, and the reporting window. The footer is
sources, definitions, and any honesty notes (illustrative figures, carried-
forward panels, data-quality caveats). Nothing else belongs in either.
- Conclusions the user themselves stated ("the March spike is the thing I need to
explain") belong on the relevant chart — as its title or a callout, in their
framing — not restated as your own analysis in a banner.
One design system, only the colors change
Every visible component follows charts-lib's design language: its type scale,
weights, spacing rhythm, stroke widths, hairlines, legend position and chart
geometry. Those proportions are what make ten different chart types read as one
family, and the page chrome inherits them so the cards don't look bolted on.
The colors are the exception, and the only exception. When a user supplies a
brand, recolor via Charts.applyPalette — once, before the first factory call,
never per chart — and let the page chrome pick those same values up from the sync
block in the template. Corner radius may follow the brand too, since square vs.
rounded is a brand signature the charts themselves don't express.
Do not introduce a second visual system on top: no custom card headers with
their own type scale, no gradient hero panels, no shadows or borders the template
doesn't already have.
Type is the one place where copying the brand usually backfires. Most brand faces
are licensed webfonts you cannot load into a local file, and naming one in
font-family just falls through to a system fallback you didn't choose — worse
than keeping charts-lib's stack, which was picked to work at 11px in a chart. In
a chart the cost is not only aesthetic: the engines measure label widths against
the face they think they have, so a substituted one makes axis labels the engine
had fitted collide. And a page that reaches for a webfont stops being standalone
(step 8). Match the brand's font only when the face is genuinely available — a
system font, or a file the user supplied. extract-theme.js reports the
design's face and says outright whether it is loadable; when it isn't, keep the
template stack and tell the user which face you couldn't use and why. Full
method in references/theming.md.
The page has exactly five kinds of component, all already in the template:
header, optional KPI row, chart panels, optional soft surface
card (.note), footer. The KPI row
exists because headline figures genuinely help — use the template's .kpi
markup, which is sized off the chart type scale so the tiles look like they
belong to the same page. Writing your own KPI strip with new CSS is the most
common way this page ends up looking like two designs stapled together, and it
is the thing to resist even though it feels helpful. A KPI tile is a label, a
number, and at most one line of plain context — no arrows, no red/green verdicts,
no "▲ 12% vs LY" badges.
If you find yourself writing new CSS classes, stop and ask whether a chart panel
would carry the information better. Usually it would.
Output
One HTML file, standalone — no sibling charts-lib/ folder, no CDN tags, no
network at open time (see step 8). A deck ships the same way, and a reader turns
it into a PDF with their browser's own Print → Save as PDF: the template sets
A4 landscape, one slide per sheet. Write it to the working directory (or where
the user asked). Then surface
it however your environment does that — attach or render the file if you can (in
Claude Code: SendUserFile with display: "render"); otherwise print the
absolute path and tell the user to open it in a browser. Either way, state which
figures came from the user's data and which, if any, were illustrative.
Environment notes
Nothing in this skill requires a specific agent or vendor. It needs only the
ability to read files from this directory, write an HTML file, copy a folder,
and run Node (for finalize.js and the static checks). Without Node, inline
the three library files by hand — paste charts.css into a <style> and
theme.js then charts.js into <script> blocks, in that order, replacing the
placeholder tags. Browser preview, screenshots, and file attachment are used
when available and degrade gracefully when not.
1---2name: chart-dashboard3description: Build a self-contained HTML dashboard, data-story report, or slide deck from supplied information (metrics, tables, notes, pasted data, a topic), rendered with the bundled zero-dependency charts-lib SVG chart library. Use whenever the user asks for a dashboard, analytics page, KPI/bento view, illustrated report, or a presentation, slides or a deck built from data they provide or describe.4---56# Chart dashboard78Turn whatever information the user gives — a table, pasted numbers, a set of9metrics, notes, or just a topic and some facts — into a single self-contained10HTML page of SVG charts rendered with `charts-lib`: a dashboard, a report, or a11slide deck.1213## Workflow14151. **Extract the data.** Pull every number, category, and time series out of the16 user's input into a short plan: for each planned panel note *title, chart17 type, categories, series*. If the user gave a topic with no numbers, say18 plainly that figures are illustrative and label them as such on the page.19 Never silently invent numbers that read as real measurements.2021 Three things count as invention, and the last two are easy to miss:22 - **Filling a gap.** A missing week is a gap (`null`), not a zero — a zero23 draws a collapse that never happened. And a gap only reads as one if the24 series is unsmoothed: `Charts.line` defaults to `spline`, which drops nulls25 and draws an unbroken curve through the hole. Set `type: 'line'` on any26 series with an interior gap.27 - **Estimating onto a real chart.** If you interpolate or model a value, it28 does not belong as another point on the primary trend, however carefully you29 dash the line or footnote it. Readers remember the shape, not the caveat.30 Put estimates in their own panel, or leave the hole visible.31 - **Rescaling stale numbers.** When you're updating an existing page and the32 user gave you new figures for only some panels, label the rest as carried33 forward. Nudging last quarter's numbers so they look current is fabrication34 even though every individual figure came from somewhere real.352. **Pick the format by what the data has to say** (see `references/layout.md`).36 The question is whether the page states a conclusion or lets the reader draw37 their own:38 - **Dashboard** — a monitoring surface. Panels stand on their own, the reader39 scans for what changed, and no prose tells them what to think. Use40 `templates/dashboard.html`. This is the right default when the user hands41 you metrics without an argument attached.42 - **Report** — an argument with evidence. Reach for it when the user is trying43 to convince someone ("write up", "for the board", "retrospective",44 "analysis"), or when they told you the conclusion themselves and the page45 exists to support it. Use `templates/report.html`.46 - **Deck** — an argument delivered *by someone*, one claim per slide, read47 from across a room or clicked through in a tab. Reach for it when the user48 says presentation, slides, deck, "present this", "walk them through it", or49 names a meeting the page has to survive. Use `templates/slides.html`.5051 When it's genuinely ambiguous, ask yourself who reads it and whether you will52 be in the room. Nobody presents a bento grid to a board, and nobody watches a53 five-section narrative to see if last night's numbers moved. The deck is the54 one format that assumes a presenter: if the page has to stand alone with no55 one narrating, it is a report, however much the user said "slides".563. **Copy the template.** It lives in this skill's own directory — resolve57 `templates/` relative to the directory containing this SKILL.md, never from a58 hard-coded home path:59 ```60 <skill-dir>/templates/dashboard.html → ./index.html61 <skill-dir>/templates/report.html → ./index.html62 <skill-dir>/templates/slides.html → ./index.html63 ```64 Do **not** copy `assets/charts-lib/` next to the output. The template's three65 `charts-lib/…` tags are placeholders; leave them exactly as written while you66 build the page, and fold the library in as the last step (step 9). Their67 order matters and the inliner preserves it — theme must load before charts.684. **Derive the structure from the findings, not from the template.** The69 dashboard template deliberately ships with a placeholder two-cell grid,70 because any arrangement shipped there would end up on every page this skill71 produces. *(Building a deck? The same rule, applied to slide order rather72 than grid cells: `references/layout.md` § Deck. The template shows one73 example of each of its eighteen layouts so the markup is visible in one74 place — that sequence is a catalogue, never a running order. Delete what the75 argument does not need.)*76 Before writing markup, answer: what is the dominant shape of this analysis77 (one trend / a head-to-head comparison / a ranking / a funnel / a78 distribution / parallel equal measures / geography)? Is there genuinely one79 panel that is the reason the page exists? The answers pick the opening row —80 worked derivations for each shape are in `references/layout.md` § Compose the81 grid from the findings.8283 Two checks before you move on. **A hero must be earned**: `w8 h2` goes to a84 panel only when one finding dominates; co-equal measures get equal cells, and85 promoting one is a claim the data doesn't make. And **if your top row came out86 as a wide line chart plus a donut plus a small panel, verify that it came from87 the data** — that is the shape this skill falls into by reflex, and it is88 right only when a single trend really does lead and composition really is the89 second thing the reader needs.90915. **Choose a chart per panel** using `references/chart-selection.md`, then write92 the config against `references/chart-api.md` (the full charts-lib API: every93 factory, option, and theme token). Read that file before writing chart code —94 don't guess option names.9596 For a single fact about one engine — what it refuses, whether it self-sizes,97 how many grid tracks it wants, its minimum readable size — read98 `assets/charts-lib/charts.manifest.json` instead of the whole API file. It is99 the same index the library carries as `Charts.meta`, kept in step with the100 code by the build, and it also holds the shared `plotBox` and `grid` rules.101102 **Check the chart's input contract first** (`chart-selection.md` § Input103 contract). Each engine accepts a particular kind of x and y, and a mismatch104 is a broken panel rather than a style choice. The one that bites most often:105 a line chart needs an *ordered* x — dates, numbers, or labels that carry106 their own rising sequence (`'Jan'…'Dec'`, `'Q1'…'Q4'`, `'Week 1'…'Week 12'`).107 Named categories (regions, browsers, departments) render an error panel108 instead of a chart; use `Charts.column` when x is a name.109110 Two more references come in at this step when they apply: write the panel111 titles against `references/narrative.md` (where a finding goes, and how to112 state it without editorializing), and if the page has a filter or dropdown,113 read `references/controls.md` before wiring it.114115 **When a panel marks something up** — an intervention, a projection, a116 target, a labelled anomaly — read `references/annotation.md` for the cue and117 its mechanics. Colour and stroke are per-series on a line, so actual-vs-118 forecast is two series, not one styled midway.1196. **If the user pointed at a brand** — their site, a stylesheet, a screenshot, a120 set of hex codes — recolor to match, and change nothing else. Read121 `references/theming.md` and run the bundled extractor:122 ```bash123 node <skill-dir>/scripts/extract-theme.js <their-css-or-html>124 ```125 It harvests the design's canvas, series hue, and any color reserved for a126 utility role, then runs the same OKLCH recipe described in `theming.md`:127 paper, greyscale ink, a seven-step series ramp, and `accent`/`annotation`/128 `counter` — taken from the design where it has a color that fits the role,129 derived by hue rotation where it doesn't. Apply its `Charts.applyPalette`130 block once, before the first chart call — hand it the palette and let131 `theme.js` re-derive the roles; assigning roles one by one leaves tiles,132 tooltips and dimmed legend keys on the old colours. Fix anything it marks FAIL rather than133 shipping it. **Check the series hue it picked** against what you know the134 brand's colour to be: the script ranks by how the CSS uses a colour, which135 usually finds the brand colour and occasionally promotes a heavily-used136 secondary instead. The report also names the design's typeface — reported,137 not applied, unless the page can genuinely load the face (see § One design138 system, only the colors change).139140 **If all you have is one brand color** — a single hex, no CSS to harvest —141 run the same recipe with nothing observed:142 ```bash143 node <skill-dir>/scripts/generate-theme.js '#2323FF'144 ```1457. **Verify before reporting done.** The page still has its `charts-lib/…`146 placeholders at this point, so to *run* it you need the library beside it147 temporarily. Stage it — step 8 removes it again:148 ```bash149 node <skill-dir>/scripts/finalize.js index.html --stage150 ```151 Then use the strongest check your environment supports:152 - *Browser tooling available* — open the file, read the console for errors,153 and screenshot it to confirm layout. (In Claude Code: `preview_start`, then154 `read_console_messages` and a screenshot. Serve over a local HTTP server155 rather than `file://` so the scripts execute.)156 - *No browser tooling* — run the bundled checker and fix what it reports:157 ```bash158 node <skill-dir>/scripts/check-page.js index.html159 ```160 It catches the four failures that do not throw and so survive a161 confident-looking build: a panel whose chart was never wired (an empty162 box), a line over unordered categories (an error panel *inside* the163 chart), a page still pointing at `charts-lib/`, and anything else that164 reaches the network. Each one reads as a styling bug rather than the165 missing wiring it is. Exit code is non-zero when something fails, so it166 also works as a gate. Run it here without `--final` — the page is not167 inlined yet, and mid-build that is simply where you are. (Step 8 runs it168 for you either way; running it now just shortens the loop.)169 Either way, fix any panel that renders empty or overflows its cell first. A170 panel reading *"Line charts need a continuous or temporal x-axis"* is the171 input-contract failure above — change the chart type or the x values, don't172 restyle it.173174 **If you built a deck, check it on paper too.** It is made to be handed175 round as a PDF, and that path has failures the screen never shows: print to176 PDF (or open the print preview) and confirm one slide per sheet, nothing177 crossing a page edge, and the dark slides still dark. A slide whose content178 outgrew its frame is silently cropped there rather than scrolled.179180 **If the page has any control, test it.** Change each dropdown to a181 non-default value and confirm — with a screenshot or by reading the rendered182 text back — that the affected charts redraw *and* that any action title183 recomputed with them. An untested filter is usually a broken filter.1848. **Fold the library into the page, and ship one file.** A dashboard outlives185 the folder it was written in — it gets emailed, dropped in Slack, committed186 to a wiki, opened from Downloads. A page that loads `charts-lib/charts.js`187 from a sibling folder renders as an empty grid the moment it travels alone,188 and it fails *silently*: the markup, the headings and the KPI numbers are all189 there, so it looks like a styling bug rather than a missing dependency. So190 the last build step replaces the three placeholder tags with the library's191 own contents. One command does the whole ending — checks the page as built,192 inlines the library, removes the staged copy, then re-checks with `--final`,193 which this time *insists* the page is standalone:194 ```bash195 node <skill-dir>/scripts/finalize.js index.html196 ```197 It stops before inlining if the build checks fail, since inlining a broken198 page only makes it a bigger broken page, and its exit code is the gate: a199 non-zero exit means you are not done. It removes a sibling `charts-lib/`200 only when that folder holds exactly the three files it staged, so a folder201 of your own that happens to share the name survives.202 The result is one ~590 KB HTML file that opens over `file://` with no server,203 no network, and no sibling folder. Some rules that follow from that:204 - **Never reintroduce a `<script src>` or `<link href>` to anything.** No CDN205 for a chart library, a font, an icon set, or a CSS reset — an offline206 reader, a locked-down laptop, or an air-gapped review gets a broken page.207 Web fonts are the common slip: name the family in the CSS stack and let it208 fall back to the system face, which is what the template already does.209 - **Images go in as `data:` URIs** or not at all.210 - **The library is inlined verbatim.** Don't minify it, don't strip the parts211 you think a page doesn't use, and don't hand-edit the inlined copy — a212 bug fixed in the page instead of in `assets/charts-lib/` is lost on the213 next build. Rerunning `finalize.js` on an already-inlined file is a214 harmless no-op, so it is safe to re-run after edits.215 - **If the user explicitly asks for the split form** — they're checking the216 page into a repo beside other pages that share the library, say — skip217 `finalize.js` and keep the staged `charts-lib/` next to the output, with218 the placeholder tags as the real references. Check that page with219 `scripts/check-page.js index.html` (no `--final`, which would fail it for220 the references it is supposed to have). That is the exception, not the221 default.222223## Rules that keep output good224225- One idea per panel. A panel whose title needs "and" is two panels.226- Lead with the finding that matters most: if one trend is the reason the page227 exists, give it the wide top-left cell (`w8 h2`). If nothing dominates — three228 equally important measures, say — don't manufacture a hero; equal panels are229 the honest layout. The rest of the grid follows the same logic: the shape of230 the analysis picks the rows, and a layout reused from the last page is a layout231 that describes the last page's data (`references/layout.md`).232- Every panel gets a `title` and a `subtitle` that states units and scope233 ("USD thousands · Q4 2025"). Put units in `yAxis.suffix` and234 `tooltip.valueSuffix` too.235- A projection is never drawn in the same stroke as a measurement: dash the236 forecast (or `scenario:'forecast'` on bars) and name the notation in the237 subtitle. See `references/annotation.md`.238- Match the chart to what the data *is*, not to what looks good: a line only239 where x is time or a number, a donut/waffle only where the parts are240 non-negative and sum to one whole, a scatter only where both axes are241 measures. See `chart-selection.md` § Input contract.242- Order categorical bars by value, not alphabetically. Keep time on the x-axis243 left-to-right.244- Donuts stop being readable somewhere around six wedges — below a few percent245 the angles are indistinguishable and the reader is just reading the legend.246 Roll the tail into "Other", or use a ranked bar list if the tail is the point.247- Don't restate a series in two panels unless the second adds a new cut.248- Annotate what matters: `callouts: [{ x, text }]` on line charts for spikes,249 launches, and anomalies mentioned by the user.250251### How many charts? One per finding — no quota, no padding252253The page is not a container to fill up. Each panel should answer a question the254reader actually has, and the count falls out of the data rather than out of a255target. Ask of every panel: *what would the reader do differently after seeing256this?* If the answer is nothing, it isn't a panel.257258That cuts both ways, and both failures are common:259260- **Padding.** Given four numbers from an A/B test, the honest page is two or261 three panels and a plain statement of the lift. Filling a twelve-cell grid262 means inventing a donut of two nearly-identical sample sizes, a fabricated263 daily time series, a "by segment" split nobody measured. The moment you are264 reaching for something to chart, you have run past the end of the data — stop265 there. A small page that answers the question is a better deliverable than a266 full grid that pads it, even though the full grid looks more impressive at a267 glance.268- **Compression.** Given twenty measures that each carry a finding, don't force269 them into eight panels by stacking unrelated series onto shared axes. Let the270 page be long.271272When there are only a few panels, widen them (`w6`/`w8`/`w12`) so the grid still273reads as a designed page rather than a half-empty one — a two-panel dashboard is274two big panels, not two small panels marooned top-left.275276Reports work the same way: a figure exists because a claim in the prose needs277evidence. A section that states no claim needs no chart, and a claim the reader278will accept without proof doesn't need one either.279280### Fit the page to how it will be read281282The dashboard and report templates are tuned for someone reading at a desk.283When the user tells you otherwise — "I'm presenting this", "send it round",284"print it" — the same page fails badly in that other context. Presenting is the285case with its own template: a deck (`templates/slides.html`) is the right answer286to "I'm presenting this", not a dashboard with the type scaled up. For the other287two the fix is how much you put on the page and at what size, never a different288design system. All three cases are in `references/layout.md` § Fit the page to289how it will be read.290291### Make the chart show the finding, not just the data292293Three defaults in charts-lib are deliberately plain, and taking them as-is is294how a page ends up technically correct and useless. Override them on purpose:295296- **Emphasis.** Read the title you just wrote. If it names specific categories,297 a specific series, or a specific moment — "the top two account for 90%",298 "Direct outperformed Partner", "the drop came after the March update" — the299 chart leaves categorical mode and enters emphasis mode: the subject takes the300 accent, everything else takes `Charts.theme.muted`. A finding stated in words301 above eight identical bars is a finding the reader has to re-derive.302303 The mechanism differs by what the subject is — per-point `color` for304 categories, series `color` + `lineWidth` for one line among many, `plotBands`305 and `callouts` for a moment, a tinted `centerText` for a donut's focal wedge.306 All of them, plus the grouped-chart series-vs-cluster split, are in307 `references/chart-selection.md` § Emphasis. Three constraints hold across all308 of them: **two colors, not a rainbow** (multi-hue *and* an accent reads as no309 emphasis at all), **at most 2–3 accented items** (past that, go back to the310 full categorical palette), and **muted still has to be readable** — context311 bars are data too, which is why `muted` is derived at 3:1 against the canvas312 rather than picked for how quiet it looks.313314 **In a deck, emphasis mode is the exception and the plain ramp is the315 default.** Not a different rule — the same one, biting harder: a deck is where316 the urge to make the point is strongest, and a grey supporting bar stops317 saying anything by the third slide that has one. Write no `color` unless the318 chart's own title names the points it means.319320 Three standing rules that apply whether or not the chart is in emphasis mode:321 residual categories ("Other", "Don't know", a rolled-up tail) always take322 `muted`, since they can never be the finding; several context series take one323 identical mute rather than a ramp, so they read as a single band; and one324 emphasis per chart — an accented bar *and* a callout on that same bar is two325 competing signals, not double the emphasis.326- **Status vs. emphasis.** When a chart mixes measured, planned, and projected327 numbers, don't spend a palette color on the distinction — encode it in the328 fill with `scenario: 'plan' | 'forecast'` (outlined / hatched) and keep color329 for the finding. A projection drawn identically to a measurement is the same330 failure as inventing the number.331- **Data labels.** The library draws them in every chart type by default. Opt332 out with `dataLabels: false` where they'd collide — dense lines, many bars,333 grouped columns with 3+ series. See `references/chart-selection.md` § Data labels.334- **Geofacet variant.** `'bar'` is what you get by typing nothing, which is not335 a reason to use it three times on one page. `'heat'` when the spatial pattern336 is the point, `'gauge'` when regions are measured against a shared target.337 See `references/chart-api.md` § Geofacet.338339### If you add a control, wire it340341A dropdown that doesn't change the charts is worse than no dropdown: it reads as342a broken page, and the reader stops trusting the numbers that *are* correct. So343either add no controls at all — a static page is a perfectly good deliverable —344or wire them completely, which means the data is filtered rather than the label,345every dependent panel re-renders, and any action title recomputes from the same346filtered rows. **If the page has a control, read `references/controls.md`**: it347has the `render(state)` pattern in full, the guards to apply before shipping348one, and the cases where small multiples beat a filter.349350### Legends go in one place351352charts-lib puts the legend at the top, under the subtitle, and shows it353automatically once a chart has two or more series or wedges. Leave it there. A354reader scanning a grid of panels learns the legend's location once; a page where355it sits above one chart, beside another and below a third makes them re-hunt for356it every time, and that hunting is the entire cost of an inconsistent layout.357358So: don't pass `legend` position options per chart, don't build legends in HTML359next to the chart, and don't hand-place colored dots in a panel's corner. If a360legend genuinely doesn't earn its space — single-series panels, or a donut whose361wedges are already labelled by callouts — turn it off with362`legend: { enabled: false }` **for every panel in that situation**, not just the363cramped one. The only sanctioned alternative is charts-lib's own364`lineLabels: 'inline'`, and if you use it on one line chart, use it on all of365them.366367### Put the finding in the title368369A chart titled "Weekly throughput by site" makes the reader do the work of370finding the point. A chart titled "Throughput fell 12% the week of the WMS371cutover" hands it to them and then proves it underneath — the insight lives in372the chart's own hierarchy, so it travels with the figure into a screenshot, a373slide, or an email. The finding goes in `title`, the units and scope stay in374`subtitle`.375376The line that keeps this from becoming editorializing is whether the chart377proves the sentence: "Billing drives 27% of all tickets" is measurable off the378chart, "Billing is a serious problem" is a verdict the reader should reach379themselves. When no single finding dominates, a plain descriptive title is the380honest choice — don't manufacture a headline.381382**Read `references/narrative.md` before writing the titles.** It has the383write-this/not-this table, the length budget per cell, and the three-way choice384between an action title, `Charts.barInsightTable`, and a soft surface card —385including the rule that stops the same sentence appearing in two of them.386387### Nothing on the page that isn't data or its labels388389- No invented narrative furniture that repeats what a chart already says: no390 "Key insight" banners, no "Executive summary" you wrote yourself, no391 highlighted takeaway strip across the top, no emoji, no "🚀".392- The header is title, one line of scope, and the reporting window. The footer is393 sources, definitions, and any honesty notes (illustrative figures, carried-394 forward panels, data-quality caveats). Nothing else belongs in either.395- Conclusions the user themselves stated ("the March spike is the thing I need to396 explain") belong on the relevant chart — as its title or a callout, in their397 framing — not restated as your own analysis in a banner.398399### One design system, only the colors change400401Every visible component follows charts-lib's design language: its type scale,402weights, spacing rhythm, stroke widths, hairlines, legend position and chart403geometry. Those proportions are what make ten different chart types read as one404family, and the page chrome inherits them so the cards don't look bolted on.405406The colors are the exception, and the only exception. When a user supplies a407brand, recolor via `Charts.applyPalette` — once, before the first factory call,408never per chart — and let the page chrome pick those same values up from the sync409block in the template. Corner radius may follow the brand too, since square vs.410rounded is a brand signature the charts themselves don't express.411412Do not introduce a second visual system on top: no custom card headers with413their own type scale, no gradient hero panels, no shadows or borders the template414doesn't already have.415416Type is the one place where copying the brand usually backfires. Most brand faces417are licensed webfonts you cannot load into a local file, and naming one in418`font-family` just falls through to a system fallback you didn't choose — worse419than keeping charts-lib's stack, which was picked to work at 11px in a chart. In420a chart the cost is not only aesthetic: the engines measure label widths against421the face they think they have, so a substituted one makes axis labels the engine422had fitted collide. And a page that reaches for a webfont stops being standalone423(step 8). Match the brand's font only when the face is genuinely available — a424system font, or a file the user supplied. `extract-theme.js` reports the425design's face and says outright whether it is loadable; when it isn't, keep the426template stack and tell the user which face you couldn't use and why. Full427method in `references/theming.md`.428429The page has exactly five kinds of component, all already in the template:430**header**, optional **KPI row**, **chart panels**, optional **soft surface431card** (`.note`), **footer**. The KPI row432exists because headline figures genuinely help — use the template's `.kpi`433markup, which is sized off the chart type scale so the tiles look like they434belong to the same page. Writing your own KPI strip with new CSS is the most435common way this page ends up looking like two designs stapled together, and it436is the thing to resist even though it feels helpful. A KPI tile is a label, a437number, and at most one line of plain context — no arrows, no red/green verdicts,438no "▲ 12% vs LY" badges.439440If you find yourself writing new CSS classes, stop and ask whether a chart panel441would carry the information better. Usually it would.442443## Output444445One HTML file, standalone — no sibling `charts-lib/` folder, no CDN tags, no446network at open time (see step 8). A deck ships the same way, and a reader turns447it into a PDF with their browser's own Print → Save as PDF: the template sets448A4 landscape, one slide per sheet. Write it to the working directory (or where449the user asked). Then surface450it however your environment does that — attach or render the file if you can (in451Claude Code: `SendUserFile` with `display: "render"`); otherwise print the452absolute path and tell the user to open it in a browser. Either way, state which453figures came from the user's data and which, if any, were illustrative.454455## Environment notes456457Nothing in this skill requires a specific agent or vendor. It needs only the458ability to read files from this directory, write an HTML file, copy a folder,459and run Node (for `finalize.js` and the static checks). Without Node, inline460the three library files by hand — paste `charts.css` into a `<style>` and461`theme.js` then `charts.js` into `<script>` blocks, in that order, replacing the462placeholder tags. Browser preview, screenshots, and file attachment are used463when available and degrade gracefully when not.