一个例子
引入echart折线图
const echartsInstance = echarts.init(document.getElementById("main"))
const option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line'
}]
};
echartsInstance.setOption(option)
这样,一个简单的折线图绘制完成。
然而echart是如何绘制出一副图表的呢?这是这篇文章探讨的问题。
初始化
首先需要初始化echart的实例
// ./charts.ts
import { init } from './core/echarts';export default { init() { return init.apply(null, arguments); }};
// ./core/echarts.ts
export function init( dom: HTMLElement, theme?: string | object, opts?: { renderer?: RendererType, devicePixelRatio?: number, width?: number, height?: number, locale?: string | LocaleOption }): EChartsType { const chart = new ECharts(dom, theme, opts); // 将Echarts实例化 chart.id = 'ec_' + idBase++; instances[chart.id] = chart; ... return chart;}
看起来,初始化的核心工作集中在Echarts对象中,
class ECharts extends Eventful<ECEventDefinition> {
id: string;
group: string;
private _zr: zrender.ZRenderType;
private _dom: HTMLElement;
private _model: GlobalModel;
private _throttledZrFlush: zrender.ZRenderType extends {flush: infer R} ? R : never;
...
constructor(
dom: HTMLElement,
theme?: string | ThemeOption,
opts?: {
locale?: string | LocaleOption,
renderer?: RendererType,
devicePixelRatio?: number,
useDirtyRect?: boolean,
width?: number,
height?: number
}
) {
super(new ECEventProcessor());
...
// 借助zrender进行画布的初始化
const zr = this._zr = zrender.init(dom, {
renderer: opts.renderer || defaultRenderer,
devicePixelRatio: opts.devicePixelRatio,
width: opts.width,
height: opts.height,
useDirtyRect: opts.useDirtyRect == null ? defaultUseDirtyRect : opts.useDirtyRect
});
...
// 注册交互事件的回调函数,保证重绘
zr.animation.on('frame', this._onframe, this);
...
}
...
}
上述仅保留了构造函数的关键逻辑,
首先,基于zrender进行画布初始化(zrender.init),关于zrender的初始化的细节介绍,安排在zrender章节进行介绍。
接着注册了一些交互事件的回调函数,诸如zr.animation/ resize等。回调函数的核心功能是图表的重绘,细节这里不多介绍。
setOption
用户通过当前方法,能够将设置好的属性对象绑定至echarts实例上,实现图表的构建和首次渲染。具体逻辑如下
setOption<Opt extends ECBasicOption>(option: Opt, notMerge?: boolean | SetOptionOpts, lazyUpdate?: boolean): void { ... // 准备数据
prepare(this);
// 更新视图 updateMethods.update.call(this); ... }
上述片段仅保留了最核心的逻辑。
1. 准备数据
prepare方法会将图表中的子组件组合到一起,将组装好的图表对象存储到zr对象中来
// 准备数据
prepare(this);
// 图表组建准备工作,引入各种图表 prepare = function (ecIns: ECharts): void { ... prepareView(ecIns, true); ...};prepareView = function (ecIns: ECharts, isComponent: boolean): void { ... isComponent ? ecModel.eachComponent(function (componentType, model) { componentType !== 'series' && doPrepare(model); }) : ecModel.eachSeries(doPrepare); function doPrepare(model: ComponentModel): void { const viewId = '_ec_' + model.id + '_' + model.type; let view = !requireNewView && viewMap[viewId]; if (!view) { const classType = parseClassType(model.type); const Clazz = isComponent ? (ComponentView as ComponentViewConstructor).getClass(classType.main, classType.sub) : (ChartView as ChartViewConstructor).getClass(classType.sub)); // 组合图表中的各个子组件 view = new Clazz(); view.init(ecModel, api); viewMap[viewId] = view; viewList.push(view as any); zr.add(view.group); } } ...};
2. 更新视图
通过调用update方法,实现图表视图的首次更新
updateMethods.update.call(this)
而update方法中,调用了render方法,它是渲染的核心方法
updateMethods = { update(this: ECharts, payload: Payload, updateParams: UpdateLifecycleParams): void { ... render(this, ecModel, api, payload, updateParams); ... }}
render = ( ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload, updateParams: UpdateLifecycleParams) => { renderComponents(ecIns, ecModel, api, payload, updateParams); renderSeries(ecIns, ecModel, api, payload, updateParams);};
render方法分为renderComponents(渲染Component)以及renderSeries(渲染series)两大部分
renderComponents方法中通过each遍历调用component下的render方法
renderComponents = ( ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload, updateParams: UpdateLifecycleParams, dirtyList?: ComponentView[]) => { // 渲染所有组件 each(dirtyList || ecIns._componentsViews, function (componentView: ComponentView) { componentView.render(componentModel, ecModel, api, payload); });};
renderSeries方法在echarts3.0中也是通过each遍历调用series下的render方法,在4.0版本之后,便将控制渲染的逻辑交给了scheduler调度器进行处理,通过调用charts中的reset方法进而调用series下的render方法,具体逻辑不再赘述。
在echart中,将通常所说的图表组件称为Series,如line折线图/ bar柱状图 / pie饼图**;**而将除了在此之外的相关组件称之为component, 如title 图标标题 / legend 图例组件 / AxisPointer 坐标轴指示器 / 坐标系 / 坐标轴等。
接下来分别介绍series和component。
第二部分 Component
针对各种图表相关组件,echart采用了MVC结构来拆分代码结构,其中Model用来管理组件数据, View用来渲染视图。
以图表标题组件(Title)为例,
使用时在option的对应字段,增加配置数据信息
title: { left: 'center', text: '例子Title' }
title文件中Model部分通过extendComponentModel方法扩展自Component Model,重写了defaultOption属性,用于设置title的默认option
View部分通过extendComponentView方法扩展Component View,重写了render方法对title进行渲染
render(titleModel: TitleModel, ecModel: GlobalModel, api: ExtensionAPI) { // 新增text元素 const textStyleModel = titleModel.getModel('textStyle'); const textEl = new graphic.Text({ style: createTextStyle(textStyleModel, { text: titleModel.get('text'), fill: textStyleModel.getTextColor() }, {disableBox: true}), z2: 10 }); group.add(textEl); // 新增样式 const alignStyle = { align: textAlign, verticalAlign: textVerticalAlign }; textEl.setStyle(alignStyle); // 新增背景 groupRect = group.getBoundingRect(); const rect = new graphic.Rect({ shape: {...}, style: style, subPixelOptimize: true, silent: true }); group.add(rect);}
title文本渲染主要是通过zrender graphic中的Text进行渲染的,通过zrender Style中定义的setStyle方法对元素进行样式设定。
第三部分 Series
与component的组件结构类似,Series组建也拆分为model和view两部分
以折线图Line组件为例
LineSeries
LineSeries通过extend方法扩展自Series Model,重写了defaultOption属性以及getInitialData方法。
Lineview
LineView通过extend方法扩展自Chart View,重写了init、render、highlight及downplay等方法
render(seriesModel: LineSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { ...
// 通过zrender Polyline绘制折线图 polyline = this._newPolyline(points, coordSys, hasAnimation);
if (isAreaChart && !polygon) { // 通过zrender Polygon绘制折线区域 polygon = this._newPolygon( points, stackedOnPoints ); } else if (polygon && !isAreaChart) { // If areaStyle is removed lineGroup.remove(polygon); polygon = this._polygon = null; } ... if (polygon) { ... polygon.useStyle(zrUtil.defaults( areaStyleModel.getAreaStyle(), { fill: visualColor, opacity: 0.7, lineJoin: 'bevel' as CanvasLineJoin, decal: data.getVisual('style').decal } )); polygon.setShape({ smooth, stackedOnSmooth, smoothMonotone, connectNulls }); ... } ...}
其中,基础折线、折线区域的绘制,依赖了zrender的基础绘图api的支持。
以上就是echart的核心绘图逻辑。(事件机制等重要但非核心的模块暂时未做介绍)
可以看到,无论是图表的初始化(zrender.init)、还是具体组件的渲染(new graphic.text)等,均依赖了一个叫做zrender的底层绘图库,所有的绘图细节都被封壮在其中。接下来,我们来分析下zrender中的结构。
Ps:Zrender基础绘图库
zrender是基于canvas的一个绘图库,整体架构采用MVC
- Storage(M):图形数据的CRUD
- Painter(V):对canvas元素的渲染和更新进行管理
- Handler(C):实现事件交互处理,对canvas元素实现类dom的模拟封装
在基础架构下,首先关注下提供的一系列基础形状组件。目录在./src/graphic/shape
以Circle.ts为例,
class Circle extends Path<CircleProps> { shape: CircleShape constructor(opts?: CircleProps) { super(opts); } buildPath(ctx: CanvasRenderingContext2D, shape: CircleShape, inBundle: boolean) { if (inBundle) { ctx.moveTo(shape.cx + shape.r, shape.cy); } ctx.arc(shape.cx, shape.cy, shape.r, 0, Math.PI * 2); }};
Circle类继承自基础图形类Path,重写了buildPath方法,完成了canvas具体图形的绘制。
另外
Path类定义了基础图形的接口方法,如buildPath等;并实现了部分通用方法,如contain()等。Path类继承自Displayable类。
技术方案特点
1.模块拆分清晰
renderer提升为接口,具体渲染器可扩展canvas、svg等。 ./src/renderer/{renderer} 默认canvas渲染器会引入zrender/src/canvas/Painter
具体canvas的基础图形渲染,以及基础的绘图api的能力独立拆分出,封装为zrender库。
2. 扩展性强
tooltip支持canvas渲染、dom渲染
3.渲染性能强
canvas绘图,支持同步渲染、异步批量渲染。