preface

Finally to the weekend, this time to share with you a visualization chart is relatively simple chart πŸ“ˆ but at the same time we have to learn that is the line chart. What can you learn from reading this article

  1. Js implements the equation of a line
  2. The expression of a line chart
  3. A few tricks on Canvas

Straight line chart

Let’s go to the famous official website of Echarts and have a look. What does his line chart look like? As shown in figure:

The following 2D graphic elements can be obtained from the figure:

  1. Straight line (two ends are round)
  2. Straight line (two ends that are straight)
  3. The text

It seems that there is nothing wrong with careful analysis. It is just drawing straight lines and adding text. OK, ask yourself how to draw a line on the canvas? Is there a ctx. lineTo method that draws a line with no endpoints? We encapsulate on this basis, and the graph of the end point of the line is controllable. At the same time, is it possible to draw such a graph when the text is in the position of the line? Let’s move on to the actual operation.

Canvas Creation

The first step is definitely to create the canvas, so there’s nothing to cover here. Here I’m going to create a new canvas in HTML, and I’m going to create a new class called LineChart and I’m going to go straight to the code:

    class lineChart {
        constructor(data, type) {
          this.get2d()
        }

        get2d() {
          const canvas = document.getElementById('canvas')
          this.ctx = canvas.getContext('2d')
        }
      }

There’s nothing to talk about, and then I’m setting the background color for the Canvas Canvas. The code is as follows:

    <style>
      * {
        padding: 0;
        margin: 0;
      }
      canvas {
        background: aquamarine;
      }
    </style>

Canvas drawing operation review

In fact, line diagram is essentially a drawing of a straight line, but in the original ability to draw a straight line, to do some enhancement. Let me use a triangle-drawing example to familiarize you with the line-drawing operation.

Let’s start with the API:

lineTo(x, y)

Draws a line from the current position to the specified x and y positions.

A line is usually made up of two points, and the method takes two parameters: x and y, which represent the point at which the line ends in the coordinate system. The start point is related to the previous drawn path, the end point of the previous path is the start point of the next path, etc… The starting point can also be changed using the moveTo() function.

What moveTo is is just moving the stroke across the canvas, which is the first point you start drawing, or you can imagine working on paper, where the tip of a pen or pencil moves from one point to another.

moveTo(*x*, *y*)

Moves the stroke to the specified coordinates x and y.

The end of the introduction, start the actual combat link:

drawtriangle() {
  this.ctx.moveTo(25, 25)
  this.ctx.lineTo(105, 25)
  this.ctx.lineTo(25, 105)
}

Let’s move a point, and then draw a line, and then draw a line. If you think it’s over, you’re wrong

One of the most important things you need to do is to stroke the canvas or fill it, and I forget that when I start learning it.

Here I will sort out the entire drawing process of Canvas

  1. First, you need to create the starting point of the path.
  2. Then you use the draw command to draw the path.
  3. And then you close the path.
  4. Once the path is generated, you can render the graph by stroking or filling in the path area.

So all we’re doing is preparing the path, so we need a stroke or a fill to render the graph, so let’s look at those two APIs.

// Draw the outline of the graph with a line. Ctx.stroke () // Generates a solid graph by filling the content area of the path. ctx.fill()

Let’s add the padding:

Let’s look at the Stroke effect:

And you see why is it not closed? , the code looks like this:

this.moveTo(25, 25)
this.lineTo(105, 25)
this.lineTo(25, 105)
this.stroke()

And that tells us an important question which is what?

Stroke is not closed by default, so we need to close it manually


Padding by default will help us close the shape and fill it

Now that we have found the problem, we need to solve the problem, so how does Canvas close the path?

closePath:

After closing the path, the drawing command points back to the context.

The code is as follows:

this.moveTo(25, 25)
this.lineTo(105, 25)
this.lineTo(25, 105)
this.closePath()
this.stroke()

At this time, the renderings have come out:

Have closePath? Didn’t you start the path? The answer is yes:

// Create a new path. After the path is created, the graph drawing command is directed to the path to create the path. this.beginPath()

And they say, well, what does this do?

The first step in generating the path first is called beginPath(). In essence, a path is made up of a number of subpaths, all of which are in a list, and all of the subpaths (lines, arcs, etc.) form a graph. Each time this method is called, the list is cleared and reset, and we can redraw the new graph.

Note: The current path is empty, after the beginPath() is called, or when the canvas is first built. The first path construction command is usually treated as moveTo (), regardless of what it is. For this reason, you almost always specify your starting position specifically after setting the path.

ClosePath is not necessary. If the graph is closed, there is no need to call it. This is the end of the review of basic drawing operations of Canvas.

Encapsulate draw line method

Once again, I used a point2D point to represent the position of each point in Canvas and wrote some methods, which I have discussed in detail in previous articles. I won’t discuss it here: the article that realizes the movement (point, line and plane) of any regular polygon on Canvas with a length of 3,000 words. I’ll just put the code in here:

export class Point2d { constructor(x, y) { this.x = x || 0 this.y = y || 0 this.id = ++current } clone() { return new Point2d(this.x, this.y) } equal(v) { return this.x === v.x && this.y === v.y } add2Map() { pointMap.push(this) return this } add(v) { this.x += v.x this.y += v.y return this } abs() { return [Math.abs(this.x), Math.abs(this.y)] } sub(v) { this.x -= v.x this.y -= v.y return this } equal(v) { return this.x === v.x && this.y === v.y } rotate(center, angle) { const c = Math.cos(angle), s = Math.sin(angle) const x = this.x - center.x const y = this.y - center.y this.x = x * c - y * s + center.x this.y = x  * s + y * c + center.y return this } distance(p) { const [x, y] = this.clone().sub(p).abs() return x * x + y * y } distanceSq(p) { const [x, y] = this.clone().sub(p).abs() return Math.sqrt(x * x + y * y) } static random(width, height) { return new Point2d(Math.random() * width, Math.random() * height) } cross(v) { return this.x * v.y - this.y * v.x } }

They correspond to static methods, cross products, distance between two points, and so on.

First, we draw a basic line on the canvas. First, we use RANDOM to regenerate two points on the canvas, and then draw a random line. The code is as follows:

New lineChart(). DrawLine (point2d. random(500, 500), point2d. random(500, 500)) {const {x: = 1; startX, y: startY } = start const { x: endX, y: endY } = end this.beginPath() this.moveTo(startX, startY) this.lineTo(endX, endY) this.stroke() }

Js implements the equation of a line

There is nothing to show here. Let's analyze the official line chart of Echarts. There are two circles on both sides of the line. In fact, there is a math knowledge involved here. Dear friends, Fly is a math teacher to explain to you again, mainly to help some friends review. So we know where the line starts and ends, and in mathematics we can determine the equation of a line, so we can figure out the (x,y) coordinate of any point on the line. So we can determine the centers of the two ends of the line. Right? And the radius is the distance between the center of the circle and the beginning and the end.

Step 1: Implement the equation of the line

Let’s look at several ways to express the equation of a line:

  1. Ax+By+C=0(A, B)
  2. Point-slope form: y-y0=k(x-x0) [for lines that are not perpendicular to the X-axis] represents a line with a slope of k and passing through (x0,y0)
  3. X /a+y/b=1
  4. (x1β‰ x2,y1 β‰ y2); (x2 = y2);

    Two point

Here it is obvious that we fit into the fourth category: we know the starting and ending points of a line and we can find the equation of a line. I present the following code:

export function computeLine(p0, p1, If (x1 === x2) {return new point2D (x1, 1) {let x1 = p0.x let y1 = p0.y let y2 = p1. If (y1 === y2) {return new point2D (t, y) {return new point2D (t, y); y1) } const y = ((t - x1) / (x2 - x1)) * (y2 - y1) + y1 return new Point2d(t, y) }

P0, P1, and the corresponding two line points T are the parameters, and the corresponding line x, we solve for y, and we return the new point. By default, we find the center of the circle by subtracting or adding a fixed value from the starting and ending x positions, respectively. Take a look directly at the image:

The distance between 1 and 2 is the radius, so let’s just figure out points 1 and 4. The canvas has an arc API:

arc(x, y, radius, startAngle, endAngle, anticlockwise)

Draw an arc (circle) with radius centered at (x,y), starting at startAngle and ending at endAngle, generated in the direction given by Anticlockwise (which is clockwise by default).

Note:arc()The units of Angle in the function are radians, not angles. JS expressions for Angle and radian:

Radians =(Math.pi /180)* Angle.

The circle must be from 0-360 degrees. The code is as follows:

drawCircle(center, radius = 4) { const { x, y } = center this.ctx.beginPath() this.ctx.arc(x, y, radius, 0, Math.pi * 2, true) // Draw this.ctx.fill()}

Now that we’ve done our homework, let’s start to implement a straight line with a circle. And the way to draw it is

  1. Let me draw the opening circle
  2. Draw a straight line
  3. Draw the end of the round

Drawing a starting circle and drawing an ending circle can be encapsulated as a method: the main difference between them is actually the difference of starting point. The code is as follows:

drawLineCircle(start, end, type) { const flag = type === 'left' const { x: startX, y: startY } = start const { x: endX, y: endY } = end const center = this.getOnePointOnLine( start.clone(), end.clone(), flag ? Startx-this. distance: endX + this.distance) const radius = (flag? start : end).clone().distanceSq(center) this.drawCircle(center, radius) }

So we can draw the circle. Let’s take a look at the effect picture:

This concludes the first part of the line chart, followed by the second part:

Let me draw the XY axes

The axes are essentially two straight lines, so the first step is to determine the origin of the coordinates, and then to draw the vertical and horizontal lines from the original coordinates. We set the left padding of the origin from the canvas and the bottom padding, so that we can subtract the bottom padding from the height of the canvas to get y of the origin, and then subtract the left padding from the width of the canvas to get x, so it’s not a big deal to draw the axes with the original coordinates. The code is as follows:

This. origin = new Point2d(this.origin = new Point2d) this.origin = new Point2d(this.origin = new Point2d) this.origin = new Point2d(this.origin = new Point2d) this.origin = new Point2d(this.origin = new Point2d) this.origin = new Point2d(this.origin = new Point2d) this.paddingLeft, this.height - this.paddingBottom ) this.drawCircle(this.origin, 1, 'red') this.addXAxis () this.addYAxis () {const end = this.origin.clone ().add(new) Point2d(this.width - this.paddingLeft * 2, 0)) this.drawLine(this.origin, Const end = this.origin.clone (). Sub (new point2D (0, 0, 0, 0, 0, 0, 0, 0, 0); this.height - this.paddingBottom * 2)) this.drawLine(this.origin, end) }

The important thing to note here is that first of all, the entire canvas is on the top left of the whole screen, but we show that the origin is on the bottom left, and then when we draw the Y axis we subtract from the origin, which is a subtraction of vector points.

The effect picture is as follows:

But unlike the one of Echarts, his X-axis is both cable and text, so we started to transform the X-axis. It’s dividing the X-axis into segments,

And then you have a set of points, where the y’s are the same, and the x’s are different. The code is as follows:

drawLineWithDiscrete(start, end, N = 5) {// Push (start) const points = [] const startX = start.x const endX = end.x points.push(start) const segmentValue = (endX - startX) / n for (let i = 1; i <= n - 1; i++) { points.push(new Point2d(startX + i * segmentValue, } point. push(end); forEach((point) => {this.drawLine(point, point.clone(). Add (new point2D (0, 0)); 5)))})}

The thing to notice here is the number of cycles, because there are starting points and ending points. Take a look at the effect picture:

Now there’s text. Canvas draws text API

FillText (text,x,y,[, maxWidth]) fillText (text,x,y,[, maxWidth])

So to be clear, to calculate the coordinates of the text points, first define the X – and Y-axis data in the project initialization. The code is as follows:

this.axisData = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] this.yxisData = ['0', '50', '100', '150', '200', '250', '300')

We put the text in the middle of the line segment and you just compute the length of each segment and then you add half of the length of each segment at the end. The code is as follows:

Const segmentValue = (endX-startX)/n for (let I = 0; i <= n - 1; i++) { const textpoint = new Point2d( startX + i * segmentValue + segmentValue / 2, Start. Y + 20) // Push ({point: textpoint, text: textpoint) // Push ({point: textpoint, text: textpoint) This.axisData [I],})} this.clearFillColor() textPoints. Foreach ((info) => {const {text, this.axisData[I],})}) point } = info this.ctx.fillText(text, point.x, point.y) })

The effect picture is as follows:

But looking at the figure as if the text is not in the middle of the position, panhu thought about πŸ€”, in fact, because the text also has length, so each text coordinate to subtract half of the length of the text value on the right. The third parameter to this.ctx.fillText is important, so we can limit the length of the text so we can handle it.

This.ctx. fillText(text, point.x, point.y, fillText()); Const TextPoint = new point2D (startX + I * segmentValue + segmentValue / 2-10, start.y + 20)

See the renderings directly:

And this is perfect.

Now that we’re done with the X-axis, we’re done with the Y-axis, and the Y-axis is actually a relatively simple line that corresponds to each piece of data.

For the Y axis, you need to calculate the length of each line segment and then draw a straight line. The special thing to pay attention to here is the placement of the text. You need to fine-tune it at each end point. Center text and line. The code is as follows:

addyAxis() {
  const end = this.origin
    .clone()
    .sub(new Point2d(0, this.height - this.paddingBottom * 2))
  const points = []
  const length = this.origin.y - end.y
  const segmentValue = length / this.yxisData.length
  for (let i = 0; i < this.yxisData.length; i++) {
    const point = new Point2d(end.x, this.origin.y - i * segmentValue)
    points.push({
      point,
      text: this.yxisData[i],
    })
  }
  points.forEach((info) => {
    const { text, point } = info
    const end = point
      .clone()
      .add(new Point2d(this.width - this.paddingLeft * 2, 0))
    this.setStrokeColor('#E0E6F1')
    this.drawLine(point, end)
    this.clearStrokeColor()
    this.ctx.fillText(text, point.clone().x - 30, point.y + 4, 20)
  })
}

Since the process is very similar to the X-axis, it should be noted that after the stroke setting, it should be returned to default, otherwise the previous color will be referenced.

As shown in figure:

The last thing we need to do is create a line graph, and we’ve already encapsulated a line with a circle on it, so we just need to find all the points to plot it. First of all, the x-coordinate of each point is OK, and that corresponds to the midpoint of each text, mainly the y-coordinate: remember how we calculated the y-coordinate before was the length divided by the number of segments. Thus lead to a problem, the result could be a decimal, 223 this because we are the actual data may be lead to draw graphics point error is too big, so in order to reduce the error, I change a computing mode, is equal, this point can be expressed in range, the error can be a little bit, actually in the actual project, The tolerance problem is a certain problem in calculation, and JS itself has such problems as 0.1+0.2. Therefore, within the tolerance range, we can consider these two points to be equivalent. The code is as follows:

const length = this.origin.y - end.y
const division = length / 300
const point = new Point2d(end.x, this.origin.y - i * division * 50)

And then I introduce real data here:

this.realData = [150, 230, 224, 218, 135, 147, 260]
this.xPoints = []
this.yPoints = []

The respective corresponding data is real, xPoints is what text the midpoint coordinate code is as follows:

This.clearFillColor () textpoints.foreach ((info) => {const {text, this.clearFillColor() textpoints.foreach ((info) => {const {text, point } = info this.xPoints.push(point.x) this.ctx.fillText(text, point.x, point.y, 20) })

Ypoints is actually relatively simple, real data * each of the distance is good.

const division = length / 300 for (let i = 0; i < this.yxisData.length; I ++) {const point = new point2D (end.x, this.origin.y - I * division * 50); Const realData = this.realData[I] this.ypoints.push (this.origin.y - realData * division) points.push({this.origin.y - realData * division) points.push({this.realData[I] this.ypoints.push (this.origin.y - realData * division) points.push({this.origin.y - realData * division) point, text: this.yxisData[i], }) }

When the data is ready, we call the method to plot it:

let start = new Point2d(this.xPoints[0], This.ypoints [0]) this.setStrokeColor('#5370C6') this.xPoints. Slice (1).foreach ((x, 0)) this.setStrokeColor('#5370C6') index) => { const end = new Point2d(x, this.yPoints[index + 1]) this.drawLineWithCircle(start, end) start = end })

The important thing to note in this code is to find a starting point by default, and then keep changing the starting point, and then pay attention to the subscript position.

As shown in figure:

Current problems:

  1. Duplication of dots exists
  2. The radius of the dots is not the same, which means that we calculated the distance from the center of the circle to the line in this way is wrong, because the slope of each line is different. So there’s a problem with that.

So one way to think about it is, why are circles and lines bound together? If I draw it alone, I don’t have that problem. No sooner said than done,

let start = new Point2d(this.xPoints[0], This.ypoints [0]) this.setStrokeColor('#5370C6') this.xPoints. Slice (1).foreach ((x, 0)) this.setStrokeColor('#5370C6') this.xPoints. Slice (1).foreach ((x, 0)) this.setStrokeColor('#5370C6')) Index) => {const end = new point2D (x, this.yPoints[index + 1]) end) start = end })

Notice that there will be a missing opening circle, so we can just fill it in at the beginning, because I have set the radius of the circle uniformly.

As shown in figure:

So far, the line graph is complete. To make it more perfect, I will add hints and dotted lines.

Show tooltip

Here I see that most charts have a dotted line and a hint when the mouse moves, how else can I clear it up and look at the data. Again, let’s initialize a div and make it hidden.

#tooltip {
  position: absolute;
  z-index: 2;
  background: white;
  padding: 10px;
  border-radius: 2px;
  visibility: hidden;
}

<div id="tooltip"></div>

Add listener events to Canvas:

canvas.addEventListener('mousemove', OnMousemove (E) {const x = e.offsetx const y = e.offsety}

And what we’re going to do is actually very simple first of all we’re going to compare the mouse dot to the actual dot and I’m going to display it within a certain range, sort of a snap, and from the user’s point of view it’s impossible to completely move there and then display it.

The code is as follows:

onMouseMove(e) { const x = e.offsetX const find = this.xPoints.findIndex( (item) => Math.abs(x - item) <= this.tolerance ) if (find >-1) {this.tooltip. TextContent = 'Data: ${this.axisData[find]}_ ${this.yxisData[find]}` this.tooltip.style.visibility = 'visible' this.tooltip.style.left = e.clientX + 2 + 'px' this.tooltip.style.top = e.clientY + 2 + 'px' } else { this.tooltip.style.visibility = 'hidden' } }

You can actually just compare the position of x, and you can customize the tolerance.

Draw a perpendicular dotted line

I have seen a lot of charts and they all have dotted vertical lines. Here comes a question about how to draw dotted lines on Canvas. I am using Canvas to move rectangles (point, line and surface) (1), which is introduced in this article. The code is as follows:

drawDashLine(start, end) { if (! start || ! end) { return } this.ctx.setLineDash([5, 10]) this.beginPath() this.moveTo(start.x, start.y) this.lineTo(end.x, end.y) ctx.stroke() }

Once again, we have changed onMousemove:

onMouseMove(e) { const x = e.offsetX const find = this.xPoints.findIndex( (item) => Math.abs(x - item) <= this.tolerance ) if (find >-1) {this.tooltip. TextContent = 'Data: ${this.axisData[find]}_ ${this.yxisData[find]}` this.tooltip.style.visibility = 'visible' this.tooltip.style.left = E.lientx + 2 + 'px' this.tooltip. Style. top = e.lienty + 2 + 'px' const start = new point2D (this.xpoints [find], this.origin.y) const end = new Point2d(this.xPoints[find], 0) this.drawDashLine(start, end) } else { this.tooltip.style.visibility = 'hidden' } }

Added the following code, but this is a problem, is that we keep moving the mouse, so the last drawn dotted line will not cancel. Something like this can happen:

So I did a data sweep and also cleared the canvas and redrew it:

clearData() {
  this.ctx.clearRect(0, 0, 600, 600)
  this.xPoints = []
  this.yPoints = []
}

The overall code is as follows:

const start = new Point2d(this.xPoints[find], this.origin.y) const end = new Point2d(this.xPoints[find], This. clearData() this.drawDashLine(start, drawDashLine) This.ctx. setLineDash([]) this.addXAxis () this.addYAxis () this.setStrokeColor('#5370C6') this.addYAxis () this.setStrokeColor('#5370C6') this.generateLineChart()

Use of restore and save

One more trick ** is that if a drawing on the canvas only works on one drawing at a time: there are save and restore methods

Use the save() method to save the current state and restore() to the original state

So we can rewrite the way we draw the dotted line, so we start with svae, and then end with restore, kind of like a stack, go in first, and then finish with the dotted line, and pop out. Each item has its own unique drawing state and does not affect the other items.

drawDashLine(start, end) { if (! start || ! end) { return } this.ctx.save() this.ctx.setLineDash([5, 10]) this.beginPath() this.moveTo(start.x, start.y) this.lineTo(end.x, end.y) this.stroke() this.ctx.restore() }

This is the end of the whole line diagram I want to explain to you, let’s have a look at the effect:

The last

This article can be regarded as the first one of Canvas visualization charts. I will continue to share, pie charts, tree charts, K charts and other visual charts in the future

While writing the article, I am also constantly thinking about how to express it better. If you’re interested in visualization, check out the thumb up favorites at πŸ‘! You can follow me

Data Visualization column, and share one article per week, either 2D or three.js. I will create every article with my heart, never hydrology.

One last word: everyone join me to be an API creator, not a caller!

Download the source code

All the code for the examples in this article is on my GitHub, starβ˜†πŸ˜―! If you are interested in graphics, you can pay attention to my official account [front-end graphics] to receive visual learning materials!! See you next time πŸ‘‹