A case in point

Introduce the Echart line chart

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)
Copy the code

In this way, a simple line chart is drawn.

But how does Echart produce a chart? This is the question addressed in this article.

Initialize the

First you need to initialize an instance of 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); // instantiate Echarts chart.id = 'ec_' + idBase++; instances[chart.id] = chart; . return chart; }Copy the code

It seems that the core of initialization is concentrated in the Echarts object,

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()); . 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 }); . On ('frame', this._onframe, this); // Register the callback function for the interaction event to ensure that zr.animation.on('frame', this._onframe, this) is redrawn; . }... }Copy the code

The above only preserves the key logic of the constructor,

First, canvas initialization based on ZRender (zrender. Init), the details of zRender initialization are arranged in the ZRender section.

We then register callback functions for interactive events, such as zr.animation/ resize. The core function of the callback function is to redraw the chart, which is not covered in detail here.

setOption

With the current method, the user can bind the set property object to the Echarts instance for the build and first rendering of the diagram. The logic is as follows

setOption<Opt extends ECBasicOption>(option: Opt, notMerge? : boolean | SetOptionOpts, lazyUpdate? : boolean): void { ... // Prepare (this); / / update the view updateMethods. Update. The call (this); . }Copy the code

The above snippet preserves only the core logic.

1. Prepare data

The Prepare method groups the child components of the chart together, storing the assembled chart object in a ZR object

// 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); }}... };Copy the code

2. Update the view

The first update of the chart view is achieved by calling the UPDATE method

updateMethods.update.call(this)
Copy the code

In the update method, the render method is called, which is the core method of rendering

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); };Copy the code

Render method is divided into renderComponents and renderSeries

RenderComponents calls the Render method under Component with each traversal

renderComponents = ( ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload, updateParams: UpdateLifecycleParams, dirtyList? : ComponentView []) = > {/ / render all components each (dirtyList | | ecIns. _componentsViews, function (ComponentView: ComponentView) { componentView.render(componentModel, ecModel, api, payload); }); };Copy the code

RenderSeries also calls the render method under series via each iteration in echarts3.0. After version 4.0, the logic controlling rendering is handed over to the scheduler for processing. By calling the reset method in Charts and then calling the render method under Series, the specific logic will not be described again.

In Echart, what are commonly called chart components are called Series, such as line chart/bar chart/pie chart **; ** And call related components outside of this component component, such as title icon title/Legend legend component/AxisPointer axis indicator/coordinate system/axis, etc.

Next, series and Component, respectively.

Part 2 Component

For various chart-related components, Echart adopts MVC structure to split the code structure, where Model is used to manage component data and View is used to render the View.

Take the Title component of a chart,

Add configuration data in the corresponding field of Option

Title: {left: 'center', text: 'sample title'}Copy the code

The Model part of the title file extends from the ComponentModel with the extendComponentModel method, overriding the defaultOption property to set the defaultOption for title

View extends ComponentView through extendComponentView method, rewriting the render method to render the title

render(titleModel: TitleModel, ecModel: GlobalModel, api: ExtensionAPI) {// Add text element 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); // Add background groupRect = group.getboundingRect (); const rect = new graphic.Rect({ shape: {... }, style: style, subPixelOptimize: true, silent: true }); group.add(rect); }Copy the code

Title Text rendering is primarily rendered by Text in the Zrender Graphic, and elements are styled using the setStyle method defined in Zrender Style.

Part III Series

Similar to the component structure of Component, the Series component is split into model and View parts

Take the Line graph component as an example

LineSeries

LineSeries extends the Series Model with the extend method, overrides the defaultOption property, and the getInitialData method.

Lineview

LineView extends from Chart View with the extend method, overriding init, Render, highlight, and downplay methods

render(seriesModel: LineSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { ... Polyline = this._newPolyline(points, coordSys, hasAnimation); Polyline = this._newPolyline(points, coordSys, hasAnimation); if (isAreaChart && ! Polygon = this._newPolygon(points, stackedOnPoints); // Zrender 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 (), {the fill: visualColor, opacity: 0.7, lineJoin: 'bevel' as CanvasLineJoin, decal: data.getVisual('style').decal } )); polygon.setShape({ smooth, stackedOnSmooth, smoothMonotone, connectNulls }); . }... }Copy the code

Among them, the basic line, line area drawing, rely on zRender’s basic drawing API support.

This is the core of Echart’s plotting logic. (Event mechanism and other important but non-core modules are not introduced for the moment)

As you can see, both the initialization of diagrams (Zrender. Init) and the rendering of specific components (new graphic.text) rely on an underlying drawing library called Zrender, where all the drawing details are encapsulated. Next, let’s analyze the structure in ZRender.

Ps: Zrender base drawing library

Zrender is a drawing library based on Canvas, and the overall architecture adopts MVC

– Storage(M) : CRUD of graphics data

– Painter(V) : Manages the rendering and updating of canvas elements

– Handler(C) : Realizes interactive event processing and simulated encapsulation of canvas element dom

In the context of infrastructure, focus first on the series of basic shape components provided. Directory at. / SRC/graphic/shape

Take Circle.ts, for example.

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); }};Copy the code

The Circle class inherits from the basic graphics class Path and overwrites the buildPath method to complete the drawing of canvas concrete graphics.

In addition

The Path class defines interface methods for basic graphics, such as buildPath; Contain () and implement some common methods, such as contain(). The Path class inherits from the Displayable class.

Technical features

1. Clear module splitting

Renderer is an interface that can be extended to canvas, SVG, etc. The. / SRC/renderer / {the renderer} the default canvas renderer will introduce zrender/SRC/canvas/Painter

The basic graphics rendering of canvas and the capabilities of the basic drawing API are separately separated and packaged into zRender library.

2. Strong expansibility

Tooltip supports canvas rendering and DOM rendering

3. Strong rendering performance

Canvas drawing supports synchronous rendering and asynchronous batch rendering.