Cluster Points with Custom Styling ​
Group dense point data into clusters that show the count, and expand to individual points on click.
Enable Clustering on a Source ​
javascript
map.addSource('points', {
type: 'geojson',
data: geojsonData,
cluster: true, // enable clustering
clusterMaxZoom: 14, // max zoom to cluster at
clusterRadius: 50, // radius (pixels) to cluster within
});Cluster Layers ​
javascript
// Cluster bubbles — sized by count
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'points',
filter: ['has', 'point_count'],
paint: {
'circle-radius': [
'step', ['get', 'point_count'],
15, // radius for count < 10
10, 20, // radius for count >= 10
100, 28 // radius for count >= 100
],
'circle-color': [
'step', ['get', 'point_count'],
'#3b82f6',
10, '#f97316',
100, '#ef4444'
],
}
});
// Count labels
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'points',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-size': 13,
},
paint: { 'text-color': '#fff' }
});
// Unclustered individual points
map.addLayer({
id: 'point',
type: 'circle',
source: 'points',
filter: ['!', ['has', 'point_count']],
paint: { 'circle-radius': 6, 'circle-color': '#22c55e' }
});Expand Cluster on Click ​
javascript
map.on('click', 'clusters', (e) => {
const feature = map.queryRenderedFeatures(e.point, { layers: ['clusters'] })[0];
const clusterId = feature.properties.cluster_id;
map.getSource('points').getClusterExpansionZoom(clusterId, (err, zoom) => {
if (err) return;
map.easeTo({
center: feature.geometry.coordinates,
zoom: zoom
});
});
});Complete Example ​
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link href="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.css" rel="stylesheet" />
<script src="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.js"></script>
<style>#map { height: 500px; width: 100%; }</style>
</head>
<body>
<div id="map"></div>
<script>
const map = new mapmetricsgl.Map({
container: 'map',
style: '<StyleFile_URL_with_Token>',
center: [10, 50],
zoom: 3
});
map.on('load', () => {
map.addSource('points', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: Array.from({ length: 100 }, (_, i) => ({
type: 'Feature',
properties: { id: i },
geometry: { type: 'Point', coordinates: [Math.random() * 40 - 10, Math.random() * 25 + 38] }
}))
},
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
map.addLayer({
id: 'clusters', type: 'circle', source: 'points',
filter: ['has', 'point_count'],
paint: {
'circle-radius': ['step', ['get', 'point_count'], 15, 10, 22, 100, 30],
'circle-color': ['step', ['get', 'point_count'], '#3b82f6', 10, '#f97316', 100, '#ef4444'],
'circle-stroke-width': 2, 'circle-stroke-color': '#fff'
}
});
map.addLayer({
id: 'cluster-count', type: 'symbol', source: 'points',
filter: ['has', 'point_count'],
layout: { 'text-field': '{point_count_abbreviated}', 'text-size': 13, 'text-font': ['Noto Sans Medium'] },
paint: { 'text-color': '#fff' }
});
map.addLayer({
id: 'unclustered-point', type: 'circle', source: 'points',
filter: ['!', ['has', 'point_count']],
paint: { 'circle-radius': 6, 'circle-color': '#22c55e', 'circle-stroke-width': 2, 'circle-stroke-color': '#fff' }
});
map.on('click', 'clusters', (e) => {
const f = map.queryRenderedFeatures(e.point, { layers: ['clusters'] })[0];
map.getSource('points').getClusterExpansionZoom(f.properties.cluster_id, (err, zoom) => {
if (!err) map.easeTo({ center: f.geometry.coordinates, zoom });
});
});
});
</script>
</body>
</html>jsx
import React, { useEffect, useRef } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const pointsData = {
type: 'FeatureCollection',
features: Array.from({ length: 100 }, (_, i) => ({
type: 'Feature',
properties: { id: i },
geometry: {
type: 'Point',
coordinates: [Math.random() * 40 - 10, Math.random() * 25 + 38]
}
}))
};
const HtmlClusters = () => {
const mapContainer = useRef(null);
const map = useRef(null);
useEffect(() => {
if (map.current) return;
map.current = new mapmetricsgl.Map({
container: mapContainer.current,
style: '<StyleFile_URL_with_Token>',
center: [10, 50],
zoom: 3
});
map.current.on('load', () => {
map.current.addSource('points', {
type: 'geojson',
data: pointsData,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
map.current.addLayer({
id: 'clusters', type: 'circle', source: 'points',
filter: ['has', 'point_count'],
paint: {
'circle-radius': ['step', ['get', 'point_count'], 15, 10, 22, 100, 30],
'circle-color': ['step', ['get', 'point_count'], '#3b82f6', 10, '#f97316', 100, '#ef4444'],
'circle-stroke-width': 2, 'circle-stroke-color': '#fff'
}
});
map.current.addLayer({
id: 'cluster-count', type: 'symbol', source: 'points',
filter: ['has', 'point_count'],
layout: { 'text-field': '{point_count_abbreviated}', 'text-size': 13, 'text-font': ['Noto Sans Medium'] },
paint: { 'text-color': '#fff' }
});
map.current.addLayer({
id: 'unclustered-point', type: 'circle', source: 'points',
filter: ['!', ['has', 'point_count']],
paint: { 'circle-radius': 6, 'circle-color': '#22c55e', 'circle-stroke-width': 2, 'circle-stroke-color': '#fff' }
});
map.current.on('click', 'clusters', (e) => {
const f = map.current.queryRenderedFeatures(e.point, { layers: ['clusters'] })[0];
map.current.getSource('points').getClusterExpansionZoom(f.properties.cluster_id, (err, zoom) => {
if (!err) map.current.easeTo({ center: f.geometry.coordinates, zoom });
});
});
});
return () => { map.current?.remove(); map.current = null; };
}, []);
return <div ref={mapContainer} style={{ height: '500px', width: '100%' }} />;
};
export default HtmlClusters;For more information, visit the MapMetrics GitHub repository.