用 ECharts GL 把地图「立」起来

0 阅读7分钟

你有没有在数据大屏前驻足过?一块块区域从平面里「长」出来,随着风险等级泛着红黄蓝绿,跳动到某一块,左侧立刻弹出详尽的指标面板,那种一眼扫过去就能抓住重点的感觉,往往就来自 3D 地图 的加持。

image.png

那如何实现3D地图呢?或许你第一个想到的就是three.js,但本文却是用 echarts-gl 让地图「立」起来的,相对来说上手更容易,效果也差强人意。

一、技术选型——先看效果再动手

从UED给出的图上不难看出,这是一张3D地图,笔者一开始也尝试使用three.js去实现,但因工时有限,最终还是选择了echarts-gl。

用 echarts-gl 的 map3D,你可以轻松得到:

  • 立体挤出:每个行政区像一块块积木,从底图上「长」出来;
  • 分级着色:按风险等级、指标区间等给不同区域上色,热力一目了然;

下面就从零开始,把这一套串起来。


二、环境准备:ECharts + GL 扩展

依赖就两样:ECharts 5.xecharts-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,
  },
},

alphabeta 可以改变光照方向,配合深色背景,边缘会自然形成高光,立体感更强。


六、视角与交互:viewControl

大屏若希望「固定视角、不让人转来转去」,可以用正交投影并关掉旋转/缩放/平移:

viewControl: {
  projection: 'orthographic',
  orthographicSize: 65,
  autoRotate: false,
  rotateSensitivity: 0,
  zoomSensitivity: 0,
  panSensitivity: 0,
},

orthographicSize 相当于「相机拉多远」:数值越大,地图在画布里显得越小。不同省/市 GeoJSON 的包围盒不一样,需要按实际效果微调(例如 50~120 都常见)。建议先把地图渲染出来,再拖这个值到顺眼为止。

若要做「可旋转、可缩放」的交互大屏,把 rotateSensitivityzoomSensitivitypanSensitivity 调大即可。


七、高亮与标签: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 里为 nameicon 配不同的样式或背景图。


八、双层叠加:底图 + 数据层,层次更清晰

单独一层 map3D 已经能表达数据和立体,但若希望「底下有一层半透明底图、上面一层按数据着色」,可以叠两个 map3D:

  • 底层zlevel: 0regionHeight: 0,整块半透明色(如 rgba(0,79,168,0.7)),不显示 label;
  • 顶层zlevel: 1regionHeight: 1.8itemStyle.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 的 datasetOption 即可,例如:

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 保持一致,观众扫一眼就能读懂地图上的色块含义。


十二、常见坑

  1. 引入顺序:必须先 import 'echarts-gl' 再使用 map3D,否则会报错或无效。
  2. name 对不齐:GeoJSON 的 properties.namedata[].name 必须完全一致(包括空格、简繁体),对不上的区域会没有 data,颜色会变成默认。
  3. orthographicSize:不同区域 GeoJSON 差异大,建议以「铺满容器、不变形」为目标,多试几个值。
  4. 性能:区域特别多(例如到区县级)时,可考虑只渲染当前层级,或适当降低 regionHeight、关闭 shadow,以减轻 GPU 压力。
  5. 地图轮播2d地图轮播使用到的API dispatchAction 在map3D中会失效,可能需要手动实现
  6. GeoJSON数据不全:如果开发的是新疆的地图,数据可能不完善,因为新疆兵团在不断地建设新的行政区划中,DataV部分数据未及时更新,其他地区应该问题不大。

如果你还想玩出更多花样,可以尝试:在 3D 地图上叠加 scatter3D 做城市点位,或者根据数据动态改 regionHeight,使其形成「高度也表示一个维度」的立体图。ECharts GL 的文档里还有更多 3D 系列,值得翻一翻。