代码改变世界

Echarts 实现飞线图效果

2022-03-01 00:06  龙恩0707  阅读(8263)  评论(3编辑  收藏  举报

Echarts 实现飞线图效果

实现的基本效果如下所示:

实现echarts飞线图的灵感是来自网上的demo,比如 https://github.com/guohaining/echarts_map 中的 china_lines.html中的demo配置,然后根据自己的需求改善配置变成自己的需求。

china_lines.html 的代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>中国地图飞线</title>
    <style>
      #china {
        width: 1370px;
        height: 600px;
      }
    </style>
</head>

<body>
  <div id="china"></div>
</body>
<script src="js/echarts.min.js"></script>
<script src="js/china.js"></script>
<script type="text/javascript">
    var chinaGeoCoordMap = {
      '黑龙江': [127.9688, 45.368],
      '内蒙古': [110.3467, 41.4899],
      "吉林": [125.8154, 44.2584],
    };
    var chinaDatas = [
      [{ name: '黑龙江', value: 0 }],
      [{ name: '内蒙古', value: 0 }],
      [{ name: '吉林', value: 0 }],
    ];
    var convertData = function(data) {
      var res = [];
      for(var i = 0; i < data.length; i++) {
        var dataItem = data[i];
        // 飞线图的起点
        var fromCoord = chinaGeoCoordMap[dataItem[0].name];
        var toCoord = [116.4551,40.2539]; // 飞线图的目标点
        if (fromCoord && toCoord) {
          res.push([{
            coord: fromCoord,
            value: dataItem[0].value
          }, 
          {
            coord: toCoord,
          }]);
        }
      }
      return res;
    };
    var series = [];
    [['北京市', chinaDatas]].forEach(function(item, i) {
      console.log('---item----', item[1]);
      series.push({
        type: 'lines',
        zlevel: 2,
        effect: {
          show: true,
          period: 4, //箭头指向速度,值越小速度越快
          trailLength: 0.02, //特效尾迹长度[0,1]值越大,尾迹越长重
          symbol: 'image://./images/airline.png',
          symbolSize: [20, 20], //图标大小
        },
        lineStyle: {
          normal: {
            width: 1, //尾迹线条宽度
            opacity: 1, //尾迹线条透明度
            curveness: .3 //尾迹线条曲直度
          }
        },
        data: convertData(item[1])
      });
    });
    var option = {
      tooltip: {
        trigger: 'item',
        backgroundColor: 'rgba(166, 200, 76, 0.82)',
        borderColor: '#FFFFCC',
        showDelay: 0,
        hideDelay: 0,
        enterable: true,
        transitionDuration: 0,
        extraCssText: 'z-index:100',
        formatter: function(params, ticket, callback) {
          //根据业务自己拓展要显示的内容
          var res = "";
          var name = params.name;
          var value = params.value[params.seriesIndex + 1];
          res = "<span style='color:#fff;'>" + name + "</span><br/>数据:" + value;
          return res;
        }
      },
      backgroundColor:"#013954", // 地图背景
      geo: {
        map: 'china',
        zoom: 1.2,
        label: {
          emphasis: {
            show: false
          }
        },
        roam: true, //是否允许缩放
        itemStyle: {
          normal: {
            color: 'rgba(51, 69, 89, .5)', //地图背景色
            borderColor: '#516a89', //省市边界线00fcff 516a89
            borderWidth: 1
          },
          emphasis: {
            color: 'rgba(37, 43, 61, .5)' //悬浮背景
          }
        }
      },
      series: series
    };
    let china = echarts.init(document.getElementById('china'));
    china.setOption(option);
</script>
</html>

根据上面的demo配置后,我们就可以根据配置数据格式,来做自己的需求。所有代码如下:

import React, { useState, useEffect } from 'react';
import _ from 'lodash';
import {
    Chart,
    Interval,
    Axis,
    Tooltip,
  } from 'bizcharts';

const MyChart = (props) => {
    let {
      datas,
    } = props;
    var china;
    var series = [];
    let [ isFull, setFull ] = useState(false);
    let [ isClick, setClick ] = useState(false);
    let [ data2, setData2 ] = useState([]);
    let [ isVisible, setVisible ] = useState(false);
    let [ isFixed, setFixed ] = useState(false);
    let [ xy_value, setXYValue ] = useState(null); // 保存鼠标在页面上移动的 x / y 轴的坐标值
    let [ line_datas, setLineData ] = useState(null); // 保存飞线图的数据
    let [ currentIndex, setCurrentIndex ] = useState(0); // 当前tab项的索引值,默认为0,第一项
    // 地图数据
    var chinaGeoCoordMap = {};
    
    // 全局保存地图缩放的zoom 默认为1
    var GLOBAL_ZOOM = 1;
  
    // 保存用户是否已经点击了飞线图的状态 默认未点击,点击后,通过该参数锁定
    var isAlreadyClickFlight = false;
  
    // 保存options
    window.GLOBAL_OPTIONS = {};
  
    // 保存 seriesIndex 索引值
    var GLOBAL_SERIES_INDEX = 1;
  
    // 保存 飞线图的宽度,默认为1
    var GLOBAL_LINE_WIDTH = 1;
  
    function convertData(data, formdata, formdataValue) {
      var res = [];
      for(var i = 0; i < data.length; i++) {
          var dataItem = data[i];
          var fromCoord = chinaGeoCoordMap[dataItem[0].name];
  
          if(fromCoord && formdataValue) {
            res.push(
                [ 
              { 
                formName: formdata, 
                coord: formdataValue,
              },
              // 飞线往哪里去
                  { 
                toName: dataItem[0].name, 
                coord: fromCoord, 
                value: dataItem[0].value,
                item: dataItem[0]
              }
                ]
            );
          }
      }
      return res;
    }
    /*
     * @param { formdata } 中心点
     * @param { chinaDatas } 飞行数据
     * @param { formdataValue } 中心点坐标
    */
    function seriesFunc(formdata, chinaDatas, formdataValue) {
      let name = '';
      let color = '';
      let startColor = ''; // 渐变起始颜色
      let levelParams = 0; // 保存判断级别的变量
      if (currentIndex == 0) { // 碳排量
        levelParams = chinaDatas[0][0].totalCarbonEmission;
      } else if (currentIndex == 1) { // 单位公里碳排量
        levelParams = chinaDatas[0][0].unitCarbonEmission;
      }
      if (levelParams >= 9000) {
          name = '9000以上';
          color = '#c51b37';
          startColor = '#b34c5d';
      } else if (levelParams >= 6000 && levelParams < 9000) {
          name = '6000~9000';
          color = '#e28e10';
          startColor = '#886127';
      } else if (levelParams >= 3000 && levelParams < 6000) {
          name = '3000~6000';
          color = '#3f90ff';
          startColor = '#2f5c98';
      } else if (levelParams >= 0 && levelParams < 3000) {
          name = '0~3000';
          color = '#14be8b';
          startColor = '#3d675b';
      }
      if (chinaDatas && chinaDatas.length) {
          [[formdata, chinaDatas]].forEach(function(item, i) {
            series.push(
              {
                type: 'lines',
                name: name,
                coordinateSystem: 'geo',
                zlevel: 2,
                effect: {
                  show: true,
                  period: 8, //箭头指向速度,值越小速度越快
                  trailLength: 0.02, //特效尾迹长度[0,1]值越大,尾迹越长重
                  // symbol: 'image://./images/airline.png', // 箭头图标
                  symbol: 'arrow',
                  // symbolSize: [20, 20], //图标大小
                  symbolSize: 10,
                  color: color, // 图标颜色
                },
                lineStyle: {
                  normal: {
                    show: true,
                    width: 2, //尾迹线条宽度
                    opacity: 0.6, //尾迹线条透明度
                    curveness: 0.3, //尾迹线条曲直度
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                      {
                        offset: 0,
                        color: color // 0% 处的颜色
                      },
                      {
                        offset: 1,
                        color: startColor // 100% 处的颜色
                      }
                    ])
                  },
                },
                data: convertData(item[1], formdata, formdataValue),
              },
              // 出发点
              {
                type: 'effectScatter',
                coordinateSystem: 'geo',
                zlevel: 2,
                hoverAnimation: true, // 鼠标移动上去后效果
                rippleEffect: { //涟漪特效
                  period: 8, //动画时间,值越小速度越快
                  brushType: 'stroke', //波纹绘制方式 stroke, fill
                  scale: 10 //波纹圆环最大限制,值越大波纹越大
                },
                label: {
                  normal: {
                    show: true,
                    position: 'bottom', //显示位置
                    offset: [6, 6], //偏移设置
                    color: '#a7b9cd',
                    formatter: function(params){ // 圆环显示文字
                      // return '';
                      return params.data.name;
                    },
                    fontSize: 11,
                  },
                  // 高亮时效果
                  emphasis: {
                    show: true,
                    areaColor: 'null',
                  }
                },
                symbol: 'circle',
                symbolSize: function(val) {
                  return 5 + val[2] * 5; // 圆环大小
                },
                itemStyle: {
                  normal: {
                    show: false,
                    color: color,
                  }
                },
                data: [{
                  name: chinaDatas[0][0].isStartAndEnd ? '' : formdata,
                  value: formdataValue,
                }],
              },
              // 到达点
              {
                type: 'effectScatter',
                coordinateSystem: 'geo',
                zlevel: 2,
                hoverAnimation: true, // 鼠标移动上去后效果
                rippleEffect: { //涟漪特效
                  period: 8, //动画时间,值越小速度越快
                  brushType: 'stroke', //波纹绘制方式 stroke, fill
                  number: 1,
                  scale: 10 //波纹圆环最大限制,值越大波纹越大
                },
                label: {
                  normal: {
                    show: true,
                    position: 'right', //显示位置
                    color: '#a7b9cd',
                    offset: [6, 6], //偏移设置
                    formatter: function(params) { // 圆环显示文字
                      // return '';
                      return params.data.name;
                    },
                    fontSize: 11
                  },
                  // 高亮时效果
                  emphasis: {
                    show: true,
                    areaColor: 'null',
                  }
                },
                symbol: 'circle',
                  symbolSize: function(val) {
                    return 2 + val[2] * 5; // 圆环大小
                  },
                  itemStyle: {
                    normal: {
                      show: false,
                      color: color,
                    }
                  },
                  data: item[1].map(function(dataItem) {
                    return {
                      name: chinaDatas[0][0].isStartAndEnd ? '' : dataItem[0].name,
                      value: chinaGeoCoordMap[dataItem[0].name].concat([dataItem[0].value])
                    };
                  }),
                }
              )
          })
      }
    }
    /*
     * 获取飞行数据的所有原点和终点
     * @return { Array } 返回所有飞行数据的原点和终点
     * 类似数据如下: [{label: '深圳', value: ['经度', '纬度']}, ....依次类推]
    */
    function getFlightOrigins(data) {
      let rets = []; // 保存飞行数据的原点
      let rets2 = []; // 保存飞行数据的终点
      let obj = {};
      let obj2 = {};
      if (data && data.length) {
        data.forEach(item => {
          rets.push({
            label: item.shipment,
            value: item.shipmentCoordinate
          });
          rets2.push({
            label: item.destination,
            value: item.destinationCoordinate,
          })
        })
      }
      const flightOriginsStart = rets.reduce((prev, cur) => {
        obj[cur.label] ? '' : obj[cur.label] = true && prev.push(cur);
        return prev;
      }, []);
      rets2 = rets2.concat(rets);
      const flightOriginsEnd = rets2.reduce((prev, cur) => {
        obj2[cur.label] ? '' : obj2[cur.label] = true && prev.push(cur);
        return prev;
      }, []);
      let obj3 = {};
      if (flightOriginsEnd && flightOriginsEnd.length) {
        flightOriginsEnd.forEach(item => {
          obj3[item.label] = item.value;
        })
      }
      chinaGeoCoordMap = obj3;
      return {
        flightOriginsStart,
        flightOriginsEnd,
      }
    };
  
    // 先克隆一下对象,遍历下,给对象添加新属性,如果原点和目标点相同的话,说明既是原点又是终点
    const { flightOriginsStart } = getFlightOrigins(datas);
    for (let i = 0; i < flightOriginsStart.length; i++) {
      const itemLabel = flightOriginsStart[i].label;
      for (let j = 0; j < datas.length; j++) {
        if (datas[j].destination === itemLabel) {
          datas[j].isStartAndEnd = true;
        } 
      }
    }
    /**
     * 获取飞行数据的所有原点对应的所有终点
     * @param { flightOrigins } 根据飞行数据的所有原点来获取对应的所有终点数据
     * 原起点数据如下 flightOrigins = [{label: '深圳', value: ['经度', '纬度']}, ....依次类推] 
     * @return { Array } 返回所有飞行数据的终点 key对应的飞行原点的label
     * 返回的数据如下:[
     *  {'深圳': [ [[{name: '', value: 0}]], [[{name: '', value: 0}]] ]},
     *  {'东莞': [ [[{name: '', value: 0}]], [[{name: '', value: 0}]] ]}
     *  .... 以此类推
     * ]
     */
    function getFlightEnd(flightOrigins) {
      let rets = [];
      if (flightOrigins && flightOrigins.length) {
        for (let i = 0; i < flightOrigins.length; i++) {
          const itemLabel = flightOrigins[i].label; // 获取飞行数据的原点对应的所有终点数据
          rets.push(getItemFlightEnd(itemLabel, flightOrigins[i].value));
        }
      }
      return rets;
    };
    function getItemFlightEnd(itemLabel, pos) {
      let rets = [];
      let obj = {};
      for (let i = 0; i < datas.length; i++) {
        const item = datas[i];
        if (itemLabel === item.shipment) {
          rets.push([[{
            name: item.destination,
            value: 0,
            unitCarbonEmission: item.unitCarbonEmission,
            totalCarbonEmission: item.totalCarbonEmission,
            totalTruckSize: item.totalTruckSize,
            totalKilometres: item.totalKilometres,
            isStartAndEnd: item.isStartAndEnd || false,
            carSizeData: item.carSizeData,
          }]])
        }
      }
      obj = {
        'label': itemLabel,
        'pos': pos,
        'value': rets,
      }
      return obj;
    }
    /*
     * 定位legend
     * 获取legend的个数
    */
    function getLegendCount() {
      const { series = [] } = window.GLOBAL_OPTIONS;
      let rets = [];
      if (series.length) {
        series.forEach(item => {
          if (item.type === 'lines') {
            rets.push(item.name);
          }
        });
      }
      console.log('----window.GLOBAL_OPTIONS----', window.GLOBAL_OPTIONS);
      return Array.from(new Set(rets));
    }
    function initFlight() {
      // 获取到所有的数据
      const allDatas = getFlightEnd(getFlightOrigins(datas).flightOriginsStart);
      // 初始化飞行数据
      if (allDatas && allDatas.length) {
        for (let init = 0; init < allDatas.length; init++) {
          const formdata = allDatas[init].label;
          const formdataValue = allDatas[init].pos;
          const { value = [] } = allDatas[init];
          if (value && value.length) {
            for (let y = 0; y < value.length; y++) {
              const chinaDatas = value[y];
              seriesFunc(formdata, chinaDatas, formdataValue)
            }
          }
        }
      }
    }
    initFlight();
  
    function openFullScreen() {
      let el = document.getElementById("myChart");
      if (el.requestFullScreen) {
        el.requestFullScreen();
      } else if (el.mozRequestFullScreen) {
        el.mozRequestFullScreen();
      } else if (el.webkitRequestFullScreen) {
        el.webkitRequestFullScreen();
      } else if (el.msRequestFullScreen) {
        el.msRequestFullScreen();
      }
    };
    function exitFullScreen() {
      if (document.exitFullScreen) {
        document.exitFullScreen();
      } else if (document.mozCancelFullScreen) {
        document.mozCancelFullScreen();
      } else if (document.webkitCancelFullScreen) {
        document.webkitCancelFullScreen();
      } else if (document.msExitFullScreen) {
        document.msExitFullScreen();
      }
    }
    // 是否为全屏
    function isFullScreen() {
      return !!(
        document.fullscreen ||
        document.mozFullScreen || 
        document.webkitIsFullScreen || 
        document.webkitFullScreen || 
        document.msFullScreen
      );
    };
    // 获取鼠标在地图上移动的位置
    function pos(event) {  //鼠标定位赋值函数
      var posX = 0, posY = 0;  //临时变量值
      var e = event || window.event;  //标准化事件对象
      if (e.pageX || e.pageY) {  //获取鼠标指针的当前坐标值
        posX = e.pageX;
        posY = e.pageY;
      } else if (e.clientX || e.clientY) {
        posX = event.clientX + document.documentElement.scrollLeft + document.body.scrollLeft;
        posY = event.clientY + document.documentElement.scrollTop + document.body.scrollTop;
      }
      return {
        x: posX,
        y: posY,
      };
    }
    // 重置 layoutSize
    function resetLayoutSize() {
      // 如果是全屏下
      if (isFullScreen()) {
        window.GLOBAL_OPTIONS.layoutSize = window.screen.height;
        window.GLOBAL_OPTIONS.layoutCenter = ['50%', '50%'];
      } else {
        if (window.screen.height > 950 && window.screen.height < 1200) {
          window.GLOBAL_OPTIONS.layoutSize = 800;
        } else if (window.screen.height > 1200) {
          window.GLOBAL_OPTIONS.layoutSize = 1100;
        } else {
          window.GLOBAL_OPTIONS.layoutSize = 600;
        }
        window.GLOBAL_OPTIONS.layoutCenter = ['50%', '35%'];
      }
      console.log('-----resetLayoutSize---', window.GLOBAL_OPTIONS);
    }
    // 设置legend位置
    function setLegendPos() {
      let left = 340, bottom = window.screen.height > 1200 ? 395 : 345; // 页面初始化的时候,定位在该位置
      const count = getLegendCount().length;
      const datas = getLegendCount();
      let rets = [];
      if (datas && datas.length) {
        datas.forEach(item => {
          rets.push({
            name: item,
            icon: 'rect'
          })
        })
      }
      if (count === 1) {
        if (isFullScreen()) {
          left = 10;
          bottom = 50;
        } else {
          left = 340;
          bottom = window.screen.height > 1200 ? 420 : 370;
        }
      } else if (count === 2) {
        if (isFullScreen()) {
          left = 10;
          bottom = 76;
        } else {
          left = 340;
          bottom = window.screen.height > 1200 ? 446 : 396;
        }
      } else if (count === 3) {
        if (isFullScreen()) {
          left = 10;
          bottom = 100;
        } else {
          left = 340;
          bottom = window.screen.height > 1200 ? 470 : 420;
        }
      } else if (count === 4) {
        if (isFullScreen()) {
          left = 10;
          bottom = 120;
        } else {
          left = 340;
          bottom = window.screen.height > 1200 ? 496 : 446;
        }
      }
      window.GLOBAL_OPTIONS.title[0].left = left;
      window.GLOBAL_OPTIONS.title[0].bottom = bottom;
      window.GLOBAL_OPTIONS.legend.data = rets;
    }
  
    // 获取seriesIndex
    function getSeriesIndex(options, params) {
      const { data = {} } = params;
      const { formName = '', toName = '' } = data;
      const { series = [] } = options;
      let seriesIndex = 0;
      if (series.length) {
        for (let s = 0; s < series.length; s++) {
          const item = series[s];
          if (item.type === 'lines') {
            const sData = item.data;
            const currentFormName = sData[0][0].formName;
            const currentToName = sData[0][1].toName;
            if ((formName === currentFormName) && (toName === currentToName)) {
              seriesIndex = s;
              break;
            }
          }
        }
      }
      return seriesIndex;
    }
    
    var option = {
      layoutCenter: ['50%', '35%'],
      layoutSize: (window.screen.height > 950 && window.screen.height < 1200) ? 800 : (window.screen.height > 1200 ? 1100 : 600),
      title: [
        {
          text: '单位(吨)',
          left: 340,
          bottom: 446,
          width: 100,
          textStyle: {
            fontSize: 12,
            color: 'rgba(255,255,255,0.65)',
          }
        }
      ],
      legend: {
        orient: 'vertical',
        left: 340,
        bottom: window.screen.height > 1200 ? 395 : 345,
        itemWidth: 8,
        itemHeight: 8,
        itemGap: 12,
        data: [
          {
            name: '9000以上',
            icon: 'rect'
          },
          {
            name: '6000~9000',
            icon: 'rect'
          },
          {
            name: '3000~6000',
            icon: 'rect'
          },
          {
            name: '0~3000',
            icon: 'rect'
          }
        ],
        textStyle: {
          color: '#fff'
        },
      },
      color: ["#c51b37", "#e28e10", "#3f90ff", "#14be8b"],
      toolbox: {
        show: true,
        itemGap: 5,
        right: 358,
        top: window.screen.height > 1200 ? 72 : 122,
        itemSize: 32,
        zlevel: 1000000,
        showTitle: false, // 鼠标移动上去不显示标题
        feature: {
          dataView: {
            show: false,
          },
          dataZoom: {
            show: true,
            iconStyle: {
              opacity: 0,
            },
          },
          restore: { show: false },
          saveAsImage: { show: false },
          // 全屏
          myFull: {
            show: true,
            title: '',
            icon: 'image://https://img.alicdn.com/imgextra/i1/O1CN01uVpZCF1V30vM3BxGI_!!6000000002596-2-tps-200-200.png',
            onHover: (e) => {
              console.log('---onHover---', e);
              var opts = e.getOption();
              if (isFullScreen()) {
                opts.toolbox[0].feature.myFull.icon = "image://https://img.alicdn.com/imgextra/i3/O1CN01vgzs4g1xQCDGIka2M_!!6000000006437-2-tps-200-200.png";
              } else {
                opts.toolbox[0].feature.myFull.icon = "image://https://img.alicdn.com/imgextra/i3/O1CN01PPeB6j1pA9PUu5aL3_!!6000000005319-2-tps-200-200.png";
              }
              china.setOption(opts);
            },
            onHide: (e) => {
              var opts = e.getOption();
              if (isFullScreen()) {
                opts.toolbox[0].feature.myFull.icon = "image://https://img.alicdn.com/imgextra/i4/O1CN01WjTtjv1xCuj5ZiLyZ_!!6000000006408-2-tps-200-200.png";
              } else {
                opts.toolbox[0].feature.myFull.icon = "image://https://img.alicdn.com/imgextra/i1/O1CN01uVpZCF1V30vM3BxGI_!!6000000002596-2-tps-200-200.png";
              }
              china.setOption(opts);
            },
            onclick: (e) => {
              var opts = e.getOption();
              if (isFullScreen()) {
                opts.toolbox[0].feature.myFull.icon = "image://https://img.alicdn.com/imgextra/i1/O1CN01uVpZCF1V30vM3BxGI_!!6000000002596-2-tps-200-200.png";
                opts.toolbox[0].right = 358;
                opts.toolbox[0].top = window.screen.height > 1200 ? 72 : 122;
  
                opts.layoutCenter = ['50%', '35%'];
  
                if (window.screen.height > 950 && window.screen.height < 1200) {
                  opts.layoutSize = 800;
                } else if (window.screen.height > 1200) {
                  opts.layoutSize = 1100;
                } else {
                  opts.layoutSize = 600;
                }
                opts.legend[0].left = 340;
                opts.legend[0].bottom = window.screen.height > 1200 ? 490 : 345;
                // 如果是全屏模式,就退出全屏
                exitFullScreen();
              } else {
                opts.toolbox[0].feature.myFull.icon = "image://https://img.alicdn.com/imgextra/i4/O1CN01WjTtjv1xCuj5ZiLyZ_!!6000000006408-2-tps-200-200.png";
                opts.legend[0].left = 10;
                opts.legend[0].bottom = 20;
  
                opts.layoutCenter = ['50%', '50%'];
                opts.layoutSize = window.screen.height;
  
                opts.toolbox[0].top = 30;
                opts.toolbox[0].right = 30;
                // opts.geo[0].zoom = 1.5;
                // 否则,打开全屏
                openFullScreen();
              }
              if (opts.series[GLOBAL_SERIES_INDEX]) {
                opts.series[GLOBAL_SERIES_INDEX].lineStyle.width = 1;
              }
              let left = 340, bottom = window.screen.height > 1200 ? 395 : 345; // 页面初始化的时候,定位在该位置
              const count = getLegendCount().length;
              if (count === 1) {
                if (isFullScreen()) {
                  left = 340;
                  bottom = window.screen.height > 1200 ? 520 : 370;
                } else {
                  left = 10;
                  bottom = 50;
                }
              } else if (count === 2) {
                if (isFullScreen()) {
                  left = 340;
                  bottom = window.screen.height > 1200 ? 540 : 396;
                } else {
                  left = 10;
                  bottom = 76;
                }
              } else if (count === 3) {
                if (isFullScreen()) {
                  left = 340;
                  bottom = window.screen.height > 1200 ? 565 : 420;
                } else {
                  left = 10;
                  bottom = 100;
                }
              } else if (count === 4) {
                if (isFullScreen()) {
                  left = 340;
                  bottom = window.screen.height > 1200 ? 584 : 446;
                } else {
                  left = 10;
                  bottom = 120;
                }
              }
              opts.title[0].left = left;
              opts.title[0].bottom = bottom;
              console.log('---opts-1223444--', opts);
              window.GLOBAL_OPTIONS = opts;
  
              console.log('--xxx--opts---', opts);
              // 解决窗口缩放事件导致不重新渲染的bug
              setTimeout(() => {
                china.setOption(opts);
              }, 350)
            }
          },
        }
      },
      tooltip: {
        show: true,
        // triggerOn: 'click',
        trigger: 'item',
        backgroundColor: 'rgba(27, 28, 33, 0.85)',
        borderColor: 'rgba(27, 28, 33, 0.85)',
        showDelay: 0, // 浮层显示的延迟
        hideDelay: 100, //浮层隐藏的延迟
        enterable: true, // 鼠标是否可进入提示框浮层中
        transitionDuration: 0, // 提示框浮层的移动动画过度时间 设置0的时候会紧跟着鼠标移动
        extraCssText: 'z-index:100000',
        textStyle: {
          color: '#fff',
        },
        formatter: function(params, ticket, callback) {
          const { data = {} } = params;
          const {
            formName = '',
            item = {},
            toName = ''
          } = data;
          if (!formName) {
            return;
          }
          // 如果已经点击了飞线图 就不需要再提示了
          if (isAlreadyClickFlight) {
            return;
          }
          setVisible(false);
          setClick(true);
          const {
            unitCarbonEmission,
            totalCarbonEmission,
            totalTruckSize,
            totalKilometres,
            carSizeData = [],
          } = item;
  
          // 弹窗柱状图显示
          const rets = [];
          carSizeData && carSizeData.length && carSizeData.forEach(item => {
            rets.push({
              label: item[0],
              value: item[1],
            })
          })
  
          if (isFullScreen()) {
            let legend = {};
            let toolbox = {};
    
            if (option.toolbox[0]) {
              toolbox = option.toolbox[0];
            } else {
              toolbox = option.toolbox;
            }
            toolbox.left = null;
            toolbox.top = 40;
            toolbox.right = 30;
            toolbox.bottom = null;
            toolbox.feature.myFull.icon = "image://https://img.alicdn.com/imgextra/i4/O1CN01WjTtjv1xCuj5ZiLyZ_!!6000000006408-2-tps-200-200.png";
    
            // 标题
            option.title[0].left = 10;
            option.title[0].bottom = 120;
            if (option.legend[0]) {
              legend = option.legend[0];
            } else {
              legend = option.legend;
            }
            legend.left = 10;
            legend.bottom = 20;
          } else {
            
            let toolbox = {};
            let legend = {};
            if (option.toolbox[0]) {
              toolbox = option.toolbox[0];
            } else {
              toolbox = option.toolbox;
            }
            toolbox.top = window.screen.height > 1200 ? 72 : 122;
            toolbox.right = 358;
    
            toolbox.feature.myFull.icon = "image://https://img.alicdn.com/imgextra/i1/O1CN01uVpZCF1V30vM3BxGI_!!6000000002596-2-tps-200-200.png";
            
            option.title[0].left = 340;
            option.title[0].bottom = 200;
    
            if (option.legend[0]) {
              legend = option.legend[0];
            } else {
              legend = option.legend;
            }
            legend.left = 340;
            legend.bottom = window.screen.height > 1200 ? 395 : 345;
          }
          setData2(rets);
          //根据业务自己拓展要显示的内容
          var res = "";
          res = "<div class='tool-tip'>" + 
                  "<div class='title'><span>"+formName+"</span>-<span>"+toName+"</span></div>" + 
                  "<div class='t-content'>"+
                    "<div><span class='desc'>单位公里排放量</span><span class='t-content'><i>"+unitCarbonEmission+"</i>吨/公里</span></div>" + 
                    "<div><span class='desc'>碳排量</span><span class='t-content'><i>"+totalCarbonEmission+"</i></span></div>" + 
                    "<div><span class='desc'>总车次</span><span class='t-content'><i>"+totalTruckSize+"</i></span></div>" + 
                    "<div><span class='desc'>总公里数</span><span class='t-content'><i>"+totalKilometres+"</i>公里</span></div>" + 
                  "</div>"
                "</div>";
          return res;
        }
      },
      backgroundColor: new echarts.graphic.LinearGradient(0.1, 1, 1, 0.5, [
        {
          offset: 0,
          color: '#0f0f0f', // 0% 处的颜色
        },
        {
          offset: 0.2,
          color: '#272c37', // 0% 处的颜色
        },
        {
          offset: 0.75,
          color: '#272c37', // 0% 处的颜色
        },
        {
          offset: 0.9,
          color: '#0f0f0f', // 0% 处的颜色
        },
        {
          offset: 1,
          color: '#0f0f0f' // 100% 处的颜色
        }
      ], false),
    
      animation: false, // 解决阻止拖拽和缩放时上下图层不同步的问题
      geo: [{
        map: 'china', // 地图名
        zlevel: 2, // geo显示级别,默认是0
        roam: true, // 设置为false,不启动roam缩放
        scaleLimit: { //滚轮缩放的极限控制
          min: 0.5,
          max: 3
        },
        selectedMode: false, // 不让点击
        zoom: 1,
        label: {
          emphasis: {
            show: false,
            areaColor: 'null',
          }
        },
        regions: [
          {
            name: "南海诸岛",
            itemStyle: {
              // 隐藏南海诸岛
              normal: {
                opacity: 0,
              }
            }
          }
        ],
        itemStyle: { // 顶层地图的样式,如地图区域颜色,边框颜色,边框大小等
          normal: {
            borderColor: '#33587F', // 边框颜色
            areaColor:'#31344a', // 地图区域颜色
            borderWidth: 1, // 边框大小
          },
          emphasis: {
            areaColor: 'null',
            color: 'rgba(37, 43, 61, 0)', //悬浮背景
          }
        }
      },{
        map: 'china',
        zoom: 1,
        roam: true,
        selectedMode: false, // 不让点击
        scaleLimit: { //滚轮缩放的极限控制
          min: 0.5,
          max: 3
        },
        label: {
          emphasis: {
            show: false,
            areaColor: 'null',
          }
        },
        regions: [
          {
            name: "南海诸岛",
            itemStyle: {
              // 隐藏南海诸岛
              normal: {
                opacity: 0,
              }
            }
          }
        ],
        itemStyle: { // 地图的样式,如地图区域颜色,边框颜色,边框大小等
          normal: {
           borderColor: '#56b7ec', // 边框颜色
           areaColor:'#31344a', // 地图区域颜色
           borderWidth: 4, // 边框大小
           
           // 边框设置阴影 颜色渐变
           shadowBlur: 10,
           shadowColor: 'rgba(86, 183, 236, .5)',
           shadowOffsetX: 1,
           shadowOffsetY: 1,
          },
          emphasis: {
            color: 'rgba(37, 43, 61, 0)', //悬浮背景
            areaColor: null,
          }
        }
      }],
      series: series,
      zlevel: 1,
    };
    option.series.unshift({
      type: 'map',
      map: 'china',
      selectedMode: false, // 不让点击
      label: {
        show: false,
      },
      roam: true,
      geoIndex: 0,
      data: [],
    })
  
    window.GLOBAL_OPTIONS = option;
    // 首次渲染页面
    useEffect(() => {
      let mapWidth = document.getElementById('myChart').offsetWidth;
    
      var screenWidth = window.screen.width;
      let h = window.screen.height;
      console.log('----window.screen.height---', window.screen.height);
      
      if (window.screen.height > 1200) {
        h = document.body.clientHeight
      } else if (window.screen.height < 900) {
        h = 900;
      }
  
      document.getElementById('china').style.height = h + 'px';
  
      china = echarts.init(document.getElementById('china'));
  
      setLegendPos();
  
      console.log('----初始化---', window.GLOBAL_OPTIONS);
  
      china.setOption(window.GLOBAL_OPTIONS);
  
      const ulList = document.getElementById('ul-list');
      const liLists = ulList.getElementsByTagName('li');
  
      // 设置画布的高度
      document.getElementById('myChart').style.height = h - 48 + 'px';
  
      // 监听鼠标滚动的坐标
      document.onmousemove = function (event) {
        const values = pos(event);
        if (!isAlreadyClickFlight) {
          setXYValue(values);
        }
      }
      
      document.onclick = function(event) {
        if (!isFixed) {
          setFixed(false);
        }
        if (!isClick) {
          setClick(false);
        }
        if (!isVisible) {
          setVisible(false);
        }
        isAlreadyClickFlight = false;
        event.stopPropagation();
      }
  
      ulList.addEventListener('click', function(e) {
        e.stopPropagation();
        console.log('---window.GLOBAL_OPTIONS---', window.GLOBAL_OPTIONS);
        const target = e.target || e.srcElement;
        const cIndex = target.getAttribute('data-index');
        setCurrentIndex(cIndex);
  
        // 先删除所有的类名
        for (let i = 0; i < liLists.length; i++) {
            liLists[i].classList.remove('current');
        }
        var currentClass = target.getAttribute('class');
        if (currentClass) {
            currentClass = currentClass.concat(" current");
        } else {
            currentClass = "current";
        }
        target.setAttribute("class", currentClass);
        initFlight();
        if (isFullScreen()) {
          let legend = {};
          let toolbox = {};
  
          if (window.GLOBAL_OPTIONS.toolbox[0]) {
            toolbox = window.GLOBAL_OPTIONS.toolbox[0];
          } else {
            toolbox = window.GLOBAL_OPTIONS.toolbox;
          }
          toolbox.left = null;
          toolbox.top = 30;
          toolbox.right = 30;
          toolbox.bottom = null;
          toolbox.feature.myFull.icon = "image://https://img.alicdn.com/imgextra/i4/O1CN01WjTtjv1xCuj5ZiLyZ_!!6000000006408-2-tps-200-200.png";
  
          // 标题
          window.GLOBAL_OPTIONS.title[0].left = 10;
          window.GLOBAL_OPTIONS.title[0].bottom = 120;
          if (window.GLOBAL_OPTIONS.legend[0]) {
            legend = window.GLOBAL_OPTIONS.legend[0];
          } else {
            legend = window.GLOBAL_OPTIONS.legend;
          }
          legend.left = 10;
          legend.bottom = 20;
        } else {
          let toolbox = {};
          let legend = {};
          if (window.GLOBAL_OPTIONS.toolbox[0]) {
            toolbox = window.GLOBAL_OPTIONS.toolbox[0];
          } else {
            toolbox = window.GLOBAL_OPTIONS.toolbox;
          }
          toolbox.top = window.screen.height > 1200 ? 72 : 122;
          toolbox.right = 358;
  
          toolbox.feature.myFull.icon = "image://https://img.alicdn.com/imgextra/i1/O1CN01uVpZCF1V30vM3BxGI_!!6000000002596-2-tps-200-200.png";
          
          window.GLOBAL_OPTIONS.title[0].left = 340;
          window.GLOBAL_OPTIONS.title[0].bottom = 200;
  
          if (window.GLOBAL_OPTIONS.legend[0]) {
            legend = window.GLOBAL_OPTIONS.legend[0];
          } else {
            legend = window.GLOBAL_OPTIONS.legend;
          }
          legend.left = 340;
          legend.bottom = window.screen.height > 1200 ? 395 : 345;
        }
        const { geo = [] } = window.GLOBAL_OPTIONS;
        if (geo && geo.length) {
          geo.forEach(item => {
            item.zoom = GLOBAL_ZOOM;
          });
        };
        setLegendPos();
        resetLayoutSize();
        if (window.GLOBAL_OPTIONS.series[GLOBAL_SERIES_INDEX]) {
          window.GLOBAL_OPTIONS.series[GLOBAL_SERIES_INDEX].lineStyle.normal.width = GLOBAL_LINE_WIDTH;
        }
        console.log('----click---click---window.GLOBAL_OPTIONS----', window.GLOBAL_OPTIONS);
        china.setOption(window.GLOBAL_OPTIONS);
      });
  
      window.onresize = function() {
        
        let h;
        let w = mapWidth;
        
        if (isFullScreen()) {
          setFull(true);
          h = window.screen.height;
          w = screenWidth;
        } else {
          setFull(false);
          h = window.screen.height;
          w = mapWidth; // 要加200
        }
        console.log('---h---', h);
        china.resize({
          height: h,
          width: w,
        });
      }
      // 捕捉到georoam事件,使下层的geo随着上层的geo一起缩放拖拽
      china.off('georoam').on('georoam', function(params) {
        var option = china.getOption();
  
        const { geo = [] } = option;
        if (geo && geo.length) {
          GLOBAL_ZOOM = geo[0].zoom || 1;
        }
        if (params.zoom != null && params.zoom != undefined) { // 捕捉到缩放时
          option.geo[1].zoom = option.geo[0].zoom; // 下层geo的缩放跟着上层的geo一起改变
          option.geo[1].center = option.geo[0].center; // 下层的geo的中心位置随着上层的geo一起改变
        } else {
          option.geo[1].center = option.geo[0].center; // 下层的geo的中心位置随着上层的geo一起改变
        }
        china.setOption(option);
      });
      china.off('mouseout').on('mouseout', function(params) {
        if (!isAlreadyClickFlight) {
          if (params.data) { // 有数据说明移到了飞线图那个地方
            setVisible(true);
          }
        }
      });
      china.off('click').on('click', { type: 'effectScatter' }, function(params) {
        let {
          data = {},
        } = params;
        initFlight();
        setLegendPos();
        resetLayoutSize();
        console.log('----window.GLOBAL_OPTIONS----', window.GLOBAL_OPTIONS);
        
        const cloneOptions = _.cloneDeep(window.GLOBAL_OPTIONS);
  
        let seriesIndex = getSeriesIndex(cloneOptions, params);
  
        if (cloneOptions.series[seriesIndex] && cloneOptions.series[seriesIndex].lineStyle) {
          GLOBAL_LINE_WIDTH = 6;
          // 改变状态,已经点击了飞线图
          GLOBAL_SERIES_INDEX = seriesIndex;
          setTimeout(() => {
            cloneOptions.series[seriesIndex].lineStyle.normal.width = 6;
            setFixed(true);
            setClick(true);
            setVisible(false);
            isAlreadyClickFlight = true;
          }, 300);
          const datas = params.data ? params.data : null;
          setLineData(datas);
        }
        if (isFullScreen()) {
          let legend = {};
          let toolbox = {};
  
          if (cloneOptions.toolbox[0]) {
            toolbox = cloneOptions.toolbox[0];
          } else {
            toolbox = cloneOptions.toolbox;
          }
          toolbox.left = null;
          toolbox.top = 30;
          toolbox.right = 30;
          toolbox.bottom = null;
          toolbox.feature.myFull.icon = "image://https://img.alicdn.com/imgextra/i4/O1CN01WjTtjv1xCuj5ZiLyZ_!!6000000006408-2-tps-200-200.png";
          
          if (cloneOptions.legend[0]) {
            legend = cloneOptions.legend[0];
          } else {
            legend = cloneOptions.legend;
          }
          legend.left = 10;
          legend.bottom = 20;
        } else {
          
          let toolbox = {};
          let legend = {};
          if (cloneOptions.toolbox[0]) {
            toolbox = cloneOptions.toolbox[0];
          } else {
            toolbox = cloneOptions.toolbox;
          }
          toolbox.top = window.screen.height > 1200 ? 72 : 122;
          toolbox.right = 358;
  
          toolbox.feature.myFull.icon = "image://https://img.alicdn.com/imgextra/i1/O1CN01uVpZCF1V30vM3BxGI_!!6000000002596-2-tps-200-200.png";
  
          if (cloneOptions.legend[0]) {
            legend = cloneOptions.legend[0];
          } else {
            legend = cloneOptions.legend;
          }
          legend.left = 340;
          legend.bottom = window.screen.height > 1200 ? 395 : 345;
        }
        const { geo = [] } = cloneOptions;
        if (geo && geo.length) {
          geo.forEach(item => {
            item.zoom = GLOBAL_ZOOM;
          });
        };
        
        console.log('----isFull--isFull000---', cloneOptions);
        window.GLOBAL_OPTIONS = cloneOptions;
        setTimeout(() => {
          china.setOption(cloneOptions);
        }, 350);
        const { item = {} } = data;
        const rets = [];
        const { carSizeData = [] } = item;
        if (carSizeData && carSizeData.length) {
          carSizeData.forEach(item => {
            rets.push({
              label: item[0],
              value: item[1],
            })
          });
          setData2(rets);
        }
        params.event.event.stopPropagation();
      });
      const closed = document.getElementById('closed');
      const showBtn = document.getElementById('show-table-btn');
      closed?.addEventListener('click', function(e) {
        console.log('---closed---');
        setVisible(true);
        e.stopPropagation();
      });
      showBtn?.addEventListener('click', function(e) {
        setVisible(false);
        e.stopPropagation();
      });
    }, [datas]);
    
    console.log(1111);
    return (
      <div>
        <div className="mychart-container" id="myChart">
          <div id="wrapMap" className={ isFull ? '' : (window.screen.height > 1200 ? 'wrapMap current' : 'wrapMap' ) }>
            <div id="china"></div>
            <div className={ isFull ? 'south-sea isfull' : ( window.screen.height > 1200 ? 'south-sea current' : 'south-sea')}></div>
          </div>
          <ul id="ul-list" className={ isFull ? 'ul-list isfull' : 'ul-list'}>
            <li className="current" data-index="0">碳排量</li>
            <li data-index="1">单位公里碳排量</li>
          </ul>
          <div className="show-table-container">
            <div id="show-table" className={
              !isFull ? (isVisible ? "show-table slideOutLeft" : (isClick ? "show-table slideInLeft" : "show-table slideOutLeft none")) : 
              (isVisible ? "show-table slideOutLeft isfull" : (isClick ? "show-table slideInLeft isfull" : "show-table slideOutLeft isfull none"))
            }>
              <div className="closed" id="closed"></div>
              <div className="msg1">车次</div>
              <div className={ isFull ? 'msg2 isfull' : 'msg2'}>单位公里碳排放量(t/km)</div>
              <div className="table-content">
                <Chart height={ isFull ? 233 : 155 } autoFit data={data2} padding={[30, 30, 30, 50]} >
                  <Interval position="label*value" size={8} />
                  <Tooltip visible={false} />
                  <Axis
                    name="value"
                    label={
                      {
                        style: {
                          fill: 'rgba(255,255,255,0.65)',
                        }
                      }
                    }
                    grid={
                      {
                        line: {
                          type: 'line',
                          style: {
                            stroke: '#2b2c38'
                          }
                        }
                      }
                    }
                  />
                  <Axis
                    name="label"
                    // visible={false}
                    label={
                      {
                        style: {
                          fill: 'rgba(255,255,255,0.65)',
                        }
                      }
                    }
                    line={{
                      style: {
                        stroke: '#2b2c38'
                      }
                    }}
                    tickLine={{
                      style: {
                        stroke: '#2b2c38'
                      }
                    }}
                  />
                </Chart>
              </div>
            </div>
          </div>
          <div id="show-table-btn" className={ 
            !isFull ? (isVisible ? 'show-table-btn' : 'show-table-btn none') : (isVisible ? 'show-table-btn isfull' : 'show-table-btn isfull none') 
          }></div>
          <div 
            className={ isFixed ? "alert-pos isFixed" : 'alert-pos' }
            style={{ top: xy_value ? xy_value.y + 'px' : '-9999px', left: xy_value ? xy_value.x + 'px' : '-9999px' }}
          >
            <div className='tool-tip'>
              <div className='title'>
                <span>{line_datas && line_datas.formName}</span>-
                <span>{line_datas && line_datas.toName}</span>
              </div>
              <div className='t-content'>
                <div>
                  <span className='desc'>单位公里排放量</span>
                  <span className='t-content'>
                    <i>{line_datas && line_datas.item && line_datas.item.unitCarbonEmission}</i>吨/公里
                  </span>
                </div>
                <div>
                  <span className='desc'>碳排量</span>
                  <span className='t-content'>
                    <i>{line_datas && line_datas.item && line_datas.item.totalCarbonEmission}</i></span>
                </div>
                <div>
                  <span className='desc'>总车次</span>
                  <span className='t-content'><i>{line_datas && line_datas.item && line_datas.item.totalTruckSize}</i></span>
                </div>
                <div>
                  <span className='desc'>总公里数</span>
                  <span className='t-content'><i>{line_datas && line_datas.item && line_datas.item.totalKilometres}</i>公里</span>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div className="isFull_default"></div>
        <div className="isShinkage_default"></div>
        <div className="isFull_default_hover"></div>
        <div className="isShinkage_default_hover"></div>
        <div className="show-table-btn-hover"></div>
        <div className="show-table-closed-hover"></div>
      </div>
    );
  };
  
  export default MyChart;

echarts如何设置地图外框,及实现多个geo缩放拖拽同步

设置地图边框的基本原理:两个地图叠加,第一个地图的底层地图设置边框,即 geo中的 itemStyle.normal的属性。(显示的是中国边界线的边框,较宽)。
第二个地图的上层地图设置边框,即 series中的 itemStyle.normal属性。

如上配置部分代码实现边框:

geo: [
  {
    map: 'china', // 地图名
    zlevel: 2, // geo显示级别,默认是0
    roam: true, // 设置为false,不启动roam缩放
    scaleLimit: { //滚轮缩放的极限控制
      min: 0.5,
      max: 3
    },
    selectedMode: false, // 不让点击
    zoom: 1,
    label: {
      emphasis: {
        show: false,
        areaColor: 'null',
      }
    },
    itemStyle: { // 顶层地图的样式,如地图区域颜色,边框颜色,边框大小等
      normal: {
        borderColor: '#33587F', // 边框颜色
        areaColor:'#31344a', // 地图区域颜色
        borderWidth: 1, // 边框大小
      },
      emphasis: {
        areaColor: 'null',
        color: 'rgba(37, 43, 61, 0)', //悬浮背景
      }
    }
  },
  {
    map: 'china',
    zoom: 1,
    roam: true,
    selectedMode: false, // 不让点击
    scaleLimit: { //滚轮缩放的极限控制
      min: 0.5,
      max: 3
    },
    label: {
      emphasis: {
        show: false,
        areaColor: 'null',
      }
    },
    itemStyle: { // 地图的样式,如地图区域颜色,边框颜色,边框大小等
      normal: {
        borderColor: '#56b7ec', // 边框颜色
        areaColor:'#31344a', // 地图区域颜色
        borderWidth: 4, // 边框大小
      },
      emphasis: {
        color: 'rgba(37, 43, 61, 0)', //悬浮背景
        areaColor: null,
      }
    }
  }
]

如上代码,geo 设置了 true,允许缩放,但是两个地图如何实现拖拽缩放同步呢?

解决的办法:添加如下js监听代码,捕捉 georoam事件,使下层 geo 跟着上层 geo 一起拖拽缩放。如下监听代码:

// 捕捉到georoam事件,使下层的geo随着上层的geo一起缩放拖拽
china.off('georoam').on('georoam', function(params) {
  var option = china.getOption(); 
  const { geo = [] } = option;
  if (geo && geo.length) {
    GLOBAL_ZOOM = geo[0].zoom || 1;
  }
  if (params.zoom != null && params.zoom != undefined) { // 捕捉到缩放时
    option.geo[1].zoom = option.geo[0].zoom; // 下层geo的缩放跟着上层的geo一起改变
    option.geo[1].center = option.geo[0].center; // 下层的geo的中心位置随着上层的geo一起改变
  } else {
    option.geo[1].center = option.geo[0].center; // 下层的geo的中心位置随着上层的geo一起改变
  }
  china.setOption(option);
});

如上代码只是对 缩放有效,对拖动的时候,底层地图和上层地图还是不能同步到,需要把 animation: false, 设置false 解决阻止拖拽时上下图层不同步的问题。