当前位置: 首页 > news >正文

自定义vue项目的雷达图组件

主要思路:

利用canvas,通过传入一组数组数据,根据数组数据的个数,自动生成一个多边形的雷达图形,并在对应的坐标点绘制。

还有一个难点,就是需要计算原点是否在有数值的几个点连成的图形中,如果在图形中,则不连接原点,如果是在图形外,则要连接原点的坐标形成新的图形。

这里判断原点是否在图形中,用的基本思想是利用射线法,以被测点Q为端点,向任意方向作射线(一般水平向右作射线),计算射线与多边形各边的交点,如果是偶数,则点在多边形外,否则在多边形内。还会考虑一些特殊情况,如点在多边形顶点上,点在多边形边上等特殊情况。

判断算法描述如下:首先,对于多边形的水平边不作考虑;其次,对于多边形的顶点和射线相交的情况,如果该顶点是其所属的边上纵坐标较大的顶点,则计数,否则忽略该点;最后,对于Q在多边形边上的情形,直接可以判断Q属于多边形

参考:http://paulbourke.net/geometry/insidepoly/

目前可传入的配置参数,后续可根据项目的需求自由添加实现新的配置参数:

areaColor:  ’rgba(140,144,220,0.55)  // 雷达数据坐标点围起来的图形的填充颜色
segmentLineColor: '#d8d8d8' // 原点到数据坐标点的连线的颜色
diagonalLineColor: 'rgba(216,216,216,0.4)' // 雷达对角线的颜色
numberLineColor: ‘rgba(140,144,220,0.3)’ // 雷达线的颜色
axisTextColor: ‘rgba(140,144,220,0.8)’ // 数值文字的颜色
edgeNumber: 4 // 分割线的数量
textSize: 16 // 文字的尺寸大小
textSpace: 12 // 文字的间距大小
polygons: [ 0.1, 0.2 ] // 雷达坐标数值的数组(小数0-1)
texts: [ '1', '2' ] // 雷达坐标标题的数组
fontSize: 14 // 字体字号大小
pointColor: '#d5d6f0' // 小圆点颜色
showNumber: true // 显示数值

组件代码:

polygon-custom.vue

<template>

  <div class="polygon-container">
    <canvas ref="polygon" id='polygon' class='polygon' width="100" height="100"></canvas>
  </div>

</template>


<style lang="less" scoped>

  .polygon-container {
    .polygon {
      display: block;
      width: 3rem;
      height: 3rem;
      margin: 0 auto;
    }
  }
  
</style>
<script>
let context = null;
let canvasWidth = 0;
let canvasHeight = 0;
let TEXT_SPACE = 12;

export default {
  /**
   * 组件的属性列表
   */
  props: {
    areaColor: { // 雷达数据坐标点围起来的图形的填充颜色
      type: String,
      default: "rgba(140,144,220,0.55)",
    },
    segmentLineColor: { // 原点到数据坐标点的连线的颜色
      type: String,
      default: "#d8d8d8",
    },
    diagonalLineColor: { // 雷达对角线的颜色
      type: String,
      default: "rgba(216,216,216,0.4)",
    },
    numberLineColor: { // 雷达线的颜色
      type: String,
      default: "rgba(140,144,220,0.3)",
    },
    axisTextColor: { // 数值文字的颜色
      type: String,
      default: "rgba(140,144,220,0.8)",
    },
    edgeNumber: { // 分割线的数量
      type: Number,
      default: 4,
    },
    textSize: { // 文字的尺寸大小
      type: Number,
      default: 16,
    },
    textSpace: { // 文字的间距大小
      type: Number,
      default: TEXT_SPACE,
    },
    polygons: { // 雷达坐标数值的数组(小数0-1)
      type: Array,
      default: () => [],
    },
    texts: { // 雷达坐标标题的数组
      type: Array,
      default: () => [],
    },
    fontSize: { // 字体字号大小
      type: Number,
      default: 14
    },
    pointColor: { // 小圆点颜色
      type: String,
      default: '#d5d6f0'
    },
    showNumber: { // 显示数值
      type: Boolean,
      default: true
    }
  },

  /**
   * 组件的初始数据
   */
  data: {},

  mounted () {
    this.$nextTick(() => {
      setTimeout(() => {
        const canvas = this.$refs['polygon'];

        context = canvas.getContext('2d');
        canvas.width = canvas.offsetWidth
        canvas.height = canvas.offsetHeight
        canvasWidth = canvas.offsetWidth;
        canvasHeight =  canvas.offsetHeight;
        this.run();
      }, 600)
      
    })
    

  },

  /**
   * 组件的方法列表
   */
  methods: {

    run() {
      if (this.polygons.length < 3) {
        return;
      }

      if (this.texts.length < this.polygons.length) {
        for (let i = this.polygons.length; i >= this.texts.length; i-- ) {
          this.texts.push("空");
        }
      }

      var center_x = canvasWidth / 2;
      var center_y = canvasHeight / 2;
      var radius = ((canvasWidth > canvasHeight ? canvasHeight : canvasWidth) - 2 * this.textSpace - this.textSize * 2) / 2;
      var innerAngle = 360 / this.polygons.length;

      this.drawSegmentLine(context, center_x, center_y, radius, innerAngle);
      this.drawDiagonalLine(context, center_x, center_y, radius, innerAngle);
      this.drawNumberLine(context, center_x, center_y, radius, innerAngle);
      this.drawAxisText(context, center_x, center_y, radius, innerAngle);
      this.drawShadowArea(context, center_x, center_y, radius, innerAngle);
      
    },


    // 画雷达图
    drawShadowArea(context, center_x, center_y, radius, innerAngle) {
      
      context.fillStyle = this.areaColor;
      context.strokeStyle = this.segmentLineColor;
      context.lineWidth = 1;
      
      context.beginPath();

      let polygon = []

      for (let i = 0; i < this.polygons.length; i++) {
        var f = this.polygons[i];

        if (f > 1) {
          f = 1;
        }

        if (f < 0) {
          f = 0;
        }

        var currentRadius = radius * f;

        var x = center_x - Math.cos(this.angleToRadian(90 - innerAngle * i)) * currentRadius;
        var y = center_y - Math.sin(this.angleToRadian(90 - innerAngle * i)) * currentRadius;
        
        if (currentRadius !== 0) {
        
          polygon.push({
            x, y
          })

          context.lineTo(x, y);
        } 

        if(i == this.polygons.length - 1) {

          let inP = this.isPointInPolygon({x: center_x, y: center_y },polygon)
          console.log('inP',inP)
          if(!inP) {
            context.lineTo(center_x, center_y);
          }
          
        }
      }

      context.closePath();
      context.fill();
    },


    // 画分割线
    drawSegmentLine(context, center_x, center_y, radius, innerAngle) {
      
      context.strokeStyle = this.segmentLineColor;

      for (let i = 0; i <= this.edgeNumber; i++) {
        context.lineWidth = 1;
        context.beginPath();

        for (let j = 0; j < this.polygons.length; j++) {
          var x = center_x - Math.cos(this.angleToRadian(90 - innerAngle * j)) * ((radius * i) / this.edgeNumber);
          var y = center_y - Math.sin(this.angleToRadian(90 - innerAngle * j)) * ((radius * i) / this.edgeNumber);

          context.lineTo(x, y);
        }
        context.closePath();
        context.stroke();
      }
    },


    // 画雷达数据线,即原点到数据点的连线
    drawNumberLine(context, center_x, center_y, radius, innerAngle) {
      for (let j = 0; j < this.polygons.length; j++) {
        context.strokeStyle = this.numberLineColor;
        if (this.polygons[j]) {
          let temp_x = center_x - Math.cos(this.angleToRadian(90 - innerAngle * j)) * this.polygons[j] * radius;
          let temp_y = center_y - Math.sin(this.angleToRadian(90 - innerAngle * j)) * this.polygons[j] * radius;

          context.beginPath();
          context.moveTo(center_x, center_y);
          context.lineTo(temp_x, temp_y);
          context.closePath();
          context.stroke();
          // 画小圆形
          context.strokeStyle = this.pointColor;
          context.beginPath();
          context.arc(temp_x, temp_y, 2, 0, 2 * Math.PI);
          context.closePath();
          context.fill();

          if(this.showNumber) {
            // 写数值
            var text_size = 10
            var text_x = center_x - Math.cos(this.angleToRadian(90 - innerAngle * j)) * (this.polygons[j] * radius + text_size / 2);
            var text_y = center_y - Math.sin(this.angleToRadian(90 - innerAngle * j)) * (this.polygons[j] * radius + this.textSize / 2);

            context.beginPath();
            context.font = text_size+'px'; // 字体大小 注意不要加引号
            context.fillStyle = this.axisTextColor; // 字体颜色
            context.textAlign = "center"; // 字体位置
            context.textBaseline = "middle"; // 字体对齐方式
            context.fillText(Math.floor(this.polygons[j]*100)+"%", text_x, text_y); // 文字内容和文字坐标
            context.closePath();
          }
        }
      }
    },


    // 画对角线
    drawDiagonalLine(context, center_x, center_y, radius, innerAngle) {
      
      context.strokeStyle = this.diagonalLineColor;
      context.lineWidth = 0.5;
      for (let j = 0; j < this.polygons.length; j++) {
        context.setLineDash([2, 6]);
        context.beginPath();
        context.lineTo(center_x, center_y);
        var x = center_x - Math.cos(this.angleToRadian(90 - innerAngle * j)) * radius;
        var y = center_y - Math.sin(this.angleToRadian(90 - innerAngle * j)) * radius;
        context.lineTo(x, y);
        context.closePath();
        context.stroke();
      }
      context.setLineDash([]);
    },


    // 写文字
    drawAxisText(context, center_x, center_y, radius, innerAngle) {
      
      for (let j = 0; j < this.texts.length; j++) {
        let text = this.texts[j];
        context.lineTo(center_x, center_y);
        var x = center_x - Math.cos(this.angleToRadian(90 - innerAngle * j)) * (radius + this.textSpace + this.textSize / 2);
        var y = center_y - Math.sin(this.angleToRadian(90 - innerAngle * j)) * (radius + this.textSpace + this.textSize / 2);

        context.beginPath();
        context.font = this.fontSize + 'px'; // 字体大小 注意不要加引号
        context.fillStyle = this.axisTextColor; // 字体颜色
        context.textAlign = "center"; // 字体位置
        context.textBaseline = "middle"; // 字体对齐方式
        context.fillText(text, x, y); // 文字内容和文字坐标
      }
    },

    angleToRadian(angle) {
      return ((2 * Math.PI) / 360) * angle;
    },

    // 判断点是否在平面中
    isPointInPolygon(point, polygon) {

      // 下述代码来源:http://paulbourke.net/geometry/insidepoly/,进行了部分修改
      // 基本思想是利用射线法,计算射线与多边形各边的交点,如果是偶数,则点在多边形外,否则在多边形内。还会考虑一些特殊情况,如点在多边形顶点上,点在多边形边上等特殊情况。

      var N = polygon.length;
      var boundOrVertex = true; // 如果点位于多边形的顶点或边上,也算做点在多边形内,直接返回true
      var intersectCount = 0; // cross points count of x 
      var precision = 2e-10; // 浮点类型计算时候与0比较时候的容差
      var p1, p2; // neighbour bound vertices
      var p = point; // 测试点

      p1 = polygon[0]; //left vertex        
      for (var i = 1; i <= N; ++i) { //check all rays            
          if (p.x == p1.x && p.y == p1.y) {
              return boundOrVertex; //p is an vertex
          }

          p2 = polygon[i % N]; //right vertex            
          if (p.y < Math.min(p1.y, p2.y) || p.y > Math.max(p1.y, p2.y)) { //ray is outside of our interests                
              p1 = p2;
              continue; //next ray left point
          }

          if (p.y > Math.min(p1.y, p2.y) && p.y < Math.max(p1.y, p2.y)) { //ray is crossing over by the algorithm (common part of)
              if (p.x <= Math.max(p1.x, p2.x)) { //x is before of ray                    
                  if (p1.y == p2.y && p.x >= Math.min(p1.x, p2.x)) { //overlies on a horizontal ray
                      return boundOrVertex;
                  }

                  if (p1.x == p2.x) { //ray is vertical                        
                      if (p1.x == p.x) { //overlies on a vertical ray
                          return boundOrVertex;
                      } else { //before ray
                          ++intersectCount;
                      }
                  } else { //cross point on the left side                        
                      var xinters = (p.y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y) + p1.x; // x轴方向上射线与p1,p2线段的交点的x坐标
                

                      // 判断p点的x坐标是否与交点的x坐标重合,允许有误差值precision
                      if (Math.abs(p.x - xinters) < precision) {
                          return boundOrVertex;
                      }

                      if (p.x < xinters) { //before ray
                          ++intersectCount;
                      }
                  }
              }
          } else { // special case when ray is crossing through the vertex                
              if (p.y == p2.y && p.x <= p2.x) { //p crossing over p2                    
                  var p3 = polygon[(i + 1) % N]; //next vertex                    
                  if (p.y >= Math.min(p1.y, p3.y) && p.y <= Math.max(p1.y, p3.y)) { //p.y lies between p1.y & p3.y
                      ++intersectCount;
                  } else {
                      intersectCount += 2;
                  }
              }
          }
          p1 = p2; //next ray left point
      }

      if (intersectCount % 2 == 0) { // 偶数在多边形外
          return false;
      } else { // 奇数在多边形内
          return true;
      }
    }
  },
}
</script>

组件调用: 

<template>
    <PolygonCustom class='polygon' :polygons='polygons' :texts='texts' :fontSize="10" :areaColor="'rgba(90, 205, 199, 0.35)'" :segmentLineColor="'rgba(90, 205, 199, 0.37)'" :axisTextColor="'#39D1CA'" :edgeNumber="10" :textSize="30" :textSpace="1" :pointColor="'#5ACDC7'" :showNumber="false"></PolygonCustom>
</template>


<script>

import PolygonCustom from 'xxx'

export default {

    data() {
        retutn {
            polygons: [], // [0.85, 0, 0.65, 0, 0.8, 0.9, 1, 0.7],
            texts: [], // ['湿热质', '气虚质', '气郁质', '平和质', '痰湿质', '气郁质', '平和质', '痰湿质'],
        }
    },
    
    components: {
        PolygonCustom
    }

}

</script>

相关文章:

  • CREO:CREO软件之工程图【表】之一明细表、表格创建、创建BOM球标、自动生成零件报表的简介及其使用方法(图文教程)之详细攻略
  • 接口测试实战 | Android 高版本无法抓取 HTTPS,怎么办?
  • 嵌入式开发:清理可重用软件的API
  • Windows与网络基础-3-虚拟机安装Kali Linux
  • 面试题-谈谈你对JVM的理解
  • 面试时Dubbo原理记不住?来看看《Dubbo原理浅析——从RPC本质看Dubbo》
  • 开源治理:安全的关键
  • 什么是快应用?与原生APP相比优势在哪里
  • 卷积神经网络结构有哪些,卷积神经网络结构特点
  • 阿里内部首发面试终极指南V3.0,相对一线大厂面试知识点+面试题
  • vue路由原理
  • idea常用快捷键和插件
  • 04_feign介绍(OpenFeign)
  • (已更新)关于Visual Studio 2019安装时VS installer无法下载文件,进度条为0,显示网络有问题的解决办法
  • 长安链源码学习v2.2.1--ioc机制(九)
  • 【MySQL经典案例分析】 Waiting for table metadata lock
  • 2017-09-12 前端日报
  • 5分钟即可掌握的前端高效利器:JavaScript 策略模式
  • CentOS7 安装JDK
  • ES6系列(二)变量的解构赋值
  • JavaScript设计模式与开发实践系列之策略模式
  • javascript数组去重/查找/插入/删除
  • java取消线程实例
  • 测试开发系类之接口自动化测试
  • 从重复到重用
  • 二维平面内的碰撞检测【一】
  • 利用DataURL技术在网页上显示图片
  • 实现菜单下拉伸展折叠效果demo
  • 教程:使用iPhone相机和openCV来完成3D重建(第一部分) ...
  • 数据可视化之下发图实践
  • #Ubuntu(修改root信息)
  • (11)MATLAB PCA+SVM 人脸识别
  • (Forward) Music Player: From UI Proposal to Code
  • (附程序)AD采集中的10种经典软件滤波程序优缺点分析
  • (附源码)ssm失物招领系统 毕业设计 182317
  • (译)2019年前端性能优化清单 — 下篇
  • (原創) 如何讓IE7按第二次Ctrl + Tab時,回到原來的索引標籤? (Web) (IE) (OS) (Windows)...
  • ... fatal error LINK1120:1个无法解析的外部命令 的解决办法
  • .NET 4.0中的泛型协变和反变
  • .net 受管制代码
  • .NET命令行(CLI)常用命令
  • .NET设计模式(2):单件模式(Singleton Pattern)
  • @GlobalLock注解作用与原理解析
  • @SuppressLint(NewApi)和@TargetApi()的区别
  • [ vulhub漏洞复现篇 ] Grafana任意文件读取漏洞CVE-2021-43798
  • []C/C++读取串口接收到的数据程序
  • [AHOI2009]中国象棋 DP,递推,组合数
  • [C++]:for循环for(int num : nums)
  • [C++]Leetcode17电话号码的字母组合
  • [codeforces] 25E Test || hash
  • [CSS]浮动
  • [git] windows系统安装git教程和配置
  • [GXYCTF2019]禁止套娃
  • [LeetBook]【学习日记】获取子字符串 + 颠倒子字符串顺序
  • [LeetBook]【学习日记】数组内乘积