ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

eCharts 344县区地图资源:GeoJSON注册与两种加载方式实战

eCharts 344县区地图资源:GeoJSON注册与两种加载方式实战

简介:这是一份面向 Web 前端开发者与数据分析师的 eCharts 地图资源包,目标是在中国县级行政区划层面实现可视化展示与交互操作。资源提供 344 个县区的地图数据,并围绕两种常见集成方式给出可运行的 demo:其一是直接引用内置预置地图,适合快速搭建;其二是通过外部 JSON 文件加载自定义县区边界信息,适合需要定制行政区划或更新边界的项目,包内 JSON 已整理为可直接配套 ECharts 使用的格式,省去手动拼接行政区划边界坐标的步骤。两种方式均覆盖 ECharts 地图组件从初始化、数据绑定、渲染到提示框与区域高亮等交互配置的完整过程,示例中还包含 344 个县区与中国基础地图的对应关系,便于核对区域编码与展示层级,也可直接在此基础上扩展缩放、拖拽等地图交互。压缩包共 391 个文件,以 380 个 JSON 地图数据文件为主体,另有 6 个 JS 脚本负责加载地图与构建图表、2 个 HTML 页面作为示例入口、1 份 docx 文档说明常见的地图数据加载问题,整体仅有 2.82MB,轻量方便。资源目前已有 1552 人学习/下载,适合需要快速上手 eCharts 县区地图的入门与中级开发者,也可作为区域经济、人口统计、疫情分布等场景的可视化参考。

1. 拿到 344 个县区的 eCharts 地图资源,先别急着 setOption

当初要在一张大屏里展示某省 344 个县区的业务指标,我写好 setOption 后刷新页面,结果地图区域一片空白,只剩网格底图和缩放按钮。排查了两小时,最后发现是 GeoJSON 里properties.name和业务数据的 name 对不上——这类问题在 eCharts 地图可视化里太常见了。这份资源解决的正是这个麻烦:它包含 344 个县区的边界数据,并配了两种可运行 demo,一种是 script 标签静态引入,一种是 fetch 动态加载后注册渲染。适合做区县数据大屏、地图下钻和离线展示的前端开发者,也适合想搞懂 registerMap 工作原理的初学者。拿到手后,我把两个 demo 都跑了一遍,又拆了数据结构和源码,下面把运行步骤、目录说明和踩过的坑整理出来。

2. 资源结构拆解:一份 GeoJSON 和两条渲染路径

解压这份资源,会看到一个典型的「数据 + 两个 demo」布局。最值钱的部分是map-data/county344.json,它是 344 个县区边界的标准 GeoJSON 文件,整个 eCharts 地图的所有区域轮廓都来自它。页面上的每一条边界线、每一次缩放看到的细节,都是从这份坐标数据里算出来的。

2.1 目录里每个文件的职责

路径作用
map-data/county344.json344 个县区边界的 GeoJSON 数据,FeatureCollection格式
map-data/county344.js把同一份 GeoJSON 封装成全局变量的脚本,供静态 demo 用 script 标签引入
demo1-static/index.html方式一完整页面:静态引入 GeoJSON 后渲染
demo2-dynamic/index.html方式二完整页面:fetch 请求 JSON 后注册渲染
data/biz-data.json示例业务数据,演示 name 和 value 如何与地图关联
README.md运行说明、字段说明和常见问题

map-data下有两个文件,内容其实都是同一份边界数据,只是承载形式不同。county344.json是纯粹的 JSON,方便 fetch 和接口中转;county344.js里写的是window.COUNTY344_GEO = {...},把数据挂到全局变量上,因为 script 标签没法直接消费一个.json文件,只能用 JS 文件的方式注入。后面跑 demo1 时你会看到,页面先加载这个 JS,再从全局变量里取数据。

这里要先建立两个概念:地图边界数据是「原料」,业务数据是「配料」。344 个县区的区域轮廓由原料决定,每个区域显示什么颜色、tooltip 出什么内容,由配料决定。两份数据靠县区名字段撮合,名字对不上,渲染出来就是白屏或漏区域。

2.2 registerMap 的工作原理

eCharts 自定义地图的核心入口是一行代码:

echarts.registerMap('county344', geoJson);

这行代码做的事可以拆成三步:解析 GeoJSON 里的features数组,把每个 Feature 的 geometry 坐标换算成 canvas 绘制用的路径,再以county344这个 mapName 为 key 存入内部地图存储。之后无论geo组件还是series里的map类型,只要引用同一个 mapName,就能拿到这份边界数据。

GeoJSON 的关键结构长这样:

{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "name": "示例县A", "adcode": 110101 }, "geometry": { "type": "MultiPolygon", "coordinates": [[[[10.1, 10.2], [10.3, 10.2], [10.3, 10.4]]]] } } ] }

注意看properties里的name字段,它是渲染时匹配业务数据的唯一凭证。上面的坐标是我编的占位值,真实文件里是 344 个 Feature,每个县区一组经纬度闭合环。adcode一般是区划代码,做下钻、联动或请求下级数据时会用到。如果你拿到的 GeoJSON 里没有 adcode 也没关系,只做展示的话有 name 就够。

2.3 静态引入和动态加载怎么选

对比项静态引入(demo1)动态加载(demo2)
数据获取方式script 标签同步加载 JS,数据挂全局变量fetch 异步请求 JSON 文件
首屏行为HTML 加载时数据已经就位多一次网络请求,可先显示 loading
数据更新改 JS 文件后刷新页面重新 fetch,配合接口可以随时换
适用场景内网离线、数据基本不变数据频繁更新、需要对接后端

如果你在内网离线环境部署,或者边界数据一年半载不变,静态引入最省事,少一次请求少一个出错点。如果业务数据随时在变,甚至要按条件切换显示区域,就用动态加载。两份 demo 的渲染链路完全一致,区别只在「数据什么时候进入 registerMap」。

我一般会先用 demo1 验证地图能渲染、数据能对上,再根据实际部署环境决定要不要改成 fetch 方式。这个顺序能帮你把「地图问题」和「接口问题」分开排查。

3. Demo 一:静态引入,把 344 个县区边界直接挂到页面

方式一的核心思路是用 script 标签把county344.js引进来,GeoJSON 数据作为一个全局变量存在,页面加载完成就能直接注册渲染。适合离线演示、内网部署和快速验证数据。

3.1 完整可运行的 index.html

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>eCharts 344县区地图 - 静态引入</title> <!-- 1. 引入 eCharts 核心库 --> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> <!-- 2. 引入本地边界数据脚本,内部挂到 window.COUNTY344_GEO --> <script src="../map-data/county344.js"></script> </head> <body> <!-- 3. 地图容器必须有宽度和高度 --> <div id="map-container" style="width: 1000px; height: 700px;"></div> <script> // 4. 从全局变量取出 GeoJSON,注册为名为 county344 的地图 const geoJson = window.COUNTY344_GEO; echarts.registerMap('county344', geoJson); // 5. 初始化图表实例 const chart = echarts.init(document.getElementById('map-container')); // 6. 业务数据,name 必须与 GeoJSON 的 properties.name 一致 const bizData = [ { name: '示例县A', value: 231 }, { name: '示例县B', value: 87 } // 真实场景这里补齐 344 条 ]; // 7. 配置项 const option = { tooltip: { trigger: 'item', formatter: '{b}: {c}' }, visualMap: { min: 0, max: 300, left: 20, bottom: 20, inRange: { color: ['#e0f3f8', '#abd9e9', '#2c7bb6'] } }, geo: { map: 'county344', roam: true, itemStyle: { areaColor: '#e0e0e0', borderColor: '#ffffff', borderWidth: 0.5 }, emphasis: { itemStyle: { areaColor: '#ffd666' }, label: { show: true } } }, series: [{ type: 'map', map: 'county344', geoIndex: 0, data: bizData }] }; chart.setOption(option); // 8. 窗口尺寸变化时自适应 window.addEventListener('resize', function () { chart.resize(); }); </script> </body> </html>

这段代码里值得注意的点有几个。geo组件负责地图的通用样式、缩放和拖拽,series里的 map 类型负责绑定业务数据,两者通过geoIndex: 0建立关联。业务数据在 series.data 里,而区域样式在 geo 里,拆开配的好处是后续只改 data 不用动样式。

3.2 从全局变量到地图渲染的完整链路

执行顺序是这样的:浏览器先加载 eCharts 核心库,再加载 county344.js,此时window.COUNTY344_GEO已经就位。然后 registerMap 把 344 个县区的坐标解析进内部存储,等 eCharts 初始化容器并 setOption 时,geo 组件按 mapName 找到边界数据开始绘制,series 再按 name 字段把 value 逐个映射到区域上。

如果这一步没出图,优先检查两处:容器 div 是否有明确宽度高度,以及 registerMap 是否在 setOption 之前执行。容器宽高缺失会导致 eCharts 渲染出零尺寸画布,这在第一次写地图时很容易忽略。

roam 参数决定用户能不能缩放拖拽,设为 true 后鼠标滚轮缩放和拖拽平移默认开启。visualMap会根据 value 值给区域着色,值小偏浅蓝,值大偏深蓝,视觉上能直接看出各县区指标差异。

3.3 业务数据如何对到 344 个县区上

不管你数据来自接口还是本地文件,最终都要转成[{ name: 'xxx', value: 123 }]的结构。最常见的做法是把接口返回的列表 map 一遍:

// 假设接口返回 [{ countyName: '示例县A', count: 231 }, ...] const rows = await fetch('./api/stats').then(res => res.json()); const bizData = rows.map(row => ({ name: row.countyName, value: row.count }));

这里最大的坑是 name 对不上。业务库里可能叫「示例县A区」,GeoJSON 里叫「示例县A」,哪怕多一个字,这个县区也不会渲染数据,表现为区域显示默认灰色、tooltip 没有内容。遇到这种情况,我一般先打印一遍 GeoJSON 里的全部 name:

console.log(geoJson.features.map(f => f.properties.name));

再和业务数据的 name 做 diff,就知道差在哪些县区了。也可以维护一个映射表,把业务名映射成地图名,代码层面解决字段不一致的问题。还有一类遗漏:某个县区没有业务值,如果你直接跳过它,这个区域会不显示数据。真正稳妥的做法是给所有 344 个县区都补一条{ name: '某县', value: 0 },保证每个区域都有对应的数据项。

4. Demo 二:动态加载,fetch 拿到 GeoJSON 再注册渲染

方式二把边界数据的加载从 script 标签换成了 fetch 请求。页面启动时先显示 loading,拿到 JSON 后再注册地图、填充数据。这个模式更接近真实生产环境——边界数据放在服务器上,业务数据从接口取,两者互不阻塞。

4.1 完整可运行的 index.html

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>eCharts 344县区地图 - 动态加载</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> </head> <body> <div id="map-container" style="width: 1000px; height: 700px;"></div> <script> async function initMap() { const container = document.getElementById('map-container'); const chart = echarts.init(container); // 1. 显示加载提示,防止用户面对空白页 chart.showLoading({ text: '地图数据加载中...' }); try { // 2. 请求边界数据,注意 file:// 协议会被浏览器拦截跨域 const res = await fetch('./data/county344.json'); if (!res.ok) { throw new Error('GeoJSON 请求失败,HTTP 状态码: ' + res.status); } const geoJson = await res.json(); // 3. 重复注册前先注销,避免同名地图残留旧边界数据 echarts.unregister('county344'); echarts.registerMap('county344', geoJson); chart.hideLoading(); // 4. 业务数据可以来自另一个接口 const bizData = [ { name: '示例县A', value: 231 }, { name: '示例县B', value: 87 } ]; // 5. 渲染配置,和方式一基本一致 chart.setOption({ tooltip: { trigger: 'item', formatter: '{b}: {c}' }, geo: { map: 'county344', roam: true, itemStyle: { areaColor: '#f5e6ca', borderColor: '#ffffff', borderWidth: 0.5 } }, series: [{ type: 'map', map: 'county344', geoIndex: 0, data: bizData }] }); } catch (err) { chart.hideLoading(); // 失败时把错误渲染到页面,方便定位是网络问题还是解析问题 container.innerHTML = '<p style="color: #d00; padding: 20px;">地图加载失败: ' + err.message + '</p>'; } } initMap(); </script> </body> </html>

这段代码比方式一多了一层异步逻辑。fetch先拿响应对象,res.ok检查 HTTP 状态,res.json()解析成 JS 对象,再进入 registerMap。一旦 fetch 失败、JSON 格式不对或接口返回 404,异常会被 catch 接住,错误信息直接显示在容器里,不用开控制台就能定位。

4.2 从 fetch 到 registerMap 的完整链路

执行顺序是:页面加载完成了,才发起 fetch 请求。此时 eCharts 实例已经创建,loading 动画先顶上。请求回来后先 unregister 掉旧的 county344 地图,再注册新的,最后 setOption。为什么强调 unregister?因为 registerMap 同名注册不会自动覆盖历史数据,同一个 mapName 注册两次可能导致边界新旧交错的奇怪效果,尤其是你切换了不同版本的 GeoJSON 时。

有一个很容易踩的坑:直接双击 index.html 用 file:// 协议打开,浏览器会拦截 fetch 请求,控制台报跨域错误。这不是代码的问题,是浏览器的安全机制。解决方法是起一个本地静态服务,这是我跑这类 demo 的标准操作:

cd 项目根目录 python3 -m http.server 8080

然后浏览器访问http://localhost:8080/demo2-dynamic/。没有 Python 的也可以用npx serve或任意静态服务器工具。只要是 http 协议打开,fetch 就能正常工作。

4.3 两种方式的运行时差异

动态加载比静态引入多了一次网络往返,但换来的是数据可更新、可缓存、可按需加载。边界数据如果很大,几百个县区的经纬度全部打进 HTML 会让首屏明显变慢;拆成独立 JSON 文件后,浏览器可以缓存它,后续刷新页面甚至不需要重新下载。

从代码维护角度看,动态加载把「地图数据」和「页面逻辑」完全解耦。你可以只替换 JSON 文件,得到一份新的边界范围;也可以把 fetch 改成 axios,url 指向后端接口,由服务端决定返回哪几个县的边界。两种模式的数据流不一样,但 registerMap + setOption 的核心链路完全一致。

提示:demo2 的 fetch 请求在 file:// 下会被跨域拦截,务必用本地静态服务打开,这一点是静态引入方式不需要关心的。

5. 常见问题和避坑记录:地图跑不起来的五个原因

第一次跑这两个 demo 时我翻车了好几次,每次都是玄学一样的怪现象,查到最后全是基础问题。下面按数据类问题和运行类问题分成两组,把排查思路写清楚。

5.1 数据映射类问题

现象一:地图渲染出来了,但所有区域都是默认灰色,tooltip 没有任何内容。原因几乎都是 series.data 里的 name 和 GeoJSON 的 properties.name 对不上,eCharts 匹配不到数据项,就把区域按无数据处理。解决方法是先打印geoJson.features.map(f => f.properties.name),和业务数据做一次全量 diff,再把不一致的字段用映射表修正。多一个空格、多一个「区」字都会匹配失败,别看它们是汉字就觉得不会错。

现象二:能出图但县区边界和真实地理位置明显错位,或者整块区域漂到海里。这类问题通常是坐标系不一致,GeoJSON 用的是 GCJ-02,而 eCharts 默认按 WGS-84 经纬度理解。解决方法是换用坐标系正确的数据源,或者用地图工具做坐标纠偏,千万别在渲染层强行偏移 X 像素,那只是视觉自欺欺人。验证坐标系最直观的办法是拿几个周边县区的相对位置和真实地图对照一下。

现象三:地图有轮廓线,但内部没有填充色,只有描边。这个现象容易误判成样式问题,其实根因是 series 的 map 类型没有正确挂到 geo 组件上。检查配置里 series 是否写了geoIndex: 0,如果没有,series 会自己尝试再画一层地图,和 geo 组件出现叠边或填充异常。或者你也可以不用 geo,直接把样式写在 series 的 itemStyle 里,两条路都能走通,别混用产生歧义。

5.2 运行与渲染类问题

现象四:页面卡顿明显,滚轮缩放像掉帧一样。344 个县区的路径加起来顶点数非常多,如果 GeoJSON 没有抽稀,渲染性能会很差。常见做法有两种:一是对 GeoJSON 做顶点抽稀,用 simplify 算法把边界精度降到缩放可接受的程度;二是打开 eCharts 的渐进渲染,让浏览器分帧绘制。我自己更倾向第一种,把数据量直接降下来,一劳永逸。另外要确认容器尺寸固定,resize 频繁触发也可能导致不必要的重绘。

现象五:本地双击 HTML 文件直接打开,demo2 的 fetch 请求报 CORS 错误,地图一直 loading。这是 file:// 协议下的浏览器安全限制,不是代码问题。解决方式是起本地静态服务,前面已经写了python3 -m http.server 8080,跑起来后用 http 访问。如果这个请求被部署到 Nginx 下,还要确认服务器给 JSON 文件返回了正确的 Content-Type,否则res.json()解析阶段可能报错。

6. 从 demo 到二次开发:点击下钻、热力着色和图层叠加

把两个 demo 跑通之后,最常见的改造方向有三个。第一个是点击下钻,把某个县区边界替换成它下辖的乡镇边界。实现思路是监听 chart 的 click 事件,用 params.name 拿到县区名,然后请求下一级的 GeoJSON,重新 registerMap 再 setOption:

chart.on('click', function (params) { if (!params.name) return; // 请求这个县区下辖乡镇的边界数据 fetch('./api/sub-region/' + encodeURIComponent(params.name)) .then(res => res.json()) .then(geoJson => { // 先换边界,再换业务数据 echarts.unregister('county344'); echarts.registerMap('county344', geoJson); chart.setOption({ series: [{ type: 'map', map: 'county344', geoIndex: 0, data: nextLevelData }] }); }); });

第二个是改成热力效果。你已经在 demo 里见过 visualMap 的配置了,只要把业务数据的 value 范围设好,visualMap 会自动按数值分档上色。如果想让颜色更明显,可以改 inRange 里的 color 数组,三段渐变比两段渐变过渡自然很多。

第三个是叠加散点图层。在 series 数组里加一个 scatter 类型,用每个县区的中心经纬度做坐标,就能把学校、医院、门店之类的点位叠到地图上。散点数据的经纬度可以从 GeoJSON 的 geometry 里算出来,用 turf 库的 center 方法最省事。

每次改完,我习惯先用一条 mock 数据验证再换真实数据。验证步骤是固定的三行:打印 geoJson.features.length 确认边界数量,打印 features[0].properties 确认 name 字段,再点击两个县区确认下钻逻辑没有串数据。从那以后,我每次拿到任何地图数据,都会先强制走一遍这三步,再开始写 setOption。这个习惯救了我很多次,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进