React,Echarts实现2D地图并且支持地图下钻
功能及效果图
- 渲染地图图表
- 点击某个地域,地图下钻,地图图像更新
- 支持全国,省及地市展示
实现过程
- 简去搭建react项目,引入echart库等步骤
- 收集地图数据
- 需要获取具体地点的GeoJson文件
- 通过https://datav.aliyun.com/portal/school/atlas/area_selector网站,免费下载相关数据的json文件
- 构建本地数据集
- ts 以广东省和广州市为例,最外层index.json为全国数据,每个文件夹内index.为省数据,具体命名文件夹为市数据
-
- 建立文件名映射表
geoMap.ts
type stringKey = Record<string, string> export const provinceMap: stringKey = { 广东省: 'guangdong' } export const cityMap: stringKey = { 广州市: 'guangzhou' }
- ts 以广东省和广州市为例,最外层index.json为全国数据,每个文件夹内index.为省数据,具体命名文件夹为市数据
- 建立echart图表
- 除了最基础的图表建立过程,还需要用到echarts的
echarts.registerMap()
- 配置图表option并建立
import geoJson from './geoJson.json' const Map: React.FC = () => { const mapInstance = useRef<echarts.ECharts>() const option = { title: { text: '全国地图', textStyle: { color: '#000' }, left: 'true' }, series: [ { name: '全国地图', type: 'map', mapType: 'map', scaleLimit: { min: 0.5, max: 10 }, label: { color: '#fff', show: true, position: [1, 100], fontSize: 8, offset: [2, 0], align: 'left' }, itemStyle: { areaColor: '#000' }, roam: true, zoom: 1.25, animation: true } ] } useEffect(() => { echarts.registerMap('map', geoJson) mapInstance.current = echarts.init(document.getElementById('map') as HTMLDivElement) mapInstance.current?.setOption(option) },[]) return <div id="map"></div> }
- 除了最基础的图表建立过程,还需要用到echarts的
- 核心逻辑
- 设置一个数组存放地图路径,例如当前进入到广州市,则数组为
['中国','广东省','广州市']
-
const [geoLevel, setGeoLevel] = useState<any>([]) const geoLevelRef = useRef<any>([])
-
- 建立响应式变量存放GeoJson文件的内容
-
const [geoData, setGeoData] = useState<any>() const geoRef = useRef<any>(null)
-
- 编写获取对应GeoJson文件的路径方法
-
//通过路径数组的长度,判断当前节点属于国,省还是市 //通过之前建立的geoJsonMap表,进行中文和英文配对,获取文件名 const geoJsonPath = () => { const length = geoLevelRef.current?.length || 0 const baseDir = '/src/config/mapData/' const province = provinceMap[geoLevelRef.current[1]] const city = cityMap[geoLevelRef.current[2]] switch (length) { case 1: return `${baseDir}index.json` case 2: return `${baseDir}${province}/index.json` case 3: return `${baseDir}${province}/${city}.json` default: return `${baseDir}index.json` } }
-
- 编写获取GeoJson文件内容的方法
-
//childrenName:当前点击地图位置的名称(例:广东省) action:当前操作是下钻还是上行 //数据下钻则对level数组进行添加,数据上行则对level数组进行删除 //最后通过fetch获取文件内容 const updateGeoData = ( childrenName: string = '中国', action: boolean = true ) => { if (action) { setGeoLevel((l: any) => { return [...l, childrenName] }) geoLevelRef.current = [...geoLevelRef.current, childrenName] } else { setGeoLevel((l: any) => { return l.slice(0, -1) }) geoLevelRef.current = geoLevelRef.current.slice(0, -1) } fetch(geoJsonPath()) .then(res => res.json()) .then(res => { setGeoData(res) geoRef.current = res }) }
-
- 设置一个数组存放地图路径,例如当前进入到广州市,则数组为
- 最后配置地图左右键点击事件
-
useEffect(() => { //阻止浏览器默认的右键事件 document.getElementById('map')!.oncontextmenu = function () { return false } //绑定左键事件,当地图显示市级数据时无法再进行数据下钻 mapInstance.current?.on('click', (params: any) => { if (geoLevelRef.current.length >= 3) return updateGeoData(params?.name) }) //绑定右键事件,当数据上行到国数据时无法再进行数据上行 mapInstance.current?.on('contextmenu', (params: any) => { if (geoLevelRef.current.length === 1) return updateGeoData(params?.name, false) }) //每当地图数据更新时,重新绑定事件,卸载旧的事件监听,否则会出现多次执行的情况 return () => { if (mapInstance.current) { mapInstance.current.off('contextmenu') mapInstance.current.off('click') } } }, [])
-
- 完整代码
-
import * as echarts from 'echarts' import { useEffect, useRef, useState } from 'react' import { provinceMap, cityMap } from '../../config/mapData/geoMap' const MyMap: React.FC = () => { const mapInstance = useRef<echarts.ECharts>() const [geoData, setGeoData] = useState<any>() const geoRef = useRef<any>(null) const [geoLevel, setGeoLevel] = useState<any>([]) const geoLevelRef = useRef<any>([]) const option = { title: { textStyle: { color: '#000' }, left: 'center' }, series: [ { name: '全国地图', type: 'map', mapType: 'map', scaleLimit: { min: 0.5, max: 10 }, label: { color: '#fff', show: true, position: [1, 100], fontSize: 8, offset: [2, 0], align: 'left' }, itemStyle: { areaColor: '#000' }, roam: true, zoom: 1.25, animation: true } ] } const geoJsonPath = () => { const length = geoLevelRef.current?.length || 0 const baseDir = '/src/config/mapData/' const province = provinceMap[geoLevelRef.current[1]] const city = cityMap[geoLevelRef.current[2]] switch (length) { case 1: return `${baseDir}index.json` case 2: return `${baseDir}${province}/index.json` case 3: return `${baseDir}${province}/${city}.json` default: return `${baseDir}index.json` } } const updateGeoData = ( childrenName: string = '中国', action: boolean = true ) => { setGeoLevel((prevLevel: any) => { const newLevel = action ? [...prevLevel, childrenName] : prevLevel.slice(0, -1) geoLevelRef.current = newLevel return newLevel }) } useEffect(() => { const path = geoJsonPath() if (geoLevel.length !== 0) { fetch(path) .then(res => res.json()) .then(data => { setGeoData(data) geoRef.current = data }) } }, [geoLevel]) useEffect(() => { updateGeoData() }, []) useEffect(() => { document.getElementById('map')!.oncontextmenu = () => false }, []) useEffect(() => { if (geoData) { echarts.registerMap('map', geoRef.current as any) mapInstance.current = echarts.init( document.getElementById('map') as HTMLDivElement ) option.title.text = `${geoRef.current?.name}地图` || '地图' mapInstance.current?.setOption(option) } }, [geoData]) useEffect(() => { mapInstance.current?.on('click', (params: any) => { if (geoLevelRef.current.length >= 3) return updateGeoData(params?.name) }) mapInstance.current?.on('contextmenu', (params: any) => { if (geoLevelRef.current.length === 1) return updateGeoData(params?.name, false) }) return () => { mapInstance.current?.off('click') mapInstance.current?.off('contextmenu') } }, [geoData]) useEffect(() => { window.addEventListener('resize', () => { mapInstance.current?.resize() }) return () => { window.removeEventListener('resize', () => { mapInstance.current?.resize() }) } }, []) return <div style={{ width: '100%', height: '100%' }} id="map"></div> } export default MyMap
-
遇坑
- 单独依赖useState()的响应式数据无法实现更新地图
- setState是异步更新,在调用setState之后访问state拿到的是旧的数据。
- 每次点击后都会通过setGeoLevel去更新geoLevel数组,然后立刻访问geoLevel获取GeoJson文件。但是由于异步更新,每次拿到的都是当前的快照数据。
通过建立ref变量,更新state数据的同时也更新ref变量。因为useEffect将state数据作为依赖项用来更新数据,所以单独使用ref无法实现更新
- geoJsonPath一开始使用了useMemo进行包裹,并且将geoLevel作为依赖项,相关数据也从geoLevel进行获取。结果是无法获取最新值也无法通过依赖项监听更新数据
将geoJsonPath改写为函数,level值通过geoLevelRef进行获取,每次获取路径都会重新调用该函数,所以每次都能获取到新值
- 对地图绑定了点击事件后,点击后会重复执行。
需要利用return,在组件重新渲染前解除点击事件的监听
mapInstance.current.off('click')
- fetch和import获取静态文件的区别
fetch模拟网络请求,需要配置完成的请求地址后缀。如
/src/config/mapData/guangdong/gaungzhou.json
import是ESM引入文件语句,可以通过文件路径进行获取。如
../../config/mapData/guangdong/guangzhou.json