跳到正文
册页

图表

Facet - 小型多图

同一张图按分组各画一遍,共用同一把尺。

用法

什么时候用它

八条序列挤在一个画框里会变成一团乱麻。真的需要逐点比较的两三条序列,还是该待在同一张图里。

来自@misoto22/folio/charts

TSX
import { Facet } from '@misoto22/folio/charts'

说明

同一张图,每组重复一次,共用同一把刻度——小型多图(small multiples)。

一张多序列图到五条序列左右就答不上来的那个问题,答案在这里:八条线挤在一张图里是一团乱麻,读者的注意力全花在拆线上,而不是读线。把同样这八条线拆到八张小图里,代价是没法直接叠着比,换回来的是每一条自己的形状——这笔买卖几乎总是划算的。这就是 Tufte 说的小型多图,Observable Plot 和 Vega-Lite 把它当作一等操作,也是同一个理由。

当这些序列确实必须互相对照着读时——交叉、间距、在同一个总量里各占多少——而且它们的数量还少到分得清,那就改用多序列的 <LineChart> 或 <AreaChart>。当每一组自己的形状都值得被看见、当组数很多,或者读者的问题是「这里面哪一个不一样」时,用这个。

它比手写一个 data.map() 对在哪:默认情况下,各个面板共用同一个 domain。刻度各自独立时,每个面板都会把自己那张图填满,于是一组峰值 40 和一组峰值 4,000 画出来是同一个形状——比较不只是丢了,而是反过来了,而且屏幕上没有任何东西说出这件事。panel.domain 就是那把共享刻度;渲染函数必须把它交给面板的数值轴。它还会给网格封顶、把长尾折起来或者数出来,并且只印一个图例、一个轴标签,而不是十二个。

结构

Facet anatomy
部件说明
图框必填ChartFigure 的那个 <figure>,包住整个网格,由 title 命名。和单张图表不同,它的空状态没有 false 这个后门:一对空坐标轴好歹还是一张图,而一个空网格什么都不是。
格子网格必填一个 role="list",里面是排在 CSS auto-fit 轨道上的 <li> 格子。list-none 会在 Safari 里把 list 角色抹掉,所以这里手动把它设了回来——一个由十二张图组成、却不播报十二个条目的网格,等于拿走了读者判断还要走多远的唯一线索。
格子名称每张图上方的一个 <p>。showPanelNames={false} 是把它变成 sr-only,而不是移除它,因为一个读屏软件分不出各个格子的网格,就是一张里面装着十二块匿名绘图区的图。
共享取值域必填panel.domain,在通过上限筛选后剩下的格子上算出来,然后交给渲染函数。它不会替你应用:一个没有把它传给自己值轴的格子,等于又选回了各自独立的刻度。
网格级图例与轴标签legend、yLabel 和 xLabel,在网格上下各印一次。每个格子里都放一个图例,就是同样三块色标重复十二遍,把读者从第一个那里已经学到的东西再讲一遍。
溢出说明被截断的网格下面那一行,说明有多少个分组被留下没画、或者被折了进去。limit 默认是 12,任何东西都不会被悄悄丢掉。

实践建议

推荐

  • 把 panel.domain 传给格子内图表的值轴。这个组件存在的意义就是这一行:各自独立取刻度的话,峰值 40 的一组和峰值 4,000 的一组会画出同样的形状,于是那个比较不只是丢了,是被反转了——而屏幕上没有任何东西说明这一点。
  • 每一个要画的字段都要在 value 里写出来。共享取值域正是按它算出来的,所以一个格子画了一条 value 没提到的序列,它就可能冲出自己的坐标轴,而网格其余部分看上去还一切正常。
  • 格子内的图表自带表格时,设上 hideDataTable。这个包里每一张图都会把自己的数据行渲染成一张 sr-only 表格,否则一个十二格的网格会往无障碍树里塞进十三张表。
  • 供读者扫读的网格保持 sort="max",供他们按名字查的换成 "name"。读者会把阅读顺序当成排名,所以无论选哪一个,这都是一次决定。

避免

  • overflow="fold" 不是白来的。折起来的那个格子是长尾在每个分类上的合计,而且它的统计量会进入共享取值域,所以把二十八个小分组折起来,可能造出一个高耸的格子,把读者真正要看的那十二个压扁。长尾是数量多而不是量大时,用 note。
  • 柱状或面积的格子下面,不要把 includeZero 关掉。它默认打开,理由和柱状轴锚在零上是同一个——对着一条被截断的基线去读长度,会把每一处差异都夸大,而网格存在的意义就是让这些差异被拿来比较。
  • 只有两三条序列时不要用它。分面换回了每条序列各自的形状,代价是失去了直接的叠加,而交叉点、缺口和在同一个总量里的占比,恰恰是叠加原本要回答的。

示例

默认

六个渠道画成六张共用一个 domain 的小图,而这就是这个组件存在的全部理由。Social 的峰值是 410,Organic 是 6,400,而这个比例正是这张网格说出的第一件事——在各自独立的刻度上,两者会画出同样一条上升的线,于是读者此行要做的那个比较,不只是丢了,而是被颠倒了。这把共享的刻度是跨面板算出来的,并作为 panel.domain 交给渲染函数;一个不把它传给自己数值轴的面板,等于主动退回了那个坏掉的版本。yLabel 和 xLabel 是为整张网格印一次、而不是每个面板印一次,这就少了五个读者不必再读一遍的坐标轴标题。

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

叠画与分面

同样六个渠道来两遍,而这个取舍被摆成了一幅画。上面是一张图、六条线:图例得被一路带回那团乱麻里去对,而 Paid search 腰斩这件事,藏在 Organic 的上升底下看不见。下面是六张共用一个 domain 的小图,那一次下跌是你看到的第二件事,而排名是第一件。分面的代价是失去了直接叠加——两条具名序列之间的交叉和缺口没了,而超过五条序列左右,这个代价每次都值得付——它还要求你重塑数据,因为一张多序列图表每个月吃一行、每个渠道占一列,而一张分面图是每个月每个渠道吃一行。

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

刻度与上限

两张网格只差一个属性,然后是那个上限。右边每个面板都填满自己的盒子,于是从没越过 410 的 Social,画出和 Organic 那个 6,400 一样的爬升:这不是更难的比较,而是一个错误的比较,而且页面上没有任何东西承认这一点,这正是为什么 scales 用 independent 从来不是那个安全答案。下面是十四个渠道画成六个面板加一个 Other——sort 用 last 按各自的收尾位置排序,limit 在这张网格变成十四张邮票之前就把它封顶,而 overflow 用 fold 把长尾汇总进最后那个面板,好让合计仍然为真。无论哪种,这张网格都会在自己下面印一行,说明它略掉了什么,因为一张悄悄停在第六个的网格,是一张读者没法核查的网格。

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

无障碍

  • 共享取值域是默认,也是这件事的全部意义:各自独立取刻度的话,峰值 40 的一组和峰值 4,000 的一组会画出同样的形状,读者要来做的那个比较不只是丢了,是被反转了。
  • 每一格都是一个有自己可访问名称的 figure,所以读屏软件走过的是八张各有其名的图,而不是一整块没有名字的网格。
  • 超过 max 的格子会折进一条明说的 overflow 里,而不是被丢掉,而且数量会印出来——一个悄悄少了四组的网格,读者是察觉不到的。
  • 格子的顺序由调用处通过 sort 明确选定,因为读者会把阅读顺序当成排名。