Implementing Point Clustering
Clustering is essential when dealing with high-density point data. It prevents visual clutter and improves performance by grouping nearby points into a single "cluster" symbol. In AurOpenlayers, clustering is handled declaratively within the layer descriptor.
This guide walks you through setting up a clustered layer, defining dynamic styles for different cluster sizes, and adding interactions.
1. Define the Data Model
First, ensure your data model contains the necessary coordinates. The framework will use these to generate geometries.
export interface Station {
id: string;
name: string;
lat: number;
lng: number;
}
2. Configure the Clustered Layer
To enable clustering, use the ClusteredLayerDescriptor. The key addition compared to a standard vector layer is the cluster configuration object.
const clusterLayer: ClusteredLayerDescriptor<Station, Point, any> = {
id: 'stations-clusters',
cluster: {
distance: 40, // Distance in pixels within which features will be clustered
minDistance: 20, // Minimum distance between clusters
},
feature: {
id: (model) => model.id,
geometry: {
fromModel: (model) => new Point(fromLonLat([model.lng, model.lat])),
applyGeometryToModel: (model, geom) => {
const coords = toLonLat((geom as Point).getCoordinates());
return { ...model, lng: coords[0], lat: coords[1] };
}
},
// Styles and interactions go here
}
};
3. Implement Dynamic Styling
A clustered layer needs to distinguish between a single point and a group of points. The style.base function receives a ClusterModel which contains an array of the original models.
style: {
base: (cluster) => {
const size = cluster.features.length;
// Logic for a single item
if (size === 1) {
return {
color: '#2563eb',
radius: 8,
label: cluster.features[0].name
};
}
// Logic for a cluster
return {
color: size > 10 ? '#dc2626' : '#f59e0b',
radius: 12 + Math.min(size, 10),
label: String(size)
};
},
render: (opts) => new Style({
image: new CircleStyle({
radius: opts.radius,
fill: new Fill({ color: opts.color }),
stroke: new Stroke({ color: '#fff', width: 2 })
}),
text: new Text({
text: opts.label,
fill: new Fill({ color: '#fff' }),
font: 'bold 12px sans-serif'
})
})
}
4. Add Cluster Interactions
Commonly, users want to click a cluster to zoom in and see the individual points. You can implement this using the select interaction.
interactions: {
select: {
enabled: true,
onSelect: ({ items, map }) => {
if (items.length === 0) return;
const cluster = items[0].model;
if (cluster.features.length > 1) {
// Zoom to the cluster extent
const extent = createEmpty();
cluster.features.forEach(f => {
extend(extent, fromLonLat([f.lng, f.lat]));
});
map.getView().fit(extent, { duration: 500, padding: [50, 50, 50, 50] });
} else {
console.log('Selected single station:', cluster.features[0]);
}
return true;
}
}
}
5. Integrating with the Map Host
Finally, pass your descriptor to the mff-map-host component via the MapHostConfig.
export class MyMapComponent {
readonly mapConfig: MapHostConfig = {
schema: {
layers: [clusterLayer]
},
view: {
centerLonLat: [37.61, 55.75],
zoom: 10
}
};
onReady(ctx: MapContext) {
const api = ctx.layers['stations-clusters'];
api.setModels(this.myStationData);
}
}
Key Considerations
- Performance: For datasets with 10,000+ points, adjust the
distanceproperty to reduce the number of rendered features. - Z-Index: Ensure clustered layers are positioned correctly relative to base maps or other vector data using the order in the
layersarray. - State Styling: You can still use the
statesobject (e.g.,HOVERorSELECTED) to highlight clusters when the user interacts with them.