Canvas 繪製雷達圖

最近作的一個需求,場景之一是繪製一個雷達圖,找了一圈,彷佛 AntV 下的 F2 很適合拿來主義:html

可是接着又考慮了一下,我當前所作的項目並非可視化項目,從此大機率也不會有這種可視化圖表的需求,只是爲了單個需求一兩個圖表就引入一個可視化庫,性價比有點低,辛辛苦苦優化下來的代碼體積,就由於一個冗餘的代碼庫一會兒回到解放前,那可真要不得(雖然 F2已經夠精簡的了)git

再加上我恰好對於 canvas這塊有點興趣,送上門的練手機會更不可能錯過了,除此以外,我看了一下 F2的文檔 看得有點腦袋疼,有看這文檔的時間我還不如直接去看 Canvas原生 Api呢,因而決定本身來搞定這個東西github

繪製多邊形

這個雷達圖看起來好像挺簡單,實際上仍是有點門道的,我考慮了一下,將其分紅三部分:canvas

  • 正多邊形
  • 正多邊形頂點處文案
  • 雷達區域

多邊形示意以下(這裏以正五邊形做爲示例):api

這個圖其實由多個尺寸不一樣的正五邊形嵌套而成,而且經過 5條線將正五邊形的對角頂點連在了一塊兒,關鍵在於須要知道正五邊形的五個頂點座標,這其實就是求解幾何數學題bash

如圖,旋轉正五邊形,令其中心點位於座標軸原點,其最左側的一條邊平行於 y軸,在此狀態下的其 x座標最大的點(即最右側的點)位於 x軸上,而後再畫一個此正五邊形的外接圓,接下來就能夠進行求解了性能

這裏之因此這麼旋轉正五邊形,只是爲了更方便的求解座標,你固然也能夠令正五邊形的一條邊平行於 x軸或者其餘任意的旋轉進行求解,只要能取得正多邊形各個頂點的相對座標便可優化

顯而易見神特麼顯而易見,正五邊形的每一個頂點的座標就是:ui

(radius * cosθ, radius * sinθ)
複製代碼

這裏的 radius就是正五邊形外接圓半徑,θ是頂點與原點之間連線和 x的夾角spa

其中 radius是咱們本身規定的,只剩下 θ的求解了,按照上圖,若是正多邊形最右側(即 x座標最大)的頂點爲第一個頂點,逆時針旋轉依次爲 第2、第三...第 n

顯而易見的是,這個 θ值其實就是正五邊形內角角度的一半,正多邊形(n)內角角度(mAngle)爲 Math.PI * 2 / n, 則第 n個點的座標爲:

(radius * cos(θ * (n - 1)), radius * sin(θ * (n - 1)))
複製代碼

這裏只是拿正五邊形舉個例子,放寬到正n邊形都是這個道理

頂點拿到了,正多邊形就很好畫了,不過還有一點須要注意,實際需求中,通常是要求正多邊形是正着放置,即底邊與 x平行,而按照本文這裏的求解方式獲得的頂點座標畫出來的正多邊形,是側邊與 y平行,因此須要將獲得的 正多邊形的座標進行必定的映射,將之轉換爲正着放置

canvas也提供了這種操做,即 rotate,只要先把或者後把 canvas的座標系旋轉一下,那麼畫出來的多邊形在視覺上看就是 正着放置的了

function drawPolygon () {
  // #region 繪製多邊形
  const r = mRadius / polygonCount
  let currentRadius = 0
  for (let i = 0; i < polygonCount; i++) {
    bgCtx.beginPath()
    currentRadius = r * (i + 1)
    for (let j = 0; j < mCount; j++) {
      const x = currentRadius * Math.cos(mAngle * j)
      const y = currentRadius * Math.sin(mAngle * j)
      // 記錄最外層多邊形各個頂點的座標
      if (i === polygonCount - 1) {
        polygonPoints.push([x, y])
      }
      j === 0 ? bgCtx.moveTo(x, y) : bgCtx.lineTo(x, y)
    }
    bgCtx.closePath()
    bgCtx.stroke()
  }
  // #endregion

  // #region 繪製多邊形對角連線
  for (let i = 0; i < polygonPoints.length; i++) {
    bgCtx.moveTo(0, 0)
    bgCtx.lineTo(polygonPoints[i][0], polygonPoints[i][1])
  }
  bgCtx.stroke()
  // #endregion
}
複製代碼

正多邊形頂點處文案

文案的位置其實就是在頂點附近,按照頂點的座標進行必定的偏移便可,但前面說過了,因爲 canvas座標系已經經過 rotate進行了旋轉,這裏想要讓繪製出來的文字是正着放置的,就須要再次將座標系旋轉回去

除此以外,還要注意一下文字繪製的對其方式,這個經過 textAlign能夠解決:

function drawVertexTxt () {
  bgCtx.font = 'normal normal lighter 16px Arial'
  bgCtx.fillStyle = '#333'
  // 奇數多邊形,距離設備頂邊最近的點(即最高點的那一點),須要專門設置一下 textAlign
  const topPointIndex = mCount - Math.round(mCount / 4)
  for (let i = 0; i < polygonPoints.length; i++) {
    bgCtx.save()
    bgCtx.translate(polygonPoints[i][0], polygonPoints[i][1])
    bgCtx.rotate(rotateAngle)
    let indentX = 0
    let indentY = 0
    if (i === topPointIndex) {
      // 最高點
      bgCtx.textAlign = 'center'
      indentY = -8
    } else {
      if (polygonPoints[i][0] > 0 && polygonPoints[i][1] >= 0) {
        bgCtx.textAlign = 'start'
        indentX = 10
      } else if (polygonPoints[i][0] < 0) {
        bgCtx.textAlign = 'end'
        indentX = -10
      }
    }
    // 若是是正四邊形,則須要單獨處理最低點
    if (mCount === 4 && i === 1) {
      bgCtx.textAlign = 'center'
      indentY = 10
    }
    // 開始繪製文案
    mData[i].titleList.forEach((item, index) => {
      bgCtx.fillText(item, indentX, indentY + index * 20)
    })
    bgCtx.restore()
  }
}
複製代碼

雷達區域

雷達區域就是文章開頭那個圖中的紅色線框內的區域,這個區域也是一個多邊形,只不過不是正的,但座標的求解其實和正多邊形是差很少的,只須要在求座標的過程當中,對座標參數進行必定比例的縮放罷了,而這個比例就是所在的頂點表明的實際值與總值的比例(好比,100分是滿分,第一個點只有80分,那麼就是 80%

若是隻是一個靜態圖,那麼到此爲止也就沒什麼好說的了,求得雷達區域各個點座標,而後鏈接路徑、閉合路徑,再描邊就完事,但若是是想要作成文章開頭那種雷達區域動態填充的,就稍微要麻煩一點了

我一開始的想法是,動態求解每一幀雷達區域的各個頂點座標,後來算了半天發現也太麻煩了,怎麼扯出來那麼多數學公式,這就算是能求出來性能應該也好不到哪裏去吧

遂棄之,另尋他法,忽得一技

文章開頭的那種動態填充法,看起來很像是一個圓以扇形打開的樣子啊,看了一下 canvas裏有個叫 clip的東西,因而想到,只要先將須要的雷達區域裁切(clip)好,再用一個足夠覆蓋這個裁切區域的圓放到這個裁切面上進行動態扇形展開,不就達到目的了嗎?

for (let i = 0; i < mCount; i++) {
  // score不能超過 fullScore
  score = Math.min(mData[i].score, mData[i].fullScore)
  const x = Math.cos(mAngle * i) * score / mData[i].fullScore
  const y = Math.sin(mAngle * i) * score / mData[i].fullScore
  i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)
}
ctx.closePath()
ctx.clip()
// ...
ctx.moveTo(0, 0)
ctx.arc(0, 0, canvasMaxSize, 0, currentAngle)
ctx.closePath()
ctx.fill()
複製代碼

效果如圖:

彷佛可行,可是與文章開頭的那個圖對比了一下,發現還有點欠缺,頭圖的雷達區域是有紅色描邊的,而且在完整繪製完畢後,雷達區域的每一個頂點處都有紅色小圓點

頂點處的紅色小圓點好辦,頂點座標是已知的,無非是在頂點處在畫個小圓罷了,可是描邊有點麻煩

描邊的長度是緊跟雷達區域繪製進度的,這就須要知道每一幀雷達區域每一個頂點的座標,這不又回去了嗎?說好了不搞那麼多公式計算的

後來又想了下,若是事先把雷達圖最終態畫好,而後用一個蒙層遮住,接着再把這個蒙層動態打開不也行嗎?

又看了一下canvas的文檔,發現了一個叫 globalCompositeOperationAPI,就是它了

爲了方便繪製,我從新劃分了一下,一共用了三個 canvas

第一個 canvas做爲最終呈現效果的畫布,第二個用於繪製完整版的靜態雷達區域,第三個則繪製用於給完整版的靜態雷達區域進行遮罩的蒙層,三個canvas一組合,就達到了預期效果:

小結

本文完整示例 Live Demo示例代碼 已經上傳,感興趣的能夠親自試下

相關文章
相關標籤/搜索