echarts像素坐标逻辑坐标转换、根据角度绘制射线、绘制多边形【私藏,其他人无参考意义】

182 阅读2分钟
<template>
  <div class="TuBanChart">
    <div :id="chartConfigs.chartId" :style="{'width':chartConfigs.width+'px','height': chartConfigs.height+'px','margin':'0 auto'}"></div>
  </div>
</template>
<script>
import { nanoid } from "nanoid";
import {jsjlLegends as legends} from './legends.js';
import _ from "lodash";
export default {
  props:{
    chartConfigs:{
      type: Object,
      required: true,
      default: ()=>({})
    },
    targetWell:{ // 需要高亮闪烁的目标井
      type:String,
      default:''
    },
    valueZoneDisable:{ // 是否开启价值区
      type:Boolean,
      default:true
    },
    isDefineValueZone:{ // 是否允许编辑价值区
      type:Boolean,
      default:true
    },
    isEditValueZone:{ // 是否允许删除价值区
      type:Boolean,
      default:true
    },
  },
  watch:{
    chartConfigs:{
      handler(val){
        if(val.chartData){
          this.handleChartData(val.chartData)
        }
      },
      deep:true
    },
    isDefineValueZone(val){
      if(val)this.areaPoints = [];
    }
  },
  data(){
    return {
      option: {
        grid:{
          left:60,
          bottom:30,
          containLabel:true
        },
        legend:{
          show:true,
          bottom:0,
          itemWidth: 20,
          itemHeight: 20
        },
        xAxis: [],
        yAxis: [],
        tooltip:{
          show:true,
          formatter:v=>{
            if(v.componentSubType.includes('scatter') || v.componentSubType.includes('effectScatter')){
              if(!v.seriesName) return;
              let myChart = this.$echarts.init(document.getElementById(this.chartConfigs.chartId));
              const {xAxis,yAxis} = this.option;
              let point = myChart.convertToPixel({ seriesIndex: 0 }, v.data); //当前点逻辑坐标转像素坐标
              const originPoint = [xAxis[0].min,yAxis[0].min]; //原点逻辑坐标
              const originPoint1 = myChart.convertToPixel({ seriesIndex: 0 }, originPoint); //原点像素坐标
              const w1 = point[0] - originPoint1[0]; //当前点距原点的水平像素距离
              const h1 = originPoint1[1] - point[1]; //当前点距原点的垂直像素距离
              // Z轴预测
              let Z = '';
              const {min, max} = this.zAxis;
              // 当前点与X轴夹角
              let angle = Math.round(Math.atan(h1 / w1) * 180 / Math.PI);
              let lines = this.option.series.filter(it=>it.type === 'line');
              let angles = [...lines.map(it=>it.angle), angle].sort((a,b) => a - b);
              let index = angles.indexOf(angle);
              if(index > 0 && index < angles.length - 1){// 点左右两侧均有Z轴
                let leftLineAngle = angles[index + 1];
                let rightLineAngle = angles[index - 1];
                // 过当前点的水平线与左侧射线交点距Y轴的水平像素距离
                let lx = h1/(Math.tan(leftLineAngle / 180 * Math.PI));
                // 过当前点的水平线与右侧射线交点距Y轴的水平像素距离
                let rx = h1/(Math.tan(rightLineAngle / 180 * Math.PI));
                // 过当前点的水平线与左侧射线交点的像素横坐标
                let lx_px = originPoint1[0] + lx;
                // 过当前点的水平线与右侧射线交点的横坐标
                let rx_px = originPoint1[0] + rx;
                // 过当前点的水平线与左侧射线交点的逻辑横坐标
                let lx_lj = myChart.convertFromPixel({ xAxisIndex: 0 }, lx_px)
                // 过当前点的水平线与左侧射线交点的逻辑横坐标
                let rx_lj = myChart.convertFromPixel({ xAxisIndex: 0 }, rx_px)

                // 过当前点的水平线与左右侧射线交点逻辑距离
                let B = rx_lj - lx_lj;
                // 当前点与右侧的射线水平逻辑距离
                let C = rx_lj - v.data[0];

                let leftLineIndex = lines.findIndex(it=>it.angle === leftLineAngle);
                let leftLineTick = (max - min)/(lines.length + 2 - 1) * (leftLineIndex + 1);
                let rightLineIndex = lines.findIndex(it=>it.angle === rightLineAngle);
                let rightLineTick = (max - min)/(lines.length + 2 - 1) * (rightLineIndex + 1);

                Z = ((leftLineTick - rightLineTick)/ B * C + rightLineTick).toFixed(4);
              }else if(index === 0){// 点右侧无Z轴,以X轴作为右侧射线
                let leftLineAngle = angles[index + 1];
                // 过当前点的垂直线与左侧射线交点的垂直像素距离
                let y1_l = w1 * (Math.tan(leftLineAngle / 180 * Math.PI))
                // 过当前点的垂直线与左侧射线交点的垂直像素坐标
                let y1_px = originPoint1[1] - y1_l;
                // 过当前点的垂直线与左侧射线交点的Y逻辑坐标
                let y1_lj = myChart.convertFromPixel({ yAxisIndex: 0 }, y1_px)

                // 当前点与右侧射线的垂直像素坐标
                let y2_px = point[1];
                // 过当前点的垂直线与左侧射线交点的Y逻辑坐标
                let y2_lj = myChart.convertFromPixel({ yAxisIndex: 0 }, y2_px)

                // 过当前点的垂直线与左右侧射线交点逻辑距离
                let B = y1_lj - originPoint[1];
                // 当前点与右侧射线的垂直逻辑距离
                let C = y2_lj - originPoint[1];

                let leftLineIndex = lines.findIndex(it=>it.angle === leftLineAngle);
                let leftLineTick = (max - min)/(lines.length + 2 - 1) * (leftLineIndex + 1);
                let rightLineTick = min;

                Z = ((leftLineTick - rightLineTick)/ B * C + rightLineTick).toFixed(4);
              }else if(index === angles.length - 1){// 点左侧无Z轴,以Y轴作为左侧射线
                let rightLineAngle = angles[index - 1];
                // 过当前点的水平线与右侧射线交点距Y轴的水平像素距离
                let rx = h1/(Math.tan(rightLineAngle / 180 * Math.PI));
                // 过当前点的水平线与右侧射线交点的像素横坐标
                let rx_px = originPoint1[0] + rx;
                // 过当前点的水平线与左侧射线交点的逻辑横坐标
                let lx_lj = originPoint[0]
                // 过当前点的水平线与右侧射线交点的逻辑横坐标
                let rx_lj = myChart.convertFromPixel({ xAxisIndex: 0 }, rx_px)
                let B = rx_lj - lx_lj;
                let C = rx_lj - v.data[0];

                let leftLineTick = max;
                let rightLineIndex = lines.findIndex(it=>it.angle === rightLineAngle);
                let rightLineTick = (max - min)/(lines.length + 2 - 1) * (rightLineIndex + 1);

                Z = ((leftLineTick - rightLineTick)/ B * C + rightLineTick).toFixed(4);
              }

              const realData = `<span>${this.$t('jh')}:</span>${v.data[2].jh} <br/>
                <span>x-${xAxis[0].name}:</span>${Number(v.data[0]).toFixed(4)} <br/>
                <span>y-${yAxis[0].name}:</span>${Number(v.data[1]).toFixed(4)} <br/>
                <span>${this.$t('syjl')}:</span>${v.data[2].syjl} <br/>`
              let predictData = `${this.$t('ycsj')} <br/>`;
              // 像素坐标转逻辑坐标
              // 上X轴预测,垂直取值;右Y轴预测,水平取值
              xAxis.slice(1).forEach((item,index)=>{
                const px = myChart.convertFromPixel({ xAxisIndex: index + 1 }, point[0]).toFixed(4);
                const rx = v.data[2][item.name];
                if(rx){
                  predictData += `<span>x-${item.name}:</span>${px}/${Number(rx).toFixed(4)} <br/>`
                }else{
                  predictData += `<span>x-${item.name}:</span>${px} <br/>`
                }
              })
              yAxis.slice(1).forEach((item,index)=>{
                const py = myChart.convertFromPixel({ yAxisIndex: index + 1 }, point[1]).toFixed(4);
                const ry = v.data[2][item.name];
                if(ry){
                  predictData += `<span>y-${item.name}:</span>${py}/${Number(ry).toFixed(4)} <br/>`
                }else{
                  predictData += `<span>y-${item.name}:</span>${py} <br/>`
                }
              })
              const rz = v.data[2][this.zAxis.name];
              if(rz){
                predictData += `<span>z-${this.zAxis.name}:</span>${Z}/${Number(rz).toFixed(4)}`;
              }else{
                predictData += `<span>z-${this.zAxis.name}:</span>${Z}`;
              }
              return `<div class="chartTooltip">${realData}${predictData}</div>`;
            }
          }
        },
        series: []
      },
      areaPoints:[],
      zAxis:{}
    }
  },
  mounted(){
    this.initChart();
  },
  methods:{
    initChart(){
      let myChart = this.$echarts.init(document.getElementById(this.chartConfigs.chartId));
      myChart.setOption(this.option);
      // 左键单击
      myChart.getZr().on('click', params=> {
        if(this.valueZoneDisable ||!this.isDefineValueZone) return;
        // 获取点击事件里面的坐标轴的偏移值
        const pointInPixel = [params.offsetX, params.offsetY];
        if (myChart.containPixel({ seriesIndex: 0 }, pointInPixel)) {
          // 像素坐标转逻辑坐标
          let point = myChart.convertFromPixel({ seriesIndex: 0 }, pointInPixel);
          this.areaPoints.push(point)
          this.drawPoints();
        }
      });
      // 右键单击
      myChart.getZr().on('contextmenu', params=> {
        if(this.valueZoneDisable) return;
        if(this.isDefineValueZone){
          if(this.areaPoints.length>2){
            this.drawZone(this.areaPoints);
            this.areaPoints = [];
          }
        }else if(this.isEditValueZone){
          const target = params.target
          if(target?.type === 'polygon'){
            const {index} = target.parent.__ecComponentInfo;
            this.deleteZone(index)
          }
        }
      })
    },
    drawPoints(){
      const {series} = this.option;
      const index = series.findIndex(it=>it.type==='scatter'&&it.symbolSize===5);
      if(index>-1){
        series[index].data=this.areaPoints;
      }else{
        series.push({
          type: 'scatter',
          name:'',
          color:'#000',
          symbolSize:5,
          data:this.areaPoints
        })
      }
      this.option.series = series;
      let myChart = this.$echarts.init(document.getElementById(this.chartConfigs.chartId));
      myChart.setOption(this.option,true);
    },
    handleChartData(data){
      const {axis,valueZone,axisData,axisType} = data;
      const xAxis = {
        bottom:[],
        top:[]
      };
      const yAxis = {
        left:[],
        right:[]
      };
      const zAxis = [];
      axis.forEach(item=>{
        const {type,curveName,startValue,endValue,defaultValue,zaa} = item;
        const obj = {
          name:curveName,
          min:startValue,
          max:endValue
        }
        if(type == 1 && defaultValue == 1){
          xAxis.bottom.push(obj)
        }else if(type == 2 && defaultValue == 1){
          yAxis.left.push(obj)
        }else if(type == 3){
          xAxis.top.push(obj)
        }else if(type == 4){
          yAxis.right.push(obj)
        }else if(type == 5){
          zAxis.push({
            ...obj,
            angles:zaa.map(it=>it.angle)
          })
        }
      })
      const option = _.cloneDeep(this.option);
      const xAxisData = [];
      if(xAxis.bottom.length){
        const {name,min,max} = xAxis.bottom[0];
        xAxisData.push(
          {name, type: axisType.xAxis, min:(axisType.xAxis === 'log' && min == 0)?.001:min, max,nameLocation:'start',nameGap:30 }
        )
      }
      if(xAxis.top.length){
        xAxis.top.forEach((it,index)=>{
          const {name,min,max} = it;
          xAxisData.push(
            {name, type: axisType.xAxis, min:(axisType.xAxis === 'log' && min == 0)?.001:min, max,nameLocation:'start',nameGap:30,position:"top",offset:35*index,axisLine:{ onZero:false } }
          )
        })
      }
      option.xAxis = xAxisData;
      const yAxisData = [];
      if(yAxis.left.length){
        const {name,min,max} = yAxis.left[0];
        yAxisData.push(
          {name, type: axisType.yAxis, min:(axisType.yAxis === 'log' && min == 0)?.001:min, max,nameGap:20 }
        )
      }
      if(yAxis.right.length){
        yAxis.right.forEach((it,index)=>{
          const {name,min,max} = it;
          yAxisData.push(
            {name, type: axisType.yAxis, min:(axisType.yAxis === 'log' && min == 0)?.001:min, max,nameGap:20,position:"right",offset:35*index,axisLine:{ onZero:false } }
          )
        })
      }
      option.yAxis = yAxisData;
      const scatterData = [];
      const syjls = [...new Set(axisData.map(it=>it.syjl))].filter(it=>it!==undefined);
      syjls.forEach(item=>{
        const d1 = axisData.filter(it=>it.syjl === item && it[item] && it[xAxis.bottom[0].name] && it[yAxis.left[0].name]);
        if(d1.length){
          scatterData.push({
            type: 'scatter',
            name: item,
            symbol:d1[0][item]?.startsWith('#')?'circle':'image://' + d1[0][item],
            color:d1[0][item]?.startsWith('#')?d1[0][item]:'',
            symbolSize:20,
            data:d1.map(i=>[
              i[xAxis.bottom[0].name],
              i[yAxis.left[0].name],
              i
            ])
          })
        }
      })
      if(this.targetWell){
        const well = axisData.find(it=>it.jh === this.targetWell);
        if(well && scatterData.length){
          const index1 = scatterData.findIndex(it=>it.name === well.syjl);
          if(index1 > -1){
            const index2 = scatterData[index1].data.findIndex(it=>it[2].jh === well.jh);
            if(index2 > -1){
              scatterData[index1].data.splice(index2,1);
            }
          }
          scatterData.push({
            type: 'effectScatter',
            name: well.syjl,
            symbol:well[well.syjl]?.startsWith('#')?'circle':'image://' + well[well.syjl],
            color:well[well.syjl]?.startsWith('#')?well[well.syjl]:'',
            symbolSize:20,
            data:[
              [well[xAxis.bottom[0].name],well[yAxis.left[0].name],well]            
            ]
          })
        }
      }
      option.series = [...scatterData];
      this.option = option;
      if(!this.valueZoneDisable && valueZone.filter(it=>it.status).length){
        valueZone.forEach(item=>{
          if(item.status){
            const points = item.valueCoordinate.split(',').map(it=>it.split(' '));
            this.drawZone(points,item);
          }
        })
      }else{
        let myChart = this.$echarts.init(document.getElementById(this.chartConfigs.chartId));
        myChart.setOption(this.option,true);
      }
      this.$nextTick(()=>{
        this.handleLines(zAxis[0]);
      })
    },
    handleLines(zAxis){
      this.zAxis = zAxis;
      const myChart = this.$echarts.init(document.getElementById(this.chartConfigs.chartId));
      const {xAxis,yAxis} = this.option;
      const minX = xAxis[0].min;
      const maxX = xAxis[0].max;
      const minY = yAxis[0].min;
      const maxY = yAxis[0].max;
      const point1 = [
        myChart.convertToPixel({ xAxisIndex: 0 }, minX),
        myChart.convertToPixel({ yAxisIndex: 0 }, minY)
      ];
      const point2 = [
        myChart.convertToPixel({ xAxisIndex: 0 }, maxX),
        myChart.convertToPixel({ yAxisIndex: 0 }, minY)
      ];
      const point3 = [
        myChart.convertToPixel({ xAxisIndex: 0 }, minX),
        myChart.convertToPixel({ yAxisIndex: 0 }, maxY)
      ];
      if(!point1 || !point2 || !point3){
        return;
      }
      const w_px = point2[0] - point1[0]; //坐标轴像素宽度
      const h_px = Math.abs(point3[1] - point1[1]); //坐标轴像素高度
      const curA = Math.round(Math.atan(h_px / w_px) * 180 / Math.PI); //坐标轴宽高对角线夹角角度
      const lineData = [];
      const originPoint = [minX,minY];
      const {name,min,max,angles} = zAxis;
      angles.sort((a,b)=>a-b).forEach((angle,index)=>{
        let endPoint = []
        if(angle <= curA){ // 根据X算Y
          const h1 = Math.tan( angle/ 180 * Math.PI) * w_px; //直线结束点距X轴的像素高度
          const y1 = point2[1] - h1; //直线结束点的像素纵坐标
          const y2 = myChart.convertFromPixel({ yAxisIndex: 0 }, y1); //直线结束点的逻辑纵坐标
          endPoint = [maxX,y2];
        }else{ // 根据Y算X
          const w1 = h_px / Math.tan( angle/ 180 * Math.PI); //直线结束点距Y轴的像素宽度
          const x1 = point3[0] + w1; //直线结束点的像素横坐标
          const x2 = myChart.convertFromPixel({ xAxisIndex: 0 }, x1); //直线结束点的逻辑横坐标
          endPoint = [x2,maxY];
        }
        lineData.push({
          type: 'line',
          color:'red',
          symbol:v=>v[0]===originPoint[0]&&v[1]===originPoint[1]?'none':'circle',
          symbolSize: 1,
          lineStyle:{
            width:1
          },
          label:{
            show:true,
            color:'red',
            // Z轴名字(刻度):(Z轴右值-Z轴左值)/(Z轴数量+2-1)*n (n表示Z轴序列号)
            formatter:`${name}=${((max - min)/(angles.length+2-1)*(index+1)).toFixed(0)}`
          },
          angle,
          data:[originPoint,endPoint]
        })
      })
      this.option.series.push(...lineData);
      myChart.setOption(this.option,true);
    },
    drawZone(data,updateObj){
      const echarts = this.$echarts;
      const renderItem = (params, api) => {
        if (params.context.rendered) {
            return;
        }
        params.context.rendered = true;
        let points = [];
        for (let i = 0; i < data.length; i++) {
            points.push(api.coord(data[i]));
        }
        return {
          type: 'polygon',
          shape: {
            points: echarts.graphic.clipPointsByRect(points, {
                x: params.coordSys.x,
                y: params.coordSys.y,
                width: params.coordSys.width,
                height: params.coordSys.height
            })
          },
          style: api.style({
              fill: updateObj?.colorValue || '#ff00004d',
              stroke: '#000'
          })
        };
      }
      const nid = updateObj?.valueName || this.$t('jzq') + nanoid();
      this.option.series.push({
        nid,
        type: 'custom',
        renderItem,
        data
      })
      // 绘制价值区时删除历史打点
      const valPointsIndex = this.option.series.findIndex(it=>it.type==='scatter'&&it.symbolSize===5);
      if(valPointsIndex>-1){
        this.option.series[valPointsIndex].data = [];
      }
      let myChart = this.$echarts.init(document.getElementById(this.chartConfigs.chartId));
      myChart.setOption(this.option,true);
      if(!updateObj){
        this.addTableData(nid);
      }
    },
    deleteZone(index){
      const {nid} = this.option.series[index];
      this.option.series.splice(index,1);
      let myChart = this.$echarts.init(document.getElementById(this.chartConfigs.chartId));
      myChart.setOption(this.option,true);
      this.$emit('deleteRow',nid)
    },
    addTableData(nid){
      let color = '#ff00004d';
      let wellCount = this.getPointNumInZone();
      const obj ={
        valueName:nid,
        wellCount,
        colorValue:color,
        status:true,
        xAxis:this.option.xAxis[0].name,
        yAxis:this.option.yAxis[0].name,
        points:[...this.areaPoints],
      }
      this.$emit('addRow',obj)
    },
    getPointNumInZone(){
      const scatterSeries = this.option.series.filter(it=>it.type.includes('scatter')&&it.symbolSize!==5);
      const points = [];
      scatterSeries.forEach(it=>{
        points.push(...it.data)
      })
      let num = 0;
      points.forEach(it=>{
        let isIn = this.pointInPolygon(it,this.areaPoints);
        if(isIn) num++;
      })
      return num;
    },
    pointInPolygon(point, polygon) {
      var x = point[0], y = point[1];
      var inside = false;
      for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
        var xi = polygon[i][0], yi = polygon[i][1];
        var xj = polygon[j][0], yj = polygon[j][1];

        var intersect = ((yi > y) != (yj > y)) &&
            (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
        if (intersect) inside = !inside;
      }
      return inside;
    },
    deleteZoneByName(name){
      const index = this.option.series.findIndex(it=>it.type==='custom' && it.nid === name);
      if(index>-1){
        this.option.series.splice(index,1)
        let myChart = this.$echarts.init(document.getElementById(this.chartConfigs.chartId));
        myChart.setOption(this.option,true);
      }
    },
    changeColorByName(row){
      const index = this.option.series.findIndex(it=>it.type==='custom' && it.nid === row.valueName);
      if(index>-1){
        this.option.series.splice(index,1)
        this.drawZone(row.points,row)
      }
    },
    changeStatusByName(row){
      if(row.status){
        this.drawZone(row.points,row)
      }else{
        this.deleteZoneByName(row.valueName)
      }
    }
  }
}
</script>
  
<style scoped lang="scss">
  .TuBanChart{

  }
</style>
<style>
.chartTooltip span{
  display: inline-block;
  width:80px;
  text-align: right;
}
</style>