资讯动态

ECharts 中国地图 china.js 注册、下钻与按需加载实践

发布时间:2026/10/26 17:43:21 来源:尧图企业网站定制
简介这份压缩包面向使用 ECharts 制作中国地图和省级区域可视化的前端开发者、数据分析人员及可视化学习者重点解决网页地图底图缺失、全国与地方数据难以分层展示的问题。包内共 35 个 js 文件压缩包约 841KB以 china.js 全国地图脚本和各地省级地图脚本为主可直接配合 ECharts 的 geo、series 配置使用覆盖全国轮廓、省份边界及区域数据呈现等常见需求。已有 5705 人学习下载适合作为地图类图表入门与项目实践的参考素材。读者可获得较完整的地图脚本集合用于区域着色、标注提示、缩放漫游、点击交互和动态数据更新等场景也能据此梳理全国与省级地图的配置方式减少自行整理地理数据的时间为省市级可视化页面开发提供现成基础。1. 一个只有地图容器、却怎么都不出图的下午那年接手一个运营看板页面上摆着一个 800×600 的div需求写得干脆中国地图、各省份销售额染色、点击下钻到省。我照着记忆写完echarts.init写上series.type map、map: china刷新屏幕上干干净净控制台也安安静静。排查了快半小时才想起来ECharts 5 之后内置地图数据被拆出去了china.js这类地图文件得自己拿、自己registerMap。这就是 china.js 中国及各地 js 文件压缩包的价值所在——它不是组件库是一批按行政级别切好的地图数据脚本专门喂给 echarts 的地图组件。做数据大屏、区域经营看板、门店或物流分布图的前端还有那些内网环境里不方便临时去抠 GeoJSON 的人拿到它就能少走这一段弯路。2. 压缩包里到底是什么registerMap 的注册机制与文件构成先把一个误解掰正这套文件里没有任何图表逻辑没有主题、没有配置项、没有封装好的组件。它只是「数据」只不过穿了js的外衣。真正的渲染能力来自 ECharts 本身地图文件负责把一份行政区边界的几何数据塞进 ECharts 的全局地图注册表让series.type map或者geo组件能按名字找到它。理解了这层关系后面所有的加载、下钻、按需引入才有得谈。2.1 从 registerMap 说起js 文件与 GeoJSON 的等价关系打开任意一个地图 js剥掉外层包装核心只有一句echarts.registerMap(名字, GeoJSON)。外层那段 UMD 包装是为了兼容三种运行环境AMD、CommonJS 和直接script引入。所以同一份数据在 Webpack 项目里require能拿到registerMap的副作用在纯静态页里script src也能自动执行注册。// 一份典型地图 js 的内部结构结构示意非原始代码 (function (root, factory) { if (typeof define function define.amd) { // AMD配合 RequireJS 这类加载器 define([exports], factory); } else if (typeof exports object) { // CommonJS / Node 打包环境 factory(exports); } else { // 浏览器全局挂到 window.echarts 上 factory((root.echarts root.echarts || {})); } })(this, function (exports) { // 全部内容就只有这一次注册调用 exports.registerMap(某省级行政区, { type: FeatureCollection, // GeoJSON 顶层类型固定写法 UTF8Encoding: true, // 部分老文件带的编码标记可忽略 features: [ { type: Feature, properties: { name: 下辖某市, // 与数据里 name 对齐的关键字段 adcode: 000000, // 行政区划代码下钻和联动的稳定主键 cp: [116.4, 39.9] // 区域中心点经纬度做标签定位用 }, geometry: { type: Polygon, // 或 MultiPolygon海岛类常见后者 coordinates: [ /* 环状坐标数组闭合多边形 */ ] } } ] }); });第一段是环境判定决定了这份文件在打包器和浏览器里都能直接用。第二段是真正干活的地方registerMap的第一个参数是地图名之后option里写map: 某省级行政区时必须和它逐字一致中文、空格、后缀都不能差第二个参数是标准 GeoJSON 的FeatureCollectionfeatures数组里每个元素是一个行政区面。properties.adcode这个字段值得单独记一笔name可能因为简称/全称对不上导致染色失败但adcode是数字主键做下钻、做区域联动、做数据表关联时用它比用名字稳得多。2.2 目录结构与命名规则全国包、省级包与特殊文件压缩包解开之后大致是三个层次一份全国地图一批省级地图外加少量特殊用途文件。文件名基本就是行政区名称中文命名居多个别版本会带-或拼音后缀做区分。判断该引哪份的标准很简单——看你的map字段要填什么名字以及用户能下钻到第几级。文件类型内容体积量级使用场景全国地图 js省级行政区面数据数百 KB 至 1 MB 级首屏总览、全国分布省级地图 js地级行政区面数据数十 KB 至 200 KB 级点击省份后下钻特殊区域文件单个地市或特殊区域几十 KB局部放大、专题图配套 geoJSON与 js 同源的几何数据与 js 相近走异步加载路线时用体积那一列只能给量级因为同一份数据在压缩、精度保留程度不同的情况下差异很大。实际经验是全国包塞进首屏 bundle加上 ECharts 本体和业务代码首屏 JS 很容易堆到 1.5 MB 以上移动端或弱网场景下首屏白屏时间会明显拉长。这也是后面第 6 章要讲按需加载的原因。2.3 三个容易被忽略的字段name、adcode 与 cpname是渲染和匹配的主力但它是人写的容易出现简繁、简称全称、新旧名称不一致的问题。adcode是数字稳定适合做关联主键。cp是中心点坐标做地图标签、做飞线起点、做聚合点的默认位置都用得上很多自己转 GeoJSON 的团队会漏掉它导致标签只能飘在多边形几何中心上一个狭长地市的名字会落在很奇怪的位置。还有一个坐标系问题必须提前说清楚地图 js 里的经纬度是常规地理坐标WGS84 近似如果你的散点数据来自某些使用偏移坐标的在线底图服务两者叠加会出现整体平移看起来像「点飘到海里去了」。这不是 echarts 的玄学是坐标系没对齐。解决办法是让业务数据在入库时就统一到同一套坐标别在渲染层做补偿。提示判断一份地图文件能不能用最省事的办法不是看图好不好看是在控制台打印echarts.getMap(地图名)能拿到对象且geoJson.features.length不为 0说明注册成功。3. 跑起来把中国地图挂到页面上理论讲完落到能跑的东西上。这一章的目标是新建一个 HTML 文件双击打开就能看到中国地图并且能把一份最朴素的数据染上去。不依赖构建工具不依赖脚手架先把链路打通再谈工程化。3.1 最小可运行页面八行 option 出图先准备目录一个index.html一个lib文件夹放 ECharts 的 dist 文件一个map文件夹放地图 js。地图文件的引入顺序必须在 ECharts 之后因为它的包装函数会去找window.echarts早于 ECharts 引入就会直接报echarts is not defined。!DOCTYPE html html langzh-CN head meta charsetUTF-8 / title地图最小示例/title style /* 容器必须有明确宽高否则 init 出来是 0×0图不会显示 */ #map { width: 900px; height: 600px; } /style /head body div idmap/div !-- 1. 先加载 echarts 本体 -- script src./lib/echarts.min.js/script !-- 2. 再加载地图数据它会自动执行 registerMap -- script src./map/china.js/script script const chart echarts.init(document.getElementById(map)); const option { tooltip: { trigger: item }, series: [{ type: map, map: china, // 必须与 registerMap 里的名字完全一致 roam: true, // 允许缩放和平移大屏场景建议开 label: { show: false },// 省份多时先关掉避免文字重叠 itemStyle: { areaColor: #f3f6fb, borderColor: #c9d3e3 }, emphasis: { itemStyle: { areaColor: #ffe7a3 } // 悬停高亮 }, data: [] // 数据后补格式 [{ name: 某省, value: 100 }] }] }; chart.setOption(option); // 容器尺寸变化时必须手动调否则拖窗口会错位 window.addEventListener(resize, () chart.resize()); /script /body /html这段代码里几个参数值得展开。map: china是唯一的地图标识写错就是空白图不会报错非常隐蔽。roam: true打开滚轮缩放和拖拽大屏上体验好但嵌在小卡片里容易误操作我一般只在全屏视图里开。label.show默认是开的全国视图下 30 多个标签挤在一起会糊成一片先关掉、只在emphasis里开悬停标签是更常见的做法。最后那个resize监听别省容器跟着 flex 布局变宽时不调resize地图会保持旧尺寸看起来像「图被裁了一半」。3.2 系列类型怎么选map、geo 与散点图的组合新手最容易在这里翻车想在地图上叠散点于是写了一个type: map的系列又写了一个type: scatter的系列结果散点坐标被当成像素值点全堆在左上角。原因是散点系列默认用直角坐标系要让它认地图坐标得给它配coordinateSystem: geo而geo是一个独立组件不是map系列。两者的区别记一句话就够series.map是「一张会染色的地图」geo是「一套地图坐标系」散点、飞线、涟漪点都要挂在geo上。如果你既想染色又想叠点通常有两种写法——用geo当底图再加一个map系列并设置geoIndex让它复用坐标系或者只用geo用regions手写区域颜色。前者配置少后者控制细。// 常见组合geo 打底 map 系列染色 散点叠加 const option { geo: { map: china, roam: true, itemStyle: { areaColor: #eef3fa, borderColor: #c2cfe0 } }, series: [ { type: map, geoIndex: 0, // 复用上面那个 geo 的坐标系和底图 data: [] // 染色数据 }, { type: scatter, coordinateSystem: geo, // 关键声明使用地理坐标 symbolSize: 8, data: [ /* [{ name, value: [lng, lat, 数值] }] */ ] } ] };geoIndex: 0表示这个 map 系列跟着第 0 个 geo 组件走好处是底图只渲染一次缩放平移时散点和面始终同步。散点的value是三元素数组[经度, 纬度, 业务值]前两个是坐标第三个才是数值维度visualMap和symbolSize回调都读第三位。刚上手时经常写成[值, 经度, 纬度]图能出来但位置离谱属于典型顺序踩坑。3.3 数据绑定与 visualMap染色不生效的三个检查点染色不生效是反馈最多的问题排查顺序我固定成三步。第一步看数据的name地图里叫「某省」数据里写「某省份」字符串不相等就不会匹配这种情况用nameMap做一次重命名最省事。第二步看visualMap的seriesIndex页面上有多个系列时不指定索引它会作用到第一个系列上。第三步看min/max数据全部挤在极小区间时颜色差异肉眼看不出来看起来像没生效。const option { visualMap: { type: continuous, // continuous 连续 / piecewise 分段 min: 0, max: 1000, // 别用数据的实际极值硬写留出业务口径的上下限 left: 20, bottom: 20, text: [高, 低], inRange: { color: [#e8f3ff, #4a90e2, #1d4f91] }, seriesIndex: 0 // 明确指定作用于哪个系列 }, series: [{ type: map, map: china, data: [ { name: 某省A, value: 820 }, { name: 某省B, value: 145 } ] }] };inRange.color建议三到五档超过五档人眼分辨不出来图例也会变长。max写死业务上限而不是数据极值是为了让不同时间点的截图可比——今天最大值 900明天最大值 200若跟着数据自适应同一个颜色在两天里代表不同含义看图的人会误读。4. 核心链路地图下钻与按需加载的实现全国地图只是入口真正的业务价值在下钻点某个省加载该省的地图展示地市级分布再点能返回。这条链路里容易出问题的不是 echarts 配置而是加载时序和状态管理。省文件是异步来的用户手快连点两次两个请求回来顺序颠倒地图就可能停在一个错误层级上。4.1 下钻的两种做法多系列预注册与动态换图第一种做法在初始化时把所有省文件都script进来一次性注册完下钻只是切换map字段。优点是切换零延迟缺点是首屏要下载全量数据几十个文件加起来的体积对首屏很不友好只适合内网或离线演示。第二种是下钻时按需加载对应省的 js加载完再registerMap然后setOption换图。这是线上项目的主流做法。它带来两个额外问题重复点击会重复插入script以及返回上一级时要把map切回全国、把数据换回全国口径。下面这段封装同时解决了这两点。// 地图加载器缓存 并发去重 失败重试 const mapLoader (() { const cache new Map(); // 已注册的地图名 - Promise const BASE ./map/; function loadScript(name) { return new Promise((resolve, reject) { const el document.createElement(script); el.src ${BASE}${name}.js; el.async true; el.onload () resolve(name); el.onerror () reject(new Error(地图加载失败: ${name})); document.head.appendChild(el); }); } return function ensureMap(name) { // 已注册过就直接返回避免重复请求和重复插入 script if (echarts.getMap(name)) return Promise.resolve(name); // 正在加载中的同名请求复用同一个 Promise解决连点竞态 if (cache.has(name)) return cache.get(name); const task loadScript(name) .then(() { if (!echarts.getMap(name)) { throw new Error(脚本已加载但未注册: ${name}); } return name; }) .catch((err) { cache.delete(name); // 失败要清缓存否则永远重试不了 throw err; }); cache.set(name, task); return task; }; })();这段封装有三个关键点。echarts.getMap(name)是同步的注册检查能挡掉绝大多数重复加载。cache存的是 Promise 而不是布尔值是为了让同一时刻的多个调用共享一次网络请求——用户连点两下只会发一次。catch里删缓存是后悔药网络抖动导致的失败如果不清理这个地图在当前会话里就再也加载不了了。el.async true保证不阻塞解析但也意味着onload之后地图一定已经注册好了因为注册语句在脚本顶层同步执行。4.2 下钻与返回状态机比 if-else 好维护有了加载器下钻逻辑本身不复杂但层级状态必须显式记下来否则「返回上一级」会变成猜谜。我一般维护一个栈栈里存{ mapName, data, title }下钻时压栈返回时出栈。const stack [{ mapName: china, title: 全国, data: nationwideData }]; async function drillTo(mapName, title, data) { try { await mapLoader(mapName); // 先确保地图注册完成 stack.push({ mapName, title, data }); render(stack[stack.length - 1]); } catch (e) { // 加载失败时保持当前层级只给轻提示不要清空地图 console.warn(e.message); } } function goBack() { if (stack.length 1) return; // 已经在最顶层直接返回 stack.pop(); render(stack[stack.length - 1]); } function render(state) { chart.setOption({ title: { text: state.title }, series: [{ type: map, map: state.mapName, data: state.data }] }, true); // 第二个参数 true 表示不合并彻底替换旧配置 }setOption(option, true)那个true很关键。默认是合并模式从全国切到某个省时旧的data、旧的visualMap极值都可能残留表现就是「换了地图但颜色还是上一级的口径」。彻底替换会丢掉一些公共样式配置所以实践中我把公共样式抽成一个baseOption每次Object.assign拼出来再setOption(..., true)。4.3 交互回退面包屑、双击返回与键盘下钻做了返回就一定得做而且要显眼。最常见的做法是右上角面包屑点任意一级直接跳回。实现上不要用pop循环直接把栈截断到目标索引即可。另外两个细节容易被忘一是地图区域点击事件要用chart.on(click, params ...)并判断params.componentType series否则点图例、点标题都会触发下钻二是从省级返回全国时roam的缩放状态不会自动重置用户看到的是上次放大后的全国图需要调chart.dispatchAction({ type: restore })或者重新setOption。4.4 渲染取舍canvas 与 svg、大数据量下的降级ECharts 默认用 canvas 渲染地图面数多、边界点密的时候canvas 在低端设备上首次渲染会明显卡顿。如果页面还要做高清截图、导出 PDFsvg 渲染器更合适echarts.init(el, null, { renderer: svg })即可切换代价是复杂图形下 DOM 节点数量暴涨。我的判断标准是面数过千、需要频繁重绘的选 canvas图形简单、需要矢量导出的选 svg。还有一个常被忽略的点是地图精度。同一份边界数据高精度版本的点数是低精度版本的好几倍在手机上看全国地图那些细微的海岸线弯曲根本分辨不出来却要付出几倍的解析和渲染时间。如果压缩包里带了简化版文件移动端优先用简化版这是最省事的性能优化。5. 避坑与排查五条踩过的记录这一章全是现场记录每条按「现象、原因、解决」写。地图这块的坑有个共同特点不报错只是不出图或者出得不对所以排查全靠对照检查。5.1 地图一片空白控制台毫无提示现象容器宽高正常init没报错页面就是没有图形连边框都没有。原因series.map的值和registerMap注册的名字不一致或者地图 js 压根没加载成功。后者常见于路径写错——./map/china.js在打包后可能变成相对根目录的路径404 了但浏览器不一定会显眼地报出来。解决控制台先执行echarts.getMap(china)返回undefined就是没注册成功再打开 Network 面板看那个 js 的响应状态。名字不一致的情况用console.log(Object.keys(...))之类的手段把已注册的地图名列出来比对比肉眼改快得多。5.2 数据染不上色只有底图现象地图出来了data也传了但所有区域都是同一个颜色。原因三种可能按概率排序是name不匹配、visualMap没指定seriesIndex、min/max覆盖了数据范围。name不匹配最常见地图里写的是全称业务表里存的是简称。解决在visualMap和series之间先做一次名称归一化用nameMap配置或者加载数据时map一遍。排查时可以临时把所有区域设成固定色只留一个区域写value: 999看它变不变色能快速定位是匹配问题还是映射问题。5.3 打包后体积翻倍首屏白屏时间变长现象本地开发很快构建上线后首屏明显变慢产物分析里能看到一堆地图 js。原因用import ./map/某省.js这种静态引入打包器会把所有引用到的地图全部打进主 chunk哪怕用户根本没点下钻。压缩包里的文件是「全量可用」不代表「全量该加载」。解决改成动态import()让打包器单独切 chunk或者干脆不走打包器用第 4 章那个loadScript从静态目录按需拉。判断标准很简单——地图文件是否在首屏就必然用到不是就异步。5.4 缩放平移之后散点和地图对不上现象初始状态点位置正确用户滚轮放大后点开始偏移越放越离谱。原因散点系列没配coordinateSystem: geo走的是直角坐标系或者散点和地图分别用了两个独立的geo各自缩放不同步。解决散点必须声明coordinateSystem: geo并且与地图共用同一个geo组件geoIndex指向它。如果用的是series.map而非geo那散点无法直接叠上去需要改成geo打底的写法这是结构问题不是调参能救的。5.5 从省级返回全国后颜色和标题错乱现象下钻正常点返回后地图变成全国了但颜色还是省里的口径标题也没变回来。原因setOption用了默认的合并模式上一层的data、visualMap.max、title都残留在配置里没被覆盖的字段会一直留着。解决切层级时用setOption(option, true)完全替换或者显式把data、title、visualMap都重写一遍。我一般抽一个buildOption(state)纯函数用栈顶状态生成完整配置切层级只调这一处避免东补西补。注意行政区划是会调整的撤县设区、改名之后旧地图文件里的adcode和name会和新业务表对不上。这类问题不会报错只会表现为某几个区域永远染不上色排查时容易被当成代码 bug。6. 二次开发用 GeoJSON 加动态 import 替掉整包 js真正长期维护的项目我不会直接把压缩包原样扔进仓库。原因有两个地图 js 是「可执行脚本」在严格的 CSP 策略下可能被拦而且它把数据和注册逻辑耦在一起做数据校验、做增量更新都不方便。更顺手的做法是把 js 转成纯 GeoJSON自己控制加载和注册。6.1 转换脚本把 js 里的 GeoJSON 抽出来写个一次性脚本在 Node 里模拟一个假的echarts对象接住registerMap的调用跑一遍所有 js就能把每个文件里的 GeoJSON 落盘成.json。这样做的收益是数据与代码分离GeoJSON 可以走 fetch 缓存、可以压缩、可以做 diff。// convert.js —— 在 Node 环境把地图 js 转成 geoJSON 文件 const fs require(fs); const path require(path); const vm require(vm); const SRC_DIR path.resolve(__dirname, map); const OUT_DIR path.resolve(__dirname, geo); fs.mkdirSync(OUT_DIR, { recursive: true }); // 造一个假的 echarts 对象只实现 registerMap const registry {}; const sandbox { echarts: { registerMap(name, geoJson) { registry[name] geoJson; } }, window: {}, // 部分文件会读 window给个空对象兜住 module: { exports: {} }, exports: {} }; sandbox.self sandbox.window; vm.createContext(sandbox); for (const file of fs.readdirSync(SRC_DIR)) { if (!file.endsWith(.js)) continue; const code fs.readFileSync(path.join(SRC_DIR, file), utf8); try { vm.runInContext(code, sandbox); // 执行脚本触发 registerMap } catch (e) { console.warn(跳过 ${file}: ${e.message}); } } // 统一写出文件名用地图名 for (const [name, geo] of Object.entries(registry)) { const features geo geo.features ? geo.features : []; if (!features.length) continue; fs.writeFileSync( path.join(OUT_DIR, ${name}.json), JSON.stringify({ type: FeatureCollection, features }) ); console.log(${name}: ${features.length} 个区域); }vm.runInContext让脚本在一个隔离的沙箱里执行沙箱里只有一个伪造的echarts所以脚本调用registerMap时数据就被我们接住了不会真的去操作 DOM。window、module、exports这几个对象是为了让 UMD 包装的三种分支都能跑通。跑完之后geo目录下就是干净的数据文件体积通常比 js 小一截因为省掉了包装代码和多余的转义。6.2 按需加载与缓存把 GeoJSON 接回 echarts转出来之后加载逻辑要自己写registerMap由我们手动调。好处是可以加 HTTP 缓存、可以做加载失败重试、可以在注册前做一次数据体检。const geoCache new Map(); async function loadGeoMap(name) { if (echarts.getMap(name)) return name; if (geoCache.has(name)) return geoCache.get(name); const task fetch(/geo/${name}.json) .then((res) { if (!res.ok) throw new Error(HTTP ${res.status}); return res.json(); }) .then((geo) { // 注册前做一次体检避免脏数据导致空白图 if (!geo.features || !geo.features.length) { throw new Error(地图数据为空: ${name}); } echarts.registerMap(name, geo); return name; }) .catch((e) { geoCache.delete(name); throw e; }); geoCache.set(name, task); return task; }fetch走的是浏览器缓存策略配合服务端的Cache-Control可以做到二次进入零等待比每次插入script干净。体检那一步别省数据管线出问题时一个features: []的空对象注册进去页面就是纯白排查成本远高于在这里抛错。6.3 上线前的验证清单改完之后别急着发我固定跑一遍下面这几项。地图这类东西出错不报异常靠肉眼看图很容易漏掉个别区域。验证项方法预期注册完整性遍历所有地图名调getMap返回对象且features非空区域数量统计features.length与业务表行政区数量一致名称对齐数据侧name与地图侧取交集交集覆盖率接近 100%坐标系抽一个已知坐标的点叠加落点在预期区域内层级切换下钻两层再返回两次标题、颜色、缩放全部复位那张表里「名称对齐」一项最值得自动化我一般写个脚本跑一次比对把对不上的名字打出来人工确认是简称问题还是区划调整问题然后决定加nameMap还是更新数据源。这件事做一次能省掉后面无数次「这个市怎么没颜色」的追问。血泪经验是地图数据别当成静态资源一放了之它和业务数据一样会过期。我现在的习惯是每次区划口径有变动、或者业务方换了一批区域表就把上面那张验证清单强制走一遍先跑数据比对再改代码。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑