preface

It has been many days since the Tokyo 2020 Olympic Games opened. I still remember when I was a child, I watched the Olympic Games in Beijing in 2008. The theme song was Beijing welcomes you. Time has gone to 2021 and I also became a primary school students every day constantly knock code programmer 👩💻, watch the Olympic Games time and less, but think of a component, since it is a programmer, thinking of what to do for the Olympic Games? The first time thought of is to give the Olympic medal number 🏅 do visualization, because the table data alone, can not reflect our Chinese cow force 🐂, yesterday Su God is to create a miracle, Asian speed, nonsense is not to say, directly open to write.

Data acquisition

Let’s take a look at the table of the Number of Olympic MEDALS, this thing must be obtained by the interface, I can’t write it, and it is updated every day, do I have to change every day, it is definitely not so, I was thinking of becoming a reptile at that time, Using Puppeteer to simulate the browser’s behavior and get the page’s native DOM, then pull out the table’s data, I was excited to do it and wrote the following code:

Const puppeteer = require('puppeteer') async function main() {const browser = await puppeteer.launch({ // // Specify an executablePath for the browser // chromiumPath, // Whether to use headless browser mode. The default is headless: false, }) // create a newPage in a default browser context const page1 = await browser.newpage () // blank page just asks the specified url await page1. 'https://tiyu.baidu.com/tokyoly/home/tab/%E5%A5%96%E7%89%8C%E6%A6%9C/from/pc') / / wait for the title nodes appear await Const titleDomText2 = await page1.evaluate(() => {const titleDom = document.querySelectorAll('#kw') return titleDom }) console.log(titleDomText2, Screenshot ({path: 'google.png'}) //await page1.pdf({path: 'google.png'}) //await page1.pdf({// path: 'google.png'}) './baidu.pdf', // }) browser.close() } main()Copy the code

Then when I was excited to go to the results, the results were empty. Baidu is not made anti-reptile agreement, after all I am reptile rookie, made for a long time. Still not worked out. If there is a big guy will, welcome to give me directions!

However, this puppeteer is a bit of a crass library that can capture web pages, generate PDFS, intercept requests, and actually act as an automated test. If you are interested, you can find out for yourself. This is not the focus of this article.

The interface for

Then at this time began to crazy Baidu, began to look for a ready-made API, is really out of nowhere, come all the time. Was found by me, the original is a big guy has started to do, at this time I directly to the local request for the interface is a problem, the front end has to deal with the problem – cross-domain. Look at things I headache wow, but it doesn’t matter, I directly node up a server, I node to request that interface, and then the background under the configuration of cross-domain, interface data directly to get, I use the background service Express, set up the server directly casually. The code is as follows:

const axios = require('axios')
const express = require('express')
const request = require('request')
const app = express()
​
const allowCrossDomain = function (req, res, next) {
  res.header('Access-Control-Allow-Origin', '*')
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
  res.header('Access-Control-Allow-Headers', 'Content-Type')
  res.header('Access-Control-Allow-Credentials', 'true')
  next()
}
app.use(allowCrossDomain)
​
app.get('/data', (req, res) => {
  request(
    {
      url: 'http://apia.yikeapi.com/olympic/?appid=43656176&appsecret=I42og6Lm',
      method: 'GET',
      headers: { 'Content-Type': 'application/json' },
    },
    function (error, response, body) {
      if (error) {
        res.send(error)
      } else {
        res.send(response)
      }
    }
  )
})
app.listen(3030)
​
Copy the code

In this way, I realized the interface forwarding and solved the cross-domain problem. In the foreground, I directly used FETCH to request data and then did a layer of data conversion. However, this interface could not frequently request and crash easily, which was really annoying, SO I directly did an operation to save the data to localstorage. Then do a regular refresh, about once a day. This ensures the validity of the data. The code is as follows:

getData() { let curTime = Date.now() if (localStorage.getItem('aoyun')) { let { list, time } = JSON.parse(localStorage.getItem('aoyun')) console.log(curTime - time, If (curtime-time <= 24 * 60 * 60 * 60) {this.data = list} else {this.fetchdata ()}} else {this.fetchdata ()} this.fetchData() } } fetchData() { fetch('http://localhost:3030/data') .then((res) => res.json()) .then((res) => { const  { errcode, List} = json.parse (res.body) if (errcode === 100) {alert(' too many requests ')} else if (errcode === 0) {this.data = list const  obj = { list, time: Date.now(), } localStorage.setItem('aoyun', JSON.stringify(obj)) } }) .catch((err) => { console.log(err) }) }Copy the code

The data are shown in the figure below:

Histogram representation

In fact, I thought of many ways to express the number of Gold MEDALS in China, and finally I chose to use 2D bar chart to express, and at the same time do animation effect, it is not easy to show each gold medal 🏅. Instead of using the Echarts library, LET’s look at the bar chart first:

Some elements can be analyzed from the diagram

  1. X and y and some lines, so I just encapsulate a way to draw a line
  2. There are many rectangles, encapsulating a way to draw a rectangle
  3. There are also scales and rulers
  4. The last is the entry of the animation effect

Canvas initialization

Creates a canvas on the page, retrieves some properties of the canvas, and binds a move event to the canvas. The code is as follows:

get2d() {
    this.canvas = document.getElementById('canvas')
    this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this))
    this.ctx = this.canvas.getContext('2d')
    this.width = canvas.width
    this.height = canvas.height
  }
Copy the code

Drawing axes

The coordinate axis is also a line in essence, and the two points corresponding to the line are different. In fact, different lines correspond to different endpoints, so I directly encapsulated a method of drawing a line:

DrawLine (x, y, x, y) {this.ctx.beginPath() this.ctx.mopath () this.ctx.mopath (x, y) this.ctx.lineto (x, y) Y) this.ctx.stroke() this.ctx.closePath() }Copy the code

For some of you who are not familiar with canvas, HERE I will say roughly: open a path, move the brush to the beginning point, then draw a line to the end point, and then stroke. This step is canvas rendering, which is very important. If many small whites are not written, the line will not come out, and then close the path. Over over!

To draw the axes, let’s first figure out where the origin is, let’s first give the canvas a padding distance, and then, let’s figure out the actual width and height of the canvas.

The code is as follows:

InitChart () {// leave a margin this.padding = 50 // Figure out the actual width and height of the canvas this.cheight = this.height-this.padding * 2 this.cwidth = This.originx = this.padding this.originy = this.padding + this.cheight}Copy the code

Now that we have the origin we can draw the X-axis and the Y-axis, just add the width and height of the actual canvas. The code is as follows:

This.drawline (this.originx, this.originy, this.originx, this.originx, this.originx, this.originx) This. drawLine(this.originx, this.originy, this.originx + this.cwidth, this.originx + this.cwidth, this.originx + this.cwidth, this.originx + this.cwidth) this.originY )Copy the code

The first function is to set the style of the canvas, which is not a big deal. Let’s look at the effect:

Many people think this is the end of the line. Hahaha, you are thinking too much. Canvas I set the line width to 1px. You can’t see it without looking, so we have to learn to think about what is the problem? In fact, this problem is also I see Echarts source found, learning without thought is useless, thought without learning is perilous!

How to draw a 1PX line on CANVAS

So just to give you an example, let’s say I want to draw a line from (50,10) to (200,10). To draw this line, the browser first reaches the initial starting point (50,10). This line is 1px wide, so leave 0.5px on each side. So basically the starting point goes from (50,9.5) to (50,10.5). Now browsers can’t display 0.5 pixels on the screen – the minimum threshold is 1 pixel. The browser has no choice but to extend the boundary of the starting point to the actual pixel boundary on the screen. It adds 0.5 times more “garbage” to both sides. So now, the initial starting point is extended from (50,9) to (50,11), so it looks 2px wide. The situation is as follows:

Now you know that browsers can’t display 0.5 pixels wow, that’s rounded up, we know we have a solution

Translation CANVAS

Ctx.translate (x,y) This method:

The translate() method translates the canvas in the horizontal direction of the original x point and the vertical direction of the original Y point

As shown in figure:

To put it more bluntly, when you translate the canvas, all the dots you drew before will be offset relative to each other. So, back to our problem, what’s the solution? I shifted the whole canvas down by 0.5, so the original coordinates (50,10) became (50.5,10.5) and (200.5, 10.5) ok, and then the browser to draw again, he still has to reserve pixels. So let’s draw OK from (50.5, 10) to (50.5, 11), which is 1px. Let’s try it.

The code is as follows:

OriginX, originY, originX, originX, originX, originX, originX This. drawLine(this.originx, this.originy, this.originx + this.cwidth, this.originx + this.cwidth, this.originx + this.cwidth, this.originx + this.cwidth) Enclosing originY) enclosing CTX. Translate (0.5, 0.5)Copy the code

After the offset or to restore the past, or to be very careful. I drew two pictures for comparison:

After the migrationBefore the migration

Don’t say much, see here, if feel helpful to you, or learned words, I hope you give me 👍, comment, add collection.

Drawing scale

Now we only have the X-axis and Y-axis, bare, I add some rulers at the bottom of the X-axis and Y-axis, the ruler corresponding to the X-axis must be the name of each country, the general idea is to make a section based on the amount of data, and then fill it.

The code is as follows:

drawXlabel() { const length = this.data.slice(0, 10).length this.ctx.textAlign = 'center' for (let i = 0; i < length; i++) { const { country } = this.data[i] const totalWidth = this.cWidth - 20 const xMarker = parseInt( this.originX + totalWidth * (i / length) + this.rectWidth ) const yMarker = this.originY + 15 this.ctx.fillText(country, xMarker, YMarker, 40)Copy the code

First, leave 20px space on both sides. First, define the width of each bar chart. Let’s say it is 30, which corresponds to this. So we can draw the initial x plus the number of ends plus the width of the rectangle

As shown in figure:

The X-axis is done, so let’s draw the Y-axis, and the general idea of the Y-axis is to divide it into the most MEDALS, so I’ll divide it into six.

YSegments = 6 // Define the maximum width of the font this.fontMaxWidth = 40Copy the code

For now we began to calculate each point Y, Y X coordinate actually very good calculation As long as the origin of coordinates X to the left shift a few distance, mainly calculated Y coordinates, it is important to note here is that we have from the coordinate is relative to the upper left corner, so diminishing Y coordinates should be upward.

drawYlabel() { const { jin: maxValue } = this.data[0] this.ctx.textAlign = 'right' for (let i = 1; i <= this.ySegments; i++) { const markerVal = parseInt(maxValue * (i / this.ySegments)) const xMarker = this.originX - 5 const yMarker = parseInt((this.cHeight * (this.ySegments - i)) / this.ySegments) + this.padding + 20 this.ctx.fillText(markerVal, XMarker, yMarker)Copy the code

The largest data is the first data array, and then each scale is the proportion of good, Y because we are decreasing the coordinates of the corresponding coordinates should be 1 – share, because it’s only calculate the actual height of the icon, conversion to the canvas, plus our original setting padding, due to add the words, The text also takes up a certain number of pixels, so 20 is added. OK, the Y-axis is finished. Now that I have the coordinates of each Y axis break, I will draw the corresponding solid lines behind it.

The code is as follows:

this.drawLine(
  this.originX,
  yMarker - 4,
  this.originX + this.cWidth,
  yMarker - 4
)
Copy the code

The final rendering is as follows:

Draw a rectangular

Everything isReady, so let’s start drawing the rectangle, and we’ll do the same thing to wrap the method of drawing the rectangle, and then we’ll just pass in the corresponding data.

I’m using the canvas native rect method. The parameters are as follows:

The width of the rectangle is custom, and the height of the rectangle is the height of the canvas for the corresponding medal count, so we just need to determine the starting point of the rectangle, where (x,y) is actually the point in the upper left corner.

The code is as follows:

DrawRect (x, y, width, height) {this.ctx.beginpath () this.ctx.rect(x, y, width, height) {this.ctx.rect(x, y, width, height); height) this.ctx.fill() this.ctx.closePath() }Copy the code

The first step is to do a point mapping, so when we draw the Y-axis, we put all the points on the Y-axis of the canvas in an array, and remember to put the Y of the origin in. So just figure out what percentage of each medal count is at headquarters? And then you subtract the Y value from the origin to get the true Y coordinate. The x-coordinate is easy, the x-coordinate of the origin plus, and then you add half the width of the rectangle. This truth is the same as the painting text, but the text should be in the center.

The code is as follows:

drawBars() {
  const length = this.data.slice(0, 10).length
  const { jin: max } = this.data[0]
  const diff = this.yPoints[0] - this.yPoints[this.yPoints.length - 1]
  for (let i = 0; i < length; i++) {
    const { jin: count } = this.data[i]
    const barH = (count / max) * diff
    const y = this.originY - barH
    const totalWidth = this.cWidth - 20
    const x = parseInt(
      this.originX + totalWidth * (i / length) + this.rectWidth / 2
    )
    this.drawRect(x, y, this.rectWidth, barH)
  }
}
Copy the code

The renderings are as follows:

Rectangular interactive optimization

Black bald also ugly, a person who does not know that this is which country won how many fast gold MEDALS.

  1. Add a gradient to the rectangle
  2. Add some text

Now let’s add some text to the rectangle. The code looks like this:

this.ctx.save()
this.ctx.textAlign = 'center'
this.ctx.fillText(count, x + this.rectWidth / 2, y - 5)
this.ctx.restore()
Copy the code

The gradient is designed to Canvas as an API, createLinearGradient

The createLinearGradient() method requires you to specify four parameters representing the start and end points of the gradient line segment.

So I’m gonna go ahead and make sure to create the gradient:

getGradient() { const gradient = this.ctx.createLinearGradient(0, 0, 0, 300) gradient.addColorStop(0, AddColorStop (1, 'rgba(67,203,36,1)') return gradient}Copy the code

Let’s use the restore and save methods to prevent contamination of the text style.

DrawRect (x, y, width, width) height) { this.ctx.save() this.ctx.beginPath() const gradient = this.getGradient() this.ctx.fillStyle = gradient this.ctx.strokeStyle = gradient this.ctx.rect(x, y, width, height) this.ctx.fill() this.ctx.closePath() this.ctx.restore() }Copy the code

As shown in the figure:

Add animation

A static light can’t see our cowhide 🐂, so the animation effect has to be slowly increased right? We can actually think 🤔 throughout the animation process, are two changes, the height of the column and words, actually axes, and the histogram of the x coordinate is the same, so I just defines the values of two variables, a start, and a total value, the height and the size of the text In fact in each frame to by the height of the correspondence.

The code is as follows:

This.ctr = 1 this.numctr = 100Copy the code

Let’s change the drawBars method:

Const barH = (count/Max) * diff * dis // * diff * dis (count/Max) * diff * dis) FillText (parseInt(count * dis), x + this.rectwidth / 2, If (this.ctr < this.numctr) {this.ctrrequestAnimationFrame (() => {this.ctx.clearRect(0, 0, 0, 0); this.width, this.height) this.drawLineLabelMarkers() }) }Copy the code

You add one each time until you’re bigger than the total, and then you keep redrawing. And you can animate it. Let’s take a look at the GIF:

conclusion

This is the end of this article. I would like to summarize:

  1. How can a canvas draw a 1px line with a pit
  2. And how to do the design of the animation, essentially to find those changes, and then to deal with it
  3. How to do linear gradient in Canvas.
  4. Reptile I was a failure, I did not have what good summary, but there is a point: the puppet people this library, we can play.

This article can be regarded as the second one of Canvas’s realization of visual charts. In the future, I will continue to share, pie chart, tree chart, K-line chart and other kinds of visual charts. While writing the article, I am also constantly thinking about how to express myself better. If you’re interested in visualizations, like them at 👍! You can follow my data visualization column below and share a weekly article, either in 2D or three.js. I will create every article carefully, never hydrology.

We together for China 🇨🇳 Olympic Refueling! Ollie here!!

The source code for

Pay attention to the public number [front-end graphics], reply [Olympic] two words, you can get all the source code.