Sales materials
Charts and data viz
One spec covers charts in an article, on a slide and inside a client PDF. The PNG renderer screenshots the live HTML, so the interactive version and the static one can never diverge in styling.
Source of truth: varick-charts/references/chart-spec.md
The ramp
Five steps from navy through grey, plus the accent as the second series. Monochromatic plus one blue.
Chart 1
chart-1
Chart 2
chart-2
Chart 3
chart-3
Chart 4
chart-4
Chart 5
chart-5
Past five series, interpolate
chart-spec.md is authoritative here, not globals.css. The two disagree on --chart-3/4/5. See known drift.
Picking a chart type
Start from what the reader needs to do with the data, not from what the data looks like.
| The data or intent | Chart type |
|---|---|
| Rank items by one metric, with detail on click | Standings: horizontal bars |
| Compare a few items on one metric | Column bar |
| Compare items on several metrics | Grouped bars |
| Parts summing to 100% across a few rows | Stacked horizontal bars |
| Composition of a few periods, with values and share % | Labelled stacked column |
| Two numeric dimensions plus a size dimension, by category | Bubble scatter |
| Trend of one to three series over an ordered axis | Line |
| A single trend where volume matters | Area, soft fill |
| Distribution or histogram | Column bar |
| Two numeric dimensions: cost against quality | Scatter |
| One category's share of a whole | Donut |
| Several views the reader toggles between | Tabbed dashboard |
Default to standings
Card and grid
The frame around every chart. Fixed, so a page of charts reads as one set.
- Card
- White, 1px #EBEBEB, radius 12, padding 24.
- Card title
- The 10px uppercase label look, or a 14–22px heading at 500. Pick one, not both.
- Card description
- 12px grey, weight 400.
- Grid
- strokeDasharray "4 4", stroke var(--border), horizontal lines only for time and category charts. Both axes for scatter.
- Tooltip
- White, 1px border, radius 8, shadow 0 8px 24px rgba(0,0,0,0.08). Swatch, grey label, value at 500 with tabular-nums. cursor={false} on bar and scatter.
- Tabs
- Hairline-bordered inline row, radius 6. Active is navy fill with white text at 500; inactive is transparent grey. Content switches instantly.
Radii
| Element | Radius | Note |
|---|---|---|
| Card | 8px | n/a |
| Control, segmented | 4px | n/a |
| Bar | 4px | [4,4,0,0] for columns |
| Track | 4px | n/a |
Type
- Card label
- 10px uppercase, 0.04em, 400
- Card heading
- 14–22px, 500
- Axis tick
- 12px, 400
- Tooltip
- 12px
- Standings name
- 14px, 500
- Value
- 13px, 500, tabular-nums
- Legend
- 12px, 400
Type inside a chart
Helvetica Neue, 400 or 500. This is the rule with the fewest exceptions in the whole system. There are none.
* {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-synthesis: none;
}Forbidden inside a chart
For a label that wants to look like code, use uppercase at 0.04em tracking, weight 400, 10px, in --muted-foreground. That is the sanctioned substitute for a mono face.
Motion
Charts enter once and then hold still.
- Bars fill via transform: scaleX() from a left center origin, 800ms.
- Lines and areas draw in via Recharts animationDuration, 900ms.
- Standings fill 280ms on cubic-bezier(0.23,1,0.32,1), staggered 30ms per row.
- Tab switches are instant. Never animate a keyboard-triggered action.
- Under prefers-reduced-motion, standings bars start filled and charts set isAnimationActive={false}.
Gradients
The sanctioned way to make a chart feel less plain without breaking the flat, hairline discipline. They stay inside the navy-to-accent ramp.
Line stroke
A horizontal navy-to-accent stroke gradient across the x-axis, which reads as progression over time, plus a faint fade-down fill. Use an AreaChart so the fill comes for free, and bump strokeWidth to about 2.5 so the gradient has room to read.
Area fill
The standard: a vertical stopOpacity from 0.3 to 0 in one colour.
Gradient-fade bars
Each column is a vertical fade from a saturated accent at the top to near-transparent at the baseline, topped by a small solid navy cap. Pair it with a dashed threshold line carrying a filled navy pill: the “TOP 1%” badge. Implement the bar as a custom Recharts shape so you control both the fade rect and the cap.
h("linearGradient", { id: "barFade", x1:"0", y1:"0", x2:"0", y2:"1" },
h("stop", { offset:"0%", stopColor:"var(--chart-2)", stopOpacity:0.95 }),
h("stop", { offset:"55%", stopColor:"var(--chart-2)", stopOpacity:0.35 }),
h("stop", { offset:"100%", stopColor:"var(--chart-2)", stopOpacity:0.02 }))Every gradient gets defined in a <defs> inside the chart and referenced by url(#id). Never inline a hex into a stop. Use the ramp variables so a palette change propagates.
Delivery
Two formats, and the choice is made by where the chart lands.
| Destination | Format | How |
|---|---|---|
| Blog or article | Self-contained HTML | Charts stay hoverable and toggleable. |
| Client document or slide | PNG | render_png.py --scale 2 for screen, 3 for print. |
| LinkedIn carousel slide | PNG at scale 3 | It gets rasterised into a PDF and re-compressed, so it needs the headroom. |
| A machine with no network | Inlined HTML | Inline the three UMD bundles, keep the app in React.createElement form so nothing needs unsafe-eval. |
python scripts/render_png.py chart.html \
--selector "#capture" \
--scale 3 \
--width 720 \
--wait 1400The renderer waits for the mount animation and for fonts to settle, then screenshots the #capture element. Pass --transparent for the card only, without the page ground behind it.
