经纬志
首页 / MapLibre / 教程

聚合与热力图

先看结论

几万个点直接画圆,屏幕会糊成一片,性能也会掉下来。聚合把邻近的点折叠成带计数的圆, 热力图把点密度渲染成颜色渐变。两者解决同一个问题,呈现方式不同。

最后验证 2026-11-03 · MapLibre GL JS 4.x · 更新 2026-10-29 · 引用本文

客户端聚合

GeoJSON 源开启 cluster 后,MapLibre 在客户端按网格聚合, 图层拿到的就是聚合结果。聚合发生在数据进入渲染管线之前, 与图层数量无关。

map.addSource('sites', {
  type: 'geojson',
  data: sites,
  cluster: true,
  clusterRadius: 50,          // 像素,聚合半径
  clusterMaxZoom: 14,         // 超过该级别不再聚合
  clusterProperties: {
    total: ['+', ['get', 'value']]     // 累加每个点的 value
  }
});

// 聚合圆
map.addLayer({
  id: 'clusters',
  type: 'circle',
  source: 'sites',
  filter: ['has', 'point_count'],
  paint: {
    'circle-color': ['step', ['get', 'point_count'],
      '#a8c8b8', 10, '#5f9b7d', 50, '#17694a'
    ],
    'circle-radius': ['step', ['get', 'point_count'],
      18, 10, 24, 50, 30
    ],
    'circle-stroke-width': 1,
    'circle-stroke-color': '#ffffff'
  }
});

// 聚合数字
map.addLayer({
  id: 'cluster-count',
  type: 'symbol',
  source: 'sites',
  filter: ['has', 'point_count'],
  layout: {
    'text-field': ['get', 'point_count_abbreviated'],
    'text-font': ['Noto Sans Regular'],
    'text-size': 12
  },
  paint: { 'text-color': '#ffffff' }
});

// 未聚合的原始点
map.addLayer({
  id: 'sites-points',
  type: 'circle',
  source: 'sites',
  filter: ['!', ['has', 'point_count']],
  paint: {
    'circle-radius': 5,
    'circle-color': '#c2410c',
    'circle-stroke-width': 1,
    'circle-stroke-color': '#ffffff'
  }
});

三个图层共用一份源,靠 ['has', 'point_count'] 区分。 clusterProperties 让每个聚合点带上自定义累加值, 便于直接写「覆盖 128 个站点」这类注记。

点开聚合展开

聚合圆默认不可交互。要做出「点击放大并展开」的效果,需要查询 cluster_id,再取该簇的展开级别。

map.on('click', 'clusters', function (e) {
  var f = e.features[0];
  var clusterId = f.properties.cluster_id;

  map.getSource('sites').getClusterExpansionZoom(clusterId, function (err, zoom) {
    if (err) return;
    map.easeTo({
      center: f.geometry.coordinates,
      zoom: zoom
    });
  });
});

getClusterExpansionZoom 返回的是「刚好能把这簇拆开」的级别, 比自己加 2 级更准,也不会越过 clusterMaxZoom。

热力图

热力图不是聚合,它把每个点的权重累积成密度场再上色。 heatmap-color 只能用 ['heatmap-density'] 作为输入, 这是它与其他图层最大的写法差异。

map.addSource('heat-points', {
  type: 'geojson',
  data: points
});

map.addLayer({
  id: 'heat',
  type: 'heatmap',
  source: 'heat-points',
  paint: {
    'heatmap-weight': ['interpolate', ['linear'], ['get', 'value'],
      0, 0,
      10, 1
    ],
    'heatmap-intensity': ['interpolate', ['linear'], ['zoom'],
      8, 0.6,
      14, 1.4
    ],
    'heatmap-radius': ['interpolate', ['linear'], ['zoom'],
      8, 12,
      14, 34
    ],
    'heatmap-opacity': 0.8,
    'heatmap-color': ['interpolate', ['linear'], ['heatmap-density'],
      0,    'rgba(33,102,172,0)',
      0.2,  'rgb(103,169,207)',
      0.4,  'rgb(209,229,240)',
      0.6,  'rgb(253,219,199)',
      0.8,  'rgb(239,138,98)',
      1,    'rgb(178,24,43)'
    ]
  }
});

heatmap-radius 必须随缩放变大,否则放大后每个点都变成孤立的光斑。 heatmap-intensity 控制整体亮度,配合透明度能在放大后逐渐淡出, 让位给真实点位。

两种呈现方式的对照

对比项聚合热力图
数据源要求GeoJSON,开启 clusterGeoJSON,不聚合
能否点选单个点展开后可点不可,密度是像素
表达什么数量与分布强度与趋势
适合的缩放中低缩放中低缩放,放大后应淡出
样式属性前缀circle-* / text-*heatmap-*

注意事项

同一个源不能既聚合又做热力

cluster: true 的源里只有聚合点,热力图层拿不到原始点, 画出来只有几十个模糊光斑。需要两种视图时,建两份源指向同一份数据, 用图层显隐切换。

聚合计数的文字取错字段

point_count 是数字,直接给 text-field 会报类型错误。用 point_count_abbreviated(字符串,超过千位显示 1.2k), 或者 ['to-string', ['get', 'point_count']]。

热力图遮住底图道路

热力图层是不透明的色块叠加。heatmap-opacity 保持在 0.6 到 0.8, 并在缩放超过一定级别后把图层 visibility 设为 none, 既保留趋势又让底图细节透出来。

引用本文

经纬志(2026)。《聚合与热力图》。https://www.jingweizhi.com/maplibre/tutorial/cluster-heatmap.html(访问日期 2026-11-03)。