架构概述
AIGC 可视化组件库的程序架构介绍。
AIGC 产品 -> AI 可视化推荐模型算法服务 -> 推荐结果 + 业务参数 -> 渲染可视化效果
整体简化的业务流程如上图所示,客户端首先通过调用 AI 可视化推荐模型算法服务获得部分规范参数,再结合一些指定的其它规范参数统一传递给组件库,组件库对 JSON 格式的规范参数进行解析,然后调用底层特定组件库完成可视化效果渲染。
其中,组件库的部分规范参数会作为特定类型组件的特征(例如数据集的编码映射约束)用来训练 AI 可视化推荐模型,随后在具体的业务场景中,客户端需要将要进行可视化渲染的数据集和其它参数传递给 AI 可视化推荐模型算法服务,获取部分规范参数的值。
该项目的可视化能力取决于底层的可视化组件库,目前接入了以下组件库:
规范设计
为了隔离业务侧和组件侧的实现细节,设计 JSON 格式规范(Specification)作为中间协议层,所有不同类型的组件按统一的规范向外暴露接口。协议层的作用主要体现在以下几个方面:
- 统一不同类型组件的数据结构(一维对象数组/表格数据)
- 统一抽象组件可视化特征(用于 AI 可视化推荐模型训练的元信息)
- 统一工程化相关的业务参数
规范(协议层)的具体定义可直接查看源码:
// src/api.ts
interface Specification {
/** Hook API */
hook: Partial<Hook>;
/** 设计 tokens */
token: Partial<Token>;
/** 布局 */
layout: (typeof LAYOUT)[keyof typeof LAYOUT];
/** 数据集 */
data: { metadata?: unknown; values: TabularData }[];
/**
* 视图配置,分块
*
* - 例如主副图视图中,主图为一个块,副图为另一个块
*/
view: {
/** 主图 */
main: NormalViewSpec | StandardChartViewSpec | HXKLineViewSpec;
/** 副图 */
secondary?: SimpleKLineViewSpec;
};
}
主题与布局
主题是为了实现视觉多样化适配,其作为规范参数中的 token 字段存在。其设计参考了设计 Token 这一概念,将一些原子化的视觉配置统一为 Token 集合的引用,替换 Token 集合的实现即可达到视觉效果的多样化目的,具体实现兼容 ECharts 主题。
布局是设计规范中关于布局的分类定义,以实现适应不同尺寸的差异化可视化效果,其作为规范参数中的 layout 字段存在,不再维护。
关于设计 Token 可参考该文章进一步了解:Design tokens, what?
更多信息,查看设计 Token。
数据集
数据集放在全局定义,目的是为了实现在多个层中重复引用以降低内存消耗,其作为规范参数中的 data 字段存在。数据集的数据结构为标准的表格数据集(即一维对象数组),可以同时指定多个数据集,并在层中通过 dataIndex 字段指定引用的数据集的索引。
其中,还可以通过 data[].metadata 字段额外指定数据集的元信息,以便获取对数据集的精确描述,创造更高效的可视化效果。
视区块与层
这里引入了两个新的概念,一个是视区块(View Block),另一个是层(Layer)。
前者是为了抽象 UI 布局,其作为规范参数中的 view 字段存在。由于 AIGC 组件不再是一个简单的常规图表,而是高度封装的复杂可视化效果,所以将其进行拆解抽象以实现灵活性。例如,设计规范中的上下布局(即主副图模式),即对应了 view.main( 上方主图)和 view.secondary(下方副图)。
后者是为了实现多个单一类型组件的叠加进行的抽象设计,其作为规范参数中 view.main.layers 字段存在。层(Layer)是画布上一类可视化效果(单一类型图表组件)渲染的基本单位,层之间可以叠加实现更复杂的可视化效果,例如折柱图。
目前仅部分层(柱、折、饼)可实现叠加。
层(Layer)可以类比为 ECharts 的系列(Series),但比其抽象程度更高,这一概念参考了 Vega 的设计。
Hook APIs
Hook 机制参考了插件架构设计,首要目的是为了解决一个组件的实现在多个业务场景下规范具有差异性的问题,即将有差异的部分逻辑从组件实现中抽象为对应的 Hook API,具体实现可由多个业务方自己实现。
例如,对于数值的格式化,两个不同的业务可能对于数值的小数处理、小数位保留等实现细节有自己的规范实现。默认实现为:
// src/hook/preset/formatDataNumberValue.ts
import type { Hook } from '../types';
export function formatDataNumberValue(value: number) {
return {
value,
unit: ''
} as ReturnType<Hook['formatDataNumberValue']>;
}
export default formatDataNumberValue;
针对 F10 业务做差异化实现:
import type { Hook } from '../types';
export function formatDataNumberValue(value: number) {
if (typeof value === 'undefined' || Number.isNaN(+value) || Math.abs(value) === Infinity) {
return {
value: '',
unit: '--'
};
}
return F10Utils.getFormatYAxis(value) as ReturnType<Hook['formatDataNumberValue']>;
}
export default formatDataNumberValue;
更多信息,查看 Hook APIs。
程序流程
下图给出更详细的规范解析并渲染可视化的流程图:

主题与配置解析流程
AIGC 组件库的核心是抽象并解析底层依赖的组件库的配置项。
因此,主题(Token)机制本质上是提供了一套图表组件的默认配置项,在渲染阶段,View 中的每一个组件(Layer)最终也会生成图表组件的一部分配置项,组件库会将这两部分配置项进行合并,其中组件(Layer)生成的配置项优先级高于主题(Token)解析后得到的配置项。
主题(Token)机制的目标是实现不同类型图表组件的风格统一化,更关注整体。如果实际的业务场景中要实现不同类型图表组件的差异化,组件库也支持直接传参的方式:
{
token,
hook,
view: {
main: {
layers: [
{
type: 'bar',
encoding: {
x: 'date',
y: 'y'
},
showBackground: true,
}
],
gird: {
show: true,
},
}
}
}
所以,组件库对于配置项的解析有三个关键阶段,优先级从低到高依次为:
- 主题(Token)解析为默认配置
- 组件(Layer)渲染生成部分配置
- 直接传参进行配置
范式组件库的配置项解析流程
在实现基于范式组件库开发的组件(Layer)的配置项合并逻辑中借鉴了 ECharts 的主题解析策略,即对于特定 type 的组件配置,允许同时存在一个指定 type 的高优先级配置和一个不指定 type 的低优先级通用配置,按优先级顺序进行合并。
例如,对于不同类型的 series,在 Token 中可以按以下方式进行配置:
{
token: {
dvStandardChart: {
baseOption: {
series: [
{
label: {
show: true
}
},
{
type: "bar",
colorBy: "series"
},
{
type: "line",
colorBy: "data"
}
]
}
}
}
}
按以上 Token 配置,如果存在以下一个柱状图的配置项:
{
type: "bar",
data: []
}
在合并时,会先按 type 字段找到高优先级的配置进行合并,即:
{
type: "bar",
data: [],
colorBy: "series"
}
然后,再找到无 type 字段的低优先级通用配置进行合并,最终该柱状图的配置项如下:
{
type: "bar",
data: [],
colorBy: "series",
label: {
show: true
}
}
具体的合并逻辑实现可参考 src/helper/standardChart.ts 文件中的 matchMergeOption() 和 findOptionByType() 函数。
项目结构
src/
|-- core/ # 核心 APIs 实现
|-- helper/ # 辅助类
|-- ui/ # 可复用的一些公共 UI 组件
|-- hook/ # 一些公共的类似插件功能的实现
|-- config/ # 一些静态配置文件
└-- layer/ # 单个类型组件的具体实现
以上是组件库的核心项目结构。
其中 ui 目录下是为了减少每个组件的具体实现代码规模而剥离的可重用的基础组件;config 目录下是全局相关的一些静态配置。
需要重点说明的是 hook 目录,该设计的首要目的是为了解决一个组件的实现在多个业务场景下规范具有差异性的问题,即将有差异的部分从组件实现中抽象为对应的 Hook API,具体实现可由多个业务方自己实现。例如,对于数值的格式化,两个不同的业务线可能对于数值的小数处理、小数位保留等实现细节有自己的规范实现。
此外,hook 目录下的部分模块实际上也承担了公共代码剥离的任务,即将多个组件中的相同逻辑代码抽象为对应的 Hook API,放在该目录下统一实现,降低后续维护成本。