如何在组件库内开发组件
在开发一个新的组件(Layer)之前,请先了解前置概念,如协议层设计、数据规范、Hook APIs 等。
组件概述
了解清楚组件的定义及其在项目架构中的定位,才能实现松耦合的代码设计。
组件是什么?
组件对应的是 Layer 概念,后者是为了实现灵活性而引入的概念,以便实现不同类型图表的叠加展示(目前大部分类型的图表是不支持叠加展示的,通过声明 isIndependentLayer 字段来表明该层是否支持叠加)。
另一方面,组件(Layer)可类比于 ECharts 中的系列(Series)概念,即代表一类特定类型的图表组件。这也意味着,基于灵活的实现,通过暴露相关配置项,一个组件(Layer)应该可以有多种用例,例如对于 type=line 组件(Layer),不同的配置参数可以实现单折线、多折线、堆叠折线、面积图等可视化效果。
考虑到业务侧的客观需要,有时候需要基于一种特定类型的组件(Layer),针对某类具体的业务场景实现高度定制化的可视化效果,建议将其作为特例来实现,即新增一个 Layer 实现,避免将不通用且过度复杂的逻辑耦合到通用组件(Layer)中。
例如,基于 type=line 组件(Layer)要实现一种类似置信区间的可视化效果,由于其逻辑过于复杂且业务化,选择作为特例新增一个不同类型的组件(Layer)来实现。
组件该做什么?
传统的业务前端开发要接入标准范式图表组件库时,需要完成两部分工作:数据集预处理和样式配置。
业务开发之所以要对数据集做预处理的背景是由于渲染不同类型图表组件所需要的数据结构和解析逻辑不一致,而 AIGC 可视化场景需要保证简单、快速地生成多样的可视化效果,因此对数据集的格式做了约束和标准化,并将数据预处理的解析过程抽象为声明式的数据编码参数配置(encoding),确保只要有标准结构的数据集并声明数据编码参数就可以快速生成可视化图表效果。
总得来说,在实现组件渲染逻辑的过程中,实际上是帮业务前端开发做了数据预处理这一部分的工作,使其仅关注样式配置即可。
组件不该做什么?
AIGC 可视化组件库与传统的标准范式组件库一样,都应该以通用性为前提,不该耦合定制化的业务逻辑,否则难以落地在不同的业务场景中。
组件除过封装数据预处理的逻辑之外,应该尽可能不包含样式配置相关的逻辑,确保在没有任何样式配置的前提下仅通过数据配置实现预期的基础可视化效果即可。样式配置应该通过主题风格或者外部传参的方式来实现,保证业务侧可控可调。
开发组件
常规静态图表
此处以简单的柱状图为例进行说明。
创建组件文件
要开发新的组件,请在 src/layer/preset/ 目录下对应的文件中创建新的文件 bar.ts ,开发完成后,在同级目录下的 index.ts 文件中将其导出。
同时,为了便于调试,可以在项目的 examples/case/ 目录下创建一个 Mock 参数的组件用例测试文件,并在 examples/case/index.js 文件中将其导出。
目前 AI 可视化推荐模型输出的结果已经是最新的组件库参数格式(详情可参考了解 AI 可视化推荐模型算法服务数据协议一节的内容),但为了保证还未升级组件库的业务方使用,组件库在 src/core/api.ts 文件中的 convertLegacySpec() 方法中实现兼容参数格式的转换逻辑,但后续新创建的图表将不在兼容旧版数据格式
协议层定义
首先,引入类 型定义和相关依赖:
// 类型定义
import { type Layer, type StandardChartLayerSpec, defineStandardChartLayer } from '../core/layer';
// 特征参数类型常量
import DEFINE from '../../utils/DEFINE';
组件被抽象为一个个 Layer,但由于底层依赖的组件库不同,也将层定义为对应的不同类型,例如基于标准范式图表库的层类型为 StandardChartLayer,基于行情图组件库的层类型为 HXKLineLayer, 基于dom元素构建的文本组件的层类型为NormalLayer,而基于业务方第三方库实现的自定义组件的层类型为ExternalLayer,这些定义可以在 src/core/layer.ts 文件中查看。
View 是 Layer 的容器,也会被定义为同样的类型,两者共同实现了完整的可视化效果,所以前者包含所有不同类型组件的通用逻辑(例如图例),而后者只是特定类型组件的差异化实现。
声明组件基本信息和定义:
// 组件的可视化信息,部分信息用来做训练
const visInfo: Layer['visInfo'] = {
id: 'bar',
name_en: 'Bar',
name_zh: '柱状图',
types_en: [],
purpose_en: [],
purpose_zh: [],
standers: '',
rules: {
x: amount => amount > 1
},
/** 编码映射的字段,用来作为数据特征参数训练 AI 模型,保证和 Spec.encoding 字段一致 */
parameters: {
x: {
itemType: [DEFINE.STR, DEFINE.DATE],
category: DEFINE.SINGLE,
priority: DEFINE.DATE
},
y: {
itemType: [DEFINE.NUMBER],
category: DEFINE.ARRAY
},
z: {
itemType: [DEFINE.DATE, DEFINE.STR],
category: DEFINE.SINGLE,
priority: DEFINE.STR,
optional: true
}
}
};
// 组件的规范定义
export interface Spec extends StandardChartLayerSpec {
/** 编码映射参数 */
encoding: {
x: string;
y: string;
z?: string;
};
}
// 组件的定义和实现
export const bar = defineStandardChartLayer<Spec>({
type: visInfo.id,
/** 独立的层,单独渲染,不能和其它层合并共用 x 轴, y 轴等组件 */
isIndependentLayer: false,
visInfo,
// 渲染函数
render(api, spec, $dom) {}
});
这里需要注意的是,id 作为组件的唯一标识,在所有组件中必须唯一。
查看 Layer['visInfo'] 的源码定义,可以看到还有一些组件的可视化特征参数在这里未声明,例如意图、分类等,这些信息统一在同级目录下的 __metadata.ts 文件中进行管理,不需要在这里显式声明。
实现组件渲染逻辑
Layer 定义中的 render() 函数字段是用来实现具体的图表渲染逻辑。
其中,render() 函数有 3 个参数:
- 参数
api为一些公共的 APIs 能力,例如工具函数(util)、Hook API(hook)、UI 组件(ui)等; - 参数
spec为组件规范定义的参数,例如数据相关字段(数据集的索引dataIndex、数据编码参数encoding); - 参数
$dom为渲染图表的 DOM 元素实例; - 返回 常规的静态图表只要返回常规的
standardChart配置项Option即可。
组件渲染逻辑的具体实现目前版本没有过多的约束,只需要满足以上提到的接口定义约束即可。
这里针对基于标准范式组件库开发的组件给出一些参考建议。
前面提到 View 和 Layer 共同组合起来实现了一个完整组件的渲染逻辑,所以针对 Layer 的实现根据情况将其分为两类:常规静态图表和其它定制图表。
其中常规静态图表指的是仅需要返回一个解析好的配置对象就能实现渲染的组件,该类组件的 render() 函数仅需要返回一个配置对象即可,其它逻辑均可由 View 统一实现。(例如折线图,参考 src/layer/line.ts 文件中的源码实现)
除此之外,一些稍微复杂的定制化组件还需要额外的处理逻辑,并且不同类型之间的逻辑难以统一复用,该类组件的 render() 函数需要返回一个回调函数,其参数为 View 传递过来的代理渲染 APIs,像一些图表实例初始化、时间轴组件初始化的逻辑均必须借助相关代理 APIs 实现。(例如动态折线图,参考 src/layer/dynamicLine.ts 文件中的源码实现)
定制图表
此处以动 态折线图为例进行说明。
创建组件文件
要开发新的组件,请在 src/layer/preset/ 目录下对应的文件中创建新的文件 dynamicLine.ts ,开发完成后,在同级目录下的 index.ts 文件中将其导出。
同时,为了便于调试,可以在项目的 examples/case/ 目录下创建一个 Mock 参数的组件用例测试文件,并在 examples/case/index.js 文件中将其导出。
目前 AI 可视化推荐模型输出的结果已经是最新的组件库参数格式(详情可参考了解 AI 可视化推荐模型算法服务数据协议一节的内容),但为了保证还未升级组件库的业务方使用,组件库在 src/core/api.ts 文件中的 convertLegacySpec() 方法中实现兼容参数格式的转换逻辑,但后续新创建的图表将不在兼容旧版数据格式
协议层定义
首先,引入类型定义和相关依赖:
// 类型定义
import { type Layer, type StandardChartLayerSpec, defineStandardChartLayer } from '../core/layer';
// 特征参数类型常量
import DEFINE from '../../utils/DEFINE';
组件被抽象为一个个 Layer,但由于底层依赖的组件库不同,也将层定义为对应的不同类型,例如基于标准范式图表库的层类型为 StandardChartLayer,基于行情图组件库的层类型为 HXKLineLayer,而基于dom元素构建的文本组件的层类型为NormalLayer,而基于业务方第三方库实现的自定义组件的层类型为ExternalLayer,这些定义可以在 src/core/layer.ts 文件中查看。
View 是 Layer 的容器,也会被定义为同样的类型,两者共同实现了完整的可视化效果,所以前者包含所有不同类型组件的通用逻辑(例如图例),而后者只是特定类型组件的差异化实现。
声明组件基本信息和定义:
// 组件的可视化信息,部分信息用来做训练
const visInfo: Layer['visInfo'] = {
id: 'dynamicLine',
name_en: 'Dynamic Line',
name_zh: '动态折线图',
types_en: [],
purpose_en: [],
purpose_zh: [],
standers: '',
rules: {
data: amount => amount > 1,
x: amount => amount == 1,
y: amount => amount == 1
},
/** 编码映射的字段,用来作为数据特征参数训练 AI 模型,保证和 Spec.encoding 字段一致 */
parameters: {
x: {
itemType: [DEFINE.DATE],
category: DEFINE.SINGLE
},
y: {
itemType: [DEFINE.NUMBER],
category: DEFINE.SINGLE
}
}
};
// 组件的规范定义
export interface Spec extends StandardChartLayerSpec {
/** 编码映射参数 */
encoding: {
x: string;
y: string;
};
}
// 组件的定义和实现
export const bar = defineStandardChartLayer<Spec>({
type: visInfo.id,
/** 独立的层,单独渲染,不能和其它层合并共用 x 轴, y 轴等组件 */
isIndependentLayer: true,
/** 图表包含时间轴组件 */
withTimeline: true,
visInfo,
// 渲染函数
render(api, spec, $dom) {}
});
这里需要注意的是,id 作为组件的唯一标识,在所有组件中必须唯一。
查看 Layer['visInfo'] 的源码定义,可以看到还有一些组件的可视化特征参数在这里未声明,例如意图、分类等,这些信息统一在同级目录下的 __metadata.ts 文件中进行管理,不需要在这里显式声明。
实现组件渲染逻辑
Layer 定义中的 render() 函数字段是用来实现具体的图表渲染逻辑。
其中,render() 函数有 3 个参数:
- 参数
api为一些公共的 APIs 能力,例如工具函数(util)、Hook API(hook)、UI 组件(ui)等; - 参数
spec为组件规范定义的参数,例如数据相关字段(数据集的索引dataIndex、数据编码参数encoding); - 参数
$dom为渲染图表的 DOM 元素实例。 - 返回 需要返回一个回调函数,参数为
View传递过来的代理渲染 APIs
export type StandardChartLayerCustomRender = (renderApi: {
/** 合并主题配置 */
mergeChartOptionProxy: <T = StandardChartOption>(option: T) => T;
/** 请按 (时间轴 -> 图例 -> 图表) 的顺序进行渲染 */
renderTimelineProxy: (data: string[]) => unknown;
/** 请按 (时间轴 -> 图例 -> 图表) 的顺序进行渲染 */
renderLegendProxy: (data: StandardChartLayerRenderResult['legend']['data']) => unknown;
/** 请按 (时间轴 -> 图例 -> 图表) 的顺序进行渲染 */
renderChartProxy: <T = StandardChartOption>(
renderParams: { option: T },
initParams?: { useECharts?: boolean; domContainer?: HTMLElement; theme?: string }
) => StandardChartIns;
/** 重新设置当前周期的配置,并触发legend的事件绑定 */
cacheRenderOptionProxy: <T = StandardChartOption>(option: T) => void;
/** 获取当前图表(合并后)的配置 */
getRenderOption: () => StandardChartOption;
}) => void;
定制图表组件不同类型之间的渲染逻辑不同,且相对复杂,像一些图表实例初始化、时间轴组件初始化的逻辑均必须借助相关代理 APIs 实现,可以参考其他定制图表的实现(例如动态折线图,参考 src/layer/dynamicLine.ts 文件中的源码实现)。