自定义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 = []
      let jgNum = 0, jgNumFlag = false // 前一个数值跟第二个数值的间隔

      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
          })


          // 防止间隔过大,导致连线的图形交叉
          if(jgNum > this.polygons.length/2) {
            jgNum = 0;
            jgNumFlag = true
            context.lineTo(center_x, center_y);
          }
          context.lineTo(x, y);
        } else {
          jgNum++
        }

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

          let inP = this.isPointInPolygon({x: center_x, y: center_y },polygon)
          console.log('inP',inP)
          if(!inP && !jgNumFlag) {
            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
            
          // p是顶点          
          if (p.x == p1.x && p.y == p1.y) {
              return boundOrVertex; //p is an vertex
          }

          p2 = polygon[i % N]; //right vertex
            
          // p射线不相交,直接跳过         
          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
          }


          // p射线跟线段相交
          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 
              // p射线跟顶点相切
               
              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>

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue3中使用echarts图表组件进行可视化的话,可以参考上篇博客中提供的echarts图表组件封装模板。这个模板可以在Vue2和Vue3中都使用,并且可以根据需要进行一些修改。[1] 具体到你提到的vue3 aqi雷达图,你可以使用echarts的雷达图组件实现。首先,你需要安装echarts库,并在你的Vue项目中引入echarts库。 然后,你可以在封装的echarts图表组件中使用option配置来绘制雷达图。option配置可以包括数据、样式、系列等内容,通过调整option配置可以实现不同的雷达图效果。 下面是一个简单的示例代码,展示如何在Vue3中使用echarts绘制aqi雷达图: ```vue <template> <div> <echarts :option="chartOption" :style="{ width: '100%', height: '400px' }" /> </div> </template> <script> import { ref } from 'vue'; import * as echarts from 'echarts'; export default { setup() { const chartOption = ref({ title: { text: 'AQI雷达图', }, radar: { indicator: [ { name: '空气质量', max: 100 }, { name: 'PM2.5', max: 100 }, { name: 'PM10', max: 100 }, { name: 'CO', max: 100 }, { name: 'O3', max: 100 }, { name: 'SO2', max: 100 }, ], }, series: [ { name: 'AQI', type: 'radar', data: [ { value: [80, 50, 70, 30, 40, 60], name: '实际值', }, ], }, ], }); return { chartOption, }; }, components: { echarts, }, }; </script> <style> </style> ``` 在上面的代码中,我们通过使用echarts库来创建一个雷达图组件。chartOption对象包含了雷达图的配置项,包括标题、雷达图的指标以及相应的数据。通过调整chartOption对象中的数据,你可以自定义雷达图的展示效果。 这个示例中只是一个简单的演示,你可以根据实际需求进行更详细的配置和美化。希望这个示例对你有帮助!<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [echarts雷达图示例](https://blog.csdn.net/m0_51431448/article/details/123947866)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值