ECharts地图实战:china.js接入、registerMap注册与行政区域下钻避坑指南
简介一份面向ECharts数据可视化开发者的中国地图JavaScript资源集解决在网页中快速绘制中国及省级行政区地图的需求。压缩包内共35个js文件以china.js为核心并配套全国34个省级行政区各自的独立js文件文件类型单一、体积紧凑整体仅841KB可直接引入项目使用。已有5703人学习下载。使用时先加载china.js再在ECharts的geo配置中指定map为china即可渲染出支持缩放、拖拽和点击交互的中国地图各省js文件则支撑下钻到省一级视图结合事件绑定可完成区域高亮、数据提示、动态更新等常见可视化功能。这套资源省去了开发者手动整理GeoJSON的繁琐步骤拿到即可运行适合构建数据大屏、区域分析仪表盘或物流分布地图等场景同时包内文件命名规范、按省级区域拆分便于按需单独引入对应数据既能帮助初学者理解ECharts地图配置流程也能直接复用于正式项目交付。1. 做数据大屏时ECharts 的地图组件为什么绕不开 china.js做数据可视化大屏时经常会遇到这样一个情况ECharts 的柱状图、折线图画得很顺手一到地图组件就白屏、报“Map 不存在”、或者只显示一个空坐标系。原因很简单——ECharts 从 5.0 版本之后不再内置地图数据地图 GeoJSON 需要自己准备。而 china.js 这个压缩包就是打包好的国内地图边界数据文件集合里面既有全国地图也有按省、按市拆分的独立 JS 文件配合 ECharts 的 registerMap 接口使用是大屏项目里最常见的地图数据来源。这篇笔记我会从数据结构讲起把接入步骤、省市联动、踩坑排查一次说透适合正在做地图可视化、需要快速把手头数据落到地图上的开发者。2. china.js 内部结构与 registerMap 注册原理先搞懂数据长什么样再决定怎么接2.1 拆开压缩包目录布局与两种格式的区别拿到压缩包第一件事不是急着引入页面而是先解压看一下里面文件的组织方式。我一般会先看目录树再打开一个 JS 文件确认它的导出格式。大多数同类资源包的目录结构基本如下china.js # 全国地图包含各省及南海诸岛 province/ beijing.js shanghai.js guangdong.js zhejiang.js ... city/ beijing/ beijing.js dongcheng.js xicheng.js ...目录命名上省份和城市一般采用拼音小写部分城市子目录里还包含区县级数据。实际使用时如果你只需要省级粒度大概率只用得到根目录的china.js和province/下的文件如果要做下钻才会用到city/里的区县数据。打开一个 JS 文件看内容你会发现它实际上是直接把 GeoJSON 数据赋值给一个全局变量// 截取自 beijing.js 的典型结构 var beijing { type: FeatureCollection, features: [ { type: Feature, properties: { name: 北京市, adcode: 110000, center: [116.407526, 39.90403], centroid: [116.41995, 40.19064], childrenNum: 16, level: province, parent: { adcode: 100000 } }, geometry: { type: MultiPolygon, coordinates: [ ... ] } } ] };这个var beijing {...}的写法决定了它和 ECharts 对接时有两种方式。如果你的项目是传统方式直接script引入那么引入之后全局作用域里会多出beijing这个变量后续只要把它作为第二个参数传给registerMap即可。有的压缩包里还会附带 GeoJSON 原文件不带var xxx 的.json文件这就涉及到另一种加载方式用fetch请求 GeoJSON再调用echarts.registerMap(beijing, data)。对于现代前端工程用.json原文件配合 fetch 加载是更干净的方式——它不污染全局变量数据放在项目资源目录里由构建工具统一管理版本。这里要特别注意一个细节JS 文件里的变量名是固定的beijing、shanghai如果你同时引入多个省份的 JS全局变量会越来越多一旦项目里有同名的全局变量就会冲突。这也是为什么很多工程师更倾向于使用 JSON 原文件 fetch 的方案后面第 6 章我会给出一套封装思路。2.2 coordinates 三维数组与 GeoJSON 标准写法要真正理解 china.js 能做什么需要先读懂 GeoJSON 的核心结构。它本质上是一个“FeatureCollection”也就是一个包含多个 Feature 的集合。每个 Feature 包含 properties属性和 geometry几何信息。几何部分的 coordinates 是嵌套的多维数组这是最容易看晕的地方。常见的有三种Point一个二元数组如[116.4, 39.9]表示经纬度坐标。MultiPolygon四维数组第一层是多个多边形面比如海南省包含很多岛屿第二层是某个面的环第三层是环上的点数组。Polygon三维数组最外层只有一个面适合没有飞地的省份。举个实际例子一个省级行政区可能由主岛和若干离岛组成所以地理数据通常是MultiPolygon。在 ECharts 渲染时它会把每个面分别填充颜色、绘制边界这就是地图能正确显示多个岛屿的底层原因。// 一个简化的 MultiPolygon 结构描述 { type: MultiPolygon, coordinates: [ [ [ [lng1, lat1], [lng2, lat2], [lng3, lat3] ] // 第一个面 ], [ [ [lng4, lat4], [lng5, lat5], [lng6, lat6] ] // 第二个面离岛 ] ] }明白了 coordinates 的层级你就可以在投递地图数据前先做一个校验用代码判断geometry.type是Polygon还是MultiPolygon然后递归取坐标算出边界框。这是后面做区域聚焦、地图缩放时的基础。2.3 registerMap 的第二个参数到底传什么echarts.registerMap是地图组件注册的唯一入口它的签名是registerMap(mapName, geoJSON, specialAreas)。在 ECharts 5.x 中mapName是你自己定义的名称后面series里的map字段要与之保持一致。geoJSON就是上面提到的 FeatureCollection。第三个参数specialAreas是可选的它是一个对象用来把 GeoJSON 中的某些区域映射成另外的形状。最典型的场景是南海诸岛很多版本的全国地图里南海诸岛被简化或者不显示在底图范围内这时候把它单独拉出来放到右下角的小方框里。// 注册地图时的典型调用 echarts.registerMap(china, chinaJson); // 如果需要对南海诸岛做特殊处理 echarts.registerMap(china, chinaJson, { 南海诸岛: { // 重新指定显示区域范围常见做法是映射到左下角 left: 110, top: 0, width: 40, height: 20 } });这里有个容易踩坑的点specialAreas的 key 必须与 GeoJSON 中properties.name完全一致多一个空格都匹配不上。所以在使用特殊区域之前先console.log一下数据的 properties确认 name 的实际值。另一个要点是如果你用的 ECharts 版本是 5.xGeoJSON 里的坐标系通常是经纬度EPSG:4326。有些网上找来的地图数据是投影坐标系比如 EPSG:3857直接注册会出现地图形状被拉伸得不成样子。后面我会在避坑章节专门讲这种坐标系的识别方法。3. 在 ECharts 里把 china.js 跑起来完整接入步骤与数据映射3.1 最小可运行示例引入、注册、配置三步走把地图跑起来其实只需要三步引入 ECharts、注册地图、配置 series。先看一个使用 JS 文件版的最简示例确认整体链路通畅。!DOCTYPE html html head meta charsetUTF-8 title地图最小示例/title script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script script src./china.js/script /head body div idmapContainer stylewidth: 800px; height: 600px;/div script // 1. 获取容器 DOM var dom document.getElementById(mapContainer); // 2. 初始化 echarts 实例 var myChart echarts.init(dom); // 3. 注册地图china 是全局变量来自 china.js echarts.registerMap(china, china); // 4. 配置并渲染 myChart.setOption({ series: [{ type: map, map: china, roam: true, label: { show: true, fontSize: 10 } }] }); /script /body /html逻辑说明第 3 步中registerMap(china, china)的第一个参数china是一个自定义的注册名你可以叫 it中国地图也可以叫cn只要后续series.map字段保持一致即可。第二个参数取的是全局变量china它由china.js在页面加载时创建。参数说明roam: true允许用户通过滚轮缩放、拖拽平移地图这是大屏交互的基础配置。label.show控制是否显示省份名称如果地图区域太小建议把 fontSize 调小或者直接关闭否则文字重叠会非常影响观感。跑通这个示例后你会发现全国地图立刻就能显示出来了。接下来要做的就是把业务数据塞上去。3.2 各省份子文件的加载路径与异步处理在很多项目中你需要展示省份粒度的数据就不必每次都加载完整的全国地图而是按需加载对应省份。这种方式能显著减少首屏加载体积尤其在做专项运营大屏时很有用。传统script引入的写法是script src./province/beijing.js/script script echarts.registerMap(beijing, beijing); /script问题在于如果省份是用户在页面上切换的你就不能把所有省份的 JS 全部一次性引入。这时候常见做法是动态创建script标签等脚本加载完成后再注册地图。function loadScript(url) { return new Promise(function(resolve, reject) { var script document.createElement(script); script.src url; script.onload resolve; script.onerror reject; document.head.appendChild(script); }); } async function loadProvinceMap(provinceName) { await loadScript(./province/ provinceName .js); // 假设全局变量名与文件名一致 echarts.registerMap(provinceName, window[provinceName]); return provinceName; }逻辑说明这里用 Promise 包裹动态加载script的过程确保 JS 文件里的全局变量已经挂载到window上之后再调用registerMap。如果不等待 onload 就直接注册会拿到 undefined 导致注册失败。参数说明window[provinceName]利用了 JS 全局变量的特性变量名就是文件名拼音所以可以用字符串取到。如果你的压缩包里变量名与文件名不一致需要先解压确认这是这种方案的脆弱点。工程化项目建议直接用 fetch 加载 JSON 原文件不走全局变量这条路。3.3 对接真实业务数据的映射逻辑地图显示出来只是第一步业务场景里你手上通常有一份“省份-数值”的数据需要把它映射到地图的颜色深浅上。ECharts 的series.data里每一项要有一个name字段对应 GeoJSON 的properties.name这是关联的关键。// 模拟的业务数据各省份销售额 var salesData [ { name: 北京市, value: 12800 }, { name: 广东省, value: 23500 }, { name: 浙江省, value: 10800 }, { name: 江苏省, value: 19600 } ]; myChart.setOption({ tooltip: { trigger: item }, visualMap: { min: 0, max: 30000, left: 20, bottom: 20, text: [高, 低], inRange: { color: [#e0f3f8, #abd9e9, #74add1, #4575b4, #313695] } }, series: [{ type: map, map: china, data: salesData }] });逻辑说明visualMap组件负责把数据值映射成视觉元素最常见的是颜色。它有一个min和max范围如果业务数据的最小值不是 0需要手动调整否则颜色区分度会很低。比如你的数据在 5000-20000 之间波动而 max 设置成了 100000那所有省份看起来都是浅色等于白做。参数说明inRange.color是一个颜色数组ECharts 会在数据值从小到大的过程中线性插值。一般选 4-6 个渐变色即可过多会让图例分界不清晰。tooltip.trigger: item这个配置让鼠标悬停到某个省份时展示该省份的名称和数值请确保 tooltip 里面的formatter不被覆盖否则会出现悬停无数据的情况。业务数据对接里还有一个高频问题数据里的省名是简称比如“北京”“广东”而 GeoJSON 里是“北京市”“广东省”匹配不上时地图会显示为空白区域且不报任何错误。这种问题排查成本极高我的习惯是在配置之前先做一次数据对齐清洗把简称统一成全称。var shortNameMap { 北京: 北京市, 广东: 广东省, 浙江: 浙江省 }; salesData salesData.map(function(item) { if (shortNameMap[item.name]) { item.name shortNameMap[item.name]; } return item; });如果你手上的数据是带 adcode 的如 110000GeoJSON 的 properties.adcode 也能对得上。用 adcode 匹配是更稳妥的做法因为省份名称的高频变化简称、带不带省市后缀都会被绕开。4. 行政层级联动省—市—区县逐级下钻的实现方案4.1 下钻的层级表结构与数据准备做全国地图往往只是起点真正复杂的是省级页面点击某个省份下钻到该省的城市地图。城市地图需要用到province/xxx.js对应的市级地理数据而每一个省包里的 features 结构其实和全国地图一致只是粒度更细。下钻的第一步是先准备好层级数据关系。我一般会用一个常量表来维护上下级关系var provinceCityMap { 北京市: { level: city, path: city/beijing/beijing.js, mapName: beijing }, 浙江省: { level: city, path: province/zhejiang.js, mapName: zhejiang } };这里有一个需要注意的判断逻辑北京、上海、天津、重庆四个直辖市省的下一级直接是市辖区而浙江省这类省份下一级是杭州、宁波等地级市。如果你用 ECharts 加载了province/zhejiang.js地图上显示的是以地级市为边界的图谱直接就跳过了县域粒度。对于省份下钻常见做法是在省份页面配置regions选项把每个地级市的数据值映射到底图上。// 假设 zhejiang.js 已被注册为 zhejiang myChart.setOption({ series: [{ type: map, map: zhejiang, data: [ { name: 杭州市, value: 120 }, { name: 宁波市, value: 80 }, { name: 温州市, value: 95 } ] }] });4.2 click 事件与地图切换的常见做法下钻交互的核心是监听地图的点击事件。ECharts 的click事件会返回一个params对象里面有name字段表示点击区域的名称。拿到名称后去层级关系表里查对应数据再切换地图。myChart.on(click, function(params) { var name params.name; var target provinceCityMap[name]; if (!target) { return; // 没有对应下级地图直接返回 } // 动态加载目标地图数据 loadScript(target.path).then(function() { echarts.registerMap(target.mapName, window[target.mapName]); myChart.setOption({ series: [{ type: map, map: target.mapName, data: getCityData(name) // 根据省份名取该省的城市数据 }] }); }); });逻辑说明loadScript是第 3 章封装过的动态加载函数。每次点击时先判断有没有下级地图如果点击的是省级地图里的地级市且该省还没有被注册过就先加载再注册最后更新series.map字段。参数说明切换地图时setOption需要传入完整的 series 配置否则之前的data会残留。更干净的写法是调用myChart.clear()后重新setOption但 clear 会清空所有状态包括 visualMap 和 tooltip所以并不总是最优选择。我倾向于保留 tooltip 和 visualMap只更新 series这样能减少无明显感知的闪烁。还有一个使用习惯上的注意点地图切换后上一次的 roam 缩放状态会被保留。如果用户之前把地图缩放到了某个比例尺切换新地图时还停留在那个 zoom 级别视觉上会很突兀。所以我一般会在切换时重置一下视图myChart.dispatchAction({ type: restore });restore动作会把图表恢复到初始配置状态包括 roam 的重置。在用户下钻较深之后点击“返回上一级”时这个动作尤其重要否则视图停留在上次的子级缩放比例视觉上会很突兀。5. 避坑指南地图不显示、区域空白、边界漂移等 5 个高频问题排查5.1 现象一注册完地图后页面空白控制台报 “Map china not exists”这是初学时最常遇到的情况。registerMap确实调用了但页面仍然显示空白报错提示地图不存在。原因最常见的情况是 GeoJSON 的加载是异步的而registerMap的执行时机早于数据加载完成。比如 fetch 的 Promise 还未 resolve你已经调用了registerMap拿到的 data 为 undefined。另一种情况是series.map的值与registerMap的第一个参数不一致。解决把registerMap放到数据加载完成的回调里执行并确认map字段与注册名完全一致。我习惯在注册前加一行调试代码fetch(/map/china.json) .then(function(res) { return res.json(); }) .then(function(geoJson) { console.log(geoJson.type); // 确认是 FeatureCollection echarts.registerMap(china, geoJson); });如果console.log打印出来是undefined或者Object就说明数据结构和预期不符。如果加载出来是字符串还要JSON.parse后再传入。5.2 现象二地图能显示但个别区域颜色异常或完全空白这种问题最让人上火的是不报错但地图上就是有一块区域没有填充颜色也不响应点击。原因多半是数据匹配问题。业务数据里的name与 GeoJSON 里properties.name不一致。ECharts 在匹配不到数据时该区域会用默认的浅色显示但视觉上和“有数据没匹配上”看起来很像。解决先打印 GeoJSON 里所有区域的 name再和你业务数据里的 name 逐一比对注意空格、全半角括号。最稳妥的做法是直接用 adcode 匹配// 用 adcode 做 key从业务数据中取值 function matchByAdcode(geoJson, dataList) { var adcodeMap {}; dataList.forEach(function(item) { adcodeMap[item.adcode] item.value; }); return geoJson.features.map(function(feature) { var adcode feature.properties.adcode; return { name: feature.properties.name, value: adcodeMap[adcode] || 0 }; }); }这里说明一下series.data里每项可以省略name直接传{ value: xxx }吗不可以省略地图内部始终通过名称来关联地理区域没有 name 是没有办法对应上某个 Feature 的。如果连 name 都不想维护就得保证 GeoJSON 里的 adcode 存在且唯一然后按上面的方式构造数据。5.3 现象三地图边界与底图不符坐标偏移或形状严重变形这通常出现在叠加第三方底图比如天地图、高德地图时ECharts 的地图边界和底图边界差出好几个像素或者完全错位。原因坐标系不一致。ECharts 的geo组件默认使用 WGS84 经纬度而很多地图服务使用 GCJ-02 加密坐标。另一种情况是 GeoJSON 本身用了投影坐标比如 EPSG:3857经纬度数组的值非常巨大x 值有上百万而不是正常的 110-120 范围。解决先判断数据是经纬度还是投影坐标。如果coordinates里的数字在 -180 到 180 之间一般是经纬度如果出现百万级别的数值就是投影坐标需要先做坐标反算。GCJ-02 偏移问题在这里不做主张因为涉及不同数据源的配准策略单靠配置项解决会比较依赖项目场景。如果是纯粹使用 ECharts 地图组件而不叠加任何底图这种情况通常不会出现可先排除这个方向。5.4 现象四地图加载正常但缩放后精度不够边界呈锯齿状大屏分辨率很高地图放大之后边界不够平滑看起来像锯齿。原因china.js 里的 GeoJSON 本身是简化过的数据。为了控制文件体积发布者在制作时把坐标点做了抽稀处理放大后就看到折线。解决看这个压缩包定位是“够用就好”还是“精细展示”。如果只是看省份色块简化数据完全没问题如果要展示精确的省界、市界建议换高精度的 GeoJSON 源或者自己用 ArcGIS/GeoGad 对原始 shp 文件做简化后导出。我没有在压缩包里看到自带高精版本所以这点要知道边界。加载高精度数据的同时也可以通过配置项提升渲染效果series: [{ type: map, map: china, itemStyle: { borderColor: #999, borderWidth: 0.8 }, emphasis: { itemStyle: { areaColor: #ffd666 } } }]borderWidth不能设太粗大屏上 1px 已经是极限过宽的描边会让边界糊成一团。emphasis只控制高亮样式不影响数据映射。5.5 现象五动态切换地图后上一次的 visualMap 失效或颜色范围不对多级下钻时市级地图的数据取值范围往往和省级地图差异很大。比如省级数据最大是 30000市级最大只有 3000。如果 visualMap 的min和max保持不变市级地图的所有区域会呈现同样的深色等于没有可视化。原因visualMap.min和max是静态配置的切换地图时没有随数据范围动态更新。解决在切换地图时一并重新计算数据范围并设置 visualMap。如果不想手动算还可以设置visualMap为calculable: true让用户自己去拖动范围功能上更人性化。function updateMap(mapName, data) { var values data.map(function(item) { return item.value; }); var min Math.min.apply(null, values); var max Math.max.apply(null, values); myChart.setOption({ visualMap: { min: min, max: max }, series: [{ type: map, map: mapName, data: data }] }); }6. 进阶从静态注册到按需加载——封装一个简易地图数据管理器地图资源包的价值在于一次接入、长期复用。但如果每次接入项目都复制粘贴注册逻辑代码会越来越散加载策略也难统一。我习惯在项目里封装一个 MapManager把“加载脚本 / 注册地图 / 配置数据”统一收敛在一个模块里。// MapManager.js const MapManager (function() { const registeredMaps {}; const loadScriptCache {}; function loadScript(url) { if (loadScriptCache[url]) { return loadScriptCache[url]; } const promise new Promise(function(resolve, reject) { const script document.createElement(script); script.src url; script.onload resolve; script.onerror reject; document.head.appendChild(script); }); loadScriptCache[url] promise; return promise; } return { registerMap: function(mapName, url, variableName) { if (registeredMaps[mapName]) { return Promise.resolve(); } return loadScript(url).then(function() { echarts.registerMap(mapName, window[variableName]); registeredMaps[mapName] true; }); }, isRegistered: function(mapName) { return !!registeredMaps[mapName]; } }; })();使用的时候只需要一行代码MapManager.registerMap(beijing, ./province/beijing.js, beijing) .then(function() { renderMap(beijing, getBeijingCityData()); });这个封装解决了两件事一是防止同一个省份的 JS 被重复加载二是把注册逻辑集中在一个地方出现问题时只需排查这一个模块。注意variableName必须手动传入因为不能保证压缩包里每个文件的全局变量名都和文件名一致手动传参更可靠。在数据安全方面如果你是在内网环境部署大屏本地的 GeoJSON 文件不会通过公网传输风险相对可控但仍建议在代码里做一层数据范围校验避免地图上某些区域因为数据缺失被自动填充成默认色从而掩盖了数据质量问题。从那以后我每次接 ECharts 地图项目都会强制走一遍“验证 GeoJSON 结构 → 确认 name/adcode → 注册 → 数据映射 → 切换测试”的流程其中被坑得最多的就是 name 匹配和坐标系这两个方向。这套流程花不了几分钟但能省掉大半的排查时间希望帮到你。本文还有配套的精品资源点击获取