Web 前端實戰(三):雷達圖

2022-05-31 21:00:33

前言

《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);
}

對於這個函數的幾個引數講解:

  1. data:順時針開始最外層每一個點的文字;
  2. axios:最外層多邊形每一個點的座標資訊;
  3. currentPoint:當前迴圈到的多邊形的一個點座標;
  4. radarX:雷達圖中心座標的 x 座標軸。

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接收以下幾個引數:

  1. axis:順時針開始最外層每一個點的文字;
  2. currentPoint:當前迴圈到的多邊形的一個點座標;
  3. radarX:雷達圖中心座標的 x 座標軸;
  4. radarY:雷達圖中心座標的 y 座標軸。

這裡需要特別說明data[currentPoint].star - 1,因為 star 是從 0 開始,最大值為 5,必須要減 1,不然陣列索引值越界。

呼叫雷達圖函數

在繪製雷達圖的函數下面新增drawStriadrawDataArea兩個函數,完整一個完整的雷達圖繪製

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);

Gitee 倉庫-雷達圖完整程式碼

雷達圖浮動面板

這一節是擴充套件雷達圖的功能,當滑鼠浮在資料區域節點之上時,就出現一個浮動面板,展示具體的資料資訊。浮動面板和雷達圖被包裹在一個 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>
點選檢視 CSS 程式碼
#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函數,我們要儲存資料區域每一個點的座標。

點選檢視 JQuery 程式碼
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" });
		}
	});
};

Gitee 倉庫-雷達圖浮動面板完整程式碼