Van-Analysis 图表引擎设计分析:BI 场景下的数据重建、交互与大数据渲染
AI 摘要基于 van-analysis 源码与历史提交,分析其协议重建、图形语法分层、交互状态机以及大数据能力的演进过程。
van-analysis 不是一个“给页面上插一张图”的轻量图表组件,而是一套服务于 BI 运行时的图表内核。它的目标不是把一份普通数组渲染成折线图,而是承接服务端下发的复杂协议、兼容 PC 与移动端、处理地图与联动、支持导出和大数据模式,并在这些约束下维持可接受的交互体验。
如果说通用图表库更像“图形 API”,那 van-analysis 更像“图表领域的运行时系统”。理解这一点,才能解释它为什么会同时拥有 reconstruct、store、interaction、bigdata、export、request 这些模块,而不是只有一个 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.0、6.0、6.1、7.0 乃至更高版本线,并同时维护:
devrelease/*persist/*final/*- 若干 feature / bugfix 分支
这说明它不是“代码写完就结束”的项目,而是被长期产品需求持续塑形的图表引擎。
3.1 从图表渲染器,到 BI 运行时
一个明显的转折点是 2024 年 9 月的提交 106ffd7e5 feat: 增加图表新的数据结构。这次改动不是修一个局部 bug,而是直接在 src/reconstruct/common/ 下新增了一整套公共数据结构转换层,同时联动了:
src/reconstruct/src/chart/chart.jssrc/bigdata/
这类提交说明系统的重心已经不再只是“支持更多图形类型”,而是开始升级内部数据模型,让图表能够承接更复杂的 BI 协议和运行时状态。
换句话说,van-analysis 的演进方向不是“多加几个图”,而是“把图表变成 BI 运行时的一部分”。
3.2 大数据能力不是一次性设计好的
大数据模式的演进痕迹尤其明显。
2024 年 5 月的 b1938cedb BI-148363 fix: bigdata标记位设计缺陷 已经明确暴露了一个事实:第一代大数据模式的标记位设计本身就有问题。随后几个月,相关能力快速迭代:
4c9e97635 BI-149371 feat: 引入DataVisitor23f98d6ac BI-150527 fix: 大数据图表排除移动端3b395673f BI-151262 fix: 保持最后一个切片稳定0a8db31ec BI-151949 fix: 增加一个超时检测04a91ee9c BI-152633 feat: 散点图,大数据模式
这条线几乎把演进脉络完整暴露出来了:
- 先有一版老大数据模式
- 再暴露出
bigData标记位设计缺陷 - 然后引入
DataVisitor这类更结构化的数据访问层 - 再把切片渲染从局部优化推进成一条独立的控制链路
- 最后逐步扩展到更多图形类型,并补上超时检测和稳定性处理
所以今天看到的 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.09a66fe2a1 BI-182443 fix: 移动端饼图无法上下滑动
这说明移动端/Pad 并不是天然从 PC 架构里长出来的,而是后期逐步分叉、迁移和补齐的。
开放性方面,则有:
dd9841d61 INO-22202 fix: 抽象图形开接口
这类提交说明系统在较晚阶段才开始认真处理“如何让外部能力挂接进来”的问题。也就是说,早期重点是先跑通 BI 场景,后期才轮到开放抽象图形这类扩展点。
所以如果把 van-analysis 看成一个静态架构,很容易误解它为什么显得“重”;但如果把它看成一个跨多个版本线不断演进的图表内核,很多设计就会变得合理得多。
4. 渲染内核:不是直接画图,而是先建立运行时
src/chart/chart.js 是整套系统的调度中枢。它做的第一件事不是渲染,而是建立运行时环境:
normalizeChartOption规范化入口参数initStore初始化 Redux store- 通过
EventEmitter建立对外事件系统 - 收到数据后调用
createDataController - 再进入
_updateState -> reconstruct -> dispatch(init) - 最后才走
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
源码里通过 visible 和 zrender 初始化渲染实例,再把 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
- 计算
tagFilter、sliderFilter、resetFilterState - 提取
fontScale、adaptive、darkTheme、enableMap等运行时标志
换句话说,后端发来的数据并不是“图表 option”,而是“业务协议”;reconstruct 才是把业务协议还原成图表运行时的那层翻译器。
5.1 这层为什么必须存在
很多图表项目喜欢追求“前后端共享一份 option 协议”,看起来很干净,但在 BI 场景里通常走不通。因为 BI 服务端下发的不只是可视化参数,还混有:
- 初始化交互状态
- 地图中心点和缩放
- 联动、点击、过滤相关状态
- 设计态 / 运行态差异
- 脱敏、格式化、域映射信息
这些信息如果直接灌进渲染层,只会导致每个图元组件都认识服务端协议,最终整个系统失控。
van-analysis 的做法更务实:先承认服务端协议不是前端协议,再单独做一层重建。
5.2 这层的代价
reconstruct 的问题也同样明显。
它把大量领域知识压进了一堆转换函数里,例如 transformGeoms、transformFacet、transformLegend、prepareMapConfig、transformInitState。这种设计早期推进很快,因为业务逻辑都能找到地方塞;但随着图表类型和兼容逻辑继续膨胀,转换层会逐渐变成系统里最难维护的部分。
换句话说,reconstruct 很重要,但它天然会走向“大而杂”。这是这类系统的宿命,不是编码风格问题。
6. grammar 分层:图表被拆成 Geometry + Guide
src/core.js 注册的内容很有代表性。它没有注册“柱状图页面组件”“折线图页面组件”,而是注册:
geom:point、line、interval、heatMap、pie、treeMap、gauge、sankey等guide:plot、facet、legend、hovermap、CrossLine等扩展能力
这说明 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 双实现
交互层被拆成 Mouse 和 Touch 两套实现,而不是用一层非常抽象的 Pointer Event 统一掉。这个选择看上去笨,但在 BI 历史包袱下很合理。
因为 PC 和移动端的差异并不只是事件名不同,而是整套体验不同:
- 移动端可能启用特殊交互模式
- 移动端允许使用 native tooltip
- 触摸环境下空白区域点击、滚动、缩放和选择模式边界不同
- 移动端请求路径和外部容器也不同
这类差异如果硬塞进一套统一抽象里,最后大概率会变成一堆 if (mobile)。现在拆成两层实现,至少边界清楚。
7.3 事件系统承担桥接职责
EventEmitter 支持 legend:toggle 这种 componentType:eventType 格式,store middleware 则在关键 action 后向外触发:
initStateChangedscrollBoundsChangeinteractionModeChangelegend:togglelegend:rangeChange
这说明事件系统不是 UI 内部的小工具,而是图表运行时和外部 BI 容器之间的桥。
在企业 BI 里,图表几乎不可能孤立运行。它必须和筛选器、联动面板、外层容器通信。所以 EventEmitter + Redux middleware 这一套是必须的基础设施。
8. 大数据模式:最有业务味道的设计
src/bigdata/dataControl.js 里把大数据模式分成了三类:
normalsimpleLTTBslice
这不是一个学术上完美的体系,但非常符合真实工程。
更重要的是,这套设计并不是一开始就完整存在的。从历史提交看,大数据能力经历了一个非常典型的演进过程:
- 先发现
bigData标记位本身存在设计缺陷 - 再引入
DataVisitor统一切片数据访问 - 再通过“保持最后一个切片稳定”“增加超时检测”这类修补,把切片模式从实验状态推向可用
- 最后再逐步扩展到散点图等更多类型,并明确排除移动端等不适配场景
所以今天的 createDataController 看起来像一个干净入口,但它背后其实是多轮架构修补之后形成的统一门面。
8.1 simpleLTTB:后端先采样
如果服务端已经提供了 shared.bigDataSlice.sample,前端直接进入 simpleLTTB。这说明前端并不执着于“所有性能优化都必须在前端完成”,而是接受服务端预处理。
这是正确的。因为大数据图表的瓶颈不只在渲染,也在传输和内存占用。很多时候先减小数据体积比前端花哨优化更有效。
8.2 slice:前端滑动窗口切片
更有意思的是 sliceRendering。它不是简单地“采样后全量展示”,而是基于滚动方向、视口大小和单元尺寸,动态计算当前应该渲染的数据窗口。
核心策略包括:
- 只在 PC 端启用
- 只支持特定图元类型:
line、area、interval、point、forcePoint - 必须只有一个方向滚动
- 首屏按视口估算先渲染两屏
- 后续按当前位置扩展到约三倍窗口,减少滚动抖动
- 用 debounce 和最大等待时间平衡空白与卡顿
这是一种非常经典、也非常有效的“视口虚拟化”思想,只不过这里虚拟化的不是 DOM 列表,而是图表数据。
8.3 这套设计的优点
相比只做一个 “bigData = true” 标志位,这种模式分裂更成熟:
- 后端采样和前端切片分责清楚
- 不同图元类型可以渐进式接入
- 保留普通模式,避免所有图都被大数据逻辑污染
8.4 这套设计的局限
代价同样存在:
- 能进入切片模式的图表类型受限
- 交互命中和真实全量数据之间会存在语义差异
- 切片状态本身又变成一套新的运行时,需要和滚动、过滤、tagFilter 协调
但从工程角度看,这已经比“在主线程上硬扛千万点渲染”成熟太多了。
9. 请求与容器适配:图表不是独立应用
src/request/index.js 只定义了一个可扩展的 Request 壳:
getSettingrequestDatagetCompleteUrlservices.refreshservices.filterservices.tooltipInfo
随后在 bi-pc.js 和 bi-mobile.js 里再接入具体环境。
这说明 van-analysis 从一开始就默认自己运行在宿主平台内,而不是一个 npm 装上就能独立完成全部上下文的组件库。
这种设计的优点是可嵌入、可接平台;缺点是边界容易变得模糊。尤其当图表内部开始依赖全局 BI、全局 Van、运行时注入的 Request 实现时,测试和独立演进都会变难。
不过站在 BI 产品的角度,这种耦合常常不可避免。图表既要展示,也要成为平台工作流的一部分。
10. 导出设计:TileChart 说明作者不是只做页面渲染
src/export/TileChart.js 很值得单独说一句。它继承 Chart,但面向的不是浏览器交互,而是图块导出、命中信息查询、平铺图像生成。
这背后的信息量很大:
- 图表状态和图表输出是可以解耦的
- 渲染结果不能绑死在一个 viewport 上
- 导出场景需要自己的命中计算和图元检索
很多团队做图表时,只关注页面上那张图能不能显示;到了导出、打印、切片、服务端截图时,就发现架构完全不支持。TileChart 的存在说明 van-analysis 在设计早期就意识到了这一点。
这不是“锦上添花”,而是 BI 场景里的基础能力。
11. 工程取舍:它强在哪里,也老在哪里
如果要评价这套库,我会说它有一种很典型的企业内核气质:问题意识非常强,现代感不算强。
11.1 它强的地方
第一,它没有把自己误判成一个纯渲染库,而是很早就把协议重建、交互、导出、大数据、宿主适配纳入设计范围。
第二,它的架构虽然重,但大体是自洽的。Chart 负责编排,reconstruct 负责翻译,store 负责状态,interaction 负责命中与行为,bigdata 负责性能模式,模块边界是清楚的。
第三,它对 BI 真实复杂度有敬畏。比如地图初始化状态、移动端差异、图例过滤回传、导出切片,这些都不是“后面再补”的小功能,而是系统长期演进后被稳定沉淀下来的能力。
11.2 它老的地方
从 package.json 就能看到一批明显的时代痕迹:
webpack 4preact 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 不是一套静态的图表框架,而是一台被多个版本周期、多个业务场景和大量线上问题持续塑形的“图表机器”。