Charts
Facet
The same chart once per group, on one shared scale.
Usage
When to reach for it
Ships from@misoto22/design/charts
import { Facet } from '@misoto22/design/charts'Notes
The same chart repeated once per group, on one shared scale — small multiples.
The answer to the question a multi-series chart stops being able to answer at around five series: eight lines in one plot is a hairball, and the reader spends their attention untangling strands instead of reading them. Splitting the same lines across eight small plots costs the direct overlay comparison and buys back every individual shape, which is almost always the better trade — this is Tufte's small multiple, and Observable Plot and Vega-Lite both treat it as a first-class operation for the same reason.
Reach for a multi-series <LineChart> or <AreaChart> instead when the series really do have to be read AGAINST each other — crossovers, gaps, shares of one total — and when there are few enough of them to tell apart. Reach for this when each group has its own shape worth seeing, when the groups are many, or when the reader's question is "which of these is different".
What it gets right that a hand-rolled data.map() does not: the panels share one domain by default. On independent scales every panel fills its own plot, so a group peaking at 40 and a group peaking at 4,000 come out the same shape — the comparison is not merely lost, it is inverted, and nothing on screen says so. panel.domain is that shared scale; the render function has to pass it to the panel's value axis. It also caps the grid, folds or counts the tail, and prints one legend and one axis label instead of twelve.
Anatomy
| Element | Description |
|---|---|
| Figure framerequired | ChartFigure’s <figure> around the whole grid, named by title. Its empty state has no false escape hatch, unlike a single chart’s: an empty pair of axes is at least a chart, and an empty grid is nothing. |
| Panel gridrequired | A role="list" of <li> panels on a CSS auto-fit track. list-none strips the list role in Safari, so it is set back by hand — a grid of twelve plots that does not announce twelve items takes away the reader’s only cue for how far they have to go. |
| Panel name | A <p> above each plot. showPanelNames={false} makes it sr-only rather than removing it, because a grid whose panels a screen reader cannot tell apart is one figure with twelve anonymous plots in it. |
| Shared domainrequired | panel.domain, computed across the panels that survive the cap and handed to the render function. It is not applied for you: a panel that does not pass it to its own value axis has opted back into independent scales. |
| Grid-level legend and axis labels | legend, yLabel and xLabel, printed once above and below the grid. A legend inside every panel is the same three swatches twelve times, restating what the reader learned from the first one. |
| Overflow note | The line under a capped grid, saying how many groups were left out or folded. limit is 12 by default and nothing is ever dropped in silence. |
Best practices
Do
- Pass panel.domain to the panel chart’s value axis. That one line is what the component is for: on independent scales a group peaking at 40 and one peaking at 4,000 draw the same shape, so the comparison is not merely lost, it is inverted — and nothing on screen says so.
- Name every plotted field in value. It is what the shared domain is measured over, so a panel that draws a series value does not mention can overflow its own axis while the rest of the grid looks correct.
- Set hideDataTable when the panel charts carry their own. Every chart in the package renders its rows as an sr-only table, so a twelve-panel grid otherwise puts thirteen tables into the accessibility tree.
- Keep sort="max" for a grid a reader scans and switch to "name" for one they look things up in. Reading order is what a reader takes as ranking, so it is a decision in either direction.
Don’t
- overflow="fold" is not free. The folded panel is the SUM of the tail at each category and its statistics go into the shared domain, so folding twenty-eight small groups can produce one tall panel that squashes the twelve the reader came for. Use note when the tail is numerous rather than large.
- Do not turn includeZero off under bars or areas. It is on by default for the same reason a bar axis is anchored at zero — a length read against a truncated baseline overstates every difference, and a grid exists to have its differences compared.
- Do not reach for it at two or three series. Faceting buys back every individual shape and pays for it with the direct overlay, and crossovers, gaps and shares of one total are exactly what the overlay was for.
Examples
default
Six channels as six small plots on ONE domain, which is the entire reason the component exists. Social peaks at 410 and Organic at 6,400, and that ratio is the first thing the grid says — on independent scales the two would draw the same rising line, so the comparison the reader came to make would be inverted rather than merely lost. The shared scale is computed across the panels and handed to the render function as panel.domain; a panel that does not pass it to its value axis has opted back into the broken version. yLabel and xLabel are printed once for the whole grid rather than once per panel, which is five axis titles the reader does not have to read again.
Organic search
Organic search visitors Direct
Direct visitors Paid search
Paid search visitors Referral
Referral visitors Email
Email visitors Social
Social visitors
| month | channel | visitors |
|---|---|---|
| Jan | Organic search | 4,200 |
| Jan | Paid search | 2,100 |
| Jan | Direct | 1,800 |
| Jan | Referral | 620 |
| Jan | 430 | |
| Jan | Social | 210 |
| Feb | Organic search | 4,400 |
| Feb | Paid search | 2,000 |
| Feb | Direct | 1,850 |
| Feb | Referral | 700 |
| Feb | 460 | |
| Feb | Social | 260 |
| Mar | Organic search | 4,100 |
| Mar | Paid search | 1,900 |
| Mar | Direct | 1,900 |
| Mar | Referral | 880 |
| Mar | 450 | |
| Mar | Social | 240 |
| Apr | Organic search | 4,800 |
| Apr | Paid search | 1,750 |
| Apr | Direct | 1,950 |
| Apr | Referral | 810 |
| Apr | 470 | |
| Apr | Social | 320 |
| May | Organic search | 5,200 |
| May | Paid search | 1,600 |
| May | Direct | 2,000 |
| May | Referral | 1,020 |
| May | 520 | |
| May | Social | 300 |
| Jun | Organic search | 5,600 |
| Jun | Paid search | 1,400 |
| Jun | Direct | 2,050 |
| Jun | Referral | 1,180 |
| Jun | 610 | |
| Jun | Social | 280 |
| Jul | Organic search | 6,100 |
| Jul | Paid search | 1,250 |
| Jul | Direct | 2,100 |
| Jul | Referral | 1,260 |
| Jul | 590 | |
| Jul | Social | 350 |
| Aug | Organic search | 6,400 |
| Aug | Paid search | 1,100 |
| Aug | Direct | 2,150 |
| Aug | Referral | 1,400 |
| Aug | 640 | |
| Aug | Social | 410 |
overplotted vs faceted
The same six channels twice, and the trade stated as a picture. Above, one plot and six lines: the legend has to be carried back into the hairball, and Paid search falling by half is invisible under Organic's rise. Below, six small plots on one domain, where that fall is the second thing you see and the ranking is the first. Faceting costs the direct overlay — crossovers and gaps between two named series are gone, and past about five series that is a trade worth making every time — and it costs a reshape, since a multi-series chart eats one row per month with a column per channel while a facet eats one row per month PER channel.
Overplotted — six series, one plot
| month | Organic search | Paid search | Direct | Referral | Social | |
|---|---|---|---|---|---|---|
| Jan | 4,200 | 2,100 | 1,800 | 620 | 430 | 210 |
| Feb | 4,400 | 2,000 | 1,850 | 700 | 460 | 260 |
| Mar | 4,100 | 1,900 | 1,900 | 880 | 450 | 240 |
| Apr | 4,800 | 1,750 | 1,950 | 810 | 470 | 320 |
| May | 5,200 | 1,600 | 2,000 | 1,020 | 520 | 300 |
| Jun | 5,600 | 1,400 | 2,050 | 1,180 | 610 | 280 |
| Jul | 6,100 | 1,250 | 2,100 | 1,260 | 590 | 350 |
| Aug | 6,400 | 1,100 | 2,150 | 1,400 | 640 | 410 |
Faceted — six plots, one domain
Organic search
Organic search visitors Direct
Direct visitors Paid search
Paid search visitors Referral
Referral visitors Email
Email visitors Social
Social visitors
| month | channel | visitors |
|---|---|---|
| Jan | Organic search | 4,200 |
| Jan | Paid search | 2,100 |
| Jan | Direct | 1,800 |
| Jan | Referral | 620 |
| Jan | 430 | |
| Jan | Social | 210 |
| Feb | Organic search | 4,400 |
| Feb | Paid search | 2,000 |
| Feb | Direct | 1,850 |
| Feb | Referral | 700 |
| Feb | 460 | |
| Feb | Social | 260 |
| Mar | Organic search | 4,100 |
| Mar | Paid search | 1,900 |
| Mar | Direct | 1,900 |
| Mar | Referral | 880 |
| Mar | 450 | |
| Mar | Social | 240 |
| Apr | Organic search | 4,800 |
| Apr | Paid search | 1,750 |
| Apr | Direct | 1,950 |
| Apr | Referral | 810 |
| Apr | 470 | |
| Apr | Social | 320 |
| May | Organic search | 5,200 |
| May | Paid search | 1,600 |
| May | Direct | 2,000 |
| May | Referral | 1,020 |
| May | 520 | |
| May | Social | 300 |
| Jun | Organic search | 5,600 |
| Jun | Paid search | 1,400 |
| Jun | Direct | 2,050 |
| Jun | Referral | 1,180 |
| Jun | 610 | |
| Jun | Social | 280 |
| Jul | Organic search | 6,100 |
| Jul | Paid search | 1,250 |
| Jul | Direct | 2,100 |
| Jul | Referral | 1,260 |
| Jul | 590 | |
| Jul | Social | 350 |
| Aug | Organic search | 6,400 |
| Aug | Paid search | 1,100 |
| Aug | Direct | 2,150 |
| Aug | Referral | 1,400 |
| Aug | 640 | |
| Aug | Social | 410 |
scale and overflow
Two grids one prop apart, and then the cap. On the right every panel fills its own box, so Social — which never clears 410 — draws the same climb as Organic's 6,400: not a harder comparison but a false one, and nothing on the page admits it, which is why scales independent is never the safe answer. Below, fourteen channels drawn as six panels and an Other — sort last orders them by where each ended up, limit caps the grid before it becomes fourteen postage stamps, and overflow fold sums the tail into that last panel so the total stays true. Either way the grid prints a line under itself saying what it left out, because a grid that quietly stops at six is a grid the reader cannot audit.
Organic search
6,400Direct
2,150Referral
1,400Social
410
Organic search
6,400Direct
2,150Referral
1,400Social
410
Organic search
6,400Direct
2,150Paid search
2,100Referral
1,400Email
640Social
410Other
1,165
| month | channel | visitors |
|---|---|---|
| M1 | Organic search | 3,968 |
| M2 | Organic search | 3,712 |
| M3 | Organic search | 4,544 |
| M4 | Organic search | 4,224 |
| M5 | Organic search | 5,120 |
| M6 | Organic search | 5,632 |
| M7 | Organic search | 6,080 |
| M8 | Organic search | 6,400 |
| M1 | Direct | 1,333 |
| M2 | Direct | 1,247 |
| M3 | Direct | 1,527 |
| M4 | Direct | 1,419 |
| M5 | Direct | 1,720 |
| M6 | Direct | 1,892 |
| M7 | Direct | 2,043 |
| M8 | Direct | 2,150 |
| M1 | Paid search | 1,302 |
| M2 | Paid search | 1,218 |
| M3 | Paid search | 1,491 |
| M4 | Paid search | 1,386 |
| M5 | Paid search | 1,680 |
| M6 | Paid search | 1,848 |
| M7 | Paid search | 1,995 |
| M8 | Paid search | 2,100 |
| M1 | Referral | 868 |
| M2 | Referral | 812 |
| M3 | Referral | 994 |
| M4 | Referral | 924 |
| M5 | Referral | 1,120 |
| M6 | Referral | 1,232 |
| M7 | Referral | 1,330 |
| M8 | Referral | 1,400 |
| M1 | 397 | |
| M2 | 371 | |
| M3 | 454 | |
| M4 | 422 | |
| M5 | 512 | |
| M6 | 563 | |
| M7 | 608 | |
| M8 | 640 | |
| M1 | Social | 254 |
| M2 | Social | 238 |
| M3 | Social | 291 |
| M4 | Social | 271 |
| M5 | Social | 328 |
| M6 | Social | 361 |
| M7 | Social | 390 |
| M8 | Social | 410 |
| M1 | Affiliate | 198 |
| M2 | Affiliate | 186 |
| M3 | Affiliate | 227 |
| M4 | Affiliate | 211 |
| M5 | Affiliate | 256 |
| M6 | Affiliate | 282 |
| M7 | Affiliate | 304 |
| M8 | Affiliate | 320 |
| M1 | Display | 161 |
| M2 | Display | 151 |
| M3 | Display | 185 |
| M4 | Display | 172 |
| M5 | Display | 208 |
| M6 | Display | 229 |
| M7 | Display | 247 |
| M8 | Display | 260 |
| M1 | Podcast | 112 |
| M2 | Podcast | 104 |
| M3 | Podcast | 128 |
| M4 | Podcast | 119 |
| M5 | Podcast | 144 |
| M6 | Podcast | 158 |
| M7 | Podcast | 171 |
| M8 | Podcast | 180 |
| M1 | Newsletter | 87 |
| M2 | Newsletter | 81 |
| M3 | Newsletter | 99 |
| M4 | Newsletter | 92 |
| M5 | Newsletter | 112 |
| M6 | Newsletter | 123 |
| M7 | Newsletter | 133 |
| M8 | Newsletter | 140 |
| M1 | Partner | 68 |
| M2 | Partner | 64 |
| M3 | Partner | 78 |
| M4 | Partner | 73 |
| M5 | Partner | 88 |
| M6 | Partner | 97 |
| M7 | Partner | 105 |
| M8 | Partner | 110 |
| M1 | Print QR | 43 |
| M2 | Print QR | 41 |
| M3 | Print QR | 50 |
| M4 | Print QR | 46 |
| M5 | Print QR | 56 |
| M6 | Print QR | 62 |
| M7 | Print QR | 67 |
| M8 | Print QR | 70 |
| M1 | Events | 34 |
| M2 | Events | 32 |
| M3 | Events | 39 |
| M4 | Events | 36 |
| M5 | Events | 44 |
| M6 | Events | 48 |
| M7 | Events | 52 |
| M8 | Events | 55 |
| M1 | SMS | 19 |
| M2 | SMS | 17 |
| M3 | SMS | 21 |
| M4 | SMS | 20 |
| M5 | SMS | 24 |
| M6 | SMS | 26 |
| M7 | SMS | 29 |
| M8 | SMS | 30 |
Accessibility
- The shared domain is the default and the whole point: on independent scales a group peaking at 40 and one peaking at 4,000 draw the same shape, and the comparison the reader came for is not merely lost but inverted.
- Every panel is a figure with its own accessible name, so a screen reader walks eight named charts rather than one unnamed grid.
- Panels beyond max fold into a stated overflow rather than being dropped, and the count is printed — a grid silently missing four groups is not something a reader can detect.
- Panel order is a choice the call site makes explicitly through sort, because reading order is what a reader takes as ranking.