Files
2026-08-23 21:23:41 +08:00

6.8 KiB
Raw Permalink Blame History

图表开发约定

新建图表时

必须从 theme.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 → 26H12026H2 → 26全年 简写)

测试

每次只测本次改动的图表,不要跑全量。 这是硬要求,不是建议:

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.topgrid.top、折线 symbolSize/borderWidth,并检查 SVG 字体引号。加新图记得同步往 regress.mjs 顶部的 all 数组加一行,否则不会被测到。

右侧纵向图例的图会报 legend.top=middle (期望85),这是预期的,不要去"修"。 那条断言的 85px 基准是给顶部横向图例定的,对 orient: 'vertical' + top: 'middle' 的图不适用(ppmt_ip_shareppmt_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.jslabeledData():比较上下间隙取大者,最高值必朝上、最低值必朝下。

矮柱子的标签会被自动隐藏。 数据跨度大时(如 1.9 ~ 169.1),矮柱标签比柱子还高就不渲染了。barSeries() 已设 label.overflow: 'none' 兜住;手写柱状系列时记得加。

数据口径

数值默认保留一位小数。 CSV 数据列与图表展示(柱/点标签、tooltip)统一保留一位小数(如 71.1311.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 + 只去世纪前缀的 formatterv.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」,可 bluenavy 自己才差 14.4——这个先例本身就偏严,别照抄。

脚注

footnote() 单行约 60 字符封顶(1200px 画布),超了用 \n 手动断行。结尾统一 | @思考的Joey

水印文案在 theme.jsWATERMARK_TEXT,改一处全局生效。