Van-Analysis 图表引擎设计分析:BI 场景下的数据重建、交互与大数据渲染

AI 摘要基于 van-analysis 源码与历史提交,分析其协议重建、图形语法分层、交互状态机以及大数据能力的演进过程。

van-analysis 不是一个“给页面上插一张图”的轻量图表组件,而是一套服务于 BI 运行时的图表内核。它的目标不是把一份普通数组渲染成折线图,而是承接服务端下发的复杂协议、兼容 PC 与移动端、处理地图与联动、支持导出和大数据模式,并在这些约束下维持可接受的交互体验。

如果说通用图表库更像“图形 API”,那 van-analysis 更像“图表领域的运行时系统”。理解这一点,才能解释它为什么会同时拥有 reconstructstoreinteractionbigdataexportrequest 这些模块,而不是只有一个 render(option)

1. 定位与核心约束

从源码结构看,这套库显然不是面向纯前端 Demo 场景设计的。它至少被以下几类约束塑造:

约束技术后果
图表配置主要来自 BI 服务端协议,而不是前端手写 option需要单独的协议转换层,即 reconstruct
同一图表要同时跑在 PC、移动端、设计态、运行态需要统一状态内核 + 双端交互适配
图表不只是展示,还要支持联动、过滤、缩放、框选、地图漫游需要独立的交互系统和事件桥
大屏 / 仪表板中会同时存在大量图表,且单图可能数据量很大必须设计大数据模式和受控刷新机制
图表需要导出、切片、远程取图、服务端回传初始化状态渲染结果不能只面向浏览器 DOM,必须可外部驱动

这也是它和 ECharts、G2 这类通用图表库最本质的差异。后者首先是“图形表达工具”,前者首先是“业务运行时容器”。

2. 架构分层

这套库的主干可以概括为六层:

src/index.js
  └── 对外暴露 VanChart / TileChart / Request / 全局 Van

src/core.js
  └── 注册 geometry、guide、map、component

src/chart/chart.js
  └── Chart 实例编排:normalize -> store -> reconstruct -> render

src/reconstruct/
  └── 将 BI 协议转换为内部统一 state

src/store/ + src/interaction/
  └── 用 Redux 管理状态,用 Mouse / Touch 管理交互

src/bigdata/ + src/export/
  └── 大数据切片渲染、导出平铺渲染

这个分层有一个很鲜明的特点:渲染不是中心,状态编排才是中心

普通图表库往往会把大量逻辑压到“解析 option -> 渲染图元”两步里;而 van-analysis 明显把“协议重建”“交互状态”“导出模式”“大数据模式”提升到了和渲染同级的地位。这是 BI 图表和普通图表的根本差异。

3. 一条真实的演进主线

如果只看当前代码,van-analysis 很容易被理解成“一套做得比较重的图表内核”。但翻一下它的分支和提交历史,会发现这套库更接近一个长期演进中的产品内核,而不是一次性设计完成的框架。

从仓库历史看,它至少跨过了 5.06.06.17.0 乃至更高版本线,并同时维护:

  • dev
  • release/*
  • persist/*
  • final/*
  • 若干 feature / bugfix 分支

这说明它不是“代码写完就结束”的项目,而是被长期产品需求持续塑形的图表引擎。

3.1 从图表渲染器,到 BI 运行时

一个明显的转折点是 2024 年 9 月的提交 106ffd7e5 feat: 增加图表新的数据结构。这次改动不是修一个局部 bug,而是直接在 src/reconstruct/common/ 下新增了一整套公共数据结构转换层,同时联动了:

  • src/reconstruct/
  • src/chart/chart.js
  • src/bigdata/

这类提交说明系统的重心已经不再只是“支持更多图形类型”,而是开始升级内部数据模型,让图表能够承接更复杂的 BI 协议和运行时状态。

换句话说,van-analysis 的演进方向不是“多加几个图”,而是“把图表变成 BI 运行时的一部分”。

3.2 大数据能力不是一次性设计好的

大数据模式的演进痕迹尤其明显。

2024 年 5 月的 b1938cedb BI-148363 fix: bigdata标记位设计缺陷 已经明确暴露了一个事实:第一代大数据模式的标记位设计本身就有问题。随后几个月,相关能力快速迭代:

  • 4c9e97635 BI-149371 feat: 引入DataVisitor
  • 23f98d6ac BI-150527 fix: 大数据图表排除移动端
  • 3b395673f BI-151262 fix: 保持最后一个切片稳定
  • 0a8db31ec BI-151949 fix: 增加一个超时检测
  • 04a91ee9c BI-152633 feat: 散点图,大数据模式

这条线几乎把演进脉络完整暴露出来了:

  1. 先有一版老大数据模式
  2. 再暴露出 bigData 标记位设计缺陷
  3. 然后引入 DataVisitor 这类更结构化的数据访问层
  4. 再把切片渲染从局部优化推进成一条独立的控制链路
  5. 最后逐步扩展到更多图形类型,并补上超时检测和稳定性处理

所以今天看到的 simpleLTTB + slice 并不是“起点设计”,而是多轮失败和修补之后的结果。

3.3 交互系统是被业务一点点逼复杂的

交互层的历史也非常典型。它不是一开始就有完整的框选、滚动、十字线和多端手势体系,而是在连续需求下逐渐增厚:

  • cb998709a BI-143031 fix: 框选效果修复
  • 6fdd39ae8 BI-150414 fix: 框选报错
  • 97bb9a0e7 BI-164317 ... 修改chart滚动效果以及修改十字线颜色
  • 6900084c9 BI-182971 fix: 饼图漏斗图框选失败

这些提交说明,interaction 不是“附属模块”,而是被复杂交互需求反复推着演进的核心层。很多能力看起来像细节,但它们之所以最终被抽成独立模块,恰恰是因为业务已经不允许再把这些逻辑散落在图元渲染里了。

3.4 多端支持和开放接口都是后期主题

另外两条很清楚的演进线是多端和开放性。

多端方面,历史里能看到:

  • 23f98d6ac BI-150527 fix: 大数据图表排除移动端
  • 035dc0b50 BI-182173 fix: pad端适配代码迁移到7.0
  • 9a66fe2a1 BI-182443 fix: 移动端饼图无法上下滑动

这说明移动端/Pad 并不是天然从 PC 架构里长出来的,而是后期逐步分叉、迁移和补齐的。

开放性方面,则有:

  • dd9841d61 INO-22202 fix: 抽象图形开接口

这类提交说明系统在较晚阶段才开始认真处理“如何让外部能力挂接进来”的问题。也就是说,早期重点是先跑通 BI 场景,后期才轮到开放抽象图形这类扩展点。

所以如果把 van-analysis 看成一个静态架构,很容易误解它为什么显得“重”;但如果把它看成一个跨多个版本线不断演进的图表内核,很多设计就会变得合理得多。

4. 渲染内核:不是直接画图,而是先建立运行时

src/chart/chart.js 是整套系统的调度中枢。它做的第一件事不是渲染,而是建立运行时环境:

  1. normalizeChartOption 规范化入口参数
  2. initStore 初始化 Redux store
  3. 通过 EventEmitter 建立对外事件系统
  4. 收到数据后调用 createDataController
  5. 再进入 _updateState -> reconstruct -> dispatch(init)
  6. 最后才走 render(<VanChart ... />)

也就是说,Chart 实例本质上是一个容器对象,而不是一个单纯的绘图对象。

这里有两个重要设计判断。

4.1 store 先于 render

渲染组件 src/component/Chart.js 本身非常薄,只是把几个 guide 组装起来:

<group>
    <Facet />
    <Plot />
    <Cordon />
    <Legend />
    <Hover />
    <Tooltip />
</group>

这说明作者有意把复杂度从“组件树”转移到“状态树”。组件层更像是状态的投影,而不是业务逻辑的承载者。

这类架构在 BI 场景里是合理的。因为图表真正复杂的地方并不是“怎么把一个圆画出来”,而是:

  • 当前筛选是否要回传服务端
  • 图例切换是否影响联动
  • 地图缩放是否要写回 chartInitState
  • 大数据模式下 hover 是否还能精确命中
  • 移动端是不是要切到特殊交互模式

这些问题都更适合放在 store 和 action 体系中处理。

4.2 渲染后端是图元系统,不是 DOM

源码里通过 visiblezrender 初始化渲染实例,再把 JSX 风格的组件树落到图元层。这意味着它天生就不依赖浏览器 DOM 布局模型,而是运行在一个更接近“Canvas 场景树”的抽象之上。

这样做的直接收益有三个:

  • 更容易统一鼠标命中、拖拽、选区和导出逻辑
  • 地图、标注、图例、悬浮层都能在同一图形语义下管理
  • 可以衍生出 TileChart 这种平铺导出能力,而不是被 DOM 截图能力限制

代价也很明显:整套渲染链路更像一个小型图形引擎,学习成本和维护成本都高于“直接基于 DOM + SVG 组件开发”。

5. reconstruct:真正的业务核心

如果让我从这套库里挑一个最能体现 BI 特性的模块,不是 Plot,而是 reconstruct

src/reconstruct/index.js 做了一件事:把外部传入的数据,变成图表内部真正能消费的统一结构。

它包含两条路径:

  • 已经是内部状态的,直接补齐 computedState
  • resultType 的 BI 协议,走 transformBIOption

transformBIOption 的工作并不轻:

  • 转 facet
  • 转 geoms
  • 转 legend
  • 组装 mapConfig
  • 计算 tagFiltersliderFilterresetFilterState
  • 提取 fontScaleadaptivedarkThemeenableMap 等运行时标志

换句话说,后端发来的数据并不是“图表 option”,而是“业务协议”;reconstruct 才是把业务协议还原成图表运行时的那层翻译器。

5.1 这层为什么必须存在

很多图表项目喜欢追求“前后端共享一份 option 协议”,看起来很干净,但在 BI 场景里通常走不通。因为 BI 服务端下发的不只是可视化参数,还混有:

  • 初始化交互状态
  • 地图中心点和缩放
  • 联动、点击、过滤相关状态
  • 设计态 / 运行态差异
  • 脱敏、格式化、域映射信息

这些信息如果直接灌进渲染层,只会导致每个图元组件都认识服务端协议,最终整个系统失控。

van-analysis 的做法更务实:先承认服务端协议不是前端协议,再单独做一层重建。

5.2 这层的代价

reconstruct 的问题也同样明显。

它把大量领域知识压进了一堆转换函数里,例如 transformGeomstransformFacettransformLegendprepareMapConfigtransformInitState。这种设计早期推进很快,因为业务逻辑都能找到地方塞;但随着图表类型和兼容逻辑继续膨胀,转换层会逐渐变成系统里最难维护的部分。

换句话说,reconstruct 很重要,但它天然会走向“大而杂”。这是这类系统的宿命,不是编码风格问题。

6. grammar 分层:图表被拆成 Geometry + Guide

src/core.js 注册的内容很有代表性。它没有注册“柱状图页面组件”“折线图页面组件”,而是注册:

  • geompointlineintervalheatMappietreeMapgaugesankey
  • guideplotfacetlegendhover
  • mapCrossLine 等扩展能力

这说明 van-analysis 的抽象不是“一个图表类型对应一个大组件”,而是更接近图形语法系统:

  • geom 负责“画什么”
  • facet 负责“怎么分面”
  • legend 负责“怎么解释编码”
  • hover / tooltip / cordon 负责“怎么交互和辅助表达”

这套抽象有两个明显优点。

6.1 适合 BI 里的组合复杂度

BI 图表很少是单一类型。常见问题包括:

  • 多指标映射为多图元
  • facet 横纵轴决定图元排列
  • 图例过滤影响图元显隐
  • tooltip 需要拿到 measure、metaData、格式化结果
  • 某些图元要叠加 cordon、crossline、marker

如果按“每种图写一个巨型组件”去做,复杂度会迅速爆炸。拆成 geom + guide 之后,很多能力可以横向复用。

6.2 适合导出和平铺

导出系统 TileChart 沿用了同一套状态和图元抽象,只是换了一种渲染视口和命中计算方式。这说明原有架构不是写死在页面渲染流程里,而是具备一定可移植性。

这是一种很工程化的设计。它不优雅,但实用。

7. 交互系统:独立于渲染组件之外

src/interaction/base.js 里有一句注释很关键,大意是:

理论上通过纯 data 可以做所有事情,但交互时如果从 data 重新推导目标点,计算太复杂;而这些计算结果其实已经在 render 结果里了,所以交互直接从渲染结果取。

这几乎点明了整套交互系统的核心思路:

  • 状态仍然存在 store 中
  • 但命中测试、tooltip 定位、高亮对象选择,并不完全依赖纯数据推导
  • 一部分逻辑直接读取 zrender 已经生成的图元结果

这不是“最纯”的架构,却是性能和复杂度之间很现实的折中。

7.1 为什么不坚持纯状态推导

如果每次 hover 都从数据重新计算最近点、包围盒、命中区域,代价会很高,尤其在:

  • 折线图点很多
  • 地图图形复杂
  • 大数据模式存在采样或切片
  • 图元存在视觉补偿对象时

所以它选择让交互层有权读取渲染结果。这相当于把 render tree 当成了“命中索引”。

如果结合历史看,这种设计不是纯粹的架构偏好,更像是业务逼出来的结果。框选、滚动、连续点击联动、移动端手势、十字线这些能力在多个版本里反复修补,最终把交互逻辑推成了一个独立层。换句话说,interaction 不是“想拆就拆”,而是“不拆就维护不下去”。

7.2 Mouse / Touch 双实现

交互层被拆成 MouseTouch 两套实现,而不是用一层非常抽象的 Pointer Event 统一掉。这个选择看上去笨,但在 BI 历史包袱下很合理。

因为 PC 和移动端的差异并不只是事件名不同,而是整套体验不同:

  • 移动端可能启用特殊交互模式
  • 移动端允许使用 native tooltip
  • 触摸环境下空白区域点击、滚动、缩放和选择模式边界不同
  • 移动端请求路径和外部容器也不同

这类差异如果硬塞进一套统一抽象里,最后大概率会变成一堆 if (mobile)。现在拆成两层实现,至少边界清楚。

7.3 事件系统承担桥接职责

EventEmitter 支持 legend:toggle 这种 componentType:eventType 格式,store middleware 则在关键 action 后向外触发:

  • initStateChanged
  • scrollBoundsChange
  • interactionModeChange
  • legend:toggle
  • legend:rangeChange

这说明事件系统不是 UI 内部的小工具,而是图表运行时和外部 BI 容器之间的桥

在企业 BI 里,图表几乎不可能孤立运行。它必须和筛选器、联动面板、外层容器通信。所以 EventEmitter + Redux middleware 这一套是必须的基础设施。

8. 大数据模式:最有业务味道的设计

src/bigdata/dataControl.js 里把大数据模式分成了三类:

  • normal
  • simpleLTTB
  • slice

这不是一个学术上完美的体系,但非常符合真实工程。

更重要的是,这套设计并不是一开始就完整存在的。从历史提交看,大数据能力经历了一个非常典型的演进过程:

  • 先发现 bigData 标记位本身存在设计缺陷
  • 再引入 DataVisitor 统一切片数据访问
  • 再通过“保持最后一个切片稳定”“增加超时检测”这类修补,把切片模式从实验状态推向可用
  • 最后再逐步扩展到散点图等更多类型,并明确排除移动端等不适配场景

所以今天的 createDataController 看起来像一个干净入口,但它背后其实是多轮架构修补之后形成的统一门面。

8.1 simpleLTTB:后端先采样

如果服务端已经提供了 shared.bigDataSlice.sample,前端直接进入 simpleLTTB。这说明前端并不执着于“所有性能优化都必须在前端完成”,而是接受服务端预处理。

这是正确的。因为大数据图表的瓶颈不只在渲染,也在传输和内存占用。很多时候先减小数据体积比前端花哨优化更有效。

8.2 slice:前端滑动窗口切片

更有意思的是 sliceRendering。它不是简单地“采样后全量展示”,而是基于滚动方向、视口大小和单元尺寸,动态计算当前应该渲染的数据窗口。

核心策略包括:

  • 只在 PC 端启用
  • 只支持特定图元类型:lineareaintervalpointforcePoint
  • 必须只有一个方向滚动
  • 首屏按视口估算先渲染两屏
  • 后续按当前位置扩展到约三倍窗口,减少滚动抖动
  • 用 debounce 和最大等待时间平衡空白与卡顿

这是一种非常经典、也非常有效的“视口虚拟化”思想,只不过这里虚拟化的不是 DOM 列表,而是图表数据。

8.3 这套设计的优点

相比只做一个 “bigData = true” 标志位,这种模式分裂更成熟:

  • 后端采样和前端切片分责清楚
  • 不同图元类型可以渐进式接入
  • 保留普通模式,避免所有图都被大数据逻辑污染

8.4 这套设计的局限

代价同样存在:

  • 能进入切片模式的图表类型受限
  • 交互命中和真实全量数据之间会存在语义差异
  • 切片状态本身又变成一套新的运行时,需要和滚动、过滤、tagFilter 协调

但从工程角度看,这已经比“在主线程上硬扛千万点渲染”成熟太多了。

9. 请求与容器适配:图表不是独立应用

src/request/index.js 只定义了一个可扩展的 Request 壳:

  • getSetting
  • requestData
  • getCompleteUrl
  • services.refresh
  • services.filter
  • services.tooltipInfo

随后在 bi-pc.jsbi-mobile.js 里再接入具体环境。

这说明 van-analysis 从一开始就默认自己运行在宿主平台内,而不是一个 npm 装上就能独立完成全部上下文的组件库。

这种设计的优点是可嵌入、可接平台;缺点是边界容易变得模糊。尤其当图表内部开始依赖全局 BI、全局 Van、运行时注入的 Request 实现时,测试和独立演进都会变难。

不过站在 BI 产品的角度,这种耦合常常不可避免。图表既要展示,也要成为平台工作流的一部分。

10. 导出设计:TileChart 说明作者不是只做页面渲染

src/export/TileChart.js 很值得单独说一句。它继承 Chart,但面向的不是浏览器交互,而是图块导出、命中信息查询、平铺图像生成。

这背后的信息量很大:

  1. 图表状态和图表输出是可以解耦的
  2. 渲染结果不能绑死在一个 viewport 上
  3. 导出场景需要自己的命中计算和图元检索

很多团队做图表时,只关注页面上那张图能不能显示;到了导出、打印、切片、服务端截图时,就发现架构完全不支持。TileChart 的存在说明 van-analysis 在设计早期就意识到了这一点。

这不是“锦上添花”,而是 BI 场景里的基础能力。

11. 工程取舍:它强在哪里,也老在哪里

如果要评价这套库,我会说它有一种很典型的企业内核气质:问题意识非常强,现代感不算强。

11.1 它强的地方

第一,它没有把自己误判成一个纯渲染库,而是很早就把协议重建、交互、导出、大数据、宿主适配纳入设计范围。

第二,它的架构虽然重,但大体是自洽的。Chart 负责编排,reconstruct 负责翻译,store 负责状态,interaction 负责命中与行为,bigdata 负责性能模式,模块边界是清楚的。

第三,它对 BI 真实复杂度有敬畏。比如地图初始化状态、移动端差异、图例过滤回传、导出切片,这些都不是“后面再补”的小功能,而是系统长期演进后被稳定沉淀下来的能力。

11.2 它老的地方

package.json 就能看到一批明显的时代痕迹:

  • webpack 4
  • preact 8
  • 较早期的 Babel 组合
  • 大量运行时注册和全局对象注入

这些不是简单地“版本旧”而已,它们会实打实影响工程体验:

  • 类型系统弱,协议演进风险高
  • 构建链路和调试体验不够现代
  • 全局注册和副作用初始化让模块边界变脆
  • 复杂转换函数容易越长越难拆

更关键的是,很多业务逻辑是“运行时拼起来”的,而不是“编译期约束出来”的。这个方向很灵活,但长期维护成本高。

12. 如果今天重做,我会怎么改

如果以“不推翻现有业务能力”为前提做现代化演进,我会优先改四件事。

12.1 先给协议层加类型,而不是先重写渲染层

最值得收敛的是 reconstruct 的输入输出边界。先把服务端协议、内部 state、派生 state 分成独立类型层,哪怕只是渐进式 TypeScript,也比直接重写 Plot 更有效。

12.2 把大数据控制器进一步纯化

createDataController 这条链路已经很有价值,但仍然存在对原始数据对象的原位修改。后续如果要接 Worker、缓存层、回放工具或服务端预渲染,纯函数化会明显降低复杂度。

12.3 让交互命中层显式化

现在交互直接读取 render 结果,这很实用,但最好把“命中索引”作为一等抽象显式暴露出来,而不是隐含依赖 zrender 图元实例。这样后面做离屏渲染、导出复用、Worker 化时会更稳。

12.4 升级工具链,但不要急着抹平双端差异

工具链升级值得做,但“Mouse / Touch 是否统一”反而不是最高优先级。因为这里的差异大多是业务差异,而不是语法差异。先升级构建、测试、类型和模块边界,收益更直接。

13. 总结

van-analysis 的价值,不在于它有多“现代”,而在于它非常清楚自己在解决什么问题。

它不是那种适合拿来做营销 Demo 的图表库,而是一套长期被 BI 场景打磨出来的运行时系统。它的真正能力不只是画图,而是把服务端协议、图形语法、状态管理、交互系统、大数据模式和导出能力缝成一条能工作的生产链路。

从纯前端审美看,这套库确实很重,也背着明显的历史包袱;但从企业 BI 内核的角度看,它的很多设计其实并不落后,反而非常诚实。因为它没有假装问题很简单,而是直接把复杂度摊开来处理。

如果再把历史维度加进来,这种判断会更强:van-analysis 不是一套静态的图表框架,而是一台被多个版本周期、多个业务场景和大量线上问题持续塑形的“图表机器”。