在《Canvas 線性圖形(五):多邊形》實現了繪製多邊形的函數。本篇文章將記錄如何繪製雷達圖。最終實現的效果是這樣的:
如動圖中所示,雷達圖從裡到外一共有 6 層,所以,我們需要改造繪製多邊形的函數:
function calcPolygonX(radarX, radius, increaseAngle) {
return radarX + radius * Math.cos(increaseAngle);
}
function calcPolygonY(radarY, radius, increaseAngle) {
return radarY - radius * Math.sin(increaseAngle);
}
// 繪製多邊形的函數
function drawPolygon(radarMapTotalSides, radius, radarX, radarY, ctx) {
let averageAngle = Math.PI * 2 / sides;
let increaseAngle = 0;
let targetX, targetY;
ctx.beginPath();
for ( let i = 0; i < radarMapTotalSides; i++ ) {
targetX = calcPolygonX(radarX, radius, increaseAngle);
targetY = calcPolygonY(radarY, radius, increaseAngle);
ctx.lineTo(targetX, targetY);
increaseAngle += averageAngle;
}
ctx.closePath();
ctx.stroke();
}
// 繪製雷達圖的函數
function drawRadarMap(radarLayers, polygonPerStep, radarMapTotalSides, radarX, radarY, ctx) {
let radius = polygonPerStep;
for ( let j = 0; j < radarLayers; j++ ) {
drawPolygon(radarMapTotalSides, radius, radarX, radarY, ctx);
radius = radius + polygonPerStep;
}
}
polygonPerStep
的意思是每一個多邊形之間相差多少距離,radarMapTotalSides
的意思是雷達圖的多邊形是幾邊形:
let canvas = document.getElementById("radar-map");
let ctx = canvas.getContext("2d");
drawRadarMap(5, 40, 10, 300, 300, ctx);
到目前為止我們就利用繪製多邊形的函數drawPolygon
成功繪製了一個雷達圖的雛形:
當前的雷達圖還缺少了最外層每一個點的文字,以及連線雷達圖中心到最外層點的直線。
連線雷達圖裡外層時,要在繪製多邊形的時候儲存每一層每一個點的座標,也就是在drawPolygon
函數中儲存座標資訊。
在drawRadarMap
函數裡面宣告一個陣列axis
陣列,它專門用於儲存每層多邊形的每個點座標資訊:
function drawRadarMap(radarLayers, polygonPerStep, radarMapTotalSides, radarX, radarY, ctx) {
let axis = []; // 用於儲存每一個多邊形的每一個點座標
let radius = polygonPerStep;
for ( let j = 0; j < radarLayers; j++ ) {
drawPolygon(radarMapTotalSides, radius, radarX, radarY, axis, j, ctx);
radius = radius + polygonPerStep;
}
}
從順時針開始的第一個點的座標到最後一個點座標,以及這些點座標在第幾層多邊形上:
function drawPolygon(radarMapTotalSides, radius, radarX, radarY, axis, currentPolygonLayer, ctx) {
let averageAngle = Math.PI * 2 / sides;
let increaseAngle = 0;
let targetX, targetY;
ctx.beginPath();
axis.push({ layer: currentPolygonLayer, coords: [] }); // 儲存點座標的陣列,
for ( let i = 0; i < radarMapTotalSides; i++ ) {
targetX = calcPolygonX(radarX, radius, increaseAngle);
targetY = calcPolygonY(radarY, radius, increaseAngle);
ctx.lineTo(targetX, targetY);
increaseAngle += averageAngle;
axis[currentPolygonLayer].coords.push({ x: targetX, y: targetY }); // 新增點座標到陣列中
}
ctx.closePath();
ctx.stroke();
}
drawPolygon
函數新增了兩個引數:axis 和 currentPolygonLayer。axis 就是儲存沒層多邊形點的座標陣列;currentPolygonLayer 就是當前多邊形在第幾層多邊形,比如第一層就是 0。
上面的工作是點座標,目的就是連線雷達圖中心點到最外層多邊形的每一個點。所以,在這裡我們新增一個函數,這個函數專門處理連線直線的函數drawStria
:
function drawStria(radarLayers, axis, radarX, radarY, ctx) {
let coords = axis[axis.length - 1].coords;
for ( let i = 0; i < radarLayers; i++ ) {
ctx.beginPath();
ctx.moveTo(radarX, radarY);
ctx.lineTo(coords[i].x, coords[i].y);
ctx.closePath();
ctx.stroke();
drawPointText(data, coords, i, radarX, ctx);
}
}
axis[length - 1].coords
代表雷達圖中最外層的多邊形的所有點座標。順時針遍歷其中元素,ctx.lineTo(coords[i].x, coords[i].y)
從圓點開始依次連線最外層的多邊形的點,從而構成一條條直線。
在雷達圖最外層的多邊形的每一個點新增文字,表示直線代表的是什麼資料。
function drawPointText(data, axis, currentPoint, radarX, ctx) {
ctx.font = `16px Georgia`;
if ( axis[i].x <= radarX ) {
ctx.textAlign = "right";
} else {
ctx.textAlign = "left";
}
ctx.fillText(data[currentPoint].title, axis[currentPoint].x, axis[currentPoint].y);
}
對於這個函數的幾個引數講解:
在drawStria
函數迴圈體內呼叫該函數完成最外層的文字渲染:
接下來就是雷達圖最重要的部分了。雷達圖中每一條直線上該文字所達到的值在此直線上進行移動,連線這些點構成一塊區域,就是資料區域。
function calcDataAreaTopX(areaTopLayer, axis, radarX, currentPoint) {
if ( areaTopLayer < 0 ) {
return radarX;
} else {
return axis[areaTopLayer].coords[currentPoint].x;
}
}
function calcDataAreaTopY(areaTopLayer, axis, radarY, currentPoint) {
if ( areaTopLayer < 0 ) {
return radarY;
} else {
return axis[areaTopLayer].coords[currentPoint].y;
}
}
function drawDataAreaTop(axis, currentPoint, radarX, radarY, ctx) {
let x = calcDataAreaTopX(data[currentPoint].star - 1, axis, radarX, currentPoint);
let y = calcDataAreaTopY(data[currentPoint].star - 1, axis, radarY, currentPoint);
if ( i === 0 ) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
return { x: x, y: y };
}
function drawDataArea(radarMapTotalSides, radius, axis, radarX, radarY, data, ctx) {
ctx.beginPath();
for ( let i = 0; i < radarMapTotalSides; i++ ) {
drawDataAreaTop(axis, i, radarX, radarY, ctx);
}
ctx.closePath();
ctx.strokeStyle = "rgba(68,226,155, 1)";
ctx.stroke();
ctx.fillStyle = "rgba(81,182,137, 0.6)";
ctx.fill();
}
需要為資料函數提供一個data
,這個是資料區域的資訊,最外層多邊形每一個點對應的值:
let data = [
{
title: "js",
star: 4
},
{
title: "ts",
star: 2
},
{
title: "html",
star: 4
},
{
title: "css",
star: 4
},
{
title: "vue",
star: 4
},
{
title: "uniapp",
star: 4
},
{
title: "java",
star: 2
},
{
title: "flutter",
star: 3
},
{
title: "dart",
star: 4
},
{
title: "python",
star: 0
}
];
以上就是繪製資料區域的函數的完整程式碼。drawDataAreaTop
是圈畫資料區域的函數,而drawDataArea
完成最後的顏色填充工作。drawDataAreaTop
接收以下幾個引數:
這裡需要特別說明data[currentPoint].star - 1
,因為 star 是從 0 開始,最大值為 5,必須要減 1,不然陣列索引值越界。
在繪製雷達圖的函數下面新增drawStria
和drawDataArea
兩個函數,完整一個完整的雷達圖繪製
function drawRadarMap(radarLayers, polygonPerStep, radarMapTotalSides, radarX, radarY, ctx) {
// ...
// ...
drawStria(radarMapTotalSides, axis, radarX, radarY, ctx);
drawDataArea(radarMapTotalSides, axis, radarX, radarY, data, ctx);
}
drawRadarMap(5, 40, 10, 300, 300, ctx);
這一節是擴充套件雷達圖的功能,當滑鼠浮在資料區域節點之上時,就出現一個浮動面板,展示具體的資料資訊。浮動面板和雷達圖被包裹在一個 div 標籤裡,並設定為相對定位,浮動面板設定為絕對定位。
<div id="radar-wrap">
<canvas id="radar-map" width="400" height="400">Your browser version is too late.</canvas>
<div id="floating-panel"></div>
</div>
#radar-map {
cursor: pointer;
position: absolute;
border: 1px solid rgba(110, 110, 110, 0.8);
border-radius: 10px;
}
#radar-wrap {
width: 400px;
height: 400px;
box-sizing: border-box;
position: relative;
}
#floating-panel {
position: absolute;
display: none;
border-style: solid;
white-space: nowrap;
z-index: 9999999;
transition: left 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s, top 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s;
background-color: rgba(50, 50, 50, 0.7);
border-width: 0;
border-color: rgb(51, 51, 51);
border-radius: 4px;
color: rgb(255, 255, 255);
font: 14px / 21px "Microsoft YaHei";
padding: 5px;
left: 29px;
top: 145px;
pointer-events: none;
}
接下來就是最重要的 JS 程式碼,這裡為了方便控制樣式,我就用 JQuery 來實現。這裡需要改造一下drawDataArea
函數,我們要儲存資料區域每一個點的座標。
drawFloatingPanel(axis) {
let floatingPanel = $("#floating-panel");
let timeout = null;
$("#radar-map").on({
mousemove: function (e) {
if ( timeout != null ) clearTimeout(timeout);
timeout = setTimeout(() => {
axis.forEach((value, index) => {
if ( (value.x >= e.offsetX - 5 && value.x < e.offsetX + 5) && (value.y >= e.offsetY - 5 && value.y < e.offsetY + 5) ) {
$(floatingPanel).css({
"display": "block", "left": `${ e.offsetX }px`, "top": `${ e.offsetY }px`
});
$(floatingPanel).empty().append(`
<div class="tech">技術:${ value.title }</div>
<div class="star">掌握程度:${ value.star } 顆星</div>
`);
}
});
}, 50);
},
mouseleave: function (e) {
$(floatingPanel).css({ "display": "none" });
}
});
};