← Files MapboxARCHIVED FILE

skills/mapbox-data-visualization-patterns/references/clustering.md

3.84 KB · Sep 30, 2026 · 23:11 UTC

↓ Download file

# Clustering (Point Density)

**Best for:** Grouping nearby points, aggregated counts, large point datasets

**Pattern:** Client-side clustering for visualization

Clustering is a valuable point density visualization technique alongside heat maps. Use clustering when you want **discrete grouping with exact counts** rather than a continuous density visualization.

```javascript
map.on('load', () => {
  // Add data source with clustering enabled
  map.addSource('locations', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: [
        // Your point features
      ]
    },
    cluster: true,
    clusterMaxZoom: 14, // Max zoom to cluster points
    clusterRadius: 50 // Radius of each cluster (default 50)
  });

  // Clustered circles - styled by point count
  map.addLayer({
    id: 'clusters',
    type: 'circle',
    source: 'locations',
    filter: ['has', 'point_count'],
    paint: {
      // Color clusters by count (step expression)
      'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 10, '#f1f075', 30, '#f28cb1'],
      // Size clusters by count
      'circle-radius': ['step', ['get', 'point_count'], 20, 10, 30, 30, 40]
    }
  });

  // Cluster count labels
  map.addLayer({
    id: 'cluster-count',
    type: 'symbol',
    source: 'locations',
    filter: ['has', 'point_count'],
    layout: {
      'text-field': ['get', 'point_count_abbreviated'],
      'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
      'text-size': 12
    }
  });

  // Individual unclustered points
  map.addLayer({
    id: 'unclustered-point',
    type: 'circle',
    source: 'locations',
    filter: ['!', ['has', 'point_count']],
    paint: {
      'circle-color': '#11b4da',
      'circle-radius': 6,
      'circle-stroke-width': 1,
      'circle-stroke-color': '#fff'
    }
  });

  // Click handler to expand clusters
  map.on('click', 'clusters', (e) => {
    const features = map.queryRenderedFeatures(e.point, {
      layers: ['clusters']
    });
    const clusterId = features[0].properties.cluster_id;

    // Get cluster expansion zoom
    map.getSource('locations').getClusterExpansionZoom(clusterId, (err, zoom) => {
      if (err) return;

      map.easeTo({
        center: features[0].geometry.coordinates,
        zoom: zoom
      });
    });
  });

  // Change cursor on hover
  map.on('mouseenter', 'clusters', () => {
    map.getCanvas().style.cursor = 'pointer';
  });
  map.on('mouseleave', 'clusters', () => {
    map.getCanvas().style.cursor = '';
  });
});
```

**Advanced: Custom Cluster Properties**

```javascript
map.addSource('locations', {
  type: 'geojson',
  data: data,
  cluster: true,
  clusterMaxZoom: 14,
  clusterRadius: 50,
  // Calculate custom cluster properties
  clusterProperties: {
    // Sum total values
    sum: ['+', ['get', 'value']],
    // Calculate max value
    max: ['max', ['get', 'value']]
  }
});

// Use custom properties in styling
'circle-color': [
  'interpolate',
  ['linear'],
  ['get', 'sum'],
  0,
  '#51bbd6',
  100,
  '#f1f075',
  1000,
  '#f28cb1'
];
```

**When to use clustering vs heatmaps:**

| Use Case                         | Clustering                       | Heatmap                    |
| -------------------------------- | -------------------------------- | -------------------------- |
| **Visual style**                 | Discrete circles with counts     | Continuous gradient        |
| **Interaction**                  | Click to expand/zoom             | Visual density only        |
| **Data granularity**             | Exact counts visible             | Approximate density        |
| **Best for**                     | Store locators, event listings   | Crime maps, incident areas |
| **Performance with many points** | Excellent (groups automatically) | Good                       |
| **User understanding**           | Clear (numbered clusters)        | Intuitive (heat analogy)   |

SHA-256: b0207f36a8a0ed5151777436b3ead14b9373f56a6ba6c5fecc0028d852a9c688