☰
eCharts县域地图实战:344个县区GeoJSON与两种加载方案
2026/10/19 2:57:08 网站建设 项目流程

简介:面向需要在中国县级行政区划层面做数据可视化的前端开发者,这份 eCharts 地图资源专门整理了 344 个县区的完整地图数据,并附带两种实现方式的示例 Demo,直接解决县区级地理数据获取难、配置繁琐的常见问题。这两种方式分别对应预置地图数据集的快速引用,以及通过自定义 JSON 边界数据完成地图集成,覆盖人口统计、经济指标、地域分布等常用可视化场景,也方便初学者对比不同写法的差异。压缩包共 391 个文件,以 380 个 JSON 地理数据文件为主,配合少量 JS 脚本、HTML 示例页面、project 工程文件以及 1 份 docx 说明文档,整体体积仅 2.82MB,结构轻量,下载后即可直接运行和二次修改。目前已有 1552 人学习下载,适合具备基本 HTML/JavaScript 基础、希望快速上手县区级地图的前端开发者和数据分析人员。参照示例可以掌握引入 ECharts 库、准备地图数据、初始化容器、配置 option、绑定悬浮与点击交互的完整链路,还可根据项目需要选择最合适的数据接入方式,同时文档中也对地图不能下载或加载异常等问题给出了排查思路,能有效缩短开发和调试时间。

1. eCharts地图344个县区:一套能直接用的中国县域GeoJSON与两种加载方案

做数据可视化的人,十有八九都被“地图数据”卡过脖子。省市级的地图网上随便一搜就是一堆,但一落到县区级,要么数据残缺,要么坐标系混乱,要么素材老旧到连地名都对不上。这份eCharts地图资源,一次性整理了344个县区的GeoJSON数据,并附带了两种引入方式的demo,解决了从“拿到数据”到“渲染上屏”之间最磨人的那段路。它不是教程页面,是拆开就能用的代码资料包,适合正在做县域经济分析、人口热力分布或者区划数据看板的前端开发者。我当初拿到这套数据时,第一反应是“终于不用再满网找边界文件了”,但真正把它跑起来之后发现,更值钱的是demo里对边界数据异常和加载时序的处理思路。

2. 县区级地图的底层原理:为什么GeoJSON比常规写法更适合县域渲染

2.1 弄清楚GeoJSON的结构与坐标系,是第一步不是废话

我们直接看一份从这套资源里抽出来的县区边界数据,长这样:

{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "adcode": 110101, "name": "东城区", "center": [116.418, 39.917] }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [116.388, 39.941], [116.392, 39.935], [116.404, 39.935] ] ] ] } } ] }

这段数据结构里有两个关键点。第一是properties,它承载着每个县区的行政区划代码(adcode)、名称和中心点坐标,我们的 tooltip、label、事件绑定都要从这里取数据;第二是geometry,MultiPolygon意味着这个区域可能由多个不相连的多边形组成,典型的例子是那些下辖岛屿的区县。eCharts 的registerMap方法就是读取这套结构生成地图实例。

关于坐标系,这套数据用的是 GCJ-02 坐标系,也就是俗称的国测局坐标。如果你手里的业务数据是从 GPS 设备或者原始的 WGS-84 坐标采集来的,那么直接塞进地图里会出现几百米的偏移。常见做法是在数据入库前做一次坐标转换,把 WGS-84 转成 GCJ-02,或者在拿到经纬度之后先跑一遍转换函数再交给 eCharts 渲染。

注意:判断一份 GeoJSON 能不能直接用,最简单的办法是拿center字段里的坐标画一个点,再拿对应区划的边界画个面,两者叠在一起如果吻合就没问题,偏移超过五百米就得检查坐标系了。

2.2 动态注册地图,是县区级渲染唯一靠谱的姿势

eCharts 渲染地图的流程不像折线图那样“丢数据进去就完事”。它必须先走registerMap,把 GeoJSON 注册成一个 echarts 能识别的名称,然后再用这个名称去初始化 option 里的map字段。这套资源里的 demo 也是这么做的:

// 引入eCharts核心 import * as echarts from 'echarts'; // 引入这份资源里提供的县区GeoJSON import countyData from './geo/344_counties.json'; // 关键:先把地理数据注册成名为'county_map'的地图 echarts.registerMap('county_map', countyData); // 拿到容器实例 const chartDom = document.getElementById('chart-container'); const myChart = echarts.init(chartDom); // 在option中引用已注册的名称 const option = { tooltip: { trigger: 'item', formatter: function (params) { return params.name + '<br/>' + (params.value || '无数据'); } }, series: [ { type: 'map', map: 'county_map', roam: true, label: { show: true, fontSize: 10 }, data: [ { name: '东城区', value: 86 }, { name: '西城区', value: 72 } ] } ] }; myChart.setOption(option);

这段逻辑有几个值得展开说明的细节。roam: true开启了缩放和平移,县区级地图在交互时经常要放大看某个具体区域,这个参数建议保持开启。label.show在边界密集的县区地图上要谨慎使用,因为县域面积小、名字又长,全量显示 label 很容易糊成一团,我一般会把fontSize压到 10px 甚至更低,或者在缩放级别达到一定阈值时才显示名字。data里的name字段必须和 GeoJSON 里properties.name完全一致,差一个空格就匹配不上,这是一个非常隐蔽的坑,后面避坑章节我会再详细说。

3. 两种demo的实战拆解:script标签直引与npm模块化

3.1 方案一:script标签直接加载,适合无需构建环境的临时看板

这套资源里第一种 demo 走的是最传统的路子,直接通过 script 标签把 eCharts 和 GeoJSON 引进来。这种方式的适用场景很明确:比如你做的是一个临时性的数据看板,或者公司的内网管理系统不方便上构建工具,又或者只是想在本地快速验证一下数据长什么样。它的核心代码结构如下:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>县区地图-直引方案</title> <style> #map { width: 100%; height: 600px; border: 1px solid #eee; } </style> </head> <body> <div id="map"></div> <!-- 先引eCharts核心库 --> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <!-- 再引本地GeoJSON文件 --> <script src="./geo/344_counties.json"></script> <script> // 注意:json文件被script引入后,会以js对象形式暴露出来 const countyData = window.countyData || JSON.parse( document.getElementById('county-data').textContent ); // 注册地图 echarts.registerMap('county_map', countyData); const chart = echarts.init(document.getElementById('map')); chart.setOption({ series: [ { type: 'map', map: 'county_map', label: { show: true }, data: [ { name: '海淀区', value: 128 }, { name: '朝阳区', value: 95 } ] } ] }); </script> </body> </html>

这里有个实操中很容易踩的细节:直接用<script src="./geo/344_counties.json">这种方式引入 JSON 文件,不一定能通过window访问到数据,因为浏览器对纯 JSON 文件的 script 引入没有统一的处理规范。更稳的做法是用 fetch 去拉:

fetch('./geo/344_counties.json') .then(res => res.json()) .then(countyData => { echarts.registerMap('county_map', countyData); // 剩余渲染逻辑 });

fetch 方案的另一个好处是浏览器会异步加载,不会因为 GeoJSON 文件体积过大而阻塞页面首屏渲染。这套资源里的 344 个县区合并数据,文件大小通常在一百多KB到几百KB之间,在本地服务和内网环境下还好,如果部署到公网就必须考虑用 gzip 压缩传输,或者做数据裁剪。

3.2 方案二:npm模块化引入,适合Vue/React工程化项目

第二种 demo 面向的是正式的工程化项目,这也是我在实际业务中采用的方式。资源里给出了一个基于 Vite + Vue 3 的示例结构,核心代码长这样:

// 在Vue3组件中引入 import * as echarts from 'echarts'; import { onMounted, ref, shallowRef } from 'vue'; import countyGeo from '@/assets/geo/344_counties.json'; // 用shallowRef管理chart实例,避免Vue的响应式代理干扰echarts内部状态 const chartRef = ref(null); const chartInstance = shallowRef(null); onMounted(() => { // 注册地图 echarts.registerMap('county_map', countyGeo); chartInstance.value = echarts.init(chartRef.value); chartInstance.value.setOption({ tooltip: { trigger: 'item', formatter: (params) => { const val = params.value ?? 0; return `${params.name}:${val}`; } }, visualMap: { min: 0, max: 100, left: 'left', top: 'bottom', text: ['高', '低'], inRange: { color: ['#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695'] } }, series: [ { type: 'map', map: 'county_map', roam: true, scaleLimit: { min: 1, max: 5 }, itemStyle: { borderColor: '#ccc', borderWidth: 0.5 }, emphasis: { itemStyle: { areaColor: '#ffd666' } }, data: [ { name: '东城区', value: 86 }, { name: '朝阳区', value: 112 } ] } ] }); });

模块化方案和直引方案最大的区别不只是代码组织方式,更重要的是import机制让 GeoJSON 变成了打包过程中的一个静态资源模块。Vite 会把它做一次 JSON 解析并缓存,后续组件的热更新不会再触发重复的网络请求。还有个细节是visualMap组件的使用——县域数据如果要按数值做色阶展示,visualMap的min和max必须提前根据你的数据范围计算好,否则高值县区全部显示最深色,低值县区全是浅色,视觉区分度会非常差。我一般会先遍历一遍数据求最大最小值,再把它们动态设置到visualMap里。

3.3 两种方案的选型建议:从项目形态和团队技术栈出发

这两种 demo 的选型逻辑其实不复杂。如果你只是做一个内部用的临时页面、或者手里只有静态HTML文件、不愿意引入node_modules那一大堆依赖,方案一足够了。它的运行条件只有一个:能打开浏览器并访问到 echarts 的 CDN 地址。但方案一的隐患在于 CDN 外链如果挂了,地图就白屏了。

方案二适合代码需要长期维护、迭代频繁的产品级项目。模块化引入让地图数据和业务代码处在同一个构建体系内,版本有保障,也方便做按需加载。我个人的经验判断是:凡是活在代码仓库里、后续有小迭代需求的项目,直接上方案二;凡是跑在单机环境、生命周期不超过三个月的临时工具,方案一更省心。这两种方式资源包里都给了完整demo,不存在取舍问题。

4. 县区级地图的常见坑:从边界数据到渲染时序

4.1 细节一:数据匹配不上——name字段完全一致才算数

这个坑我印象很深,第一次用这套数据时拿业务库的县区名称直接去匹配,结果地图上空白一片,只显示轮廓不显示任何高亮区域。后来检查发现,业务库里写的是“东城区”,GeoJSON 的 properties 里也是“东城区”,但业务库里有的县区名称带了一个空格,或者用了简称。eCharts 的 map 系列做数据关联时,用的是严格的全等匹配。

解决的办法是在塞数据之前做一次名称归一化处理:

// 先读取GeoJSON里的合法名称列表 const validNames = new Set( countyGeo.features.map(f => f.properties.name) ); // 过滤业务数据中无法匹配的项 const filteredData = rawData.filter(item => validNames.has(item.name)); // 可选:打印出没匹配上的数据,人工核对 const unmatched = rawData .filter(item => !validNames.has(item.name)) .map(item => item.name); console.warn('以下名称未匹配到地图数据:', unmatched);

这段代码的逻辑是先构建一个合法名称的Set集合,再过滤业务数据。这样做的意义在于,与其让 eCharts 在渲染时静默丢弃匹配不上的数据,不如在前置环节显式地发现同义词问题。

4.2 细节二:拖动缩放时图形变形——坐标系被强制拉伸

还有一次遇到的情况是地图渲染出来了,但整个图形是扁的,圆形湖泊变成了椭圆。问题出在容器元素上——echarts 初始化时读取的是容器当前的宽高,如果容器宽度和高度不是按地图比例设置的,渲染出来的地图就会被拉伸。针对这种状况,最直接的解决方法是给容器设定一个合理的宽高比例,或者在初始化后监听容器尺寸变化手动调chart.resize():

// 容器尺寸变化时重绘 window.addEventListener('resize', () => { chart.resize(); });

如果你明确希望地图按等比例缩放且保持固定宽高比,可以在series里加一个aspectScale参数,这个参数控制了地图宽高比的自适应范围。不同的 GeoJSON 数据因为纬度跨度不同,默认的宽高比也不一致,aspectScale太小图形会被拉扁,太大会被拉高,需要先出图看一眼再微调。

4.3 细节三:地图外轮廓有零散小岛——边界噪声的滤波处理

这类问题出现在某些沿海或者江河交汇区域的县区。GeoJSON 数据里会包含很多面积非常小的多边形碎块,渲染出来就是地图边缘多了一些针尖大小的色块,视觉上非常脏。这套数据里同样存在类似现象,我处理时用了一个基于面积的过滤函数:

function filterTinyPolygons(feature, minArea = 0.0001) { const geometry = feature.geometry; if (geometry.type === 'Polygon') { return polygonArea(geometry.coordinates) > minArea; } if (geometry.type === 'MultiPolygon') { geometry.coordinates = geometry.coordinates.filter( coords => polygonArea(coords) > minArea ); } return true; }

判断面积的方法是用鞋带公式遍历多边形顶点计算近似面积。这里需要注意minArea的具体取值取决于你的坐标系单位,在 GCJ-02 坐标系下经度跨度一度约为一百公里级别,0.0001 对应的实际面积大约就是一平方公里以下的小岛。过滤时要留意别把那些面积虽小但属于行政区域主体的飞地给过滤掉了,最稳妥的操作是先跑一遍过滤函数,然后逐个检查被筛掉的多边形名称。

4.4 细节四:GeoJSON加载时序问题——初始化必须在数据到位之后

这个坑在工程化项目里尤其常见。有人在onMounted里直接写echarts.init,但 GeoJSON 是异步 import 进来的,初始化动作先执行了,等到数据返回再注册地图就晚了。表现就是页面上这个部分完全空白,控制台不报错,排查起来极其迷惑。

正确顺序永远是先拿数据、再注册、再初始化、最后setOption。如果采用异步加载,代码是这样组织:

// 异步加载GeoJSON完成后,再初始化图表 async function initCountyMap() { const geoModule = await import('./geo/344_counties.json'); echarts.registerMap('county_map', geoModule.default); const chart = echarts.init(document.getElementById('map')); chart.setOption(option); } initCountyMap().catch(err => { console.error('地图数据加载失败', err); });

把动态import和异步函数组合在一起,数据加载、地图注册、实例初始化整个流程串行执行,顺序就有保证了。这也是我习惯了在工程化项目里使用的写法,不依赖外部状态去保证时序。

5. 从demo到项目落地的改造指南:数据格式、多级联动与性能优化

5.1 业务数据如何正确转换成eCharts所需的散点映射格式

很少有人会把 demo 里的静态数据直接用于生产环境,通常业务数据来自接口且字段命名五花八门。常规做法是在拿到原始数据后做一次中间层转换,我在实际项目中习惯先写一个纯函数的映射器:

/** * 业务数据转地图数据 * @param {Array} bizData 业务接口返回的数据 * @param {string} nameField 名称字段 * @param {string} valueField 数值字段 * @returns {Array} 转换后的地图数据 */ function toMapData(bizData, nameField, valueField) { return bizData.map(item => ({ name: item[nameField], value: item[valueField] })); }

这个转换函数做的工作很朴素,但它的价值在于统一了数据出入口。如果接口返回的数据里有超出 344 个县区范围的外地数据,在这个环节就可以顺手过滤掉。还有一个需要注意的细节是value字段的类型,eCharts 在做色阶映射时如果遇到字符串类型的数字,偶尔会表现异常,所以转换时最好顺手Number()一次。

5.2 多级下钻:从省级到县区的缩放联动

实际项目中常见的需求是点击某个省、下钻到该省下辖各县区的数据面板。做这一类联动有几个具体的实现路径,但核心思路都是在点击地图区域时用adcode作为筛选条件去切换 GeoJSON 和业务数据。

在这套资源处理中,一个比较顺手的办法是先注册省市级的地图集合,然后预加载所有县区的 GeoJSON:

// 点击下钻 myChart.on('click', function (params) { const adcode = params.adcode; // 在registerMap时挂到properties里 const targetGeo = countyGeoMap.get(adcode); if (targetGeo) { // 重新注册新的地图作用于当前实例 echarts.registerMap('current_county', targetGeo); myChart.setOption({ series: [ { map: 'current_county', data: getCountyData(adcode) } ] }); } });

这里我习惯给每个县级 GeoJSON 的properties里挂上省级上级的adcode,这样在点击事件里可以通过params.adcode拿到当前区域编码。下钻的实质是重复调用setOption替换 series 里的map字段指向,不需要销毁图表实例。返回上一级时同理,只需要把省市级 GeoJSON 再注册回去。

5.3 性能优化:数据裁剪、按需注册和防抖渲染

说到性能,县级地图和大区域地图的差异非常明显。344 个县区的边界数据全部渲染,如果直接塞给 eCharts 一次画出来,首屏要承受较大的绘制压力。针对这种情况我做三层优化:

第一层是数据裁剪。如果当前视口只需要展示某几个县区,那在保存 GeoJSON 时就按区域分包存储,不让全局地图一次性加载。第二层是懒注册。地图实例初始化时不注册任何区域,当用户缩放到某一个层级时,再把对应的 GeoJSON 注入。第三层是在业务数据的频繁变化场景下使用防抖:

// 业务数据更新时防抖渲染 function debounceRender(chart, nextData, wait = 300) { let timer = null; return function () { if (timer) clearTimeout(timer); timer = setTimeout(() => { chart.setOption({ series: [ { data: nextData } ] }); }, wait); }; }

也就是说,当你在一个看板里同时放多个地图实例、或者数据每隔几秒刷新一次时,防抖能把重复的渲染任务合并成一次。这个套路的底气在于 GeoJSON 数据通常变化频率极低,需要高频更新的只是散点数值。

5.4 字体与label优化:县域密集区域的可读性设置

县区地图因为区域密集,label 展示策略直接影响阅读体验。344 个县区如果全部常显名称,文字互相遮挡的问题非常严重。我采用的取舍方式是:只在缩放级别达到一定程度后显示 label。eCharts 没有直接提供按缩放程度控制 label 显隐的参数,需要靠监听georoam事件来判断:

myChart.on('georoam', function () { const zoom = myChart.getOption().series[0].zoom; // 当前缩放比例 myChart.setOption({ series: [ { label: { show: zoom >= 3, // 放大到3倍以上再显示名称 fontSize: zoom >= 5 ? 12 : 9 } } ] }); });

虽然这个监听写法会频繁触发setOption,但相比每次缩放都让图形重绘的代价,这个开销完全可接受。如果你用的县区名比较长,还可以配合label.overflow设置截断方式,避免名称超出区域边界。

6. 一条必须掌握的验证路径:渲染前检查数据,渲染后核对坐标

拿到这套数据后,不要急于套进业务代码里,建议先走一遍快速验证流程。我每次拿到新的 GeoJSON 都会在本地起一个最简单的 HTML 页面或一个空白的 Vue 工程,先只渲染地图、不给 series 传任何业务数据,确认轮廓和边界数据完整。这一步能排除 90% 的“地图空白”问题。

接着第二步,我用地图本身的center坐标属性生成几个散点叠加在图上,逐一点击核对经纬度是否落在对应区域内。举个例子,某县区的center是 [116.4, 39.9],那这个点应该落在该县区轮廓内,点出去很远就说明坐标系有问题。

第三步才接入业务数据,按照前面说的转换函数和名称过滤逻辑,把业务数据渲染出来。渲染完成后我会随机抽三个县区点击一下,看看 tooltip 里的名称和值是否正确。这三步全部通过,这个地图模块才能正常交付。

回到这套资源本身,它的价值在于把最耗时的数据整理工作做完了。344 个县区的 GeoJSON 以及两种加载 demo 都已经封装好,剩下的就是按你自己的业务字段做映射。我到现在还很清楚地记得某次项目交付前一刻,因为一个县的名称多了个空格,地图上整整五个区域没有数据,排查了三个小时。从那以后我每次跑地图数据,都会强制走一遍名称匹配校验,把不匹配的项提前暴露在控制台里,而不是等渲染结束再盯着空白区域发呆。这份资源里的数据文件结构是标准的 GeoJSON 格式,只要你不去手工改动里面的 name 字段,按我上面说的验证流程走一遍,基本不会有意外情况。希望这些经验和代码能帮你在做县域地图可视化时少走一段弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询