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把這個圖重新畫一遍。