# ECharts 引擎迁移 + 双框架 demo 展示页重做 — 设计

> 日期: 2026-07-01
> 状态: 草案,待 owner review
> 起因: react-pilot demo(App.tsx dirty 未提交)展示效果不完整 → 诊断出 Chart 渲染 bug + demo 潦草 → owner 决定 Chart 引擎按团队标准迁移到 ECharts,并要求 demo 展示页零硬编码。

---

## 1. 背景与动机

### 1.1 Chart 渲染 bug 诊断结论(已确诊,非本 spec 要"修"的对象)

对照实验(同一份 `src/components/Chart/Chart.vue` 两条渲染链):

| 渲染链 | 结果 |
|---|---|
| Vue docs 站直接渲染(非 shadow DOM) | 正常 |
| 编译成 Web Component、shadow DOM 内、React wrapper 包(react-pilot) | canvas 溢出裁切、pie 图例丢失 |

**根因**: [Chart.vue](../../../src/components/Chart/Chart.vue) 用 chart.js `responsive: false` + `maintainAspectRatio: false`,且 width/height **只设了外层 `<div>` 的 CSS、从没传给 canvas 本身**。`responsive:false` 下 canvas 只认自身 width/height attribute,缺失时回落 chart.js 硬编码默认(pie 300×300 / bar 300×150)。Vue 直接渲染链恰好靠常规 DOM 布局让 canvas 隐式贴合容器;shadow DOM 内该隐式路径不成立 → canvas 掉到默认值 → 溢出。

**关键判断**: 这**不是** shadow DOM 破坏 canvas,**不是** chart.js 引擎缺陷,而是 canvas 尺寸绑定用法缺陷。因此"换 ECharts"不是这个 bug 的必要解药——但 owner 出于**团队/产品标准对齐**(消费端产品已用 ECharts)决定迁移,这是独立且合理的战略动机。迁移会一并、彻底地重写渲染,顺带消除该 bug(迁移即止血,不单独做 chart.js 补丁)。

### 1.2 制度性缺口(记录,增强项)

render-gate 对 Chart **只校验 `.tvu-chart` wrapper 的盒子(box/padding/radius/bg/legend-text),canvas 像素明确不在校验范围**([components.config.ts:1104-1108](../../../src/web-components/components.config.ts#L1104-L1108))。这正是溢出 bug 一路溜到肉眼才被发现的原因。SVG 渲染器让图表内容首次可被 gate introspect(见 §3.4)。

---

## 2. 范围与不变量

两个 workstream,WS-B 的 Chart 展示依赖 WS-A 完成:

- **WS-A**: Chart 组件内部引擎 chart.js → ECharts 迁移。
- **WS-B**: 双框架 demo 展示页(Vue docs `ChartPage` 及总览 + React `react-pilot/App.tsx`)重做——零硬编码 token 化 + 全变体矩阵 + 修 Badge 用法 + Vue/React 对等。

### 硬不变量(贯穿两 WS)

- **对外 API 不变**(non-breaking): Chart props `type / datasets / labels / width / height`、类型 `ChartType` / `ChartDatasetInput` 原样。→ 不需要 major changeset(内部实现变更,patch/minor)。
- **不改**: `src/canonical/Chart.vue` · WC 注册(tag `tvu-chart` / props) · React wrapper(`react-pilot/src/wrappers/Chart.tsx`) · React 侧组件代码。React→WC→Vue 桥零改动——它们只认 props,不认引擎。
- **零字面 hex**(AGENTS 硬规则 #4): 组件与 demo 里都不写死颜色;hex fallback 只留在 `use-chart-tokens.ts` 解析层。
- **配色沿用现有 12 色 token 板** `--chart-color-1..12`(theme-aware),零 divergence 改动。
- **不碰已发布 Vue 包的其它组件**;WS 只触及 Chart + demo 展示页 + 依赖。

---

## 3. WS-A — ECharts 引擎迁移设计

### 3.1 依赖

- 移除: `chart.js`、`vue-chartjs`。
- 新增: `echarts`(仅按需子路径 import,禁止 `import * from 'echarts'` 全量)。

### 3.2 按需注册清单(tree-shaking 核心)

```
echarts/core                         // init, use, getInstanceByDom
echarts/renderers → SVGRenderer      // 见 §3.4
echarts/charts    → PieChart, BarChart, LineChart
echarts/components→ LegendComponent, TooltipComponent, GridComponent
```

只注册用到的;6 种 type 全部落在这几个 series 上。

### 3.3 props → ECharts option 映射

| type | ECharts 映射 |
|---|---|
| `pie` | PieChart,默认 radius |
| `donut` | PieChart,`radius: ['40%','70%']` |
| `line` | LineChart + category xAxis |
| `bar` | BarChart + category xAxis |
| `bar-horizontal` | BarChart,value xAxis / category yAxis(轴对调) |
| `line-bar` | 混合:按 `datasets[].type`('line'/'bar')分别生成 series,共享 category xAxis |

- `datasets: { label, data, type?, color? }[]` → ECharts `series[]`;`labels` → xAxis category(直角系)或 pie 各扇区 name。
- 配色: `option.color = resolveChartPalette()`(现有 12 token 色);`datasets[].color` 若给则覆盖该 series。
- 图例(对齐现有 Figma legend 规格,node 4959): `legend` bottom-center,`icon: 'circle'`,`itemWidth/itemHeight: 8`,`textStyle` = `--text-2` 色 + `--font-family-base` + 12px。
- **将 `props → option` 抽成纯函数** `buildChartOption(props, palette, legendColor, fontFamily)`(见 §3.6 可测性)。

### 3.4 渲染器: SVGRenderer(owner 已拍板)

选 SVG 而非 Canvas,三条理由:
1. 规避整类 canvas 尺寸/DPR 坑(当前 bug 即此类);SVG 无 canvas 尺寸 attribute 那套,shadow DOM 内更稳。
2. SVG 是真实 DOM,`getComputedStyle`/查节点可行 → render-gate 首次能验图表内容(补 §1.2 缺口)。
3. 设计系统展示型图表数据量小,canvas 的大数据性能优势用不上。
- 代价: 超大数据量(数千点)SVG 慢于 canvas——设计系统不碰该量级,接受。

### 3.5 shadow DOM 里的 init / resize / theme(核心技术)

Chart.vue 内部(直接 `echarts/core`,不引 vue-echarts):

- `onMounted`: `const chart = echarts.init(chartRef.value, null, { renderer: 'svg', width: props.width, height: props.height })` — **显式尺寸**,直接吸取 bug 教训,不依赖容器自动测量。
- `setOption(buildChartOption(...))`。
- `watch(() => [props.type, props.datasets, props.labels])` → `setOption`(可 `notMerge` 视情况); `watch(() => [props.width, props.height])` → `chart.resize({ width, height })`。
- 主题切换: **复用现有** `MutationObserver(document.documentElement, { attributeFilter: ['data-theme'] })` → 重解析 `resolveChartPalette()` / `resolveLegendColor()` → `setOption`(color 更新)。
- `onUnmounted`: `chart.dispose()` + `observer.disconnect()`。
- 空/异常输入: `datasets` 为空 → 渲染空坐标系(不报错);init 前断言 `chartRef` 存在。

### 3.6 可测性 & hex 纪律

- `buildChartOption` 为纯函数(不碰 DOM/echarts 实例)→ vitest 直接断言 option 结构(type→series 映射、color 来自传入 palette、legend 配置),无需 mock init。
- Chart.vue 与 option 里只喂**解析后的字符串**,零字面 hex;fallback hex 仍只在 `use-chart-tokens.ts`。

---

## 4. WS-B — 双框架 demo 展示页重做设计

### 4.1 目标

Demo 展示页成为"设计系统的活样例":零硬编码、全变体、Vue/React 对等。当前 `react-pilot/src/App.tsx`(dirty 未提交)那版**不直接入库**——它有 Badge 用法 bug、inline 硬编码、Chart 溢出、变体不全,由本 WS 重做后再入库(闭合最初"App.tsx 要不要 commit"问题:重做后入库)。

### 4.2 零硬编码 token 化(owner 要求)

- 颜色/圆角/字体/常规间距 → 全 `var(--token)`,**连 fallback hex 一并去掉**(`var(--x,#141414)` 的 `#141414` 也是硬编码且掩盖 token 缺失)。可用: `--bg-layer1..4` / `--brand` / `--line-border`/`--line-deep` / `--text-*` / `--sp-xxs..xxxl` / `--r-xs..xxl` / `--font-family-base`。
- **边界(诚实标注,不硬凑)**: 纯展示脚手架的页面级布局值(整页 `max-width`、demo 网格 `gap`、卡片 `min-width`)——设计系统 token 覆盖组件级语义,未必有"展示页宽度"。优先套最接近的 `--sp-*`;实在无对应语义的极少数值,以注释标为 `/* demo layout, not a component token */`,不伪造 token 匹配(避免 tail-wagging-the-dog)。

### 4.3 全变体矩阵

每个组件展示其有意义的变体集,而非单一代表:
- Message: success / info / error / warning × (M/L)。
- Button: color × variant × size × radius × status(含 **loading**)有代表性的组合矩阵。
- 其余组件同理按其 axis 铺变体。
- **修 Badge 用法**: canonical Badge 内容组件内部固定生成(Circle→'5' / Rectangle→按 color 出词),**不传 children**;展示 `color × tag × type` 变体矩阵。
- Chart: 6 种 type 全展示(依赖 WS-A 完成,渲染正常)。

### 4.4 Vue / React 对等

- Vue 侧: docs 站现有 `ChartPage.vue` 及组件页/总览。
- React 侧: `react-pilot/src/App.tsx` 重做。
- 两侧展示同一变体集,便于交叉核对双框架一致性。

---

## 5. 测试与 gate

- **vitest**: `buildChartOption` 纯函数单测(6 type 映射 / color / legend);Chart.vue 挂载测试在 jsdom(SVG renderer 不需 canvas API,jsdom 友好)。
- **render-verification(Vue 930 + React)**: wrapper computed-style gate 不变;API 未变 → React 侧 cross-gate **应零回归**,跑一遍确认。
- **新增(增强,补 §1.2 缺口)**: 断言 Chart 的 SVG root 尺寸 ≤ `.tvu-chart` 容器(不溢出)——canvas 时代做不到,SVG 可做。
- **双框架 demo 实测**: App.tsx + Vue ChartPage playwright 截图,确认 6 种 Chart 不再溢出、图例显示、两侧一致。
- **既有 audit 全绿**: `audit:self-audit-phase2` / `audit:render-drift-gate` / `audit:binding-config-parity` / vue-tsc。

---

## 6. bundle 影响

按需 `echarts/core` + SVG + 3 charts + 3 components 预计显著小于全量 echarts;记录迁移前(chart.js+vue-chartjs)vs 迁移后 WC build 体积对比,写入迁移记录。

---

## 7. 风险与回滚

| 风险 | 缓解 |
|---|---|
| SVG renderer 在 shadow DOM 内某 type 表现异常 | init/resize 显式尺寸 + 逐 type playwright 截图验;异常则单 type 排查,非整体回退 |
| ECharts 在 jsdom 单测环境报错 | SVG renderer 无 canvas 依赖;buildChartOption 纯函数测覆盖主要逻辑,挂载测退化为 smoke |
| cross-gate 回归 | API 未变;若回归说明 wrapper/桥被误动,立即定位 |
| 依赖切换遗漏(chart.js 残留 import) | grep 全仓 `chart.js`/`vue-chartjs` 归零 |

回滚: WS-A 改动集中在 Chart.vue + package.json,git revert 即回 chart.js 版。

---

## 8. 非目标(YAGNI)

- 不扩展 Chart 对外 API(owner: 无对外调整需求)。
- 不新增图表类型(保持现有 6 种)。
- 不对齐消费端 ECharts 的自定义 theme(配色沿用设计系统 token 板)。
- 不做大数据量性能优化(设计系统展示型场景不需要)。
- 不改其它组件(仅 Chart + demo)。

---

## 9. 实施顺序(交 writing-plans 细化)

1. WS-A: 依赖切换 + Chart.vue 重写(echarts/core + SVG + buildChartOption 纯函数 + shadow init/resize/theme)。
2. WS-A 验证: vitest + render-verification + 双框架 playwright 截图(6 type 不溢出)。
3. WS-B: demo 展示页重做(token 化 + 全变体 + 修 Badge + Vue/React 对等)。
4. WS-B 验证: playwright 截图两侧对等 + 零硬编码自查。
5. 全 gate 绿 + owner ack → 入库(含重做后的 App.tsx),双 remote 推送。
