Skip to main content

标注能力(P0):设计与实施计划

状态:P0.1 / P0.2 / P0.3 已实现并发布(见下方「实施结果」)。起因:盘点图表覆盖度时发现,「目标线 / 阈值线 / 异常点 / 目标区间」 这类标注在业务里出现频率极高(预算线、SLA 阈值、告警线、目标区间),而本库完全没有 —— 它比「再多一种图表类型」更能解决实际诉求:一条目标线能顶掉很多"想要新图表"的需求。

1. 定位与边界

  • 不引入新的系列类型。标注是坐标系上的图层:数据来自 option、几何来自坐标轴的比例尺, 与系列数据无关,因此不该混进 series 里(否则图例、堆叠、数据索引都会被它污染)。
  • 与已有的三个组件同族:GridLines(网格线)、Crosshair(准星)、Highlight(高亮)—— 标注是它们的"用户可配置版本"。
  • 明确不做:地图 / 地理。重(地理数据 + 投影 + 交互),生态里有专业方案,需要的应用可通过 自定义系列注册口自行接入。这条应写进定位文档,避免每次都被问一遍。

2. 接口草案(option 形态)

annotation: {
// 标注线:水平/垂直线,可按数值或类目定位
lines: [
{ axis: 'y', value: 3000, text: '目标 3000', color: '#f59e0b', dashed: true, textPosition: 'end' },
{ axis: 'x', value: '18', text: '上线' },
],
// 标注点:在坐标系里定位一个点(异常点 / 事件点)
points: [{ x: '18', y: 2100, text: '异常点', color: '#ef4444', symbol: 'circle' }],
// 标注区间:沿某轴的一段(达标区 / 维护窗口)
areas: [{ axis: 'y', from: 0, to: 1000, color: 'rgba(16,185,129,0.08)', text: '达标区' }],
}

约束:

  • 纯数据、可序列化(与快照 / DSL 一致;不出现函数,text 由调用方给字符串)。
  • 定位走现有比例尺:数值轴按值、类目轴按类目名或下标、时间轴按时间戳/日期串。
  • 越界的标注不画(不裁剪成半条线),并在诊断里给出提示 —— 与现有 validate* 系列的结构化诊断同款。
  • 图层位置:在系列之上、工具层之下(标注要被系列遮住还是压住?→ 压住 ✓ 因为它是"说明")。

3. 渲染与像素纪律

  • 复用 GridLines 的绘制入口与 Axis 的比例尺,不新造坐标系。
  • 文字与线段都要进 stylePaintPad 口径的落墨盒__localBox / __paintWorldBox), 否则会重演「墨迹超出盒子 → 脏矩形/离屏缓存切掉半截」那类问题(引擎文档里有专门的铁律)。
  • 标注随缩放 / 平移 / 联动一起动:它读比例尺,不读像素,因此天然跟随(与 GridLines 一致)。

4. 交互

  • 默认不参与命中(点标注不应抢走下面的数据点);tooltip 里以独立行显示当前标注值(可选,后续)。
  • 悬停高亮不作用于标注(避免与系列高亮混淆)。

5. 验收(照本仓既有惯例)

  1. 单测:三类标注的定位(数值轴 / 类目轴 / 时间轴)、越界不画、序列化往返(存盘 → 载入 → 再出图一致)。
  2. 真浏览器:新增示例页(一条目标线 + 一个异常点 + 一段达标区),截图人工复核; 并接入现有的示例页冒烟(e2e/examples-smoke.spec.ts,判据是"内容像素占比")。
  3. 门禁lint / types:check / build / 单测 / e2e 全绿(verify:full)。
  4. 像素:与「无标注」的同一张图对照,除标注覆盖区域外不得有差异(这条能挡住"画标注时把别的图层带歪")。

6. 分期

内容说明
P0.1annotation.lines最高频(目标线 / 阈值线 / 告警线),半天级 —— 已完成
P0.2annotation.points异常点 / 事件点 —— 已完成
P0.3annotation.areas目标区间 / 维护窗口 —— 已完成
P1树图、旭日图、日历热力层级与时间热力的标配;已有 treemap / heatmap 打底
P2趋势/回归线、误差棒、子弹图按需;冷门形态走自定义系列注册口

7. 实施结果(与上面设计稿的差异,都记在这里)

落点

  • src/types.tsAnnotationLineOption / AnnotationPointOption / AnnotationAreaOption / AnnotationOption / AnnotationDiagnostic
  • src/annotation/resolve.ts纯函数 resolveAnnotation() —— 数据值 → 像素 + 诊断。 它同时是「表单预览」「审计断言几何」「解释为什么不画」的唯一入口,不依赖 DOM / ctx。
  • src/components/Annotation.ts:绘制层(盒 = 整块画布,interactive: falsezIndex = 310)。
  • src/option/normalize.tsnormalizeAnnotation() 只收拢形状(值 → 像素要等比例尺,放在 resolve 里)。
  • ICEChartannotationDiagnostics() / annotationErrors()

实现时定下来的几条

  1. 区间允许一端越界(与线 / 点不同):夹到绘图区边缘只画可见的那一段 —— 这是图上 「看到一半的达标区」的正常语义。整段在窗外才不画,并给一条 out-of-range。 实现上就变成「先把两端各自夹到绘图区,再看区间是否退化成一个点」。
  2. from / to 保持数据顺序(y 轴像素是反的,所以 from 可能大于 to), 组件绘制前自己取上下界。这样审计与表单能直接按数据语义读几何。
  3. 类目轴允许写下标value: 2 → 第 3 个类目),但只在「下标不是现有类目」时启用 —— 否则数值类目(x 就是 0/1/2…)会被下标语义抢走(与 BandScale.indexOf 不做数值兜底同因)。
  4. 非直角场景说明一次就够(饼图 / 雷达 / 桑基 / 树图):一条 unsupported-scene 警告, 不逐条刷屏。
  5. 文字的描边走主题的 labelHaloColor(浅色白、深色深),复用既有铁律,没有新造机制。

验收结果(2026-09-14)

  • 单测 tests/chart/annotation.test.ts 14 条全绿(含「标注不吃命中」「横向排布」「多 y 轴 axisIndex」与「序列化往返」)。
  • 真浏览器 e2e/annotation.spec.ts 6 条全绿,其中像素判据由示例页里的 window.__annotationDemo.compare() 在页面内跑:有标注 / 无标注两张图逐像素比对, 差异像素 69493 个,落在标注墨迹盒之外的是 0 个 —— 第 4 条验收(除标注区域外零差异)达成。 另外两条也是实测出来的:存盘 → 载入 → 再出图逐像素一致(差异 0), 以及布局变化后旧位置不留残墨(y 轴刻度变窄 → plot.x 从 78 挪到 60,标注跟着挪,残墨 0 像素)。 —— 后者是标记层曾踩过的坑:这里的盒是整块画布,脏矩形按「旧盒 ∪ 新盒」清, 所以旧位置的线一定被擦掉;标记层那种小盒才需要额外操心。 探针第一次跑出来差异 7546 像素,查下来是拍摄时机问题(restore 会先建空图再载入快照, 坐标轴有一次从空态滑过来的过渡),先 finishAnimations() 再比就归零了 —— 不是序列化缺陷。
  • npm run verify:full:39 套件 / 367 用例 + 36 条浏览器用例全绿; 示例页冒烟 30 页、交互审计 334 步、悬停扫描 19 种类型均无失败。