ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue+ECharts-GL实现可交互3D中国地图大屏:5层叠加可视化方案

Vue+ECharts-GL实现可交互3D中国地图大屏:5层叠加可视化方案 做可视化大屏这几年3D地图基本是每次汇报的保留节目。后台一上数据大屏上一投射领导第一眼注意到的永远是那个带立体轮廓、能旋转、能下钻的中国地图。但说实话很多3D地图方案要么引入Three.js这种重型引擎学习成本直接拉满要么用现成的大屏模板改个颜色都费劲更别说跟自己的业务数据联动。这个项目的思路很直接用Vue做壳ECharts-GL做3D渲染核心把一张普通中国地图拆成5个层叠的视觉层次——立体地图主体、散点分布、柱状统计、飞线轨迹、涟漪标注。每一层负责一个维度的信息表达叠在一起既能看整体又不丢细节。本文会从头到尾把这套5层叠加方案拆开讲清楚包含完整可跑的Vue组件代码和每个参数的含义适合正在做大屏可视化、数据展示或者想在项目里快速落地3D地图但不想被WebGL复杂度劝退的同学参考。1. 项目概述与整体设计思路1.1 这个项目要解决什么问题做数据可视化2D地图的局限其实很明显平面地图表达不了数值高低只能靠颜色深浅硬顶色阶稍微一多就分不清。而3D地图天然多了一个高度维度柱子能长出来飞线能连过去城市的分布能立体呈现数据越多层次越清晰。这套方案的定位很明确不炫技、不出格用最常规的技术栈组合实现一套看着有冲击力、开发成本可控、又能灵活对接业务数据的中国地图可视化底座。整个项目跑下来一张地图上能同时承载五类信息地域轮廓、点位分布、数据大小、联动关系、重点标注。做运营大屏、物流调度中心、销售看板这套底层逻辑都是通用的。1.2 为什么选 ECharts-GL 而不是 Three.js先说结论如果你的需求是要3D效果但团队没有WebGL基础项目排期也不允许从零造轮子ECharts-GL 就是最务实的答案。Three.js 确实更强能实现光照散射、物理材质、镜头粒子这些电影级效果但代价是你要从头搭建场景、相机、灯光、渲染循环还得自己处理地图数据到3D坐标的映射。一套流程下来光是把中国地图的边界变成3D几何体就要折腾好几天。ECharts-GL 的思路完全不同它是在 ECharts 配置项体系上扩展了3D能力你写的还是 series、xAxis、legend 那套老伙计只是把 type 换成 map3D、scatter3D。地图的 GeoJSON 数据格式可以直接复用坐标系自动帮你映射光照、视角、旋转全都封装成了配置项真正做到了配置驱动。有人说 ECharts-GL 远看是3D近看细节不够。这话对但场景也得分。大屏上地图普遍是从3到5米的距离观看要的是整体轮廓、颜色区块、动效指引没有人会趴上去看贴图纹理。用40%的工夫达到80%的效果这个账怎么算都划算。1.3 5层叠加的完整结构整张地图从下往上我设计了五个独立的视觉层次每一层对应一个 series 配置逻辑上完全解耦第1层3D地图主体。用 map3D 绘制立体省份轮廓是整张图的底盘所有其他图层都依附在这个坐标系上。第2层散点分布层。用 scatter3D 在地图上的指定经纬度打点适合标注分公司、门店、站点这类离散点位。第3层柱状统计层。用 bar3D 在对应城市位置生成高度不一的立体柱子柱子越高数值越大直观表达区域间的数据对比。第4层飞线联通层。用 lines3D 在两个城市之间画弧线并配合拖尾动画模拟数据流动方向。第5层涟漪标注层。选中几个重点城市用动态放大的半透明球体做脉冲式的点击提醒视觉效果类似地图 App 的定位标识。分层的核心价值在于业务数据变化时你只需要更新对应 layer 的数据源其他层完全不动。比如物流公司只看飞线层的数据把轨迹接口换掉即可地图主体和散点层一丝都不用碰。这就是解耦带来的维护便利。2. 环境搭建与地图数据准备2.1 Vue 项目创建与依赖安装我这里的项目用的是 Vue 3 Vite比 Vue CLI 轻量很多启动速度也快。如果你还在 Vue 2 项目里思路是一样的只是组件写法需要换成 options API。# 创建 Vue 项目 npm create vitelatest china-map-gel -- --template vue # 进入项目 cd china-map-gel # 安装所需依赖 npm install echarts echarts-gl装完依赖之后在 main.js 里做全局注册比较省事。注意 echarts 和 echarts-gl 都要引入gl 模块是通过副作用方式把 3D 系列注册进 echarts 的顺序不能反否则会报错说找不到 map3D 这个系列。// main.js import { createApp } from vue import * as echarts from echarts import echarts-gl import App from ./App.vue const app createApp(App) app.config.globalProperties.$echarts echarts app.mount(#app)踩坑提示echarts-gl 对 echarts 版本有兼容要求。我之前遇到过一次图表白屏排查半天发现是 echarts 升到了 6.x而 echarts-gl 还在适配旧的 API。建议锁定 echarts5.x 版本等 echarts-gl 官方明确支持 6.x 再升级。2.2 地图 GeoJSON 数据的获取ECharts 5 之后不再内置地图数据需要自己准备 GeoJSON 并注册。中国地图的 GeoJSON 有两个常见来源阿里云 DataV 的 GeoJSON 数据接口以及各省市的地理信息公共服务平台。DataV 的那个接口可以直接按需取全国或者单个省份的地图数据日常开发完全够用。拿到地图 JSON 之后在组件里注册// 以本地资源为例你也可以在 onMounted 里 fetch 远程 JSON import chinaJson from /assets/china.json echarts.registerMap(china, chinaJson)有一点要提醒 GeoJSON 文件省市区县级别的数据体积会膨胀得很快全国地图精简过的大概在几百 KB但全国到县级的完整数据可能好几兆字节。大屏项目首次加载如果卡顿大概率是地图数据拖了后腿。建议开发阶段用全量数据上线前置换成用 TopoJSON 简化过的版本或者自己写脚本抽稀坐标点。2.3 组件目录结构为了后面扩展方便我把地图功能拆分成了独立组件不是一把梭全写在 App.vue 里src/ ├── components/ │ ├── ChinaMap3D.vue // 3D 地图主组件 │ ├── layers/ │ │ ├── useMapLayer.js // 第1层地图主体 │ │ ├── useScatterLayer.js // 第2层散点分布 │ │ ├── useBarLayer.js // 第3层柱状统计 │ │ ├── useLinesLayer.js // 第4层飞线轨迹 │ │ └── useRippleLayer.js // 第5层涟漪标注 │ └── config/ │ ├── chartTheme.js // 颜色主题、光照参数 │ └── mapData.js // 各层的测试数据 ├── assets/ │ └── china.json // 中国地图 GeoJSON └── App.vue每个 use 开头的 js 文件导出的是一个配置对象或配置函数最终在 ChinaMap3D.vue 里把五块配置合并进一个 option。这种结构最直接的收益是你改柱子颜色时不会误碰飞线的配置排查问题的时候思路也清晰——出问题先定位是哪个 layer。3. 五层叠加逐层代码实现3.1 第1层3D 地图主体打好模型的底子地图主体是整个方案的地基后续四层全部要依赖同一个 geo3D 坐标系。我建议用 geo3D 组件统一管理地图坐标系这样散点层、柱状层、飞线层可以通过coordinateSystem: geo3D挂靠到同一张地图上位置天然对齐。// useMapLayer.js export const mapLayerOption { geo3D: { map: china, roam: true, regionHeight: 4, // 每个省份弹出的高度 itemStyle: { color: #1a5f8a, opacity: 0.9, borderWidth: 1.5, borderColor: #7ccfff }, label: { show: true, textStyle: { color: #b3e5ff, fontSize: 10 } }, emphasis: { label: { show: true, distance: 4, textStyle: { color: #ffffff, fontSize: 14 } }, itemStyle: { color: #2fc9ff, opacity: 1 } }, shading: lambert, light: { main: { color: #ffffff, intensity: 1.2, shadow: true }, ambient: { intensity: 0.3 } }, viewControl: { distance: 110, alpha: 40, beta: 0, center: [0, 0, 0], autoRotate: false } }, series: [ { type: map3D, map: china, coordinateSystem: geo3D } ] }参数说明几个重点。regionHeight是每个省从地图表面长出来的高度值越大立体感越强但超过 8 之后反而像一堆积木不真实。shading: lambert选择兰伯特光照模型效果是平面会随着光线的角度产生明暗变化——这是立体感的核心来源。想让受光面更亮、背光面更暗就调高主光源 intensity同时用 ambient 环境光补暗部细节避免死黑一片。地图主体配好之后下面每一层都是在 viewControl 同一个视角系统下叠加内容。这里最让我意外的是很多照着文档抄的开发者第一眼看到的是地图是出来了但为什么转起来像纸片。原因基本就在光照参数上环境光开太高立体感全被冲淡了。3.2 第2层散点层经纬度定位的精准打点散点层用来标注点位分公司、门店、物流仓、基站只要是能给出经纬度的数据它都能打在地图对应的位置。散点层的配置用 scatter3D关键是coordinateSystem: geo3D和它对齐到同一张地图。// useScatterLayer.js export const scatterData [ { name: 北京, value: [116.4074, 39.9042, 30] }, { name: 上海, value: [121.4737, 31.2304, 42] }, { name: 广州, value: [113.2644, 23.1291, 18] }, { name: 成都, value: [104.0665, 30.5723, 26] }, { name: 武汉, value: [114.3054, 30.5931, 15] } ] export function scatterLayerOption(data) { return { series: [ { type: scatter3D, name: 业务网点, coordinateSystem: geo3D, data: data.map(item ({ name: item.name, value: item.value })), symbol: circle, symbolSize: 8, itemStyle: { color: #ffd166, opacity: 0.9 } } ] } }散点数据里value是个三维数组前两位是经度和纬度第三位是这个点的高度。高度填 0 会让点贴在地图表面但有时候点在凹陷的省份边界里会被遮住这种时候可以把第三位抬高一些比如放到 2~5 之间点会浮在地图上方视觉上更清爽。散点的大小、颜色可以跟业务数值挂钩。销售业绩高的省份打大点用亮暖色业绩低的打小点用冷色。ECharts 的visualMap组件能干这件事。实际操作中比起让视觉编码完全服从数据精度更重要的是保证大屏上看得清、分得开颜色对比拉得开往往比精确映射重要。3.3 第3层柱状层数据大小一眼见分晓柱状层是5层里信息量最大、视觉冲击力最强的一层它回答的问题是哪个区域数值高、哪个区域数值低。用 bar3D 实现每个柱子底部锚定在经纬度上高度映射到数值。// useBarLayer.js export const barData [ { name: 北京, coords: [116.4074, 39.9042], value: 86 }, { name: 上海, coords: [121.4737, 31.2304], value: 92 }, { name: 广东, coords: [113.2644, 23.1291], value: 74 }, { name: 四川, coords: [104.0665, 30.5723], value: 53 }, { name: 湖北, coords: [114.3054, 30.5931], value: 47 } ] export function barLayerOption(data) { return { series: [ { type: bar3D, name: 业务规模, coordinateSystem: geo3D, data: data.map(item [...item.coords, item.value]), barSize: 1.5, bevelSize: 0.2, bevelSmoothness: 8, minHeight: 2, itemStyle: { color: #3ac8e8, opacity: 0.85 }, emphasis: { itemStyle: { color: #ffd166 } } } ] } }bar3D 的数据格式是[经度, 纬度, 柱高]。这里有个特别重要的参数是minHeight如果不设置它数值接近 0 的柱子会压缩成纸片地图转起来不够立体底部反而不跟地图贴合很出戏。我一般会把 minHeight 定在 2 左右保证最矮的柱子也有足够的视觉厚度。bevelSize和bevelSmoothness控制柱子的倒角大小和顺滑程度。倒角越大柱子越圆润看起来越有质感但性能也会略微下降。大屏上有几百根柱子的时候bevelSmoothness 建议 4 以内没必要追求完美的倒角3 米外没人看得出区别。柱子叠上地图后最常见的视觉冲突是五根柱子挤在华东这么小一块区域内互相遮挡严重。解法有两个一是把对应省份的点位在经纬度上做微小的偏移让柱子错开一点二是调低柱子的透明度比如 0.7 左右相互遮挡时还能透出后面的柱身视觉上不那么堵。3.4 第4层飞线层流动轨迹讲清联动关系飞线层负责表达城市之间的流动关系——可能是物流线路、资金流向、用户来源、数据传输路径任何一对多、多对多的业务关系都能用飞线画出来。// useLinesLayer.js export const linesData [ { coords: [ [116.4074, 39.9042], [121.4737, 31.2304] ] }, { coords: [ [116.4074, 39.9042], [113.2644, 23.1291] ] } ] export function linesLayerOption(data) { return { series: [ { type: lines3D, name: 数据流动, coordinateSystem: geo3D, data: data, effect: { show: true, period: 4, trailWidth: 3, trailLength: 0.5, trailOpacity: 0.8 }, lineStyle: { width: 1.5, color: #7ef0ff, opacity: 0.4 } } ] } }飞线的关键在effect配置。trailWidth 是拖尾亮带的宽度太宽会糊2~4 比较合适trailLength 是拖尾长度0 到 1 之间0.5 意味着粒子从起点飞到终点时身后留下一半长度的光带period 是粒子飞完全程的秒数4 比较适中太快看不清轨迹太慢显得迟钝。如果想让飞线呈现出性感的弧线效果ECharts-GL 原生 lines3D 对曲线支持比较有限我自己用下来是两条路一是升级到新版 echarts-gl 看是否支持lineStyle.curveness二是自己用二次贝塞尔曲线公式把直线路径插值成一串中间高、两端低的坐标点再把插值后的点传给 lines3D。第二种方式自由度更高弧线的高度可以按业务含义控制比如量大的走高空量小的走低空。3.5 第5层涟漪标注层重点城市要会自己喊话第5层是对重点城市的强化标注视觉上做的是地图 App 定位图标的脉冲效果。ECharts-GL 原生没有现成的涟漪3D组件但有个讨巧的办法准备多个大小不同的球体通过定时器循环切换它们的透明度、半径和高度模拟出由小到大、由实到虚的呼吸感。// useRippleLayer.js export const rippleOptions (rippleIndex) { const city [ { name: 北京, coords: [116.4074, 39.9042] }, { name: 上海, coords: [121.4737, 31.2304] } ] const radius 4 rippleIndex * 2 const opacity Math.max(0, 0.8 - rippleIndex * 0.2) return { series: city.map(c ({ type: scatter3D, name: c.name, coordinateSystem: geo3D, data: [{ value: [...c.coords, 3] }], symbol: circle, symbolSize: radius * 2, itemStyle: { color: #ff6b6b, opacity: opacity } })) } }通过 setInterval 每 200 毫秒更新一次 rippleIndex让它从 1 跑到 5 再回到 1就能看到一圈一圈外扩、淡出的脉冲球。这个动效在动线上很容易抓住用户眼球但不能加太多——全地图20个城市一起脉冲就变成迪厅了选两到三个最重要的业务重心即可。这里还有一个实用技巧涟漪层的 pulse 更新频率不需要很高6 到 8 fps 就足够让肉眼觉得流畅CPU 和内存占用会明显下降。大屏项目经常 7x24 小时开机跑着性能开销能省一点是一点。4. 交互联动与视觉动效4.1 点击地图实现下钻省份一个做成了大屏的地图如果没有下钻能力给人的感觉就是一个3D雕塑缺乏可操作感。下钻的思路是监听 map3D 的 click 事件拿到被点击省份的 name然后重新 registerMap 对应的省份 GeoJSON更新 option 里 map 字段为china/${provinceName}。这在数据上是要准备两级 GeoJSON 的全国一份各省一份。chart.on(click, (params) { if (params.seriesType map3D params.name) { const provinceName params.name // fetch 省份 GeoJSON 并注册 fetch(/geo/${provinceName}.json).then(res res.json()).then(geo { echarts.registerMap(province, geo) chart.setOption({ geo3D: { map: province, regionHeight: 3 }, series: [ { type: map3D, map: province, coordinateSystem: geo3D } ] }) }) } })下钻有几个容易翻车的点。第一点击时 map3D、scatter3D、bar3D 都会触发 click 事件要先用 seriesType 过滤否则点柱子也会触发下钻体验很怪。第二下钻后要提供返回按钮把 mouse 事件绑到一个自定义返回控制器上重新 setOption 回全国地图。第三下钻后原飞线、散点数据必须全部清掉否则残留在不存在的坐标系上渲染会报错或者出现错位。4.2 Tooltip 的信息层级和遮罩问题3D 地图的 tooltip 跟 2D 不一样地图是可以旋转的tooltip 的定位如果固定挂在屏幕某个位置旋转时它会孤零零地悬在地图外。实际项目里我一般不依赖 echarts 内置 tooltip而是改成自绘 HTML 的浮层。这样处理有个额外的好处你可以完全自定义浮层里显示的内容、图表和按钮不受 formatter 返回字符串的限制。比如点击或 hover 某个省份时旁边可以弹出一个小小的 sparkline 折线图展示近6个月的趋势这是原生 tooltip 很难优雅实现的。自绘浮层要注意的是定位逻辑监听鼠标 move 事件通过chart.convertToPixel({ seriesIndex: 0 }, params.value)换算屏幕坐标再根据视口边缘动态翻转浮层的位置防止它挤出屏幕外被裁掉。4.3 视角控制和自动旋转的平衡默认地图刚加载时如果直接立着不动冲击力有限我觉得加一个自动旋转会好很多。viewControl 里的autoRotate: true会让地图绕 Y 轴缓慢自转配合autoRotateSpeed: 8控制转速再加载完数据后让视觉自动扫一圈特别适合汇报开场。但长期开 autoRotate 会有一个问题用户想仔细看某一个区域时地图一直在自己转交互体验会变得烦躁。所以我的做法是加载之后自动旋转 6 秒然后自动关闭用户一旦开始拖拽地图就立刻停掉自动旋转。这个在 init 后 setTimeout 里 setOption 即可实现整体动效由动转静节奏感也更好。5. 常见问题与性能优化实录5.1 地图卡顿和首屏加载慢大屏项目最尴尬的一刻是地图数据还没 load 完领导已经盯着屏幕看空气了。性能我从三个方向去优化。第一地图 GeoJSON 数据压缩和抽稀。全国地图的边界坐标往往有几万个点很多点肉眼根本分辨不出来。用 simplify 算法把坐标点抽稀到 1%地图轮廓几乎看不出区别文件体积能缩到原来的 1/10 以内。第二series 里不要塞多余的数据。尤其是 scatter3D每个点都会创建独立的 3D 几何体几十个点没问题几千个点就可能让帧率掉到 10 以下。如果数据量确实大到这个程度改用 heatmap 或者聚类而不是硬扛。第三黑色背景大屏可以作为天然的优化优势。让大屏使用暗色背景地图外其他区域不渲染透明材质能省掉很多 blend 计算。配合 anisotropy 各向异性光照能把暗色背景下的立体感再拉高一层。5.2 5个图层叠加层级和渲染顺序错乱这是踩坑最多的地方。有的同学歇力把 5 个 series 按顺序一写发现柱子跑到地图底下或者散点被飞线遮挡就问为什么看不到。答案要先理解 ECharts 的 z-level 渲染顺序。同一层级的 series后定义的会覆盖先定义的。所以 series 内部顺序应该是地图主体map3D、柱状层、散点层、飞线层、涟漪层。如果你把飞线放在 map3D 前面飞线就会被地图主体整个挡住。千万别乱序。如果遇到不同 series 之间因为 z 值冲突导致的渲染半透明错乱可以显式给每个系列分配zlevel让 zlevel 不同的系列分铜渲染。但要注意 zlevel 不连续时可能会多出额外的 canvas 合成开销所以能用 z 控制就用 z实在不行再动 zlevel。5.3 常见报错速查现象原因解决方案图表空白控制台报 series.map3D not exists没有引入 echarts-gl 模块或 echarts 版本与 echarts-gl 不兼容确认 import echarts-gl 已执行建议锁定 echarts5.x地图区域显示但与预期位置偏差registerMap 注册的 GeoJSON 坐标系与散点/柱子的经纬度不一致检查 GeoJSON 是否有加密偏移统一使用 GCJ-02 或 WGS-84 坐标系散点/柱子在旋转后从地图中穿出去坐标的高度值设置过高或地图主体 regionHeight 与图层高度不匹配调低第三维高度将 regionHeight 控制在 3~6 之间飞线没有动效lines3D 的 effect.show 未开启或数据和 map 坐标系不一致检查 effect.show: true以及飞线两端坐标必须是数组对切后台再回来后地图变成灰色WebGL 上下文丢失监听 WebGL context lost 事件自动执行 chart.dispose() 后重新 init5.4 关于纹理贴图的建议想让地图更艳一些很多同学喜欢给地图贴纹理比如把省份贴上木纹、磨砂颗粒甚至国旗图案。ECharts-GL 的 texture 支持确实有但用下来我的经验是正式项目里少用。纹理贴图的好处是质感丰富代价是文件体积增加长宽 512 或 1024 的贴图动辄几百 KB且材质复杂后渲染帧率下降明显。大屏项目的观感优先级永远是轮廓清晰 动效顺滑 质感精致纯色加光照阴影已经能做出非常高级的效果。真想提升质感我建议优先调光照和投影而不是堆纹理。6. 项目扩展方向与我的实操体会6.1 这套方案还能往哪扩展当前项目做的是中国省级地图但整套架构和五层叠加的思路完全可以横向迁移到其他粒度。想看市级数据就换市级 GeoJSON想做世界地图就换成世界各国的 GeoJSON。地图底座换了散点、柱状、飞线、涟漪的逻辑不用动一行代码因为它们依赖的只是跟 coordinateSystem: geo3D 对齐这个抽象前提。更进阶的扩展方向是把地图跟真实业务数据联动。比如飞线层的数据不要写死而是从 WebSocket 推送实时订单轨迹柱状层的高度绑定数据库里某个业务表的 count 值点击省份后弹窗拉取该省份的所有门店散点。这套方案最值钱的不是 3D 效果本身而是图层解耦之后数据接入的成本被压到了最低。6.2 我踩过的几个坑和摸索出的心得第一ECharts-GL 的配置项学习曲线比 2D 陡得多但它的排查套路很成熟先静态再动态、先单层再多层。图层叠加做出来之后一坨乱麻不要直接去看全部 option把其他 series 注释掉只留一个图层逐个排查效率会高很多。第二大屏上的颜色一定不要用低饱和色。3D 地图有光照参与计算同样的蓝色在光照弱的位置会暗一大截看起来像是脏了。我的调色原则是主色用亮饱和色副色用高对比色背景彻底压黑这样无论光照怎么转动视觉焦点不会散。第三地图坐标系的坑最难识别。开发环境跑得好好的部署上线后散点位置全部偏移基本就是 GeoJSON 坐标系和业务坐标系的基准不统一。最好的办法是在开发第一天就确认整体采用哪个坐标系并且把全国数据和省份数据来源固定在同一个渠道不要混用多个来源的地图数据。第四真要深挖性能的时候优先看 WebGL 层面的瓶颈不要只看 JS 代码。ECharts-GL 渲染是 GPU 密集的series 数量、shading 模型、antialias 开关、纹理贴图尺寸这些才是影响帧率的大头。数据项不多但很卡基本就是材质和细分参数开太高了。最后说一句实在的3D 地图可视化这件事很多团队上来就追求视觉炸裂我建议还是先把这个5层叠加的地图底座跑起来然后一层层去加能和业务对上话的数据。地图再炫最终的价值还是落到领导一眼看懂业务趋势这六个字上。方向对了细节慢慢磨这套方案会给你的大屏项目省下大量不必要的折腾时间。
返回列表