Skip to content

Cluster

Clustering groups nearby points into a single bubble at low zoom levels. As users zoom in, clusters expand to reveal individual features. MapLibre handles clustering natively, with no JavaScript or server-side code needed.

200 random points around Paris. Clusters show a count and collapse/expand as you zoom.

How clustering works

flowchart LR
    LOW["Low zoom<br/><small>points group into<br/>large clusters</small><br/>● 142  ● 38  ● 20"]
    MID["Mid zoom<br/><small>clusters split into<br/>smaller clusters</small><br/>● 45  ● 97  ● 12  ● 26"]
    HIGH["High zoom<br/><small>individual points<br/>appear</small><br/>· · · · ·  · · · · ·"]
    LOW -- "zoom in" --> MID -- "zoom in" --> HIGH

Clustering is a source-level feature, you enable it on GeojsonSourceProperties, then add separate layers for clusters and individual points.

Full setup

// 1. Add source with clustering enabled, passing the data inline.
await controller.addSource(
  'events',
  GeojsonSourceProperties(
    data: featureCollection,   // inline GeoJSON map, or a URL string
    cluster: true,
    clusterMaxZoom: 14,   // stop clustering above this zoom
    clusterRadius: 50,    // pixel radius to group into one cluster
  ),
);

// 2. Cluster circles: sized by point count
await controller.addCircleLayer(
  'events', 'cluster-circles',
  CircleLayerProperties(
    circleRadius: [
      Expressions.step,
      [Expressions.get, 'point_count'],
      18,        // base size
      10,  24,   // >= 10 points
      50,  32,   // >= 50 points
      100, 42,   // >= 100 points
    ],
    circleColor: [
      Expressions.step,
      [Expressions.get, 'point_count'],
      '#51bbd6',
      10,  '#f1f075',
      50,  '#f28cb1',
      100, '#E74C3C',
    ],
    circleOpacity: 0.85,
    circleStrokeWidth: 2,
    circleStrokeColor: '#ffffff',
  ),
  filter: ['has', 'point_count'],  // only cluster features
);

// 3. Cluster count label
await controller.addSymbolLayer(
  'events', 'cluster-count',
  SymbolLayerProperties(
    textField: [Expressions.get, 'point_count_abbreviated'],
    textSize: 13,
    textColor: '#1a1a2e',
    textAllowOverlap: true,
  ),
  filter: ['has', 'point_count'],
);

// 4. Individual (unclustered) points
await controller.addCircleLayer(
  'events', 'unclustered-point',
  const CircleLayerProperties(
    circleRadius: 5,
    circleColor: '#296CA8',
    circleStrokeWidth: 1.5,
    circleStrokeColor: '#ffffff',
  ),
  filter: ['!', ['has', 'point_count']],  // only non-cluster features
);

Cluster properties on GeoJSON source

Property Default Description
cluster false Enable clustering
clusterMaxZoom 14 Zoom at which clustering stops
clusterRadius 50 Pixel radius to group points

Cluster feature properties

When clustering is enabled, cluster features have extra properties:

Property Description
point_count Number of points in the cluster
point_count_abbreviated Abbreviated count: "142", "1.2k"
cluster_id Internal cluster ID
cluster Always true for cluster features

Filters for cluster vs. point layers

// Show only cluster bubbles
filter: ['has', 'point_count']

// Show only individual points
filter: ['!', ['has', 'point_count']]

Always add these filters. Without them, both layers apply to all features.

Update cluster data

// Refresh cluster data (e.g., after fetching from API)
await controller.setGeoJsonSource('events', newFeatureCollection);
// Clusters recalculate automatically

Key APIs