Development case: Use canvas to implement line chart of chart series

1. Functional structure

When implementing a public component, first analyze the general implementation structure and development ideas, so that we can avoid detours and make the component easier to expand and more maintainable. Then I will break down the functions one by one so that everyone can learn more detailed content. The following is a brief explanation of the functional structure of the line chart component:

The above is the basic functional structure framework, which includes some relatively simple basic functions. In the future, click triggering, animation and other functions will also be planned. In this issue, we will first implement the above basic functions, and then slowly expand them in the future.

2. Public attributes

1. A component will definitely have some public properties as dynamic parameters to facilitate information transfer between components. Let’s explain the functions of the five public properties respectively: the width (cWidth) and height (cHeight) of the canvas. This is the most basic of. But here I control whether it must be passed or not, and the default value is 100%.

2. The internal white space of the canvas (cSpace). It is mainly used to control the distance between the content area and the canvas frame to prevent the painting content from being cut off.

3. Font size (fontSize). Mainly to control the font size of the entire painting content, globally, to avoid the need to pass the font size for every small function.

4. Font color (color). Functionally consistent with font size.

5. Chart data. Array used to store chart content, where name and value are required.

The following is the specific code:

 // 图表数据的特征接口
interface interface_data {
  name: string | number;
  value: string | number;
  [key: string]: any;
}


// 图表的特征接口
interface interface_option {
  cWidth?: string | number,
  cHeight?: string | number,
  fontSize?: string | number,
  color?: string,
  cSpace?: number,
  data?: interface_data[]
}


// option 默认值
const def_option: interface_option = {
  cWidth: '100%',
  cHeight: '100%',
  fontSize: 10,
  color: '#333',
  cSpace: 20,
  data: []
}


@Component
export struct McLineChart {
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
  @State options: interface_option = {}
  aboutToAppear() {
    this.options = Object.assign({}, def_option, this.options)
  }
  build() {
    Canvas(this.context)
      .width(this.options.cWidth)
      .height(this.options.cHeight)
      .onReady(() => {
        
      })
  }
}

3. Painting coordinate axes

In the content area of ​​the drawing chart, the first thing is to draw the coordinate axis. The coordinate axis is divided into the If the width is the maximum value, then the starting point coordinates of the Y-axis and the X-axis will deviate, which will cause the entire painting to be misaligned. The picture below is the effect of the complete coordinate axis.

1. Draw Y axis

The Y-axis as a whole is composed of four parts: axis, dividing line, tick mark, and text label. The four parts have a sequential relationship and contain certain algorithm logic. Let’s briefly explain it with a concept diagram.

First, use a 500*500 rectangle as our canvas this time. We can see in the picture that the Y-axis as a whole includes four parts: text label, Y-axis line, dividing line, and tick mark. Canvas painting is basically positioned by coordinates. The starting point and end coordinates of the four parts of the Y-axis are all related to each other. It even needs to calculate the four attributes of internal spacing, division spacing, y-axis height, and maximum text width. Inside. The above are the concepts and ideas. Next, we will explain the code one by one:

1. Calculate the longest width of the text (maxNameW). We can see from the figure that whether it is the y-axis, the tick mark or the starting point coordinates of the dividing line, the content spacing, text label, text label and dividing line interval need to be added together. Calculated, and in order to maintain alignment, we need to calculate the maximum width of the text. The text on the y-axis is generally the value corresponding to the data (data), so we need to get the maximum value in the incoming data (data). Then the maximum value is divided into five equal parts. The following is the code to calculate and obtain the maximum text width. I will also write part of the logic in the code:

build() {
    Canvas(this.context)
      .width(this.options.cWidth)
      .height(this.options.cHeight)
      .backgroundColor(this.options.backgroundColor)
      .onReady(() => {
        const values: number[] = this.options.data.map((item) => Number(item.value || 0))
        const maxValue = Math.max(...values)
        let maxNameW = 0
        let cSpiltNum = 5 // 分割等分
        let cSpiltVal = maxValue / cSpiltNum // 计算分割间距
        for(var i = 0; i <= this.options.data.length; i++){
          // 用最大值除于分割等分得到每一个文本的间隔值,而每一次遍历用间隔值乘于i就能得到每个刻度对应的数值了,计算得到得知需要保留整数且转成字符串
          const text = (cSpiltVal * i).toFixed(0)
          const textWidth = this.context.measureText(text).width; // 获取文字的长度
          maxNameW = textWidth > maxNameW ? textWidth : maxNameW // 每次进行最大值的匹配
        }
      })
}

2. Draw the text label. We can see from the figure that the x-coordinate of the text label is only related to the internal spacing, and we have obtained the division spacing of each scale from the above code, so that we can get the y-axis of each text.

.onReady(() => {
   ....
   for(var i = 0; i <= this.options.data.length; i++){
     ...
     // 绘画文本标签
     this.context.fillText(text, this.options.cSpace, cSpiltVal * (this.options.data.length - i) + this.options.cSpace , 0);
   }
})

3. Draw the tick marks. We can get from the concept map that the x-coordinate algorithm of the starting point of the tick mark is: internal spacing (cSpace) plus the longest text width (maxNameW) plus the distance between the text and the tick mark. The y-coordinate of the starting point is the same as the text, by dividing the spacing and The relationship between subscripts gets the y coordinate of each scale; the end point x coordinate is the length of the tick mark, and the end point y coordinate is the same as the starting point y coordinate. I set the default length to 5, so that we can get our tick marks. code show as below:

.onReady(() => {
  ....
  const length = this.options.data.length
  for(var i = 0; i <= length; i++){
    ...
  }
  // 上面是获取最长文本宽度的代码
  // 画线的方法
  function drawLine(x, y, X, Y){
    this.context.beginPath();
    this.context.moveTo(x, y);
    this.context.lineTo(X, Y);
    this.context.stroke();
    this.context.closePath();
  }
  for(var i = 0; i <= length; i++){
    const item = this.options.data[i]
    // 绘画文本标签
    ctx.fillText(text, this.options.cSpace,  cSpiltVal * (this.data.length - i) + this.options.cSpace, 0);
    // 内部间距+文本长度
    const scaleX = this.options.cSpace + maxNameW
    // 通过数据最大值算出等分间隔,从而计算出每一个的终点坐标
    const scaleY = cSpiltVal * (length - i) + this.options.cSpace
    // 这里的5就是我设置文本跟刻度线的间隔与刻度线的长度
    drawLine(scaleX, scaleY, scaleX + 5 + 5, scaleY);
  }
})

4. Draw the y-axis. Continuing to analyze the overview diagram, we can get from the diagram: The algorithm of the x coordinate of the starting point of the y axis is: internal spacing (cSpace) plus the longest text width (maxNameW) plus the distance between the text and the tick mark and the length of the tick mark, the starting point y The coordinate is the internal upper spacing; the end point x coordinate is the same as the starting point x coordinate, and the end point y coordinate algorithm is: the height of the canvas minus the internal spacing between the upper and lower sides. Through the above calculation relationship, the y-axis can be drawn. code show as below:

.onReady(() => {
   
     ...  // 上面是绘画其他组成部分代码   const startX = this.options.cSpace + maxNameW + 5 + 5   const startY = this.options.cSpace   const endX = startX   const endY = this.context.height - (this.options.cSpace * 2)   drawLine(startX, startY, endX, endY); // 绘画y轴})

5. Draw dividing lines. In fact, it can be seen from the figure that the dividing line is similar to the scale mark. The starting point x coordinate algorithm is: add the scale line length to the starting point x coordinate of the scale mark; the starting point y axis is the same as the scale mark. The x-coordinate algorithm of the end point is: the width of the canvas minus the x-coordinate of the starting point; the y-coordinate of the end point is the same as the y-coordinate of the starting point. The specific code is as follows:

.onReady(() => {
  ....
  // 上面是获取最长文本宽度的代码
  for(var i = 0; i <= length; i++){
    const item = this.options.data[i]
    // 绘画文本标签跟刻度
    ...
    // 绘画分割线
    const splitX = scaleX + 5 + 5
    const splitY = scaleY
    drawLine(splitX, splitY, this.context.width - splitX - this.options.cSpace, splitY);
  }
})

2. Draw the X axis

After drawing the Y-axis, we then draw the X-axis. The drawing logic of the X-axis and the Y-axis are the same, but in different directions. The specific algorithms will not be explained in detail one by one. You can refer to the concept diagram.

What is inconsistent with the Y-axis of the painting is:

1. The longest objects are different. The longest Y-axis is the text width; and the longest X-axis needs to be obtained is the text height.

2. The number of interval divisions is different. The Y-axis is the custom division number; and the X-axis division line is the length of the actual data.

3. The algorithm of segmentation distance length is different. The Y-axis algorithm uses the maximum value of the data at a custom division number; while the X-axis algorithm uses the canvas width to subtract (the internal gaps on the left and right sides and the Y-axis width (the longest width of the text plus the tick width)), and then removes The length of the data, getting the length of each interval.

In addition to the above three points that need to be noted, the other thing is to change the calculation position. The overall code for the X-axis is as follows:

.onReady(() => {
  const cSpace = this.options.cSpace
  // 上面是绘制y轴的代码
  ....
  // 绘制x轴
  // 获取每个分割线的间距:this.context.width - 20为x轴的长度
  let xSplitSpacing = parseInt(String((this.context.width - cSpace * 2 - maxNameW) / this.options.data.length))
  let x = 0;
  for(var i = 0; i <= this.options.data.length; i++){
    // 绘画分割线
    x = xSplitSpacing * (i + 1) // 计算每个数值的x坐标值
    this.drawLine(x + cSpace + maxNameW, this.context.height - cSpace, x + cSpace + maxNameW, cSpace);
    // 绘制刻度
    this.drawLine(x + cSpace + maxNameW, this.context.height - cSpace, x + cSpace + maxNameW, this.context.height - cSpace);
    // 绘制文字刻度标签
    const text = this.options.data[i].name
    const textWidth = this.context.measureText(text).width; // 获取文字的长度
    // 这里文本的x坐标需要减去本身文本宽度的一半,这样才能居中显示, y坐标这是画布高度减去内部间距即可
    this.context.fillText(text, x + cSpace + maxNameW - textWidth / 2, this.context.height - cSpace, 0);
  }
this.context.save();
  this.context.rotate(-Math.PI/2);
  this.context.restore();
})

4. Painting the polyline area

After drawing the coordinate axis, you can draw the content of the polyline area. It is also the focus of the entire canvas. The polyline area is divided into three parts: polyline drawing, punctuation drawing, and text drawing.

1. Draw polylines

From the picture above, we can see that the polyline directly converts the actual data values ​​into x and y coordinates, and then connects them through lines. The x-coordinate algorithm of each turning point is the same as the scale or text of the x-axis, and the y-coordinate is the actual value converted into the height we need through a certain algorithm. We have already obtained the x coordinate, we just need to conquer our y coordinate. You can observe the relationship between the canvas and the actual data through the diagram:

First of all, the height of the Y-axis represents the maximum value of the actual data. This is the result we get when we draw the Y-axis. Then we can calculate the scaling factor (scale) of the Y-axis height and the actual data, and each of the polyline The y coordinate also corresponds to an actual value. You need to convert the actual value into the height of the canvas. Then multiply the actual value with the scale you just obtained to get the converted height.

Although we have obtained the scaled height of each turning point, if we want to draw the y coordinate that corresponds one-to-one with the Y-axis coordinate, we need to subtract the lower inner height plus the x-axis height from the height of the canvas, and then subtract the scaled actual height. What is calculated in this way is the y coordinate value we want. We probably already know the algorithm relationship. The following is the final code:

.onReady(() => {
  ...
  // 上面是绘制x轴跟y轴的代码
  // 绘画折线
  const ySacle = (this.context.height - cSpace *2) / maxValue // 计算出y轴与实际最大值的缩放倍数
  //连线
  this.context.beginPath();
  for(var i=0; i< this.options.data.length; i++){
    const dotVal = String(this.options.data[i].value);
    const x = xSplitSpacing * (i + 1) + cSpace + maxNameW // 计算每个数值的x坐标值
    const y = this.context.height - cSpace - parseInt(dotVal * ySacle); // 画布的高度减去下边内部高度加x轴高度,再减去缩放后的实际高度
    if(i==0){
      // 第一个作为起点
      this.context.moveTo( x, y );
    }else{
      this.context.lineTo( x, y );
    }
  }
  ctx.stroke();
})

2. Painting punctuation and text labels

After drawing the polyline, we can basically get a lot of things, such as the x and y coordinates of each turning point on the polyline. This is very convenient for us to draw punctuation and text labels:

.onReady(() => {
  ...
  // 上面是绘制x轴跟y轴的代码
  // 绘画折线
  const ySacle = (this.context.height - cSpace *2) / maxValue // 计算出y轴与实际最大值的缩放倍数
  this.context.beginPath();
  for(var i=0; i< this.options.data.length; i++){
    // 绘画折线代码
    ...
    // 绘制标点
    drawArc(x, y);
    // 绘制文本标签
    const textWidth = this.context.measureText(dotVal).width; // 获取文字的长度
    const textHeight = this.context.measureText(dotVal).height; // 获取文字的长度
    this.context.fillText(dotVal, x - textWidth / 2, y - textHeight / 2); // 文字
  }


  function drawArc( x, y ){
    this.context.beginPath();
    this.context.arc( x, y, 3, 0, Math.PI*2 );
    this.context.fill();
    this.context.closePath();
  }
  this.context.stroke();
})

The final effect is as follows:

5. Summary

The above is this technical analysis, I hope it can inspire everyone, and I also wish all developers can develop the ideal effect. In the future, we will encapsulate the chart-related series of components into a component library and release them to the market, so that they can be directly unboxed. Ready to use. Please stay tuned, there will be many technical sharings in the future, don’t miss it!

Guess you like

Origin blog.csdn.net/HarmonyOSDev/article/details/134978188