vue封装一个雷达图组件

170 阅读5分钟

说在前面

大家对于雷达图应该都不陌生了吧,比如龙珠中这个经典的龙珠雷达,今天让我们一起来看看怎么封装一个雷达图vue组件。

image.png

效果展示

圆形网格雷达图

矩形网格雷达图

体验地址

jyeontu.xyz/jvuewheel/#…

组件实现

雷达图配置

backgroundColor

雷达图的背景色,默认为rgba(0, 0, 0, 0.8)

gridColor

网格线颜色,默认为 #2EB74E

gridType

网格类型:圆形网格: circle;正方形网格: square;默认为 circle

  • 圆形网格(circle)

  • 正方形网格(square)

gridCount

圆形网格圈数,gridTypecircle 时生效,默认为 5

  • 5圈效果

  • 10圈效果

tickCount

圆形网格刻度数,gridTypecircle 时生效,默认为 4

  • 4格效果

  • 6格效果

rowCount

正方形网格行(列)数,gridTypesquare 时生效,默认为 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,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。