1. 程式人生 > >konva canvas外掛寫雷達圖示例

konva canvas外掛寫雷達圖示例

最近,做了一個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/[email protected]/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}:\r\n${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把這個圖重新畫一遍。