Files
smart-fin-chart/CLAUDE.md
T
2026-08-23 21:23:41 +08:00

91 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 图表开发约定
## 新建图表时
必须从 `theme.js` 取共享配置,不要手写等价的值:
```js
import {
PALETTE, FONT, axisCommon, periodAxis, titleStyle,
lineSeries, barSeries, standardLegend, footnote, watermarks, LAYOUT,
} from '../theme.js';
```
- 折线用 `lineSeries()`,柱状用 `barSeries()` —— 空心圆点、标签强显等细节已内置
- 图例用 `standardLegend(names)`,间距用 `LAYOUT.legendTop` / `LAYOUT.gridTop`
- 期间轴用 `periodAxis`(自带 `2026H1 → 26H1``2026H2 → 26全年` 简写)
## 测试
**每次只测本次改动的图表,不要跑全量。** 这是硬要求,不是建议:
```bash
node test/regress.mjs ppmt_ip_share # 改了一张,就只测这一张
node test/regress.mjs ppmt_ip_share ppmt_core_margin # 改了多张,列出来
npm test # 全量,只在改了 theme.js / csv.js 等公共模块时才跑
```
全量跑纯属浪费,而且十几张无关图表的输出会盖住本次改动的真实问题。不传参数时才是全量,所以 `npm test` 保持向后兼容。
它会断言 `legend.top``grid.top`、折线 `symbolSize`/`borderWidth`,并检查 SVG 字体引号。加新图记得同步往 `regress.mjs` 顶部的 `all` 数组加一行,否则不会被测到。
**右侧纵向图例的图会报 `legend.top=middle (期望85)`,这是预期的,不要去"修"。** 那条断言的 85px 基准是给顶部横向图例定的,对 `orient: 'vertical'` + `top: 'middle'` 的图不适用(`ppmt_ip_share``ppmt_ip_share_history` 都会报)。
## 踩过的坑
**不要用 `...legendStyle`。** 它遗留 `top: 62` 且曾带 `icon: 'roundRect'`——后者会盖掉 ECharts 按系列类型自动生成的图例(折线本该是「短线+空心圆」)。用 `standardLegend()`
**`label.position` 不接受函数。** 写了会被静默忽略、退化成默认位置,实测确认过。要让同一系列内不同点朝向不同,只能逐点覆盖 `data``{ value, label: { position: 'top' } }`
**字体名用单引号。** `FONT` 里若用双引号,SVG 渲染时会提前闭合 `style="..."` 属性生成非法 SVG。canvas 看不出来,导出 SVG 才炸。
**多折线标签避让:按「朝间隙大的一侧」放,不要按排名奇偶交替。** 奇偶交替会让排名相邻的两条线一个朝下一个朝上、相向靠拢,值接近时标签间距被压到小于字高,然后被 `hideOverlap` 丢弃(表现是「某个数看不到」)。正确做法见 `ppmt_margins.js``labeledData()`:比较上下间隙取大者,最高值必朝上、最低值必朝下。
**矮柱子的标签会被自动隐藏。** 数据跨度大时(如 1.9 ~ 169.1),矮柱标签比柱子还高就不渲染了。`barSeries()` 已设 `label.overflow: 'none'` 兜住;手写柱状系列时记得加。
## 数据口径
**数值默认保留一位小数。** CSV 数据列与图表展示(柱/点标签、tooltip)统一保留一位小数(如 `71.1``311.4%`),四舍五入;确实需要其他精度时必须在 `footnote()` 或代码注释里说明原因。
**CSV 存四位年份**`2026H1`),简写只在显示层做。核对数据时无歧义。
**利润表的 `H2` 列是全年累计,不是下半年。** 已核对:2025H2 营收 371.2 = 2025H1 的 138.76 + 单半年 232.44。所以 `26全年` 这类标签指累计值。
**但「IP收入和占比」表的 `H2` 是单半年,不是累计。** 同一个 workbook 里两种口径,别想当然。已核对:该表 2025H2 合计 232.44 = 利润表 371.2 138.76,五年逐年都吻合。**所以这张表的图不能用 `periodAxis`**——它会把 `2025H2` 渲染成「25全年」,是错的。改用 `categoryAxis` + 只去世纪前缀的 formatter`v.replace(/^20/, '')``25H2`),见 `ppmt_ip_share_history.js`。新建图表前先确认数据源用的是哪种口径。
**衍生指标标注来源。** 非 GAAP 净利率是 `非GAAP净利 ÷ 营业收入` 算出来的,表里没这行;2020H1/H2 非 GAAP 缺失,用归母净利替代。这类替代必须写进 `footnote()`
**桥图/瀑布图要验算闭合。** 各项占营收比是税前口径,加总到净利率前须乘税盾 `(1 - 有效税率)`;税率变化单独成项。恒等式 `净利率 = 税前利润率 × (1-税率)`,分解为 `Δ净 = Δ税前×(1-t₁) − 税前₂×Δt`。改完核对首尾误差应 < 0.05pct。
## 配色
先分清两件事,别混用同一个门槛。**选色的功夫要和系列数成正比**——两个系列的图不该选半天。
**① 同一张图内的系列要能互相区分。** 这条永远要满足,是可读性问题:
- ≤ 4 个系列:手挑,凭常识就够,**不用跑 ΔE**
- 5~9 个系列:手挑后验算一次,两两 > 25
- ≥ 10 个系列:ΔE 到 ~24 就收手,硬凑 > 25 会把颜色逼得很丑(见 `ppmt_ip_share.js` 的 11 色谱)
**② 跨图撞色只在「同类编码 + 位置紧邻」时才查。** 目的是防止读者把两张图的系列对应起来,所以:
- 折线图 vs 紧邻的折线图、或两图共用同一套分类色谱 → 要查
- 柱+线双轴图 vs 饼图 → **不用查**,编码类型本身就区分开了
- 隔着好几张、主题无关的图 → 不用查
**不要拿整页累积的所有颜色当避让集合。** 这是个只会越来越紧的棘轮:每加一张图就多几个色要避让。实测过,对整页 26 色都要求 ΔE > 25,可行域只剩 18%(只对紧邻图要求则有 39%);而且**现有配色自己就有 13 对不达标**——`星星人`/`orange` ΔE 6.5、`DIMOO`/`green` 7.5、`MONSTERS`/`red` 10.4、`blue`/`navy` 14.4。拿一条自己都没遵守的规则去卡新图,结果就是在两个系列的图上白耗三轮验算。
**不要用贪心/模拟退火/随机搜索去"解"配色。** 太耗时,且算出来的色往往不好看(机器只顾拉开距离,不管协调)。手写一条色相谱再验算:
1. 按色相环顺序挑(如 红→橙→金→橄榄→翡翠→青→蓝→紫),残差项("其他"/"外采")用中性灰
2. 同色系连续多项靠**明暗交替**拉开(深蓝 → 浅蓝紫 → 紫 → 浅兰紫),不然挤在一起分不清
3. 只对不达标的几项微调,一两轮收手
`ppmt_margins.js` 顶部记着当初选深青/芥黄/茄紫/玫红的过程。注意那张图是「4 条折线 + 同页有别的折线图」,正属于 ② 要查的情形;别把它的严格度套到所有图上。它当年淘汰 `slate`/`cyan`/`indigo` 的理由是「离 `blue`/`navy` 仅 ΔE 17~19」,可 `blue``navy` 自己才差 14.4——这个先例本身就偏严,别照抄。
## 脚注
`footnote()` 单行约 60 字符封顶(1200px 画布),超了用 `\n` 手动断行。结尾统一 `| @思考的Joey`
水印文案在 `theme.js``WATERMARK_TEXT`,改一处全局生效。