外观
图表层
ui/charts/ 的 9 个组件与共享的 Chart.js 桥接层。色值口径见 design-tokens.md。
1. 架构:一层 canvas 宿主 + 一层主题桥
chartjs.ts ChartCanvas.svelte TrendChart / Donut / …
┌──────────────────────┐ ┌──────────────────┐ ┌────────────────────┐
│ ensureChart() │ │ 拥有 <canvas> │ │ $derived 出 config │
│ chartTheme() │◀───────│ 每个 canvas 一个 │◀───────│ 组件只管数据与配置 │
│ resolveColor() │ │ Chart 实例 │ │ │
│ 4 个自绘插件 │ │ 就地 update() │ └────────────────────┘
└──────────────────────┘ └──────────────────┘数据、格式化、主题的变化统一表现为「一个新的 config」,由 ChartCanvas 用 chart.update('none') 就地应用,而不是销毁重建。这既保住了性能,也让主题切换不闪。
颜色为什么在运行时解析
resolveColor('var(--c-data-1)') 会去 :root 上读实际值。Chart.js 不认识 CSS 变量 (它往 canvas 里画,不走 CSS 层叠),所以必须解析。
chartTheme() 里第一行是 void theme.mode;——一次刻意的反应性订阅。宿主的明暗态 一变,所有 $derived 的 config 重算,颜色随之重解析。theme.mode 因此只是重绘信号, 颜色本身仍然只有 CSS 一个来源。
主题令牌
chartTheme() 返回的是文字色的 alpha 派生,因此不需要为亮暗各写一套:
| 键 | alpha | 用途 |
|---|---|---|
grid | 0.10 | 常规网格线 |
gridDash | 0.15 | 虚线网格(日期界) |
axisLine | 0.20 | 轴线 / 外框 |
baseline | 0.30 | 参考线(比轴线略强,又不与系列竞争) |
crosshair | 0.35 | 悬停十字线 |
tick | 0.65 | 轴刻度文字 |
另外三个直接取表面令牌:tooltipBg / tooltipText / tooltipBorder。
字体
ensureChart() 里把 Chart.defaults.font.family 设成 --sans、size 设成 11。 不设的话每张图都会渲染成 Chart.js 默认的 12px Helvetica,与周围的 Inter 明显打架。
只注册用到的部件
ensureChart() 显式注册 LineController / BarController / DoughnutController 及配套元素——为了 tree-shaking。加新图表类型要在这里补注册。
外置 HTML tooltip
Chart.js 默认把 tooltip 画在 canvas 内部,在小图上会被裁掉(150px 的仪表、 火花线、110px 的时间线)。因此库里用的是一个单例 HTML 元素(.chart-tip,样式在 app.css),定位在 caret 旁并做视口夹取,可以自由溢出。
2. 四个自绘插件
Chart.js 没有内建这些,都在 chartjs.ts 里,通过 options.plugins.<id> 传参。
| 插件 | 做什么 | 参数来自 |
|---|---|---|
plotFrame | 在绘图区外围画一圈细实线框(Radar 的 chart-outer-line) | { color } |
crosshair | 悬停时在活动点位置画竖直十字线 | { color } |
zeroLine | 在固定 y 值处画横向虚线参考线;超出当前 y 范围时跳过 | { color, value } |
missingRegions | 用斜纹填充标出「无上报」的区间 | { regions, color } |
TrendChart 四个全带:plotFrame 用 axisLine 色、missingRegions 用 gridDash、 crosshair 用 crosshair 色,zeroLine 只在传了 baseline 时启用。
missingRegions 是把 null ≠ 0 这条数据口径变成视觉的地方:空桶是缺报, 画成斜纹缺口;真的没有变化才是 0。规则本身见 api/flaps.md。
3. 组件
ChartCanvas
所有 Chart.js 图表的宿主。业务代码一般不直接用它,除非要画库里没有的图。
| prop | 类型 | 说明 |
|---|---|---|
config | ChartConfiguration<any> | 由父组件 $derived 出 |
height? / width? | number | — |
fixed? | boolean | 固定尺寸画布(火花线、迷你条)vs 随容器响应(线图、柱图、环图) |
fill? | boolean | 撑满 flex 父容器的高度;此时 height 变成最小值 |
label? | string | 无障碍标签 |
plugins? | Plugin[] | 图表局部插件,必须在组件生命周期内保持不变 |
config的类型参数是any,好让ChartConfiguration<'doughnut'>这类具体化配置 不必绕unknown转型。
内部有一个容易被忽视的守卫:创建实例前先 Chart.getChart(canvas)?.destroy(), 清掉上一次挂载在这个 canvas 上残留的实例。否则开发时的 HMR 重挂会让 new Chart 抛 "Canvas is already in use",图表从此静默不再绘制。
TrendChart
Radar 式时序趋势:多系列平滑折线、可选渐变面积、虚线对比系列。
| prop | 类型 | 说明 |
|---|---|---|
series | Series[] | 见下 |
timestamps | (string | number | null)[] | ISO 串或 epoch 整数都认,内部走 parseTs |
height? | number | 默认 200 |
fill? | boolean | 撑满 flex 父高;height 变成最小值 |
zeroBased? | boolean | y 轴从 0 起 |
baseline? | number | 在该 y 值画虚线参考线并保证它落在 y 范围内 |
format? | (v) => string | tooltip 数值格式化,默认 fmtNum |
tickFormat? | (v) => string | y 轴刻度单独的格式化(例如紧凑记数) |
leftAxisWidth? | number | 强制左轴宽度,让本图的绘图区与下方另一张图对齐 |
ts
interface Series {
label: string;
color: string;
values: (number | null)[]; // null = 空桶 → 画成缺口
fill?: boolean; // 主系列的渐变面积
dash?: boolean; // 对比系列(上一周期)
step?: boolean; // 阶梯线
}step 不是审美选择:计数类序列(路由数、对端数)在两次采样之间并不连续变化, 阶梯线才是语义正确的画法。
对比系列用同色虚线,不是另一种颜色——这样「本期 vs 上期」读起来是同一条指标的 两个时间窗,而不是两个指标。
leftAxisWidth 解决的是上下堆叠两张图时绘图区左边缘对不齐的问题(两张图的 y 轴 刻度位数不同)。
Sparkline
内联的小趋势线 / 面积图,固定尺寸。
ts
values: (number | null)[]; // null = 空桶,画成缺口
width?: number; // 默认 120
height?: number; // 默认 34
color?: string; // 默认 var(--c-data-1)
fill?: boolean; // 渐变填充,默认 true
strokeWidth?: number; // 默认 1.5
smooth?: boolean; // 默认 true
interactive?: boolean; // 悬停读数
fluid?: boolean; // 拉伸到容器宽度(此时 width 被忽略)
format?: (v: number) => string;内部对
values做了一次浅拷贝。Chart.js 会用Object.defineProperty给数据数组 装桩,这在$state代理数组上会抛异常。传 runes 状态数组进来是安全的,就是因为 这个拷贝。
BarChart
按时间桶的堆叠竖条。
ts
groups: { label: string; parts: { key: string; value: number; color: string }[] }[];
height?: number; // 默认 130
maxLabels?: number; // 默认 6,x 轴标签抽稀
leftAxisWidth?: number; // 与上方图表对齐用的固定左槽分段的顺序与颜色按首次出现确定并在各组间保持稳定——否则某一桶缺了某个 key 时, 其上下的颜色会错位。
Donut
环图 / 半环,Chart.js doughnut 支撑。悬停高亮分段并在中心显示其数值。
ts
segments: { label: string; value: number; color: string }[];
size?: number; // 默认 150
thickness?: number; // 默认 20
centerValue?: string | number;
centerLabel?: string;
half?: boolean; // 半环(仪表)形态
format?: (v: number) => string; // tooltip / 悬停中心的格式化中心文字是 HTML 覆盖层——Chart.js 没有原生的中心标签。
half的实现里有一处必须知道的几何修正:Chart.js 把 180° 弧垂直居中在方形画布里 (getRatioAndOffset),半圆的顶边落在size/4、赤道落在3·size/4,而不是0…size/2。所以要把画布上提size/4,裁切窗口才正好露出完整半圆。按0…size/2裁只会看到拱顶那一条细弧。
HalfDonutGauge
半环仪表 + 弧上下各一组 .pair 统计。仪表盘的流量构成与 FleetRouting 的 RPKI / 协议族仪表共用这一套解剖。
ts
segments: { label: string; value: number; color: string }[];
size?: number; // 默认 190
thickness?: number; // 默认 28
top: { color: string; label: string; value: string }; // value 已格式化,如 '63%'
bottom: { color: string; label: string; value: string };
format?: (v: number) => string; // tooltip 格式化RateGauge
对数刻度的半弧速率表盘(对齐 FlapAlerted)。
ts
value: number; // 900s 90 分位截尾均值(rate_per_s)
size?: number; // 默认 220它是「一个值配一把刻度」的主角数字,不是百分比环(那是 HalfDonutGauge 的活)。
刻度是固定的四段等长弧:0→1→10→100→1000(次/秒)。1 以下线性、各数量级之间取对数。 理由是量程:平稳期机群在 1/s 以下,全网事件期能到几百(生产实测 214/s)——线性扫描 会让指针永远贴在某一端。
超出量程的值夹取到满弧,但主角数字仍然说真话。
填充色是中性数据色:fleet 级没有定义阈值,表盘陈述量级,不做判断。
ShareBar
比例条,纯 CSS / SVG,不走 Chart.js。上方一行图例点 + 标签 + 大号百分比,下方一条 分段条。
ts
segments: { label: string; value: number; color: string }[];
height?: number; // 默认 30
showHeader?: boolean; // 图例 + 百分比行,默认 true
format?: (v: number) => string; // tooltip 的绝对值格式化,默认 fmtNum参数(2px 间隙、2px 圆角、< 0.1% 的处理)是从 Radar 的 StackedBar 源码里读出来的, 详见 design-tokens.md。
小于 0.1% 的段渲染成 < 0.1% 并保留一条细缝宽度,而不是消失。
ChartLegend
Radar 式的紧凑 HTML 图例。用 HTML 而不是 Chart.js 内建,这样字体与间距跟设计系统 走。
ts
items: {
label: string;
color: string;
dash?: boolean; // 对比系列 → 虚线 swatch
line?: boolean; // 线系列 → 实线 swatch(Radar 的 22×6 标记)
value?: string | number; // 标签后的加粗数值
}[];
column?: boolean; // 竖排(环图侧边图例)三种 swatch 形态(点 / 实线 / 虚线)不是装饰:它们区分「分类项 / 线系列 / 对比系列」, 让图例本身就说明了那条线在图上长什么样。
4. 加新图表时
- 在
ensureChart()里注册需要的 controller / element; - 组件里
$derived出 config,颜色一律经resolveColor(); - 主题相关的值取自
chartTheme(),不要自己读 CSS 变量拼 alpha; - 交给
ChartCanvas渲染,不要自己new Chart; - 数量序列用
--c-data-*,状态序列用--c-ok/warn/bad,两套不混; - 空桶画缺口或斜纹,不要画成 0。