Skip to content

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.