作者使用的是react框架实现的,如何想要在vue项目上使用就需要自己魔改一下,不过大部分实现思路都是一样的
📖 目标与总体架构概览
公司给我一个需求要在react项目中需要实现一个可下钻(国家 → 省 → 市)、支持多图层立体效果(柱状体 + 水波 + 高亮)、并且在性能、内存与并发上健壮的 ECharts 地图组件。为了解决上述问题,我们在本项目中基于 React + ECharts 构建了一个 可下钻、可联动、可扩展的三维立体效果数据地图组件。
核心目标包括:
- 支持多层级地理下钻(国家 → 省 → 市)
- 与业务数据(计数/指标)动态绑定与实时渲染
- 实现柱状立体、波纹闪烁等视觉强调效果
- 保证在复杂交互与频繁切换场景下的性能和稳定性
- 具备可复用、可扩展、可维护的工程化结构
- 适配不同页面的响应式布局
最终效果如下: - 用户点击地图区域可下钻查看下一层级的指标分布;
- 区域热点值以柱体高度或散点大小体现;
- 涟漪动效用于高亮重点区域;
- 业务数据与地图渲染完全联动,并具有缓存、并发控制与内存释放策略;
- 提供返回上一级操作,实现完整的区域多层级定位能力。
📽️ 演示效果
📋 核心功能实现步骤
- 组件初始化与状态管理
-
geoCoordMap用于存储阿里地图数据。 -
toolTipData用于存储业务坐标数据。 -
currentMap采用栈(数组)结构,最顶端为当前展示层级,便于 push/pop 下钻与返回。
// 定义核心状态类型
interface MapState {
name: string; // 地图名称
code: number; // 区域代码
level: "nation" | "province" | "city"; // 层级
}
// 主要状态
const [geoCoordMap, setGeoCoordMap] = useState<IProperties[]>([]); // 地理坐标数据
const [toolTipData, setToolTipData] = useState<Partial<ToolTipDataVo>[]>([]); // 提示框数据
const [currentMap, setCurrentMap] = useState<MapState[]>([...]); // 当前地图层级栈
- 地图数据加载流程
- 获取地图数据然后注入到geocoordMap里,展示地图数据
// 步骤1: 注册地图数据
const createMapData = async (name: string, code: number) => {
const data = await queryChinaMapApi(code);
echarts.registerMap(name, data); // 注册到ECharts
const mapData = data.features.map((item: any) => item.properties);
setGeoCoordMap(mapData);
};
// 步骤2: 获取业务数据
const queryCount = async () => {
const res = await queryCityCountApi();
setToolTipData(res);
};
- ECharts 配置核心要点
- 多个地图层级叠加实现3D地图效果
- 可配置定制化变量
const initMapChart = (mapContainer: HTMLElement) => {
// 3.1 图层配置工厂函数
const createGeoLayer = (zlevel, centerY, borderColor, shadowColor, shadowOffsetY, areaColor) => ({
type: "map",
map: currentMap[currentMap.length - 1].name,
zlevel,
layoutCenter: ["50%", centerY],
// ... 其他配置
});
// 3.2 多层地图效果
const geoLayers = [
createGeoLayer(-1, "51%", "rgba(58,149,253,0.8)", "...", 5, "..."),
createGeoLayer(-2, "52%", "rgba(58,149,253,0.6)", "...", 5, "..."),
// ... 更多图层
];
// 3.3 数据系列配置
const series = [
{ type: "map", ... }, // 基础地图
{ type: "lines", ... }, // 柱状体主干
{ type: "scatter", ... }, // 柱状体顶部
{ type: "effectScatter", ... } // 水波效果
];
};
- 地图交互功能
- 地图点击下钻
- 返回上一级
const handleMapClick = async (params: any) => {
const { adcode, name, level } = params.data;
if (level === "district") return; // 街道级别不下钻
setCurrentMap(prevMap => [
...prevMap,
{ name, code: adcode, level }
]);
};
const onBlack = () => {
setCurrentMap(prevMap => prevMap.slice(0, -1));
};
⚠️ 关键注意事项
- 性能优化要点
- 使用 useRef 避免重复创建
- 防抖处理图表初始化
- 组件卸载清理
// ✅ 使用 useRef 避免重复创建
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
// ✅ 防抖处理图表初始化
useEffect(() => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
if (chartRef.current) initMapChart(chartRef.current);
}, 200); // 200ms 防抖
}, [toolTipData, geoCoordMap]);
// ✅ 组件卸载清理
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
if (chartInstanceRef.current) {
chartInstanceRef.current.dispose();
}
};
}, []);
- 内存管理
- 每次初始化前销毁旧实例
// 每次初始化前销毁旧实例
const initMapChart = (mapContainer: HTMLElement) => {
if (chartInstanceRef.current) {
chartInstanceRef.current.dispose(); // 🚨 重要:避免内存泄漏
}
const myChart = echarts.init(mapContainer);
chartInstanceRef.current = myChart;
};
- 事件管理
- 创建点击事件实现点击下钻地图
- 创建resize事件,设配不同页面响应式布局
- 及时回收事件,防止内存泄漏
// 防止事件重复绑定
myChart.off("click"); // 先移除已有事件
myChart.on("click", handleMapClick);
// 窗口resize事件管理
window.addEventListener("resize", handleResize);
// 记得在清理时移除
window.removeEventListener("resize", handleResize);
- 错误边界处理
try {
await createMapData(name, code);
await queryCount();
} catch (error) {
console.error("初始化地图失败:", error);
notification.error({ message: "地图初始化失败" }); // 用户友好提示
}
🎯 核心设计模式
- 状态驱动更新
- 通过
currentMap变化实现地图更新
// currentMap 变化触发地图更新
useEffect(() => {
const { code, name } = currentMap[currentMap.length - 1];
if (currentMap.length > 1) {
createMapData(name, code).then(() => {
setToolTipData([]); // 清空数据重新加载
});
}
}, [currentMap]);
- 配置工厂模式
// 复用图层配置逻辑
const createGeoLayer = (zlevel, centerY, borderColor, shadowColor, shadowOffsetY, areaColor) => ({
// 统一配置结构
});
🚀 完整代码如下
- 主要业务数据
import React, { useEffect, useState, useRef } from "react"
import { Button, notification } from "antd"
import * as echarts from "echarts"
import { queryCityCountApi, queryChinaMapApi } from "@/api"
import type { IProperties } from "@/typing"
export interface ToolTipDataVo {
name: string
value: number
areas: string[]
}
interface MapState {
name: string
code: number
level: "nation" | "province" | "city"
}
const BOX_BG =
"image://data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGUAAAAxCAYAAADDY2cuAAAPBUlEQVR4Xu1ca4xd11X+9uu***x/z8sx4PK0Te4idxJYIKY6QIpAYSFWVquFHW6MEhKoghAAJhBAvp9DGSVwifsAfpEooapVUNLFpg5AKrZAgU9qQJvE4Tpq4SWslE9u1x573zL33PPYLrX3OHY8fjRzVUkzvXM3xGXnunbl3f2etb61vffswbD5uuBVg7/qOvP/xP2fM33Cf5kZ6Qz/B2l256P4hPonPcWBGAh25hkTU0OYWgsUoXIrcGdxsUiyZE3jdAvsdNgG6eDl4z/dhWvRhWFxAR9aq9aMntGB9AzXr0DArWLVh/dhv2MuvpUtB8V5MYkYtYzkRiGsCPAG84h***XuhgL***0h005os4CkJ/ELg3G3I10kb5v78V7tg/TUkNFCaLEw9QleAxI6WA4h3QMXFsg9zCpxUAKnNXT2Gc2XtgXQQkR8ukoR6fuwfsd7IBEXHcwMQfjFvAECOBSD6wxmJUUrvUqFnOwXzHv20LcSH/Ye74LJ9U2pHUH2e/B+h18g4PFDCxc2AysAHyWA2sCenkArvWfuCMDY+sRsw7KpPdyGcebCZJBBr71wytDH/4F33d/xNgo/bJAIC6c3JvzneW//86P3jx7wbRt5owrHP2k5BjXY0HDeXlZeDBIMBFz2egT0b13bhnfv2dkZy3iyjMwRnjQ8ngUZ7n+nyf6z39Rw56V4AuncWZtBpN5N1o2gPJ2soZ0SEBs/cjKwMc/JkYeeuKHsyvvtPPCO+adcZ5Z5q2BW1rV+gdnWp3Oiil85pw3zsF5D3hPf7WnOIbRYnP6YkwyziPBRV3I8dE42bmtVotiziE8Z5RuJGM1xfl9E1sHeIyj/zB46i8MilmFeP453Na6FBTv2SRONDoQowJ+/MHlnf/09ZnFsX+dmV+zOZzNvXUa3mvnXG6sSb12bW1cx1qnnfGFpTjxcIRIrxVlFAOEC2cQnDHFhUi4lA0peJ0rnkhJQDEJxmMIGTPRXxPi6V++ffsh//b955rFawbywjReWeqSfhkp3rM7cXxAojYm4***fWZn4l8deOVMcP9dOTdtb3XbGZtagMNYX1rjUWJ874zJjvbEOdFiChdJiD5bKjFGCYp***okXwRHEWS8ESIVkUDsEiKVRTSNmAVDUmnrrn9u1P6fMHToys/ZdF7VwNEwtTjAVuXgfll/C9QYNkG4cbP7gyceTz06eyY6fbbb3mdTFfFLajCwIFmg7rPJ0L42ApdVXpyxKfUBrrkt***xMLX+b2sJ/4ACMB4AAW***yjOmZICMRWuXCBSBJSUQyqOBkQUNZk8/NHbb37anD/w2kjrWQE+m6E1N83u0leA4hFt1bBbD63c8syjL5xJj8202sVSkRcLRebbWqPQBEQJjNEOxCXW0LlMXZ7SVw8+CBAifMGoe2CQkkMKDikFSnAEEiXVUBJHW2Si+rn66r17dh42F/78ldGVKQt+AejMXwHKPkz3CzS3ABg+tLzzPx7+zun02Mzaml7Mc72YZ75dVKBog0K7EhhHZwdnPCyBEnilt4ieOnfBGRgHJLUisgsKRQgPoERKohZJORjHcjhKogEVPfOJvRPPZBf++OXxxW9lMAsD+NDSFenrDrxaV1D9AqzvgbfG//0LL86yH55ZbemlPLdLee7bOYFikFOkaIu8oCgJqSwAYojsKXW58NUTj1ANc7oIWeivqcYiYJTkUBFHTIBEApGUSGIhCJTBKJEDcfzYRyY+eFSt/t5rE63vAsnKNKZblxI9gL3+9aiGpCbBkgtf6jw+t6R35itp7taK3K6mBTpaI88pfRlkhYUuHLS1MMZBOw9rHXwApLdSGKd+kFIXpTDGEVWREikCQyCO6JBIIiX6a4o1o5j3J9G24Thu7BYPDE76HxRIs2nsS6/oU+C9+DWclCkK9dzf5Y/bjrmVdfLCUZ/SyjXSTCMrSkAKOqqIKaj6ovTlXai+Aig9hAsnkhcMAhQtJZcoxQPJEzAlIOFgjZrizTjyzSjitVgmN+M3f/a3srcLRGaj1LJBZgmKMAOmOD848IRPi1uRUdrKDNpZEQBJMwKFOKUCRjuUoJAsdpFTeiJ3bfiQoUehkjhiAZRIEBgXQamRlBgpNCPF6jWFWqJQj6J4KPpU9idvvAPs9xv1w6tL83/90peR6d1oZxQhBdq5QZ5pdAqDnL4vLDICJhA+pTHiF4qWild6CBXmGbgsKy8qiYncpeKoUfqKBJK4ipREoh4r1AmQRKEWReDykzj08+9cvlpXB+XAS08iK3ajkxVIU41OrtGhKKFDG6SFLfmFgOk2jyQeU0lMvNJDDSSnPqUqhyltEaeokLbKUrgW00FET2AQMBFqNfo+QsI/iYN3nbp2UNJ8FzodjXZeoBMipkxfaeAVg5wAyV1oIEP66lZgvUQopexb9iiCQRGnKI4kEDwPwBAg4aDoCOcIjZpCg0ARn7p2UP7qhSdBoBCXdKpIoYghfsm1QZZTSWyQGRdSWGgkrS+llqos7pUM1iV5SWWxLNMXHXEsEMsuIBKNpASmTF9Reaj3CEonvyUAEiKFgKHUFdIY8QlxS8kp1LfYwkN7FzgFvTZaqaIkVF+yBCSiKKHURVVXLELaovTVqJWR0qT0FRMo+99bpKyDQtFCJJ+ZcKYoIVDoTGVxIHtdpi+qwHqmc+ymgquBIstIIT6hcrhO4FDKut6gUJ9C6WsTlMsS8yYoNyBTbYKyCUpJ9BWnbKavH3NBvJ+Rskn0/w9AoZKYyuHN6qtsHC8pibvVV7ckvl7VVzvbhTR083SYILdcbB4r/auomscgtVTNI5XFPfSgWUro6PmGjj6oxKVCXHbxspRYfpLm8cALT6Kjd6FDc5TQo2ikqUGaVzJL6Owt8qAS02yFdC+a1ZNq31ug0PVXyvYMghpHUomrjp5EyXgjKDEpxBFIKW6+V5klCJI5qcQVKNU8hbSvILNkJLOU00cSJEPzSPMUQ26zHgMliJHlLCXILARKXEVKECTDKDhES5BZut18AOUaBMnSKc7wN9NPsjzf7UliaacaaUFq8YZIWVeIS1GSjBM0eSSZxV3hV/7pzmXrQ64qfUU0Cg5yC8n2pVoc0leQ8EmMlKwRR54EyYF4P/7sjncu92JvHHKtu8W//ejil3xW7PatIkc7LcJ8vjt5TDMid1KKy9RVipEXZZZemc9vUFnKGT2BQi4WAoXSF5E9yfdVpNDkkUTIZjV9rEdRYzvuu+l36m+fwF57VS/xPn9UDWN7tIaF+Pv/qB9PV/WEXc0Kv5bntkXcUlTjYEpdJEhWmhcBQkMuR0RPJN9Ds5QuMGQxCtYiGnRVgISZSjWjT0iQjCTrSyKaz/NmHNcHkmjwFvbpbffWT24B8m9gV3GFbfVuPJ9IjDY1TOPO/+078s03FvvOnW117HKR2eWMxsI6kHsYB9OMXpP/q5ylkHHCVemrFwXJbqTQ9FEpihjye1WRQtFCEn6ixEAcicEkFgNxfN9d4yPzo9nvLu8tjjvw1nO4rd1NY+sOyV/Em80MdkhADn12/qZvfv6509nxk8ureqnIzEKe+aAQk+eLCJ5ME5S+yPtVlcM0Rwner17LX1R9kb2ockfKMOwSwWLUdbOQxagWSzmUxGI4TqLBOPnKJ/ZOfN0s/OGrH1j+tgVfehGzK90tJeugTOL4QI6+EUBvfXh54t8eef5M59hbK61iIc/0fJb6FpnxjAmer9LNUrojdXBJktu+3FNE6atXCjCyq4I2OnQtRmRdrYheknmCzHiUxoJDUqnhOFbDSU0Nqfhrv75n4oid/9PXRtMpi3TOIF+8wiF5N04MeagxwI49svIzX330xVPpsZl2q1go8mI+T33HlLMUAiX4iYNdlXxf5IqkHqV0R/aGk3hjRVmBQmNhms/T1ocuMJwipgQlVlKRO3IkTuJBro58bM/OI3buL783sjLFIc5nuG1umtGmrA0GbwLFAOMSctvDKzsPHzp6Knv5VLuVr1it53Vmg22VGsXKxVICUhq8ieCpPyEvcdhW1COtCjkkQ7TQ/hTq7InoZWXyJl4hDxidlWCJlBQp8ZCMo37Iwx/ds+OwOf/gqyPtZzn4bIFbL1wBSpm+6tsY/AfIdf+3x0/lL/+o3TEtWN2yhetY47W2PrfW5yZsgyjd91QWEygECFVeFSC90NmHDUMVMKECY5xRpCjJeSIEi4VkFCUqbIWQakBK1WBKNZl4+p7bb3rKzh14fUv63wX07FUN3kT0HmIrgxv/zPLNX/nim+ejb51Z6ZjcW5vC0hY6b7ylTUM+tcZ0jPEdQ/9vw04uipJec0eWiJT/kP4lOWeSc55wKepSsLqQMpGSQOGKc5FAyBoTMgL/2q/uvekLbvb33xpY+26K7PzL+NBit1dZbx7v9qdrDovDCo2x314e/aMxre578NjMhflUG1eAAKGtdd4VsDa1Rq8ZY1OrXeos7fBCqUP25oy+1EEYAYKIc0k7uZpSyqZQvMaFkGBM0NY7MKkYf2DX2OA92wfbn+2bud9BnCvA5qbx5TWwh0Lpug7KPu+VwBv9tBVCQmz7g6Xxz+yJ6/dktBGI1puVeYkKq1dmW53Hps6***Z0rcpuXEQR6HiOZ5adbVbnqpwubUcm2Ck4RUW8K+fGf2zL0wJ3bRhoxD7uCw0ZVBiSCsyWjT/+zm334+4PZUQM3F6Fv+Xlspx3CYY0vl1mSGINNBjNkwQdruR8ea6sPMsY459wxy4xwyNLULp442zo3f86srp2NsvaZ3CLuFXZ/l4tOxWLLDqcGd0T1HaNsdPtQY8wL17TMRxQp9MqOsMtzzfSsA19xkEsAVmvY1enuTbkUFACT/lm5iC2xQlKLoRoO7bpEFFmYal8yN92N+TF8S6IvncJL+mp3TejBeAl7R+nmBgmaiYGuO8QNQNcUoBxYdXMDujkEMo2iTTeIWEORncBeukHEesl6+R0nQn23CyelgI22wCqGWOYoRB1ABu8dpAGkjnC+uNzC35NAXP6hvRe78A25HTtUDkTd9UvAmYG2BpEF1nSEsSLGdj0FkBh5SQ9xdYP3xZu9BAqb3JDmpkqG6R69tZXuWq+6sH4HGbCXTWKUAZPVK6cwhUkHHATwOdr+***WG7t3vYnStb2Lzedd1BTZBua7LeX1+2f8ByDqSuffFKG8AAAAASUVORK5CYII="
const TheMap = (): React.ReactElement => {
const [geoCoordMap, setGeoCoordMap] = useState<IProperties[]>([])
const [toolTipData, setToolTipData] = useState<Partial<ToolTipDataVo>[]>([])
const [currentMap, setCurrentMap] = useState<MapState[]>([
{
name: "中国",
code: 100000,
level: "nation",
},
])
const chartRef = useRef<HTMLDivElement>(null)
const chartInstanceRef = useRef<echarts.ECharts | null>(null)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
// 常量配置
const LAYOUT_SIZE = "100%"
const ZOOM = "1.1"
const convertData = (data: Partial<ToolTipDataVo>[]) => {
return data
.map(item => {
const geoCoord = geoCoordMap.find(gc => gc.name === item.name)
return geoCoord
? { name: geoCoord.name, value: geoCoord.center }
: null
})
.filter(Boolean) as { name: string; value: number[] }[]
}
// 柱状体的主干
const lineData = () => {
if (!toolTipData.length) {
return [
{
coords: [
[118.767413, 32.041544],
[118.767413, 33.541544],
],
},
]
}
return toolTipData
.map(item => {
const geoCoordinate = geoCoordMap.find(
gc => gc.name === item.name
)
if (geoCoordinate) {
return {
coords: [
geoCoordinate.center,
[
geoCoordinate.center[0],
geoCoordinate.center[1] + 1.5,
],
],
}
}
return null
})
.filter(Boolean) as { coords: number[][] }[]
}
// 柱状体的顶部
const scatterData = () => {
return toolTipData
.map(item => {
const geoCoordinate = geoCoordMap.find(
gc => gc.name === item.name
)
if (geoCoordinate) {
return [
geoCoordinate.center[0],
geoCoordinate.center[1] + 2,
item,
]
}
return null
})
.filter(Boolean) as any[]
}
// 初始化地图
const initializeMap = async () => {
try {
await createMapData(currentMap[0].name, currentMap[0].code)
await queryCount()
} catch (error) {
console.error("初始化地图失败:", error)
notification.error({ message: "地图初始化失败" })
}
}
useEffect(() => {
initializeMap().then()
// 组件卸载时清理
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
if (chartInstanceRef.current) {
chartInstanceRef.current.dispose()
chartInstanceRef.current = null
}
}
}, [])
/**
* 创建地图数据
*/
const createMapData = async (name: string, code: number) => {
try {
const data = await queryChinaMapApi(code)
echarts.registerMap(name, data)
const mapData = data.features.map((item: any) => item.properties)
setGeoCoordMap(mapData)
return name
} catch (error) {
console.error(`获取${name}地图数据失败:`, error)
notification.error({ message: `无${name}区域地图显示!` })
throw error
}
}
// 移除getJSON函数,直接在createMapData中使用queryChinaMapApi
// 接口获取数据
const queryCount = async () => {
try {
const res = await queryCityCountApi()
setToolTipData(res)
} catch (error) {
console.error("获取城市数据失败:", error)
notification.error({ message: "获取数据失败" })
setToolTipData([])
}
}
useEffect(() => {
// 清理之前的定时器
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
timeoutRef.current = setTimeout(() => {
if (chartRef.current) {
initMapChart(chartRef.current)
}
}, 200)
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [toolTipData, geoCoordMap])
/**
* 地图点击事件处理
*/
const handleMapClick = async (params: any) => {
if (params.data) {
// 防止点击其他位置触发报错
if (!params.data?.level) return
const { adcode, name, level } = params.data
// 街道级别不再下钻
if (level === "district") {
notification.error({ message: "无此区域地图显示!" })
return
}
try {
setCurrentMap(prevMap => [
...prevMap,
{
name,
code: adcode,
level,
},
])
} catch (error) {
console.error("切换地图失败:", error)
}
}
}
// 点击返回按钮
const onBlack = () => {
try {
setCurrentMap(prevMap => prevMap.slice(0, -1))
} catch (error) {
console.error("返回上一级地图失败:", error)
notification.error({ message: "返回上一级失败,请重试" })
}
}
// 判断是否需要更新数据
useEffect(() => {
const { code, name } = currentMap[currentMap.length - 1]
if (currentMap.length > 1) {
createMapData(name, code).then(() => {
setToolTipData([])
})
} else {
initializeMap().then()
}
}, [currentMap])
const createGeoLayer = (
zlevel: number,
centerY: string,
borderColor: string,
shadowColor: string,
shadowOffsetY: number,
areaColor: string
) => ({
type: "map" as const,
map: currentMap[currentMap.length - 1].name,
zlevel,
aspectScale: 1,
zoom: ZOOM,
layoutCenter: ["50%", centerY],
layoutSize: LAYOUT_SIZE,
roam: false,
silent: true,
itemStyle: {
normal: {
borderWidth: 1,
borderColor,
shadowColor,
shadowOffsetY,
shadowBlur: 15,
areaColor,
},
},
})
const initMapChart = (mapContainer: HTMLElement) => {
// 销毁之前的实例
if (chartInstanceRef.current) {
chartInstanceRef.current.dispose()
}
const myChart = echarts.init(mapContainer)
chartInstanceRef.current = myChart
const tooltipFormatter = (params: any) => {
if (!toolTipData.length) return ""
// 优化tooltip格式化逻辑
const data = toolTipData.find(item => item.name === params.name)
return data ? `${data.name}:${data.value}` : ""
}
const options = {
title: {
show: true,
text: currentMap[currentMap.length - 1].name,
x: "center",
top: "10",
textStyle: {
color: "#fff",
fontFamily: "等线",
fontSize: 20,
},
},
tooltip: {
trigger: "none",
formatter: tooltipFormatter,
backgroundColor: "#fff",
borderColor: "#333",
padding: [5, 10],
textStyle: {
color: "#333",
fontSize: "16",
},
},
geo: [
{
layoutCenter: ["50%", "50%"], //位置
layoutSize: LAYOUT_SIZE, //大小
show: true,
map: currentMap[currentMap.length - 1].name,
roam: false,
zoom: ZOOM,
aspectScale: 1,
label: {
normal: {
show:
currentMap[currentMap.length - 1].level !==
"nation",
textStyle: {
color: "#fff",
fontSize: "120%",
},
formatter: (params: any) => params.name || "",
},
emphasis: {
show: true,
textStyle: {
color: "#fff",
fontSize: "140%",
fontWeight: "bold",
},
},
},
itemStyle: {
normal: {
areaColor: {
type: "linear",
x: 1200,
y: 0,
x2: 0,
y2: 0,
colorStops: [
{
offset: 0,
color: "rgba(3,27,78,0.75)", // 0% 处的颜色
},
{
offset: 1,
color: "rgba(58,149,253,0.75)", // 50% 处的颜色
},
],
global: true, // 缺省为 false
},
borderColor: "#c0f3fb",
borderWidth: 1,
shadowColor: "#8cd3ef",
shadowOffsetY: 10,
shadowBlur: 120,
},
emphasis: {
areaColor: "rgba(0,254,233,0.6)",
},
},
},
// 使用工厂函数创建重复的图层配置
createGeoLayer(
-1,
"51%",
"rgba(58,149,253,0.8)",
"rgba(172, 122, 255,0.5)",
5,
"rgba(5,21,35,0.1)"
),
createGeoLayer(
-2,
"52%",
"rgba(58,149,253,0.6)",
"rgba(65, 214, 255,1)",
5,
"transparent"
),
createGeoLayer(
-3,
"53%",
"rgba(58,149,253,0.4)",
"rgba(58,149,253,1)",
15,
"transparent"
),
{
...createGeoLayer(
-4,
"54%",
"rgba(5,9,57,0.8)",
"rgba(29, 111, 165,0.8)",
15,
"rgba(5,21,35,0.1)"
),
itemStyle: {
normal: {
borderWidth: 5,
borderColor: "rgba(5,9,57,0.8)",
shadowColor: "rgba(29, 111, 165,0.8)",
shadowOffsetY: 15,
shadowBlur: 10,
areaColor: "rgba(5,21,35,0.1)",
},
},
},
],
series: [
{
type: "map",
map: currentMap[currentMap.length - 1].name,
geoIndex: 0,
aspectScale: 1, //长宽比
zoom: 0.05,
showLegendSymbol: true,
roam: true,
itemStyle: {
normal: {
areaColor: {
type: "linear",
x: 1200,
y: 0,
x2: 0,
y2: 0,
colorStops: [
{
offset: 0,
color: "rgba(3,27,78,0.75)", // 0% 处的颜色
},
{
offset: 1,
color: "rgba(58,149,253,0.75)", // 50% 处的颜色
},
],
global: true,
},
borderColor: "#fff",
borderWidth: 0.2,
},
},
layoutCenter: ["50%", "50%"],
layoutSize: LAYOUT_SIZE,
data: geoCoordMap,
},
//柱状体的主干
{
type: "lines",
zlevel: 5,
effect: {
show: false,
symbolSize: 5, // 图标大小
},
lineStyle: {
width: 6, // 尾迹线条宽度
color: "rgba(249, 105, 13, .6)",
opacity: 1, // 尾迹线条透明度
curveness: 0, // 尾迹线条曲直度
},
label: {
show: false,
position: "end",
formatter: "245",
},
silent: true,
data: lineData(),
},
// 柱状体的顶部
{
type: "scatter",
coordinateSystem: "geo",
geoIndex: 0,
zlevel: 5,
label: {
normal: {
show: true,
formatter: (params: any) => {
const name = params.data[2]?.name
const value = params.data[2]?.value
return `{tline|${name}} : {fline|${value}}个`
},
color: "#fff",
rich: {
fline: {
color: "#fff",
fontSize: 14,
fontWeight: 600,
},
tline: {
color: "#ABF8FF",
fontSize: 12,
},
},
},
emphasis: {
show: true,
},
},
itemStyle: {
color: "#00FFF6",
opacity: 1,
},
symbol: BOX_BG,
symbolSize: [110, 60],
symbolOffset: [0, -20],
z: 999,
data: scatterData(),
},
// 底部水波圆
{
name: "Top 5",
type: "effectScatter",
coordinateSystem: "geo",
data: convertData(toolTipData),
showEffectOn: "render",
rippleEffect: {
scale: 5,
brushType: "stroke",
},
label: {
normal: {
formatter: "{b}",
position: "bottom",
show: false,
color: "#fff",
distance: 10,
},
},
symbol: "circle",
symbolSize: [20, 10],
itemStyle: {
normal: {
color: "#16ffff",
shadowBlur: 10,
shadowColor: "#16ffff",
},
opacity: 1,
},
zlevel: 4,
},
],
}
myChart.setOption(options)
myChart.off("click") // 先移除已有事件,防止重复绑定
myChart.on("click", handleMapClick)
// 添加窗口大小改变的响应
const handleResize = () => {
myChart && myChart.resize()
}
window.addEventListener("resize", handleResize)
// 返回清理函数
return () => {
window.removeEventListener("resize", handleResize)
myChart && myChart.dispose()
}
}
return (
<>
{currentMap[currentMap.length - 1].level !== "nation" && (
<Button
style={{
position: "absolute",
right: "20px",
cursor: "pointer",
color: "#fff",
zIndex: 999,
}}
onClick={onBlack}
>
上一级
</Button>
)}
<div
ref={chartRef}
style={{
width: "100%",
height: "calc(100vh - 180px)",
position: "relative",
}}
></div>
</>
)
}
export default TheMap
- map.ts类型定义文件
export interface IGeoJson {
type: string;
features: IFeature[];
}
interface IFeature {
type: string;
properties: IProperties;
geometry: IGeometry;
}
export interface IProperties {
name: string
acroutes?: string[]
adcode: number
center: number[]
centroid?: number[]
childrenNum?: number
level: string
parent?: { adcode: number }
subFeatureIndex?: number
}
interface IGeometry {
type: string;
coordinates: number[][][];
}
-
获取阿里云的省市区数据接口
https://geo.datav.aliyun.***/areas_v3/bound/geojson?code=100000_full -
mock接口数据
[
{
"name": "安徽省",
"value": 12
},
{
"name": "江苏省",
"value": 7
},
{
"name": "河南省",
"value": 1
},
{
"name": "河北省",
"value": 1
},
{
"name": "湖北省",
"value": 1
},
{
"name": "江西省",
"value": 7
},
{
"name": "湖南省",
"value": 1
}
]