ECharts中国地图数据文件指南:GeoJSON与城市经纬度实践

ECharts中国地图数据文件指南:GeoJSON与城市经纬度实践 简介这是一套针对ECharts地图功能整理的省级地理数据文件面向需要在前端展示区域分布的可视化开发者和数据分析人员可解决制作省份轮廓地图时缺少本地边界坐标的常见问题。资源为rar压缩包内部共35个js文件整体约844KB覆盖中国全境及山东、广东、四川、云南、新疆、西藏等主要省级行政区文件内以JSON结构保存各省边界经纬度点集支持直接通过script引用并配合ECharts的mapType、visualMap等配置快速渲染出带行政轮廓的交互地图。资源已有2056人学习非常适合离线开发、内网部署或需要自定义地图视觉样式的项目使用时可灵活按需加载单个省份或将多个省份数据合并使用配合区域数值展示色阶、提示框、缩放漫游等效果从而减少自行收集和处理地理边界数据的成本提升大屏或报表中区域数据的呈现效率。1. 很多人用 ECharts 画中国地图第一行代码就卡在数据文件上做数据可视化大屏的时候最常遇到的一个“假需求”是明明 ECharts 官网文档写得很清楚echarts.registerMap(china, geoJson)就能画出一张完整地图但真正动手时要么找不到可用的 JSON 文件要么找到的只有省级边界没有城市中心点。你需要的其实不止是“地图轮廓数据”还有一套和地图边界严格对应的“省份 主要城市经纬度数据文件”。这两个东西分开存在但在实际项目里必须同时用对否则 scatter 散点图打不上点、label 标错城市名、tooltip 里的经纬度对不上行政区划。这篇文章会从数据结构、数据来源、地图注册、坐标偏移、精度取舍几个维度把一套可复用的落地方案讲透。适合正在做数据大屏、地理信息展示或需要把业务数据落到地图上的工程师阅读。2. 省份与城市经纬度 JSON 的三种来源边界数据和打点数据别混用2.1 先从 GeoJSON 结构说起properties、geometry 和坐标嵌套层级无论你从哪里下载省份地图 JSON本质都是 GeoJSON 格式核心结构只有两个字段properties和geometry。properties存的是行政区划信息比如name省份名、adcode行政区划代码、centroid中心点坐标geometry存的是真正的多边形轮廓坐标类型通常是Polygon或MultiPolygon。{ type: FeatureCollection, features: [ { type: Feature, properties: { name: 广东省, adcode: 440000, centroid: [113.280637, 23.125178] }, geometry: { type: MultiPolygon, coordinates: [ [[[113.5, 22.5], [114.2, 22.8], [114.5, 23.1]]] ] } } ] }关键点在于centroid是给散点图用的“代表坐标”geometry.coordinates是给地图描边用的“边界坐标”两者用途完全不同。很多新手用geometry.coordinates[0][0]手动取坐标做打点结果拿到的是边界上的点发散到地图上就成了“城市坐标落在了邻省边界线上”。处理这项数据的基本认知是边界数据和打点数据分开维护不要互相推导。2.2 从 DataV.GeoAtlas 和阿里云 DataV 获取基础 json 文件先说最常见的免费数据文件来源。DataV.GeoAtlas 提供了全国、省份、市区三级的 GeoJSON 下载链接返回的数据精简过文件体积比其他开源项目小很多加载速度更快。其链接格式统一适合在后端或构建脚本里批量下载界面右上角也有“复制 JSON”的入口适合单独拉一个省份的数据做人肉调试。这套数据是阿里云 DataV 团队维护发布的国内主流商用可视化和低代码项目的常用选项。# 全国地图数据简化版包含全国所有省份的边界 curl -o china.json https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json # 单省份数据示例广东省adcode 为 440000 curl -o guangdong.json https://geo.datav.aliyun.com/areas_v3/bound/440000_full.json # 城市级数据示例广州市adcode 为 440100 curl -o guangzhou.json https://geo.datav.aliyun.com/areas_v3/bound/440100_full.json注意_full后缀这个版本包含下一级行政区划的子区域边界适合做下钻交互。如果不带_full则只有当前层级自身的轮廓文件更小渲染速度更快适合顶层总览场景。下载完用 jq 检查一下结构防止网络代理层给它包了一层文本。jq .type, (.features | length) china.json # 输出 FeatureCollection 和 34全国省区数量建议在项目里固定住数据文件的版本和时间点。在线链接背后的数据文件是持续更新的行政区划每年都会有调整如果不定版本比如存到本地 git 仓库里几个月后整张地图的边界可能悄悄变掉排查时极难定位。2.3 自己整理打点文件全国主要城市经纬度表的关键是“主城坐标”地图边界有现成来源但“主要城市经纬度”这类数据在公开仓库里的质量参差不齐。一个常见问题是很多开源的数据文件里北京写的是怀柔区的坐标上海写的是崇明岛的坐标文件发布者直接把“市辖区面积中心”当成“市中心”没有经过人工校验。我在项目里的一般做法是拿各省会、主要城市的市政府地址作为基准坐标或者使用高德地图坐标拾取器按城市名逐个获取坐标再批量写入 JSON。格式建议直接写成散点图可用的数组{ cities: [ { name: 北京市, province: 北京市, lng: 116.4074, lat: 39.9042 }, { name: 上海市, province: 上海市, lng: 121.4737, lat: 31.2304 }, { name: 广州市, province: 广东省, lng: 113.2644, lat: 23.1291 }, { name: 深圳市, province: 广东省, lng: 114.0579, lat: 22.5431 } ] }字段上至少要有name、province、lng、lat四个。province字段的作用是支持“按省过滤数据”的下钻交互不要偷懒省略。如果前端要做数据大屏的省份联动这个字段能直接省掉一次后端聚合查询。整体数据文件里每个城市的坐标统一保留 4 位小数因为在高德地图和天地图的坐标体系下这个精度对应的物理距离约 10 米左右足够做视觉散点标注了。3. 用 GeoJSON 和城市坐标文件在 ECharts 里把地图跑起来3.1 地图注册与首个可复现的渲染代码拿到 JSON 数据文件之后下一步就是让地图真正显示在页面上。ECharts 5 的注册方式和旧版类似但官方不再建议在 HTML 里直接用script标签全局引入全部地图数据而是推荐在框架里按需注册。下面是一个不依赖框架的最小示例浏览器里直接新建 HTML 文件就能跑通!DOCTYPE html html langzh-CN head meta charsetUTF-8 / titleECharts 中国地图示例/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body div idmain stylewidth: 900px; height: 600px;/div script // 1. 加载本地地理数据需要先准备好 china.json fetch(./china.json) .then(response response.json()) .then(geoJson { // 2. 注册地图名称和 registerMap 第一个参数保持一致 echarts.registerMap(china, geoJson); // 3. 初始化实例并配置 series-map const chart echarts.init(document.getElementById(main)); chart.setOption({ geo: { map: china, roam: true, itemStyle: { areaColor: #e0e0e0, borderColor: #596c83 } }, series: [ { type: map, map: china, roam: true, label: { show: true, color: #333 } } ] }); }); /script /body /html这段代码的关键点有三个。第一fetch加载的是本地 JSON不是线上链接部署时不会因为跨域问题挂掉第二registerMap(china, geoJson)中的china是一个自定标识符后面的geo.map和series.map必须和它保持一致第三最常见的问题是同时配置了geo和series-map时series 里的data没写地图只有轮廓但没有任何统计数值视觉效果上像是一张空白底图。3.2 series-map 与 geo 坐标系的配合用 scatter 叠加城市坐标只显示省份轮廓的图没有业务含义真正的可视化大屏一般会在省级地图上叠加城市散点。散点的数据源就是前面整理好的城市经纬度 JSON。这种场景下我通常不使用series-map的自带 label而是通过series-scatter配合coordinateSystem: geo把城市坐标点压到地图上这样图例、颜色映射和 tooltip 都更好控制。const cityData [ { name: 北京, value: [116.4074, 39.9042, 100] }, { name: 上海, value: [121.4737, 31.2304, 85] }, { name: 广州, value: [113.2644, 23.1291, 72] } ]; chart.setOption({ geo: { map: china, roam: true }, series: [ { name: 城市指标, type: scatter, coordinateSystem: geo, data: cityData, symbolSize: function(val) { // 第三个维度当作指标值控制泡泡大小 return Math.max(6, val[2] / 10); }, label: { show: true, position: right, formatter: function(params) { return params.name; } } } ] });坐标定义上value数组三个元素依次表示lng、lat和业务指标值。顺序不能写错成先 lat 后 lngECharts 的 geo 坐标系严格按照[lng, lat]顺序读取写反了散点会出现在地图外GUI 上完全看不到报错数据又对不上这一步容易成为隐藏排错陷阱。另一点是 symbolSize 里对val[2]做归一化直接拿指标原始值除以固定系数会让数据差异极大的城市没有可比性建议先做一次线性或对数归一化再映射到尺寸区间。3.3 同一份城市 JSON 文件里扩展字段的约定把人口、GDP 等指标缓存进来到了实战阶段城市经纬度文件就不应该只有name/lng/lat三个字段了。因为散点图经常需要展示多套业务指标比如每个城市的销售额、客流量、设备数如果把指标放到单独的接口或变量里每次切换指标就得做一次数组匹配劳心费神。我的做法是直接在坐标 JSON 里给每个城市预留一个metrics对象{ name: 杭州市, province: 浙江省, lng: 120.1551, lat: 30.2741, metrics: { sales: 52342, visits: 12000, devices: 330 } }前端读取时按当前指标做数据映射const currentDimension sales; const seriesData cityJson.cities.map(item ({ name: item.name, value: [item.lng, item.lat, item.metrics[currentDimension]] }));这个做法的好处是数据文件和可视化配置彻底解耦。后端同学更新坐标时不会影响前端的维度切换逻辑前端切换维度也不需要重新请求数据接口尤其是数据大屏在会议室演示时不会出现“等接口响应”的尴尬。4. 经纬度数据处理避坑指南坐标偏移、精度选择和文件组织方式4.1 坐标偏移问题从 GPS 转高德经纬度数据才能打到地图上做数据大屏时最容易踩的一个技术点就是坐标偏移。如果你通过手机 GPS 或者运动类 App 拿到一批设备轨迹点经纬度是原始 WGS-84 坐标系下的值而高德地图、天地图以及很多基于 GCJ-02 坐标系做地图底图的可视化项目直接用 WGS-84 坐标画散点会出现整体偏移几百米甚至更远。原因是国内地图应用出于法律法规要求进行了坐标加密偏移偏移量大约在 300 到 500 米之间对散点图来说视觉上非常明显。处理办法是在数据进入前端前统一做一次坐标系转换。常见做法是高德地图官方提供了 JS API 里的AMap.convertFrom方法但接口调用有频率限制。数据量大的场景比如几千条设备轨迹建议在后端用 Python 内置的坐标偏移库做批量转换或者自己实现一个标准转换函数import math def wgs84_to_gcj02(lng, lat): 将 WGS-84 坐标转换为 GCJ-02 坐标高德/腾讯地图坐标系 输入输出均为浮点数单位度 a 6378245.0 ee 0.006693421622965943 def transform_lat(x, y): ret -100.0 2.0 * x 3.0 * y 0.2 * y * y 0.1 * x * y 0.2 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * math.pi) 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(y * math.pi) 40.0 * math.sin(y / 3.0 * math.pi)) * 2.0 / 3.0 ret (160.0 * math.sin(y / 12.0 * math.pi) 320 * math.sin(y * math.pi / 30.0)) * 2.0 / 3.0 return ret def transform_lng(x, y): ret 300.0 x 2.0 * y 0.1 * x * x 0.1 * x * y 0.1 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * math.pi) 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(x * math.pi) 40.0 * math.sin(x / 3.0 * math.pi)) * 2.0 / 3.0 ret (150.0 * math.sin(x / 12.0 * math.pi) 300.0 * math.sin(x / 30.0 * math.pi)) * 2.0 / 3.0 return ret d_lat transform_lat(lng - 105.0, lat - 35.0) d_lng transform_lng(lng - 105.0, lat - 35.0) rad_lat lat / 180.0 * math.pi magic math.sin(rad_lat) magic 1 - ee * magic * magic sqrt_magic math.sqrt(magic) d_lat (d_lat * 180.0) / ((a * (1 - ee)) / (magic * sqrt_magic) * math.pi) d_lng (d_lng * 180.0) / (a / sqrt_magic * math.cos(rad_lat) * math.pi) gcj_lat lat d_lat gcj_lng lng d_lng return gcj_lng, gcj_lat这段 Python 代码的精度在绝大多数民用场景下已经足够偏移误差控制在 1 米以内。注意它只适用于国内区域坐标转换如果数据包含港澳台及周边海域需要再找对应区域系数修正。实际项目中排查坐标偏移问题时有一个非常快的验证方法把自己所在楼栋的经纬度拿到地图页面打点如果点落在自己脚下说明坐标系是正确的否则就需要做转换。4.2 数据精度与 JSON 文件大小4 位小数和 6 位小数差出几兆字节省份边界坐标文件的字节数和坐标小数位密切相关。从工程角度讲地图轮廓数据保留 5 位小数完全够了5 位小数对应精度约 1 米多保留到 6 位只会让文件体积增加约 20%而人眼在屏幕上根本分辨不出这种差别。城市中心点数据则建议保留 4 位小数因为散点图的最小可视化单位是像素4 位小数的误差远比像素小。如果对数据体积敏感比如移动端 H5 页面需要在弱网环境加载可以对下载好的 GeoJSON 做一次后处理去掉所有坐标中多余的小数位并删除空字段const fs require(fs); // 读取下载好的省份 json const geo JSON.parse(fs.readFileSync(china_full.json, utf8)); // 坐标数组逐层压缩到 5 位小数 function trimCoordinates(coords) { if (typeof coords[0] number) { return [Number(coords[0].toFixed(5)), Number(coords[1].toFixed(5))]; } return coords.map(trimCoordinates); } geo.features geo.features.map(feature { // 删除不需要的字段如全名全称拼音 delete feature.properties.name_en; delete feature.properties.center; if (feature.geometry feature.geometry.coordinates) { feature.geometry.coordinates trimCoordinates(feature.geometry.coordinates); } return feature; }); fs.writeFileSync(china_optimized.json, JSON.stringify(geo));这段 Node.js 脚本在任何安装了 Node 的机器上都能跑压缩之后全国级别的边界文件通常会减少 30% 到 40% 的体积。注意遍历坐标时用了递归函数因为 GeoJSON 的Polygon是三维数组MultiPolygon是四维数组层级不固定。4.3 按省拆分还是全量平铺地图下钻场景下的两种文件组织策略当项目需要支持“全国 → 省份 → 城市”逐级下钻时城市坐标文件怎么组织直接影响前端加载逻辑。常见的做法有两种一种是全量平铺把全国三百多个地级市的坐标全部放在一个 JSON 文件里简单直接但首屏加载要一次性下载完另一种是按省拆分每个省份单独一个 JSON 文件点击省份下钻时只加载该省的城市坐标。我一般建议采用第二种。因为省级下钻时只需要当前省的城市散点按省拆分后单文件体积通常只有 2KB 到 5KB加载快且代码逻辑更清晰。文件命名直接采用adcode.json例如广东省的城市文件就叫440000_cities.json这样和 GeoJSON 文件的 adcode 逻辑完全统一。前端动态加载时使用import()或fetch拼接路径async function loadProvinceCities(adcode) { const res await fetch(./geo/cities/${adcode}_cities.json); if (!res.ok) throw new Error(城市数据文件不存在: adcode); const data await res.json(); renderScatterByCity(data.cities); }按省拆分还有一个附加好处地图下钻的缓存策略更好做。用户只看某个省时不会把全国三百多个城市的坐标都拉下来而且后续如果想对个别省份做坐标修正只需要覆盖单个小文件不影响主包版本。5. 坐标文件校验的两个硬性检查用边界包络线和重复名排查脏数据拿到一份城市经纬度 JSON无论是自己整理的还是从开源仓库下载的上线前做两道硬性检查能提前拦截大量“地图上城市点飘到国外”的线上事故。第一个检查是边界包络线检查。中国的经纬度边界大致在lng: 73.5 ~ 135.1lat: 18.2 ~ 53.6之间如果某个城市的坐标明显超出这个范围基本可以确定是混入了坐标顺序写反或使用了错误的坐标系数据。写一个极简校验脚本即可jq .cities[] | select(.lng 73 or .lng 136 or .lat 17 or .lat 54) | .name city_center.json如果没有返回说明所有城市坐标都在合理包络范围内。检查不能直接拿地图边界比对因为边界本身就是 GeoJSON 里带的数据坐标数据作为独立来源更可靠。正式检查时可以把城市坐标点转换为 GeoJSON Point再使用turf.js的布尔运算判断点是否在对应省份的多边形内部这个方案能让异常定位精确到具体城市const turf require(turf/turf); const chinaGeo require(./china.json); // 构建省份名称到多边形特征的映射 const provinceFeatureMap {}; chinaGeo.features.forEach(f { provinceFeatureMap[f.properties.name] f; }); // 校验单个城市是否落在它所属省份的边界内 function validateCityInProvince(city) { const provinceFeature provinceFeatureMap[city.province]; if (!provinceFeature) return { valid: false, reason: 省份不存在 }; const point turf.point([city.lng, city.lat]); const isValid turf.booleanPointInPolygon(point, provinceFeature); return { valid: isValid }; }第二项是重复名检查。城市坐标 JSON 文件可能存在 “中山市” 和 “台湾省” 之类的地名出现在下辖市和省份两层 name 字段里的情况或者广东省的 city 列表里混入了一个“广州市旧”之类的脏条目。用 Node.js 跑一遍按name字段分组计数const cities require(./city_center.json).cities; const countMap {}; cities.forEach(c { countMap[c.name] (countMap[c.name] || 0) 1; }); const duplicates Object.entries(countMap).filter(item item[1] 1); console.log(duplicates);拿到重复清单后逐个业务确认有的重复是因为同城不同区比如“东莞市”下有多个街道维护成了独立条目需要业务方给出取舍方案不能直接删。这两个检查做完这份“省份 主要城市经纬度数据文件”才算真正达到了可上线状态后续所有 EChartsseries-scatter的精细调参才有意义。本文还有配套的精品资源点击获取