1. 給奧運金牌做可視化

        共 15794字,需瀏覽 32分鐘

         ·

        2021-08-03 08:43

        前言

        2020 東京奧運會已經開幕很多天了,還記得小時候看奧運會的是在2008年的北京奧運會,主題曲是「北京歡迎你」, 那個時候才上小學吧,幾乎有中國隊的每場必看,當時也是熱血沸騰了, 時間轉眼已經到了2021年而我也從小學生變成了一個每天不斷敲代碼的程序員?????,看奧運的時間又少,但是又想出分力,既然是程序員,想著能為奧運會搞點什么?第一時間想到了就是給奧運獎牌數??做可視化,因為單看表格數據,不能體現(xiàn)出我們中國的牛逼??, 廢話不多說,直接開寫。

        數據獲得

        我們先看下奧運獎牌數的表格,這東西肯定是接口獲得的吧,我不可能手寫吧,而且每天都是更新的,難道我要每天去改,肯定不是這樣的,我當時腦子里就想著去做爬蟲,去用「puppeteer」 去模擬瀏覽器的行為然后獲取頁面的原生dom,然后將表格的數據搞出來, 然后我就很興奮的去搞了,寫了下面的代碼:

        const puppeteer = require('puppeteer')

        async function main({
          // 啟動chrome瀏覽器
          const browser = await puppeteer.launch({
            // // 指定該瀏覽器的路徑
            // executablePath: chromiumPath,
            // 是否為無頭瀏覽器模式,默認為無頭瀏覽器模式
            headlessfalse,
          })

          // 在一個默認的瀏覽器上下文中被創(chuàng)建一個新頁面
          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節(jié)點出現(xiàn)
          await page1.waitForSelector('title')

          // 用page自帶的方法獲取節(jié)點

          // 用js獲取節(jié)點
          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()

        然后當我很興奮的想要去結果的時候,結果發(fā)現(xiàn)是空。百度是不是做了反爬蟲協(xié)議, 畢竟我是爬蟲菜鳥,搞了很久。還是沒搞出來。如果有大佬會,歡迎指點我下哦!

        image-20210731112152170

        不過這個「puppeteer」,這個庫有點牛皮的,可以實現(xiàn)網頁截圖、生成pdf、攔截請求,其實有點自動化測試的感覺。感興趣的同學可以自行了解一下,這不在本篇文章介紹的重點。

        接口獲得

        然后這時候就開始瘋狂百度,開始尋找有沒有現(xiàn)成的「api」, 真是踏破鐵鞋無覓處,得來全不費工夫。被我找到了,原來是有大佬已經開始做了, 這時候我本地直接去請求那個接口是有問題的,前端不得不處理的問題—— 跨域??粗鴸|西我頭疼哇, 不過沒關系, 我直接node起一個服務器, 我node去請求那個接口,然后后臺在配置下跨域, 搞定接口數據就直接獲得了, 后臺服務我是用的express, 搭建的服務器直接隨便搞搞的。代碼如下:

        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)

        這樣我就是實現(xiàn)了接口轉發(fā),也搞定了跨域問題,前臺我直接用 fetch去請求數據然后做一層數據轉換,但是這個接口不能頻繁請求,動不動就crash, 是真的煩, OK所以直接做了一個操作, 將數據 存到localstorage中,然后做一個定時刷新,時間大概是一天一刷。這樣就保證數據的有效性。代碼如下:

        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,
                  timeDate.now(),
                }
                localStorage.setItem('aoyun'JSON.stringify(obj))
              }
            })
            .catch((err) => {
              console.log(err)
            })
        }

        數據如下圖所示 :

        image-20210731114644399

        柱狀圖的表示

        其實我想了很多表達中國金牌數的方式,最終我還是選擇用2d柱狀圖去表示,并同時做了動畫效果,顯得每一塊金牌??來的并不容易。我還是用原生手寫柱狀圖不去使用「Echarts」 庫, 我們首先先看下柱狀圖:

        柱狀圖

        從圖中可以分析出一些元素

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

        畫布初始化

        在頁面上創(chuàng)建canvas和獲取canvas的一些屬性,并對canvas綁上移動事件。代碼如下:

        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?不仔細觀察根本發(fā)現(xiàn)不了這個問題, 所以我們要學會思考這到底是什么問題?其實這個問題也是我看「Echarts」源碼發(fā)現(xiàn)的, 學而不思則罔,思而不學則殆哇!

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

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

        實際效果圖

        現(xiàn)在你就應該明白了原來「瀏覽器不能顯示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) 這個區(qū)間去畫OK, 就是1px了。我們來try it.

        代碼如下:

        this.ctx.translate(0.50.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)

        偏移完之后還是要恢復過去的, 還是要十分注意的。我畫了兩張圖作比對:

        偏移后              偏移前

        不多說了, 看到這里,如果覺得對你有幫助的話, 或者學到了話, 我是希望你給我點贊??、評論、加收藏。

        畫標尺

        我們現(xiàn)在只有X軸和Y軸, 光禿禿的,我給X軸和Y軸底部增加一些標尺,X軸對應的標尺,肯定就是每個國家的名字,大概的思路就是數據的數量去做一個分段, 然后去填充就好了。

        代碼如下:

        drawXlabel() {
          const length = this.data.slice(010).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
        )

        最終呈現(xiàn)的效果圖如下:

        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(010).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)
          }
        }

        畫出的效果圖如下:

        獎牌數

        矩形交互優(yōu)化

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

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

        現(xiàn)在畫矩形的基礎上加一些文字吧,代碼如下:

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

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

        ?

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

        ?

        那我就開始了首先肯定創(chuàng)建漸變:

        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()
        }

        如圖所示:

        漸變圖

        添加動畫效果

        光一個靜態(tài)的不能看出我們的牛皮??,所以得有動畫的效果慢慢的增加對吧。其實我們可以思考??下整個動畫過程,變化的其實就兩個, 柱狀圖的高度和文字,  其實坐標軸, 以及柱狀圖的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
        )

        // 最后執(zhí)行動畫
        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. 爬蟲我是失敗了,我就沒啥好總結的,不過有一點:木偶人這個庫, 大家可以玩一下的。

        我們一起為中國????奧運加油!奧利給?。?!

        源碼獲得

        關注公眾號【前端圖形學】, 回復【加群】兩個字,就可以找到助手獲得所有源碼。

        瀏覽 59
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 国产成人免费1000部网站 | 国产精品久久成人 | 欧美在线成人网站 | 精品国产乱码久久久久久绯色 | japanese日本老师xxxx18一19 |