前言
公司内有个项目,统计信息的时候参考了某测评小程序,大概图例如下
这里左边的相对来说好实现,直接使用 echart 或者 uchart 即可实现,而右边这个三角金字塔对比了下也和echart金字塔形漏斗图业务上也不一样,就单纯是一个层级样式罢了。
设计实现
原本以为简单使用几个div + css裁剪实现的,搜寻一番发现大部分绘制三角形和梯形都是基于border-width来实现的, 链接再下方:
摘要一下要点,主要是通过border-width 和 linear-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可以直接用纯色背景的梯形来实现,这样就不用处理间隔之间那个宽度差异了
完整测试代码
<!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>