跳到正文
册页

数据

BulletChart - 子弹图

一个度量、它的目标,以及说明这个数好不好的几条背景带。

用法

什么时候用它

一页要盯十个数的状态页。Stephen Few 设计它就是为了替掉仪表盘上的表盘——那种东西花掉整张卡片,只把一个数说得很差。

来自@misoto22/folio/charts

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

说明

一个度量、它的目标值,以及说明这个数到底好不好的区间带——每一个被追踪的东西占密密的一行。

Stephen Few 设计它,就是为了取代仪表盘上那个表盘,而这个理由至今站得住:一个速度表要花掉整整一张卡片,才把一个数字表达得很糟;子弹图则在一行文字的高度里,把同一个数字、它的目标值,以及围绕它的定性背景一并说清楚。十个叠起来,就是一页读者一眼扫得完的状态页。

它不做的事,是呈现随时间的变化。子弹图只是一个瞬间,target 是它唯一带着的对照;当问题是「我们是怎么走到这一步的」,它要的是 <LineChart>;当几个度量需要互相比较、而不是各自跟自己的目标比时,<BarList> 会替你排出名次,而这里不会。

真正要当心的是那些区间带。它们是一种判断——是某个人决定了 80 算「好」——却和测量值用同一种笔墨画出来,读者根本分不清哪个是达成共识的阈值、哪个是谁随手敲进去的数。它们还会压缩信息:一个接近刻度顶端的值,无论是压线过关一分还是甩开三十分,都落在同一条带里。区间带回答的是「这个能不能接受」,永远不是「差了多少」。

刻意不带渲染引擎——这是整套里唯一一张什么都不用算的图。每一行都只是一把线性刻度,没有坐标轴、没有刻度线、也没有共享的绘图区,所以它是用 HTML 排出来的:区间带和条形都是内联轴方向上的偏移量,这意味着整张图在从右往左的文档里能正确镜像,而一张画在用户坐标系里的 SVG 做不到。

结构

BulletChart anatomy
部件说明
图题必填公共图表 figure 上的 title,用 aria-labelledby 明确引用,而不是留给 figcaption 去被推断;默认看不见,除非 showTitle 把它印出来、description 排在下面。
度量行必填每条轨道上面的那一行:行首是度量的名称和它可选的补充说明,行尾是格式化后的数值,有目标值时跟在一条斜杠后面。读屏用户拿到的读数就是这一行,因为它下面那张图根本不在无障碍树上。
轨道必填24px 高、aria-hidden 的盒子,装着背景带、条形和目标线,用行内轴的偏移排版,而不是在 SVG 用户坐标系里——正是这一点让整张图在从右到左的文档里能正确镜像。
背景带定性的底,由 ranges 按递增的上界建出来:刻度低端最重,往上逐渐变浅,所以实心的条形在表现最好的那一段最跳。
度量条必填那个值,实色 --series-1,高度是轨道的三分之一,从刻度起点开始画。它是这里唯一一个属于测量而非判断的标记,也是唯一一个用足墨色画的。
目标线target,画成一条 2px 的 --ink 横线直接穿过条形,而不是在旁边再放一根条——所以哪个数是做到的、哪个数是要求的,一眼就分得出,不用去比。

实践建议

推荐

  • ranges 的每一个界都要落在 domain 里面。位于两端或两端之外的界,在带子建起来之前就被丢掉,所以在 0 到 50 的 domain 上给出 60 和 80 的 ranges,画出来是一条平的带子、哪儿都没有边界——这一行看着像被评估过,其实没有,而且它对此一声不吭。
  • 带子守住五条。无论传进来多少条,权重都是从满值的 --chart-fill 均匀铺到它的十分之三,所以第六条、第七条边界只是把同一段跨度切得更细,底也就不再有可以让读者读出阈值的边。
  • 除非页面自己会把这些度量印出来,否则 hideDataTable 就别开。整张图都是 aria-hidden——带子、条形、目标线一样——所以那张自动生成的、装着值、目标和各段界限的 sr-only 表格,是读屏用户唯一够得到的、对这张图的交代。

避免

  • 超出刻度末端的值是被夹住,不是溢出:0 到 100 的 domain 上,130 把轨道填满,和 100 填得一模一样,只有轨道上方印出来的那个数字说得清是哪一个。domain 要固定得足够宽,容得下你预期的超额,否则那个远远冲过目标的行,看起来只是刚好做完。
  • 不要把某一段的界限正好放在目标值上。两者是同一把刻度摆出来的,所以那条线会不偏不倚落在带子的边上,而唯一一个说明「要求做到多少」的标记,就消失在它本该被对照着读的那块底里。

示例

默认

四个度量,共用一把 0 到 100 的刻度,于是那些区间带在每一行上含义相同,各行之间既能相互比较、也能各自跟自己的目标比。条形是测量值,横穿它的那道线是目标:两类不同的数,这也是为什么目标不是画成旁边的第二根条。那些区间带是某个人做出的判断,却用和测量值同一种笔墨画出来,而且它们会压缩——一个压线过关一分的值,和一个甩开三十分的值,落在同一条带里,所以一条带回答的是「这个数能不能接受」,永远不是「差了多少」。没有渲染引擎,也没有状态:每一行都只是一把用普通 HTML、按逻辑属性排出来的线性刻度,可以服务端渲染,并且在从右往左的文档里能正确镜像,而一张画在用户坐标系里的 SVG 做不到。

Quarterly targetsBar is the measure, rule is the target
Availability96 / 99
Support CSAT84 / 80
Onboarding completion61 / 75
Docs coverage43 / 70
Quarterly targets
nameValueTargetRange bounds
Availability969950, 80
Support CSAT848050, 80
Onboarding completion617550, 80
Docs coverage437050, 80

每个度量各自的刻度

四个度量,既不共用单位也不共用范围,各自带着自己的 domain 和自己的 ranges——正是这一点让一个以毫秒计的延迟,能和一个百分比待在同一个块里。只有当这些度量共用一把刻度时,共享的区间带才有意义,所以这里图表级的 ranges 和 domain 干脆都不写。showScale 默认是关的,这里由那个开关打开,因为四个不同的 domain 摞在一起时,读者没有别的办法知道一条轨道的两端各值多少。

Platform health
Uptime30 days99.4 / 99.9
98100
p95 latencyms, lower is better310 / 250
0600
Error budget left%38 / 50
0100
Open incidents3 / 0
08
Platform health
nameValueTargetRange bounds
Uptime99.499.999, 99.5
p95 latency310250200, 400
Error budget left385025, 60
Open incidents301, 4

空数据

一个状态页在没人填过它的那个早晨所处的状态——一个新季度、一个什么都匹配不上的筛选、一个没有历史的账户——而这也正是大多数图表库会回给你一摞空轨道的那个状态,读者分不出它和一张加载失败的图表有什么区别。无论如何,这个图形都保住了它的标题和描述,所以这一页仍然说得清缺的是什么。文案说的是发生了什么,而不是「暂无数据」,而且它带着那个唯一能改变现状的东西:一个没有动作的空状态是一条死路。

Quarterly targetsBar is the measure, rule is the target

No targets set for this quarter

Targets carry over when a quarter opens. This one has none yet.

无障碍

  • 纯 HTML + 逻辑属性写成,不需要渲染引擎,可服务端渲染,在从右到左的文档里也是对的。recharts 不在场时照样能用。
  • 几条带子是一个判断,却和被测量的那个数用同一种墨画出来,所以页面必须交代它们从哪来。只是把量程三等分的带子会让图看起来「已经被评估过」,其实并没有。
  • 它呈现的是一个瞬间,除了 target 之外不携带任何比较。「我们是怎么走到这儿的」要的是 LineChart。
  • 共用的带子只有在几个度量共用同一把尺时才有意义——延迟和转化率并排时,要给每个度量各自的 ranges 和 domain。