­

konva canvas插件寫雷達圖示例

  • 2019 年 10 月 30 日
  • 筆記

最近,做了一個HTML5的項目,裡面涉及到了雷達圖效果,這裡,我將react實戰項目中,用到的雷達圖單拎出來寫一篇部落格,供大家學習。

以下內容涉及的程式碼在我的gitlab倉庫中:
Konva canvas雷達圖示例

先看效果圖:

1. konva簡單了解

現在js社區非常發達,有很多強大的插件,可以簡化開發者的工作,我這裡選用的canvas 2d插件是konva,它機會可以繪製我們能想到的所有平面圖形,學習參考地址:
https://konvajs.org/docs/

這裡我們簡單了解下konva是如何工作的:

  • konva的一起工作開始於Konva.stage, 它可以包含一個或者多個 Konva.Layer.
  • 每一個 Konva.Layer 都有兩個canvas渲染出來,一個畫布用戶顯示,一個隱藏畫布用於高性能事件監測
  • 每一個 layer可以包含 shapes, groups
  • groups可以包含 groups以及shapes
  • stage, layers, groups, shapes都是 vitual nodes,類似於html頁面的DOM nodes
  • 所有的nodes都能夠被設置style以及做transform動畫效果

konva的Node等級如下圖:

2. react中引入konva

有兩種方式引入,一種是npm安裝之後,使用import引入

還有一種直接在html文件的<head></head>中引入,我建議直接使用文件引入,可以使用cdn加速,並且在react的index.html中引入後,可以直接使用Konva這個全局變數

<script src="https://unpkg.com/konva@4.0.0/konva.min.js"></script>

3. 圖形繪製

在react入口文件,引入繪製圖形的js程式碼,獲取canvas畫布的大小後,調用繪製方法進行繪製圖形。

在繪製圖形前,先構造一個json數據,存放在state中:

    this.state = {        data: {          "label": "Your score:",          "score": 92,          "scores": [            { "type": "health", "score": "98" },            { "type": "wealth", "score": "93" },            { "type": "career", "score": "90" },            { "type": "love", "score": "83" },            { "type": "happiness", "score": "87" }          ]        }      }

App.js所有程式碼如下:

import React, { Component } from 'react';  import './App.css';    import { initScene } from './tools/renderRadar.js';    class App extends Component {    constructor(props) {      super(props);      // 雷達圖數據      this.state = {        data: {          "label": "Your score:",          "score": 92,          "scores": [            { "type": "health", "score": "98" },            { "type": "wealth", "score": "93" },            { "type": "career", "score": "90" },            { "type": "love", "score": "83" },            { "type": "happiness", "score": "87" }          ]        }      }    }      componentDidMount() {      const { data } = this.state;      // 獲取canvas畫布的寬度      const offsetWidth = document.getElementById('radar-canvas').offsetWidth;      // 繪製canvas      initScene(data, offsetWidth, offsetWidth);    }      render() {      return (        <div className="App">          <div className="demo">            <h1>Konva canvas demo:</h1>            <div className="radar-canvas" id="radar-canvas"></div>          </div>        </div>      );    }  }    export default App;

 

上面程式碼中調用 initScene來繪製canvas影像,我先簡單寫一下這個函數的結構

const Konva = window.Konva;    let canvasHeight = 540;  let canvasWidth = 540;  
// 用於獲取一個可變的值,這個值和canvas畫布的寬度等比例
function ratio(num){
return canvasWidth * num;
}
/**   * 繪製canvas   * @param init 雷達圖數據結構   * @param offsetWidth canvas畫布寬度   * @param offsetHeight canvas畫布高度   * @returns {Konva.Stage}   */  function initScene(init, offsetWidth, offsetHeight) {    // 設置畫布大小    canvasHeight = offsetHeight;    canvasWidth = offsetWidth;    // 創建Konva Stage,實際上就是創建一個canvas畫布    const stage = new Konva.Stage({      container: 'radar-canvas',      width: canvasWidth,      height: canvasHeight,    });    // 創建一個Konva layer    const layer = new Konva.Layer();      // todo:: 繪製雷達底圖      // todo:: 繪製雷達數值圖      // todo:: 繪製文字      // todo:: 繪製各角文字      // 添加layer到stage    stage.add(layer);      // 繪製layer    layer.draw();      // 這裡返回stage,可以用戶調用函數獲取畫布資訊,比如用戶獲取base64資訊等    return stage;  }

注意這裡有一個ratio方法,這個方法可用於設置等比的大小,用於適配各種解析度的移動設備。

1)雷達底圖繪製

雷達底圖主要是使用Konva.RegularPolygon來繪製等邊多邊形的。

/**   * 繪製雷達地圖   * @param stage   * @returns {Konva.Group}   */  function getPentagon(stage) {    // 創建一個組,用於容納5個大小遞減的多邊形,    // group的大小正好是整個canvas畫布的大小    const group = new Konva.Group({      x: 0,      y: 0,      width: stage.width(),      height: stage.height(),      offsetX: 0,      offsetY: 0,    });    for (let i = 0; i < 5; i++) {      let radius = stage.width() * 0.3; // 這個為外圈的半徑      radius = radius / 5 * (i + 1); // 5等分半徑      // 創建一個等邊多邊形      const pentagon = new Konva.RegularPolygon({        x: stage.width() / 2,        y: stage.height() / 2,        sides: 5, // 邊數        radius, // 半徑        fill: 'transparent', // 填充顏色        stroke: '#b04119', // 邊框顏色        strokeWidth: ratio(1 / 640 * 3), // 邊框寬度        opacity: 0.8,      });      group.add(pentagon);    }      return group;  }

在initScene函數中調用:

// 繪製雷達底圖  const pentagonGroup = getPentagon(stage);  layer.add(pentagonGroup);

繪製後如下圖:

2)雷達數值圖繪製

使用Konva.shap可以繪製不規則的圖形,實際上就是利用了canvas的moveTo, lineTo的功能:

/**   * 繪製數值圖   * @param init   * @param stage   * @returns {Konva.Shape}   */  function getValues(init, stage) {    const topics = init.scores;    // 按照實際數組大小進行360的n等分    const angle = Math.floor(360 / topics.length);    // 便宜角度,用於和雷達底圖角度對齊    const offsetAngle = -angle / 4;    // 繪製不規則圖形    const triangle = new Konva.Shape({      sceneFunc(context, shape) {        context.beginPath();        const startX = stage.width() / 2;        const startY = stage.height() / 2;        for (let i = 0; i < topics.length; i++) {          const value = getValuePoint(startX, startY, topics[i].score, angle * (i + 1) + offsetAngle);          if (i === 0) {            context.moveTo(value.x, value.y);          } else {            context.lineTo(value.x, value.y);          }        }        context.closePath();        context.fillStrokeShape(shape);      },      fill: '#2c00b0',      stroke: '#ffc71d',      strokeWidth: ratio(1 / 640 * 3),      opacity: 0.6,    });    return triangle;  }    /**   * 根據分數獲取需要移動的坐標   * @param xDef 中心點x   * @param yDef 中心點y   * @param value 數值   * @param angle 偏移角度   * @returns {{x: *, y: *}}   */  function getValuePoint(xDef, yDef, value, angle) {    // rat為底圖外圈的半徑*value/100    const rat = ratio(0.3) / 100 * value;    const x = xDef + rat * Math.cos(angle * Math.PI / 180);    const y = yDef + rat * Math.sin(angle * Math.PI / 180);    return {      x,      y,    };  }

在initScene中調用方法繪製:

// 繪製雷達數值圖  const values = getValues(init, stage);  layer.add(values);

繪製後圖形:

3)雷達文字繪製

文字就是調用Konva.Text進行繪製,很簡單,直接貼程式碼:

  // 繪製文字    const text = new Konva.Text({      text: init.label,      fill: '#b04119',      fontSize: ratio(1 / 640 * 28),      fontStyle: 'bold italic',      fontFamily: 'Arial',      x: stage.width() / 2, // x設置為中心點      y: stage.height() / 2, // y設置為中心點      align: 'center', // 文字對齊方式      offsetY: ratio(1 / 640 * 90),      opacity: 1,    });    text.offsetX(text.width() / 2); // 對text向左偏移50%    layer.add(text);    const textScore = new Konva.Text({      text: init.score,      fill: '#ffda1d',      fontSize: ratio(1 / 640 * 160),      fontStyle: 'bold italic',      fontFamily: 'Arial',      x: stage.width() / 2,      align: 'center',      y: stage.height() / 2,      offsetY: ratio(1 / 640 * 60),      opacity: 1,    });    textScore.offsetX(textScore.width() / 2);    layer.add(textScore);

繪製後圖:

4)各角文字繪製

繪製各角文字,同樣利用了getValuePoint方法獲取每個定點的坐標位置:

// 首字母大寫  function titleCase(str) {    const arr = str.split(' ');    for (let i = 0; i < arr.length; i++) {      arr[i] = arr[i].slice(0, 1).toUpperCase() + arr[i].slice(1).toLowerCase();    }    return arr.join(' ');  }    function getTopics(init, layer, stage) {    const topics = init.scores;      const angle = Math.floor(360 / topics.length);    const offsetAngle = -angle / 4;    const startX = stage.width() / 2;    const startY = stage.height() / 2;    for (let i = 0; i < topics.length; i++) {      const angleCur = angle * (i + 1) + offsetAngle;      // 獲取角坐標      const pointCoordinate = getValuePoint(startX, startY, 115, angleCur);      // 設置container, 每個container都以離五邊形的定點15%的距離為中心點      // 寬度為畫布寬度,高度為畫布高度      const container = new Konva.Group({        x: pointCoordinate.x,        y: pointCoordinate.y,        width: stage.width(),        height: stage.height(),        offsetX: stage.width() / 2,        offsetY: stage.height() / 2,      });        const topic = topics[i];      // 文本      const value = titleCase(`${topic.type}:rn${topic.score}`);      const text = new Konva.Text({        text: value,        fill: '#671fc5',        fontSize: ratio(0.04),        fontStyle: 'bold',        fontFamily: 'Arial',        x: stage.width() / 2,        y: stage.height() / 2,        align: 'center',        offsetX: 0,        offsetY: 0,      });      // 文本向左,向上分別偏移50%,達到在container居中的效果      text.offsetX(text.width() / 2);      text.offsetY(text.height() / 2);      // 添加文字到container      container.add(text);      // 添加container到layer      layer.add(container);    }  }

在initScene中調用:

// 繪製各角文字  getTopics(init, layer, stage);

這樣就得到了最終結果圖:

 

繪製這個雷達圖,多次使用了數學函數,計算左邊,實際上就是利用了直角三角形邊的計算方法

Math.cos()  Math.sin()

 

到這裡,這篇文章就結束啦,後面有空,我會使用原生的canvas把這個圖重新畫一遍。