Echarts实现省地图并下探到县区(Vue3 + Echarts)

711 阅读5分钟

背景介绍

需要画一个江西省地图,可以下探到县区,在县区中可以进行打标记,以下为个人总结与学习Echarts文档

1.获取地图json

获取江西省地图json和地市json,从阿里云数据可视化平台中下载,保存在项目中(也可以通过接口去请求数据)

2.配置江西省地图option

const option = {
    title: {
      text: "“足迹”江西省", // 标题
      subtext: "", // 副标题
      show: true, // 是否显示标题
      left: "center", // 标题x轴位置,可以为像素20,百分比20%,字符串'center''left''right'
      top: 20, // 标题y轴位置 同left
      textStyle: {
        color: "#ccc", // 主标题文字的颜色。
      },
    },
    geo: { // geo:地理坐标系组件
      show: true, // 是否显示,默认为true
      map: 'jiangxi', // registerMap注册的地图名称,与series-map中的map名需要一致
      roam: true, // 是否开启缩放与平移,可以设置成 'scale' 或者 'move'。设置成 true 为都开启
      aspectScale: 0.75, // 地图长宽比
      zoom: 1.1, // 地图缩放比例
      animationDurationUpdate: 0, // 缩放与平移后的动画的时长
      itemStyle: { // 地图区域的多边形图形样式
        areaColor: '#013C62',
        shadowColor: '#182f68',
        shadowOffsetX: 0,
        shadowOffsetY: 25
      },
      emphasis: { // 高亮状态下的多边形和标签样式,比如鼠标悬浮样式变化
        itemStyle: {
          areaColor: '#2AB8FF',
          borderWidth: 0,
          color: 'green',
          label: {
            show: false
          }
        }
      }      
    },
    series: [
      {
        type: "map", // 地图:地图主要用于地理区域数据的可视化
        map: "jiangxi", // registerMap注册的地图名称,与geo中的map名需要一致
        roam: true, // 是否开启缩放与平移
        animationDurationUpdate: 0, // 缩放与平移后的动画的时长
        selectedMode: false, // 是否支持选中,支持布尔值和字符串,字符串取值可选'single'表示单选,或者'multiple'表示多选
        label: { // 省、市、县名
          normal: { // 正常时候显示的样式
            show: true,
            textStyle: {
              color: '#fff'
            }
          },
          emphasis: { // 高亮时候显示的样式(比如鼠标点击,鼠标悬浮)
            textStyle: {
              color: '#fff'
            }
          }
        },
        itemStyle: { // 整个地图的样式
          normal: { // 正常情况下的样式
            borderColor: '#2ab8ff',
            borderWidth: 1.5,
            areaColor: '#12235c'
          },
          emphasis: { // 高亮情况下的样式,也可以像geo那样配置
            areaColor: '#2AB8FF',
            borderWidth: 0,
            color: 'green'
          }
        },
        zoom: 1.1, // 缩放比例
      },
    ],
}

3.绘制江西省地图

let jsonArr = [
  {
    name: '江西省',
    alias: 'jiangxisheng',
    json: jiangxisheng, // 自行下载json,并引入
  },
  {
    name: '赣州市',
    alias: 'ganzhoushi',
    json: ganzhoushi,
    coor: [114.940278, 25.85097]
  },
  {
    name: '吉安市',
    alias: 'jianshi',
    json: jianshi,
    coor: [114.986373, 27.111699]
  },
  {
    name: '上饶市',
    alias: 'shangraoshi',
    json: shangraoshi,
    coor:  [117.971185, 28.44442]
  },
   {
    name: '九江市',
    alias: 'jiujiangshi',
    json: jiujiangshi,
    coor: [115.992811, 29.712034],
  },
  {
    name: '抚州市',
    alias: 'fuzhoushi',
    json: fuzhoushi,
    coor: [116.358351, 27.98385],
  },
  {
    name: '宜春市',
    alias: 'yichunshi',
    json: yichunshi,
    coor: [114.391136, 27.8043],
  },
  {
    name: '南昌市',
    alias: 'nanchangshi',
    json: nanchangshi,
    coor: [115.892151, 28.676493],
  },
  {
    name: '景德镇市',
    alias: 'jingdezhenshi',
    json: jingdezhenshi,
    coor: [117.214664, 29.29256],
  },
   {
    name: '萍乡市',
    alias: 'pingxiangshi',
    json: pingxiangshi,
    coor:  [113.852186, 27.622946],
  },
   {
    name: '鹰潭市',
    alias: 'pingxiangshi',
    json: yingtanshi,
    coor: [117.033838, 28.238638],
  },
  {
    name: '新余市',
    alias: 'xinyushi',
    json: xinyushi,
    coor:  [114.930835, 27.810834],
  },
]
/**
* @params mapName 地图名(获取对应的json)mapData 标点数据
*/
function echartInit(mapName, mapData = []) {
  const myChart = echarts.init(document.getElementById("main")) // 获取DOM元素
  let mapInfo = {}
  jsonArr.forEach(item => {
    if (item.name === mapName) {
      mapInfo = item
    }
  })
  geoJson = mapInfo.json
  echarts.registerMap("jiangxi", geoJson)
  const option = [...] // 步骤2的option
  myChart.setOption(option);
}

4.配置标记

// 在步骤2中的option.series添加配置
series=[
  {
  type: 'map',
  ...
  },
  // 1.增加散点气泡图1、2、3,
  {
    type: 'scatter', // 散点气泡图
    coordinateSystem: 'geo', // 使用的坐标系为geo地理坐标系
    symbol: '/images/example.png', // 使用的图标
    symbolSize: [32, 41], // [width,height]
    symbolOffset: [0, -20], // [top, left]
    z: 9999, // css中的z-index
    data: mapData, // 根据数据在哪里显示,数据格式为[{name:'宜丰县', value: ['114.767853', '28.448926']}]
  },
  // 2.增加散点气泡图4
  {
    type: 'scatter',
    coordinateSystem: 'geo',
    label: { 
      normal: {
        show: true,
        formatter: function (params) { // 自定义label
          const value = params.data.county;
          const text = `{tline|${value}}`;
          return text;
        },
        rich: { 
          tline: { // 配置formatter中的tline属性
            padding: [0, 0],
            color: '#ABF8FF',
            fontSize: 12,
          },
        },
      },
    },
    symbol: '/images/tu4.png', // 配置一个背景图
    symbolSize: [50, 25], // 图标大小
    symbolOffset: [0, -60], // 图标位置
    z: 999,
    data: mapData,
  },
  // 3.增加带有涟漪特效动画的散点(气泡)图
  {
    type: 'effectScatter',
    coordinateSystem: 'geo',
    rippleEffect: {
      scale: 10,
      brushType: 'stroke',
    },
    showEffectOn: 'render', // 带有涟漪特效动画的散点(气泡)图
    itemStyle: {
      normal: {
        shadowColor: '#0ff',
        shadowBlur: 10,
        shadowOffsetX: 0,
        shadowOffsetY: 0,
        color: function (params) { // 配置一个渐变色
          return new echarts.graphic.LinearGradient(1, 0, 0, 0, [
              {
                offset: 0,
                color: '#64fbc5',
              },
              {
                offset: 1,
                color: '#018ace',
              },
            ]);
        },
      },
    },
    symbol: 'circle', // 标记的图形,'circle', 'rect', 'roundRect', 'triangle'等
    symbolSize: [10, 5], // 宽度,高度
    data: mapData
  },
]

5.地图下探到县区

// 城市列表
const citys = '赣州市,吉安市,抚州市,萍乡市,南昌市,九江市,景德镇市,上饶市,鹰潭市,宜春市,新余市'
// 增加点击事件
myChart.on('click', function (e) {
  if (e.componentSubType !== 'map') return // 点击的组件为地图
  if (citys.includes(e.name)) { // 市级地区才进行下探地图
    myChart.clear()
    const mapData = [] // 实际应用中进行请求数据
    echartInit(mapName, mapData)
  }
})

6.标记与取消标记

// 城市列表
const citys = '赣州市,吉安市,抚州市,萍乡市,南昌市,九江市,景德镇市,上饶市,鹰潭市,宜春市,新余市'
// 增加点击事件
myChart.on('click', function (e) {
  if (e.componentSubType !== 'map') return // 点击的组件为地图
  if (citys.includes(e.name)) { // 市级地区才进行下探地图
    myChart.clear()
    const mapData = [] // 实际应用中进行请求数据
    echartInit(mapName, mapData)
  } else {
    openMark() // 标记函数
  }
})

7.配置graphic实现人员搜索

// 人员信息与显示
const persons = {
  li: {
    name: '李墨阳',
    alias: 'li',
    area: '赣州市,吉安市,抚州市,萍乡市',
    show: true,
    data: []
  },
  long: {
    name: '龙藤虎',
    alias: 'long',
    area: '南昌市,九江市,景德镇市,上饶市',
    show: true,
    data: []
  },
  lan: {
    name: '兰有斌',
    alias: 'lan',
    area: '鹰潭市,宜春市,新余市',
    show: true,
    data: []
  },
}
const graphicChildren = []
for (const name in persons) {
  if (Object.hasOwnProperty.call(persons, name)) {
    const person = persons[name];
    const imgGraphic = {
      type: 'image',
      left: 10,
      top: 10 + graphicChildren.length / 2 * 32,
      z: 100,
      bounding: 'raw',
      style: {
        image: person.show ? `/images/${name}.png` : '/images/data-grey.png',
        width: 20,
        height: 26,
      },
      onclick: () => {
        drawMapByPerson(name)
      }
    }
    const textGraphic = {
      type: 'text',
      left: 42,
      top: 15 + graphicChildren.length / 2 * 32,
      z: 100,
      style: {
        fill: person.show ? '#ff8b5b' : '#ccc',
        text: person.name,
        font: '14px Microsoft YaHei'
      },
      onclick: () => {
        drawMapByPerson(name)
      }
    }
    graphicChildren.push(imgGraphic)
    graphicChildren.push(textGraphic)
  }
}

option. graphic: [{
  type: 'group',
  right: 200,
  bottom: 50,
  children: graphicChildren
}],

完整代码

<template>
  <div class="box">
    <div id="main">
    </div>
    <div class="back" @click="onClick" v-if="backBtn">
      <el-tag type="primary">返回</el-tag>
    </div>
  </div>
</template>

<script setup>
/**
* getList 获取标记数据
* handleCancelMark 取消标记接口
* handleMark 标记接口
* jsonArr 江西省和所有地市json
*/
import { handleCancelMark, getList, handleMark } from "@/api/index.js";
import * as echarts from "echarts";
import { onMounted } from "vue";
import jsonArr from '../../json/config.js'
import { ElMessage, ElMessageBox } from 'element-plus'

// 地图json
let geoJson = {}
// 加载的地图名
let geoName = '江西省'
// 获取的所有数据
let geoData = []
// 当前选中市、县区
let cityValue = {}
let countyValue = {}
// 获取当前选中
function getValue(name, geoJson) {
  const { features = [] } = geoJson
  let map = {}
  if (features && features.length > 0) {
    features.forEach(item => {
      if (item.properties.name === name) {
        map = item.properties
      }
    })
  }
  const value = map.centroid.join(',')
  return {
    value,
    name: map.name
  }
}

// 城市列表
const citys = '赣州市,吉安市,抚州市,萍乡市,南昌市,九江市,景德镇市,上饶市,鹰潭市,宜春市,新余市'

// 市区足迹数据
let countyData = []

// 人员信息与显示
const persons = {
  li: {
    name: '李墨阳',
    alias: 'li',
    area: '赣州市,吉安市,抚州市,萍乡市',
    show: true,
    data: []
  },
  long: {
    name: '龙藤虎',
    alias: 'long',
    area: '南昌市,九江市,景德镇市,上饶市',
    show: true,
    data: []
  },
  lan: {
    name: '兰有斌',
    alias: 'lan',
    area: '鹰潭市,宜春市,新余市',
    show: true,
    data: []
  },
}

// 地图初始化
const echartInit = (mapName, mapData = []) => {
  let mapInfo = {}
  jsonArr.forEach(item => {
    if (item.name === mapName) {
      mapInfo = item
    }
  })
  geoJson = mapInfo.json
  geoName = mapName
  geoData = mapData
  const symbolImg = 'image://' + new URL('/images/data-bg.png', import.meta.url).href
  const myChart = echarts.init(document.getElementById("main"));

  // 点击事件
  myChart.off("click");
  myChart.on('click', function (e) {
    if (e.componentSubType !== 'map') return
    if (citys.includes(e.name)) {
      myChart.clear()
      drawMap(e.name)
      cityValue = getValue(e.name, geoJson)
    } else {
      countyValue = getValue(e.name, geoJson)
      if (!(typeof e.value === 'string' && e.value)) {
        if (countyData.some(item => item.county === countyValue.name)) {
          unMark()
        } else {
          openMark()
        }
      }
    }
  })

  // 滚轮事件
  myChart.off("georoam");
  myChart.on("georoam", function (e) {
    const option = myChart.getOption();
    if (e.zoom != null && e.zoom != undefined) {
      //捕捉到缩放时
      option.geo[0].zoom = option.series[0].zoom;
      option.geo[0].center = option.series[0].center;
    } else {
      //捕捉到拖曳时
      option.geo[0].center = option.series[0].center;
    }
    myChart.setOption(option);
  });

  echarts.registerMap("jiangxi", geoJson);

  // 根据人员生成地图的graphic
  const graphicChildren = []
  for (const name in persons) {
    if (Object.hasOwnProperty.call(persons, name)) {
      const person = persons[name];
      const imgGraphic = {
        type: 'image',
        left: 10,
        top: 10 + graphicChildren.length / 2 * 32,
        z: 100,
        bounding: 'raw',
        style: {
          image: person.show ? `/images/${name}.png` : '/images/data-grey.png',
          width: 20,
          height: 26,
        },
        onclick: () => {
          drawMapByPerson(name)
        }
      }
      const textGraphic = {
        type: 'text',
        left: 42,
        top: 15 + graphicChildren.length / 2 * 32,
        z: 100,
        style: {
          fill: person.show ? '#ff8b5b' : '#ccc',
          text: person.name,
          font: '14px Microsoft YaHei'
        },
        onclick: () => {
          drawMapByPerson(name)
        }
      }
      graphicChildren.push(imgGraphic)
      graphicChildren.push(textGraphic)
    }
  }

  const option = {
    title: {
      text: "“足迹”" + mapInfo.name,
      subtext: "",
      left: "center",
      top: 20,
      textStyle: {
        color: "#ccc",
      },
    },
    geo: {
      map: 'jiangxi',
      aspectScale: 0.75, //长宽比
      zoom: 1.1,
      roam: true,
      animationDurationUpdate: 0,
      itemStyle: {
        areaColor: '#013C62',
        shadowColor: '#182f68',
        shadowOffsetX: 0,
        shadowOffsetY: 25
      },
      emphasis: {
        itemStyle: {
          areaColor: '#2AB8FF',
          borderWidth: 0,
          color: 'green',
          label: {
            show: false
          }
        }
      }
    },
    graphic: [{
      type: 'group',
      right: 200,
      bottom: 50,
      children: graphicChildren
    }],
    series: [
      {
        type: "map",
        map: "jiangxi",
        roam: true,
        animationDurationUpdate: 0,
        selectedMode: false,
        label: {
          normal: {
            show: true,
            textStyle: {
              color: '#fff'
            }
          },
          emphasis: {
            textStyle: {
              color: '#fff'
            }
          }
        },
        itemStyle: {
          borderColor: '#2ab8ff',
          borderWidth: 1.5,
          areaColor: '#12235c'
        },
        emphasis: {
          itemStyle: {
            areaColor: '#2AB8FF',
            borderWidth: 0,
            color: 'green'
          }
        },
        zoom: 1.1,
        data: mapData,
      },
      {
        type: 'effectScatter',
        coordinateSystem: 'geo',
        rippleEffect: {
          scale: 10,
          brushType: 'stroke',
        },
        showEffectOn: 'render',
        itemStyle: {
          normal: {
            shadowColor: '#0ff', // 阴影颜色
            shadowBlur: 10, // 文字块的背景阴影长度
            shadowOffsetX: 0, // 偏离中心X轴距离
            shadowOffsetY: 0, // 偏离中心为之Y轴距离
            color: function (params) {
              const colorList = {
                li: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
                  {
                    offset: 0,
                    color: '#64fbc5',
                  },
                  {
                    offset: 1,
                    color: '#018ace',
                  },
                ]),
                long: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
                  {
                    offset: 0,
                    color: '#61c0f1',
                  },
                  {
                    offset: 1,
                    color: '#6f2eb6',
                  },
                ]),
                lan: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
                  {
                    offset: 0,
                    color: '#168e6d',
                  },
                  {
                    offset: 1,
                    color: '#c78d7b',
                  },
                ]),
              }
              return colorList[params.data.username];
            },
          },
        },
        symbol: 'circle',
        symbolSize: [10, 5],
        data: mapData,
      },
      {
        type: 'scatter',
        coordinateSystem: 'geo',
        symbol: function (value, params) {
          return params.data.img;
        },
        symbolSize: [32, 41],
        symbolOffset: [0, -20],
        z: 9999,
        data: mapData,
      },
      {
        type: 'scatter',
        coordinateSystem: 'geo',
        label: {
          normal: {
            show: true,
            formatter: function (params) {
              const value = params.data.county;
              const text = `{tline|${value}}`;
              return text;
            },
            rich: {
              tline: {
                padding: [0, 0],
                color: '#ABF8FF',
                fontSize: 12,
              },
            },
          },
        },
        symbol: symbolImg,
        symbolSize: [50, 25],
        symbolOffset: [0, -60],
        z: 999,
        data: mapData,
      },
    ],
  };
  myChart.setOption(option);
};

// 人员搜索
function drawMapByPerson(name) {
  if (geoName !== '江西省') return
  const person = persons[name]
  person.show = !person.show
  let data = []
  for (const name in persons) {
    if (Object.hasOwnProperty.call(persons, name)) {
      const person = persons[name];
      if (person.show) {
        data = [...data, ...person.data]
      }
    }
  }
  echartInit('江西省', data);
}

// 地图下探
function drawMap(mapName) {
  getList(mapName).then(res => {
    let mapData = res.data || []
    mapData = mapData.map(item => {
      item.value = item.countyCoord.split(',')
      const person = persons[item.username]
      const url = `/images/${person.alias}.png`
      item.img = 'image://' + new URL(url, import.meta.url)
      return item
    })
    countyData = mapData
    backBtn.value = true

    // 重置人员显示
    for (const name in persons) {
      if (Object.hasOwnProperty.call(persons, name)) {
        persons[name].show = true
      }
    }
    echartInit(mapName, mapData);
  })
}

// 打标记
function openMark() {
  const title = `是否标记已到达${countyValue.name}?`
  ElMessageBox.confirm(
    title,
    '标记',
    {
      confirmButtonText: '确认',
      cancelButtonText: '取消',
      type: 'success',
    }
  )
    .then(() => {
      let person = {}
      for (const key in persons) {
        if (Object.hasOwnProperty.call(persons, key)) {
          const element = persons[key];
          if (element.area.includes(cityValue.name)) {
            person = element
          }
        }
      }
      const data = {
        username: person.alias,
        city: cityValue.name,
        cityCoord: cityValue.value,
        county: countyValue.name,
        countyCoord: countyValue.value
      }
      handleMark(data).then(res => {
        drawMap(cityValue.name)
        ElMessage({
          type: 'success',
          message: '成功标记!',
        })
      })
    })
}

// 取消标记
function unMark() {
  const title = `是否取消标记已到达${countyValue.name}?`
  ElMessageBox.confirm(
    title,
    '取消标记',
    {
      confirmButtonText: '确认',
      cancelButtonText: '取消',
      type: 'warning',
    }
  )
    .then(() => {
      let person = {}
      for (const key in persons) {
        if (Object.hasOwnProperty.call(persons, key)) {
          const element = persons[key];
          if (element.area.includes(cityValue.name)) {
            person = element
          }
        }
      }
      let id = null
      geoData.forEach(item => {
        if (item.countyCoord === countyValue.value) {
          id = item.id
        }
      })
      handleCancelMark(id).then(res => {
        drawMap(cityValue.name)
        ElMessage({
          type: 'success',
          message: '成功取消标记!',
        })
      })
    })
}

// 县区返回
const backBtn = ref(false)
function onClick() {
  getList('').then(res => {
    for (const name in persons) {
      if (Object.hasOwnProperty.call(persons, name)) {
        const person = persons[name];
        person.show = true;
        person.data = []
      }
    }
    let mapData = res.data || []
    mapData = mapData.map(item => {
      item.value = item.countyCoord.split(',')
      const person = persons[item.username]
      person.data.push(item)
      const url = `/images/${person.alias}.png`
      item.img = 'image://' + new URL(url, import.meta.url)
      return item
    })
    let mapName = '江西省'
    const myChart = echarts.init(document.getElementById("main"));
    myChart.clear()
    echartInit(mapName, mapData);
    backBtn.value = false
  })
}

onMounted(() => {
  for (const name in persons) {
    if (Object.hasOwnProperty.call(persons, name)) {
      const person = persons[name];
      person.show = true;
      person.data = []
    }
  }
  getList('').then(res => {
    let mapData = res.data || []
    mapData = mapData.map(item => {
      item.value = item.countyCoord.split(',')
      const person = persons[item.username]
      person.data.push(item)
      const url = `/images/${person.alias}.png`
      item.img = 'image://' + new URL(url, import.meta.url)
      return item
    })
    let mapName = '江西省'
    console.log('mapData: ', mapData);
    echartInit(mapName, mapData);
  })
});

</script>
<style scoped>
.box {
  width: 100vw;
  height: 100vh;
  padding: 0;
  margin: 0;
  background-color: #001650;
  position: relative;
}

#main {
  width: 100%;
  height: 100vh;
  margin: 0 auto;
  position: absolute;
  left: 0;
  top: 0;
  z-index: 11;
}

.back {
  position: absolute;
  left: 200px;
  top: 50px;
  z-index: 22;
  color: #fff;
  user-select: none;
  cursor: pointer;
}
</style>

数据库设计

实现效果

资料

Echarts官方文档 echarts.apache.org/zh/option.h…

阿里云可视化平台 datav.aliyun.com/portal/scho…

可视化社区 www.makeapie.cn/echarts