简介:ECharts作为百度开源的JavaScript数据可视化库,其地图组件是展示地理数据分布的重要工具,而官网已不再提供完整的离线地图资源下载。这份echarts-all-map.rar正是面向需要离线使用中国省市地图的Web前端开发者与数据分析师整理的珍贵合集,覆盖全国所有省份及地级市。压缩包整体约3.87MB,内置README说明文档与map地图数据文件夹,其中地图数据多采用JSON等格式存储,每个文件对应一个省份或地级市,文件名以行政区名称命名,便于直接检索与加载。已有151人学习下载,适合从事地理数据可视化、需要快速构建地图图表的中高级前端开发者。借助该包,开发者可绕开网络依赖,在ECharts中通过geo组件加载对应区域的JSON地图数据,实现人口分布、经济指标等多维数据的区域映射与交互展示,并可按需定制颜色方案、添加标记或组合其他图表组件,灵活扩展可视化效果。
1. echarts-all-map.rar:一份老地图包为什么还能撑起今天的地图可视化
如果你最近用 ECharts 做地图可视化,大概率撞上过同一个尴尬:ECharts 5 之后官方不再内置地图 GeoJSON,教程里那种“new 一个 ECharts 实例然后直接显示中国地图”的写法基本凉了。手头这份 echarts-all-map.rar,是早期社区流传的“全国+省市地图合集”压缩包,里面是各省市和全国的 GeoJSON / JS 地图数据。对一个被地图数据来源卡住的前端来说,它最大的价值不是“老”,而是不用去各个 CDN 碰运气,解压后本地注册即可用。这篇文章就按我的落地习惯讲清楚:包怎么解、旧格式怎么转、怎么进 Vue/React、哪里最容易踩坑,最后给出一份省市下钻的完整方案。适合做 BI 看板、大屏项目或数据可视化的开发,想快速把地图能力跑起来的人。
2. 解压与选型:从 rar 包里挑地图文件,再按 ECharts 5 的格式改造
2.1 先看包内结构:JS 老格式与 JSON 新格式怎么区分
先从解压开始。Windows 下双击就能解;Linux 和 macOS 上我一般用 7z,先装 p7zip。
# Ubuntu / Debian sudo apt install p7zip-full # macOS brew install p7zip # 解压到当前目录下的 echarts-all-map 文件夹 7z x echarts-all-map.rar -o./echarts-all-map解压后你会看到一整套地图文件,常见命名有china.js、china.json、world.js,以及按省级命名的目录,里面则是各省的js或json。要判断手头到底是哪一代格式,先看文件头。
file maps/china.js head -c 200 maps/china.js如果file输出的是 ASCII text / JavaScript source,而head看到的内容形如:
echarts.registerMap('china', {"type":"FeatureCollection","features":[...]});说明这是 ECharts 3/4 时代的地图 JS 文件,它把 GeoJSON 对象直接塞进registerMap调用里,依赖页面里有一个全局echarts。另一类json文件则是纯粹的 GeoJSON 数据,打开是{"type":"FeatureCollection", ...},这种在现代工程里直接用import或fetch就能消费。
为什么要优先选 JSON?原因有三个:其一,老 JS 文件在模块化工程里常常和按需引入的echarts/core冲突,因为它的执行依赖全局对象;其二,JSON 不包含执行逻辑,可以安全地做异步加载和缓存;其三,后续要替换新版边界数据时,JSON 格式直接替换文件即可,不需要改代码。我一般会把包内所有 JS 格式的地图都转成 JSON,再统一管理。
2.2 把老 JS 批量转成 GeoJSON:一个 Node 脚本解决
网上流传的转换方案多是用正则registerMap\('china', (.*)\)去抓 JSON,但地图 GeoJSON 里嵌套大括号和方括号,正则最容易被嵌套结构坑。推荐的做法是“括号配对扫描”:找到第一个{,然后逐字符计数,直到括号深度归零,取这一整段做JSON.parse。
// convert-map-to-json.js // 用法: node convert-map-to-json.js <input.js> [output.json] const fs = require('fs'); const path = require('path'); const input = process.argv[2]; if (!input) { console.error('用法: node convert-map-to-json.js <input.js> [output.json]'); process.exit(1); } const code = fs.readFileSync(input, 'utf8'); // 老格式地图 JS 基本都有一个大括号开头的 GeoJSON 对象 const start = code.indexOf('{'); if (start === -1) { console.error('没有找到 {,可能不是地图数据文件'); process.exit(1); } let depth = 0; let end = -1; for (let i = start; i < code.length; i++) { if (code[i] === '{') depth++; else if (code[i] === '}') { depth--; if (depth === 0) { end = i + 1; break; } } } if (end === -1) { console.error('括号没有闭合,文件损坏或格式不标准'); process.exit(1); } const jsonText = code.slice(start, end); try { const geoJson = JSON.parse(jsonText); const output = process.argv[3] || input.replace(/\.js$/, '.json'); fs.writeFileSync(output, JSON.stringify(geoJson)); console.log('转换完成:', output); console.log('要素数:', geoJson.features ? geoJson.features.length : 'N/A'); } catch (e) { console.error('JSON 解析失败:', e.message); process.exit(1); }逻辑说明:脚本从文件里找第一个{作为 GeoJSON 对象的起点,然后做括号配对,配对到深度归零的位置就是对象的末尾。这样无论对象前面是registerMap('china',还是var map =都不影响提取结果。相比正则,它在处理多层嵌套的coordinates数组时不会提前截断。
参数说明:<input.js>是待转换的老地图文件,[output.json]是可选的输出路径。不传输出路径时,脚本会把输入文件的后缀从.js替换成.json,生成在同目录。转换成功后控制台会打印要素数,方便你立刻确认文件是否完整。
批量转换时,直接写个循环:
for f in maps/*.js; do node convert-map-to-json.js "$f" done2.3 注册地图与首次渲染:最小可用代码
转换完成后,先跑一个最小例子验证数据没问题,不要直接搬进大工程。新建一个 HTML:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> </head> <body> <div id="chart" style="width: 800px; height: 600px"></div> <script> // 用 fetch 异步加载本地地图 JSON fetch('./maps/china.json') .then((res) => res.json()) .then((geoJson) => { echarts.registerMap('china', geoJson); const chart = echarts.init(document.getElementById('chart')); chart.setOption({ geo: { map: 'china', roam: true }, series: [{ type: 'map', map: 'china', data: [] }] }); }); </script> </body> </html>这段代码里关键顺序是:先fetch拿到 GeoJSON,再registerMap,最后init并setOption。如果你把init写在registerMap前面,ECharts 会在setOption时找不到名为china的地图,直接渲染一个空白坐标系。geo.map和series.map的名字必须与registerMap的第一个参数完全一致,大小写也要一致。这一步跑通,后面接 Vue 和 React 只是换个容器而已。
3. 在 Vue 与 React 工程里接入 all-map:注册时机、按需加载和更新策略
3.1 Vue 3 组件里正确注册地图并渲染
框架用法和原生 HTML 唯一的差别在于注册时机与生命周期。Vue 3 组合式 API 里,onMounted阶段 DOM 已存在,最适合echarts.init;而registerMap放在模块顶层或onMounted之前都行,但要保证它先于setOption执行。
<template> <div ref="chartRef" style="width: 100%; height: 560px"></div> </template> <script setup> import * as echarts from 'echarts'; import { onBeforeUnmount, onMounted, ref } from 'vue'; // 把上一章转换好的 china.json 放进 src/map/china.json import chinaGeoJson from '../map/china.json'; const chartRef = ref(null); let chart = null; onMounted(() => { // 注册必须在 init 之前 echarts.registerMap('china', chinaGeoJson); chart = echarts.init(chartRef.value); chart.setOption({ tooltip: { trigger: 'item' }, visualMap: { min: 0, max: 1000, left: 20, bottom: 20 }, geo: { map: 'china', roam: true, label: { show: false }, itemStyle: { areaColor: '#f0f2f5' } }, series: [ { type: 'map', map: 'china', geoIndex: 0, // 复用 geo 的坐标与样式,避免双重渲染 data: [ { name: '广东省', value: 100 }, { name: '江苏省', value: 200 } ] } ] }); }); onBeforeUnmount(() => { chart && chart.dispose(); }); </script>逻辑说明:geoIndex: 0表示这个 map 系列直接复用第一个 geo 组件的地图区域,避免 geo 和 series 各自渲染一套地图导致样式叠加或事件错乱。如果不想用 geo,去掉 geo 配置,series 里自己画视觉即可,但不能两者同时各写一套相同地图的完整配置。
参数说明:visualMap负责把数值映射成颜色,min/max决定了颜色区间。数据里有超过max的值时,建议按业务动态算,而不是写死。roam: true开启缩放和平移,大屏上一般需要。
3.2 React 函数组件里的初始化与销毁
React 端我常遇到两个问题:useEffect依赖数组里放什么,以及组件卸载时没有dispose导致内存泄漏。下面这个写法把初始化放在空依赖的useEffect中,数据变化时单独用setOption更新:
import * as echarts from 'echarts'; import { useEffect, useRef } from 'react'; import chinaGeoJson from './map/china.json'; export default function ChinaMap({ data }) { const chartRef = useRef(null); const chartRefInner = useRef(null); useEffect(() => { // 防止重复注册,ECharts 对重复 registerMap 会给出警告 if (!echarts.getMap('china')) { echarts.registerMap('china', chinaGeoJson); } const chart = echarts.init(chartRef.current); chartRefInner.current = chart; chart.setOption({ geo: { map: 'china', roam: true }, series: [{ type: 'map', map: 'china', geoIndex: 0 }] }); return () => { chart.dispose(); chartRefInner.current = null; }; }, []); useEffect(() => { const chart = chartRefInner.current; if (chart && data) { chart.setOption({ series: [{ data }] }); } }, [data]); return <div ref={chartRef} style={{ width: '100%', height: 560 }} />; }逻辑说明:第二个useEffect只负责更新 series 的 data,不需要重新 init,也不需要重新注册地图。如果依赖数组里写data,并且每次都dispose再init,组件会先白屏再闪回,在大屏上尤其明显。echarts.getMap('china')用来判断地图是否已注册,避免重复注册引发的覆盖和警告。
3.3 按需加载:异步 fetch 地图数据再注册
很多团队把地图文件放在静态目录,前端用fetch按需拉取。这样首屏不需要加载全国和所有省份的地图数据,打开速度能快不少。我自己维护了一个 map-loader:
// map-loader.js import * as echarts from 'echarts'; const fetchingMap = new Map(); export async function loadMap(mapName, url) { // 已注册就直接返回 if (echarts.getMap(mapName)) return; // 防止并发请求同一份地图 if (fetchingMap.has(mapName)) return fetchingMap.get(mapName); const promise = fetch(url) .then((res) => { if (!res.ok) throw new Error(`地图 ${mapName} 加载失败: ${res.status}`); return res.json(); }) .then((geoJson) => { echarts.registerMap(mapName, geoJson); return geoJson; }) .finally(() => { fetchingMap.delete(mapName); }); fetchingMap.set(mapName, promise); return promise; }参数说明:mapName是注册名,建议和地图数据的行政编码一致。url是地图 JSON 的实际地址,比如/map-data/guangdong.json。fetchingMap是请求中的缓存,避免用户快速点击两个不同省份时,同一份数据被并发加载两次。注册完成后再次调用会走getMap快速返回,不再请求网络。
需要注意:异步注册完成后,如果你已经init了图表,需要再调用一次setOption让地图切换到新注册的地图名。也就是说,loadMap只是保证“地图数据已注册”,渲染层的刷新仍由你控制。
4. 政区地图的几个坑:南海诸岛、地名匹配、坐标偏移
4.1 中国地图的南海诸岛显示与替换策略
手头 all-map 包里的china.json有两个版本,一种在右下角带南海诸岛小图,一种不带。不带的那种,默认渲染出来右下角是空的,观众容易误以为是数据缺失,政区展示场景尤其敏感。常见做法是:不要自己去拼 GeoJSON,而是整体换一份较新的全国边界数据。替换时保持registerMap的 name 不变,业务代码一行都不用改。
如果你暂时不能换数据,也可以用 ECharts 的双 geo 方案:主图显示中国大陆,南海诸岛单独用小比例尺的geo放在右下角。双 geo 的两个map需要各自注册,第二个的center和zoom按南海范围调整。这个方案改动更小,但要处理两个 geo 的坐标联动,缩放入口多了维护成本。我更推荐前者,直接换完整数据。
4.2 数据里的地名和地图属性对不上怎么办
地图渲染靠的是 GeoJSON 里每个 feature 的properties.name,而你的业务数据可能存的是简称、旧称或带后缀的名称。比如数据库里是“广东”,地图里是“广东省”;数据库里是“内蒙古”,地图里是“内蒙古自治区”。最常见的处理有两种。
一种是在数据侧对齐:写一个字典或直接用adcode关联,数据表里存行政区划编码,GeoJSON 的properties里也有编码时,优先用编码匹配。另一种是前端用nameMap做映射:
series: [ { type: 'map', map: 'china', nameMap: { 广东: '广东省', 广西: '广西壮族自治区', 内蒙古: '内蒙古自治区', 新疆: '新疆维吾尔自治区', 西藏: '西藏自治区', 宁夏: '宁夏回族自治区' }, data: [ { name: '广东', value: 100 }, { name: '内蒙古', value: 200 } ] } ]nameMap会把 series 数据里的name先映射成地图属性里的标准名,再去做匹配。注意它只解决改名问题,不解决“地图里根本没有这个区域”的问题。如果数据里出现“港澳台”而地图 feature 里分成了“香港”“澳门”“台湾”,需要先在数据层拆开,不能靠nameMap。
4.3 GCJ-02 坐标与底图叠加时的偏移问题
ECharts 地图包里的坐标体系大多来自国内的公开地图数据,坐标经过偏移加密(通常就是 GCJ-02),而不是 GPS 原始用的 WGS-84。如果只是用 ECharts 画纯色块地图,你看不出任何问题,因为渲染的是矢量轮廓本身。但如果你把 ECharts 的地图叠在在线影像底图,或和另一个基于 WGS-84 的地图组件并排对齐,边界会出现几百米的整体偏移。这不是配置错了,是坐标系不一致。
我的建议是:ECharts 的geo组件只用来做展示和交互,不要拿它和 GIS 底图直接叠加。真要和地图引擎叠加,让 GIS 侧输出的 GeoJSON 使用同坐标系,或者在可视化之前做坐标纠偏。多数做政区统计的前端项目不会走到这一步,但一旦涉及多源数据对不齐,优先怀疑坐标系而不是代码。
5. 避坑指南:ECharts 地图不显示的排查清单与修复顺序
如果按上面的步骤做,地图大概率一次就能出来。但遇到不显示的情况,建议按下面的顺序排查,每一条都是实际踩过的。
5.1 现象:地图区域空白,只剩坐标轴网格
刚接触 ECharts 地图的人常遇到这个。原因有三类占比最高:registerMap没有执行;setOption里的map字符串和注册名不一致;echarts引用对象不是同一个。第三个在模块化工程里最隐蔽——框架里用了按需引入的echarts/core,某个组件里又从全局echarts注册,两边不是同一个模块实例,注册等于白注册。
解决:先打开控制台执行echarts.getMap('china'),返回undefined说明注册没生效;返回对象说明注册成功,问题在setOption配置。确保全工程统一用import * as echarts from 'echarts',不要混用全局变量和按需引入。
5.2 现象:控制台提示重复注册,切换省份时地图闪白
registerMap对同一个名字重复调用,ECharts 会直接覆盖并给出警告。如果你在每次点击省份时都无条件注册新地图,闪白就是因为新地图注册完成后,setOption触发了整图重绘。
解决:所有注册入口统一走echarts.getMap(mapName)判断,已注册就跳过。我之前给的loadMap里已经包含这个逻辑,直接复用即可。如果你要在已有图例或 tooltip 上做增量更新,用chart.setOption(option, { notMerge: true })前要想清楚哪些配置需要保留,不要图省事全量覆盖。
5.3 现象:JSON.parse 报错,提示 Unexpected token
地图文件本身是 UTF-8 带 BOM 的情况下,JSON.parse会读到开头的\uFEFF而报错。这类文件从 Windows 上下载或从 rar 包里直接拖出来的概率很高。用fetch拿数据时,浏览器通常会自动处理 BOM,但fs.readFileSync和某些构建工具的 raw loader 会把 BOM 原样带进来。
解决:在转换脚本里读文件后先做code = code.replace(/^\uFEFF/, ''),或者用JSON.parse(text.replace(/^\uFEFF/, ''))。如果你遇到的是“第一行是空行导致的行号偏移”,通常也是 BOM 或\r\n换行引起的,转 JSON 前统一做一次文本清洗会更省事。
5.4 现象:同时配置 geo 和 series-map,颜色和边界互相覆盖
常见写法是把geo和series.map各配一套完整地图,结果要么只有 geo 的颜色,要么系列数据不显示。这是因为两个组件各自渲染了一份地图,图层叠加在一起,事件和视觉都乱了。
解决:只保留一个地图源。推荐保留geo(地图底图和 roam 交互),然后让 series 通过geoIndex: 0挂到同一个地理坐标系上。反过来的写法也成立,保留series.map并去掉geo,视觉更简单。二者只能留一个作为主渲染,另一个做辅助图层。
5.5 现象:地图在弹窗或折叠面板里渲染空白
容器在init时宽度或高度为 0,ECharts 会记录一个无效尺寸,之后即使容器展开,地图也不会自动恢复。这个坑不只是地图有,所有基于 Canvas 的图表都有。
解决:容器显示后再执行init。如果已经 init 过,容器展开后调用chart.resize()。在弹窗场景里,我一般用nextTick或者监听弹窗的open事件再初始化,避免去猜容器何时可见。这条建议放在排查末尾,但它发生的频率一点都不低。
6. 进阶:基于 all-map 做省市下钻与数据回退
6.1 点击下钻与返回上一级的状态管理
地图下钻是政区可视化最常用的交互。思路很简单:点击省份时,用省份名找到对应地图文件,loadMap注册后setOption切换地图;返回时从栈里弹出上一级,重新切换回全国。关键是栈的管理和地图数据的异步加载。
// drill-down.js import * as echarts from 'echarts'; import { loadMap } from './map-loader'; // 省份名 -> 地图数据文件名 const MAP_KEY_MAP = { 广东省: 'guangdong', 江苏省: 'jiangsu' }; export function bindDrill(chart, onBack) { const stack = []; // 记录每一级的点击参数 chart.on('click', async (params) => { const name = params.name; const mapKey = MAP_KEY_MAP[name]; if (!mapKey) return; // 没有对应下钻数据,不动作 if (stack.length && stack[stack.length - 1].name === name) return; // 重复点击同一省份 stack.push(params); try { await loadMap(mapKey, `/map-data/${mapKey}.json`); chart.setOption({ geo: { map: mapKey }, series: [{ map: mapKey }] }); } catch (e) { stack.pop(); // 加载失败就把栈恢复原状 } }); return function back() { if (!stack.length) return; stack.pop(); const prev = stack[stack.length - 1]; const mapName = prev ? MAP_KEY_MAP[prev.name] : 'china'; loadMap(mapName, `/map-data/${mapName}.json`).then(() => { chart.setOption({ geo: { map: mapName }, series: [{ map: mapName }] }); // 回到全国时通知外部,后续可以刷新其他图表 if (!prev && onBack) onBack(); }); }; }逻辑说明:stack里存的是点击事件的params,所以能拿到地图名称。返回时先弹栈,再看栈顶是否存在;栈为空说明回到全国。loadMap内部已有去重和缓存,重复调用不会重复请求。MAP_KEY_MAP是省名到文件名的映射,如果你的 all-map 文件命名和省市标准缩写不一致,在这里调整一次即可,不用改业务代码。
这个方案的边界在于:它只解决“地图区域切换”,数据联动需要在上层做。比如下钻到广东省后,右侧图表要同步展示广东的指标,你需要在params.name和实际数据之间建立关联。我的习惯是把当前层级和省份存到一个全局 store,由它驱动整个看板更新,而不是在下钻函数里塞业务逻辑。
6.2 边界数据会过期:更新策略与投入判断
all-map 包的价值在于“历史完整”,但行政边界不是一成不变的。做展示型项目,旧边界完全够用;做政区统计和涉及实际业务归属的项目,边界错误会直接带偏数据聚合结果。我的建议是:把地图数据当成业务资源来管理,定期用较新的边界数据替换仓库里的 JSON,替换时保持registerMap的名字不变,功能代码不动。
如果团队已经有 GIS 能力,也可以直接从内部地图服务导出 GeoJSON,走相同流程注册进 ECharts。这个方案值得投入,因为地图数据更新一次,项目里所有地图组件同时生效。说到底,echarts-all-map.rar 是被数据来源卡住时的救急方案,而不是终极方案——先把项目跑起来,再规划边界数据的迭代路径,这是我认为最务实的顺序。上面这套下钻和数据加载方案,我在多个大屏项目里复用过,最大的教训是:地图不显示时先查注册名,查容器尺寸,再查数据和样式,这个排查顺序能少走很多弯路。希望帮到你。
本文还有配套的精品资源,点击获取