聚合与热力图
几万个点直接画圆,屏幕会糊成一片,性能也会掉下来。聚合把邻近的点折叠成带计数的圆, 热力图把点密度渲染成颜色渐变。两者解决同一个问题,呈现方式不同。
客户端聚合
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,开启 cluster | GeoJSON,不聚合 |
| 能否点选单个点 | 展开后可点 | 不可,密度是像素 |
| 表达什么 | 数量与分布 | 强度与趋势 |
| 适合的缩放 | 中低缩放 | 中低缩放,放大后应淡出 |
| 样式属性前缀 | 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)。