Clustering and heatmaps
Quick answer
This tutorial shows two ways to tame tens of thousands of points: built-in client-side clustering on a GeoJSON source and heatmap density rendering. It walks through cluster, clusterRadius, clusterMaxZoom and clusterProperties, the point_count_abbreviated label, the getClusterExpansionZoom click handler, and the heatmap-* paint properties driven by heatmap-density. Afterwards you can pick the right visualization and build zoom-aware layers.
Key points
- Enable cluster: true with clusterRadius and clusterMaxZoom on the GeoJSON source, then filter layers with ['has', 'point_count'].
- Call getClusterExpansionZoom(clusterId) inside a click handler to zoom exactly to the level that splits a cluster.
- Style heatmap-color only from ['heatmap-density'] and raise heatmap-radius with zoom via interpolate.
Example
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'
}
});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
});
});
});
Notes
- A source with cluster: true returns only cluster points, so a heatmap layer on the same source shows a few blurred blobs; build two sources instead.
- point_count is a number and breaks text-field, which needs point_count_abbreviated or ['to-string', ['get', 'point_count']].
English edition written for Atlaslog. Code is taken from the original Chinese page and runs unchanged.
Atlaslog (2026). Clustering and heatmaps. https://www.jingweizhi.com/en/maplibre/tutorials/cluster-heatmap.html (accessed 2026-11-03).