跳到主要内容

自定义图表的开发

@yanhuang/chart-common

炎凰数据分析平台支持用户自行开发图表(Chart)并将之作为应用的一部分进行安装部署。

炎凰数据平台最开始的时候是基于Apache ECharts进行开发的,所以其设计模型和Apache ECharts类似,每个图表背后都有一个对应的配置项(Chart Option),如图所示:

Chart architect

当系统渲染出一个图表时,其实背后由三个部分组成,分别是:

  • 图表实例(Chart Instance)- 是根据图表类(Chart Class)实例化而来的
  • 配置项(Chart Option)
  • 配置面板(Config Form)

当用户打开一个已经保存的chart(例如dashboard中的图表)时,图表会根据储存的配置先创建Chart Option,然后根据已经注册的Chart Class实例化出对应的Chart Instance并将其与配置项进行关联,所以用户保存的一些配置,例如图例位置,图表标题,标题字号等,都会渲染在Chart Instance上

当用户打开编辑模式时,系统会对Config Form进行实例化,并将当前的Chart Option用用户界面的形式展现出来。

当用户在Config Form中修改了一些值时,系统会将这些改动反应到配置项(Chart Option)上,从而将这些改动渲染到对应的Chart Instance上。

了解了上述原理之后,我们也可以理解炎凰数据平台的开发也将针对这三部分来进行开发。

而这个package: @yanhuang/chart-common 是炎凰数据为了方便用户开发,所暴露出的一些公共类,包括:

  • 所有内置图表的基础(BaseChart)
  • 所有图表的配置项的基础(BaseConfigForm)
  • 所有图表配置项的父类(BaseChartOption)

准备工作

  • 用户需要熟悉React开发,需要熟悉ES2015/CSS/Webpack等现代化前端开发技术
  • 用户需要熟悉炎凰应用的结构,并熟练使用炎凰应用开发工具(app raiser)
  • 用户需要熟悉使用ECharts的开发,因为系统默认的图表扩展是基于Apache ECharts
  • 如果用户想完全自定义自己的图表,并使用Apache ECharts之外的图表库,则需要熟悉webpack module federation,炎凰数据平台提供了以webpack module federation为基础的微前端架构

开发步骤

创建应用框架

按照功能,我们平台的应用可以分为三种主要的类型

  • Extension: 数据集成 (Data Integration), 例如纯数据接入或者导出的集成,Data Source/GDI/Export。
  • Visualization : 可视化(Viz) ,纯可视化相关的扩展, 仪表板与报表,Dashboard/report/alerts。
  • Solution : 应用(Premium App), 完整的解决方案,可以包含上述的数据集成和可视化部分。

使用炎凰应用开发工具(app raiser)创建一个炎凰应用框架:

prz init

使用上述命令会进入创建应用的命令行向导,程序会有一些问题,按照实际情况回答完毕以后,程序会在当前目录创建一个以app id为名称的目录(app id会在向导中要求用户输入)。

关于应用类型:如果我们只是为了开发一个自定义应用,我们可以选择Visualization,如果我们需要一个端到端的应用,则建议选择solution

这个目录的大致结构类似于:

.<REPO_HOME>
├── Makefile
├── README.md
├── babel.config.js
├── package.json
├── src
│   ├── LICENSE.md
│   ├── README.md
│   ├── app.toml
│   ├── manifest.toml
│   ├── visualization <-- your visualizations are here
│   │   ├── <YOUR_VIZ_ID>
│   │   └── viz.toml
├── tools
│   ├── debug-server
│   └── debug.toml
├── webpack.config.js

可以看到,这个生成的目录是一个npm package,包括了项目描述文件package.json,各种配置项,如:webpack.config.js, babel.config.js

描述文件 viz.toml

$APP_HOME/src/visualization/viz.toml 是这个应用所包含的自定义图表的描述文件,举个例子:

[sankey-chart]
name = "桑基图" # 显示名称
option_class = "sankey/SankeyChartOption" # 相对于$APP_HOME/visualization的相对路径
export = "SankeyChart" # 基于module federation的注册名称
extend_config_form = true # 布尔值,是否使用系统提供的配置面板描述
extend_echarts = true # 布尔值,是否使用系统提供的Apache ECharts作为图表库基础
remote_module = true # 布尔值,是否使用微前端技术
  • viz.toml 是一个标准的TOML文件,其中的每一个stanza都描述了一个单独的自定义图表,而这个stanza的是以图表的id为名,上述例子中我们描述了一个id为sankey-chart的自定义图表
  • 属性option_class是一个相对路径,这个帮助系统定位这个图表的主入口,也就是这个图表对应的配置项,对应的文件必须是一个ES2015的类,必须将这个配置项类作为default export暴露出来
  • 属性export是在基于Module Federation的注册名称,在之后的webpack配置文件一节会详细讲述

配置文件viz.toml 中的属性option_class指向了一个相对路径,正如webpack配置文件一节中讲述的那样,我们的源文件需要位于$REPO_HOME/src/visualization目录下,但是$REPO_HOME/src/visualization目录和$APP_HOME/visualization目录是等价的(我们通过webpack config保证了这一点)。 正如本文开头提到的那样,YHP的图表设计包含3个部分:ChartOption,ChartClass和ConfigForm。对于开发一个自定义图表来说,ChartOption的开发是必不可少的,这将是图表的主入口。

webpack配置文件

细心的用户也许已经发现,上述例子中的注释中option_class是相对于$APP_HOME/visualization的相对路径,而我们目录结构中我们所说的所有图表都位于$REPO_HOME/src/visualization目录下。

这是因为我们现在看到的是我们进行开发的源码结构,而当用户使用prz build进行构建时,系统会根据webpack.config.js文件中的配置,将每一个visualization都输出到$REPO_HOME/.build/visualization 目录下,而这个.build隐藏目录则是我们打包后真正的APP_HOME

而与项目一同生成的webpack.config.js文件,就是是我们的构建规则。

让我们先查看vizEntry, 这是所有自定义图表的构建入口。当你新建一个图表时,除了在viz.toml中添加一个对应的stanza之外,你需要在这里添加新的构建入口,例如:

const vizEntries = {
// The key is your output path relative to .build/visualization folder
'sankey/SankeyChartOption': path.join(srcDir, 'visualization', 'sankey', 'SankeyChartOption'),
}

注意,这里的 vizEntries 的key将是构建结果的输出目录(相对于.build/visualization目录),而value则是你的option所在的文件地址。

然后让我们继续看到webpack的ModuleFederation插件的配置,在其expose属性中,我们将定义自己所编写的图表的注册入口,例如:

new ModuleFederationPlugin({
name: 'yanhuang_viz',
exposes: {
"SankeyChart": path.join(srcDir, 'visualization', 'sankey', 'SankeyChartOption')
},
filename: 'remoteEntry.js',
shared: {
react: { singleton: true },
antd: { singleton: true },
lodash: { singleton: true },
echarts: { singleton: true },
"echarts-for-react": { singleton: true },
'@ant-design/icons': { singleton: true },
'@yanhuang/ui': { singleton: true}, // 使用该包的app仅在release 2.14之后的版本可用
},
}),

注意: exposes属性的key将作为自定义图表的注册名称,上面的例子中时SankeyChart,这个必须与viz.toml中的export属性一致,这样YHP系统才能识别这个自定义图表。

添加自定义图表

在理解了本文最开始所述的图表工作原理之后,就能知道开发一个图表将分为3个主要部分:

  • 图表配置项(ChartOption)
  • 配置面板(ConfigForm)
  • 图表类(ChartClass)

炎凰数据平台的设计最初时以Apache ECharts为图表库。如果你也希望基于Apache ECharts进行开发,则可以是用@yanhuang/chart-common提供的BaseChart作为图表类,这也是我们默认的用户开发方式。

添加图表配置项

通过上文我们知道,我们的源码都将位于$REPO_HOME/src/visualization目录下,为了更好的封装我们的模块,我们推荐为每个chart设立单独的目录,这个例子中,我们需要新建一个目录$REPO_HOME/src/visualization/sankey,而桑基图所有相关的code,都将位于这个目录下。

我们需要新建第一个文件: $REPO_HOME/src/visualization/sankey/SankeyChartOption.js, 这将是我们的配置项类。

import { merge } from 'lodash';
import { BaseChartOptions } from '@yanhuang/chart-common';

const DEFAULT_SANKEY_OPTIONS = {
title: {
// ...
},
// ... options
series: []
}
const DEFAULT_SANKEY_OPTIONS_PATHS_BLACKLIST = [];
const DEFAULT_SANKEY_SERIES_OPTION = {
type: 'sankey',
layout: 'none',
labelLayout: {
hideOverlap: true
},
top: 30,
lineStyle: {
color: 'source',
curveness: 0.5
},
draggable: true,
nodeAlign: 'justify',
emphasis: {
focus: 'adjacency',
lineStyle: {
width: 10
}
},
}

export default class SanekyChartOption extends BaseChartOptions {
constructor(options, optionsPathBlacklist) {
super(merge({}, DEFAULT_SANKEY_OPTIONS, options),[]);
}

setData(data, fields) {
// TODO: put your logic here, put your final option on this._option
}
}

可以看到,在上述的sample code中, 我们继承了BaseChartOptions,这是@yanhuang/chart-common中对外暴露的基础类,是平台中所有内置图表的配置项的父类,它定义了一一系列接口,例如:

  • setData(data, fields) - 最重要的接口,用户需要在这个方法中将传入的data参数,转化成echart能够识别的data,并且设置到this._options上。
  • toJSON()
  • toFormFields() 系统在渲染config form时,会调用此方法,筛选出Config Form能够识别的配置项,用户一般不需要override此方法,仅在调试时可能会用到。

添加配置面板

我们添加$REPO_HOME/src/visualization/sankey/SankeyChartConfigForm.jsx:

import React from 'react';
import { Tabs, Form, Radio, Switch, Input, Select } from 'antd';
import { BaseConfigForm } from '@yanhuang/chart-common';

const { TabPane } = Tabs;

export default function SankeyChartConfigForm(props) {
return (
<BaseConfigForm {...props}>
<TabPane tab="桑基图" key="sankey">
<Form.Item label="标签显示" name="label.show" valuePropName='checked'>
<Switch />
</Form.Item>
<Form.Item noStyle dependencies={['label.show']}>
{({getFieldValue}) => (getFieldValue('label.show')) && (
<Form.Item label="标签位置" name="label.position" help="根据不同的排布方式可能需要调整标签的位置">
<Select>
<Select.Option key="label-position-option-source" value="right">右侧</Select.Option>
<Select.Option key="label-position-option-bottom" value="bottom">底部</Select.Option>
<Select.Option key="label-position-option-inside" value="inside">内部</Select.Option>
</Select>
</Form.Item>
)}
</Form.Item>
{/* other options... */}
</TabPane>
</BaseConfigForm>
)
}

可以看到,我们的ConfigForm使用了@yanhuang/chart-common提供的BaseConfigForm, 这是系统对外公布的公用组件,他将负责"常规","图例"等公用属性面板的渲染。 而用户在打造他们的属性面板时只需在其children中嵌入一个单独的TabPane,将chart自身相关的配置项单独包起来.

而对于图表的每一个配置项,都是使用antd的Form.Item来渲染 ,整个ConfigForm其实是一个Antd的Form。而Form.Item的 name就是option的名称(如果是嵌套的JSON属性,则需要将其打平),例如:

在ECharts中数据点的标签显示,显示的位置在内部是如下这样配置:

{
"label": {
"show": true,
"position": "inside"
}
}

而我们作为chart配置项储存时,会将其嵌套结构打平,变为:

{
"label.show": true,
"label.position": "inside"
}

当我们需要根据一个属性的值,决定另一个属性项目(Form.Item)是否显示,实现级联联动时,我们可以用antd提供的方法getFieldValue,来获取当前属性值,上述例子中,我们在属性label.show为true时才渲染渲染属性label.position对应的Select

配置文件等杂项

当我们添加完上述文件,我们需要在$REPO_HOME/src/visualization/sankey/SankeyChartOption.js中补上一句:

export { default as ConfigForm } from './SankeyChartConfigForm';

这样,系统才能够在我们的主入口中找到ConfigForm

另外,关于viz.toml:

[sankey-chart]
name = "桑基图"
option_class = "sankey/SankeyChartOption" # relative path to $APP_HOME/visualizations
config_form = "sankey/SankeyChartConfigForm" # relative path to $APP_HOME/visualizations
export = "SankeyChart"
extend_config_form = false
extend_echarts = true
remote_module = true
  • extend_config_form: 在@yanhuang/chart-common 2.10.0以前的版本中,我们仅允许通过JSON描述文件来实现配置面板,所以当你如上面例子中使用React来编写自己配置面板时,这个属性应该置成false
  • extend_echarts: 当你使用ECharts来开发自定义图表时,这个属性应该为true,否则为false
  • remote_module: 在@yanhuang/chart-common 2.10.0 及以上版本中,我们默认使用微前端来进行模块封装,所以这个为

使用其他图表库

然而,我们也并不强制用户一定要使用ECharts。当用户觉得ECharts无法满足自己的开发需求时,也可以使用第三方图表库。我们将通过一个例子来简要阐述,使用antv G6开发一个拓扑图(Graph)

添加图表配置项

添加配置面板

添加图表类

配置文件等杂项

打包

在我们完成图表的开发后,我们可以使用炎凰应用开发工具(app raiser)来进行应用的打包,在$REPO_HOME运行命令:

prz build

此命令会调用yarn build 命令,请保证运行前所有dependencies都已经安装完毕

运行完毕之后,工具会在$REPO_HOME/packages 目录生成对应的zip文件,用户可以使用该文件上传到YHP作为应用进行安装或升级,安装完成后用户将在系统中看到对应的chart

FAQ

Q: 不同app中的图表是否会冲突?

A: 不会,系统会将应用id(app id)和chart id综合,进行注册。所以,用户仅需保证,自己的应用中的chart id不重复,就不会引起冲突