React,Echarts实现2D地图并且支持地图下钻

功能及效果图

  • 渲染地图图表
  • 点击某个地域,地图下钻,地图图像更新
  • 支持全国,省及地市展示

实现过程

  1. 简去搭建react项目,引入echart库等步骤
  2. 收集地图数据
  3. 构建本地数据集
    • ts 以广东省和广州市为例,最外层index.json为全国数据,每个文件夹内index.为省数据,具体命名文件夹为市数据
      •   

    • 建立文件名映射表geoMap.ts
      type stringKey = Record<string, string>
      export const provinceMap: stringKey = {
      	广东省: 'guangdong'
      }
      
      export const cityMap: stringKey = {
      	广州市: 'guangzhou'
      }
  4. 建立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>
      }

       

       

  5. 核心逻辑
    • 设置一个数组存放地图路径,例如当前进入到广州市,则数组为['中国','广东省','广州市']
      • 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
        		})
        }
  6. 最后配置地图左右键点击事件
    • 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')
      		}
      	}
      }, [])

       

  7. 完整代码 
    • 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

posted on 2024-07-23 17:20  Karle  阅读(2)  评论(0编辑  收藏  举报