This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!

preface

Finally to the weekend, last week a 3D article to bring you introduction three.js – from 0 to 1 to achieve a 3D visual map is very happy 😺 received so many friends like, this is the affirmation of my knowledge output. Thank you again! I’m back again this week, this time to share with you the simple visualization chart πŸ“ˆ but at the same time we had to learn the β€”β€”β€”β€” line chart. What can you learn from reading this article

  1. Js implements the equation of a line
  2. Expression of line chart
  3. Some of canvas’s apis are used flexibly

Straight line chart

Let’s go to the famous Echarts website first, what does his line chart look like? As shown in figure:

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

  1. A line (with two ends of a circle)
  2. Line (with two ends of lines)
  3. The text

As if careful analysis is nothing, in fact, drawing lines and adding text. OK, ask yourself how to draw a straight line on canvas? Is there a ctx.lineto method, but it draws a line with no end points so what? We based on encapsulation, and the end of the line of the graph is controllable, but also the position of the text in the line can draw such a graph? Let’s move on to the practical part.

Canvas creation

The first step is definitely to create the canvas, so there’s nothing to talk about here. So 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')}}Copy the code

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

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

Canvas drawing operation review

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

Take a look at the API:

lineTo(x, y)

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

A line usually consists of two points. 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 drawing path, the end point of the previous path is the next starting point, etc… The starting point can also be changed with the moveTo() function.

MoveTo is moving a stroke on the canvas, the first point you start drawing, or you can imagine working on paper, moving the tip of a pen or pencil from one point to another.

moveTo(*x*, *y*)
Copy the code

Moves the stroke to the specified coordinates x and y.

After the introduction, the actual combat link began:

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

We move a point, then we draw a line, then we draw another line. If you think it’s over, you’re wrong

One of the important things that you need is the stroke or the fill of the canvas, which I forget when I’m starting out.

Here is a summary of the entire canvas drawing process

  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 figure by stroke or by filling the path area.

So all we’ve done is just prepare the path, so we need to stroke or fill to render the graphics, so let’s look at those two apis.

// Use lines to draw the outline of a graph.
ctx.stroke() 
// Generate a solid graph by filling the content area of the path.
ctx.fill()
Copy the code

Let’s add the fill:

Let’s look at the stroke effect:

You’ll see why it’s not closed? , the code looks like this:

this.moveTo(25.25)
this.lineTo(105.25)
this.lineTo(25.105)
this.stroke()
Copy the code

So what’s the big point here?

Stroke is not closed by default, we need to manually close the fill will help us close the graph by default, and fill

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

At this time, the renderings are out:

Have closePath? Is there no starting path? The answer is yes:

// Create a new path. After generation, the graph drawing command is pointed to the path to generate the path.
this.beginPath()
Copy the code

So what’s the use of this?

The first step to generate the path first is called beginPath(). Essentially, a path is made up of many subpaths, all in a list, all of which form a graph (lines, curves, and so on). And every time this method is called, the list clears and resets, and then we can draw a new graph.

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

ClosePath is not required. If the graph is already closed, you do not need to call it. The basic drawing operation of Canvas is reviewed here.

Encapsulating a straight line method

Before that, I used a point2D point to represent the position of each point in the canvas and wrote some methods, which I have talked about in detail in the previous articles. I will not expand it here: The article that the canvas with a long length of 3,000 words can realize the movement of any regular polygon (point, line, surface). I’m just going to 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
  }
}
Copy the code

Corresponding to some static methods, cross product, the distance between two points and so on.

We first draw a basic line on the canvas. We first 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))/ / draw a straight line
drawLine(start, end) {
  const { x: startX, y: startY } = start
  const { x: endX, y: endY } = end
  this.beginPath()
  this.moveTo(startX, startY)
  this.lineTo(endX, endY)
  this.stroke()
}
Copy the code

Js implements the equation of a line

I don't have anything to show you, but let's look at the echarts official line chart, a straight line with two circles on both sides, think about it? In fact, this involves a math knowledge, dear friends, Fly once again to the math teacher to explain to you, mainly to help some friends review. Here 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) coordinates of any point on the line. So we can determine the center of the circle at the end of the line, right? The radius can also be determined as the distance between the center of the circle and the starting point and the ending point.Copy the code

Step 1: Implement the equation of the line

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

  1. Ax+By+C=0(A, B =0)

  2. Point slope: y-y0=k(x-x0) [applicable to lines not perpendicular to the X-axis] denotes a line with slope k and passing through (x0,y0)

  3. X /a+y/b=1

  4. (x1,y1,y2) = (x1,y1,y2) = (x1,y1,y2) = (x1,y1,y2) = (x1,y1,y2) = (x1,y1,y2)

    Two point

So this is a clear case for the fourth one: you know where the line starts and ends and you can figure out the equation of the line. I’ll give you the following code:

export function computeLine(p0, p1, t) {
  let x1 = p0.x
  let y1 = p0.y
  let x2 = p1.x
  let y2 = p1.y
  // Indicate that the line is parallel to the Y-axis
  if (x1 === x2) {
    return new Point2d(x1, t)
  }
  // Parallel to the X axis
  if (y1 === y2) {
    return new Point2d(t, y1)
  }
  const y = ((t - x1) / (x2 - x1)) * (y2 - y1) + y1
  return new Point2d(t, y)
}
Copy the code

P0, P1, the corresponding line t is the parameter, the corresponding line x, we find y, return the new point. By default, we find the center of the circle by subtracting or adding a fixed value to the x positions of the start and end points. Take a look at the picture:

The distance between 1 and 2 is the radius, so we just need to figure out points 1 and 4.

arc(x, y, radius, startAngle, endAngle, anticlockwise)
Copy the code

Draw a radius arc centered on (x,y), starting with startAngle and ending with endAngle, in the direction given by AnticLockwise (clockwise by default).

Note:arc()The Angle in this function is in radians, not in angles. Js expression for Angle and radian:

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

The circle must be from 0 to 360 degrees, as follows:

drawCircle(center, radius = 4) {
  const { x, y } = center
  this.ctx.beginPath()
  this.ctx.arc(x, y, radius, 0.Math.PI * 2.true) / / to draw
  this.ctx.fill()
}
Copy the code

With all the preparations in place, let’s start implementing a straight line with a circle. So here’s how you draw it

  1. Let’s start with the circle
  2. Draw a straight line
  3. Draw the end of the round

Drawing the beginning circle and drawing the end circle can be encapsulated in one method: the main difference between them is the starting point, as shown in this code:

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
  )
  // The distance between two points
  const radius = (flag ? start : end).clone().distanceSq(center)
  this.drawCircle(center, radius)
}

Copy the code

So we can draw a circle. Take a look at the renderings:

Now that we have finished the first part of the line chart, we move on to the second part:

Let me draw my XY axes

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

  // Define the inner margins of the axes relative to the canvas
  this.paddingLeft = 30 // At least larger than the width of the drawn text
  this.paddingBottom = 30 // at least greater than the height of the drawn text
  this.origin = new Point2d(
    this.paddingLeft,
    this.height - this.paddingBottom
  )
  this.drawCircle(this.origin, 1.'red')
  this.addxAxis()
  this.addyAxis()

  / / draw the x axis
  addxAxis() {
    const end = this.origin
      .clone()
      .add(new Point2d(this.width - this.paddingLeft * 2.0))
    this.drawLine(this.origin, end)
  }
  
  / / y
  addyAxis() {
    const end = this.origin
      .clone()
      .sub(new Point2d(0.this.height - this.paddingBottom * 2))
    this.drawLine(this.origin, end)
  }
Copy the code

It is important to note here that first of all, the coordinate axis of the whole canvas is at the top left of the whole screen, but the origin of the coordinate we show is at the bottom left. Then, when drawing the Y axis, we subtract from the origin upwards, which is the subtraction of vector points.

The renderings are as follows:

But it’s different from echarts, whose X-axis is wired and text, so let’s start with the X-axis. I’m just dividing the X-axis into segments,

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

 drawLineWithDiscrete(start, end, n = 5) {
    // Because the x axis is the same y
    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, start.y))
    }
    points.push(end)

    // Generate a line segment
    points.forEach((point) = > {
      this.drawLine(point, point.clone().add(new Point2d(0.5)))})}Copy the code

The thing to notice here is the number of cycles, because there are starting points and ending points. Check out the renderings:

We still need the text, the CANVAS API for drawing text

FillText (text,x,y,[,maxwidth]) fills the specified text at the specified position (x,y). The maximum width of the draw is optional.Copy the code

To calculate the coordinates of the literal points, first define the X and Y axes 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')Copy the code

We put the text uniformly at the midpoint of the line segment and we just calculate the length of each segment and then we add half the length of the segment at the end. The code is as follows:

// Generate the point of the X-axis text
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
  )
  // The text of each point corresponds to the X-axis data
  textPoints.push({
    point: textpoint,
    text: this.axisData[i],
  })
}

// Generate text
this.clearFillColor()
textPoints.forEach((info) = > {
  const { text, point } = info
  this.ctx.fillText(text, point.x, point.y)
})

Copy the code

The renderings are as follows:

But look at the picture as if the text is not in the middle position, the fat cat thought πŸ€”, in fact, because the text also has length, so the coordinates of each text should be subtracted by half of the length of the text. The third argument to this.cx. FillText is important to limit the length of the text so that we can process it.

// Limit the length of the text
this.ctx.fillText(text, point.x, point.y, 20)

// Subtract half of the length from each point of the text
const textpoint = new Point2d(
  startX + i * segmentValue + segmentValue / 2 - 10,
  start.y + 20
)
Copy the code

Look directly at the renderings:

This is perfect.

Ok, so let’s do the X-axis, let’s do the Y-axis, and the Y-axis is actually a relatively simple line that corresponds to each of these numbers.

For the Y-axis, we also calculate the length of each line segment, and then draw the straight line, and pay special attention to the placement of the text here, and make some minor adjustments at each end. Center the 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)})}Copy the code

Because the process is very similar to the X-axis, a reminder that when you set the stroke, you have to restore it to the default, otherwise it will refer to the previous color.

As shown in figure:

The whole canvas just needs one last step, to generate the line chart, and we’ve already wrapped the line with the circle on it, so we just need to find all the points to draw the line chart. First of all, there’s no problem with the X coordinate of each point corresponding to the midpoint of each text, mainly the Y coordinate: remember how we calculated the Y coordinate before, by 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 definitely a problem of calculation. Js itself has such a problem as 0.1+0.2, so in other words, within the tolerance range, we can consider these two points to be equivalent code 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)
Copy the code

Then I introduce real data:

this.realData = [150.230.224.218.135.147.260]
this.xPoints = []
this.yPoints = []
Copy the code

XPoints are the midpoint coordinates of the text as follows:

// Generate text
this.clearFillColor()
textPoints.forEach((info) = > {
  const { text, point } = info
  this.xPoints.push(point.x)
  this.ctx.fillText(text, point.x, point.y, 20)})Copy the code

YPoints is actually simpler, the actual data * the distance of each copy 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)
  // Here again, we have to pay attention to the position of the axes
  const realData = this.realData[i]
  this.yPoints.push(this.origin.y - realData * division)
  points.push({
    point,
    text: this.yxisData[i],
  })
}
Copy the code

With the data ready, we call the method to draw the folding line:

let start = new Point2d(this.xPoints[0].this.yPoints[0])
// Generate a line chart
this.setStrokeColor('#5370C6')
this.xPoints.slice(1).forEach((x, index) = > {
  const end = new Point2d(x, this.yPoints[index + 1])
  this.drawLineWithCircle(start, end)
  start = end
})
Copy the code

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

As shown in figure:

Current problems:

  1. The existing dot repeats
  2. The radius of the circle is different, which means that we calculated the distance from the center of the circle to the line and that’s the wrong way to call it the radius, because each line has a different slope. So it’s a problem to figure it out.

So one way to think about it is, why are circles and lines tied together? If you draw it alone, you don’t have this problem. Just do it,

let start = new Point2d(this.xPoints[0].this.yPoints[0])
this.drawCircle(start)
// Generate a line chart
this.setStrokeColor('#5370C6')
this.xPoints.slice(1).forEach((x, index) = > {
  const end = new Point2d(x, this.yPoints[index + 1])
  / / draw circles
  this.drawCircle(end)
  / / draw a straight line
  this.drawLine(start, end)
  start = end
})
Copy the code

Notice that I’m missing a starting circle, so let’s just fill it in at the beginning, and I’ve set the radius of the circle.

As shown in figure:

So at this point, this line chart is complete, and just to make it a little bit more perfect, I’m going to add hints and dotted lines.

Show tooltip

I’m looking at most of the charts here and they show a dotted line and a hint when I move the mouse over, otherwise how would I clear the data. We’re going to initialize a div and we’re going to style it hidden.

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

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

Copy the code

Add listener events to Canvas:

canvas.addEventListener('mousemove', OnMouseMove (e) {const x = e.ffsetx const y = e.ffsety}} onMouseMove(e) {Copy the code

So what we’re going to do is we’re going to do a very simple thing first of all we’re going to compare the mouse points to the actual points and then we’re going to display, sort of like snap, it’s impossible from the user’s point of view to move all the way there.

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:The ${this.axisData[find]}_ The ${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'}}Copy the code

So we’re just comparing the x’s, and we can customize the tolerance.

Draw vertical dashed lines

I have seen a lot of charts and they all have vertical dotted lines. Here is a question about how to draw dotted lines on Canvas. I am using Canvas to realize the movement of rectangle (point, line and surface) (1). 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()
}
Copy the code

Let’s remodel onMouseMove again:

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:The ${this.axisData[find]}_ The ${this.yxisData[find]}`
    this.tooltip.style.visibility = 'visible'
    this.tooltip.style.left = e.clientX + 2 + 'px'
    this.tooltip.style.top = e.clientY + 2 + 'px'
    / / draw a dotted line
    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'}}Copy the code

Added the following code, but the problem with this is that we are constantly moving the mouse, so the last dotted line will not be cancelled. Here’s what happens:

So I did a data purge and cleared the canvas and redrew:

clearData() {
  this.ctx.clearRect(0.0.600.600)
  this.xPoints = []
  this.yPoints = []
}
Copy the code

The overall code is as follows:

const start = new Point2d(this.xPoints[find], this.origin.y) const end = new Point2d(this.xPoints[find], 0) this.cleardata () this.drawdashLine () This.cxx.setlinedash ([]) this.addxAxis() this.addyaxis () this.setstrokecolor ('#5370C6') this.generateLineChart()Copy the code

Restore and Save

Here’s another tip **, if you want to draw a canvas with a specific image that only works in one drawing: there are save and restore methods

Use the save() method to save the current state, and use restore() to restore as you started

So we can rewrite the way we draw the dotted line, so we start off with svAE, and then we end up at Restore, kind of like a stack, go in, and then we end up, pop out. Each item has its own unique drawing state that 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()
  }
Copy the code

So that’s the end of the line chart that I wanted to show you, so let’s see what it looks like:

The last

This article can be seen as the first canvas visualization chart, I will continue to share, pie chart, tree chart, K-line chart and other visualization charts, I from

Have been writing articles at the same time also constantly thinking, how to express better. If you’re interested in visualizations, like them at πŸ‘! You can follow me

Share a weekly article in either 2D or three.js. I will create every article carefully, never hydrology.

One last word: everyone join me as an Api creator, not a caller!

Download the source code

All of the code for this article’s examples is available on github, starβ˜†πŸ˜―! If you are interested in graphics, you can follow my public number [front-end graphics], get visual learning materials oh!! See you next time at πŸ‘‹