简介:面向 Web 可视化开发者、前端工程师及数据产品设计人员,提供一份专用于 ECharts 3D 地图渲染的地理数据文件。其中 china.json 基于 GeoJSON 规范封装了中国各省份行政区域的边界坐标与名称信息,搭配 ECharts 的 geo3D 组件即可快速构建可平移、缩放、旋转的 3D 中国地图,免去从零搜集和转换地理数据的繁琐工作。资源压缩包仅 1 个文件,文件类型为 json,整体占用 13KB,体积小巧、结构单一,便于直接引入项目或作为学习示例。已有 245 人学习/下载该数据包。实际开发中,可基于这份数据定义区域颜色、边界样式与标签,并配合业务数据实现按省份着色、柱状图叠加或动态轮播等效果;结合后端接口还能实时更新数值,适用于大屏展示、数据监控、智慧城市和教学演示等多种可视化场景。 做了这么多年数据可视化,地图类的需求基本绕不开 ECharts,而一旦客户说“我要一个 3D 中国地图”,那技术选型十有八九就是echarts + echarts-gl + china.json这套组合。我第一次做 3D 地图是在一个城市大数据大屏项目里,甲方要求地图能旋转、能 hover 高亮省份,还要在几个核心城市上标出实时业务量。当时网上关于geo3D + map3D + scatter3D的完整案例很少,很多配置都是自己一点点试出来的。这篇就把我踩过的坑和最终可用的方案整理出来,核心围绕echarts-gl 加载 china.json 做 3D 地图这条主线展开,适合正在做大屏可视化、或者想把平面中国地图升级成 3D 效果的读者参考。
文章会覆盖从 china.json 数据准备、地图注册、geo3D 与 map3D 的选型差异,到 visualMap 分段定制、scatter3D 城市标记数量、纹理贴图以及常见报错排查的完整过程。代码都是可直接复制使用的级别,配置参数我会顺带解释为什么这么设,方便你按自己项目需求调整。
1. 方案选型与数据准备
1.1 为什么选 echarts + echarts-gl + china.json
先回答一个很多人问过的问题:原生 ECharts 就能画中国地图,为什么非要上 echarts-gl?
普通 ECharts 的 map 系列本质是平面多边形渲染,虽然也可以设置一些伪 3D 效果(比如阴影、光晕),但地图本身不会“立起来”,也不能自由旋转视角。而 echarts-gl 是 ECharts 官方推出的 WebGL 扩展库,它提供的geo3D、map3D、scatter3D、bar3D等组件是真正跑在三维空间里的,支持光照、材质、视角旋转、环境光遮蔽等效果。大屏上的“会转的 3D 中国地图”就是用这个库做的。
之所以还要单独准备china.json,是因为 ECharts 5 开始已经不再内置中国地图数据(早期版本还有个mapData可以引用,现在基本废弃了),地图的 GeoJSON 数据需要自己加载并注册。而china.json是国内用得最广泛的省级行政区划 GeoJSON 文件,包含每个省份的边界坐标、名称、中心点等关键信息,后续做省份 hover 高亮、城市坐标标记都要依赖它。
这套方案的另一个好处是:无需引入 Cesium、Mapbox GL 这类重型 GIS 引擎。如果只是做省级颗粒度的数据展示,用 ECharts 体系可以一个技术栈通吃图表和 3D 地图,后期维护成本低,部署也简单。
1.2 china.json 数据源与版本坑
china.json 的获取方式其实有过几次变化。早期很多教程让你去 ECharts 官网的示例库里找 china.json 文件下载,后来阿里云 DataV 的 GeoAtlas 提供了在线获取行政区划数据的接口,现在主流做法就是用 DataV 的接口拉数据,地址是:
https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json这个接口返回的就是全国省级 GeoJSON,包含properties.name(省份名称)、properties.center(省份中心经纬度)、properties.adcode(行政区划编码)等字段。用新版数据的最大好处是,1945 个区县、333 个地级市、34 个省级行政区的编码都能对应上,而且南海诸岛、藏南地区这些边界信息都包含在里头,不会出现地图缺一块的尴尬情况。
注意:不同来源的 china.json 结构有差异。建议统一使用 areas_v3 版本的坐标数据,因为老版本(比如旧 echarts 示例里的 china.json)在部分省份的边界代码上有些偏色,注册之后可能会出现 hover 区域错位的问题。
我一般会把 china.json 下载到本地src/assets/map/目录下管理,而不是每次打开页面都从远程拉。这样做有两个原因:一是大屏项目通常部署在内网环境,外部 CDN 不一定通;二是 GeoJSON 文件本身有几百 KB,本地加载比远程请求稳定得多、速度也快。
// 本地引入 import chinaJson from '@/assets/map/china.json'; // 或者使用网络请求方式 // const res = await fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json'); // const chinaJson = await res.json();版本匹配上有个很容易踩的坑:echarts-gl对 ECharts 5 的支持是从 2.0.8 之后才稳定的,如果你项目里用的是 ECharts 5.x,建议这样安装依赖:
npm install echarts@5.4.3 echarts-gl@2.0.9如果装成 echarts-gl 1.x,地图组件大概率直接白屏或者报一堆奇怪的类型错误。这个问题我在排查的时候遇到过很多次,先看版本,再看代码。
2. 地图注册与三维场景搭建
2.1 注册地图并初始化最简三维场景
拿到 china.json 之后,第一步不是写 option,而是先把地图数据注册到 ECharts 中。注册方法在 ECharts 4 和 5 里都一致:
import * as echarts from 'echarts'; import 'echarts-gl'; import chinaJson from '@/assets/map/china.json'; echarts.registerMap('china', chinaJson); const chart = echarts.init(document.getElementById('mapContainer')); chart.setOption({ geo3D: { map: 'china', roam: true, itemStyle: { color: '#1a2a4a', opacity: 1, borderWidth: 1, borderColor: '#5bc0de' }, label: { show: true, textStyle: { color: '#fff', fontSize: 10 } }, emphasis: { label: { show: true, color: '#fff', fontSize: 14 }, itemStyle: { color: '#22c3aa' } }, shading: 'lambert', light: { main: { intensity: 1.2, shadow: true } }, viewControl: { distance: 100, alpha: 40, beta: 0, autoRotate: false }, regionHeight: 3 } });运行这段代码后,你应该能看到一个可旋转、可缩放、hover 省份会高亮的 3D 中国地图形状。这里重点解释几个容易出现问题的参数:
regionHeight:地图各区域向上挤出的厚度,单位是像素。默认值好像是 3,如果你觉得地图太薄、立体感不强,可以调大到 6 或 8。但注意不要调太大,否则省份之间会有明显的黑色缝隙,影响视觉。shading: 'lambert':这是材质渲染方式。可选'color'、'lambert'、'realistic'。lambert是最常用的,性能适中,且有明显的明暗面效果;realistic效果最好但吃性能,缩放时容易掉帧。viewControl.distance:相机初始距离。这个值受地图容器大小影响很大,没有固定标准,需要根据实际展示效果微调。容器越大,distance 数值可能要越大,否则地图会冲出画面。label.show:这里控制的是省份名称标签。后面会专门讲标签显示不全的处理方案。
2.2 geo3D 和 map3D 到底怎么选
很多新手会把geo3D和map3D搞混,其实这两个组件定位不同,用错了会出现“地图根本不显示数据颜色”的奇怪问题。
geo3D本质是一个三维地理坐标系组件,它可以承载多种三维系列(比如 scatter3D、bar3D、lines3D)在上面绘制数据。地图只是它的一种视觉背景,它本身不直接接收“省份-数值”的重度业务数据映射。如果你只是要一张能转、能高亮的 3D 地图,或者要在上面叠加散点、飞线,用 geo3D 最合适。
map3D则是一个系列(series),它天然支持“省份 -> 数值 -> 颜色”这种数据映射,配合visualMap可以方便地让省份颜色随数值变化。但它作为系列,承载其他三维图形的能力弱一些。
我实际项目的分工一般是:
| 需求场景 | 推荐组件 |
|---|---|
| 纯展示 3D 地图,hover 省份高亮,叠加城市散点 | geo3D + scatter3D 组合 |
| 各省份数值染色(热力效果),颜色区分区域 | map3D + visualMap 组合 |
| 省份数值 + 城市散点数量同时展示 | map3D 负责面积填色,scatter3D 叠加在 geo3D 或 map3D 之上 |
如果是那种“省份颜色不一样,同时城市上有小柱子/小球”的大屏,我会倾向于用单个chart实例同时挂载map3D(或 geo3D)和scatter3D,关键是把scatter3D.coordinateSystem指到对应的三维坐标系名称上。具体实现会在 3.2 节详细讲。
2.3 光影与交互参数调优经验
三维场景好不好看,有时候不是代码语法问题,而是光影参数没调对。这里把我的常用基线分享出来:
geo3D: { // ... shading: 'realistic', postEffect: { enable: true, SSAO: { enable: true, radius: 4, intensity: 1.2 } }, viewControl: { distance: 120, alpha: 50, beta: 0, autoRotate: true, autoRotateSpeed: 3, minAlpha: 10, maxAlpha: 90, minDistance: 60, maxDistance: 200 }, light: { main: { intensity: 1.5, shadow: true, alpha: 40, beta: 40 }, ambient: { intensity: 0.3 } } }几个参数的理解:
postEffect.SSAO:这是环境光遮蔽效果,开启后省份之间的交界处会出现自然的阴影过渡,立体感立刻上一个档次。但 SSAO 在低端显卡上会比较吃力,如果大屏是跑在普通办公电脑上,建议关掉只保留基本光照。autoRotate:很多客户就喜欢地图自动慢慢转的效果。速度默认是 4,我一般调到 3,转太快显得浮躁,转太慢又感觉不出来。light.main.alpha/beta:这是光源的俯仰角和方位角。我习惯让光源从左上角打下来,这样东边和南边的省份偏亮,西边和北边偏暗,立体感比较自然。如果觉得地图整体太暗,把ambient.intensity调高到 0.4~0.5。
这套参数直接复用到大屏里基本不会翻车,颜色可以根据项目主题微调。
3. 数据映射实战:visualMap 分段与散点标记
3.1 把 9 段图变成 10 段图——visualMap 分段机制
“9 段图变 10 段图”这个需求听起来玄乎,其实就是控制数值分区段数的问题。ECharts 官方的默认分级可能是 5 档或 6 档,但实际业务里客户需求往往很具体,比如“我要把 0~1000 的数值分成 10 个层级,每 100 一个台阶”。这就要用到visualMap的两个重要属性:splitNumber(默认等分)和pieces(自定义分段)。
如果你只想要等宽的 10 段,最省事的方式是:
visualMap: { min: 0, max: 1000, splitNumber: 10, // 等分为 10 段 inRange: { color: ['#0b1f3a', '#0e4d92', '#1b8a5a', '#42b883', '#9acd32', '#f0e442', '#f5a623', '#e24b26', '#b71d1d'] } }但很多时候数据的真实分布并不均匀。比如全国 34 个省级行政区,大多数省份数值在 50 以下,只有两三个省份冲到 900 以上。如果用等分方式,前面几个省份颜色差异极小,根本看不出层级。这时候就需要用pieces自定义分段区间:
visualMap: { type: 'piecewise', pieces: [ { min: 0, max: 100, label: '0-100' }, { min: 100, max: 200, label: '100-200' }, { min: 200, max: 300, label: '200-300' }, { min: 300, max: 400, label: '300-400' }, { min: 400, max: 500, label: '400-500' }, { min: 500, max: 600, label: '500-600' }, { min: 600, max: 700, label: '600-700' }, { min: 700, max: 800, label: '700-800' }, { min: 800, max: 900, label: '800-900' }, { min: 900, max: 1000, label: '900-1000' } ], inRange: { color: ['#313695', '#4575b4', '#74add1', '#abd9e9', '#e0f3f8', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026'] } }这就是“9 段变 10 段”背后的核心逻辑:不是改什么特殊参数,而是用pieces精确控制分段区间。默认连续型 visualMap 阈值可能是自动切的,你以为只有 9 个颜色条,其实是因为 inRange 里只给了 9 个颜色,多给一个颜色、多定义一段区间,自然就变成 10 段了。这个操作本身不难,难的是想清楚“用等分还是自定义分段”,数据分布不均时优先用pieces。
3.2 给某些市标记数量——scatter3D 实现城市散点
这是大屏里最常出现的需求:“3D 地图上,在几个重点城市位置显示指标数字或者圆点”。它的实现思路分三块:确定城市经纬度、构造 scatter3D 的数据、把数据挂到三维坐标系上。
省份的中心点可以从 china.json 的properties.center拿,但城市坐标 china.json 里没有。我一般是维护一个常用的城市坐标映射表,只挑项目需要的城市写,避免维护全量数据:
const cityCoordMap = { '北京': [116.41, 39.90], '上海': [121.47, 31.23], '广州': [113.26, 23.13], '深圳': [114.07, 22.55], '成都': [104.07, 30.57], '武汉': [114.31, 30.52], '西安': [108.94, 34.34] };散点数据格式为[经度, 纬度, 数值],第三维就是散点的高度/值。然后配置 scatter3D 系列:
const scatterData = Object.keys(cityCoordMap).map(city => ({ name: city, value: [cityCoordMap[city][0], cityCoordMap[city][1], Math.random() * 500] })); chart.setOption({ geo3D: { // 省略基础配置 }, series: [{ type: 'scatter3D', name: '城市指标', coordinateSystem: 'geo3D', data: scatterData, symbolSize: 8, itemStyle: { color: '#ffd700' }, label: { show: true, formatter: (params) => params.name, position: 'top', textStyle: { color: '#fff', fontSize: 12 } } }] });这里有一个非常容易踩的坑:coordinateSystem: 'geo3D'必须和geo3D组件的名称一致。如果你在 option 里给 geo3D 起了id: 'myGeo'或者修改默认名称,series 里的 coordinateSystem 也要改成对应的名称,否则散点数据全部飘在地图外面或者干脆不显示。
另外,散点的高度是value[2]决定的,建议把数值控制在地图厚度的量级范围内(比如 10~100),太大了散点会飞得很高,看起来像悬浮在空中的星星,而不是贴在地图表面。如果想要点状发光效果,可以加一个postEffect配合bloom,但这个比较耗性能,视项目而定。
3.3 让柱子随数值拔高——bar3D 与 regionHeight 的应用
有些需求要求城市上方立柱子,用柱子的高低来表示数量多少,这比圆点更直观。如果你用的是map3D + visualMap方案,省份颜色是映射了数值的,但 area 的高度没法自动随数值变化(regionHeight是统一厚度,不区分省份)。这时候有两种做法。
第一种是纯视觉方案:用geo3D.regions单独配置重点省份高度,比如让广东、江苏这几个数据高的省份凸起:
geo3D: { regions: [ { name: '广东', height: 12 }, { name: '江苏', height: 10 }, { name: '山东', height: 8 } ] }第二种是做“柱子”效果,用bar3D系列,数据格式和 scatter3D 类似,但每一根柱子需要手动指定经纬度和高度:
{ type: 'bar3D', coordinateSystem: 'geo3D', data: [ { name: '北京', value: [116.41, 39.90, 500] }, { name: '上海', value: [121.47, 31.23, 800] } ], barSize: 1.5, bevelSize: 0.2, itemStyle: { color: '#41a6ff', opacity: 0.9 }, label: { show: true, formatter: (params) => params.name + '\n' + params.value[2] } }这里value的第三维不是要展示的真实数值,而是柱子的高度。如果真实数值是 5000,直接填进去柱子会捅到天上去,一般要做一次线性映射,先归一化到 50~500 这个高度区间。barSize是柱子的粗细,默认值有点大,我习惯调小一点,让柱子看起来更精致。
提示:scatter3D 和 bar3D 可以同时挂载到 geo3D 上,先画散点再画柱子,也能共存。但注意图层渲染顺序,柱子太高可能会挡住散点,通常我会把散点符号调大,柱子上移一点。
4. 常见问题与排查笔记
4.1 地图不显示或一片空白
我排查过最多的就是这个问题。先说结论:80% 的情况是三件事没做好。
- 没有引入 echarts-gl。很多人只
npm install echarts,写代码时发现geo3D不生效。import 'echarts-gl'这行必须写在初始化之前。 - china.json 注册失败。
registerMap('china', mapData)里的 mapData 必须是 JSON 对象,不能是字符串。用 fetch 加载时如果忘了res.json(),会报 mapData 是 string 的错。 - 容器没有高度。ECharts 在初始化时容器必须有明确的宽高。很多组件写了个
div,CSS 里没设高度,结果地图渲染出来是 0 像素高的白屏。
给刚入门的读者一个排查顺序:先打开控制台看报错,有报错先解决报错;没有报错但空白,就用console.log(chart.getOption().geo3D)看看配置是否真的传进去;再不行就检查容器元素的实际宽高。大多数问题在这三步内都能定位。
4.2 标签显示不全/省份名称挤在一起
3D 地图的 label 渲染机制和 2D 不同,省份名称默认是贴在挤出厚度的侧面的,由于视角旋转,很多标签会被遮挡或者重叠。处理方案有几个:
- 开启
label.emphasis高亮显示,平时不显示 label,hover 到哪个省才显示哪个省的名字。这是大屏最常用的做法,既干净又避免了重叠。 - 如果要求常显,就调小字体、增大
regionHeight,给标签多留一点侧面的空间。 - 也可以给 geo3D 加
label.textStyle.fontSize并按省级名称长度做自定义 formatter,比如把“黑龙江省”改成“黑龙江”,“内蒙古自治区”改成“内蒙古”,减少标签的物理长度。
label: { show: true, formatter: (params) => { return params.name .replace('省', '') .replace('市', '') .replace('壮族自治区', '') .replace('回族自治区', '') .replace('维吾尔自治区', '') .replace('自治区', ''); } }这样处理后,标签的观感会干净很多,也不会把相邻省份的名字挤到一起。
4.3 纹理贴图与地面质感
有些主题要求地图表面有科技感纹理,比如蜂窝网格、发光线条,这其实可以用 itemStyle 的纹理图实现。ECharts GL 支持用图片作为纹理贴图,方法如下:
itemStyle: { color: '#1a2a4a', opacity: 1, borderWidth: 1, borderColor: '#5bc0de', texture: '/images/map-texture.png' }注意 texture 图片必须是方的、最好是无缝贴图,且颜色偏深,否则纹理会反客为主盖住地图边界。如果你的项目只是偶尔用一下,可以试试程序生成半透明噪点纹理;如果正式环境要求高,可以让设计师出图。纹理加完后建议把opacity调到 0.8 左右,保证省份边界能透出来。
另外,shading: 'realistic'模式下还支持colorMaterial的粗糙度和金属度设置,不过这个一般用于展示产品模型,地图场景下不用过度调,保持 lambert 就好。
4.4 性能优化与大屏适配
大屏项目普遍对性能敏感。做成 3D 后,地图旋转时每一帧都要重新计算和渲染大量顶点数据。我实际项目里的经验是:
- 如果地图不需要常驻旋转,就关掉
autoRotate,减少 GPU 负担。 - 关闭
postEffect.SSAO,这个是最耗性能的选项之一。低端机上开了 SSAO,旋转地图时简直卡成 PPT。 - 对 scatter3D 或 bar3D 的数据量做控制。城市标记一般只展示 Top 10 或 Top 20,没必要把几百个城市全画出来。
- 做大屏时,容器不要无限大。很多人的显示器是 2K 甚至 4K,图表的实际渲染分辨率过高时性能急剧下降。可以把 canvas 的 devicePixelRatio 控制在 2 左右。
还有一个很实用的适配方案:监听窗口 resize 后调用chart.resize(),同时重置viewControl.distance保证地图始终在视野正中央:
window.addEventListener('resize', () => { chart.resize(); chart.setOption({ geo3D: { viewControl: { distance: Math.min(window.innerWidth / 8, 150) } } }); });5. 工程化封装思路:从写死配置到低代码可编辑
5.1 把 3D 地图配置抽成 JSON 配置
项目做大之后,不可能每次地图需求都重新写一遍 option 代码。特别是“低代码可编辑 echarts 图表”这个需求越来越常见,前端人员总会被要求“配置项能不能让产品经理自己改”。这时候我建议把地图配置做成 JSON Schema 驱动的方式:把 geo3D 的属性、visualMap 的区间和颜色、散点城市列表全部抽成可配置项,页面根据 JSON 动态生成 option。
const mapConfig = { mapName: 'china', regionHeight: 5, visualMap: { type: 'piecewise', pieces: [], colors: [] }, scatterCities: [], autoRotate: true, shading: 'lambert' }; function buildOption(config) { const geo3D = { map: config.mapName, regionHeight: config.regionHeight // 根据 config 动态填充其他属性 }; // 拼装 series return { geo3D, series }; }这样配置与渲染逻辑分离,后续接可视化编辑面板就很容易了。我自己用的一个做法是:后台存一份 config 对象,前端编辑面板直接改 config,保存后通过 presets 下发。产品想调整颜色分区、城市列表,都不需要前端发版,体验完全就是低代码的感觉。
5.2 我踩过的最后一个坑:组件化时的实例管理
如果你用 Vue 或 React 封装 3D 地图组件,有一个特别容易翻车的地方:组件卸载时没有销毁 ECharts 实例。
// Vue 组件示例 beforeUnmount() { if (this.chart) { this.chart.dispose(); this.chart = null; } }原因很简单:echarts-gl 的 3D 渲染会占用 WebGL 上下文,如果组件多次挂载卸载却不 dispose,浏览器上下文耗尽就会报Too many active WebGL contexts,然后整个页面的 3D 组件全部白屏。这个问题在开发环境不常出现,因为页面很少刷新,但做后台管理系统或者低代码拖拽编辑时会频繁触发,务必养成组件销毁时 dispose 的习惯。
另外,如果有多个地图实例同时在页面展示,尽量用echarts.init时传入不同的 dom 容器,避免实例相互干扰。3D 地图不比 2D 图表,底层共享同一套 GL 渲染管线,实例多了以后内存占用和帧率都会受影响。
最后分享一条个人经验:3D 地图再炫,也是为业务数据服务的。我见过很多项目单纯追求地图好看,结果数据信息反而看不清,那就本末倒置了。在动手做之前,先想清楚客户要看什么:是省份总量、城市分布,还是趋势变化?明确了要表达的数据,再决定用 geo3D 还是 map3D、用散点还是柱子。这个思考过程比调任何参数都重要。希望这篇能帮你少走点弯路,有问题欢迎在评论区交流。
本文还有配套的精品资源,点击获取