说在前面
大家对于雷达图应该都不陌生了吧,比如龙珠中这个经典的龙珠雷达,今天让我们一起来看看怎么封装一个雷达图vue组件。
效果展示
圆形网格雷达图
矩形网格雷达图
体验地址
组件实现
雷达图配置
backgroundColor
雷达图的背景色,默认为rgba(0, 0, 0, 0.8)
gridColor
网格线颜色,默认为 #2EB74E
gridType
网格类型:圆形网格: circle;正方形网格: square;默认为 circle。
- 圆形网格(circle)
- 正方形网格(square)
gridCount
圆形网格圈数,gridType 为 circle 时生效,默认为 5
- 5圈效果
- 10圈效果
tickCount
圆形网格刻度数,gridType 为 circle 时生效,默认为 4。
- 4格效果
- 6格效果
rowCount
正方形网格行(列)数,gridType 为 square 时生效,默认为 10
- 10行效果
- 16行效果
scanAreaAngle
扫描区域角度,默认为30,即30度。
- 30度效果
- 60度效果
scanSpeed
扫描速度,默认为 0.01(rad),这里是通过 requestAnimationFrame 递归调用来实现扫描效果,每次移动弧度为 scanSpeed
scanColor
扫描区域颜色,默认为 ['rgba(46, 183, 78, 0.5)', 'rgba(46, 183, 78, 0.1)'] ,第一个是起始颜色,第二个是结束颜色,为渐变颜色。
- ['rgba(46, 183, 78, 0.5)', 'rgba(46, 183, 78, 0.1)']
- ["rgba(135,206,235, 0.5)", "rgba(135,206,235, 0.1)"]
- ["rgba(255,0,69, 0.5)", "rgba(255,0,69, 0.1)"]
points
雷达上的点集合,具体属性如下:
功能实现
数据初始化
initData() {
const JRadar = this.$refs.JRadar;
const boxWidth = JRadar.clientWidth;
const boxHeight = JRadar.clientHeight;
const sideLength = Math.min(boxWidth, boxHeight);
const canvas = this.$refs.canvas;
canvas.width = sideLength;
canvas.height = sideLength;
const centerX = sideLength / 2;
const centerY = sideLength / 2;
const radius = sideLength / 2; // 半径
this.sideLength = sideLength;
this.centerX = centerX;
this.centerY = centerY;
this.radius = radius;
}
根据容器大小确定雷达区域的边长、圆心坐标与半径,同时设置 canvas 元素的宽高,为后续绘图操作奠定基础。
网格绘制
- 圆形网格绘制
通过循环计算网格半径,利用ctx.arc绘制环形网格,并使用三角函数计算刻度位置。
drawCircleGird() {
const canvas = this.$refs.canvas;
const ctx = canvas.getContext("2d");
const centerX = this.centerX;
const centerY = this.centerY;
const radius = this.radius;
const gridCount = this.gridCount; // 网格数量
const gridGap = radius / gridCount; // 网格间距
for (let i = 1; i <= gridCount; i++) {
const gridRadius = gridGap * i; // 网格半径
ctx.beginPath();
ctx.arc(centerX, centerY, gridRadius, 0, Math.PI * 2, true);
ctx.strokeStyle = this.gridColor; // 网格颜色
ctx.stroke();
ctx.closePath();
}
// 绘制刻度
const tickCount = this.tickCount; // 刻度数量
const tickGap = (Math.PI * 2) / tickCount; // 刻度间距
for (let i = 0; i < tickCount; i++) {
const angle = tickGap * i; // 刻度角度
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(
centerX + Math.cos(angle) * radius,
centerY + Math.sin(angle) * radius
);
ctx.strokeStyle = this.gridColor; // 刻度颜色
ctx.stroke();
ctx.closePath();
}
},
- 方形网格绘制
通过勾股定理计算与圆相交的坐标,绘制横线与竖线构成正方形网格。
drawSquareGird() {
const canvas = this.$refs.canvas;
const ctx = canvas.getContext("2d");
const centerX = this.centerX;
const centerY = this.centerY;
const radius = this.radius;
const rowCount = this.rowCount; // 网格数量
ctx.strokeStyle = this.gridColor; // 网格颜色
const width = this.sideLength / rowCount; // 每个格子的宽度
// 绘制横线
const drawRow = (i) => {
const y = width * i;
const x = Math.sqrt(radius * radius - (centerY - y) ** 2); // 计算对应的x坐标
ctx.beginPath();
ctx.moveTo(radius - x, y);
ctx.lineTo(radius + x, y);
ctx.stroke();
ctx.closePath();
};
// 绘制竖线
const drawCol = (i) => {
const x = width * i;
const y = Math.sqrt(radius * radius - (centerX - x) ** 2); // 计算对应的y坐标
ctx.beginPath();
ctx.moveTo(x, radius - y);
ctx.lineTo(x, radius + y);
ctx.stroke();
ctx.closePath();
};
// 绘制横线
for (let i = 1; i <= rowCount; i++) {
drawCol(i);
drawRow(i);
}
},
雷达图绘制
先绘制背景圆形并填充颜色,再根据 gridType 选择对应的网格绘制方法,最后调用 drawPoints 方法绘制扫描点。
drawRadar() {
const canvas = this.$refs.canvas;
const ctx = canvas.getContext("2d");
const centerX = this.centerX;
const centerY = this.centerY;
const radius = this.radius;
// 绘制背景
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2, true);
ctx.fillStyle = this.backgroundColor;
ctx.fill();
ctx.closePath();
const map = {
circle: this.drawCircleGird,
square: this.drawSquareGird,
};
const method = map[this.gridType] || map["circle"];
method();
this.drawPoints();
},
扫描动画
通过递归调用 scanAnimation 函数,不断清除画布、重绘雷达图,并绘制扫描扇形区域。扇形区域使用线性渐变填充,模拟扫描光束的动态效果,同时更新扫描角度,确保动画流畅循环。
radarScan() {
const sideLength = this.sideLength;
const canvas = this.$refs.canvas;
const ctx = canvas.getContext("2d");
const centerX = this.centerX;
const centerY = this.centerY;
const radius = this.radius;
let angle = 0; // 初始角度
const scanSpeed = this.scanSpeed; // 扫描速度
const scanAnimation = () => {
ctx.clearRect(0, 0, sideLength, sideLength); // 清除画布
this.drawRadar(); // 重新绘制背景
ctx.beginPath();
ctx.moveTo(centerX, centerY); // 移动到圆心
ctx.arc(
centerX,
centerY,
radius,
angle,
angle + Math.PI / (180 / this.scanAreaAngle),
false
); // 绘制扇形区域
//填充渐变色且半透明
const gradient = ctx.createLinearGradient(
centerX,
centerY,
centerX + Math.cos(angle) * radius,
centerY + Math.sin(angle) * radius
);
gradient.addColorStop(0, this.scanColor[0]); // 起始颜色
gradient.addColorStop(1, this.scanColor[1]); // 结束颜色
ctx.fillStyle = gradient; // 填充渐变色
ctx.fill(); // 填充扇形区域
ctx.closePath(); // 关闭路径
angle += scanSpeed; // 更新角度
if (angle >= Math.PI * 2) {
angle = 0; // 重置角度
}
this.scanAngle = angle;
requestAnimationFrame(scanAnimation); // 递归调用
};
scanAnimation(); // 开始扫描动画
}
绘制雷达坐标点
遍历points数组,根据每个点的属性信息,计算点的角度并判断是否处于扫描区域内,若在区域内则绘制该点,实现扫描过程中点的动态显示效果。
drawPoints() {
const canvas = this.$refs.canvas;
const ctx = canvas.getContext("2d");
const centerX = this.centerX;
const centerY = this.centerY;
const points = this.points;
points.forEach((point) => {
const {
x,
y,
radius = "5",
color = "green",
isScanShow = true,
} = point;
if (isScanShow) {
let angle = Math.atan2(y, x); // 计算角度
angle += Math.PI * 2; // 调整角度
angle %= Math.PI * 2; // 将角度转换为0到2π之间的值
//处于扫描区域内才显示
if (
angle < this.scanAngle ||
angle >
this.scanAngle +
Math.PI / (180 / this.scanAreaAngle)
) {
return; // 跳过绘制
}
}
ctx.beginPath();
ctx.arc(centerX + x, centerY + y, radius, 0, Math.PI * 2, true); // 绘制圆形
ctx.fillStyle = color; // 填充颜色
ctx.fill(); // 填充圆形
ctx.closePath(); // 关闭路径
});
}
组件使用
简单示例如下图,更多具体配置属性可以查看组件文档。
<template>
<div class="content" style="width: 100%; padding: 1rem">
<div class="content-btns">
<div
@click="gridType = 'circle'"
:class="{
'content-btns-btn': true,
'content-btns-btn-active': gridType === 'circle',
}"
>
圆形网格
</div>
<div
@click="gridType = 'square'"
:class="{
'content-btns-btn': true,
'content-btns-btn-active': gridType === 'square',
}"
>
正方形网格
</div>
</div>
<JRadar
class="j-radar"
:gridType="gridType"
:points="points"
v-if="gridType === 'circle'"
>
</JRadar>
<JRadar
class="j-radar"
:gridType="gridType"
:points="points"
v-else
>
</JRadar>
</div>
</template>
<script>
export default {
data() {
return {
gridType: "circle", // 圆形网格: circle, 正方形网格: square
points: [
{ x: -50, y: -50 },
{ x: 50, y: 50, isScanShow: false, color: "red", radius: 8 },
{ x: 80, y: 80 },
{ x: 20, y: -20 },
{ x: -30, y: 30 },
]
};
},
}
</script>
组件库
组件文档
目前该组件也已经收录到我的组件库,组件文档地址如下: jyeontu.xyz/jvuewheel/#…
组件内容
组件库中还有许多好玩有趣的组件,如:
- 虚拟滚动列表
- 评论组件
- 词云组件
- 瀑布流照片容器
- 视频动态封面
- 3D轮播图
- web桌宠
- 贡献度面板
- 拖拽上传
- 自动补全输入框
- 带@功能的输入框组件
等等……
组件库源码
组件库已开源到gitee,有兴趣的也可以到这里看看:gitee.com/zheng_yongt…
-
🌟觉得有帮助的可以点个star~
-
🖊有什么问题或错误可以指出,欢迎pr~
-
📬有什么想要实现的组件或想法可以联系我~
公众号
关注公众号『 前端也能这么有趣 』,获取更多有趣内容。
发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~
说在后面
🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『
前端也能这么有趣
』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。