跳到正文
册页

数据

Heatmap - 热力图

一格一格按轻重去读的值。

用法

什么时候用它

活跃度日历、混淆矩阵、按小时与星期分布的负载。这是单色系统反而比彩色系统渲染得更好的那一种图。

来自@misoto22/folio/charts

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

说明

一格一格按轻重去读的值组成的网格——活跃度日历、混淆矩阵、按小时与星期分布的负载。

这是整套里唯一一种单色系统反而渲染得更好的形式。热力图需要一把眼睛不用查图例就能读出顺序的刻度,而明度是唯一一个顺序毫无歧义的通道——这正是别处那句“单一色相,由浅到深”的来历,也正是彩虹热力图长年是个错误的原因。而在这里,根本没有色相可以搞错。

它是用真正的 <table> 而不是 SVG 搭起来的,所以读屏软件走的结构就是眼睛读的结构:一行表头、一列表头,以及一个会念出自己数值的格子。这个包里没有别的东西能这么说,也不需要——这同时也是它压根不需要渲染引擎的原因。

结构

Heatmap anatomy
部件说明
图题必填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 是从各个单元格推导出来的,这对一张独自阅读的网格是对的,而一旦旁边摆上第二张网格,它就错了。

Commits by weekday and hourDarker is busier
Commits by weekday and hour
Row0003060912151821
Mon8Mon, 008Mon, 0332Mon, 0653Mon, 0967Mon, 1272Mon, 1567Mon, 1853Mon, 21
Tue8Tue, 008Tue, 0332Tue, 0653Tue, 0967Tue, 1272Tue, 1567Tue, 1853Tue, 21
Wed8Wed, 008Wed, 0332Wed, 0653Wed, 0967Wed, 1272Wed, 1567Wed, 1853Wed, 21
Thu8Thu, 008Thu, 0332Thu, 0653Thu, 0967Thu, 1272Thu, 1567Thu, 1853Thu, 21
Fri8Fri, 008Fri, 0332Fri, 0653Fri, 0967Fri, 1272Fri, 1567Fri, 1853Fri, 21
Sat2Sat, 002Sat, 039Sat, 0615Sat, 0919Sat, 1220Sat, 1519Sat, 1815Sat, 21
Sun2Sun, 002Sun, 039Sun, 0615Sun, 0919Sun, 1220Sun, 1519Sun, 1815Sun, 21

双向色阶

发散型读的是距中点的距离,所以两个方向都会加深,而刻度的中间就是页面本身——这是给差值、给残差、给任何「零是一条真实边界、而不只是底部」的东西用的刻度。domain 是刻意对称钉死的:推导出来的话,它会从最低的读数跑到最高的读数,把中点丢在极值碰巧落到的地方,而不是落在这把刻度所铰接的那个零上。没有读数的那一周画成一圈虚线轮廓,而不是最浅的那格,因为空缺不是零;而 showValues 会把淡色底压在渐变的 35% 以内,好让印上去的数字在每一格上都能从自己的背景里脱出来。

Weekly delta against targetZero is the page; darker is further from target in either direction
Weekly delta against target
RowW1W2W3W4W5W6
Platform034366-30-38
Growth4017-21-40-2216
Mobile-10-38-31Mobile, W4: no data3634
Data-37-7293913-25

共用值域

两张只有对照着读才有意义的网格,所以两张都被钉在同一个 domain 上。推导出来的话,每一张都会从零跑到自己最大的那个读数,两张都会用满整道渐变——法兰克福三分钟的峰值,画出来会和悉尼十分钟的峰值一样深,而这正是共享图例唯一补不了的那个失败。formatValue 把单位带进单元格:没有它,一个班次会把自己播报成 480,有了它则是 8m,而对任何通过表格、而不是靠眼睛够到这些单元格的人来说,这是一个数字和一个读数之间的差别。

Median queue wait — SydneyDarker is a longer wait
Median queue wait — Sydney
Row00–0606–1212–1818–24
Mon45sMon, 00–063m 30sMon, 06–128mMon, 12–184m 20sMon, 18–24
Tue50sTue, 00–064mTue, 06–128m 40sTue, 12–184m 35sTue, 18–24
Wed1mWed, 00–064m 20sWed, 06–128m 25sWed, 12–185mWed, 18–24
Thu55sThu, 00–064m 10sThu, 06–129mThu, 12–185m 20sThu, 18–24
Fri1m 10sFri, 00–065mFri, 06–129m 50sFri, 12–185m 55sFri, 18–24
Median queue wait — FrankfurtSame scale, so the two grids can be compared
Median queue wait — Frankfurt
Row00–0606–1212–1818–24
Mon30sMon, 00–061m 30sMon, 06–122m 40sMon, 12–182mMon, 18–24
Tue35sTue, 00–061m 45sTue, 06–123mTue, 12–182m 10sTue, 18–24
Wed40sWed, 00–061m 50sWed, 06–122m 55sWed, 12–182m 20sWed, 18–24
Thu30sThu, 00–061m 35sThu, 06–122m 45sThu, 12–182m 5sThu, 18–24
Fri45sFri, 00–062mFri, 06–123m 20sFri, 12–182m 30sFri, 18–24

类型

TSX
export type HeatmapScale = 'sequential' | 'diverging'

无障碍

  • 是真正的 <table>,不是 SVG:读屏软件走的结构就是眼睛读的结构,每个格子都会念出自己的行、列和值。
  • 明度是唯一一个顺序毫无歧义的通道——这正是别处那句“单一色相,由浅到深”的来历。而在这里,根本没有色相可以搞错。
  • null 画成一圈虚线轮廓,绝不画成最浅的那一格——缺测不是零。
  • 两张网格要对照时一定要固定 domain:各自独立取域的两张图长得一样、含义却不同,这是共享图例也补不了的错。