
1. 从平面到立体为什么我们需要3D地图在地图可视化领域我们早已习惯了二维平面带来的清晰与直接。无论是展示销售热区、物流轨迹还是人口分布传统的2D地图都堪称经典工具。然而随着数据维度的复杂化和决策者对空间关系洞察需求的加深一个不容忽视的痛点出现了二维平面无法直观地表达“高度”或“强度”信息。比如你想展示全国各省份的GDP数据用2D地图通常只能通过颜色深浅色块图或气泡大小气泡图来区分虽然有效但视觉冲击力和空间层次感有限。当数据量巨大或需要对比多个维度的地理属性时这种表达方式就显得有些“扁平”了。这时3D地图的价值就凸显出来了。它不再是一个简单的平面而是一个可以承载“高度”Z轴信息的立体模型。这个“高度”可以代表任何你想量化的指标经济数据、人口密度、海拔高度、信号强度甚至是虚拟的业务指标。通过将数据在三维空间中立体化呈现信息的对比将变得无比直观——更高的“柱子”或更深的“凹陷”能瞬间抓住观众的注意力复杂的地理空间关系也更容易被理解。ECharts GL 的引入为在Web端实现高性能的3D可视化提供了可能。它基于WebGL技术能够利用GPU进行硬件加速流畅地渲染成千上万个3D几何体。对于“ECharts 3D地图”这个需求其核心就是利用ECharts GL的map3D组件将一个二维的地理坐标系GeoJSON拉伸成一个三维的立体模型并将数据映射到这个模型的高度和颜色上。简单来说它的工作流程是准备一份标准的地理边界数据GeoJSON - 通过ECharts GL将其转换为3D网格模型 - 将你的业务数据如各省份数值绑定到对应的地理区域 - 用高度和颜色进行视觉编码。最终你得到的是一个可以360度旋转、缩放、俯视的交互式3D数据地图让数据自己“站”起来说话。2. 核心思路与方案选型为什么是ECharts GL GeoJSON当你决定要做一个3D地图时面前可能有好几条路使用Three.js从头搭建、采用专业的GIS库如Cesium、或者寻找像ECharts这样更高层级的封装。这里的选择背后是开发效率、定制化程度和性能之间的权衡。为什么选择ECharts GL对于大多数业务数据可视化场景尤其是需要快速集成、对GIS专业功能要求不极致的场景ECharts GL是一个“甜点”选择。首先它与ECharts生态无缝集成如果你已经熟悉ECharts的配置项上手会非常快学习成本低。其次它封装了WebGL的复杂细节通过声明式的option配置就能实现复杂的3D效果开发效率极高。最后其性能经过优化能够满足绝大多数中大型数据量的渲染需求。相比之下直接使用Three.js虽然灵活性无敌但你需要自己处理地图投影、地理数据加载、着色器编写等大量底层工作更适合做高度定制化的3D地球或特殊效果。地图数据源GeoJSON是标准答案3D地图的“骨架”是地理边界信息。ECharts GL的map3D组件支持标准的GeoJSON格式。GeoJSON是一种用于表示地理要素如点、线、面的开放标准格式互联网上有大量现成的资源。例如你可以从阿里云的DataV.GeoAtlas获取中国省、市、县各级别的GeoJSON数据。它的优势在于结构清晰、通用性强并且可以直接被ECharts解析和渲染。技术栈组合一个典型的技术栈组合是Vue.js/React前端框架 ECharts ECharts GL 自定义GeoJSON。在Vue中你可以通过vue-echarts组件库方便地集成在React中也有相应的echarts-for-react。这种组合既能享受现代前端框架的工程化优势又能利用ECharts强大的图表能力。注意ECharts GL在ECharts 5.x版本中已不再作为一个独立扩展其3D功能包括map3D已集成到主库中。这意味着你只需要安装echarts核心库并引入对应的3D组件即可无需单独安装echarts-gl包。这是版本迭代中的一个重要变化务必确认你的ECharts版本。3. 环境准备与核心依赖安装万事开头先搭环境。这里我们以在一个Vue 3项目中集成为例演示从零开始的步骤。React项目的思路完全一致只是组件引入方式不同。3.1 创建项目与安装依赖首先使用你熟悉的包管理器初始化项目并安装核心库。ECharts 5.x版本已经内置了3D能力。# 使用 npm npm install echarts --save # 或者使用 yarn yarn add echarts如果你使用的是Vue强烈推荐使用官方的Vue封装组件它能更好地处理ECharts实例的生命周期如自动在组件销毁时释放资源npm install vue-echarts3.2 准备地图GeoJSON数据这是构建地图的基石。你需要获取你想要展示区域的GeoJSON文件。以中国地图为例寻找数据源可以访问阿里云DataV.GeoAtlas这是一个非常友好的地理小工具系列选择“中国”和“省份”级别然后下载GeoJSON文件。你也可以从Github上的很多开源仓库中找到世界、国家、省市级别的GeoJSON数据。放置数据将下载的china.json文件放置在你的项目静态资源目录下例如public/目录Vue CLI或Vite项目或src/assets/目录。注册地图在初始化图表之前你需要用ECharts的registerMap方法将这个GeoJSON数据注册为一个可用的地图。关键点在于对于3D地图map3D注册地图的语法与2D地图完全一致。ECharts内部会识别并用于构建3D几何体。3.3 在Vue组件中引入与注册在你的Vue组件例如Map3D.vue中你需要完成以下步骤template div refchartRef stylewidth: 100%; height: 600px;/div /template script setup import { ref, onMounted, onBeforeUnmount, shallowRef } from vue; // 1. 引入ECharts核心库和3D地图组件 import * as echarts from echarts; // 必须引入3D地图和3D笛卡尔坐标系组件 import echarts-gl; // 2. 引入本地的GeoJSON数据 import chinaGeoJSON from /assets/china.json; // 根据你的实际路径调整 const chartRef ref(null); const chartInstance shallowRef(null); // 使用shallowRef避免不必要的响应式开销 onMounted(() { // 3. 初始化图表实例 chartInstance.value echarts.init(chartRef.value); // 4. 注册地图关键步骤 // 第一个参数是地图名称后续在option中会用到例如‘china’ // 第二个参数就是GeoJSON数据 echarts.registerMap(china, chinaGeoJSON); // 5. 设置配置项并渲染 const option getOption(); // 一个返回配置对象的函数 chartInstance.value.setOption(option); // 6. 响应窗口大小变化 window.addEventListener(resize, handleResize); }); // 获取配置项的函数 const getOption () { // 这里先返回一个基础配置我们将在下一节详细填充 return { // ... 详细的option配置 }; }; const handleResize () { chartInstance.value?.resize(); }; onBeforeUnmount(() { window.removeEventListener(resize, handleResize); chartInstance.value?.dispose(); // 销毁实例释放内存 }); /script实操心得shallowRef用于存储ECharts实例是一个好习惯因为ECharts实例本身是一个复杂的对象我们不需要Vue对其内部属性进行深度响应式代理这能带来一些性能提升。另外务必记得在组件销毁时调用dispose()方法否则可能导致内存泄漏和GPU资源未释放。4. 配置项深度解析构建一个基础3D中国地图现在来到了最核心的部分option配置对象。这是你与ECharts“对话”的方式告诉它你想要一个什么样的3D地图。我们先从一个最基础的、只有地形起伏的地图开始。4.1 骨架map3D与geo3D配置在ECharts GL中3D地图主要通过map3D和geo3D系列来呈现在早期版本中主要用map3D概念上可以理解为3D地图的容器。我们使用geo3D系列因为它更灵活且与GeoJSON的集成更直接。const getOption () { // 模拟一些省份的数据例如GDP单位万亿元 const mockData [ { name: 广东, value: 12.9 }, { name: 江苏, value: 12.3 }, { name: 山东, value: 8.7 }, { name: 浙江, value: 7.8 }, { name: 河南, value: 6.1 }, // ... 其他省份数据 ]; return { // 视觉映射组件将数据值映射到颜色和高度 visualMap: { show: true, min: 0, max: 13, // 根据你的数据最大值设定 inRange: { color: [#313695, #4575b4, #74add1, #abd9e9, #e0f3f8, #ffffbf, #fee090, #fdae61, #f46d43, #d73027, #a50026] // 这是一个从蓝到红的渐变色常用于表示数值大小 }, // 可以同时控制颜色和高度映射 calculable: true, textStyle: { color: #fff // 在深色背景下将文字设为白色 } }, // 地理坐标系3D配置 geo3D: { map: china, // 这里对应 registerMap 时注册的地图名称 roam: true, // 开启鼠标拖拽旋转、缩放 boxHeight: 5, // 整个地图的基底高度单位是“相对单位” // 区域样式设置 regionHeight: 2, // 每个区域的基础高度与boxHeight叠加 itemStyle: { color: #fff, // 地图区域的基础颜色通常会被visualMap覆盖 borderWidth: 0.5, borderColor: #000 }, // 将模拟数据与地图区域关联起来 // 这是实现“数据驱动高度”的关键 data: mockData.map(item ({ name: item.name, value: item.value })), // 标签显示配置 label: { show: true, // 显示省份名称 textStyle: { color: #000, // 标签颜色 backgroundColor: rgba(255,255,255,0.7), // 标签背景色 borderRadius: 3, padding: [2, 4] }, // 距离表面的偏移量避免标签嵌入模型 distance: 10 }, // 光照设置影响3D模型的明暗质感 light: { main: { intensity: 1.2 // 主光源强度 }, ambient: { intensity: 0.3 // 环境光强度 } } } }; };关键参数解读boxHeight和regionHeight: 它们共同决定了地图的“厚度”。boxHeight是整个地图的底座高度所有区域都建立在这个底座之上。regionHeight是每个区域的“墙”的高度基数。最终一个区域的总高度是boxHeight regionHeight * (value / maxValue)。通过调整这两个值你可以控制地图是看起来像一块起伏的“浮雕”boxHeight小regionHeight大还是一个有基座的“沙盘”boxHeight大。data: 这里的数组必须包含name和value字段。name必须与GeoJSON中features.properties.name里的名称严格一致否则数据无法匹配该区域就不会有高度变化。这是最容易出错的地方之一。roam: 设置为true后用户就可以用鼠标左键拖拽旋转视角右键拖拽平移滚轮缩放。这是3D可视化交互的核心。4.2 让地图“活”起来数据映射与视觉编码上面的配置已经将一个平面的中国地图变成了一个3D模型并且各省份的高度会根据mockData中的value值变化。visualMap组件则同时将数值映射到了颜色上。这样高度和颜色共同构成了数据的双重编码使得高值区域如广东、江苏既“凸起”又“鲜红”低值区域则“凹陷”且“偏蓝”信息传达效率倍增。你可以尝试修改visualMap.inRange.color数组来更换配色方案。ECharts内置了一些不错的渐变色你也可以使用在线配色工具生成自己喜欢的渐变色数组。4.3 视角ViewControl精细化配置默认的旋转和缩放可能不符合你的预期。viewControl配置项可以让你精细控制3D场景的摄像机视角。const option { // ... 其他配置 globe: { // 注意对于geo3D视角控制通常在全局的globe或geo3D顶层配置但更常见的做法是使用独立的viewControl viewControl: { projection: perspective, // 投影方式perspective透视投影近大远小orthographic正交投影 autoRotate: false, // 是否开启自动旋转做演示时很有用 autoRotateSpeed: 10, // 自动旋转速度 // 限制旋转角度防止地图被翻转到奇怪的角度 alpha: 15, // 初始绕x轴旋转角度俯仰角 beta: -15, // 初始绕y轴旋转角度方位角 minAlpha: -90, // 最小俯仰角 maxAlpha: 90, // 最大俯仰角 // 限制缩放 distance: 120, // 初始摄像机距离地图中心的距离 minDistance: 80, maxDistance: 200, // 限制平移 panMouseButton: right, // 平移操作的鼠标按键 rotateMouseButton: left, // 旋转操作的鼠标按键 } } };把viewControl配置放在geo3D的同级。通过设置alpha和beta你可以给地图一个初始的“上帝视角”。限制minAlpha/maxAlpha可以防止用户把地图完全“掀翻”到背面去看那通常没意义。distance控制了镜头的远近相当于缩放。5. 进阶实战为3D地图添加飞线图与散点图一个孤立的3D地图已经很有表现力但如果能展示区域间的关联如物流、人口迁徙、资金流向或者在高点上标记具体城市效果会更上一层楼。这就需要引入lines3D和scatter3D系列。5.1 绘制3D飞线Lines3D飞线图用于表示两点之间的流动。在3D空间中飞线是连接两个地理坐标点的贝塞尔曲线。// 在option中与geo3D并列增加一个lines3D配置 const option { // ... visualMap, geo3D 等配置 series: [ // 可以以数组形式组织多个系列 // 第一个系列是geo3D地图 { type: geo3D, // ... geo3D的所有配置 }, // 第二个系列是3D飞线 { type: lines3D, coordinateSystem: geo3D, // 关键指定坐标系为geo3D这样线的端点坐标才能正确映射 polyline: true, // 是否是多段线false则为直线 effect: { show: true, period: 4, // 特效动画周期秒 trailWidth: 2, // 拖尾宽度 trailLength: 0.5, // 拖尾长度0-1 constantSpeed: 20 // 符号移动速度 }, lineStyle: { width: 1, // 线宽 color: #ff7f50, // 线颜色 opacity: 0.8 // 透明度 }, // 飞线数据每个元素是一条线 data: [ { coords: [ [116.4, 39.9, 10], // 起点 [经度, 纬度, 高度偏移]。高度偏移可省略默认为0 [121.47, 31.23, 10] // 终点 [经度, 纬度, 高度偏移] ], value: 100 // 可选的数值可用于视觉映射 }, // 可以添加更多飞线例如从北京到广州 { coords: [ [116.4, 39.9, 10], [113.27, 23.13, 10] // 广州 ] } ] } ] };关键点解析coordinateSystem: geo3D这是连接飞线与地图的桥梁。它告诉EChartslines3D系列中coords的经纬度坐标应该被解释为geo3D系列所使用的地理坐标系下的坐标。coords中的第三个值这是相对于地图表面的高度偏移。如果你写[116.4, 39.9, 10]意味着线的起点位于北京坐标点的上空10个单位处。这可以防止飞线“埋”进地图模型里。这个值需要你根据地图的boxHeight和regionHeight来微调。effect这个配置让飞线动起来产生粒子流动的效果非常适合表现“流向”。5.2 添加3D散点Scatter3D散点图用于在地图上的特定坐标点标记信息比如标注主要城市、站点位置。// 在series数组中再增加一个scatter3D系列 { type: scatter3D, coordinateSystem: geo3D, // 同样指定坐标系 symbol: pin, // 符号类型可以是‘circle’, ‘rect’, ‘pin’图钉等 symbolSize: 20, // 符号大小 itemStyle: { color: #ff3333, borderWidth: 1, borderColor: #fff }, label: { show: true, formatter: {b}, // 显示数据项名称 position: top // 标签位置 }, // 散点数据 data: [ { name: 北京, value: [116.4, 39.9, 15] }, // [经度 纬度 高度] { name: 上海, value: [121.47, 31.23, 15] }, { name: 深圳, value: [114.05, 22.55, 15] } ] }高度Z坐标的设定散点的value数组第三项是Z坐标高度。这个高度是绝对高度而不是相对于地图表面的偏移。你需要确保这个值大于对应地理位置处地图模型的实际高度否则散点会被模型遮挡。一个简单的办法是取一个比boxHeight max(regionHeight * value)更大的固定值。5.3 多系列协同与视觉统一当同时存在geo3D、lines3D、scatter3D时确保它们使用同一个visualMap可能比较困难因为不同系列的数据范围和意义可能不同。通常的实践是为地图geo3D单独配置一个visualMap用于映射区域高度和颜色。为飞线或散点配置独立的视觉样式比如用固定的颜色或者根据其自身的value字段通过该系列自己的itemStyle. color回调函数来设置颜色。// 在lines3D系列中根据数据value动态设置颜色 lineStyle: { color: function(params) { const value params.data.value; // 获取该飞线数据中的value字段 // 根据value返回一个颜色值 return value 50 ? #ff0000 : #00ff00; } }6. 性能优化与常见问题排查当数据量变大比如绘制全国所有城市间的数百条飞线时性能可能会成为瓶颈。以下是一些优化技巧和常见问题的解决方法。6.1 性能优化要点简化GeoJSON从网络下载的GeoJSON可能包含非常精细的边界海岸线曲折这会导致顶点数激增。在保证视觉效果可接受的前提下可以使用地图简化工具如MapShaper对GeoJSON进行简化减少多边形顶点数量能显著提升渲染性能。降低采样精度对于lines3D如果线很长其分段数polyline的精度会影响性能。ECharts GL内部会处理但数据本身不宜过于密集。按需渲染如果交互时感到卡顿可以考虑在geo3D配置中设置shading: color而不是默认的lambert或realistic这会使用更简单的着色计算。或者暂时关闭postEffect后处理特效如景深、泛光。实例管理在单页应用SPA中务必在组件销毁时调用chartInstance.dispose()。在Vue/React中利用生命周期钩子或Effect Hook来管理。6.2 常见问题与解决方案问题一地图不显示控制台无报错。检查1是否成功引入了echarts-gl或ECharts 5.x的3D模块确保import语句正确。检查2registerMap是否成功执行registerMap的第一个参数地图名是否与geo3D.map配置的值完全一致大小写敏感检查3GeoJSON数据格式是否正确可以尝试在2D地图geo组件中先测试这份数据是否能正常显示。问题二数据有值但地图区域没有“凸起”效果。检查1geo3D.data中的name字段是否与GeoJSON中features.properties里的名称100%匹配一个常见的坑是GeoJSON里是“北京市”而你的数据里是“北京”。建议打印出GeoJSON中的名称列表进行核对。检查2visualMap的min和max范围设置是否合理如果你的数据值都在10-20之间而max设成了100那么所有区域的高度变化都会很不明显。可以将visualMap的calculable设为true显示一个拖拽条手动调整范围看效果。问题三飞线或散点位置飘在空中不与地图贴合。检查1是否设置了coordinateSystem: geo3D这是必须的。检查2坐标顺序是否为[经度, 纬度]GeoJSON和ECharts默认使用[lng, lat]顺序与高德、百度地图的[lat, lng]相反。检查3飞线/散点的Z坐标高度是否设置得太小如果小于地图表面高度就会被遮挡。尝试调大这个值或者使用convertToPixelAPI动态计算较复杂。问题四地图颜色一片灰白没有按数据着色。检查visualMap组件是否被正确引入并配置inRange.color是否是一个有效的颜色数组geo3D.itemStyle.color是否设置了一个固定颜色覆盖了映射通常itemStyle.color应设为‘#fff’或一个基础色让visualMap来接管最终颜色。问题五在Vue/React中图表随数据更新时出现闪烁或异常。解决方案使用ECharts的setOption方法时第二个参数很重要。对于增量更新应该使用chartInstance.setOption(newOption, { notMerge: false }); // 默认是false即合并选项如果你需要完全替换则用{ notMerge: true }。在Vue/React的响应式更新中更安全的做法是在watch中先调用chartInstance.clear()再setOption或者使用notMerge: false并确保新旧option结构一致。7. 从开发到部署让3D地图在页面上稳定运行完成开发后你需要考虑如何将它集成到真实的页面中并确保其在不同环境下的稳定性。7.1 响应式适配3D地图通常需要较大的展示空间。你需要确保图表容器能随父元素或窗口大小变化。// 在初始化后监听窗口resize事件 const handleResize () { chartInstance.value?.resize(); // 调用ECharts实例的resize方法 }; onMounted(() { // ... 初始化图表 window.addEventListener(resize, handleResize); }); onBeforeUnmount(() { window.removeEventListener(resize, handleResize); // ... 销毁实例 });更优雅的做法是使用ResizeObserverAPI来观察图表容器本身的大小变化这样即使容器大小变化不是由窗口resize引起的比如侧边栏折叠图表也能自适应。7.2 异步数据加载实际项目中地图数据GeoJSON和业务数据很可能来自后端API。你需要处理好异步加载和图表渲染的顺序。import { ref, onMounted } from vue; import * as echarts from echarts; import echarts-gl; const chartInstance ref(null); const geoDataLoaded ref(false); const businessDataLoaded ref(false); onMounted(async () { // 1. 并行或串行加载数据 const [geoJSON, businessData] await Promise.all([ fetch(/api/geojson/china).then(r r.json()), fetch(/api/business/data).then(r r.json()) ]); // 2. 注册地图 echarts.registerMap(china, geoJSON); // 3. 初始化图表此时容器必须已挂载 chartInstance.value echarts.init(chartRef.value); // 4. 准备配置项注入业务数据 const option getOption(businessData); // 5. 渲染图表 chartInstance.value.setOption(option); });7.3 在“状态”中展示解决Axure等原型工具的预览问题你提到的“在编辑页面中如何实现可以展示状态而不是只能在浏览器中预览查看效果”这是一个非常实际的需求尤其在制作高保真交互原型时。在Axure RP等工具中直接嵌入一个运行的ECharts图表是困难的因为它们本质上是静态原型工具。变通解决方案导出为动态HTML将你的Vue/React项目构建后生成一个独立的HTML文件包含所有JS、CSS。这个HTML文件可以在本地用浏览器打开并交互。你可以将这个HTML文件的路径或截图放在Axure原型中并添加一个链接注明“点击查看动态图表”。使用内联框架Iframe一些更高级的原型工具或内部演示平台支持嵌入Iframe。你可以将部署了3D地图的页面URL嵌入到Iframe中这样在原型中就能直接看到可交互的图表。但这依赖于网络环境和部署。录制交互视频/GIF对于无法嵌入动态内容的场景最可靠的方法是使用屏幕录制工具如LICEcap、ScreenToGif将图表的交互过程旋转、缩放、提示框显示录制成GIF或短视频然后将这个媒体文件插入到原型中。虽然失去了交互性但能最直观地传达效果。寻找支持ECharts的插件或平台一些在线原型协作平台如墨刀、摹客可能提供了更丰富的图表组件或自定义代码嵌入功能可以调研其是否支持。核心思路在静态原型工具中展示动态效果本质上是一个“降级”过程。要么将动态内容以可执行文件HTML的形式外链要么将其效果以媒体文件的形式内嵌。在项目沟通中明确这一点并准备好可独立运行的演示链接通常就能满足评审和演示的需求。经过以上七个部分的拆解从概念到实战从配置到排错一个功能完整、性能可控、体验流畅的ECharts 3D地图应该已经能在你的项目中运转起来了。记住3D可视化是工具最终目的是为了更有效地传达信息。避免过度追求炫酷效果而牺牲了图表的可读性和核心数据的表达始终让设计服务于数据洞察本身。