前言
17年底 G2 3.0 正式发布开源,我们部门决定将可视化图表库的底层框架统一为 G2,替换掉内部野蛮生长的 Echarts、Highcharts 等。
G2 是一套面向常规统计图表,以数据驱动的高交互可视化图形语法,具有高度的易用性和扩展性。
我们部门面向中后台业务,可视化图表是强需求。开发者涵盖对可视化不熟悉的前端开发,后端开发,合作伙伴开发等等。让大家都从零开始学习 G2 的图形语法,理解“视觉通道”、“数据映射”等专业可视化概念成本太高。因此需要基于 G2 构建一个真正开箱即用的图表库,赋能开发,提升效率,统一视觉效果。
这就是 CloudCharts 的由来,经过3年内部孵化,正式开源,有兴趣的同学可以在官网在线体验:传送门。
今天讲讲我们是如何用 G2 构建简单易用的 React 图表库,通过少量配置项即可绘制图表,服务内部近两百个产品。
三板斧
我们针对 G2 在使用中三个最常见的疑问 React化、配置化、数据映射 做详解,分为这三部分:
1、建立图表生命周期
2、开箱即用包装
3、数据格式映射
建立生命周期
首先搭建组件库整体的架构,目的是让项目能长期的维护和发展,尽量减少可能产生的历史包袱,将 G2 原生的图形语法转化为 React 组件。
我们把组件绘制图表的全部流程列出来:
观察图表绘制的过程,我们可以将其按照组件生命周期划分为几个部分:创建期、存在期、销毁期。这和 React 的生命周期是基本一致的。
我们发现这些绘制流程很大一部分是可以复用的,大多数组件只有“声明绘制逻辑”这一步不一样,以此建立一套生命周期管理机制。
组件代码由原生代码实现,负责声明 G2 的图形语法。中间的工厂函数负责创建图表实例,调用生命周期,以及一些复用的逻辑,最后返回 React 组件。
工厂函数示例
工厂函数是管理生命周期的核心,在这里简单展示工厂函数的代码。实际逻辑会多很多,有兴趣的同学可以去 Github 查看源码:传送门
import G2 from '@antv/g2';
import React from 'react';
function factory(name, component) {
class CloudCharts extends React.Component {
componentDidMount() {
// 设置初始高宽
this.initSize();
// 初始化图表
this.initChart(this.props);
}
componentDidUpdate(prevProps) {
// 配置项有变化,重新生成图表
this.rerender();
// 数据有变化,更新数据
component.changeData(data);
}
componentWillUnmount() {
this.destroy();
}
// 初始化适配高宽
initSize(props) {}
// 初始化图表
initChart(props) {
this.chart = new G2.Chart({
container: this.chartDom,
});
// 调用组件代码
component.init(this.chart);
}
destroy() {
component.destroy();
}
render() {
return (
<div ref={dom => (this.chartDom = dom)} />
);
}
}
CloudCharts.displayName = `CloudCharts${name}`;
return CloudCharts;
}
开箱即用包装
有了生命周期管理,组件可以专注在图形语法翻译,让用户传入简单的配置项即可得到图表。
我们先来看看 G2 图形语法的有哪些部分:
- 顶层配置 ✅
- 数据度量 Scale
- 坐标系 Crood
- 坐标轴 Axis ✅
- 提示信息 Tooltip ✅
- 图例 Legend ✅
- 辅助标记 Guide ✅
- 图形元素 Geom
- 文本标签 Label ✅
其中打✅的部分逻辑是很容易复用的,可以用函数抽象配置。
组件代码示例
// 折线图 Line
const lineComponent = {
init(chart, config, data) {
// 列定义
const defs = {...}
// 传入数据
chart.source(data, defs);
// 设置X轴
rectXAxis.call(this, chart, config);
// 设置Y轴
rectYAxis.call(this, chart, config);
// 设置图例
rectLegend.call(this, chart, config);
// 设置tooltip
rectTooltip.call(this, chart, config);
// 设置辅助线,辅助背景区域
guide.call(this, chart, config);
// 设置图形元素
drawLine.call(this, chart, config);
// 其它逻辑
},
...
};
配置项翻译器
我们将组件内接受配置项,声明图形语法的逻辑统称为配置项翻译器。
drawLine 中翻译线图配置项的示例:
if (config.area) {
chart.area()
.position(['x', 'y'])
.color('type', config.colors);
chart.line()
.position(['x', 'y'])
.color('type', config.colors)
.style('x*y*type*extra', {
lineJoin: 'round',
});
} else {
...
}
智能化配置项
除了G2自带的配置项外,我们还做了很多智能化配置的包装,降低了使用难度。
-
自动适配尺寸
-
自动更新数据
-
自动更新配置
-
自动计算时间格式
-
轴标签个数自动计算
-
图例过多自动折叠
…
由于篇幅原因这里就不详细展开,后续还会有文章介绍。
数据格式映射
G2 的数据映射也是很多初学者疑惑的一点,为此我们在工厂函数中做了一层数据转换,允许用户传入 HighCharts 格式的数据。那么内部是怎么实现的呢?
我们来观察一下这张图表,只由一条折线和点组成:
对应的 G2 数据格式是一个非常简单的 JSON 数组,数组中的每一项对应图表的一个数据点:
const data = [
{ year: '1995', value: 4.9 },
{ year: '1996', value: 6 },
{ year: '1997', value: 7 },
{ year: '1998', value: 9 },
{ year: '1999', value: 13 },
];
而Highcharts格式稍微复杂一些,但是关键内容都是一致的:
const data = [
{
// name 用于区分多组数据
name: '折线一',
data: [
['1995', 5],
['1996', 6],
['1997', 7],
['1998', 9],
['1999', 13],
],
},
];
所以可以通过循环处理转化为G2的格式:
const newData = [];
data.forEach(oneData => {
const { name: dataName } = oneData;
oneData.data.forEach((d, i) => {
const [x, y, ...extra] = d;
newData.push({
x,
y,
extra,
type: dataName,
});
});
});
这里返回的 x、y、type 对应了图形语法中的 field :
chart.geom().position(['x', 'y']).color('type')
等于图表库内部固定了 data fields,用户可以使用 HighCharts 格式的数据,这也方便了之前使用 HighCharts 的业务迁移到新图表库中。
写在最后
有了上面的三板斧,整个图表库的框架基本完整,可以愉快的用在业务中了。欢迎大家来使用我们的完整版本:
我们在官网还增加了“主题设计器”和“可视化配置”等功能,将会在后续的文章中继续介绍。