Skip to main content

High-Performance Point Clustering

The ClusterController module allows rendering tens of thousands of spatial coordinates smoothly at 60 FPS by leveraging the Supercluster spatial index.


Key Highlights​

  • Massive datasets: handles 10,000+ points dynamically, indexed in under 15ms.
  • Custom badges: badgeRenderer returns a style object (color, radius, and optional border) based on cluster count, or a custom weighted value.
  • Weighted clustering: aggregate a custom numeric property across clustered points instead of just counting them.
  • Safe dataset updates: setPoints() swaps or refreshes coordinates using MapLibre's setData under the hood, preventing canvas flicker, duplicate layers, or broken Supercluster state.

Default: Count-Based Clustering​

By default, ClusterController aggregates based on the native feature count (point_count), and badgeRenderer receives that count:

import { ClusterController } from '@sovereignsolutions/ss-map-gl';

const clusterController = new ClusterController(mapEngine, {
clusterRadius: 50,
clusterMaxZoom: 14,
badgeRenderer: (count) => {
// badgeRenderer receives the count of points in the cluster
if (count < 100) return { color: '#6366f1', radius: 18 };
if (count < 500) return { color: '#3b82f6', radius: 24 };
return { color: '#ef4444', radius: 32, borderWidth: 3, borderColor: '#ffffff' };
},
});

Custom Weighting​

Cluster based on the sum of a custom property instead of point count with clusterValueProperty and clusterProperties:

import { ClusterController } from '@sovereignsolutions/ss-map-gl';

const weightedClusterController = new ClusterController(mapEngine, {
clusterRadius: 50,
clusterMaxZoom: 14,
clusterValueProperty: 'total_value',
clusterProperties: {
// Sum the custom "value" of clustered pins, falling back to 1 if missing
total_value: ['+', ['coalesce', ['get', 'value'], 1]],
},
badgeRenderer: (value) => {
// badgeRenderer receives the custom total_value sum
if (value < 1000) return { color: '#6366f1', radius: 18 };
return { color: '#ef4444', radius: 32 };
},
});

Cluster Data Format​

Input points for the clustering engine use the following format:

interface ClusterPoint {
coordinates: [number, number]; // [longitude, latitude]
value?: number; // Optional custom value/weight property
properties?: Record<string, any>; // Optional user-defined properties
}

Dataset Updates​

Consume the safe point update API to dynamically swap or refresh coordinates. This operation uses MapLibre's setData under the hood, preventing canvas flicker, duplicate layers, or broken Supercluster state:

// Replace point dataset safely (displays count by default)
clusterController.setPoints([
{ coordinates: [78.96, 20.59] },
{ coordinates: [79.00, 20.60] },
]);

Cleanup​

Source: Cleanup & Unmount Behavior in the SDK README.

// Unmount Cluster Controller layers and event listeners
clusterController.destroy();

Performance & QA Evidence​

Verified with a 10,000+ coordinate dataset on Chrome 126.0, Windows 11 Enterprise (Intel Core i7, 32GB RAM):

  • Point indexing via Supercluster finishes in under 15ms.
  • Frame rates remain at a solid 60 FPS during map pans and zooms.
  • Tapping a cluster produces a smooth easeTo transition (duration 800ms) into the expanded view.
  • Calling .destroy() and unmounting is confirmed (via browser devtools) to fully reclaim map memory - no leaked layers or listeners.

See also: GeoJSON Import & Export.