图表
Facet - 小型多图
同一张图按分组各画一遍,共用同一把尺。
用法
什么时候用它
来自@misoto22/folio/charts
import { Facet } from '@misoto22/folio/charts'说明
同一张图,每组重复一次,共用同一把刻度——小型多图(small multiples)。
一张多序列图到五条序列左右就答不上来的那个问题,答案在这里:八条线挤在一张图里是一团乱麻,读者的注意力全花在拆线上,而不是读线。把同样这八条线拆到八张小图里,代价是没法直接叠着比,换回来的是每一条自己的形状——这笔买卖几乎总是划算的。这就是 Tufte 说的小型多图,Observable Plot 和 Vega-Lite 把它当作一等操作,也是同一个理由。
当这些序列确实必须互相对照着读时——交叉、间距、在同一个总量里各占多少——而且它们的数量还少到分得清,那就改用多序列的 <LineChart> 或 <AreaChart>。当每一组自己的形状都值得被看见、当组数很多,或者读者的问题是「这里面哪一个不一样」时,用这个。
它比手写一个 data.map() 对在哪:默认情况下,各个面板共用同一个 domain。刻度各自独立时,每个面板都会把自己那张图填满,于是一组峰值 40 和一组峰值 4,000 画出来是同一个形状——比较不只是丢了,而是反过来了,而且屏幕上没有任何东西说出这件事。panel.domain 就是那把共享刻度;渲染函数必须把它交给面板的数值轴。它还会给网格封顶、把长尾折起来或者数出来,并且只印一个图例、一个轴标签,而不是十二个。
结构
| 部件 | 说明 |
|---|---|
| 图框必填 | 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 是为整张网格印一次、而不是每个面板印一次,这就少了五个读者不必再读一遍的坐标轴标题。
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 |
叠画与分面
同样六个渠道来两遍,而这个取舍被摆成了一幅画。上面是一张图、六条线:图例得被一路带回那团乱麻里去对,而 Paid search 腰斩这件事,藏在 Organic 的上升底下看不见。下面是六张共用一个 domain 的小图,那一次下跌是你看到的第二件事,而排名是第一件。分面的代价是失去了直接叠加——两条具名序列之间的交叉和缺口没了,而超过五条序列左右,这个代价每次都值得付——它还要求你重塑数据,因为一张多序列图表每个月吃一行、每个渠道占一列,而一张分面图是每个月每个渠道吃一行。
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 |
刻度与上限
两张网格只差一个属性,然后是那个上限。右边每个面板都填满自己的盒子,于是从没越过 410 的 Social,画出和 Organic 那个 6,400 一样的爬升:这不是更难的比较,而是一个错误的比较,而且页面上没有任何东西承认这一点,这正是为什么 scales 用 independent 从来不是那个安全答案。下面是十四个渠道画成六个面板加一个 Other——sort 用 last 按各自的收尾位置排序,limit 在这张网格变成十四张邮票之前就把它封顶,而 overflow 用 fold 把长尾汇总进最后那个面板,好让合计仍然为真。无论哪种,这张网格都会在自己下面印一行,说明它略掉了什么,因为一张悄悄停在第六个的网格,是一张读者没法核查的网格。
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 |
无障碍
- 共享取值域是默认,也是这件事的全部意义:各自独立取刻度的话,峰值 40 的一组和峰值 4,000 的一组会画出同样的形状,读者要来做的那个比较不只是丢了,是被反转了。
- 每一格都是一个有自己可访问名称的 figure,所以读屏软件走过的是八张各有其名的图,而不是一整块没有名字的网格。
- 超过 max 的格子会折进一条明说的 overflow 里,而不是被丢掉,而且数量会印出来——一个悄悄少了四组的网格,读者是察觉不到的。
- 格子的顺序由调用处通过 sort 明确选定,因为读者会把阅读顺序当成排名。