前言
本文章主要介绍ECharts整体的架构设计,以及源码中关键的代码部分,用于简单对ECharts的设计以及工作概念有个简单的入门理解,所以不会讲到太深入源码地方,帮助想了解ECharts的同学入门。
Echarts架构图
Echart底层依赖矢量图形库ZRender,基于ZRender之上做了更高层次的抽象,定义出了以下三种元素:
- Series
- Coordinates
- Components
整体构成了下述的图表:
基石:ZRender
GitHub - ecomfe/zrender: A lightweight graphic library providing 2d draw for Apache ECharts
ZRender是二维绘图引擎,它提供Canvas、SVG、VML 等多种渲染方式。基于这些之上定义了如下几个概念:
- animation 定义动画
- graphic 定义图形
- mvc 定义模式
- 统一UI Event(封装多平台操作差异)
- 多种渲染引擎(Canvas/SVG/WebGL)
- 辅助工具库等
其中最核心的是定义图形:
于是乎,你在你就可以通过简单的几行代码画圆形了
var zr = zrender.init(document.getElementById('main'));
var circle = new zrender.Circle({
shape: {
cx: 150,
cy: 150,
r: 40
},
style: {
fill: 'none',
stroke: '#F00'
}
});
zr.add(circle);
当然这里只是画一个圆,如果用canvas代码则如下:
var canvas = document.getElementById('main1');
canvas.width = 300;
canvas.height = 300;
var ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = '#F00';
ctx.arc(150, 150, 40, 0, Math.PI * 2);
ctx.closePath();
ctx.stroke();
两者效果如下:
整体来说没太大的差别,但是你会发现如果不用ZRender的抽象的话,你会写一堆复杂更底层的canvas的api代码。假如demo是如下的饼图:
这里面又有文字 + 扇形 + 线段等元素的图形,如果直接用canvas,那代码量将会是爆表。所以ZRender基于Canvas、SVG、WebGL这些渲染方式,自己进行了底层代码封装。你只知道ZRender的基本图形以及API相关的用法,基本也能够满足日常图形绘制等等相关的需求了。当然,具体代码可以通过Github这里查看,这里拿Line作为举例(https://github.com/ecomfe/zrender/blob/master/src/graphic/shape/Line.ts)
所以,对于EChart来说,基本底座也是基于ZRender,他只需要根据实际用户传入的Option进行Create、Update,然后解析出每个图标元素所需要的图形,再交由ZRender渲染就可以了。大致流程如下图:
此外,ZRender还提供了统一的UI Event管理用于屏蔽底层差异
ECharts高级的抽象:序列 & 组件 & 坐标系
上述为EChart中具体的UI元素,主要分为两种类型:Series和Components。它们的都继承于View目录中的Chart.ts(https://github.com/apache/echarts/blob/f3471f0a70/src/view/Chart.ts) 以及Component.ts(https://github.com/apache/echarts/blob/f3471f0a70/src/view/Component.ts) 两个核心接口,具体职责是负责调用ZRender绘制展示以及处理用户交互。
Series用于描述数据的表现形态,如:折线、饼图、柱状图等
具体源码对应位置如下(https://github.com/apache/echarts):
Components用于丰富图表中的具体展现和交互形式:XYAxis、Legend、Title、Toolbox等
当然除了Series和Component并不能去构成一个图表,还缺少一个具体的坐标系系统。Coordinates其核心的主要是把Series转化成坐标系的点的位置,以及协同一系列的组件完成一个图表坐标的功能。具体坐标系在ECharts源码中如图所示:
所有的坐标系都实现了CoordinateSystem接口定义(https://github.com/apache/echarts/blob/master/src/coord/CoordinateSystem.ts)其中最核心的代码为:
定义了具体的Series的data部分数据怎么转化为坐标系中的点。举个简单的代码例子(以下以Gird坐标系为例):
然后当Model发生变化,触发UI重新Layout的时候,这里则会调用掉具体的坐标系系统进行Series的data重新生成为点的位置。具体代码位置如下(https://github.com/apache/echarts/blob/master/src/layout/points.ts):
具体Echart目前提供的Series&Components&Coordinates如下图所示:
- Series(MS的Excel很早对这种图表元素类型英文定义为Series,所以图表类型也沿用Excel的英文)
其实这里的Series并不包含坐标系的概念,你可以理解是数据的表达方式的结构
- Coordinates(坐标系:笛卡尔坐标系、极坐标系、地理坐标系等)
这里的坐标系是为了把Series中的DataList转化为坐标系上的点
- Components(图标组件:标题、提示框、工具栏、图例等)
这里的Components主要是为了辅助图表用户某些功能的组件
状态&事件:状态管理工作流与事件处理
当用户通过鼠标去点击图标中的某个元素,就会有相对应的互动。
举个例子,比如鼠标Hover到图中的某类数据的时候,其他类别的图表需要进行淡化处理。这里可跟用户输入的Option不一样,每个组件其实内部都是有自己的UI相关的状态,所以这里图标淡化或者是饼图扇区变大这种UI状态相关的操作都是由组件本身的State去控制的,最终也是通过ZRender去渲染。这里有个简单公式:
Final Option = User Option + UserData + UI State。
其中只要任何一个项变化,都会触发整个EChart实例进行渲染,其中:
- UserOption可以通过 setOption去改变
- UserData可以因为Legend点击隐藏某一项data而改变,也可以因为setOption改变
- UI State可以因为用户进行某种UI操作改变了UI状态,比如饼图弹出、Hover高亮折线图等
上述所有的行为,都会触发UI的重新Layout和Render(这一点跟浏览器的机制很像)。具体如下图所示:
所以根据上述这种UI更新的机制,EChart拆分出了Model和View两个概念,通过Model数据改变进而去驱动View展示。整体数据派发就是简单且单一的数据流。
在源码设计中,一般我们可以看到如下的命名规范:
Series中一般命名习惯为:
- XXXSerise.ts(https://github.com/apache/echarts/blob/f3471f0a70/src/chart/pie/PieSeries.ts)
- XXXView.ts(https://github.com/apache/echarts/blob/f3471f0a70/src/chart/pie/PieView.ts)
Component中一般命名习惯为:
- XXXModel.ts(https://github.com/apache/echarts/blob/f3471f0a70/src/component/legend/LegendModel.ts)
- XXXView.ts(https://github.com/apache/echarts/blob/f3471f0a70/src/component/legend/LegendView.ts)
以及其中每个EChart实例对应一个EcModel负责管理所有的Model状态分发,具体GlobalModel逻辑可以从源码去了解(https://github.com/apache/echarts/blob/f3471f0a70/src/model/Global.ts)。
每当用户点击图标中的Series或者是Component的时候,都会触发ZRender的UI事件,通过UI事件Dispatch给GlobalModel,然后GlobalModel则会通知需要更新的所有关联的组件(当然这里每个组件都做了对应的ZRender事件派发监听,如下代码所示,以Pie这个组件为例)。
https://github.com/apache/echarts/blob/f3471f0a70/src/chart/pie/install.ts
此处由统一的Action进行托管所有ZRender的事件类型,动态生成监听函数。
https://github.com/apache/echarts/blob/f3471f0a70/src/legacy/dataSelectAction.ts
这里上一张完整的EChart数据流图:
官方文档
整体小结
EChart的底层是基于ZRender的绘图能力。并且在这个基础之上,抽象了系列、组件、坐标系三种基础概念。并且在这个概念之上,设计出了一套单向的数据流,用于处理用户数据输入以及UI组件本身状态改变时候的更新处理。