图表
AreaChart - 面积图
连续轴上的一条填充序列,面积本身是有意义的。
用法
什么时候用它
来自@misoto22/folio/charts
import { AreaChart } from '@misoto22/folio/charts'说明
分类轴上的一条填充序列——用来表达一个连续的量,且线下方的面积本身有意义。
是组合出来的,不是配置出来的:坐标轴、网格、tooltip、图例以及区域本身都是子元素,所以一张图只渲染它要的那些部分,不存在被某个看不见的属性偷偷打开的东西。
当读者是在把几条序列互相比较、而不是读一个总量时,改用 <LineChart>;当分类是离散的桶时,用 <BarChart>。
结构
| 部件 | 说明 |
|---|---|
| 图框必填 | ChartFigure 画出来的那个 <figure>,名称走 aria-labelledby,而不是交给 figcaption——从 <figcaption> 推名称,只在部分读屏软件里能生效。caption 把 title 和 description 装在一起,在设置 showTitle 之前是 sr-only 的,所以写进 description 的告诫会被播报出来,却永远不会印在页面上。 |
| 绘图区必填 | ChartContainer:一个 16:9 的盒子,下限 13rem、上限 26rem,也是唯一把 Recharts 写死的 #ccc 坐标轴与网格描边重新指向 --chart-grid 和 --chart-axis 的地方。 |
| 面积必填 | <AreaChart.Area>,一条序列一个。每个都会自己生成 id,并把渐变、纹理图案和揭示蒙版都挂在这个 id 之下,所以六种 variant 共处一张图时,谁也不会覆盖谁的定义。 |
| 刷选条 | <AreaChart.Brush>,渲染在容器的页脚里、而不是 SVG 内部。两个把手都是 role="slider",并用 aria-valuetext 报出自己所在的那一行,所以这个窗口用方向键就能够到。 |
| 工具栏 | <AreaChart.Toolbar>,绘图区上方一排 role="group" 的图标按钮,最多五个、每个 44px。组合它的同时也会打开绘图区自己的滚轮、拖拽和键盘缩放,两者驱动的是同一个窗口,不是两个。 |
| 隐藏数据表 | 一张 sr-only 的 <table>,取的是完整数据、而不是刷选出来的窗口,所以读表格的人拿到的绝不会比 CSV 导出里的少。hideDataTable 会把它去掉,零行则什么都不渲染。 |
实践建议
推荐
- 只堆叠那些真的能相加的量。在 stackType="stacked" 下,一条带的高度是它自己的值,但它的位置是它下面所有值的和,所以把四个互不相干的比率堆起来,画出的是一条没人测量过的累计线。
- 读的是占比而不是体量时,用 stackType="expanded":它会设置 Recharts 的 expand 偏移,<AreaChart.YAxis> 也会自己换上 percentTick,所以调用处不写格式化函数,坐标轴也是从 0% 读到 100%。你自己传的 tickFormatter 仍然胜出——坐标轴会让位给它,而不是把它丢掉,从前它正是一声不吭地把它丢掉的。
- 只有两块面积的图上,先换 variant,再动灰阶梯度。在单色默认方案里,六种填充才是主要编码;而在强制颜色模式下,每一个 --series-* 令牌都会解析成 CanvasText——到那一步,能把两块面积分开的就只剩纹理了。
- 要传 xDataKey。它是隐藏表格的 rowKey,不传的话表格根本不会渲染行表头那一列:读屏用户拿到的是一列数字,旁边没有月份。
避免
- 四层半透明的填充叠在一起,正是这种图形的失效方式——第三块面积要透过两层 --chart-fill 去看,它自己的高度已经还原不出来了。几条序列互相比较,那是 LineChart 的事,在那里谁也不遮挡谁。
- connectNulls 默认是 false,这是有原因的:打开之后,数据里的缺口会被画成一条直线段,和一段真实测出来的平稳期分辨不出来。只有当缺口是渲染产生的假象、而不是缺失的观测时,才去设它。
- 只有一行数据什么都画不出来。一个点没有可填充的线段,dot 在没有组合 <AreaChart.Dot> 时是 false,而空状态也不会触发——因为数据行确实存在——于是坐标轴就画在一张空白的绘图区上。
示例
默认
两条序列,两种纹理。在单色的默认设定下,主要把它们分开的是填充,灰阶只是配合,而这也正是能在灰度打印和色觉障碍读者那里活下来的东西。当曲线底下那个量本身就是要读的东西时,去拿面积图,而不是折线。
| month | Desktop | Mobile |
|---|---|---|
| Jan | 186 | 80 |
| Feb | 305 | 200 |
| Mar | 237 | 120 |
| Apr | 273 | 190 |
| May | 209 | 130 |
| Jun | 314 | 240 |
填充样式
六种填充,在同一条序列上轮了一遍,好让差别可比。在没有色相可花的地方,这是那条先于灰阶把两片面积分开的轴,所以一张不止一片面积的图,应该先变它、再去变别的。gradient 是默认值,会朝着坐标轴消散;solid 读起来是一条带子而不是一道斜坡;而那三种纹理,是在灰度打印里仍然能把两条序列分开的那几种。
| Desktop |
|---|
| 186 |
| 305 |
| 237 |
| 273 |
| 209 |
| 314 |
描边与曲线
线条本身的两条轴,而它们处在不同的层级上:strokeVariant 属于某一片面积,curveType 属于整张图。animated-dashed 是一个 SMIL 循环,样式表里的 reduced-motion 规则够不着它,所以组件替它把关,并且是整个去掉、而不只是放慢。curve 是那个会改变这张图所宣称的内容、而不只是它长相的设定——step 说的是两行之间什么都没测过,monotone 说的是这个量平滑地移动过,而那些行本身没法告诉你哪一个是真的。
| Desktop |
|---|
| 186 |
| 305 |
| 237 |
| 273 |
| 209 |
| 314 |
堆叠
三个不同的问题,不是三种长相:default 比较两条序列,stacked 读它们的合计,expanded 读它们的占比——而第三种下,数值轴会自己切换成百分比。堆叠藏起来的是基线:只有最下面那条带还坐在一条直线上,所以它上面每一条带都是一个形状,其起伏有一部分来自它底下那条,这也正是为什么「移动端有没有增长」仍然要用 default。
| Desktop | Mobile |
|---|---|
| 186 | 80 |
| 305 | 200 |
| 237 | 120 |
| 273 | 190 |
| 209 | 130 |
| 314 | 240 |
标记点与入场
标记点和入场擦除是同一个主题,因为它们共用一张遮罩:它同时盖住面积的填充、它的描边和它静止时的圆点,于是标记点是跟着线一起到达的,而不是抢在它前面蹦出来,后者正是把两者分开做动画时会发生的事。图上那个 key 才是重播这次揭示的东西——单独改 animationType 不会让这片面积重新挂载。一次揭示是一张逐帧动画的 SVG 遮罩,也是这个包里最重的东西,这也是为什么 none 是一个正经答案,以及为什么操作系统的减少动效偏好也落在这里。
| Desktop |
|---|
| 186 |
| 305 |
| 237 |
| 273 |
| 209 |
| 314 |
区间选择条
三十二天已经超出一张图能一次显示的范围,而 brush 是那个既让整条序列保持可见、又让读者挑出其中一段的答案。它是一个子元素、而不是一个 showBrush 属性,而且两个手柄都是真的滑块控件:Tab 到其中一个,方向键就能一步步移它,Home 和 End 把它跳到两端,而它移植自的那个形态是只认指针的。代价是图表下面多出一张缩微的图,以及它占掉的高度,所以一张只有六行的图并不需要它。
| day | Desktop |
|---|---|
| D1 | 140 |
| D2 | 164 |
| D3 | 185 |
| D4 | 202 |
| D5 | 214 |
| D6 | 220 |
| D7 | 219 |
| D8 | 211 |
| D9 | 199 |
| D10 | 184 |
| D11 | 169 |
| D12 | 154 |
| D13 | 143 |
| D14 | 136 |
| D15 | 136 |
| D16 | 142 |
| D17 | 155 |
| D18 | 173 |
| D19 | 195 |
| D20 | 219 |
| D21 | 242 |
| D22 | 263 |
| D23 | 280 |
| D24 | 291 |
| D25 | 295 |
| D26 | 293 |
| D27 | 285 |
| D28 | 273 |
| D29 | 257 |
| D30 | 242 |
| D31 | 227 |
| D32 | 217 |
加载中
一张真实仪表盘图表大半辈子都待着的两种状态之一,也是那个说「还没有」而不是「这里什么都没有」的状态。骨架的各行是按行下标做种子生成的,不是拿 Math.random 画的,所以服务端和客户端渲染出同一个形状,一张加载中的图表不会记下一条水合不匹配——而且它保住了图表量出来的高度,所以数据落下来时页面不跳。在 prefers-reduced-motion 下,那道行进的高光会被换成一层平铺的淡色。
彩色色板
往里加色相的那条获准的路,也是唯一的一条:任意一个祖先上的那个属性,会把八个序列自定义属性重新指向一套在两种底色上都验证过的分类调色板,而下面这张图里没有任何地方提到过颜色。另一条路是在 config 的 colors 数组里手挑十六进制色值,而那正是一套未经验证、对色觉障碍不友好的调色板诞生的地方。浅色那几档里有三档在纸色上低于 3:1,而这正是「一条序列以上图例就不是可选项」这件事被写下来的理由。
| month | Desktop | Mobile | Tablet |
|---|---|---|---|
| Jan | 186 | 80 | 40 |
| Feb | 305 | 200 | 72 |
| Mar | 237 | 120 | 55 |
| Apr | 273 | 190 | 88 |
| May | 209 | 130 | 61 |
| Jun | 314 | 240 | 96 |
标注
三层,按编辑型制图沉淀下来的顺序绘制,而这个顺序由包来固定、不由它们被组合的先后决定:色带是背景语境,坐在网格后面;参考线是关于数据的一个断言,坐在标记之上;而注记解释这两者,坐在一切之上,包括 hover 出来的那个点。大多数看起来需要第二条序列的图表,其实需要的是一条参考线——「我们在线上还是线下」,一条阈值一眼就答得了,第二条序列答不了。
| month | Desktop |
|---|---|
| Jan | 186 |
| Feb | 305 |
| Mar | 148 |
| Apr | 162 |
| May | 289 |
| Jun | 341 |
空数据
一张真实仪表盘一周之内就会到达的另一种状态——一个什么都匹配不上的筛选——也是值得为它写一句话的那一种,因为一对空坐标轴和一张加载失败的图表毫无分别,于是读者刷新页面,它还是空的。它和加载骨架是两回事:那一个说「还没有」,这一个说「这里什么都没有」,而且它通常还附着一件接下来能做的事。想保留光秃秃的坐标轴,就传 empty={false},用于那种「空本身就是结论」的图。
No visits in this range
This project had no traffic before March. Try a wider range.
声音化
那张隐藏的数据表把每一个数字都交给了用读屏软件的人,那是可及性,却不是一个形状——六个数字一个一个念出来,说不出三月凹下去过,而一串音调能,只要不到两秒。这个控件在第一个音符之前,会先播报点的个数、分类轴的跨度和两个极值,于是这段旋律是一次测量、而不是一条轮廓。在有人按下按钮之前,什么都不会播放:这里没有 autoPlay 属性,也没有任何 effect 能启动一次播放。
| month | Desktop |
|---|---|
| Jan | 186 |
| Feb | 305 |
| Mar | 148 |
| Apr | 162 |
| May | 289 |
| Jun | 341 |
类型
export type ChartCurveType = ComponentProps<typeof RechartsArea>['type']
export type AreaVariant = 'gradient' | 'gradient-reverse' | 'solid' | 'dotted' | 'lines' | 'hatched'
export type AreaStrokeVariant = 'solid' | 'dashed' | 'animated-dashed'
export type AreaStackType = 'default' | 'stacked' | 'expanded'键盘操作
| 按键 | 作用 |
|---|---|
| Tab | 进入图表,Recharts 的可访问层让它可以被导航。 |
| ←→ | 在数据点之间移动光标,并逐个播报。 |
无障碍
- title 是必填的,它就是这张图的可访问名称,无论是否显示出来。
- 数据行会再渲染一份视觉隐藏的表格,所以数字是可以被读到的,而不只是被画出来。页面自己已经列了数据时,用 hideDataTable 关掉。
- 六种填充样式的存在是有原因的:在默认的单色方案里,纹理才是身份的第一载体,灰阶只是第二层——这也是两条序列在黑白打印和强制颜色模式下仍能被区分的原因。
- 入场揭示是一层逐帧的 SVG 蒙版,在 prefers-reduced-motion 下会被完全去掉,爬行虚线同理。