图表
BarChart - 柱状图
按长度比较的离散分类。
用法
什么时候用它
来自@misoto22/folio/charts
import { BarChart } from '@misoto22/folio/charts'说明
按长度比较的离散分类——用来回答“每一个有多少”。
当横轴是连续的、读者在追一条趋势而不是在比较各个桶时,改用 <AreaChart> 或 <LineChart>。
结构
| 部件 | 说明 |
|---|---|
| 图框必填 | ChartFigure 的那个 <figure>,由 title 命名,无论页面在它上方有没有印出标题。 |
| 柱子必填 | <BarChart.Bar>,通过自定义 shape 绘制:先是一块透明矩形充当命中区,再是比自己槽位矮三个像素的实际柱体,这样堆叠的分段和上面那一段之间就留着一线页面底色。比这道修剪量还短的柱子会被垫到一个像素、而不是缩成没有,所以一个很小的计数永远不会和一个根本不存在的计数在像素上一模一样。 |
| 坐标轴 | <BarChart.XAxis> 和 <BarChart.YAxis>,默认都是素面的——没有刻度线,也没有轴线。Recharts 的每一个属性都会直接透传,domain 也在内,而这正是被截断的基线进来的那扇门。 |
| 图例 | <BarChart.Legend>。开了 isClickable,每一项就是一个真正的、带 aria-pressed 的 <button>,而不是挂了事件的 div——这就是键盘够得到的筛选器和够不到的筛选器之间的差别。 |
| 数值标签 | <BarChart.Values>,组合在柱子内部的插槽。show 默认是 last;all 用在五六根柱子、而且确切数字才是重点的场合,再多就是一张披着图表外衣的表格了。 |
| 隐藏数据表 | 完整数据的那张 sr-only 表格,取的不是刷选出来的窗口,所以读表格的人拿到的绝不会比 CSV 导出里的少。hideDataTable 会把它去掉,零行则什么都不渲染。 |
| 空状态 | ChartEmpty,data 为空时顶替绘图区渲染——一个标题、一个原因,外加一个可选的动作,这样「筛选没命中」才和「加载失败」区分得开。empty={false} 则保留光秃秃的坐标轴。 |
实践建议
推荐
- 一个还没结束的周期,用 buffer。它画斜纹的是最后一行数据、不是屏幕上最后一根柱子,所以把刷选拉回区间中段时不会有任何斜纹——一个三月就已经收官的月份,永远不会被画成还在计数。
- 值轴要锚在零上。柱子编码的是从基线量起的长度,所以经 <BarChart.YAxis> 传进来的 ["dataMin", "dataMax"] 取值域,会把百分之二的差距画成翻倍的柱子。这是柱状图扛不住、而 LineChart 扛得住的那种扭曲:折线编码的是斜率,所以裁剪它的取值域只是把读数重新放大,而不是凭空造一个出来。
- 分类名很长时,用 orientation="horizontal"。另一条路是让每一列底下都躺着一个旋转过的刻度标签,而一个旋转的标签比它所命名的那根柱子还难读。
- 在 stackType="percent" 下要给 <BarChart.YAxis> 传 tickFormatter。AreaChart 的 expanded 堆叠会自己换上 percentTick,柱状图的轴不会,它保留 defaultTick——于是一张归一化的图读出来是 0 到 1,而不是 0% 到 100%。
- 需要跨分类比较的那条序列,要放在堆叠的最底层。只有最下面那一段是从零开始的;它上面每一条带都浮在下面几条之上,而横跨十二个月去读第三条带,是眼睛做不到的比较。这个比较才是重点时,就改用分组柱状图。
避免
- hideDataTable 会让确切数字在任何地方都不复存在。defaultTick 从一万起就开始简写,所以坐标轴写的是 1.2M,<BarChart.Values> 标签也一样;而每一格都是完整 toLocaleString 的那张 sr-only 表格,是真实数字唯一被写出来的地方。
- 二十根 variant="default" 的柱子是一堵墙,不是二十个值。stripped 在一层淡底上画一道 2px 的顶盖,在这个密度下仍然数得清——它存在的理由就是这个密度。
- 不要把 barCategoryGap 收掉。组与组之间的空隙,是唯一在告诉读者「相邻两根柱子是两条序列、而不是两个分类」的东西,所以没有分类间隙的分组图会被读成堆叠图。
示例
默认
两条柱状序列共用一根分类轴,而这正是分类是离散的、不是连续统时该拿的形状。每条序列按声明顺序取灰阶上的下一个档位,所以隐藏其中一条,绝不会把剩下的重新上色;第二条带着一层斜纹,因为光靠一档灰,是一个太单薄的、要读者在脑子里记住的东西。这也正是为什么一条序列以上时,图例不是可选项。
| month | Desktop | Mobile |
|---|---|---|
| Jan | 186 | 80 |
| Feb | 305 | 200 |
| Mar | 237 | 120 |
| Apr | 273 | 190 |
| May | 209 | 130 |
| Jun | 314 | 240 |
填充样式
填充一根柱子的六种方式,在同一条序列上轮了一遍,好让它们可比。stripped 是最经得起密度考验的那个:它只把顶上两个角做圆,并在一层淡色底之上印出 2px 的序列色帽,所以到二十根柱子时它仍然读起来是二十个各自不同的值,而一块实心方块那时已经变成一堵墙了。其余几种是拿分量换纹理,而在没有色相可花的地方,这就是那个先于灰阶把两条序列分开的东西。
| Desktop |
|---|
| 186 |
| 305 |
| 237 |
| 273 |
| 209 |
| 314 |
堆叠与方向
两个会互相影响的选择。堆叠在每一档上问的是不同的问题——default 比较、stacked 合计、percent 占比——而 orientation 关乎的是分类名:「Organic search」放在一行旁边可以横着读完,放在一根柱子底下就得旋转或者截断。坐标轴会跟着换,而这是唯一需要调用处自己动手的那一部分:分类的 dataKey 从 X 轴挪到 Y 轴,而那根轴需要一个宽到能装下最长标签的宽度。
| Desktop | Mobile |
|---|---|
| 186 | 80 |
| 305 | 200 |
| 237 | 120 |
| 173 | 190 |
强调
说「就是这一个」的三种方式,并排放着。一张单色图表没有更亮的颜色可以去拿,所以 glowing 在那条作为这张图重点的序列背后放一圈光晕;buffer 把最后一个分类画成一片空心斜纹,那是「这一期还没走完」的惯用画法,和别的柱子一样高、却明显不是同一类事实;而 hover 高亮会把指针没落在上面的每一根柱子降到 30%。最后这一种是由指针驱动的,所以它永远不能是唯一承载某个结论的东西。
| Desktop |
|---|
| 186 |
| 305 |
| 237 |
| 273 |
| 209 |
| 314 |
| Desktop |
|---|
| 186 |
| 305 |
| 237 |
| 273 |
| 209 |
| 314 |
| Desktop |
|---|
| 186 |
| 305 |
| 237 |
| 273 |
| 209 |
| 314 |
交互
读者能拿一张图表做什么,而不是它长什么样。图例条目是带 aria-pressed 的真按钮,所以 Tab 够得到它们,Enter 能开关一条序列——一个加了点击处理器的、带样式的 div 两样都做不到——而柱子可以点击选中,也是同一个理由。tooltip 的底色和它的圆角是两个值得暴露出来的旋钮,而 defaultIndex 会在还没有任何 hover 之前就把它开在某一行上,于是这块面板在截图里、以及对一个从不移动指针的读者来说,都是可读的。两个旋钮动的都只是那块浮动面板,卡片上其余的东西一律不动——而 frosted 是让底下的图从那块面板里透上来,所以它只在面板压着柱子的地方读得出来,别处都读不出来:压在白底上,它是白色以 75% 压在白色上,那还是白色。
| Desktop | Mobile |
|---|---|
| 186 | 80 |
| 305 | 200 |
| 237 | 120 |
| 273 | 190 |
区间选择条
二十八根柱子是一根分类轴不再可读的地方,而 brush 让整条序列留在视野里,同时让读者从中挑出一段。下面那条带子用柱形标记画的是同样那些行,于是「你在从中挑选的东西」看起来就像「你在读的东西」——一个只显示缩放结果的控件做不到这一点。上面那张图随后只渲染被选中的那个窗口,所以一切读取可见行的东西,都跟着这个选择一起走。
| day | Desktop |
|---|---|
| D1 | 90 |
| D2 | 176 |
| D3 | 241 |
| D4 | 270 |
| D5 | 254 |
| D6 | 198 |
| D7 | 115 |
| D8 | 153 |
| D9 | 226 |
| D10 | 266 |
| D11 | 263 |
| D12 | 217 |
| D13 | 140 |
| D14 | 129 |
| D15 | 208 |
| D16 | 259 |
| D17 | 268 |
| D18 | 234 |
| D19 | 164 |
| D20 | 104 |
| D21 | 188 |
| D22 | 248 |
| D23 | 270 |
| D24 | 248 |
| D25 | 187 |
| D26 | 102 |
| D27 | 166 |
| D28 | 235 |
加载中
骨架,柱子的根数是手工设的。loadingBars 决定画几根柱子,不设的话默认是十二根,而这在柱状图上比在折线图上更要紧:十二根骨架之后跟着六根真柱子,读起来像是这张图丢了一半数据。标记被换掉了,但量出来的高度保住了;而这个开着的时候,旁边组合进来的 brush 或工具栏会被抑制,因为此刻还没有东西可供你从中挑一个窗口。
数值标签
把数字印在标记上,以及该印多少个。每个点都印一个数字,是一张图表被毁掉最常见的方式——那些标签会跟它们所标注的形状打架,读者两个都丢——所以默认是 last,即那个读者本来会顺着描回坐标轴去找的唯一一个值。extremes 才是「哪个月最糟」真正在问的东西;而 all 只有在「五六根柱子和它们的确切数字」就是全部重点时才配有一席之地,过了那个点,它就是一张穿着图表外衣的表格。
| Desktop |
|---|
| 1,860 |
| 30,500 |
| 23,700 |
| 27,300 |
| 9,200 |
| 31,400 |
声音化
屏幕上两条序列,值得去听的只有一条。keys 收窄这次播放,因为播放是顺序的——每条序列完整播完,一条接一条——而听的人很少想要全部。formatValue 决定那段播报怎么念,于是范围是按坐标轴印出来的单位读出来的,是「从 $9,200 到 $31,400」而不是「9200」;不写它,念出来的范围就退回默认的刻度格式,而那并不是旁边那根坐标轴上的那个。
| month | Revenue | Refunds |
|---|---|---|
| Jan | 18,600 | 1,240 |
| Feb | 30,500 | 980 |
| Mar | 23,700 | 2,310 |
| Apr | 27,300 | 1,150 |
| May | 9,200 | 3,040 |
| Jun | 31,400 | 870 |
工具栏
工具栏最多五个控件,而且不附带溢出菜单,这既是一个尺寸上的决定,也是一个设计上的决定:一行永远不会超过五个 44px 目标的东西并不需要它,而那个菜单会让每一张笛卡尔图表的每一个使用者,都要背上图表静态引入的那个 Radix 菜单。一张放在窄卡片里的图表改成少放几个控件——zoom={false},或者只留一种导出格式而不是两种。这两个文件刻意不是同一幅画:PNG 是此刻这张图的样子,会跟着缩放走,而 CSV 永远是完整的数据集,就像这个图形隐藏的那张数据表一样,因为一份悄悄少了你缩放时掠过的那些行的表格,是一种你看不见的数据丢失。
Arrow keys pan by one point, Page Up and Page Down by a screenful, Home and End jump to the ends. Plus and minus zoom, zero resets. Ctrl and the wheel zoom around the pointer, and dragging across the plot zooms to that span.
Showing Jan 25 to Dec 26, 24 of 24 points
| month | Desktop |
|---|---|
| Jan 25 | 1,200 |
| Feb 25 | 1,534 |
| Mar 25 | 1,837 |
| Apr 25 | 2,077 |
| May 25 | 2,235 |
| Jun 25 | 2,296 |
| Jul 25 | 2,258 |
| Aug 25 | 2,131 |
| Sep 25 | 1,932 |
| Oct 25 | 1,687 |
| Nov 25 | 1,428 |
| Dec 25 | 1,189 |
| Jan 26 | 999 |
| Feb 26 | 884 |
| Mar 26 | 861 |
| Apr 26 | 937 |
| May 26 | 1,108 |
| Jun 26 | 1,360 |
| Jul 26 | 1,669 |
| Aug 26 | 2,005 |
| Sep 26 | 2,337 |
| Oct 26 | 2,631 |
| Nov 26 | 2,861 |
| Dec 26 | 3,004 |
类型
export type BarVariant = 'default' | 'hatched' | 'duotone' | 'duotone-reverse' | 'gradient' | 'stripped'
export type BarStackType = 'default' | 'stacked' | 'percent'
export type BarOrientation = 'vertical' | 'horizontal'无障碍
- title 必填;数据行同样会渲染成一份视觉隐藏的表格。
- 每根柱子都带一块看不见的通高命中区,所以刻度底部那根 3px 的柱子和满高的一样好点。
- 可点击的图例项是真正的 button 并带 aria-pressed,不是挂了点击事件的 div。
- 错峰生长动画锚定在图表自身的起始时刻、而不是每根柱子的挂载时刻,所以悬停不会让它重播——减少动态偏好下则整个去掉。