Ecarts name axis_ Build Echarts from scratch -- v1 ZRender and MVC

Posted by bmcua on Fri, 18 Feb 2022 13:15:37 +0100

Build Echarts from scratch -- v1 ZRender and MVC
This chapter begins to enter the main body.

Write in front
Graphics, elements and graphic elements all refer to XElement, depending on the situation.
ts may give a warning. I just want to use the code prompt function, no matter how hot it is.
Not all codes are posted in the article, and with the change of version, there may be inconsistencies between the code in the article and the source code due to untimely modification. You can refer to the source code for viewing.
The way to view the source code. The source code is placed here, and each version has a corresponding branch.
Due to the limited level and subsequent design changes, you can't write the best code in the first version, and there may even be some problems. If you encounter code that you think should not be written like this, please don't worry first.
zrender
Zrender is the 2d renderer used by ecarts, which means that for 2d charts, ecarts is more about data processing. This step is completed by zrender.

The general process is that the user tells echarts that I want to draw a bar chart with ten pieces of data. Echarts calculates the height and coordinates of the bar chart and uses zrender to draw the coordinate axis and ten rectangles on the canvas. It is also the only dependency of echarts. It is a lightweight two-dimensional drawing engine, but it realizes many functions. This paper starts with the implementation of zrender as the first step to implement echarts.

Objective of this article
As mentioned earlier, building ecarts starts with building a zrender, but zrender also has many functions, which can not be achieved in one step, so we start with the most basic functions, and I name it XRender in our library, that is, the renderer with unlimited possibilities. At the end of this article, it will implement the following functions of zrender.
 

import * as xrender from '../xrender'
 
let xr = xrender.init('#app')
let circle = new xrender.Circle({
  shape: {
    cx: 40,
    cy: 40,
    r: 20
  }
})
xr.add(circle)
// Now there is a circle with a radius of 20 on the canvas
 

text
pattern
To be clear, we implement the view based on the data.

Then see what we need to achieve the function we want- Element s to be drawn, such as circles and rectangles, i.e. elements, are temporarily named XElment in order to distinguish them from html- Because there will be multiple elements, we need to add, check, delete and modify them, which is similar to the common scene in 3d game development. It is called Stage here. zrender is called Storage. Almost- You need to draw the elements on the Stage onto the canvas, which is called Paniter- Finally, we need to associate the above three, namely XRender.

That is, MV mode.

Considering that there will be many kinds of graphics, Xrender finally exports a namespace, which follows the design of zrender and does not expose Xrender classes. Then you can start writing code.

Environment construction
For convenience, I use Vue cli to build the environment. You can also use other methods, as long as you can support the syntax. Then create the xrender directory. Or clone warehouse one click installation. According to the classes listed above, create the following files.

index.js # Entry of external references
Painter.js 
Stage.js
XElement.js
XRender.js

However, we need to make a small correction, because XElement should be an abstract class, which only represents one element. It does not provide any drawing method itself. The drawing method should be the Circle class that inherits it. Therefore, the modified directory is as follows.  

│  index.js
│  Painter.js
│  Stage.js
│  XRender.js
│
└─xElements
        Circle.js
        XElement.js

Then create the corresponding class in each file, and let the constructor print out the name of the current class, and then export it to build the overall architecture. For example:

class Stage {
  constructor () {
    console.log('Stage')
  }
}
 
export default Stage

 

Then write index.js

import XRedner from './XRender'
// Export specific element classes
export { default as Circle } from './xElements/Circle'
// Expose only methods and not the 'XRender' class directly
export function init () {
  return new XRedner()
}
Before we use it, we have to XRender Class addition add Method, although it doesn't do anything now.

// Although not used, it needs to be used for type prompt
// It's troublesome to use Flow, ts, or jsdoc
import XElement from "./xElements/XElement";
 
class XRender {
  /**
   * 
   * @param {XElement} xel 
   */
  add (xel) {
    console.log('add an el')
  }
}
Then you can App.vue Write the first code in. If everything goes well, you should be able to see it on the console

XRender
Circle
add an el
 

Detail filling
Before the next step, we may need some auxiliary functions. For example, we often judge whether a parameter is a string. To do this, we create a util folder to store auxiliary functions.

XElement
Graphic element is an abstract class. It should help the class that inherits it, such as circle handle the options such as style. Circle only needs to be drawn. Obviously, its constructor should accept an option as a parameter, including these:
 

import { merge } from '../util'
/**
 * There is nothing at present
 */
export interface XElementShape {
}
/**
 * colour
 */
type Color = String | CanvasGradient | CanvasPattern
export interface XElementStyle {
  // First, set only the stroke color and fill
  /**
   * fill
   */
  fill?: Color
  /**
   * Stroke 
   */
  stroke?: Color
}
/**
 * Element option interface
 */
interface XElementOptions {
  /**
   * Element type 
  */
  type?: string
  /**
   * shape
   */
  shape?: XElementShape
  /**
   * style
   */
  style?: XElementStyle
}
 

Then there is the design of the class. For all options, it should have a default value and be overwritten when updating.

class XElement {
  shape: XElementShape = {}
  style: XElementStyle = {}
  constructor (opt: XElementOptions) {
    this.options = opt
  }
  /**
   * This step is not carried out in the constructor because if it is placed in the constructor, it will be overridden by the default attribute declaration of the subclass
   */
  updateOptions () {
    let opt = this.options
    if (opt.shape) {
      // This function overwrites the original value in the first parameter
      merge(this.shape, opt.shape)
    }
    if (opt.style) {
      merge(this.style, opt.style)
    }
  }
}
 

For an element, a drawing method should be provided, as mentioned above, which is provided by its subclass. In addition, the style needs to be processed before drawing and restored after drawing. This requires a canvas context. It is considered to be provided externally. Please refer to the api involved.

class XElement {
  /**
   * draw
   */
  render (ctx: CanvasRenderingContext2D) {
  }
  /**
   * Handle styles before drawing
   */
  beforeRender (ctx: CanvasRenderingContext2D) {
    this.updateOptions()
    let style = this.style
    ctx.save()
    ctx.fillStyle = style.fill
    ctx.strokeStyle = style.stroke
    ctx.beginPath()
  }
  /**
   * Restore after drawing
   */
  afterRender (ctx: CanvasRenderingContext2D) {
    ctx.stroke()
    ctx.fill()
    ctx.restore()
  }
  /**
   * Refresh, this method is called externally
   */
  refresh (ctx: CanvasRenderingContext2D) {
    this.beforeRender(ctx)
    this.render(ctx)
    this.afterRender(ctx)
  }
 

Why not pass ctx as part of the attribute when creating it? In fact, this is completely feasible. It's just zrender's design. I'll do it for the time being. It may be for decoupling and various ctx needs.

Circle
The base class XElement has been preliminarily constructed. Next, we'll construct Circle. We just need to declare which configurations it needs and provide drawing methods. That is, how to draw a Circle.
 

import XElement, { XElementShape } from './XElement'
 
interface CircleShape extends XElementShape {
  /**
   * Center x coordinate
   */
  cx: number
  /**
   * Center y coordinate
   */
  cy: number
  /**
   * radius
   */
  r: number
}
interface CircleOptions extends XElementOptions {
  shape: CircleShape
}
 
class Circle extends XElement {
  name ='circle'
  shape: CircleShape = {
    cx: 0,
    cy: 0,
    r: 100
  }
  constructor (opt: CircleOptions) {
    super(opt)
  }
  render (ctx: CanvasRenderingContext2D) {
    let shape = this.shape
    ctx.arc(shape.cx, shape.cy, shape.r, 0, Math.PI * 2, true)
  }
}
 
export default Circle
 

Let's verify it, in app Add the following code to Vue:

mounted () {
  let canvas = document.querySelector('#canvas') as HTMLCanvasElement
  let ctx = canvas.getContext('2d') as CanvasRenderingContext2D
  circle.refresh(ctx)
}
 

View the page, there is already a black circle.

Stage

You need it to add, check, delete and modify elements. It's easy to write such code.

class Stage {
  /**
   * Collection of all elements
   */
  xelements: XElement[] = []
  constructor () {
    console.log('Stage')
  }
  /**
   * Add element
   * Obviously, multiple elements may be added
   */
  add (...xelements: XElement[]) {
    this.xelements.push(...xelements)
  }
  /**
   * Delete the specified element
   */
  delete (xel: XElement) {
    let index = this.xelements.indexOf(xel)
    if (index > -1) {
      this.xelements.splice(index)
    }
  }
  /**
   * Get all elements
   */
  getAll () {
    return this.xelements
  }
}
 

Painter

The painting controller, which draws the elements on the Stage onto the canvas, needs to provide a Stage and canvas when creating it - of course, the general practice of the library is to provide a container for the library to create the canvas.

/**
 * Create canvas
 */
function createCanvas (dom: string | HTMLCanvasElement | HTMLElement) {
  if (isString(dom)) {
    dom = document.querySelector(dom as string) as HTMLElement
  }
  if (dom instanceof HTMLCanvasElement) {
    return dom
  }
  let canvas = document.createElement('canvas');
  (<HTMLElement>dom).appendChild(canvas)
 
  return canvas
}
 
class Painter {
  canvas: HTMLCanvasElement
  stage: Stage
  ctx: CanvasRenderingContext2D
  constructor (dom: string | HTMLCanvasElement | HTMLElement, stage: Stage) {
    this.canvas = createCanvas(dom)
    this.stage = stage
    this.ctx = this.canvas.getContext('2d')
  }
}
 

It should implement a render method that traverses the elements in the stage to draw.

render () {
    let xelements = this.stage.getAll()
    for (let i = 0; i < xelements.length; i += 1) {
      xelements[i].refresh(this.ctx)
    }
  }
 

XRender

The last step is to create XRender and associate them. It's simple.

import XElement from './xElements/XElement'
import Stage from './Stage'
import Painter from './Painter'
 
class XRender {
  stage: Stage
  painter: Painter
  constructor (dom: string | HTMLElement) {
    let stage = new Stage()
    this.stage = stage
    this.painter = new Painter(dom, stage)
  }
  add (...xelements: XElement[]) {
    this.stage.add(...xelements)
    this.render()
  }
  render () {
    this.painter.render()
  }
}
 

Now remove the code of the previous test Circle. After saving, you can see that a Circle is still drawn, which indicates success!

Let's try adding a few more circles and pass in different parameters.

let xr = xrender.init('#app')
    let circle = new xrender.Circle({
      shape: {
        cx: 40,
        cy: 40,
        r: 20
      }
    })
    let circle1 = new xrender.Circle({
      shape: {
        cx: 60,
        cy: 60,
        r: 20
      },
      style: {
        fill: '#00f'
      }
    })
    let circle2 = new xrender.Circle({
      shape: {
        cx: 100,
        cy: 100,
        r: 40
      },
      style: {
        fill: '#0ff',
        stroke: '#f00'
      }
    })
    xr.add(circle, circle1, circle2)
 

You can see three circles appear on the screen. Next, let's try to expand a rectangle.

interface RectShape extends XElementShape {
  /**
   * Upper left corner x
   */
  x: number
  /**
   * Upper left corner y
   */
  y: number
  width: number
  height: number
}
interface RectOptions extends XElementOptions {
  shape: RectShape
}
 
class Rect extends XElement {
  name ='rect'
  shape: RectShape = {
    x: 0,
    y: 0,
    width: 0,
    height: 0
  }
  constructor (opt: RectOptions) {
    super(opt)
  }
  render (ctx: CanvasRenderingContext2D) {
    let shape = this.shape
    ctx.rect(shape.x, shape.y, shape.width, shape.height)
  }
}
 

Then in app Add code to Vue:

let rect = new xrender.Rect({
      shape: {
        x: 120,
        y: 120,
        width: 40,
        height: 40
      },
      style: {
        fill: 'transparent'
      }
    })
    xr.add(rect)
 

You can see that the rectangle appears.

Summary
Although there are still many problems, such as imperfect style rules, such as unnecessary redrawing when calling add multiple times; It seems a little unnecessary to realize the function of adding circles and rectangles, which is so complex. However, we have built the basic framework. Next, we believe it can be gradually improved and finally achieve the desired effect.

V2 Preview
In the next version, in addition to solving the two problems in the summary, the function of graph layering will also be realized, that is, specifying the stacking order of graphs.
Transfer from https://blog.csdn.net/weixin_35538148/article/details/112509719?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&spm=1001.2101.3001.4242

Topics: Front-end canvas