我給中國🇨🇳奧運🏅數作了數據可視化 | 8月更文挑戰

前言

2020東京奧運會已經開幕不少天了,還記得小時候看奧運會的是在2008年的北京奧運會,主題曲是北京歡迎你, 那個時候才上小學吧,幾乎有中國隊的每場必看,當時也是熱血沸騰了, 時間轉眼已經到了2021年而我也從小學生變成了一個天天不斷敲代碼的程序員👩‍💻,看奧運的時間又少,可是又想出分力,既然是程序員,想着能爲奧運會搞點什麼?第一時間想到了就是給奧運獎牌數🏅作可視化,由於單看錶格數據,不能體現出咱們中國的牛逼🐂,昨天蘇神更是創造奇蹟,亞洲速度, 廢話很少說,直接開寫。前端

數據得到

咱們先看下奧運獎牌數的表格,這東西確定是接口得到的吧,我不可能手寫吧,並且天天都是更新的,難道我要天天去改,確定不是這樣的,我當時腦子裏就想着去作爬蟲,去用puppeteer 去模擬瀏覽器的行爲而後獲取頁面的原生dom,而後將表格的數據搞出來, 而後我就很興奮的去搞了,寫了下面的代碼:node

const puppeteer = require('puppeteer')
​
async function main() {
  // 啓動chrome瀏覽器
  const browser = await puppeteer.launch({
    // // 指定該瀏覽器的路徑
    // executablePath: chromiumPath,
    // 是否爲無頭瀏覽器模式,默認爲無頭瀏覽器模式
    headless: false,
  })
​
  // 在一個默認的瀏覽器上下文中被建立一個新頁面
  const page1 = await browser.newPage()
​
  // 空白頁剛問該指定網址
  await page1.goto(
    'https://tiyu.baidu.com/tokyoly/home/tab/%E5%A5%96%E7%89%8C%E6%A6%9C/from/pc'
  )
​
  // 等待title節點出現
  await page1.waitForSelector('title')
​
  // 用page自帶的方法獲取節點
​
  // 用js獲取節點
  const titleDomText2 = await page1.evaluate(() => {
    const titleDom = document.querySelectorAll('#kw')
    return titleDom
  })
  console.log(titleDomText2, '查看數據---')
  // 截圖
  //await page1.screenshot({ path: 'google.png' })
  //   await page1.pdf({
  //     path: './baidu.pdf',
  //   })
  browser.close()
}
main()
​
複製代碼

而後當我很興奮的想要去結果的時候,結果發現是空。百度是否是作了反爬蟲協議, 畢竟我是爬蟲菜鳥,搞了好久。仍是沒搞出來。若是有大佬會,歡迎指點我下哦!ios

image-20210731112152170

不過這個puppeteer,這個庫有點牛皮的,能夠實現網頁截圖、生成pdf、攔截請求,其實有點自動化測試的感受。感興趣的同窗能夠自行了解一下,這不在本篇文章介紹的重點。程序員

接口得到

而後這時候就開始瘋狂百度,開始尋找有沒有現成的api, 真是踏破鐵鞋無覓處,得來全不費工夫。被我找到了,原來是有大佬已經開始作了, 這時候我本地直接去請求那個接口是有問題的,前端不得不處理的問題—— 跨域。 看着東西我頭疼哇, 不過不要緊, 我直接node起一個服務器, 我node去請求那個接口,而後後臺在配置下跨域, 搞定接口數據就直接得到了, 後臺服務我是用的express, 搭建的服務器直接隨便搞搞的。代碼以下:chrome

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)
​
複製代碼

這樣我就是實現了接口轉發,也搞定了跨域問題,前臺我直接用 fetch去請求數據而後作一層數據轉換,可是這個接口不能頻繁請求,動不動就crash, 是真的煩, OK因此直接作了一個操做, 將數據 存到localstorage中,而後作一個定時刷新,時間大概是一天一刷。這樣就保證數據的有效性。代碼以下:express

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()
  }
}
​
fetchData() {
  fetch('http://localhost:3030/data')
    .then((res) => res.json())
    .then((res) => {
      const { errcode, list } = JSON.parse(res.body)
      if (errcode === 100) {
        alert('接口請求太頻繁')
      } else if (errcode === 0) {
        this.data = list
        const obj = {
          list,
          time: Date.now(),
        }
        localStorage.setItem('aoyun', JSON.stringify(obj))
      }
    })
    .catch((err) => {
      console.log(err)
    })
}
複製代碼

數據以下圖所示 :json

image-20210731114644399

柱狀圖的表示

其實我想了不少表達中國金牌數的方式,最終我仍是選擇用2d柱狀圖去表示,並同時作了動畫效果,顯得每一快金牌🏅來的並不容易。我仍是用原生手寫柱狀圖不去使用Echarts 庫, 咱們首先先看下柱狀圖:canvas

柱狀圖

從圖中能夠分析出一些元素axios

  1. x軸和y軸以及一些直線,因此我只要封裝一個畫直線的方法
  2. 有不少矩形, 封裝一個畫矩形的方法
  3. 還有一些刻度和標尺
  4. 最後就是一進入的動畫效果

畫布初始化

在頁面上建立canvas和獲取canvas的一些屬性,並對canvas綁上移動事件。代碼以下:api

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
  }
複製代碼

畫座標軸

座標軸本質上也是一個直線,直線對應的兩個點,不一樣的直線其實就是對應的端點不一樣,因此我直接封裝了一個畫直線的方法:

 // 畫線的方法
  drawLine(x, y, X, Y) {
    this.ctx.beginPath()
    this.ctx.moveTo(x, y)
    this.ctx.lineTo(X, Y)
    this.ctx.stroke()
    this.ctx.closePath()
  }
複製代碼

可能有的人對canvas不熟悉,這裏我仍是大概說下, 開啓一段路徑, 移動畫筆到開始的點, 而後畫直線到末尾的點,而後描邊 這一步是canvas作渲染, 很重要,不少小白不寫, 直線就不出來, 而後閉合路徑。 結束over!

畫座標軸咱們首先先肯定原點在哪裏,咱們首先給畫布向內縮一個padding距離,而後呢,算出畫布實際的寬度和高度。

代碼以下:

initChart() {
  // 留一個內邊距
  this.padding = 50
  // 算出畫布實際的寬度和高度
  this.cHeight = this.height - this.padding * 2
  this.cWidth = this.width - this.padding * 2
  // 計算出原點
  this.originX = this.padding
  this.originY = this.padding + this.cHeight
}
複製代碼

有了原點咱們就能夠畫X軸和Y軸了, 只要加上實際畫布對應的寬度和高度 就行了 。 代碼以下:

//設置canvas 樣式
  this.setCanvasStyle()
  // 畫x軸
  this.drawLine(
    this.originX,
    this.originY,
    this.originX,
    this.originY - this.cHeight
  )
  // 畫Y軸
  this.drawLine(
    this.originX,
    this.originY,
    this.originX + this.cWidth,
    this.originY
  )
複製代碼

第一個 函數就是設置canvas畫筆的樣式的,其實這東西沒什麼。 咱們看下效果:

X軸和Y軸

不少人覺得到這裏就結束了哈哈哈, 那你想太多了, canvas我設置的畫線寬度是1px 爲何看圖片的線的寬度像是2px?不仔細觀察根本發現不了這個問題, 因此咱們要學會思考這究竟是什麼問題?其實這個問題也是我看Echarts源碼發現的, 學而不思則罔,思而不學則殆哇!

彩蛋——CANVAS如何畫出1PX的直線

在這裏我舉一個例子, 你就明白了, 假設我要畫從(50,10) 到 (200,10)這樣的一條直線。爲了畫這條線,瀏覽器首先到達初始起點(50,10)。這條線寬1px,因此兩邊各留0.5px。因此基本上初始起點是從(50,9.5)延伸到(50,10.5)。如今瀏覽器不能在屏幕上顯示0.5像素——最小閾值是1像素。瀏覽器別無選擇,只能將起點的邊界延伸到屏幕上的實際像素邊界。它會在兩邊再加0.5倍的「垃圾」。因此如今,最初的起點是從(50,9)擴展到(50,11),因此看起來有2px寬。狀況以下:

實際效果圖

如今你就應該明白了原來瀏覽器不能顯示0.5像素哇, 四捨五入了, 知道了 問題咱們就必定有解決方案

平移CANVAS

ctx.translate (x,y ) 這個方法:

translate() 方法, 將 canvas 按原始 x點的水平方向、原始的 y點垂直方向進行平移變換

如圖:

canvas平移

說的更直白點, 你對canvas作了translate變化後, 你以前全部畫的點,都會相對偏移。 因此呢,回到咱們這個問題上來, 解決辦法就是什麼呢?就我將畫布 總體向下偏移 0.5 , 因此本來座標 (50,10) 變成了(50.5,10.5) 和(200.5, 10.5)ok 而後瀏覽器的再去畫的 他仍是要預留像素, 因此就是從(50.5, 10) 到(50.5, 11) 這個區間去畫OK, 就是1px了。咱們來try it.

代碼以下:

this.ctx.translate(0.5, 0.5)
// 畫x軸
this.drawLine(
  this.originX,
  this.originY,
  this.originX,
  this.originY - this.cHeight
)
// 畫Y軸
this.drawLine(
  this.originX,
  this.originY,
  this.originX + this.cWidth,
  this.originY
)
this.ctx.translate(-0.5, -0.5)
複製代碼

偏移完以後仍是要恢復過去的, 仍是要十分注意的。 我畫了兩張圖做比對:

A偏移後 B偏移前

很少說了, 看到這裏,若是以爲對你有幫助的話, 或者學到了話, 我是但願你給我點贊👍、評論、加收藏。

畫標尺

咱們如今只有X軸和Y軸, 光禿禿的,我給X軸和Y軸底部增長一些標尺,X軸對應的標尺,確定就是每一個國家的名字,大概的思路就是數據的數量去作一個分段, 而後去填充就行了。

代碼以下:

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) // 文字
  }
}
複製代碼

這裏的話我截取了排名前10的國家, 分斷的思路, 首先兩邊留白20px, 咱們首先先定義每個柱狀圖的寬度 假設是 30 對應上文的 this.rectWidth, 而後每一個文字的座標 其實就很好算了, 起初的x + 所佔的分端數 + 矩形寬度就能夠畫出來了

如圖:

X軸標尺

x軸畫完了,咱們開始畫Y軸, Y軸的大概思路就是 以最多的獎牌數去作分段, 這裏我就分紅6段吧。

// 定義Y軸的分段數
this.ySegments = 6
//定義字體最大寬度
this.fontMaxWidth = 40
複製代碼

接下啦咱們就開始計算Y軸每一個點的Y座標, X座標其實很好計算 只要原點座標的X向左平移幾個距離就行了,主要是計算Y軸的座標, 這裏必定要注意的是, 咱們從座標是相對於左上角的, 因此呢, Y軸的座標應該是向上遞減的。

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) // 文字
  }
}
複製代碼

最大的數據就是數組的第一個數據, 而後每一個標尺就是所佔的比例就行了, Y軸的座標因爲咱們是遞減的因此 對應的座標應該是 1- 所佔的份額, 因爲這只是算的圖標的實際高度 ,換算到畫布裏面, 還要加上原先咱們設置的內邊距,因爲又加上了文字, 文字也佔有必定像素, 因此有加上了20。 OK Y軸畫結束了, 有了Y軸每一個分斷的座標, 同時就畫出背後的對應的幾條實線。

代碼以下:

this.drawLine(
  this.originX,
  yMarker - 4,
  this.originX + this.cWidth,
  yMarker - 4
)
複製代碼

最終呈現的效果圖以下:

xy軸

畫矩形

everything isReady, 下面開始畫矩形, 仍是一樣的方式 先封裝畫矩形的方法, 而後咱們只要傳入對應的數據就OK了。

這裏用到了,canvas原生的rect 方法。參數理解以下:

rect語法

矩形寬度 咱們自定義的, 矩形的高度就是對應的獎牌數在畫布中的高度, 因此咱們只要肯定 矩形的起點就搞定了, 這裏矩形的(x,y) 實際上是左上角的點。

代碼以下:

//繪製方塊
drawRect(x, y, width, height) {
  this.ctx.beginPath()
  this.ctx.rect(x, y, width, height)
  this.ctx.fill()
  this.ctx.closePath()
}
複製代碼

第一步咱們先作一個點的映射, 咱們在畫Y軸的時候,將Y軸的上的畫布的全部的點都放在一個數組中, 注意記得將原點的Y放進去。因此只要計算出每一個獎牌數在總部的比例是多少? 而後再用原點的Y值作一個相減就能夠獲得真正的Y軸座標了。X軸的座標就比較簡單了,原點的X座標加上 ( 所佔的比例 / 總長度 ) 而後在加上 一半的矩形寬度就行了。 這個道理和畫文字是同樣的, 只不過文字要居中嘛。

代碼以下:

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)
  }
}
複製代碼

畫出的效果圖以下:

獎牌數

矩形交互優化

黑禿禿的也醜了吧,一個不知道的人根本不知道這是哪個國家得到多少快金牌。

  1. 給矩形加一個漸變
  2. 加一些文字

如今畫矩形的基礎上加一些文字吧,代碼以下:

this.ctx.save()
this.ctx.textAlign = 'center'
this.ctx.fillText(count, x + this.rectWidth / 2, y - 5)
this.ctx.restore()
複製代碼

漸變就設計到Canvas一個api了,createLinearGradient

createLinearGradient() 方法須要指定四個參數,分別表示漸變線段的開始和結束點。

那我就開始了首先確定建立漸變:

getGradient() {
  const gradient = this.ctx.createLinearGradient(0, 0, 0, 300)
  gradient.addColorStop(0, 'green')
  gradient.addColorStop(1, 'rgba(67,203,36,1)')
  return gradient
}
複製代碼

而後呢咱們就改造drawReact下 ,這裏用了 restore 和save 這個方法, 防止污染文字的樣式。

//繪製方塊
drawRect(x, y, 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()
}
複製代碼

如圖所示:

漸變圖

添加動畫效果

光一個靜態的不能看出咱們的牛皮🐂,因此得有動畫的效果慢慢的增長對吧。其實咱們能夠思考🤔下整個動畫過程,變化的其實就兩個, 柱狀圖的高度和文字, 其實座標軸, 以及柱狀圖的x座標是不變的, 因此我只要定義兩個變量一個開始的值 ,和一個總共的值,高度和文字的大小 其實在每一幀去乘以對應的高度就能夠了。

代碼以下:

// 運動相關
this.ctr = 1
this.numctr = 100
複製代碼

咱們改造下drawBars 這個方法:

// 每一次的比例是多少
const dis = this.ctr / this.numctr
​
// 柱狀圖的高度 乘以對應的比例
const barH = (count / max) * diff * dis
​
// 文字這裏取整下,由於有可能除不盡 
this.ctx.fillText(
  parseInt(count * dis),
  x + this.rectWidth / 2,
  y - 5
)
​
// 最後執行動畫
if (this.ctr < this.numctr) {
  this.ctr++
  requestAnimationFrame(() => {
    this.ctx.clearRect(0, 0, this.width, this.height)
    this.drawLineLabelMarkers()
  })
}
​
複製代碼

每一次都加一,直到比總數大, 而後不斷重畫。 就能夠造成動畫效果了。咱們看下gif圖吧:

奧運gif圖

總結

本篇文章寫到這裏也算結束了,我大概總結下:

  1. canvas如何畫出1px 的直線, 這裏面是有坑的
  2. 還有就是如何進行動畫的設計,本質去尋找那些變的,而後去處理就行了
  3. canvas 中如何進行線性漸變的。
  4. 爬蟲我是失敗了,我就沒啥好總結的,不過有一點: 木偶人這個庫, 你們能夠玩一下的。

本篇文章算是canvas實現可視化圖表的第二篇吧,後面我會持續分享、餅圖、樹狀圖、K線圖等等各類可視化圖表,我本身在寫文章的同時也在不斷地思考,怎麼去表達的更好。若是你對可視化感興趣,點贊收藏關注👍吧!,能夠關注我下面的數據可視化專欄, 每週分享一篇 文章, 要麼是2d、要麼是three.js的。我會用心創做每一篇文章,毫不水文。

咱們一塊兒爲中國🇨🇳奧運加油! 奧利給!!!

源碼得到

關注公衆號【前端圖形】, 回覆【奧運】 兩個字,就能夠得到全部源碼。

相關文章
相關標籤/搜索