江苏省矢量地图ECharts多级GeoJSON数据下钻实战
简介这是一份面向数据可视化开发者与前端工程师的江苏省矢量地图资源包基于 ECharts 与 GeoJSON 实现省、市、县三级行政区划的逐层下钻展示。通过类似 ?orgCode100000orgName中国 的访问参数可层层点击加载对应层级的矢量地图并动态渲染不同颜色适用于大屏可视化、区域数据统计与地理信息展示等场景。压缩包共 111 个文件以 110 个 json 数据文件和 1 个 html 页面为主json 文件承载各级行政区划边界数据html 提供地图加载与交互示例整体约 599KB体积轻量便于集成。目前已有 1199 人学习下载说明该资源在实际开发中具备一定参考价值。读者可直接获得完整的省级、市级、县级 GeoJSON 数据与配套调用代码省去自行采集与转换边界数据的成本快速搭建可交互的江苏地图可视化页面并在此基础上叠加业务数据、调整配色与交互逻辑。1. 江苏省矢量地图 ECharts 多级 GeoJSON 数据从省级到县级的落地路径做数据大屏或者区域态势感知的开发者大概率都遇到过这个需求底图要能下钻省 → 市 → 县三级联动鼠标悬浮高亮点击进入下一级返回上一级。听起来简单但真正动手时第一个卡点往往不是 ECharts 配置而是矢量边界数据从哪来、怎么裁、怎么保证层级对应关系不乱。江苏省的情况尤其典型13 个地级市95 个县级行政区外加各种开发区、新区边界数据如果来源不统一拼在一起就会出现缝隙、重叠、飞地丢失。这篇笔记就围绕「江苏省矢量地图 ECharts 多级 GeoJSON 数据」这个具体场景把数据获取、格式转换、ECharts 注册、下钻交互、性能优化这条链路拆开讲。适合正在做大屏可视化、区域数据监控、或者需要离线部署地图的前端和全栈工程师。下面所有操作都基于公开可获取的行政区划矢量数据不涉及任何特定平台或私有接口。2. 多级 GeoJSON 数据的来源、裁剪与坐标系统一2.1 为什么不能直接拿一份全国数据切一切很多人的第一反应是找一份全国省级 GeoJSON然后用工具按属性筛选出江苏再按市界切出各市。这个思路在省级层面可行但下到县级就会翻车。原因在于全国尺度的行政区划矢量数据县级边界的精度通常被简化过简化算法在不同层级上不一致导致市级边界和县级边界对不齐。你会在 ECharts 里看到市界和县界之间出现细缝或者某个县被市界切掉一个角。常见做法是省级用一份数据市级用一份数据县级用一份数据三份数据来自同一套基础测绘成果的不同比例尺版本。实际操作中我一般会从公开的行政区划矢量数据服务里分别导出三个层级的 GeoJSON而不是从一份数据里递归裁剪。导出时注意属性表里必须保留行政区划代码这是后续做层级关联的唯一可靠字段。江苏省的行政区划代码规则省级 320000市级从 320100 到 321300县级从 320102 开始按区县顺序排列。拿到数据后第一件事是检查属性表里有没有adcode字段没有的话需要根据名称手动补一份映射表。2.2 用 ogr2ogr 做格式转换和坐标纠偏公开数据常见的格式有 Shapefile、GeoJSON、TopoJSON。ECharts 只认 GeoJSON 或 TopoJSON所以 Shapefile 需要转换。命令行工具ogr2ogr是最稳的选择它属于 GDAL 套件跨平台可用。# 将 Shapefile 转为 GeoJSON并指定输出坐标系为 WGS84 ogr2ogr -f GeoJSON \ -t_srs EPSG:4326 \ jiangsu_province.json \ jiangsu_province.shp # 批量转换市级数据保留 adcode 字段 ogr2ogr -f GeoJSON \ -t_srs EPSG:4326 \ -select adcode,name \ jiangsu_city.json \ jiangsu_city.shp逻辑说明-t_srs EPSG:4326强制输出为 WGS84 经纬度坐标这是 ECharts 默认使用的坐标系。如果源数据是 CGCS2000 或者地方坐标系不转换直接加载会出现整体偏移。-select用于裁剪属性字段只保留adcode和name减小文件体积。县级数据同理但要注意县级 Shapefile 里可能包含多个图层需要先确认图层名。参数上有一个坑-t_srs是目标坐标系-s_srs是源坐标系。如果源数据没有.prj文件GDAL 无法自动识别坐标系必须手动指定-s_srs。我遇到过一批数据缺少投影文件结果转换出来的 GeoJSON 坐标全是错的排查了半天才发现是源坐标系没声明。2.3 用 Python 做边界简化和属性清洗原始矢量数据直接转 GeoJSON 后文件可能非常大。江苏省县级数据如果精度很高单个文件超过 10MB 很常见加载到浏览器里会明显卡顿。需要在保留形状特征的前提下做简化。import json from shapely.geometry import shape, mapping from shapely.simplify import simplify def simplify_geojson(input_path, output_path, tolerance0.001): with open(input_path, r, encodingutf-8) as f: data json.load(f) for feature in data[features]: geom shape(feature[geometry]) # 使用 Douglas-Peucker 算法简化tolerance 单位为度 simplified simplify(geom, tolerance, preserve_topologyTrue) feature[geometry] mapping(simplified) # 确保 adcode 为字符串避免前导零丢失 if adcode in feature[properties]: feature[properties][adcode] str(feature[properties][adcode]) with open(output_path, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse) simplify_geojson(jiangsu_county_raw.json, jiangsu_county.json, tolerance0.0005)逻辑说明preserve_topologyTrue保证简化后不会出现自相交或边界断裂。tolerance是关键参数单位是度。对于县级数据0.0005 大约对应 50 米左右的精度损失肉眼在省级大屏上基本看不出差别但文件体积能减少 60% 以上。如果做市级下钻可以放宽到 0.001如果做县级内部街区级展示建议不要超过 0.0002。属性清洗里最容易忽略的是adcode的类型。有些数据源里 adcode 是数字类型JSON 序列化后前导零会丢失比如 320102 变成 32102导致后续按 adcode 关联时匹配不上。统一转成字符串是血泪经验。3. 在 ECharts 中注册多级地图并实现下钻交互3.1 用 registerMap 注册三级地图的正确顺序ECharts 的registerMap是全局注册同一个地图名重复注册会覆盖。多级下钻场景下我一般会一次性把省、市、县三级数据都注册好用不同的地图名区分。import * as echarts from echarts; import jiangsuProvince from ./geo/jiangsu_province.json; import jiangsuCity from ./geo/jiangsu_city.json; import jiangsuCounty from ./geo/jiangsu_county.json; // 注册省级地图 echarts.registerMap(jiangsu-province, jiangsuProvince); // 注册市级地图注意市级数据里包含所有地级市的边界 echarts.registerMap(jiangsu-city, jiangsuCity); // 注册县级地图 echarts.registerMap(jiangsu-county, jiangsuCounty); // 初始化图表 const chart echarts.init(document.getElementById(map-container)); const option { series: [{ type: map, map: jiangsu-province, roam: true, label: { show: true, fontSize: 10 }, emphasis: { label: { show: true }, itemStyle: { areaColor: #ffd700 } }, data: [] }] }; chart.setOption(option);逻辑说明registerMap的第一个参数是地图名后续在series.map里引用。省级地图只包含 13 个地级市的轮廓市级地图包含所有地级市的完整边界县级地图包含所有区县的边界。注意市级地图和县级地图是独立文件不是从省级地图里切出来的这样才能保证边界对齐。参数上roam: true允许缩放和平移做大屏时建议开启。label.show在省级层面可以关掉因为地级市名称太密集下钻到市级后再打开。3.2 点击事件驱动的下钻与返回逻辑下钻的核心是监听click事件拿到当前点击区域的adcode或name然后切换series.map和data。let currentLevel province; let currentAdcode 320000; const historyStack []; chart.on(click, function(params) { if (currentLevel province) { // 从省级下钻到市级 const cityAdcode params.data.adcode; const cityName params.name; historyStack.push({ level: province, adcode: currentAdcode }); currentLevel city; currentAdcode cityAdcode; renderCityMap(cityAdcode, cityName); } else if (currentLevel city) { // 从市级下钻到县级 const countyAdcode params.data.adcode; historyStack.push({ level: city, adcode: currentAdcode }); currentLevel county; currentAdcode countyAdcode; renderCountyMap(countyAdcode); } }); function renderCityMap(cityAdcode, cityName) { // 从市级数据中筛选出当前市的区县 const cityFeatures jiangsuCity.features.filter( f f.properties.adcode.startsWith(cityAdcode.slice(0, 4)) ); const cityGeoJson { type: FeatureCollection, features: cityFeatures }; echarts.registerMap(current-city, cityGeoJson); chart.setOption({ series: [{ map: current-city, data: cityFeatures.map(f ({ name: f.properties.name, adcode: f.properties.adcode, value: Math.random() * 100 })) }] }); }逻辑说明historyStack用于记录下钻路径返回时逐级弹出。renderCityMap里用adcode.startsWith做前缀匹配因为市级 adcode 的前四位就是所属地级市的代码。比如南京市 adcode 是 320100其下辖的玄武区是 320102前四位 3201 一致。这里有一个容易翻车的点registerMap注册的current-city会覆盖上一次注册的同名地图。如果用户快速点击不同城市可能出现地图渲染错乱。解决办法是每次注册时用唯一名称比如current-city-${cityAdcode}或者在切换前先chart.clear()。3.3 返回上一级的实现与状态同步返回逻辑比下钻简单但状态同步容易出问题。不能只改地图还要把data和label配置一起还原。function goBack() { if (historyStack.length 0) return; const prev historyStack.pop(); currentLevel prev.level; currentAdcode prev.adcode; if (currentLevel province) { chart.setOption({ series: [{ map: jiangsu-province, data: provinceData, label: { show: false } }] }); } else if (currentLevel city) { renderCityMap(currentAdcode, ); } } // 绑定返回按钮 document.getElementById(back-btn).addEventListener(click, goBack);逻辑说明返回时从historyStack弹出上一级状态然后根据层级重新设置map和data。注意label.show也要跟着切换省级隐藏、市级显示否则返回后标签会残留。参数上provinceData是预先准备好的省级数据数组包含每个地级市的名称和数值。每次下钻和返回时data都要重新绑定不能复用上一级的数据否则会出现名称对不上、数值错位的问题。4. 多级 GeoJSON 数据的避坑与常见问题排查4.1 边界缝隙与重叠现象、原因与解决现象在 ECharts 里放大后市界和县界之间出现白色细缝或者两个相邻县有轻微重叠导致颜色叠加。原因不同层级数据来自不同比例尺的测绘成果简化算法和容差不同导致边界不重合。另外GeoJSON 坐标精度截断也会引入误差。解决统一使用同一套基础数据源导出三个层级如果无法统一在 GIS 工具里做一次拓扑修复确保相邻面共享边。ECharts 层面可以通过设置itemStyle.borderWidth: 0.5和borderColor与背景色一致来视觉掩盖细缝但治标不治本。4.2 adcode 类型不一致导致下钻失败现象点击某个市之后地图没有变化控制台也没有报错。原因params.data.adcode是数字类型而 GeoJSON 属性里的 adcode 是字符串startsWith调用在数字上会报错或者返回 false。解决在数据加载阶段统一把 adcode 转为字符串。ECharts 的data数组里也保持字符串类型。可以在setOption之前做一次String(adcode)转换。4.3 县级数据加载卡顿与内存暴涨现象下钻到县级后页面明显卡顿缩放和平移不流畅严重时浏览器标签页崩溃。原因县级 GeoJSON 文件过大坐标点数量太多ECharts 渲染时计算量激增。解决对县级数据做简化tolerance控制在 0.0005 到 0.001 之间关闭不必要的label和emphasis动画使用large: true和largeThreshold优化渲染。如果还是卡考虑按市拆分县级数据每次只加载当前市的区县。4.4 飞地丢失与岛屿消失现象某些县的边界不完整或者沿海城市的岛屿没有显示。原因数据简化时preserve_topology没有开启或者源数据本身就把小岛屿过滤掉了。解决简化时务必设置preserve_topologyTrue检查源数据是否包含 MultiPolygon 类型ECharts 支持 MultiPolygon但如果数据里被转成了 Polygon岛屿就会丢失。4.5 地图名称与数据名称不匹配现象地图上某个区域没有颜色或者颜色填错了区域。原因series.data里的name和 GeoJSON 属性里的name不一致比如「南京市」和「南京市区」、「鼓楼区」和「鼓楼」。解决以 GeoJSON 属性里的name为准数据数组里的name必须完全一致。可以在加载数据后打印两份名称列表做 diff快速定位不一致的条目。5. 进阶用 TopoJSON 压缩体积与按需加载县级数据5.1 TopoJSON 的量化压缩与 ECharts 兼容性GeoJSON 的坐标是浮点数全量存储TopoJSON 通过弧段共享和量化编码能把体积压缩到原来的 20% 到 30%。ECharts 从 5.0 开始原生支持 TopoJSON直接registerMap即可。# 安装 topojson-server 和 topojson-simplify npm install -g topojson-server topojson-simplify # 将 GeoJSON 转为 TopoJSON量化精度为 1e5 geo2topo jiangsu_county.json jiangsu_county_topo.json topoquantize 1e5 jiangsu_county_topo.json jiangsu_county_quantized.json逻辑说明geo2topo把 GeoJSON 转为 TopoJSONtopoquantize做坐标量化1e5表示保留 5 位小数精度。量化后的坐标是整数体积大幅减小。ECharts 加载 TopoJSON 时不需要额外配置和 GeoJSON 用法一致。参数上量化精度不是越高越好。1e5对于县级地图足够1e6会保留更多细节但体积翻倍。建议先用1e5测试如果边界锯齿明显再提高。5.2 按需加载只请求当前市的县级数据如果县级数据总量很大一次性加载所有区县不现实。可以按市拆分成 13 个独立的 TopoJSON 文件下钻时动态fetch。async function loadCountyData(cityAdcode) { const url /geo/county/${cityAdcode}.topo.json; const response await fetch(url); const topoData await response.json(); echarts.registerMap(county-${cityAdcode}, topoData); return topoData; } // 在下钻到市级时调用 chart.on(click, async function(params) { if (currentLevel city) { const countyData await loadCountyData(params.data.adcode); chart.setOption({ series: [{ map: county-${params.data.adcode}, data: countyData.objects.counties.geometries.map(g ({ name: g.properties.name, value: Math.random() * 100 })) }] }); } });逻辑说明每个市的县级数据单独打包文件名用 adcode 命名。下钻时根据当前市的 adcode 拼出 URL动态加载并注册。这样首屏只需要加载省级数据市级和县级按需请求。注意 TopoJSON 的对象结构里objects下的键名取决于转换时的命名可能是counties或default需要先打印确认。另外动态注册的地图名要唯一避免覆盖。5.3 一个验证数据完整性的小技巧在正式接入 ECharts 之前我习惯先用 Python 做一次完整性校验检查每个市的 adcode 前缀是否覆盖了所有下辖区县检查县级数据的 adcode 是否都能在市级数据里找到对应父级。import json with open(jiangsu_city.json, r, encodingutf-8) as f: city_data json.load(f) with open(jiangsu_county.json, r, encodingutf-8) as f: county_data json.load(f) city_codes {f[properties][adcode][:4] for f in city_data[features]} county_codes {f[properties][adcode][:4] for f in county_data[features]} missing city_codes - county_codes if missing: print(f以下市缺少县级数据: {missing}) else: print(所有市的县级数据完整)逻辑说明用集合差集快速找出缺失的市级代码。这个检查能在数据准备阶段就发现问题避免上线后下钻到某个市发现是空白。我自己的习惯是每次更新地图数据后先跑一遍这个校验脚本再跑一遍 ECharts 的省级渲染确认没有报错才提交。地图数据这东西一旦层级对不上排查起来就是黑匣子不如提前把后悔药吃上。希望帮到你。本文还有配套的精品资源点击获取