资讯详情

ECharts 中国地图实战:china.js 注册、数据绑定与避坑

📅 2026/10/26 17:45:53 | 华诺云谱 👁 阅读
ECharts 中国地图实战:china.js 注册、数据绑定与避坑
简介面向 ECharts 数据可视化的中国地图 JavaScript 文件压缩包主要服务于需要在网页中呈现中国及省级行政区划地理信息的开发者与前端学习者。它解决地图数据引入、区域绘制与配置的基础问题适合已有 ECharts 使用经验、希望快速补全中国地图素材的中级读者。包内共 35 个 js 文件整体约 841KB以省级行政区划脚本与中国地图主文件为主前者用于按省份加载地理边界后者便于在图表实例中直接注册地图名称减少自行搜集与格式转换的成本。已有 5705 人学习说明其在同类地图素材中具有一定参考价值。读者可据此完成中国地图底图接入、区域颜色与交互配置并为按省分组展示、点击区域查看信息等扩展提供数据基础目录按地区拆分检索和替换相对直观。1. china.js 不是插件是一部被写进 JS 的地图字典大屏项目第一次画中国地图控制台大概率会甩出一句Map china not exists. The GeoJSON of the map must be provided.。这不是 echarts 装错了而是从 ECharts 5 起地图数据不再跟着主包一起发。china.js 中国及各地 js 文件压缩包解决的正是这件事它把全国、省级、地市级的 GeoJSON 提前封装成可以直接script引入的 JS 文件脚本执行时自动完成echarts.registerMap()注册。适合做大屏可视化、区域统计看板、点击下钻到省市的运营报表如果只是想在折线图上标个点这包对你来说是用不上的。2. 拆开压缩包registerMap 之前先搞懂这三层拿到这包文件多数人第一反应是找china.js直接怼进页面能跑就算完。但地图这类资源最容易在“换个省”“换个构建工具”的时候翻车根因几乎都藏在文件结构里。先把数据层、注册层、渲染层分清楚后面调参数才知道改的是哪一层。2.1 从 GeoJSON 到系列图一次注册到底发生了什么压缩包里的每个 JS 文件剥掉外壳之后是一段标准 UMD 包装的代码核心数据仍然是 GeoJSON 的FeatureCollection。文件的职责只有一个把这个 GeoJSON 交给 ECharts 内部的 mapStore并给它起一个名字。名字起好之后series.type: map里的map: china才有东西可查。// china.js 的典型骨架数据本体做了省略 (function (root, factory) { if (typeof define function define.amd) { // AMDRequireJS 环境 define([exports, echarts], factory); } else if (typeof exports object typeof module ! undefined) { // CommonJSNode、webpack、vite 走这个分支 factory(exports, require(echarts)); } else { // 浏览器直引依赖 window.echarts 已存在 factory({}, root.echarts); } })(this, function (exports, echarts) { if (!echarts) { // 这里是最常见的静默失败点echarts 没加载函数直接 return console.warn(ECharts is not loaded, map data will not be registered.); return; } echarts.registerMap(china, { geoJSON: { type: FeatureCollection, features: [/* ... */] }, specialAreas: { /* 可选把局部区域平移到主图外侧时的偏移量 */ } }); });这段代码值得逐行看。第一个分支判断 AMD第二个判断 CommonJS第三个才是浏览器全局。很多人把文件丢进 HTML 之后地图不出来就是因为script srcecharts.min.js写在了china.js后面走到factory({}, root.echarts)时root.echarts还是undefined于是console.warn打完就 return注册压根没发生页面上自然只剩一句Map china not exists。第二个要注意的是registerMap的第二个参数。它既可以直接传 GeoJSON 对象也可以传{ geoJSON, specialAreas }这样的包装对象。压缩包里两种写法都有读代码的时候别被格式差异骗了判断依据是看有没有geoJSON这个 key。第三个细节是注册名。registerMap(china, ...)的第一个参数就是后续series.map要填的值两者必须一字不差。有些包里的省图注册名用的是拼音有些用的是中文全称没有统一标准这也正是下一节要讲目录规律的原因。2.2 目录层级与注册名怎么一眼看出该引哪个文件包解开之后一般能看到全国、省级、地市三级目录命名规律大同小异。全国图放根目录省级放一层子目录地市级再往下沉一层文件名用小写拼音注册名与文件名保持一致。这个约定不是强制的但绝大多数地图数据包都遵循它因为拼音做文件名不会遇到编码问题做 CDN 路径也安全。层级路径形态注册名典型用途单文件量级全国china.jschina全国总览、区域对比数百 KB省级province/拼音.js省份拼音单省大屏、省内排行几十 KB地市city/拼音.js地市拼音下钻到区县、城市级热力十几 KB单文件量级这一列不是精确值只是给你一个心理预期全国图因为要素多、坐标点多体积明显大于省图地市图反而最小。这直接影响首屏策略后面对应章节会展开。判断某个文件注册了什么名字最土但最可靠的办法是打开文件搜registerMap(看第一个字符串参数。批量确认就得靠脚本了手工翻几十个文件不现实。还有一点容易被忽略省级图的features里装的是地级行政区不是县级。所以你在省图上绑数据数据里的名字必须是地市名拿一堆区县名去绑省图结果就是整张图灰着一个色块都不亮。这个错我见过不止一次排查的时候第一件事就是打印features里的 name 列表。2.3 用 Node 脚本把整包扫一遍拿到注册名、要素数、字段手工翻文件只能确认一两个。整包几十上百个文件我需要一次性拿到三样东西每个文件注册的名字、要素数量、要素属性里有哪些字段。做法是把文件丢进 Node 的vm沙箱执行同时给echarts打一个桩拦截registerMap调用把参数记下来。// tools/check-maps.js —— 扫描地图包输出注册名与要素统计 const fs require(fs); const path require(path); const vm require(vm); const MAP_DIR path.resolve(__dirname, ../maps); const report new Map(); // 打桩拦截 registerMap不真正渲染只记录关键信息 const echartsStub { registerMap(name, payload) { const geoJSON payload payload.geoJSON ? payload.geoJSON : payload; const features (geoJSON geoJSON.features) || []; report.set(name, { count: features.length, // 取前三个名字做抽样方便肉眼比对命名风格 sample: features.slice(0, 3).map(f f.properties f.properties.name).filter(Boolean), // 收集属性字段名用于判断数据该按哪个 key 对齐 keys: [...new Set(features.flatMap(f Object.keys((f.properties) || {})))] }); } }; const sandbox { echarts: echartsStub, module: { exports: {} }, exports: {}, console, // 关键把 require(echarts) 也指到桩上防止文件走 CommonJS 分支时拿到别的实例 require: (id) (id echarts ? echartsStub : require(id)) }; sandbox.window sandbox; sandbox.self sandbox; sandbox.global sandbox; vm.createContext(sandbox); function walk(dir) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full path.join(dir, entry.name); if (entry.isDirectory()) { walk(full); continue; } if (!entry.name.endsWith(.js)) continue; try { vm.runInContext(fs.readFileSync(full, utf8), sandbox, { filename: full }); } catch (err) { console.error([FAIL] ${path.relative(MAP_DIR, full)} - ${err.message}); } } } walk(MAP_DIR); for (const [name, info] of report) { console.log([OK] ${name.padEnd(20)} features${String(info.count).padEnd(4)} keys${info.keys.join(,)}); console.log( sample: ${info.sample.join( / )}); } console.log(\n共注册 ${report.size} 张地图);运行方式和输出大致是这样node tools/check-maps.js[OK] china features34 keysname,cp,childNum sample: 北京 / 天津 / 河北 [OK] hebei features11 keysname,cp sample: 石家庄 / 唐山 / 秦皇岛 [FAIL] city/broken.js - Unexpected end of JSON input逻辑上有三个关键点。第一vm.createContext建的是一个隔离的全局环境文件里的this会指向沙箱对象UMD 的判断分支才能正常走。第二require必须重写否则文件走 CommonJS 分支时require(echarts)会去加载真实的 echarts那是一个全新实例registerMap记在它身上你的桩什么都收不到。第三exports和module两个都要给缺一个有些 UMD 头会走错分支。输出里的keys特别有用。如果某个文件只有name没有cp中心点坐标那它就没法做散点叠加或者标签偏移计算选图的时候要提前知道。sample用来确认命名风格看看是“河北”还是“河北省”直接决定了你后面要不要写nameMap。[FAIL]那一类不要急着删先看报错信息。Unexpected end of JSON input通常是文件下载不完整重新取一次就好如果是xxx is not defined多半是文件依赖了浏览器环境变量改一下沙箱注入即可。3. 跑通第一张图三种引入方式与最小配置数据摸清楚了接下来是让它出现在页面上。引入方式选错后面所有参数调试都是白费功夫所以这一步值得单独占一章。3.1 script 标签直引顺序就是生命线传统页面或者不需要构建的 Demo直接两个script搞定。顺序必须是先 echarts 后地图文件中间不要插defer或async否则执行时机不可控。!-- 顺序不能反地图文件执行时要用到全局 echarts -- script src./lib/echarts.min.js/script script src./maps/china.js/script script // 此时 china 已经注册完成可以直接初始化 const chart echarts.init(document.getElementById(map)); chart.setOption({ tooltip: { trigger: item }, series: [{ type: map, map: china, // 必须与 registerMap 的第一个参数完全一致 roam: true, // 允许缩放和拖拽 label: { show: false }, emphasis: { label: { show: true } } }] }); /scriptmap: china这个字符串就是全部的关键。写错了不会报错只会给你一张空白画布再附一行Map xxx not exists的警告。roam: true打开滚轮缩放和拖拽大屏上通常关掉因为鼠标滑过会误触。3.2 模块化项目小心两个 echarts 实例在 webpack、vite、rollup 这类构建工具里事情变得微妙。地图文件走的是factory(exports, require(echarts))分支也就是说它内部又require了一次 echarts。如果你的业务代码用的是 ESM 的import * as echarts from echarts而地图文件拿到的是 CJS 那一份打包器有可能给出两个不同的模块实例——地图注册在 A 实例上你在 B 实例上画图结果就是空白。// 推荐写法先拿到 echarts 实例再引入地图文件保证同一个实例 import * as echarts from echarts/core; import { MapChart } from echarts/charts; import { TooltipComponent, VisualMapComponent } from echarts/components; import { CanvasRenderer } from echarts/renderers; echarts.use([MapChart, TooltipComponent, VisualMapComponent, CanvasRenderer]); // 地图文件依赖全局 echarts按需引入模式下必须先挂到 window 上 window.echarts echarts; await import(./maps/china.js); // 从这一步开始registerMap 才真正作用在你的实例上 echarts.init(document.getElementById(map)).setOption({ series: [{ type: map, map: china }] });按需引入echarts/core是现在的主流做法包体积能砍掉一大半代价就是必须手动echarts.use()注册用到的图表和组件。地图这块最少要MapChart、TooltipComponent要配色就再加VisualMapComponent用 Canvas 渲染器加CanvasRenderer。少注册一个页面上就是一张白图控制台还不一定报错这种玄学问题排查起来最费时间。window.echarts echarts这一行看着脏但在按需引入模式下几乎是必须的——地图文件的 UMD 头写死了从root.echarts取它不认识你的模块作用域。如果团队不接受污染全局那就换方案把地图文件当纯数据用自己读 JSON 调registerMap这样最干净。3.3 省级图切换一个 map 名换掉整张图单省看板和全国看板往往在同一个页面里切换。做法不是销毁重建而是改series[0].map的值再setOption一次。前提是对应的省图已经注册过。// 预加载需要的省图避免切换时白屏 const provinceMap { hebei: () import(./maps/province/hebei.js), shandong: () import(./maps/province/shandong.js) }; async function switchRegion(key) { if (!provinceMap[key]) return; await provinceMap[key](); // 动态引入触发 registerMap chart.setOption({ series: [{ type: map, map: key, // 与文件的注册名保持一致 nameMap: {}, // 换图时清掉上一次的名字映射防止串台 data: [] // 数据也要跟着换否则会残留上一张图的色块 }] }, { replaceMerge: [series] }); // 关键整段替换不是合并 }replaceMerge: [series]这个参数很容易漏。默认的setOption是合并语义旧 series 上的data、nameMap、label配置会残留在新图上表现为“换了省份但颜色还是旧的”“某些区域莫名高亮”。加上replaceMerge之后整段 series 被替换状态才干净。nameMap: {}显式清空也是同一个道理。上一张图如果做过名字归一映射表会一直挂在 series 上新图的要素名对不上数据全灰。3.4 数据绑定先把名字对齐再谈配色地图上色依赖data数组里的name字段与 GeoJSON 要素的name属性一致。这一步对不上visualMap调什么颜色都没用。// 数据侧name 必须与地图要素的 name 逐字一致 const data [ { name: 石家庄, value: 320 }, { name: 唐山, value: 210 } ]; chart.setOption({ visualMap: { min: 0, max: 400, inRange: { color: [#e8f4ff, #1c6fd6] }, // 由浅到深 text: [高, 低], calculable: true }, series: [{ type: map, map: hebei, data, label: { show: true, fontSize: 10 }, itemStyle: { borderColor: #fff, borderWidth: 1 } }] });visualMap的min/max建议按数据的实际分布取不要死写 0 到 100。数据集中在 200 到 300 之间时写 0 到 100 会让所有区域挤在同一个色阶上看着像没生效。calculable: true会给出一个可拖动的滑块调试阶段打开很直观上线前按需关掉。4. 参数与坐标系让地图不变形、不错位能出图只是及格线。真到上线问题集中在三个地方形状被拉伸、区域点不中、颜色不生效。这一章就解决这三类。4.1 geo 与 series-map 的分工ECharts 里有两种画地图的方式geo组件和type: map的系列。新手最容易犯的错是两个都配结果同一张地图画了两遍点击事件的坐标还是错的。geo是坐标系组件负责画底图、管缩放拖拽不承载数据series-map自带地理坐标系能直接绑data上色。选哪个看需求只要地图底图 散点图叠加 → 用geo散点作为独立 series 通过coordinateSystem: geo挂上去。要按区域上色、做热力分布 → 用series-map数据直接挂在 series 上。两者都要用geo画底图再开一个series-map但设geoIndex复用坐标系别让它自己再画一层。后者是最容易踩的坑一旦series-map没指定geoIndex它会自己建一套坐标系两层地图叠在一起肉眼看不出来但点击和 tooltip 会随机命中其中一层。4.2 变形、缩放与视角控制地图被压扁或者拉长八成是aspectScale在作怪。这个参数控制 Y 方向相对 X 方向的比例默认 0.75作用是补偿经纬度直接映射到屏幕后的形变。chart.setOption({ geo: { map: china, roam: true, aspectScale: 0.75, // 默认值改大 Y 方向变高 zoom: 1.15, // 初始缩放 center: [104, 36], // 初始中心点经纬度 layoutCenter: [50%, 50%], // 在容器内按百分比定位 layoutSize: 95%, // 按容器尺寸百分比定大小 boundingCoords: [[73, 54], [135, 3]] // 限定可见经纬度范围左上、右下 } });layoutCenterlayoutSize和zoomcenter是两套定位思路混用会互相打架。前者按容器百分比算响应式友好适合大屏自适应后者按地理坐标算适合精确控制视野范围。我一般优先用layoutCenter/layoutSize容器宽高变化时不用重算。boundingCoords值得单独说。它接受两个经纬度点把地图裁剪到这两点定义的矩形范围里。有些场景只想看某一片区域用它比调zoom精确得多因为不用反复试数值。4.3 名字匹配nameMap 与外省数据归一业务数据里的行政区名往往带全称后缀地图要素里是简称这是数据全灰的头号原因。nameMap就是干这个的。// 后缀按长度从长到短排列否则长后缀会被短后缀先吃掉 const SUFFIX /(维吾尔自治区|壮族自治区|回族自治区|特别行政区|自治区|省|市|县|区)$/; function toShortName(fullName) { return String(fullName || ).replace(SUFFIX, ); } // 用数据侧的名字反查地图要素名生成 nameMap const geoFeatures [/* 从 GeoJSON 读到的要素数组 */]; const featureNames new Set(geoFeatures.map(f f.properties.name)); const nameMap {}; for (const item of rawData) { if (featureNames.has(item.name)) continue; // 已经对得上跳过 const short toShortName(item.name); if (featureNames.has(short)) nameMap[item.name] short; } chart.setOption({ series: [{ type: map, map: china, nameMap }] });正则里的顺序是有讲究的。如果写成(省|市|自治区|...)遇到“某某壮族自治区”时“自治区”会先匹配到尾部的三个字剩下“某某壮族”这种半截名字怎么也对不上。把长后缀放前面正则才会优先匹配完整的那一段。除了nameMap还有nameProperty这个参数用来指定从 GeoJSON 的哪个属性字段取名字默认是name。有些数据包把名字放在properties.NAME或者properties.fullname里这时候改nameProperty比写一堆nameMap省事。判断方法还是第 2 章那个脚本看输出里的keys。4.4 视觉映射与高亮让颜色真的按数据走visualMap不生效常见原因有三个没注册VisualMapComponent、min/max范围不对、seriesIndex指错。前两个第 3 章提过第三个容易被忽略——页面上有多个 series 时visualMap默认作用于全部数值系列散点图的数值也会被拉进来算色阶结果地图颜色被稀释得很淡。显式写seriesIndex: 0就能锁死。chart.setOption({ visualMap: { type: continuous, seriesIndex: 0, // 只作用于第一个系列即地图 min: 0, max: 400, inRange: { color: [#e8f4ff, #1c6fd6] }, outOfRange: { color: #f2f2f2 } // 没有数据的区域统一灰 }, series: [{ type: map, map: hebei, selectedMode: single, // 单选高亮多选用 multiple itemStyle: { areaColor: #f2f2f2, borderColor: #d0d0d0 }, emphasis: { label: { show: true, color: #fff }, itemStyle: { areaColor: #ff7a45 } } }] });itemStyle.areaColor是兜底色没有数据的区域走这个颜色配合outOfRange一起用视觉上才干净。emphasis里把label打开悬停时才显示名字平时隐藏密集地图上不至于糊成一片。5. 避坑与排查地图不显示的五个现场记录下面这五条都是从实际项目里攒下来的按“现象 → 原因 → 解决”记遇到问题时对号入座比漫无目的地翻文档快。5.1 现象控制台报Map china not exists原因三种可能。地图文件没加载网络 404 或者路径写错加载了但 echarts 还没就绪UMD 走了root.echarts为空的兜底分支模块化项目里地图注册到了另一个 echarts 实例上。解决先在浏览器控制台敲echarts.getMap(china)返回undefined就是没注册成功。再查 Network 面板确认文件 200查 script 顺序确认 echarts 在前。模块化项目按 3.2 节的方式统一实例把window.echarts挂上再动态引入。5.2 现象地图能出来但被压扁成一条或者撑满容器变形原因容器本身没有明确宽高或者aspectScale被改过再或者同时设置了layoutSize和zoom导致两套定位逻辑打架。解决先给容器一个确定的heightecharts.init之后调用chart.resize()并监听窗口变化。然后检查aspectScale有没有被手改成 1 或其它值改回 0.75 试试。定位参数只保留一套要么layoutCenterlayoutSize要么zoomcenter。5.3 现象本地开发正常打包上线后地图一片空白原因构建工具的 tree-shaking 或者代码分割把地图文件摇掉了或者按需引入时漏注册了MapChart。解决确认地图文件是动态import()而不是被判定成无副作用的模块。如果用了echarts/core检查echarts.use()里有没有MapChart。快速验证的办法是在构建产物里搜注册名比如搜china这个字符串搜不到就是被打包器丢了。5.4 现象地图有形状但所有区域都是同一个灰色原因数据里的name与要素名对不上或者visualMap的min/max把数据全压在色阶一端。解决先打印series.data里的名字和echarts.getMap(china).geoJSON.features里的名字做交集交集为空就是命名不匹配按 4.3 节生成nameMap。交集不为空但颜色还淡就把min/max按实际数据的最大值最小值重新设。5.5 现象首屏加载慢地图文件拖累了整页原因全国图体积不小所有省图一次性全引首屏白屏时间明显拉长。解决全国图用link relpreload提前拉省图改成点击下钻时动态import()。再进一步把地图文件从主包里拆出去单独构建利用浏览器缓存。第 6 章会讲怎么按需裁剪。6. 进阶把地图包变成可维护的资产整套文件丢进项目只是开始。地图数据的麻烦在于它几乎不会变但一旦要改往往是在上线前一天。我习惯在做完第一次接入后花半小时补三样东西。第一样是裁剪。项目只用到几个省没必要把整包都打进去。写个脚本按注册名筛选把需要的文件合并成一个 bundle体积能降一个数量级。// tools/build-bundle.js —— 只打包用到的地图 const fs require(fs); const KEEP [china, hebei, shandong]; // 按实际业务需要的名单填 const chunks KEEP.map(name { const file ./maps/${name}.js; if (!fs.existsSync(file)) throw new Error(缺失地图文件: ${name}); return fs.readFileSync(file, utf8); }); // 加一层去重包装避免重复加载同一个 UMD 头 fs.writeFileSync(./dist/maps.bundle.js, chunks.join(\n)); console.log(已打包 ${KEEP.length} 张地图);KEEP名单建议和业务代码里的map取值一一对应别凭记忆手写。加一个存在性检查缺文件直接抛错比等到运行时白屏好得多。第二样是指纹表。用第 2 章的脚本跑一遍把注册名、要素数、属性字段名、文件哈希存成一份 JSON 提交进仓库。以后谁替换了地图文件diff 里一眼就能看出来要素数从 34 变成 33——少了一个区域这种事在代码里完全看不出来但会直接导致数据对不上。第三样是校验进 CI。把扫描脚本挂到持续集成里任何地图文件解析失败或者注册名为空直接阻断合并。地图这种“跑起来不报错、显示不对才出事”的资源靠人眼回归测试太不可靠。说个我自己后悔药吃过的场景有次替换了省图文件features里少了一个地市页面照常渲染只是那个地区的颜色一直挂在默认灰上看板上看不出来运营发现数据对不上才回头查前后折腾了大半天。从那以后我每次替换地图文件都强制先跑一遍扫描脚本把新旧两份指纹表比一遍features数量、名字列表、属性字段三项全对上才敢提交。这个包的价值不在于文件本身有多大而在于它把全国的行政区数据都按统一的注册约定准备好了省掉了自己去拼 GeoJSON 的功夫。拿到手之后建议先跑第 2 章那个脚本把家底摸清再按第 3 章跑通一张图剩下的参数按第 4 章对着改。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑