Charts

DrumKit ships no chart component — just what recharts asks for: legend, tooltip, active dot, and tick thinning. The plot itself is composed at the call site.

Demo

The five families, browsable the way the Figma sets present them: pick a chart, flip its variant axes, copy the recipe.

Component
legend
marker
import { Area, CartesianGrid, ComposedChart, Legend, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import { ChartActiveDot, ChartLegendContent, ChartTooltipContent, selectEvenlySpacedItems } from "@/components/application/charts/charts-base"; // data: { month, desktop, mobile, tablet }[] — 36 months, so the // sampler thins the axis to seven ticks. The wash under the lead // series is a <linearGradient> in <defs>, its color at 8% fading to 0. <ResponsiveContainer width="100%" height={240}> <ComposedChart data={data}> <CartesianGrid vertical={false} className="stroke-border-secondary" /> <XAxis dataKey="month" tickLine={false} axisLine={false} ticks={selectEvenlySpacedItems(months, 7)} interval={0} padding={{ right: 28 }} // keeps the centered end label inside the svg className="text-tertiary" tick={{ className: "fill-current text-xs" }} /> <YAxis width={40} tickLine={false} axisLine={false} className="text-tertiary" tick={{ className: "fill-current text-xs" }} /> <Tooltip isAnimationActive={false} cursor={{ className: "stroke-utility-brand-600", strokeWidth: 2, strokeLinecap: "round" }} // the sheet's marker cursor content={<ChartTooltipContent />} /> <Legend verticalAlign="top" align="right" content={<ChartLegendContent align="right" className="pb-4" />} /> <Area dataKey="desktop" name="Desktop" className="text-utility-brand-600" stroke="currentColor" strokeWidth={2} fill="url(#wash)" dot={false} isAnimationActive={false} activeDot={(dot) => <ChartActiveDot cx={dot.cx} cy={dot.cy} />} /> {/* Mobile and Tablet: the same Line at brand-400 and brand-700 */} </ComposedChart> </ResponsiveContainer>

The variant axes are the sheet’s own

Each family’s controls mirror its Figma set: legend placement and axis titles on the line and bar frames — plus the marker set’s three types on the line, pinned open in the preview so flipping the type shows without a hover — size and legend on the pie and gauge, legend placement on the radar. One axis is deliberately missing — the sheet’s Desktop and Mobile breakpoints are real viewport behavior (fewer ticks through the sampler, a bottom legend), which the preview route shows honestly and a fixed-width well cannot. The copied snippet changes with the variant; the full frames live on the Code tab.

Contextual demo

A legend and a tooltip only make sense attached to a chart. The preview puts the family in an analytics panel.

The family

Four exports from one file: three recharts content components and a pure helper. There is deliberately no Chart wrapper.

The components in this family, with their descriptions and import paths
ComponentDescriptionImport from
ChartLegendContentThe legend’s markup: a ul of rows, each an 8px swatch and a text-sm label. Passed to recharts’ Legend as content; horizontal and vertical layouts, three alignments, and an optional reversed order.@/​components/​application/​charts/​charts-base
ChartTooltipContentThe hover bubble’s markup: a near-black card with a semibold title and either one supporting line or one row per series. Passed to recharts’ Tooltip as content.@/​components/​application/​charts/​charts-base
ChartActiveDotThe marker recharts draws on the hovered point: a 12px ring in utility-brand-600 over a bg-primary core. Passed to a Line or Area as activeDot.@/​components/​application/​charts/​charts-base
selectEvenlySpacedItemsA pure helper, not a component: picks a fixed number of evenly spaced items from an array, with the last item pinned. Used to thin a dense x-axis down to the ticks the sheet draws.@/​components/​application/​charts/​charts-base

The five families, composed

Every chart the sheet draws, built from recharts primitives with the kit’s content in their slots. Nothing new ships — these live in this page’s config.

The line frame: three years of monthly data on the brand ramp, the lead series washed from 8% to 0, and 36 labels thinned to seven by the sampler
The bar frame: 32px stacked bars with every segment’s top rounded 6px — brand-700 under brand-500 under interface-200
The pie at md: a hole half the outer diameter, six slices down the ramp, the vertical legend beside it

Active users

8,270

The activity gauge at md: 16px round-capped rings on an interface-100 track — centerlines 224, 184, 144 in a 240px box
The radar: a polygon web, three 2px round-joined series filled at 20%, and the sheet’s value pills up the top axis — drawn at 450px by direction over the sheet’s 392×372 frame

Still no component

None of these charts is an import. Each is recharts composed in this page’s config.tsx, with the kit’s ChartLegendContent, ChartTooltipContent, and ChartActiveDot in their slots — which is the recipe the Code tab’s examples carry. The values they are built to — the ring diameters, the 50% hole, the 6px bar radius, the 20% radar fill, the 8%-to-0 wash — were read from the sheet’s exported art on 2026-08-27.

Chart markers

The sheet’s three hover treatments on one series, each pinned open on Jan 10 the way the set draws them. Hover to move the marker — the cursor, the dot, and the bubble’s second line are what change.

Line: the kit’s ChartActiveDot on a solid 2px brand cursor; the bubble draws the value over the date
Dash: a 6px solid dot composed at the call site, on the sheet’s 0.1/6 round-capped dotted cursor
Minimal: no dot, and an empty labelFormatter drops the date — the bubble is the value alone

Only the Line type ships

ChartActiveDot is the sheet’s Line marker — the ⌀12 ring over the white core. Dash and Minimal have no kit counterpart: the solid dot is a one-element component in this page’s config, and both cursors are recharts’ own cursor prop on the Tooltip — a 2px round-capped stroke-utility-brand-600 line, with strokeDasharray="0.1 6" supplying the Dash type’s dots. Read from the sheet’s exported cursor strokes.

formatter and labelFormatter split the Minimal bubble

On a single-entry payload the formatter branch takes the title, so formatter keeps the value while labelFormatter is left formatting only the supporting line — and returning an empty string from it removes the date without touching the value. Passing only labelFormatter={() => ""} would blank the title too, because without a formatter the title falls through to labelFormatter. Read from the source’s ternary chain, and the reason the Minimal cell passes both.

The tooltip

Two drawn layouts, chosen by how many entries the payload holds. Rendered on a mock payload with active set by hand.

Dec 2025

Desktop: 5,310

Mobile: 2,960

Two entries: the label is the title, one “name: value” row per series

8,270

Dec 2025

One entry: the value becomes the title and the label drops to the supporting line

2,960

Mobile web, unauthenticated sessions in the EU region

A long supporting line: the text stack is capped at max-w-74 (296px)

Two edits landed on the bubble on 2026-08-23

Read from the source. The supporting rows went from regular to font-medium, matching the sheet’s xs Medium, and the text stack gained a 296px cap (max-w-74) it did not have at all — an unbounded bubble grew as wide as its longest line. The background, padding, radius, shadow, title tokens, and supporting tokens all matched the sheet already, down to the third shadow layer’s −1px spread.

The bubble is pointer-and-arrow-key only

Read from the source: the tooltip renders a plain div with no role, no aria-live, and no accessible name. recharts’ own accessibility layer makes the chart focusable and moves the active point with the arrow keys, so a keyboard user can reach these values — but nothing announces them when they change. The Accessibility section on the Code tab says what you have to add.

The active dot

The marker recharts draws on the hovered point. A 12px box with the ring inside it, decorative, and now aria-hidden.

Default: outer ⌀12 ring in utility-brand-600 over a bg-primary core of ⌀8
className honored since 2026-08-23: a child selector is what it takes to recolor the ring
On the chart: hover any point above and the same dot appears on both lines

The geometry changed on 2026-08-23

Read from the source: the rect went from 8×8 with rx 6 and a centered 2px stroke — which draws an outer ⌀10 over a ⌀6 core — to 10×10 at x and y 1 with rx 5, so the same 2px stroke draws inside the 12px box and lands on the sheet’s ⌀12 ring over a ⌀8 core. The tokens were already right. The svg is also aria-hidden now, and it honors the className recharts types let you pass; it used to accept the prop and drop it.

Tick sampling

selectEvenlySpacedItems thins a dense axis to a fixed count, walking a fixed stride with the last item pinned.

Jan · Mar · May · Jul · Sep · Nov · Dec

12 months → 7 ticks: the sheet’s mobile 12-month axis, since 2026-08-23

2 · 6 · 10 · 14 · 18 · 22 · 26 · 30

The sheet’s 15-tick desktop 30-day row (every second day, 2 to 30) → 8 ticks: the sheet’s mobile row exactly

1 · 5 · 9 · 13 · 17 · 21 · 25 · 30

30 daily labels → 8 ticks: a long array thinned hard, and every pick distinct

Mon · Tue · Wed · Thu · Fri · Sat · Sun

7 days → 7 ticks: an identity, every label kept

The stride rule, and what changed

Read from the source. With span = length − 1, the helper walks a stride of ceil(span / (count − 1)) — clamped down to floor((span − 1) / (count − 2)) and floored at 1 — for the first count − 1 picks, then pins the array’s last item. Twelve months at seven ticks gives a stride of 2 and draws Jan, Mar, May, Jul, Sep, Nov, Dec — the sheet’s own row (1061:47553). The rounded interpolation it replaced put Aug and Oct in the fifth and sixth slots. The 7-day identity and the sheet’s fifteen-tick 30-day row are unchanged by the edit; thirty daily labels sampled to eight is a different array, and the clamp moved its middle picks from 18, 22, 26 to 17, 21, 25.

Which 30-day array the sheet’s row comes from

Worth stating exactly, because the wrong array gives the wrong answer. The sheet’s desktop 30-day axis draws fifteen ticks — every second day, 2 through 30 — and its mobile axis draws eight: 2, 6, 10, 14, 18, 22, 26, 30. That mobile row is the helper’s output for selectEvenlySpacedItems(desktopTicks, 8) on those fifteen items, which is the second cell above, and it matches to the label. Thirty daily labels sampled to eight is a different question with a different answer (the third cell), so do not check the sheet’s row against it.

Why the stride is clamped

The clamp is what keeps the last item from arriving twice. An unclamped ceil(span / (count − 1)) can walk the strided run all the way onto the final index, and the pin then repeats it — thirty labels at eight ticks did exactly that. Clamping the stride down to floor((span − 1) / (count − 2)) stops the run one index short, so the third cell above returns eight distinct days. Read from the source. One case is inherent rather than fixable: an array shorter than count has too few items to fill the request, so it repeats — four labels at eight ticks ends on five copies of the last one, four extra strided picks plus the pin. Guard short series before sampling.