Astryx ChartV2 设计研究:从「能画」到「画得对」的 API 与正确性工程
2026/9/15 13:28:20 网站建设 项目流程

Astryx ChartV2 设计研究:从「能画」到「画得对」的 API 与正确性工程

【免费下载链接】astryxAn open source design system that's fully customizable and agent ready项目地址: https://gitcode.com/GitHub_Trending/as/astryx

本文以 packages/charts/docs/CHARTV2_STAGE1_DESIGN.md 为核心骨架,结合 Astryx 仓库中@astryxdesign/charts包的实际源码与测试,深入拆解 ChartV2 在正式发布前完成的「API 正确性 + 高质量 API」设计工作:为什么 marks + 单一共享 scale 的架构是对的、channel/encoding 模型如何统一约 25 处 API 不一致、颜色系统与 scale 正确性缺口如何补齐,以及一张完整的失败模式矩阵如何驱动 Stage 2 的测试。读完你将理解 Astryx 图表库从 v1 演进到 v2 的设计决策依据,以及这些决策在 layout.ts、types.ts、getChartColors.ts 等源码中的落地形态。

1. 这份设计文档在说什么

CHARTV2_STAGE1_DESIGN.md是 ChartV2 三个阶段规划中Stage 1("make it correct + a genuinely good API")的设计研究文档,与CHARTV2_PHASE1_PLAN.md(实现顺序与证据)互为姊妹篇:前者回答"什么样的图表 API 才是好的、让人不后悔的",后者回答"按什么顺序把它做出来"。文档状态明确标注为"research + proposals",其中第 8 节的 owner 决策项已经给出推荐默认值。

根据 packages/charts/docs/README.md 的记录,这些文件名中的 "ChartV2" 是历史命名——文档写于图表功能从@astryxdesign/lab提取为独立的@astryxdesign/charts包之前。今天的 packages/charts/README.md 描述的就是这份设计落地后的形态:

import {Chart, bar, line} from '@astryxdesign/charts'; <Chart data={data} xKey="month" series={[bar('revenue'), line('trend')]} />;

核心架构一句话概括:mark 是返回配置对象的工厂函数,图表根节点拥有唯一一套 x/y scale,mark 先resolve()render()。文档的结论是:这个架构"现代且正确",应当保留;真正需要花大力气的是 API 表面的一致性和缺失的 scale 能力。

2. ChartV2 在图表面向库格局中的位置

文档将主流图表库划分为四个 API 家族,并明确 ChartV2 的定位是其中"最好的两个"的混合体:

API 家族代表库形态优缺点
组合式 / JSX childrenRecharts、Victory、visx<LineChart data><Line dataKey/><XAxis/></LineChart>可发现性强、符合 React 习惯,但 scale 难以协调、节点多时性能差、跨 mark 一致性靠手工
按图表类型的配置单体Nivo、Chart.js、Tremor<ResponsiveLine data={...} 大属性包/>常见场景快,但难以混合 mark 类型(bar + line + band 同图)、组合性差
图形语法 / marksObservable Plot、Vega-Lite、AntV G2Plot.plot({marks:[Plot.barY(data,{x,y,fill})]})mark 是函数/对象,plot 拥有 scale,每个视觉属性都是 channel,正确性与组合性最佳,概念数量略多
序列数组配置MUI X Charts、Plotly、Highcharts、EChartsseries={[{type:'bar',...}]},轴单独配置务实,仪表盘场景极常见

ChartV2 的定位:Observable Plot 风格的 marks(bar(...)line(...)返回配置)+ MUI 风格的series={[...]}数组 + 单一共享 scale。这是一个"强大且站得住脚"的基础,所以设计结论是保留架构,修复 API 表面,补齐缺失的 scale 能力

2.1 顶级库做到而 ChartV2 尚未做到的六件事

  1. 统一 channel:Plot/Vega-Lite 中 color/size/opacity 处处接受常量 | 字段 | accessor;ChartV2 当时只在bar/dot的 color 上做到。
  2. 一等公民、可配置的 scale:x/y(以及 color/size)的 scale 类型和选项应显式可覆盖;ChartV2 当时只从数据推断 linear-or-band,且不暴露任何配置。
  3. 时间 scale:所有人都有的能力,ChartV2 当时没有——金融/流式场景只能拿 band 字符串或裸数字凑合。文档特别指出ScaleTime已导入轴类型,但根节点从未真正构建过它。
  4. 多轴 / 次级轴:为价格 vs 成交量这类双单位图提供独立轴域;单一共享 y 是金融组合图看起来"坏掉"的根源。
  5. CVD(色盲)安全的默认分类色板:Vega/Plot 内置 tableau10 风格色板,ChartV2 需要验证。
  6. 分面 / small multiples(明确标注为超出范围)。

第 3 点在当前源码中仍能从类型层面看到痕迹:ChartAxis.tsxChartGrid.tsx的 scale 联合类型中保留了ScaleTime<number, number>(见 ChartAxis.tsx),印证了文档"时间 scale 能力已预留但未接通"的判断。

3. 编码 / channel 模型:最大杠杆的 API 决策

3.1 问题具象化:一个视觉属性有五种拼法

文档的 API 审计列举了同一概念被"拼写"成多种形态的实例:

  • colorColorAccessor(bar/dot)|string(line/area/band/errorBar/referenceLine)| 必填string(dotGL/dotGLInteractive/streamGL)|string[]色带(heatmapGL)|upColor/downColor对(candlestick)——五种形态。
  • 点的"大小"radius(dot,半径)vssize(dotGL,直径)。
  • opacityopacity(大多数)vsbandOpacity(referenceLine)vs 没有(line/candlestick)。

这些不一致在今天源码中的ColorAccessor类型里依然可见其雏形——types.ts 中:

export type ColorAccessor = string | ((datum: Record<string, unknown>, index: number) => string);

即"常量字符串"或"逐数据点 accessor"两种形态,而文档提议的{field: string}第三种形态(字段映射)尚未进入类型定义——这正是 Stage 1 要做的扩展。

3.2 提议:一个统一的Channel<T>概念

借用 Observable Plot 的 channel 思想:一个视觉属性接受三种东西之一。

type Channel<T> = | T // 常量:color: '#0171E3' 或一个 token | {field: string} // 读取数据列(经 scale 映射) | ((datum: Row, index: number) => T); // accessor

统一作用于随数据点/随序列变化的属性:color(fill/stroke)、size/ropacity。效果:

  • bar('rev', {color: '#...'})依旧可用(常量)。
  • bar('rev', {color: d => d.up ? green : red})可用(accessor),并且能进入图例。
  • dot('y', {color: {field: 'category'}})把一列数据经过 color scale(分类)映射——即按类别着色的散点,这是当时"不可能做到"的常见需求。

文档强调该概念是增量式的:今天的常量用法全部继续工作。同时明确范围:不需要完整的 Vega-Lite 类型系统(quantitative/ordinal/temporal),"channels + 一小撮显式 scale" 对这个库来说是恰到好处的力量。

这个单一概念同时消解了审计中的 C3/C8/C9/C14 四项不一致,并解锁 color-by-field 能力。

4. 颜色系统深潜

4.1 已有的好底子:token 集与 v1 调色板 API

Token 定义在 packages/core/src/theme/domainTokens/dataTokens.ts,结构与文档描述完全一致:

  • 10 个分类色:blue、orange、purple、green、pink、cyan、red、teal、brown、indigo,tableau10 风格,token 形如--color-data-categorical-blue
  • 9 个顺序色带 × 5 档(5=最深 → 1=最浅):blue、shamrock、orange、pink、purple、red、teal、yellow、gray,用于有序/定量(热力图、分级统计图),token 形如--color-data-blue-5
  • 1 个中性色--color-data-neutral:标签、参考线、空状态。

值得注意的实现细节:所有 data token 都以light-dark(#X, #X)形式定义,且分类色与顺序色在明暗两套值上相同——这正是文档第 4.4 节讨论的"刻意为之的设计点"。

调色板 API 目前在 getChartColors.ts 中落地为纯函数(另有 useChartColors.ts React hook 包装),完整暴露文档所述能力:

export interface ChartColorsAPI { categorical(n: number): string[]; sequential: Record<SequentialHue, (n: number) => string[]>; diverging: { positiveNegative(n: number): string[]; coldHot(n: number): string[]; custom(neg, pos, n, midpoint?): string[]; }; semantic: {positive: string; negative: string; warning: string; neutral: string}; structural: {axis: string; grid: string; tick: string; label: string}; alpha(hex: string, opacity: number): string; }

SequentialHue联合类型包含全部 9 个色相名,与 token 文件一一对应。文档的结论是"这是一个真正好的 API——整体采纳进 charts 包",源码证实这一建议已被执行。

4.2 需要修复的六个问题

  1. 失效的默认值:mark 默认引用--color-chart-1/--color-positive/--color-negative,这些 token 在 core 中并不存在 → 渲染为黑色/不可见。修复方案:由图表根节点按序列索引自动分配分类色板(mark 默认 color = "auto",根节点用categorical(n)填充未设色的序列)。当前源码中 types.ts 的SeriesDef已出现_resolvedColor?: string("由图表根节点为未提供静态颜色的序列分配的调色板颜色"),legend.ts 中的legendColor()也以series.color ?? series._resolvedColor ?? DEFAULT_SERIES_COLOR的优先级解析代表色——正是该修复方案的落地证据。

  2. GL mark 无法使用 token:文档描述hexToGLparseInt(hex,16)解析,CSS 变量或rgb()会变成NaN静默错渲染。当前 webgl.ts 的hexToGL已改为经parseHex解析(接受#rgb/#rgba/#rrggbb/#rrggbbaa,非 hex 输入返回中性回退值而非 NaN,注释明确说明"GPU 永远不会收到坏的 uniform")——这是比文档描述时更进一步的实现,但"GL 与 SVG 接受同一套颜色、可自动分配色板"的目标仍需在 Stage 1 中完成验证。

  3. accessor 颜色从图例消失deriveLegendItems当时会丢弃任何 color 不是静态字符串的序列。修复方向:序列拥有一个resolved代表色(分配的色板槽位、accessor 在稳定点采样的结果、或 color-by-field 的小 scale)。当前legendColor()的三级回退链(静态色 → 根节点分配色 → 兜底var(--color-accent))已经让 accessor 着色的序列能进入图例。

  4. 暗色模式:分类 token 明暗相同(刻意设计的中间调);真正的风险在顺序色带的深档(如--color-data-blue-5: #02165E)在暗色背景上失去对比度,使用低档位(深色档)的热力图/面积图需要专项检查。

  5. CVD 安全:10 个分类色是 tableau10 风格但未对 deuteranopia/protanopia/tritanopia 验证。需要新增验证任务,并文档化推荐的最大可区分序列数(约 8 个)。

  6. 序列过多:超过 10 个分类色后的行为——文档推荐"循环复用 + dev 警告"(Plot 是警告,ECharts 是循环复用)。

4.3 新增的小型 color scale

为支持color: {field}用法,在根节点引入显式 color scale:

  • 分类字段 → 按不同值categorical(n)(序数色标)。
  • 定量字段 → 顺序色带(选色相)。
  • 发散 → 围绕中点diverging.*

这是 color-by-field 一致且可入图例的前提。

5. Scale:正确性的地基

5.1 现状(设计文档记录的问题)

computeLayout当时对 x 轴要么构建scaleLinear(数值 x)要么scaleBand(其他情况),y 轴恒为scaleLinear且 domain 为[min,max].nice()(仅当存在 bar/area 时才包含零)。后果:端点贴边、单调曲线溢出到绘图区外(无裁剪)、空数据落入 band 分支产生 NaN、无时间 scale、无对数 scale、无按轴控制。

这些问题的详细证据记录在 CHARTV2_PHASE1_PLAN.md 的 Confirmed findings 中(含具体行号:layout.ts:128-136无 headroom、Chart.tsx:295-346无 clipPath、streamGL.tsx:146-152空数据导致 blank)。

5.2 提议的修复

  1. domain/baseline 对齐 v1yBaseline: 'auto'|'zero'|'data'yDomainxDomain,显式给出时权威优先(不做.nice());给连续图加clipPath和小幅 headroom。

  2. 退化 domain 处理:单点 / 零跨度 / 全等值 → 合成合理跨度(如v±1[0, v*2]);空数据 + 显式 domain → 尊重 domain(修复流式渲染)。

  3. 显式 scale 配置(新增、可选):

    xScale={{type: 'time'}} // 或 'linear' | 'band' | 'log' yScale={{type: 'log', domain: [1, 1e6], nice: true}}

    推断仍为默认,这是 MUI/ECharts/Plot 的通用做法,消除一类"猜错了"的 bug。

  4. 时间 scale:为 Date 型 x 值接入scaleTime,配合移植的shortDate/monthYear格式化。

  5. 次级 y 轴:允许 mark 指向独立的 y2 scale(如bar('volume', {axis: 'y2'})),类似 MUI 的yAxisId/ Plotly 的y2。设计张力需要显式说明:单一共享 scale 是核心价值主张(mark 之间不可能不一致),次级轴是"被认可的例外",必须 opt-in 且显式,绝不自动。

5.3 源码中的落地现状

layout.ts 已经实现了提议中的大部分内容:

  • LayoutInput接受yBaseline?: YBaselineyDomain?: [number, number]xDomain?: [number, number],注释明确"显式 domain 权威优先——不做 baseline/headroom/nice()",且"xDomain 在data为空时依然生效(稳定的流式窗口)"。
  • finiteExtent()防御Math.min/max的两个失败模式(NaN/Infinity 污染、超大数组展开导致栈溢出),空/退化范围自动扩展([0,1]v±0.5|v|)。
  • CONTINUOUS_HEADROOM = 0.08:连续图(无 bar/area 零基线)在数据范围外补 8% 的头部空间,正是文档"headroom"建议的具体数值。
  • _uid = \${seriesIndex}:${s.key}`` 按数组位置分配免碰撞标识,解决重复 dataKey 冲突(Phase 1 的 W1 工作项)。
  • 分类 y band scale(yBandScale,由layout.yBandKey触发)让热力图的 y 轴也共享图表 scale——对应 Phase 1 的 W6。

types.ts 中YBaseline类型定义了'auto' | 'zero' | 'data'三种模式及语义注释,与文档提议逐字对应。

6. 一切可能出错的地方:失败模式矩阵

这是文档中信息密度最高、对 Stage 2 测试驱动价值最大的部分。每一行都遵循"今天发生什么 → 应该发生什么"的格式。整理如下:

数据病理

情形今天的行为应该的行为
data=[]空数据band scale、NaN、空白(破坏流式)尊重显式 domain;否则渲染空状态(坐标轴 + "no data")
单条数据零宽度/近退化 domain合成跨度,居中绘制点
全等值 / 零跨度平 domain,nice()可能折叠刻度给 domain 加 padding
全零 / 全负baseline 处理(bar 可能反向生长)用新边界 case stories 验证;yBaseline正确性
一个大离群值其他数据被压扁文档化 log scale 方案;story 展示 log 补救
数值列中的 NaN/null/undefined/非数值静默强转为 0(误导)line/area 断开路径(gap)、dot 跳过该点;决策并文档化。倾向 gap(line/area)、skip(dot)
拼错的 dataKey到处静默 0/undefineddev 警告
band 的重复 x 值 / line 的未排序 xband 静默去重;line 画成锯齿line 未排序 x 发 dev 警告
超大 N(1 万+ SVG 节点)卡顿GL mark 已存在;文档化阈值;(m4 降采样为跟进项)

Scale / 渲染

情形今天的行为应该的行为
曲线溢出绘图区(monotone/natural)逃逸到 marginclipPath
轴标签溢出/重叠(多 band 类别、长标签、大数字)今日蜡烛图糊成一片宽度感知的自动跳过 + 旋转/截断选项
视口边缘的 tooltip可能被裁剪翻转/钳制(部分已处理,需验证)
图例溢出(多序列)legend="end"裁掉绘图区换行/滚动
参考线标签出画布溢出将 badge 钳制在绘图区内

运行时 / 平台

情形应该的行为
SSR / 无 DOM(Next.js,docsite 正是 Next.js)守卫 ResizeObserver/canvas/window访问;挂载前渲染空或占位符
WebGL 不可用 / context 丢失回退消息或 SVG 路径;处理webglcontextlost
canvas / 监听器清理(流式 rAF、环形缓冲、ResizeObserver)审计 teardown,防泄漏
高 DPI模糊 → webgl utils 已有 DPR 处理,需跨 mark 验证
运行时切换主题颜色必须重新解析(尤其缓存 hex 的 GL)→ 主题变化时重新上传
减少动态效果轴有animated,需遵循prefers-reduced-motion

无障碍

  • 屏幕阅读器:SVG 目前仅根节点有 title/desc,序列/数据点无描述 → 至少加 role/aria-label,考虑>npm install @astryxdesign/charts@canary @astryxdesign/core@canary

    Canary 构建跟踪main的最新提交(0.x.y-canary.<sha>),任意两个版本之间可能破坏兼容,需要稳定性请锁定精确版本。

    包内package.json"private": true"astryx": {"canaryOnly": true}标记是 npm 层面"绝不发稳定版"的硬保证。对于想深入理解这套设计决策的读者,packages/charts/docs/ 目录下四份记录(plan / design / readiness / verification checklist)构成了完整的演进档案:Stage 1 设计研究回答"什么是对的",Phase 1 计划给出"按什么顺序做",readiness 与 checklist 负责"做到什么程度才算能发"。

    附录:文档中的源码引用与当前仓库位置的对照

    设计文档成文时引用的部分路径指向packages/lab/src/Chart/,图表功能现已提取到packages/charts/,对应关系如下:

    设计文档引用当前仓库位置
    packages/core/src/theme/domainTokens/dataTokens.tsdataTokens.ts(未变)
    Chart/getChartColors.tsuseChartColors.tspackages/charts/src/getChartColors.ts、useChartColors.ts
    Chart/webgl.tshexToGLpackages/charts/src/webgl.ts
    Chart/utils.tsxPixelpackages/charts/src/utils.ts
    Chart/Chart.tsx(domain/baseline 参照)packages/charts/src/Chart.tsx
    完整 API 审计镜像于 CHARTV2_PHASE1_PLAN.md 的 findings

    对照之后可以清楚看到:这份设计文档不是"纸上谈兵"——它提出的 channel 模型、domain/baseline parity、退化处理、_uid身份方案、调色板 API 复用与图例修复,绝大多数已在当前packages/charts源码中落地,本文各节引用的行号与类型定义即为佐证。

    【免费下载链接】astryxAn open source design system that's fully customizable and agent ready项目地址: https://gitcode.com/GitHub_Trending/as/astryx

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询