超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

Canvas 实例之绘制雷达图(蛛网图)

之前在开发中需要用到雷达图(蛛网图),这里总结下使用 canvas 绘制雷达图的步骤。

对于雷达图这种中心对称的图形,绘制的要点就是找到圆心的位置,设为中心点,然后边旋转边绘制。

演示:

See the Pen canvas draw radar by tcatche (@tcatche) on CodePen.

下面来拆分一下绘制步骤:

接口定义

首先先定义接口和使用:

class Rader {
draw() {
this.drawGrid(); // 绘制网格
this.drawLines(); // 绘制链接网格的线
this.drawData(); // 绘制数据组成的多边形
this.drawText(); // 绘制标签
}
}

// 使用方式
new Rader({
ele: document.getElementById('canvas'),
radius: 100, // 绘制半径
gridTurns: 5, // 蛛网的圈数
data: [ // 根据 data 长度决定绘制几边形
{ label: '速度', value: 60, },
{ label: '力量', value: 50, },
{ label: '爆发力', value: 70, },
{ label: '耐久力', value: 60, },
{ label: '强度', value: 80, },
{ label: '气势', value: 10, },
]
}).draw();

核心语法

涉及到的核心语法主要有以下几个:

  • ctx.translate 更改 canvas 的原点,本例设置为多边形的中心点。
  • ctx.rotate 旋转 casvas,本例通过旋转每次只需要绘制固定坐标 [width, 0] 可以有效降低计算坐标的复杂度;
  • ctx.save 保存 canvas 全部状态,由于每次绘制都会进行旋转,每次绘制前保存状态,避免多次旋转后角度错乱导致错位;
  • ctx.restore 还原上次保存的状态,每次绘制结束后进行还原;
  • ctx.beginPath/ctx.closePath 用来关闭路径,避免不同的路径进行交叉;
  • ctx.moveTo/ctx.lineTo 用来绘制一段直线。

初始化

constructor(options) {
this.options = options;
this.ctx = options.ele.getContext('2d');
this.ctx.translate(options.ele.width / 2, options.ele.height / 2); // 指定绘制的原点为 canvas 的中心
this.edgesCount = options.data.length; // 几边形由数据的长度确定
this.rotateStep = (2 * Math.PI) / this.edgesCount; // 每次旋转的度数,注意是弧度单位而不是角度。
}

上面初始化主要指定了绘制的原点,方便计算每个要绘制的点,以及每次需要旋转的弧度

下面开始具体图案的绘制,其实每次绘制都很类似。

绘制背景网格

绘制效果

具体代码

drawGrid() {
const { options, ctx, edgesCount, rotateStep } = this;
ctx.save();
ctx.beginPath();
for (let i = 0; i < options.gridTurns; i += 1) { // 根据网格的圈数进行循环
const DRAW_RADIUS = ((i + 1) * options.radius) / options.gridTurns; // 计算每一圈网格的半径
ctx.moveTo(DRAW_RADIUS, 0);

for (let j = 0; j < edgesCount; j += 1) { // 每一圈网格都是一个绘制小多边形的步骤
ctx.rotate(rotateStep);
ctx.lineTo(DRAW_RADIUS, 0);
}
}
ctx.closePath();
ctx.stroke();
ctx.restore();
}

绘制网格连接的线

绘制效果

具体代码

drawLines() {
const { options, ctx, edgesCount, rotateStep } = this;
ctx.save();
ctx.beginPath();
for (let i = 0; i < edgesCount; i += 1) {
ctx.rotate(rotateStep);
ctx.moveTo(0, 0);
ctx.lineTo(options.radius, 0);
}
ctx.closePath();
ctx.stroke();
ctx.restore();
}

绘制数据

绘制效果

具体代码

drawData() {
const { options, ctx, edgesCount, rotateStep } = this;
ctx.save();
const data = options.data.map((item) => Math.round(item.value * options.radius / 100)); // 根绝数据的百分比确定数据的绘制长度
ctx.beginPath();
ctx.moveTo(data[0], 0);
for (let i = 1; i < edgesCount; i += 1) {
ctx.rotate(rotateStep);
ctx.lineTo(data[i], 0);
}
ctx.rotate(this.rotateStep);
ctx.lineTo(data[0], 0);
ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
ctx.fill();
ctx.closePath();
ctx.restore();
}

绘制标签

绘制效果

具体代码:

drawText() {
const { options, ctx, edgesCount, rotateStep } = this;
ctx.save();
const data = options.data.map((item) => item.label);
ctx.font = '12px';
ctx.textBaseline = 'middle';
ctx.textAlign = 'center'
const textRadius = options.radius + 20;
for (let i = 0; i < edgesCount; i += 1) {
const currentAngle = rotateStep * i;
ctx.fillText(data[i], textRadius * Math.cos(currentAngle), textRadius * Math.sin(currentAngle));
}
ctx.restore();
}
}

绘制标签的难点是需要计算标签的文本的坐标,这里用到正弦和余弦函数 x = cos(deg) * r, y = sin(deg) * r

注意,每次绘制前使用 ctx.save() 保存当前状态,绘制完成后使用 ctx.restore() 恢复保存的状态,这样可以避免绘制几次后找不到初始的状态。上面的每个绘制都是如此操作的。

完整代码和演示

点击查看完整代码

演示:

See the Pen canvas draw radar by tcatche (@tcatche) on CodePen.