A very fast geospatial point clustering library for browsers and Node.js environments.
npx @tessl/cli install tessl/npm-supercluster@8.0.0Supercluster is a very fast JavaScript library for geospatial point clustering for browsers and Node.js environments. It provides high-performance clustering of millions of points with configurable zoom levels, hierarchical cluster navigation, and custom property aggregation through map/reduce functions.
npm install superclusterimport Supercluster from 'supercluster';For CommonJS:
const Supercluster = require('supercluster');CDN/Browser (ES Module):
import Supercluster from 'https://esm.run/supercluster';Script tag (UMD):
<script src="https://unpkg.com/supercluster@8.0.1/dist/supercluster.min.js"></script>
<!-- Creates global Supercluster variable -->import Supercluster from 'supercluster';
// Create clustering index with default options
const index = new Supercluster({
radius: 40,
maxZoom: 16,
minPoints: 2
});
// Load GeoJSON Point features
const points = [
{
type: 'Feature',
properties: { name: 'Location A' },
geometry: {
type: 'Point',
coordinates: [-73.97, 40.77] // [longitude, latitude]
}
},
{
type: 'Feature',
properties: { name: 'Location B' },
geometry: {
type: 'Point',
coordinates: [-73.96, 40.78]
}
}
];
// Build the clustering index
index.load(points);
// Get clusters and points for a bounding box and zoom level
const clusters = index.getClusters([-74.0, 40.7, -73.9, 40.8], 10);Supercluster is built around several key components:
Create and configure a new Supercluster instance with customizable clustering behavior.
/**
* Creates a new Supercluster instance
* @param options - Configuration options for clustering behavior
*/
constructor(options?: SuperclusterOptions);
interface SuperclusterOptions {
/** Minimum zoom level at which clusters are generated (default: 0) */
minZoom?: number;
/** Maximum zoom level at which clusters are generated (default: 16) */
maxZoom?: number;
/** Minimum number of points to form a cluster (default: 2) */
minPoints?: number;
/** Cluster radius in pixels (default: 40) */
radius?: number;
/** Tile extent - radius is calculated relative to this value (default: 512) */
extent?: number;
/** Size of the KD-tree leaf node, affects performance (default: 64) */
nodeSize?: number;
/** Whether to log timing info (default: false) */
log?: boolean;
/** Whether to generate numeric ids for input features in vector tiles (default: false) */
generateId?: boolean;
/** Function that returns cluster properties corresponding to a single point (default: props => props) */
map?: (props: any) => any;
/** Function that merges properties of two clusters into one (default: null) */
reduce?: (accumulated: any, props: any) => void;
}Usage Examples:
// Basic clustering with default options
const index = new Supercluster();
// Custom clustering configuration
const index = new Supercluster({
radius: 60, // Larger cluster radius
maxZoom: 14, // Stop clustering at zoom 14
minPoints: 5, // Require at least 5 points to form cluster
extent: 256 // Use 256-pixel tile extent
});
// With property aggregation
const index = new Supercluster({
map: (props) => ({ sum: props.value }),
reduce: (accumulated, props) => { accumulated.sum += props.sum; }
});Load GeoJSON Point features into the clustering index.
/**
* Loads an array of GeoJSON Point features and builds the clustering index
* @param points - Array of GeoJSON Feature objects with Point geometry
* @returns The Supercluster instance for chaining
*/
load(points: GeoJSONFeature[]): Supercluster;
interface GeoJSONFeature {
type: 'Feature';
properties: any;
geometry: {
type: 'Point';
coordinates: [number, number]; // [longitude, latitude]
};
id?: any;
}Usage Examples:
const points = [
{
type: 'Feature',
properties: { name: 'Coffee Shop', category: 'restaurant' },
geometry: { type: 'Point', coordinates: [-73.97, 40.77] }
},
{
type: 'Feature',
properties: { name: 'Park', category: 'recreation' },
geometry: { type: 'Point', coordinates: [-73.96, 40.78] }
}
];
index.load(points);Get clusters and individual points within a geographic bounding box at a specific zoom level.
/**
* Returns clusters and points within a bounding box at a given zoom level
* @param bbox - Bounding box as [westLng, southLat, eastLng, northLat]
* @param zoom - Integer zoom level
* @returns Array of GeoJSON Features (clusters and individual points)
*/
getClusters(bbox: [number, number, number, number], zoom: number): GeoJSONFeature[];
interface ClusterFeature extends GeoJSONFeature {
properties: {
cluster: true;
cluster_id: number;
point_count: number;
point_count_abbreviated: string;
[key: string]: any; // Custom properties from reduce function
};
}Usage Examples:
// Get all clusters/points visible in New York area at zoom level 10
const bbox = [-74.1, 40.6, -73.8, 40.9];
const clusters = index.getClusters(bbox, 10);
// Handle clusters vs individual points
clusters.forEach(feature => {
if (feature.properties.cluster) {
console.log(`Cluster with ${feature.properties.point_count} points`);
} else {
console.log(`Individual point: ${feature.properties.name}`);
}
});
// World-wide view at low zoom
const worldClusters = index.getClusters([-180, -85, 180, 85], 2);Generate vector tile data compatible with vector tile rendering systems.
/**
* Returns a geojson-vt-compatible tile object for given tile coordinates
* @param z - Zoom level
* @param x - Tile x coordinate
* @param y - Tile y coordinate
* @returns Tile object with features array, or null if empty
*/
getTile(z: number, x: number, y: number): TileObject | null;
interface TileObject {
features: TileFeature[];
}
interface TileFeature {
type: 1; // Point type
geometry: [[number, number]]; // Tile-space coordinates
tags: any; // Feature properties
id?: any;
}Usage Examples:
// Get tile data for zoom 10, tile coordinates (512, 384)
const tile = index.getTile(10, 512, 384);
if (tile) {
console.log(`Tile contains ${tile.features.length} features`);
// Process tile features for rendering
tile.features.forEach(feature => {
const [x, y] = feature.geometry[0]; // Tile pixel coordinates
const properties = feature.tags;
// Render feature at tile coordinates
});
}Navigate cluster hierarchies by getting direct children of a cluster.
/**
* Returns the direct children of a cluster on the next zoom level
* @param clusterId - Cluster ID from cluster properties
* @returns Array of GeoJSON Features (child clusters and points)
* @throws Error if cluster ID is invalid
*/
getChildren(clusterId: number): GeoJSONFeature[];Usage Examples:
// Get children of a specific cluster
const clusterId = 164; // From cluster_id property
const children = index.getChildren(clusterId);
console.log(`Cluster ${clusterId} has ${children.length} children`);
children.forEach(child => {
if (child.properties.cluster) {
console.log(`Child cluster with ${child.properties.point_count} points`);
} else {
console.log(`Individual point: ${child.properties.name}`);
}
});Get individual points within a cluster with pagination support.
/**
* Returns all individual points within a cluster with pagination
* @param clusterId - Cluster ID from cluster properties
* @param limit - Number of points to return (default: 10)
* @param offset - Number of points to skip (default: 0)
* @returns Array of GeoJSON Features (individual points only)
*/
getLeaves(clusterId: number, limit?: number, offset?: number): GeoJSONFeature[];Usage Examples:
// Get first 10 individual points from a cluster
const clusterId = 164;
const firstBatch = index.getLeaves(clusterId, 10, 0);
// Get next 10 points (pagination)
const secondBatch = index.getLeaves(clusterId, 10, 10);
// Get all points in cluster
const allPoints = index.getLeaves(clusterId, Infinity, 0);
console.log(`Cluster contains ${allPoints.length} total points`);Determine the optimal zoom level for expanding a cluster.
/**
* Returns the zoom level at which a cluster expands into multiple children
* @param clusterId - Cluster ID from cluster properties
* @returns Integer zoom level for cluster expansion
*/
getClusterExpansionZoom(clusterId: number): number;Usage Examples:
// Implement "click to zoom" functionality
const clusterId = 164;
const expansionZoom = index.getClusterExpansionZoom(clusterId);
console.log(`Zoom to level ${expansionZoom} to expand this cluster`);
// Use in map interaction
map.on('click', 'clusters-layer', (e) => {
const clusterId = e.features[0].properties.cluster_id;
const expansionZoom = index.getClusterExpansionZoom(clusterId);
map.easeTo({
center: e.lngLat,
zoom: expansionZoom
});
});Supercluster throws specific errors for invalid operations:
try {
const children = index.getChildren(999999); // Invalid cluster ID
} catch (error) {
console.error(error.message); // "No cluster with the specified id."
}
// Graceful handling of edge cases
const emptyIndex = new Supercluster();
emptyIndex.load([]); // Handles empty arrays gracefully
const emptyClusters = emptyIndex.getClusters([-180, -85, 180, 85], 0); // Returns []const index = new Supercluster({
map: (props) => ({
sum: props.value || 0,
categories: [props.category].filter(Boolean)
}),
reduce: (accumulated, props) => {
accumulated.sum += props.sum;
accumulated.categories = accumulated.categories.concat(props.categories);
}
});
index.load(points);
// Clusters will have aggregated properties
const clusters = index.getClusters(bbox, zoom);
clusters.forEach(cluster => {
if (cluster.properties.cluster) {
console.log(`Total value: ${cluster.properties.sum}`);
console.log(`Categories: ${cluster.properties.categories.join(', ')}`);
}
});// For large datasets, tune performance parameters
const index = new Supercluster({
radius: 60, // Larger radius reduces cluster count
nodeSize: 128, // Larger node size for better performance with more data
maxZoom: 14, // Lower max zoom reduces processing levels
extent: 1024 // Higher extent for better precision
});
// Enable logging to monitor performance
const debugIndex = new Supercluster({ log: true });
debugIndex.load(millionsOfPoints); // Will log timing information// Supercluster automatically handles dateline crossing
const pacificBbox = [170, -10, -170, 10]; // Crosses dateline
const clusters = index.getClusters(pacificBbox, 5);
// Returns all relevant clusters on both sides of dateline