Skip to content
misoto22 design

Charts

Facet

The same chart once per group, on one shared scale.

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.

Examples

default

Visitors by channel, 2026
Visitors
  • Organic search

    Organic search visitors
  • Direct

    Direct visitors
  • Paid search

    Paid search visitors
  • Referral

    Referral visitors
  • Email

    Email visitors
  • Social

    Social visitors
Jan – Aug 2026
Visitors by channel, 2026
monthchannelvisitors
JanOrganic search4,200
JanPaid search2,100
JanDirect1,800
JanReferral620
JanEmail430
JanSocial210
FebOrganic search4,400
FebPaid search2,000
FebDirect1,850
FebReferral700
FebEmail460
FebSocial260
MarOrganic search4,100
MarPaid search1,900
MarDirect1,900
MarReferral880
MarEmail450
MarSocial240
AprOrganic search4,800
AprPaid search1,750
AprDirect1,950
AprReferral810
AprEmail470
AprSocial320
MayOrganic search5,200
MayPaid search1,600
MayDirect2,000
MayReferral1,020
MayEmail520
MaySocial300
JunOrganic search5,600
JunPaid search1,400
JunDirect2,050
JunReferral1,180
JunEmail610
JunSocial280
JulOrganic search6,100
JulPaid search1,250
JulDirect2,100
JulReferral1,260
JulEmail590
JulSocial350
AugOrganic search6,400
AugPaid search1,100
AugDirect2,150
AugReferral1,400
AugEmail640
AugSocial410

overplotted vs faceted

Overplotted — six series, one plot

Visitors by channel, overplotted
Visitors by channel, overplotted
monthOrganic searchPaid searchDirectReferralEmailSocial
Jan4,2002,1001,800620430210
Feb4,4002,0001,850700460260
Mar4,1001,9001,900880450240
Apr4,8001,7501,950810470320
May5,2001,6002,0001,020520300
Jun5,6001,4002,0501,180610280
Jul6,1001,2502,1001,260590350
Aug6,4001,1002,1501,400640410

Faceted — six plots, one domain

Visitors by channel, faceted
Visitors
  • Organic search

    Organic search visitors
  • Direct

    Direct visitors
  • Paid search

    Paid search visitors
  • Referral

    Referral visitors
  • Email

    Email visitors
  • Social

    Social visitors
Visitors by channel, faceted
monthchannelvisitors
JanOrganic search4,200
JanPaid search2,100
JanDirect1,800
JanReferral620
JanEmail430
JanSocial210
FebOrganic search4,400
FebPaid search2,000
FebDirect1,850
FebReferral700
FebEmail460
FebSocial260
MarOrganic search4,100
MarPaid search1,900
MarDirect1,900
MarReferral880
MarEmail450
MarSocial240
AprOrganic search4,800
AprPaid search1,750
AprDirect1,950
AprReferral810
AprEmail470
AprSocial320
MayOrganic search5,200
MayPaid search1,600
MayDirect2,000
MayReferral1,020
MayEmail520
MaySocial300
JunOrganic search5,600
JunPaid search1,400
JunDirect2,050
JunReferral1,180
JunEmail610
JunSocial280
JulOrganic search6,100
JulPaid search1,250
JulDirect2,100
JulReferral1,260
JulEmail590
JulSocial350
AugOrganic search6,400
AugPaid search1,100
AugDirect2,150
AugReferral1,400
AugEmail640
AugSocial410

scale and overflow

Visitors by channel, shared scalescales="shared" — the default
  • Organic search

    6,400
  • Direct

    2,150
  • Referral

    1,400
  • Social

    410
Visitors by channel, independent scalesscales="independent" — every panel lies about its size
  • Organic search

    6,400
  • Direct

    2,150
  • Referral

    1,400
  • Social

    410
Visitors by channel, top sixFourteen channels, sorted by where each ended up
  • Organic search

    6,400
  • Direct

    2,150
  • Paid search

    2,100
  • Referral

    1,400
  • Email

    640
  • Social

    410
  • Other

    1,165
6 of 14 shown; the other 8 are summed into Other.
Visitors by channel, top six
monthchannelvisitors
M1Organic search3,968
M2Organic search3,712
M3Organic search4,544
M4Organic search4,224
M5Organic search5,120
M6Organic search5,632
M7Organic search6,080
M8Organic search6,400
M1Direct1,333
M2Direct1,247
M3Direct1,527
M4Direct1,419
M5Direct1,720
M6Direct1,892
M7Direct2,043
M8Direct2,150
M1Paid search1,302
M2Paid search1,218
M3Paid search1,491
M4Paid search1,386
M5Paid search1,680
M6Paid search1,848
M7Paid search1,995
M8Paid search2,100
M1Referral868
M2Referral812
M3Referral994
M4Referral924
M5Referral1,120
M6Referral1,232
M7Referral1,330
M8Referral1,400
M1Email397
M2Email371
M3Email454
M4Email422
M5Email512
M6Email563
M7Email608
M8Email640
M1Social254
M2Social238
M3Social291
M4Social271
M5Social328
M6Social361
M7Social390
M8Social410
M1Affiliate198
M2Affiliate186
M3Affiliate227
M4Affiliate211
M5Affiliate256
M6Affiliate282
M7Affiliate304
M8Affiliate320
M1Display161
M2Display151
M3Display185
M4Display172
M5Display208
M6Display229
M7Display247
M8Display260
M1Podcast112
M2Podcast104
M3Podcast128
M4Podcast119
M5Podcast144
M6Podcast158
M7Podcast171
M8Podcast180
M1Newsletter87
M2Newsletter81
M3Newsletter99
M4Newsletter92
M5Newsletter112
M6Newsletter123
M7Newsletter133
M8Newsletter140
M1Partner68
M2Partner64
M3Partner78
M4Partner73
M5Partner88
M6Partner97
M7Partner105
M8Partner110
M1Print QR43
M2Print QR41
M3Print QR50
M4Print QR46
M5Print QR56
M6Print QR62
M7Print QR67
M8Print QR70
M1Events34
M2Events32
M3Events39
M4Events36
M5Events44
M6Events48
M7Events52
M8Events55
M1SMS19
M2SMS17
M3SMS21
M4SMS20
M5SMS24
M6SMS26
M7SMS29
M8SMS30

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.

Props

Facet props
PropTypeDefaultDescription
byrequiredkeyof TRow & stringThe row field the data is split on. One panel per distinct value.
childrenrequired(panel: FacetPanel<TRow>) => ReactNodeOne 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.
datarequiredTRow[]Every row, across every group. The split happens here, not at the call site.
titlerequiredstringWhat 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.
valuerequired(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.
classNamestringMerged onto the figure, last, so a call site can size or space it.
columns'auto' | number'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.
descriptionReactNodeA line under the title — the unit, the window, the caveat.
domain[number, number]Pins the domain outright, ignoring the data and `scales` alike.
emptyChartEmptyPropsWhat 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.
hideDataTablebooleanfalseDrops 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.
includeZerobooleantruePulls 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.
legendReactNodeOne 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 `<LineChart.Legend>` in one panel and this is unnecessary; put it here and take it out of the panels.
limitnumber | false12How 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.
minPanelWidthnumberMIN_PANEL_PXHow narrow a panel may get before the grid drops a column, in pixels.
nicebooleantrueRounds 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.
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.
otherLabelstring'Other'What the folded panel is called.
overflowFacetOverflow'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.
overflowNote(info: FacetOverflowInfo) => ReactNodedefaultOverflowNoteWrites the line under a capped grid. The default sentence is English; this is where a translated call site replaces it.
scalesFacetScales'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.
showPanelNamesbooleantruePrints 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.
showTitlebooleanPrints the title above the grid instead of hiding it from sight.
sortFacetSort<TRow>'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.
xDataKeykeyof TRow & stringThe 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.
xLabelReactNodeThe category axis's name, printed once under the grid.
yLabelReactNodeThe value axis's name, printed once above the grid instead of per panel.

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.