使用canvas实现分层金字塔图

762 阅读4分钟

前言

公司内有个项目,统计信息的时候参考了某测评小程序,大概图例如下

微信截图_20240428215828.png

这里左边的相对来说好实现,直接使用 echart 或者 uchart 即可实现,而右边这个三角金字塔对比了下也和echart金字塔形漏斗图业务上也不一样,就单纯是一个层级样式罢了。

设计实现

原本以为简单使用几个div + css裁剪实现的,搜寻一番发现大部分绘制三角形梯形都是基于border-width来实现的, 链接再下方:

摘要一下要点,主要是通过border-widthlinear-gradient + clip-path 切出形状

.sj{
    margin: 0 auto;
    height: 0;
    border-top: 0 solid transparent;
    border-right: 30px solid transparent;
    border-left: 30px solid transparent;
    border-bottom-width: 60px;
    border-bottom-style: solid;
    box-sizing: content-box;
}
background: 
    linear-gradient(to bottom, #cc7497 85%, transparent 95%,transparent 0) center 0%, 
    linear-gradient(to bottom, #182843 85%, transparent 95%,transparent 0) center 25%,
    linear-gradient(to bottom, #2e6c8e 85%, transparent 95%,transparent 0) center 50%, 
    linear-gradient(to bottom, #0084a4 85%, transparent 95%,transparent 0) center 75%,
    linear-gradient(to bottom, #018d9c 85%, transparent 95%,transparent 0) center 100%;
    background-size: 100% 20%; background-repeat: no-repeat;
    clip-path: polygon(50% 0, 100% 100%, 0% 100%);

canvas实现

常规操作,看到什么就绘制什么。要点就是取得中心线顶端,然后一层一层往下绘制(你也可以往上绘制)

  • 第一个形状是三角形
  • 往后就是梯形了

1.绘制三角形和梯形相关代码

// 绘制带背景和边框的三角形
function drawTriangleWithBackgroundAndBorder() {
    // 绘制背景填充
    ctx.beginPath();
    ctx.moveTo(triangleVertices[0].x, triangleVertices[0].y);
    ctx.lineTo(triangleVertices[1].x, triangleVertices[1].y);
    ctx.lineTo(triangleVertices[2].x, triangleVertices[2].y);
    ctx.closePath();
    ctx.fillStyle = "lightblue"; // 设置背景颜色
    ctx.fill();
    
    ctx.strokeStyle = "blue"; // 设置边框颜色
    ctx.lineWidth = 2; // 设置边框宽度
    ctx.stroke();
}


// 绘制带背景和边框的梯形
function drawTrapezoidWithBackgroundAndBorder() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 绘制背景填充
    ctx.beginPath();
    ctx.moveTo(trapezoidVertices[0].x, trapezoidVertices[0].y);
    ctx.lineTo(trapezoidVertices[1].x, trapezoidVertices[1].y);
    ctx.lineTo(trapezoidVertices[2].x, trapezoidVertices[2].y);
    ctx.lineTo(trapezoidVertices[3].x, trapezoidVertices[3].y);
    ctx.closePath();
    ctx.fillStyle = "lightgreen"; // 设置背景颜色
    ctx.fill();
    
    ctx.strokeStyle = "green"; // 设置边框颜色
    ctx.lineWidth = 2; // 设置边框宽度
    ctx.stroke();
}

2.层级绘制要点


let pyramidTopX = canvas.width / 2; // 金字塔顶点的 x 坐标
let pyramidTopY = 50; // 金字塔顶点的 y 坐标
// topY = 初始Y
let topY = 0 + pyramidTopY
let offsetHeight = 150; // 每一层的高度
let offsetHalf = offsetHeight / 2 // 半高

// 循环层级
for(let i = 0; i < pyramidData.length; i ++) {
    let offsetWidth = i * offsetHalf // 累计往下增加宽度
    // 间隔10
    topY += 5
    // todo 计算坐标点绘制图形,从pyramidTopX向左向右定义对应offsetWidth的坐标(x,y)
}

这里需要注意的是,我没有处理间隔之后上一个和下一个之间 offsetWidth 的小错位

具体的优化各位看官自己想想怎么处理咯,反正我这是能用就行了 :)

!!!!!!

其实想了想,中间的gap可以直接用纯色背景的梯形来实现,这样就不用处理间隔之间那个宽度差异了 [捂脸]

QQ截图20240428223147.png

完整测试代码

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<title>Canvas 绘制三角层级金字塔</title>
		<style>
			canvas {
				border: 1px solid black;
			}
		</style>
	</head>
	<body>
		<canvas id="myCanvas" width="800" height="800"></canvas>

		<script>
			var canvas = document.getElementById("myCanvas");
			var ctx = canvas.getContext("2d");
			
			// 绘制 - 循环绘制
			const pyramidData = [
				{
					name: '高等水平',
					actived: false
				},{
					name: '中等水平',
					actived: false
				},{
					name: '基本达标',
					actived: true
				},{
					name: '落后水平',
					actived: false
				},
			]
			/************************/
			let pyramidTopX = canvas.width / 2; // 金字塔顶点的 x 坐标
			let pyramidTopY = 50; // 金字塔顶点的 y 坐标
			// topY
			let topY = 0 + pyramidTopY
			let offsetHeight = 150; // 每一层的高度
			let offsetHalf = offsetHeight / 2

			function transferRgba(originalColor, alpha = 0.1) {
				// 提取 RGB 分量
				var r = parseInt(originalColor.substr(1, 2), 16);
				var g = parseInt(originalColor.substr(3, 2), 16);
				var b = parseInt(originalColor.substr(5, 2), 16);
				
				// 降低亮度
				var factor = 0.9; // 0.1 色度
				r *= factor;
				g *= factor;
				b *= factor;
				
				// 确保在 0 到 255 范围内
				r = Math.round(r);
				g = Math.round(g);
				b = Math.round(b);
				
				// 转换为 RGBA 格式
				return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")";
			}
			
			let fillColor = transferRgba('#EE6666')
			let strokeColor = '#EE6666'
		
			// 绘制文字
			ctx.font = `normal 500 32px Source Han Sans CN`;
			ctx.fillStyle = '#333333';
			ctx.textAlign = 'center';
			ctx.textBaseline = 'middle';
			// 绘制形状
			for(let i = 0; i < pyramidData.length; i ++) {
				let offsetWidth = i * offsetHalf
				// 间隔10
				topY += 5
				
				var text = pyramidData[i].name;
				if (i == 0) {
					// 左边的点需要往下一移动
					topY += offsetHeight
					offsetWidth = offsetHalf
					// 绘制背景填充
					ctx.beginPath();
					ctx.moveTo(pyramidTopX, pyramidTopY);
					ctx.lineTo(pyramidTopX - offsetWidth, topY);
					ctx.lineTo(pyramidTopX + offsetWidth, topY);
					ctx.closePath();
					ctx.fillStyle = (pyramidData[i].actived ? strokeColor : fillColor) || "lightblue"; // 设置背景颜色
					ctx.fill();
					ctx.strokeStyle = strokeColor || "green"; // 设置边框颜色
					ctx.lineWidth = 2; // 设置边框宽度
					ctx.stroke();
					// 绘制文字
					ctx.fillStyle ='black';
					ctx.fillText(text, pyramidTopX, topY - offsetHalf);
				} else {
					// 绘制边框
					ctx.beginPath();
					ctx.moveTo(pyramidTopX - offsetWidth, topY);
					ctx.lineTo(pyramidTopX + offsetWidth, topY);
					topY += offsetHeight
					offsetWidth += offsetHalf
					ctx.lineTo(pyramidTopX + offsetWidth, topY);
					ctx.lineTo(pyramidTopX - offsetWidth, topY);
					ctx.closePath();
					ctx.fillStyle = (pyramidData[i].actived ? strokeColor : fillColor) || "lightblue"; // 设置背景颜色
					ctx.fill();
					ctx.strokeStyle = strokeColor || "green"; // 设置边框颜色
					ctx.lineWidth = 2; // 设置边框宽度
					ctx.stroke();
					// 绘制文字
					ctx.fillStyle ='black';
					ctx.fillText(text, pyramidTopX, topY - offsetHalf);
				}
			}
		</script>
	</body>
</html>

QQ截图20240428222722.png