实现Echarts的世界中国地图以及下钻功能

背景

公司选用的可视化工具为finereport, 只买了填充报表,但是复杂图表地图,需要单独收费,看了echarts官方提供的地图(看着从2.0到5.0了, 好像是4.x 被Apache看上了),正好符合公司的需求,故有了以下的DEMO地址:
https://wl.juneyaoair.com/airecharts/chart_background/?type=map
有问题的可以与我联系,哈哈哈

项目启动大概是2020年的6月份初,开发时间工作七七八八,对接接口,也有4,5天左右了,事隔9个月,现在再拿出看,想要看懂,还是需要花点时间理解的。

怕时间久了,就忘没有,故重新理解一遍,做个记录。

整体图标分层:

  • 第一层:先注册世界地图,
  • 第二层:中国地图
  • 第三层: 散点图
  • 第四点:线图

静态资源下载

需要世界地图经纬度的json, common对象,中国省市区的经纬度,
地图json网盘下载地址:
链接:https://pan.baidu.com/s/1jNZ_YSYAUlMYiBUFq6EV4w
提取码:405q
示例公共配置网盘下载地址:https://pan.baidu.com/s/13jhRFWaAFAiT5rqCtkvswg
提取码:ev8m

前端代码

<template lang="pug">
.wrap-chart
  //- 自定义加载组件
  c-loading(
    :show="loadEcharts",
    loadingText="图表加载中,请稍后..."
    v-if="loadEcharts"
  )
  div(
    :v-if="!loadEcharts",
    id="main",
    :class="{'main-province': !!provinceName}",
    :style="`width: ${width};height:${height};`"
  )
  .tips
    ul
      li(
        v-for="(item, index) in tips",
        :key="index",
        :class="{'return-world': !index}",
        @click="setMap"
      ) {{ item }}
</template>

<script>
// js工具库,[http://lodash.net/docs/4.16.1.html](http://lodash.net/docs/4.16.1.html)
import _ from 'lodash'
// 按需应用echarts的模块,参考官方地址:[https://echarts.apache.org/zh/tutorial.html#%E5%9C%A8%E6%89%93%E5%8C%85%E7%8E%AF%E5%A2%83%E4%B8%AD%E4%BD%BF%E7%94%A8%20ECharts](https://echarts.apache.org/zh/tutorial.html#%E5%9C%A8%E6%89%93%E5%8C%85%E7%8E%AF%E5%A2%83%E4%B8%AD%E4%BD%BF%E7%94%A8%20ECharts)
import common from '~/plugins/common'
import echarts from 'echarts/lib/echarts'
import 'echarts/lib/chart/map'
import 'echarts/lib/chart/heatmap'
import 'echarts/lib/chart/effectScatter'
import 'echarts/lib/chart/lines'
require('echarts/lib/component/geo')
require('echarts/lib/component/graphic')
require('echarts/lib/component/tooltip')
require('echarts/lib/component/visualMap')
import CLoading from '~/components/Loading'
export default {
  data () {
    return {
      // 宽度
      width: this.getLens(),
      // 高度
      height: this.getLens('height'),
      // 颜色列表
      colorList: _.get(this.$route, 'query.colorList'),
      // 主题色
      theme: _.get(this.$route, 'query.theme') || 'blue',
      // 图表类型, 目前只有一个地图目前用不到,后续可以扩展用
      chartType: _.get(this.$route, 'query.type') || 'map',
      // ! loading标识, 待删除
      loadEcharts: true,
      // 配置对象
      option: {},
      // 站点散点列表
      scatterList: [],
      // 飞机线条列表
      linesList: [],
      // 机场列表
      airportList: [],
      // 层级提示
      tips: ['世界'],
      // 保持访问的省名称
      provinceName: '',
      // 计时器
      mapChartTimer: '',
      // 区域计时器
      provinceChartTimer: '',
      // 地图缩略图
      visualMap: {
        type: 'piecewise',
        min: 1,
        max: 5,
        left: '2%',
        bottom: 10,
        itemWidth: 16,
        itemHeight: 10,
        orient: 'vertical',
        splitNumber: 4,
        seriesIndex: 0,
        show: true,
        inRange: {
          color: common.mapColor
        },
        textStyle: {
          color: '#ffffff',
          fontSize: 12
        },
        formatter: function (params) {
          const num = params - 1
          const categoriesList = [
            '华北销售处',
            '华南销售处',
            '国际销售处',
            '上海销售处'
          ]
          return _.nth(categoriesList, num)
        }
      },
      // 经纬度映射关系
      geoCoordMap: {},
      // 是否显示全屏
      fullScreen: false
    }
  },
  components: {
    CLoading
  },
  mounted () {
  	// 初始化图表对象
    this.myChart = echarts.init(document.getElementById('main'))
    // 设置图表
    this.setChart()
    // 下钻地图
    this.myChart.on('click', params => {
      // 获取省会名称
      this.provinceName = params.name
      // 限制中文省会名称才能执行下钻,直接return
      const pattern = new RegExp('[\u4E00-\u9FA5]+')
      if (!pattern.test(this.provinceName)) { return }
      // 清理定时器
      clearTimeout(this.mapChartTimer)
      clearTimeout(this.provinceChartTimer)
      // 清除图表
      this.myChart.clear()
      // 中国地图下钻方法
      this.downMap(this.myChart, this.option, this.provinceName)
    })
  },
  methods: {
    /**
     * 对画布长宽配置
     */
    getLens (type = 'width') {
      let lenValue = _.get(this.$route, `query.${type}`)
      // TODO 此处需要检查 lenValue 是否为数值
      if (!lenValue) {
        switch (type) {
          case 'width':
            lenValue = '100%'
            break
          case 'height':
            lenValue = '100%'
            break
        }
        return lenValue
      }
      return `${lenValue}px`
    },
    /**
     * 设置图表
     */
    async setChart () {
      // TODO 此处可以根据不同图表类型,接入不同图表(原规划是一个页面根据展示不同图表)
      switch (this.chartType) {
        case 'map':
          // 首次显示加载效果
          this.loadEcharts = true
          // 获取经纬度配置(返回是有航线在运行的站点经纬度)
          await this.getCoordinateConfig()
          // 设置地图
          this.setMap()
          break
      }
    },
    /**
     * 地图(航线)转换数据
     */
    convertData (data) {
      const res = []
      _.forEach(data, v => {
        let fromCoord = _.get(this.geoCoordMap, v.depName)
        let toCoord = _.get(this.geoCoordMap, v.arrName)
        if (!!fromCoord && !!toCoord) {
          fromCoord = JSON.parse(fromCoord)
          toCoord = JSON.parse(toCoord)
          res.push({
            fromName: v.depName,
            toName: v.arrName,
            coords: [fromCoord, toCoord],
            value: 0,
            flightNo: this.$util.addDefaultData(_.get(v, 'flightNo')), // 航班号
            bookingNumber: this.$util.addDefaultData(_.get(v, 'bookingNumber')), // 订舱
            volume: this.$util.addDefaultData(_.get(v, 'volume')), // 货量
            income: this.$util.addDefaultData(_.get(v, 'income')) // 收入
          })
        }
      })
      return res
    },
    /**
     * 获取scatter图表的数据
     */
    convertSingleData () {
      const res = []
      let middleObj = {}
      _.forEach(this.airportList, v => {
        let geoCoord = _.get(this.geoCoordMap, v.airlineName)
        if (geoCoord) {
          geoCoord = JSON.parse(geoCoord)
          middleObj = {
            name: v.airlineName,
            value: geoCoord.concat(0)
          }
          middleObj = _.assign({}, middleObj, v)
          res.push(middleObj)
        }
      })
      return res
    },
    /**
     * 获取经纬度基础配置
     */
    async getCoordinateConfig () {
      const data = await this.$api.get(`/map/getAirlineLocationConfig`)
      // 配置经纬度
      this.geoCoordMap = data
    },
    /**
     * 参数name有值为下钻拉取的数据
     */
    async getStationData (name = '') {
      const requestUrl = !name ? 'getAirlineCabinData' : 'getAirlineCabinDataToProvinceList'
      const data = await this.$api.get(`/map/${requestUrl}`, {
        provinceName: name
      })
      this.$set(this, 'airportList', data)
    },
    /**
     * 参数name有值为下钻拉取的数据
     */
    async getLinesData (name = '') {
      // 有城市名称和没有城市名称是不同的列表
      const requestUrl = !name ? 'getAirlineLegList' : 'getAirlineLegDetailList'
      const data = await this.$api.get(`/map/${requestUrl}`, {
        provinceName: name
      })
      return data
    },
    /**
     * 获取站点相关联的城市列表数据
     */
    async getCityList (name = '') {
      const data = await this.$api.get(`/map/getRelateProvinceList`, {
        provinceName: name
      })
      return data
    },
    /**
     * 获取散点图数据: 站点进出港数据
     */
    async getScatterList (name) {
      // 获取机场站点数据和进出港数据
      await this.getStationData(name)
      const scatterList = []
      if (_.size(this.airportList)) {
        // 散点图数据
        scatterList.push(
          {
            type: 'effectScatter',
            coordinateSystem: 'geo',
            zlevel: 2,
            rippleEffect: {
              brushType: 'stroke'
            },
            // hover效果
            tooltip: {
              show: true,
              formatter: function (params) {
                const data = _.get(params, 'data')
                return `${params.marker}${params.name}<br/>出港订舱量:${data.outBookingTotal}<br/>出港货量:${data.outVolume}<br/>出港收入:${data.outIncome}<br/>累计班次:${data.outFlightNumber}<br/><br/>进港订舱量:${data.inBookingTotal}<br/>进港货量:${data.inVolume}<br/>进港收入:${data.inIncome}<br/>累计班次:${data.inFlightNumber}`
              }
            },
            label: {
              normal: {
                show: true,
                fontSize: 18,
                color: '#ffffff',
                position: 'right',
                formatter: '{b}'
              }
            },
            symbolSize: function (val) {
              return 10
            },
            // 散点图的颜色值的配置
            itemStyle: {
              normal: {
                // color: '#fa8737'
                color: 'rgba(255, 255, 255, 0.7)'
              }
            },
            data: this.convertSingleData()
          }
        )
      }
      this.scatterList = scatterList
      return scatterList
    },
    /**
     * 获取航线图数据
     */
    async getLinesList (name) {
      const linesData = await this.getLinesData(name)
      // 拉取航线数据
      // 航线数据
      let i = 0
      const linesList = []
      if (_.size(linesData)) {
        _.forEach(linesData, item => {
          linesList.push(
            {
              type: 'lines',
              zlevel: 1,
              // 线特效的配置
              effect: {
                show: true,
                // 特效动画的时间,单位为 s
                period: 6,
                // 特效尾迹的长度
                trailLength: 0.7,
                color: '#fff',
                // 特效标记的大小
                symbolSize: 3
              },
              lineStyle: {
                normal: {
                  color: common.mapColor[i],
                  width: 0,
                  curveness: 0.2
                }
              },
              data: this.convertData(item)
            },
            {
              type: 'lines',
              zlevel: 2,
              symbol: ['none', 'arrow'],
              symbolSize: 10,
              effect: {
                show: true,
                // 特效动画的时间,单位为 s
                period: 6,
                // 特效尾迹的长度
                trailLength: 0,
                // 特效图形的标记
                symbol: common.planePath,
                // 特效标记的大小
                symbolSize: 15
              },
              lineStyle: {
                normal: {
                  color: common.planeColor[i],
                  // color: '#ffffff'
                  width: 1,
                  opacity: 0.6,
                  curveness: 0.2
                }
              },
              data: this.convertData(item),
              tooltip: {
                formatter: function (params) {
                  // 航线上的hover
                  const data = _.get(params, 'data')
                  return `${data.fromName} - ${data.toName}<br/>航班号:${data.flightNo}<br/>订舱:${data.bookingNumber}KG<br/>货量:${data.volume}KG<br/>收入:${data.income}元`
                }
              }
            }
          )
          i = i + 1
        })
      }
      this.linesList = linesList
      return linesList
    },
    /**
     * 获取城市JSON,关联的地图的JSON合并
     */
    async getCityJson (provinceName) {
      // 获取当前点击的拼音
      const curGeoJson = {
        'type': 'FeatureCollection',
        'features': [],
        'UTF8Encoding': true
      }
      // 获取相关航线的拼音列表,以方便整理地理JSON
      let cityList = await this.getCityList(provinceName)
      cityList = [provinceName, ...cityList]
      // 转拼音列表
      const pinyinList = []
      _.forEach(cityList, v => {
        pinyinList.push(common.cityToPinyin[v])
      })
      // 拼接JSON
      _.forEach(pinyinList, v => {
        if (!v) return
        let middleGeo = {}
        middleGeo = _.cloneDeep(require(`~/static/json/provinces/${v}.json`))
        curGeoJson.features = _.concat(curGeoJson.features, middleGeo.features)
      })
      return curGeoJson
    },
    /**
     * 设置图表
     */
    async setMap () {
      this.loadEcharts = true
      clearTimeout(this.mapChartTimer)
      clearTimeout(this.provinceChartTimer)
      this.provinceChartTimer = null
      // 显示提示文本
      this.tips = ['世界']
      // 页面宽度变动
      window.onresize = this.myChart.resize
      // 世界地图json
      const mapJson = require('~/static/json/world.json')
      // 注册世界地图
      echarts.registerMap('world', mapJson)
      // 获取散点数据
      await this.getScatterList()
      // 获取航线数据
      await this.getLinesList()
      // 配置项
      const option = {
        // 地图背景色
        backgroundColor: common.bgColor,
        // hover效果
        tooltip: {
          show: true,
          formatter: function (params) {
            return params.name
          }
        },
        // 地理位置配置, 底图
        geo: {
          map: 'world',
          // 是否可以滚动
          roam: true,
          zoom: 5,
          // 中国中心的经纬度
          center: [104.2978515625, 35.8544921875],
          // 文本颜色配置
          label: {
            show: true,
            position: ['100%', '50%'],
            normal: {
              show: true,
              fontSize: 14,
              textStyle: {
                color: '#fff'
              },
              position: ['100%', '50%'],
              formatter: function (params) {
                const name = params.name
                const pattern = new RegExp('[\u4E00-\u9FA5]+')
                const internationalList = {
                  'Japan': '日本',
                  'Russia': '俄罗斯',
                  'Canada': '加拿大',
                  'Hungary': '匈牙利',
                  'Indonesia': '印度尼西亚',
                  'Greece': '希腊',
                  'Germany': '德国',
                  'Italy': '意大利',
                  'Czech Republic': '捷克',
                  'Slovakia': '斯洛伐克',
                  'Singapore': '新加坡',
                  'Cambodia': '柬埔寨',
                  'France': '法国',
                  'Thailand': '泰国',
                  'myanmar': '缅甸',
                  'Romania': '罗马尼亚',
                  'Laos': '老挝',
                  'United Kingdom': '英国',
                  'Netherlands': '荷兰',
                  'Philippines': '菲律宾',
                  'Spain': '西班牙',
                  'Afghanistan': '阿富汗',
                  'Korea': '韩国',
                  'Malaysia': '马来西亚'
                }
                // 台湾,香港,澳门修正为中国(台湾),中国(香港),中国(澳门)
                if (_.includes(common.islandCH, name)) {
                  return `中国(${name})`
                }
                // 中文直接显示
                if (pattern.test(name)) {
                  return name
                }
                // 英文为映射后,再显示
                const internationalName = _.get(internationalList, name)
                if (internationalName) {
                  return internationalName
                }
                return ''
              }
            },
            emphasis: {
              textStyle: {
                color: '#fff'
              }
            }
          },
          itemStyle: {
            normal: {
              color: 'transparent', // 地图背景色
              borderColor: '#516a89', // 省市边界线00fcff 516a89
              borderWidth: 1
            },
            emphasis: {
              color: 'rgba(238, 120, 0, 1)' // 悬浮背景
            }
          },
          // 单独对台湾、澳门、香港做了强调标记为红色
          regions: [
            {
              name: '台湾',
              emphasis: {
                itemStyle: {
                  areaColor: '#DE2910'
                }
              },
              selected: true
            },
            {
              name: '澳门',
              emphasis: {
                itemStyle: {
                  areaColor: '#DE2910'
                }
              },
              selected: true
            },
            {
              name: '香港',
              emphasis: {
                itemStyle: {
                  areaColor: '#DE2910'
                }
              },
              selected: true
            }
          ]
        },
        // 统一几大区的配色,高度依赖common对象
        visualMap: this.visualMap,
        series: [
          // 地图配置
          {
            name: '中国地图',
            type: 'map',
            mapType: 'china',
            geoIndex: 0,
            label: {
              normal: {
                show: true
              },
              emphasis: {
                show: true
              }
            },
            data: common.allProvinceData, // 默认是省级数据
            tooltip: {
              show: true,
              formatter: function (params, ticket, callback) {
                const name = _.get(params, 'name')
                // 台湾,香港,澳门修正为中国(台湾),中国(香港),中国(澳门)
                if (_.includes(common.islandCH, name)) {
                  return `中国(${name})`
                }
                return name
              }
            }
          }
        ]
      }
      // 合并地图,线条以及散点图数据
      option.series = _.concat([],
        // 地图
        option.series,
        // 散点图
        this.scatterList,
        // 线图
        this.linesList
      )
      // 设置配置项
      this.option = option
      // 清空当前选中值
      this.provinceName = ''
      // 绘制图表
      this.myChart.setOption(option, true)
      // 隐藏loading
      this.loadEcharts = false
      const second = _.get(this.$route.query, 'second') || 1800
      this.mapChartTimer = setTimeout(() => {
        if (!this.provinceName) {
          this.setMap()
        }
      }, second * 1000)
    },
    /**
     * 地图下钻
     */
    downMap (myChart, option, provinceName) {
      // 清理定时器
      clearTimeout(this.mapChartTimer)
      clearTimeout(this.provinceChartTimer)
      if (!this.provinceName) return
      // 拉取地级市的相关联的所有数据
      // 拼音
      const cityPinyin = common.cityToPinyin[provinceName]
      if (provinceName !== 'china' && cityPinyin) {
        this.tips = ['世界 > 中国', `> ${provinceName}`]
        // 获取当前城市和下钻城市的JSON
        this.getCityJson(provinceName).then(curGeoJson => {
          // 清除图表
          this.myChart.clear()
          // 设置下钻地图
          echarts.registerMap(provinceName, curGeoJson)
          // 设置城市数据
          const childOption = _.cloneDeep(option)
          childOption.series = [_.nth(childOption.series, 0)]
          // 城市数据
          childOption.series[0].data = common.allCityData
          // 获取散点数据
          this.getScatterList(provinceName).then(v => {
            if (_.size(v)) {
              childOption.series = _.concat([],
              // 地图
                childOption.series,
                // 散点图
                v
              )
            }
            // 获取航线数据
            this.getLinesList(provinceName).then(v => {
              if (_.size(v)) {
                childOption.series = _.concat([],
                  // 地图
                  childOption.series,
                  // 散点图
                  v
                )
              }
              // 设置地图展示中心
              childOption.geo.center = _.get(
                _.first(_.get(curGeoJson, 'features')
                ), 'properties.cp')
              // 设置比例大小
              childOption.geo.zoom = 2
              // 重置图表
              this.resetOption(this.myChart, childOption, provinceName)
              // 循环拉取数据
              const second = _.get(this.$route.query, 'second') || 1800
              this.provinceChartTimer = setTimeout(() => {
                if (!this.provinceName) {
                  clearTimeout(this.provinceChartTimer)
                  return
                }
                this.downMap(this.myChart, option, this.provinceName)
              }, second * 1000)
            })
          })
        })
      }
    },
    /**
     * 重置图表
     */
    resetOption (myChart, option, name) {
      // 清除图表
      this.myChart.clear()
      // 设置图表名称
      option.geo.map = name
      this.$set(this, 'option', option)
      this.$forceUpdate()
      // 重置图表
      this.myChart.setOption(option, true)
    },
    /**
     * 操作全屏
     */
    operateScreen () {
      // 跳转至外部
      this.$router.push({
        name: 'chart',
        query: {
          type: 'map',
          position: 'out'
        }
      })
      this.fullScreen = !this.fullScreen
      if (this.fullScreen) {
        this.enterScreen()
        return
      }
      this.exitFullScreen()
    },
    /**
     * 退出全屏
     */
    exitFullScreen () {
      if (document.exitFullscreen) {
        document.exitFullscreen()
      } else if (document.msExitFullscreen) {
        document.msExitFullscreen()
      } else if (document.mozCancelFullScreen) {
        document.mozCancelFullScreen()
      } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen()
      }
    },
    /**
     * 全屏
     */
    enterScreen () {
      var element = document.documentElement
      if (element.requestFullscreen) {
        element.requestFullscreen()
      } else if (element.msRequestFullscreen) {
        element.msRequestFullscreen()
      } else if (element.mozRequestFullScreen) {
        element.mozRequestFullScreen()
      } else if (element.webkitRequestFullscreen) {
        element.webkitRequestFullscreen()
      }
    }
  }
}
</script>

<style lang="stylus">
@import "~assets/css/variable"
.wrap-chart
  width 100%
  height 100%
  background-image url('~assets/img/bg.png')
  background-position 100% 100%
  background-size cover
  background-repeat no-repeat
  .main-province
    canvas:nth-child(2)
      display none
  .tips
    position absolute
    flexAlign()
    bottom 7%
    right 5%
    color $white
    font-size 14px
    margin 20px 0
    padding 15px
    background-color #232323
    border-radius 100px
    box-shadow inset 0 5px 10px -5px #191919, 0 1px 0 0 #444
    ul
      flexAlign()
      flex-direction row
      li
        display flex
        border-bottom 3px solid none
        box-sizing border-box
        &:first-child
          margin-right 5px
  .full-screen
    bottom 10px
    margin-bottom 0px
    &:hover
      cursor pointer
      opacity 0.5
  .return-world
    &:hover
      cursor pointer
      opacity 0.5
</style>

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

monkey01127

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值