# Facet The same chart once per group, on one shared scale. - Group: Charts - Import: `import { Facet } from '@misoto22/design/charts'` - Page: https://ui.misoto22.com/components/facet/ - Related: line-chart, sparkline ## When to reach for it Eight series overplot into a hairball in one frame. Two or three series that genuinely need comparing point-for-point still belong in one chart. ## 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. ## Facet 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 `` or `` 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. ### Props - `title` (required) — `string`. What the whole grid shows, in a sentence a reader could act on. Required, and announced to a screen reader even when it is not printed. - `showTitle` — `boolean`. Prints the title above the grid instead of hiding it from sight. - `description` — `ReactNode`. A line under the title — the unit, the window, the caveat. - `data` (required) — `TRow[]`. Every row, across every group. The split happens here, not at the call site. - `by` (required) — `keyof TRow & string`. The row field the data is split on. One panel per distinct value. - `value` (required) — `(keyof TRow & string) | (keyof TRow & string)[]`. The row field, or fields, holding the numbers. This is what the shared domain is measured over and what a statistical sort reads, so it has to name every series a panel draws — a panel that plots a field this does not mention can still overflow its own axis. - `xDataKey` — `keyof TRow & string`. The shared category field — the x axis every panel has in common. Feeds the hidden table view, and is what lets `overflow="fold"` add the tail up at each category instead of concatenating it. - `scales` — `FacetScales` default `'shared'`. Whether every panel is drawn against one domain or against its own. `"shared"` — the default, and the entire reason this component exists. 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, and the comparison the reader opened the grid to make is not merely hard but actively wrong. The shared domain is computed across all the panels that survive the cap and handed to each one as `panel.domain`; a panel that does not pass it to its value axis has opted back into the broken version. `"independent"` is the escape hatch for the case where the panels are not comparable — different units, different orders of magnitude that carry no relation — and where each panel's own shape is the only reading. It is never the safe answer, which is why it is not the default. - `domain` — `[number, number]`. Pins the domain outright, ignoring the data and `scales` alike. - `includeZero` — `boolean` default `true`. Pulls the shared domain out to include zero. On by default, and the same default Recharts applies on its own: bars and areas read against a truncated baseline overstate every difference in the grid, and a grid exists to be compared. Turn it off when the values live far from zero and the differences between them ARE the reading — temperatures, latencies, ratings out of five. - `nice` — `boolean` default `true`. Rounds the domain out so the panels' ticks land on numbers a reader recognises. Off, a shared domain of `[0, 314]` gives ticks at 78.5. - `sort` — `FacetSort` default `'max'`. The panel order. Defaults to the largest peak first, because that is what makes a grid readable: eye order becomes rank order, and "which of these is the problem" is answered by position before a single axis is read. `"name"` for a grid a reader will look things up in, an explicit array of names for an order the data does not carry — stages, weekdays, severities — and a comparator over `{ name, rows, stats }` for anything else. - `order` — `'asc' | 'desc'`. The sort direction. Omit and it follows the sort: A→Z for a name or an explicit list, biggest first for a statistic. - `limit` — `number | false` default `12`. How many panels are drawn before the rest are dealt with. Twelve by default rather than unlimited, because the failure this prevents is silent: a `by` field with forty values renders forty plots, each one roughly a postage stamp, and nothing on the page says the grid stopped being readable. `false` lifts the cap when the call site has genuinely decided to draw them all. - `overflow` — `FacetOverflow` default `'note'`. What happens to the groups past the cap. `"note"` — the default — draws the top N and prints a line under the grid saying how many are missing. `"fold"` adds one more panel holding the tail summed at each category, the same bargain `BarList`'s "Other" row makes: the total stays true, and one panel absorbs the long tail. Either way a capped grid says so; nothing is dropped in silence. - `otherLabel` — `string` default `'Other'`. What the folded panel is called. - `overflowNote` — `(info: FacetOverflowInfo) => ReactNode` default `defaultOverflowNote`. Writes the line under a capped grid. The default sentence is English; this is where a translated call site replaces it. - `columns` — `'auto' | number` default `'auto'`. How many columns the grid holds at its widest. `"auto"` — the default — fits as many panels as `minPanelWidth` allows and reflows on its own, which is the behaviour a card, a sidebar and a full-width page all need from the same call site. A number caps the count without pinning it: the grid still drops to fewer columns when the container is narrow, rather than shrinking twelve plots to nothing. - `minPanelWidth` — `number` default `MIN_PANEL_PX`. How narrow a panel may get before the grid drops a column, in pixels. - `legend` — `ReactNode`. One key for the whole grid, printed above it. A legend inside every panel is the same three swatches repeated twelve times, which is ink spent restating what the reader learned from the first panel. Put `` in one panel and this is unnecessary; put it here and take it out of the panels. - `yLabel` — `ReactNode`. The value axis's name, printed once above the grid instead of per panel. - `xLabel` — `ReactNode`. The category axis's name, printed once under the grid. - `showPanelNames` — `boolean` default `true`. Prints each panel's group name above its plot. On by default. Turning it off only hides the name — it stays in the accessibility tree, because a grid whose panels a screen reader cannot tell apart is a grid with one figure and twelve anonymous plots in it. - `hideDataTable` — `boolean` default `false`. Drops the hidden table view of every row. Worth setting when the panels' own charts already ship theirs — each chart in the package renders its rows as a table too, so a twelve-panel grid can otherwise put thirteen tables in the accessibility tree. - `empty` — `ChartEmptyProps`. What the grid shows when the data yields no groups at all. No `false` escape hatch, unlike a single chart's: an empty pair of axes is at least a chart, but an empty grid is nothing — there is no reading for the absence to be. - `className` — `string`. Merged onto the figure, last, so a call site can size or space it. - `children` (required) — `(panel: FacetPanel) => ReactNode`. One panel, called once per group. Hand `panel.domain` to the panel chart's value axis. That single line is what the component is for; without it the panels are on independent scales and the grid is decorative. ## Example — default ```tsx import { Facet, LineChart, type ChartConfig } from '@misoto22/design/charts' {(panel) => ( {/* The line that makes this a comparison rather than six pictures. */} )} ``` ## Example — overplotted vs faceted ```tsx import { Facet, LineChart, type ChartConfig } from '@misoto22/design/charts'

Overplotted — six series, one plot

{Object.keys(SERIES).map((channel) => ( ))}

Faceted — six plots, one domain

{(panel) => ( )}
``` ## Example — scale and overflow ```tsx import { Facet, Sparkline } from '@misoto22/design/charts' ) } export function Example() { return (
{/* Two grids, the same four channels, one prop apart. On the right every panel fills its own box, so Social — which never clears 410 — draws the same climb as Organic's 6,400. That is not a harder comparison, it is a false one, and nothing on the page admits it. */}
{(panel) => ( row.visitors)} domain={panel.domain} /> )} {(panel) => ( row.visitors)} domain={panel.domain} /> )}
{/* Fourteen channels, six panels. The tail is summed into one rather than dropped, and the line under the grid says so either way — a grid that quietly stops at six is a grid the reader cannot audit. */} {(panel) => ( row.visitors)} domain={panel.domain} /> )}
```