你有没有在数据大屏前驻足过?一块块区域从平面里「长」出来,随着风险等级泛着红黄蓝绿,跳动到某一块,左侧立刻弹出详尽的指标面板,那种一眼扫过去就能抓住重点的感觉,往往就来自 3D 地图 的加持。
那如何实现3D地图呢?或许你第一个想到的就是three.js,但本文却是用 echarts-gl 让地图「立」起来的,相对来说上手更容易,效果也差强人意。
一、技术选型——先看效果再动手
从UED给出的图上不难看出,这是一张3D地图,笔者一开始也尝试使用three.js去实现,但因工时有限,最终还是选择了echarts-gl。
用 echarts-gl 的 map3D,你可以轻松得到:
- 立体挤出:每个行政区像一块块积木,从底图上「长」出来;
- 分级着色:按风险等级、指标区间等给不同区域上色,热力一目了然;
下面就从零开始,把这一套串起来。
二、环境准备:ECharts + GL 扩展
依赖就两样:ECharts 5.x 和 echarts-gl,地图数据用标准的 GeoJSON数据即可。
npm install echarts echarts-gl
在入口或图表初始化前,务必先引入 GL 扩展,否则 map3D 会报错或无效:
import * as echarts from 'echarts';
import 'echarts-gl'; // 必须!注册 3D 地图等 GL 能力
GeoJSON 可以从 DataV、阿里云等渠道获取省/市矢量数据,或由后端按需裁剪后下发。只要 features[].properties.name 与业务数据里的区域名能对上,就能用。
三、注册地图
先用 GeoJSON 注册一个地图 ID,再在 series 里用 type: 'map3D' 引用它。
// 假设 mapGeoJson 是你拿到的某省/市 GeoJSON
echarts.registerMap('regionMap', mapGeoJson);
const option = {
series: [
{
type: 'map3D',
map: 'regionMap',
boxHeight: 0,
regionHeight: 1.8,
itemStyle: {
color: 'rgba(0,123,182,0.6)',
borderColor: '#42fff5',
borderWidth: 1,
},
},
],
};
const chart = echarts.init(document.getElementById('chart'));
chart.setOption(option);
到这里,你已经能得到一块「整块同色、带描边、有立体高度」的 3D 地图。接下来要做的,就是让颜色跟着数据走。
四、让颜色「跟着数据走」:分级着色
大屏上最常见的需求是:按风险等级或指标区间给区域上色。做法是把业务数据塞进 series[].data,并在 itemStyle.color 里用函数根据 params.data 取等级或区间,再映射到颜色。
先定义一套等级与颜色的映射(可按业务改):
const RISK_COLOR_MAP = {
normal: 'rgba(0,123,182,0.6)', // 正常
low: 'rgba(255,247,96,0.5)', // 低风险
medium: 'rgba(255,115,25,0.65)', // 中风险
high: 'rgba(255,96,96,0.5)', // 高风险
};
在 map3D 的 itemStyle 里用函数返回颜色:
itemStyle: {
color: (params) => {
if (!params?.data) return RISK_COLOR_MAP.normal;
const riskLevel = params.data.riskLevel || 'normal';
return RISK_COLOR_MAP[riskLevel] ?? RISK_COLOR_MAP.normal;
},
borderColor: '#42fff5',
borderWidth: 1,
opacity: 1,
},
这样,每个区域的 data.riskLevel 就会驱动其颜色,热力感瞬间就出来了。
五、立体感与光影优化
- boxHeight:整张地图下面的「底座」高度。
- regionHeight:每个区域的挤出高度,建议在
1~2.5之间试。太小立体感弱,太大容易显得笨重;1.8 是实践里比较顺眼的一档。
光照能明显增强立体感,主光 + 环境光即可:
light: {
main: {
intensity: 0.8,
shadow: true,
alpha: 70,
beta: 60,
},
ambient: {
intensity: 0.2,
alpha: 70,
beta: 60,
},
},
调 alpha、beta 可以改变光照方向,配合深色背景,边缘会自然形成高光,立体感更强。
六、视角与交互:viewControl
大屏若希望「固定视角、不让人转来转去」,可以用正交投影并关掉旋转/缩放/平移:
viewControl: {
projection: 'orthographic',
orthographicSize: 65,
autoRotate: false,
rotateSensitivity: 0,
zoomSensitivity: 0,
panSensitivity: 0,
},
orthographicSize 相当于「相机拉多远」:数值越大,地图在画布里显得越小。不同省/市 GeoJSON 的包围盒不一样,需要按实际效果微调(例如 50~120 都常见)。建议先把地图渲染出来,再拖这个值到顺眼为止。
若要做「可旋转、可缩放」的交互大屏,把 rotateSensitivity、zoomSensitivity、panSensitivity 调大即可。
七、高亮与标签:emphasis 和 label
鼠标悬停或点击选中时,可以用 emphasis 单独给一块区域「加戏」:换色、加粗描边、显示名称等。
emphasis: {
itemStyle: {
color: '#1263B6',
borderColor: '#60F1FF',
borderWidth: 2,
opacity: 1,
},
label: {
show: true,
color: '#FFFFFF',
fontSize: 12,
},
},
label: {
show: true,
color: '#FFFFFF',
fontSize: 12,
},
如果要做「选中态」和「未选中态」不同的图标或字号,可以用 ECharts 的 rich 文本,在 formatter 里根据当前是否选中拼出 {name|区域名}\n{icon|} 这类格式,再在 rich 里为 name、icon 配不同的样式或背景图。
八、双层叠加:底图 + 数据层,层次更清晰
单独一层 map3D 已经能表达数据和立体,但若希望「底下有一层半透明底图、上面一层按数据着色」,可以叠两个 map3D:
- 底层:
zlevel: 0、regionHeight: 0,整块半透明色(如rgba(0,79,168,0.7)),不显示 label; - 顶层:
zlevel: 1、regionHeight: 1.8,itemStyle.color用上面的函数按数据着色,并开启 label、emphasis。
只有顶层需要传 data,且 data[].name 必须和 GeoJSON 里 properties.name 一一对应,否则对应区域会用默认色。
const baseLayer = {
type: 'map3D',
map: 'regionMap',
zlevel: 0,
regionHeight: 0,
itemStyle: {
color: 'rgba(0,79,168,0.7)',
borderColor: '#42fff5',
borderWidth: 1,
},
label: { show: false },
emphasis: {
itemStyle: { color: 'rgba(0,79,168,0.7)' },
label: { show: false },
},
};
const dataLayer = {
type: 'map3D',
map: 'regionMap',
zlevel: 1,
regionHeight: 1.8,
data: chartData, // 仅这一层需要 data
itemStyle: {
color: (params) => { /* 同上,按 riskLevel 着色 */ },
borderColor: '#42fff5',
borderWidth: 1,
opacity: 1,
},
label: { show: true /* ... */ },
emphasis: { /* 高亮与标签 */ },
light: { /* ... */ },
viewControl: { /* ... */ },
};
option.series = [baseLayer, dataLayer];
这样既有「整块底图」的轮廓感,又有「数据层」的清晰分层,大屏层次会更舒服。
九、数据从哪来:data 格式与更新
map3D 的 data 是「区域名 + 业务字段」的数组,name 必须和 GeoJSON 里一致(例如地市名、区县名),其余字段随便加,供 itemStyle.color 和 tooltip 使用。
const chartData = [
{ name: 'A市', riskLevel: 'high', value: 120, fieldA: 1000, fieldB: 50 },
{ name: 'B市', riskLevel: 'low', value: 3, fieldA: 200, fieldB: 12 },
{ name: 'C市', riskLevel: 'normal', value: 0, fieldA: 80, fieldB: 0 },
];
更新数据时,只改对应 series 的 data 再 setOption 即可,例如:
chart.setOption({
series: [
{ ...baseLayer },
{ ...dataLayer, data: newChartData },
],
});
十、Tooltip:做成「信息面板」
点击或轮播到某区域时,把 tooltip 当成一块小信息面板用:多列指标、单位、甚至简单趋势,都可以在 formatter 里用 HTML 拼出来。
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(76,126,255,0.2)',
borderColor: 'transparent',
triggerOn: 'click',
alwaysShowContent: true,
formatter: (params) => {
const { name, data } = params;
if (!data) return name;
const rows = [
{ label: '指标A', value: data.fieldA ?? '--', unit: '万元' },
{ label: '指标B', value: data.fieldB ?? '--', unit: '个' },
];
const line = (item) =>
`<div style="display:flex;justify-content:space-between;margin:4px 0">
<span>${item.label}</span>
<span>${item.value} ${item.unit}</span>
</div>`;
return `<div style="font-weight:bold;margin-bottom:8px">${name}</div>${rows.map(line).join('')}`;
},
},
triggerOn: 'click' + alwaysShowContent: true 可以实现「点哪块就固定显示哪块的详情」,适合汇报时指着大屏讲。
十一、图例:风险热力说明
图例不必用 ECharts 的 legend,可以在图表容器外单独用 HTML 做一块「风险热力分析」说明,和地图同屏展示,例如:
- ■ 正常:风险个数 0(蓝)
- ■ 低风险:1~9(黄)
- ■ 中风险:10~99(橙)
- ■ 高风险:≥100(红)
颜色与 RISK_COLOR_MAP 保持一致,观众扫一眼就能读懂地图上的色块含义。
十二、常见坑
- 引入顺序:必须先
import 'echarts-gl'再使用 map3D,否则会报错或无效。 - name 对不齐:GeoJSON 的
properties.name和data[].name必须完全一致(包括空格、简繁体),对不上的区域会没有 data,颜色会变成默认。 - orthographicSize:不同区域 GeoJSON 差异大,建议以「铺满容器、不变形」为目标,多试几个值。
- 性能:区域特别多(例如到区县级)时,可考虑只渲染当前层级,或适当降低
regionHeight、关闭 shadow,以减轻 GPU 压力。 - 地图轮播:
2d地图轮播使用到的API dispatchAction 在map3D中会失效,可能需要手动实现 - GeoJSON数据不全:如果开发的是新疆的地图,数据可能不完善,因为新疆兵团在不断地建设新的行政区划中,DataV部分数据未及时更新,其他地区应该问题不大。
如果你还想玩出更多花样,可以尝试:在 3D 地图上叠加 scatter3D 做城市点位,或者根据数据动态改 regionHeight,使其形成「高度也表示一个维度」的立体图。ECharts GL 的文档里还有更多 3D 系列,值得翻一翻。