先说结论:echartsGL(也就是echarts-gl)是一套能让ECharts跑在WebGL上的扩展库,3D地图渲染是它最拿手的场景之一。它不需要你懂任何图形学基础,只要会写JSON格式的option配置,就能把一块普通平面地图变成可旋转、可缩放、带光照和纹理的立体地形。这篇文章我会从选型逻辑、环境搭建、配置项拆解、进阶玩法到坑位排查,完整记录我实际落地这类项目的过程,适合刚接触3D可视化的前端开发者、数据可视化工程师,以及想在业务里快速加一个3D地图看板的同学。
1. 为什么选echartsGL做3D地图渲染
1.1 对比Three.js、Mapbox和Cesium的真实体验
我最早做3D地图时,第一反应是在Three.js里直接用GeoJSON拉伸生成几何体。这条路能走通,但代价很高:需要自己处理地图投影坐标、经纬度转三维坐标、光照计算、相机轨道控制,还要维护着色器。做出来一个能看的东西,前前后后花了将近两周,而且后续加一个下钻、加一个飞线,又得重新折腾一套逻辑。
Mapbox和Cesium更偏向GIS场景,确实强大,但它们对地图数据源有强依赖,尤其在国内业务里还要考虑底图服务、Token配额、离线部署这些问题。对很多纯前端团队来说,引入一个完整GIS引擎来画一张数据看板,属于杀鸡用了牛刀。
echartsGL的方案本质是:在ECharts已有的GeoJSON地图注册机制之上,用WebGL做渲染层。地图的数据源还是普通的GeoJSON,配置结构还是你熟悉的series、visualMap、tooltip那一套。也就是说,如果你已经写过ECharts的2D地图,迁移到3D地图的学习成本几乎为零,只需要把series类型换成map3D或geo3D,再补几个3D特有的参数就行。
1.2 echartsGL的渲染原理
echartsGL底层是基于qtek这个WebGL图形库封装的,它会把ECharts里传入的地图几何数据在GPU上构建出三维网格。每个地图区块(比如省、市)都是一个独立的三维几何体,通过相机投射变换呈现在Canvas上。
这里有一个关键概念叫坐标转换。2D地图用的是经纬度映射到平面坐标,3D地图则是把经纬度映射到球面或平面网格顶点,再加上一个z轴高度。echartsGL的map3D默认是按平面方式拉伸,所以你会看到地图像一块浮起来的浮雕板。而geo3D可以配合球面参数,做成一个完整的3D地球。
我对普通读者解释这个原理时,喜欢用一个类比:把地图想象成一张印着省份边界的橡皮膜。2D渲染是把它平铺在桌上,3D渲染则是把这张膜从四角拎起来,每个省份按数据大小充气鼓包,你可以绕着这张鼓起来的膜随便转着看。理解了这个,你就明白boxHeight和regions的height参数到底在控制什么了。
1.3 什么场景才值得上3D地图
不是所有地图可视化都要3D,我自己在项目里总结的评判标准是:如果数据指标带有空间分布属性,并且用户有探索式查看需求,3D才有价值。比如某款产品的全国各城市销量分布,用3D柱条高度映射数值,一眼就能看出哪些区域是高点;又比如某个园区设备的实时状态分布,用3D散点加颜色区分正常和告警,立体感的视觉冲击力远好于平面气泡图。
反之,如果只是展示各省份的静态排行,老老实实做一个2D choropleth地图加一个排名表格,反而加载更快、阅读更直接。3D不是万能的,它是用来放大视觉差异和空间感的工具,是个放大器,不是转换器。
2. 环境准备:依赖版本和地图数据源
2.1 版本搭配与引入方式
写这部分的时候我特意查了下目前稳定的版本组合。ECharts 5.x需要搭配echarts-gl 2.x,ECharts 4.x则搭配echarts-gl 1.x。如果版本配错,最常见的表现是注册地图后页面白屏、报找不到registerMap方法,或者series类型直接不认。
# 使用npm安装 npm install echarts@5 echarts-gl@2如果你不想折腾打包工具,直接CDN引入也可以:
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/echarts-gl@2/dist/echarts-gl.min.js"></script>注意:CDN方式必须先引入echarts,再引入echarts-gl,顺序颠倒会直接报错。另外建议固定版本号,不要用
latest,否则生产环境某天突然升级容易出问题。
引入echarts-gl之后,ECharts会自动注册一堆新的series类型,包括map3D、scatter3D、bar3D、lines3D、surface等。你不需要额外写任何WebGL初始化代码,这是这套方案最舒服的地方。
2.2 GeoJSON地图数据从哪来
3D地图渲染的地图轮廓和板块划分完全依赖GeoJSON。国内做项目最常见的数据来源是阿里云DataV的GeoAtlas,它提供了全国、各省、市、区的GeoJSON数据,并且是免费公开的。不过在实际项目中我不建议每次都在线拉取,GeoJSON文件体积不小,全国级别的大几MB,放线上会拖慢首屏。更稳妥的做法是:第一次开发时把需要的GeoJSON下载到本地,作为静态资源放项目里,甚至用JSON压缩之后按需加载。
还有一个容易被忽略的坑:GeoJSON里的坐标精度和坐标系。echartsGL和ECharts一样,直接支持GeoJSON规范里的[经度, 纬度]结构,如果数据源是其他坐标系(比如火星坐标GCJ-02),要先用工具转换到标准经纬度再渲染,否则地图会出现几公里级的偏移。
2.3 验证GeoJSON是否能用
拿到一份GeoJSON之后,我建议先不急着上3D配置,先用一个最简单的地图验证数据有效性:
fetch('./geo/china.json') .then(res => res.json()) .then(geoJson => { echarts.registerMap('china', geoJson); const chart = echarts.init(document.getElementById('map')); chart.setOption({ series: [{ type: 'map3D', map: 'china' }] }); });如果这个能显示出一块立体地图轮廓,数据就没问题。如果显示空白,打开浏览器的Network面板看fetch是否成功,再在Console里输出解析后的geoJson,检查features数组是否存在。这是排查一切地图显示问题的第一步,后面遇到的怪问题,十有八九都是数据没注册成功。
3. 核心配置项深度拆解:从0到1渲染3D地图
3.1 map3D和geo3D怎么选
这两个series类型经常把人搞晕,我直接说结论。geo3D是独立的三维地理坐标系组件,它本身不直接绑定地图数据系列,需要配合scatter3D、bar3D等系列在里面展示数据,适合做3D地球、球面散点这类需要在地理坐标上自由放置元素的场景。map3D则是直接把注册好的地图GeoJSON渲染成一个三维块状区域,每块区域通过regions单独配置,做省市级别的区域分布图更直接。
实际开发中,我90%的项目用的是map3D。因为它开箱即用,数据直接通过data数组按区域名映射,配合visualMap就能上色。geo3D更适合做那种带飞线、带动态散点的复杂场景,但配置复杂度明显高一个档次,新手没必要一上来就碰。
3.2 一个最小可用配置
下面这个配置是能直接运行的,渲染一个3D中国地图,并且根据数值上色:
const chart = echarts.init(document.getElementById('container')); echarts.registerMap('china', geoJson); const option = { tooltip: {}, visualMap: { min: 0, max: 1000, calculable: true, inRange: { color: ['#313695', '#4575b4', '#74add1', '#abd9e9', '#e0f3f8'] } }, series: [{ type: 'map3D', map: 'china', data: [ { name: '广东', value: 100 }, { name: '浙江', value: 200 } ], boxHeight: 10, shading: 'lambert', light: { main: { intensity: 2, shadow: true, alpha: 40, beta: 20 }, ambient: { intensity: 0.3 } }, viewControl: { distance: 100, alpha: 40, beta: 0, autoRotate: false } }] }; chart.setOption(option);这里面的映射逻辑和2D地图一样,name对应GeoJSON里features属性的name字段,value用于visualMap着色。配置生效后,广东和浙江两省会被渲染成不同颜色的立体块,其他省份也有默认显示,只是颜色接近最低值。
3.3 viewControl:控制相机视角的关键参数
viewControl是map3D里最影响交互体验的参数,它控制的是三维场景中相机的观察位置,直接决定用户打开页面第一眼看到的是什么。我常用的参数有这几个:
alpha:俯仰角,范围0到90左右。0表示平视,90表示完全俯视。业务看板默认建议40到60度,既能看到立体感,又能看到地图全貌。beta:水平旋转角。0表示正北方向朝上,如果地图初始朝向不正,可以调这个值。distance:相机距离,数值越大画面越远。地图大小不同,合适的distance也不同,我一般先设100,然后根据实际效果微调。autoRotate:是否自动旋转,适合展厅大屏,轮播展示时有效果,但交互式分析场景建议关闭,否则用户鼠标操作时会有干扰。animationDurationUpdate:从2D切换到3D或地图下钻时,视角过渡动画的时长。放大了看这个参数很影响体验,设个500到1000毫秒会比较顺滑。
相机视角调整有一个实操经验:开发时先打开autoRotate: true让地图慢慢转,同时右手拖拽找到你最满意的角度,然后Console里输出当前相机的alpha和beta数值,把它抄回配置里,再把autoRotate关掉。这样能省去反复刷新页面的时间。
3.4 光照和材质:决定3D氛围感的关键一步
很多人的3D地图一眼看着假,问题多半出在光照上。echartsGL支持三种着色方式,在series里用shading字段控制:lambert是漫反射材质,表面看起来柔和,最适合默认场景;realistic是物理材质,支持反射和环境贴图,视觉效果最强但对硬件要求高;color是无光照的纯色模式,适合轻量级场景。
光照配置写在light下面,主要分main主光源和ambient环境光。我习惯直接把main.intensity设到2左右,ambient.intensity设在0.3到0.5之间。如果你发现地图整体发灰、没有立体感,大概率是环境光太亮、主光源太弱。调整办法很朴素——把环境光调低,主光调强,再看阴影有没有出来。
还有postEffect后处理。开启之后可以加环境光遮蔽(SSAO)、泛光(Bloom)、景深等特效。这个不是必须的,但如果你想把3D地图做得有质感,postEffect.enable: true加上bloom.enable: true,再把明亮度调低一点,出来的效果非常能唬人。
3.5 regions:单块区域的独立控制
regions可以在series层面单独调整某个区域的样式,不依赖外层统一配置。比如你想把某个重点省份提亮,或者把某个区域隐藏掉,就靠它。
regions: [{ name: '河南', itemStyle: { color: '#ff4d4f', opacity: 0.8 } }]这里有个实用性非常强的注意点:换地图数据源时,区域name的命名必须和GeoJSON里的properties.name完全一致,否则该区域样式不会生效。遇到过把浙江写成浙江省的,排查半天才发现是数据name对不上。
4. 实操进阶:3D地图上的数据可视化组合玩法
4.1 在3D地图上叠加柱体和散点
一个纯粹的地图只有区域色块,如果数据是点状分布或者要跨区域对比,就得叠加bar3D和scatter3D。echartsGL允许在一个option里同时存在多个3D系列,它们会共享同一个geo3D坐标系。
一个典型的叠加散点场景:
series: [{ type: 'map3D', map: 'china' }, { type: 'scatter3D', coordinateSystem: 'geo3D', data: [ { name: '北京', value: [116.46, 39.92, 100] }, { name: '上海', value: [121.48, 31.22, 80] } ], symbolSize: 5, itemStyle: { color: '#ffd700' } }]注意value的结构是[经度, 纬度, 数值],第三维数值可以决定点的大小或颜色。symbolSize控制的是最大尺寸,实际每个点会按数值比例缩放。
如果你用的是map3D并且想叠加点数据,ECharts会默认创建对应的geo3D坐标系,所以配置里只要写上coordinateSystem: 'geo3D'就能对上。这个细节我是实测确认过的,不写会报找不到坐标系。
4.2 lines3D实现飞线效果
飞线效果是3D地图里最出彩的玩法之一。比如展示城市之间的客流、物流、数据流向,用lines3D从起点飞向终点,加上尾迹特效,视觉上非常有冲击力。
series: [{ type: 'lines3D', coordinateSystem: 'geo3D', data: [{ coords: [ [116.46, 39.92], [121.48, 31.22] ] }], effect: { show: true, trailWidth: 4, trailLength: 0.5, trailOpacity: 1, trailColor: '#ffd700' }, lineStyle: { color: '#ffd700', width: 1 } }]trailLength代表尾巴的长度比例,0到1之间。我建议从0.4试起,太短没有流动感,太长会糊成一条光带。飞线的曲线高度是自动计算的,不需要额外指定,它会根据地面上两点的距离自动抬高弧度。
注意:飞线数据超过100条后,渲染压力会明显上升。优化思路是减少
trailLength、降低trailWidth,或者用progressive渐进渲染参数,让图表的加载过程更平滑。
4.3 事件交互与图表联动
3D地图的交互不只是旋转缩放,它照样支持所有ECharts事件。我项目里最常用的两个是click和dblclick,分别用来做下钻和区域数据联动。
chart.on('click', function (params) { if (params.componentType === 'series' && params.seriesType === 'map3D') { // params.name 就是点击的区域名 console.log('点击了', params.name, params.value); } });联动其他图表的方式和2D没有区别,拿到params.name之后,主动去setOption更新旁边的柱状图、折线图的数据源。这个设计让我可以把3D地图当作一个全局筛选器,点哪个省份,其他模块就跟着切换。结合Vue或React,你甚至可以把这个点击事件派发到组件外部的全局状态里,实现跨页面的联动。
4.4 大屏场景的几个性能优化手段
大屏项目里3D地图最大的痛点是性能。我的经验是把优化分成三层:
第一层是数据层面。GeoJSON文件动辄几MB,建议在构建时用JSON.stringify压缩,或者使用TopoJSON格式减少顶点数。DataV的GeoAtlas提供了低精度版本,性能要求高的时候直接用低精度。
第二层是渲染层面。把viewControl.autoRotate关掉,减少连续渲染;把postEffect里的Bloom关掉或把采样率调低;把shading从realistic换成lambert。这些操作能肉眼可见地提升帧率。
第三层是生命周期层面。3D图表销毁时要调用chart.dispose(),释放GPU资源。如果你在单页应用里反复切换路由,不释放就会累积内存,最后整个页面卡死。这个问题我踩过好几次,每次都是切了十几次页面之后突然白屏,吓得以为代码写错了。
5. 常见问题与排查技巧实录
5.1 问题速查表
下面这张表是我在实际项目中遇到频率最高的问题,基本涵盖了90%的初学者卡点。
| 症状 | 可能原因 | 解法 |
|---|---|---|
| 地图完全不显示 | GeoJSON未注册或fetch失败 | 先console.log(geoJson)确认数据存在,再echarts.registerMap,最后检查series.map名是否一致 |
| 地图显示但颜色全部一样 | visualMap的min/max与数据范围不匹配,或数据name与GeoJSON不匹配 | 打印params.name对比GeoJSON里的properties.name,把visualMap范围调宽 |
| 页面白屏并报WebGL相关错误 | 浏览器或显卡不支持WebGL | 用webglreport检查浏览器支持情况,降级为2D地图 |
| 3D地图非常卡顿 | GeoJSON顶点过多、postEffect全开、无dispose释放 | 换低精度GeoJSON,关闭后处理特效,组件卸载时dispose |
| 光照下地图很暗看不清 | 主光源强度不足或环境光过高 | 提升light.main.intensity到2以上,light.ambient.intensity降到0.3以下 |
| 点击事件无法捕获 | 事件绑定时机或事件参数判断错误 | 确认chart.on在setOption之后绑定,并检查params.seriesType是否为map3D |
| 地图倾斜后文字看不清 | label跟随视角旋转导致遮挡 | 给label设置textStyle的distance,或调整viewControl.alpha让视角更垂直 |
| 使用真实纹理时图片加载不显示 | 图片路径错误或CORS跨域限制 | 将图片转成base64或放到同域静态资源下 |
5.2 地图偏移和边界线异常的排查
地图偏移是我遇到过最隐蔽的问题。有一次渲染某市地图时,地图整体往东北方向偏移了几百米,乍一看还以为是正常的。排查后发现是GeoJSON本身用的是GCJ-02坐标系,不是WGS-84标准经纬度。解决方案是在数据源侧统一转成WGS-84,网上有公开的坐标转换公式和工具库,转完之后视觉上就完全对上了。
还有一个场景是边界线断线。这通常不是配置问题,而是GeoJSON里的MultiPolygon类型解析异常。ECharts和echartsGL对GeoJSON的Polygon支持最稳定,遇到特殊类型时,可以在数据处理环节先用库把Geometry格式归一化一遍,再注册。
5.3 关于3D地图审美的一点个人建议
这部分内容不在任何官方文档里,但我觉得比很多API细节更重要。我见过太多3D地图做得五颜六色、光照过度、阴影乱飞,最后整个页面像中了毒一样。我的审美建议是:配色控制在两个主色系内,一个用于常规区域,一个用于高亮区域;visualMap的颜色渐变用单一色相的明度变化,比如深蓝到浅蓝,比红蓝绿彩虹渐变高级得多。阴影强度不要追求夸张,能看出高度落差就足够了。光照类型优先lambert,realistic那套物理材质留给产品需要炫技的场合。
说到底,3D地图的渲染技术只是手段,用户真正关心的是能不能从图里快速读出业务答案。技术可以做加法,但审美和数据叙事能力得做减法。
最后分享一个我常用的快速调试技巧
每次做新的3D地图,我不会一上来就套完整业务数据。我会先固定三段式写法:先注册地图、渲染一个只有map3D的骨架;再把visualMap加上,用假数据试颜色;最后加业务数据和交互。每次只动一个变量,出了问题马上知道是哪一层。这样调试下来,最复杂的3D地图项目也能控制在一天内完成,大头时间反而花在GeoJSON数据清洗和业务指标设计上。这个习惯我从第一次做3D地图起就用,一直用到现在,不管后续换什么框架,排查思路都跑不出这个圈子。