数据
Heatmap - 热力图
一格一格按轻重去读的值。
用法
什么时候用它
来自@misoto22/folio/charts
import { Heatmap } from '@misoto22/folio/charts'说明
一格一格按轻重去读的值组成的网格——活跃度日历、混淆矩阵、按小时与星期分布的负载。
这是整套里唯一一种单色系统反而渲染得更好的形式。热力图需要一把眼睛不用查图例就能读出顺序的刻度,而明度是唯一一个顺序毫无歧义的通道——这正是别处那句“单一色相,由浅到深”的来历,也正是彩虹热力图长年是个错误的原因。而在这里,根本没有色相可以搞错。
它是用真正的 <table> 而不是 SVG 搭起来的,所以读屏软件走的结构就是眼睛读的结构:一行表头、一列表头,以及一个会念出自己数值的格子。这个包里没有别的东西能这么说,也不需要——这同时也是它压根不需要渲染引擎的原因。
结构
| 部件 | 说明 |
|---|---|
| 图题必填 | figcaption 里的 title,默认看不见,除非 showTitle 把它印在网格上方、description 跟在它下面。同一个字符串也是表格自己那个 sr-only 的 caption,所以进去时网格被命名一次,从表格那边再被命名一次。 |
| 列表头必填 | columns 里每一项一个 th scope="col",顺序照给的来;前面还有一个 sr-only 的角落单元格写着「Row」,好让表头这一行在行标签上方也有东西站着。 |
| 行表头必填 | rows 里每一项一个 th scope="row"——贴尾边的等宽字、--chart-axis,所以每个格子都能回溯到给它命名的那一对标签。 |
| 格子必填 | 一个 td,背景是按值在 domain 中的位置,把 --series-1 用 color-mix 混进 --chart-surface。是连续的而不是分档的,因为把一条渐变切成段,等于凭空造出数据里没有的边界;而格子之间 2px 的 border-spacing,正是让相邻两个轻重可以被数出来的东西。 |
| 缺测的格子 | null 画出来的东西——cells 里找不到的那一对行列也画成这个:页面底上一圈 --rule 的虚线轮廓,播报的是“无数据”而不是一个数字。 |
| 印出来的数值 | 格子里的 formatValue。开了 showValues 就看得见,否则是 sr-only;在强制颜色下它又被重新露出来,因为那时候承担编码的那层色已经被改掉了。 |
实践建议
推荐
- 值一旦跨过零,scale 就要换成 diverging。sequential 是按值在 domain 里的位置取渐变,所以在一张变化量的网格上,跌得最狠的那格最浅、涨得最多的那格最深;diverging 取的是离中点的距离,这才是一个增减量真正想要的读法。
- 有一个格子比其余高出一个数量级时,要固定 domain。推导出来的 domain 从零起——最低值为负时就从最低值起——到最高值,所以一堆 5 到 40 的读数里混进一个 10,000,其余每一格都被压进渐变的前百分之零点五,四十个各不相同的数字最后画成空网格上的一个深色方块。
- 格子的 row 和 column 要和 rows、columns 里写的一字不差:cells 是按这一对去查的,所以对不上的那一条什么都画不出来。它的数值却照样算进推导出来的 domain——一个拼错的字,就是这样用一个网格上根本看不到的值把整条渐变拉长的。
- 打开 showValues 就要清楚自己在拿什么换对比度。那层色被压在整条渐变的 35% 以内,好让同一种墨色在每一个格子上都过 4.5:1——格子之间的顺序原封不动,彼此的差距被压窄,而细节改由印出来的数字承担。
避免
- 不要让管线在网格看到之前先拿零把空缺填上。null 画成虚线轮廓、播报为无数据,零画成整条渐变里最浅的那一格——拿一个顶替另一个,就是把一次故障变成了一个清闲的小时,而网格上没有任何东西说得清到底是哪一种。
- 不要指望读者能从深浅里还原出一个数。这里没有图例也没有分档:轻重只给格子排序,从不说出任何一个值。强制颜色下的兜底会把数字露出来,是因为那里的背景已经不存在了——那是给一块被改掉的底色做的补救,不是那种确切数字要紧的网格上 showValues 的替代品。
示例
默认
顺序型刻度,给那种只会往上走的值用——一个计数、一段时长、一个体量。这道渐变只有一个色相,由浅到深,因为明度是唯一一条眼睛不用查图例就能读出次序的通道;把色相花在量级上是热力图历久弥新的错误,而这里已经没有色相可供搞砸了。domain 是从各个单元格推导出来的,这对一张独自阅读的网格是对的,而一旦旁边摆上第二张网格,它就错了。
| Row | 00 | 03 | 06 | 09 | 12 | 15 | 18 | 21 |
|---|---|---|---|---|---|---|---|---|
| Mon | 8Mon, 00 | 8Mon, 03 | 32Mon, 06 | 53Mon, 09 | 67Mon, 12 | 72Mon, 15 | 67Mon, 18 | 53Mon, 21 |
| Tue | 8Tue, 00 | 8Tue, 03 | 32Tue, 06 | 53Tue, 09 | 67Tue, 12 | 72Tue, 15 | 67Tue, 18 | 53Tue, 21 |
| Wed | 8Wed, 00 | 8Wed, 03 | 32Wed, 06 | 53Wed, 09 | 67Wed, 12 | 72Wed, 15 | 67Wed, 18 | 53Wed, 21 |
| Thu | 8Thu, 00 | 8Thu, 03 | 32Thu, 06 | 53Thu, 09 | 67Thu, 12 | 72Thu, 15 | 67Thu, 18 | 53Thu, 21 |
| Fri | 8Fri, 00 | 8Fri, 03 | 32Fri, 06 | 53Fri, 09 | 67Fri, 12 | 72Fri, 15 | 67Fri, 18 | 53Fri, 21 |
| Sat | 2Sat, 00 | 2Sat, 03 | 9Sat, 06 | 15Sat, 09 | 19Sat, 12 | 20Sat, 15 | 19Sat, 18 | 15Sat, 21 |
| Sun | 2Sun, 00 | 2Sun, 03 | 9Sun, 06 | 15Sun, 09 | 19Sun, 12 | 20Sun, 15 | 19Sun, 18 | 15Sun, 21 |
双向色阶
发散型读的是距中点的距离,所以两个方向都会加深,而刻度的中间就是页面本身——这是给差值、给残差、给任何「零是一条真实边界、而不只是底部」的东西用的刻度。domain 是刻意对称钉死的:推导出来的话,它会从最低的读数跑到最高的读数,把中点丢在极值碰巧落到的地方,而不是落在这把刻度所铰接的那个零上。没有读数的那一周画成一圈虚线轮廓,而不是最浅的那格,因为空缺不是零;而 showValues 会把淡色底压在渐变的 35% 以内,好让印上去的数字在每一格上都能从自己的背景里脱出来。
| Row | W1 | W2 | W3 | W4 | W5 | W6 |
|---|---|---|---|---|---|---|
| Platform | 0 | 34 | 36 | 6 | -30 | -38 |
| Growth | 40 | 17 | -21 | -40 | -22 | 16 |
| Mobile | -10 | -38 | -31 | Mobile, W4: no data | 36 | 34 |
| Data | -37 | -7 | 29 | 39 | 13 | -25 |
共用值域
两张只有对照着读才有意义的网格,所以两张都被钉在同一个 domain 上。推导出来的话,每一张都会从零跑到自己最大的那个读数,两张都会用满整道渐变——法兰克福三分钟的峰值,画出来会和悉尼十分钟的峰值一样深,而这正是共享图例唯一补不了的那个失败。formatValue 把单位带进单元格:没有它,一个班次会把自己播报成 480,有了它则是 8m,而对任何通过表格、而不是靠眼睛够到这些单元格的人来说,这是一个数字和一个读数之间的差别。
| Row | 00–06 | 06–12 | 12–18 | 18–24 |
|---|---|---|---|---|
| Mon | 45sMon, 00–06 | 3m 30sMon, 06–12 | 8mMon, 12–18 | 4m 20sMon, 18–24 |
| Tue | 50sTue, 00–06 | 4mTue, 06–12 | 8m 40sTue, 12–18 | 4m 35sTue, 18–24 |
| Wed | 1mWed, 00–06 | 4m 20sWed, 06–12 | 8m 25sWed, 12–18 | 5mWed, 18–24 |
| Thu | 55sThu, 00–06 | 4m 10sThu, 06–12 | 9mThu, 12–18 | 5m 20sThu, 18–24 |
| Fri | 1m 10sFri, 00–06 | 5mFri, 06–12 | 9m 50sFri, 12–18 | 5m 55sFri, 18–24 |
| Row | 00–06 | 06–12 | 12–18 | 18–24 |
|---|---|---|---|---|
| Mon | 30sMon, 00–06 | 1m 30sMon, 06–12 | 2m 40sMon, 12–18 | 2mMon, 18–24 |
| Tue | 35sTue, 00–06 | 1m 45sTue, 06–12 | 3mTue, 12–18 | 2m 10sTue, 18–24 |
| Wed | 40sWed, 00–06 | 1m 50sWed, 06–12 | 2m 55sWed, 12–18 | 2m 20sWed, 18–24 |
| Thu | 30sThu, 00–06 | 1m 35sThu, 06–12 | 2m 45sThu, 12–18 | 2m 5sThu, 18–24 |
| Fri | 45sFri, 00–06 | 2mFri, 06–12 | 3m 20sFri, 12–18 | 2m 30sFri, 18–24 |
类型
export type HeatmapScale = 'sequential' | 'diverging'无障碍
- 是真正的 <table>,不是 SVG:读屏软件走的结构就是眼睛读的结构,每个格子都会念出自己的行、列和值。
- 明度是唯一一个顺序毫无歧义的通道——这正是别处那句“单一色相,由浅到深”的来历。而在这里,根本没有色相可以搞错。
- null 画成一圈虚线轮廓,绝不画成最浅的那一格——缺测不是零。
- 两张网格要对照时一定要固定 domain:各自独立取域的两张图长得一样、含义却不同,这是共享图例也补不了的错。