Cluster points
Thousands of points drawn at once overlap and hide each other. A clustered GeoJSON source replaces each group of nearby points with one cluster point. As the user zooms in, clusters split into smaller clusters and then into single points.
Enable clustering
Section titled “Enable clustering”Set cluster = true in the options block of rememberGeoJsonSource:
val state = rememberMapState { val earthquakes = rememberGeoJsonSource( GeoJsonData.Uri( "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_month.geojson" ) ) { cluster = true clusterRadius = 40 clusterMaxZoom = 10 }}MaplibreMap(state = state)GeoJsonOptions.clusterRadius sets how close points must be to
join a cluster. A larger radius makes fewer, bigger clusters.
GeoJsonOptions.clusterMaxZoom is the last zoom level with
clusters. When the user zooms in further, the map shows every point.
Draw clusters and single points
Section titled “Draw clusters and single points”A clustered source holds two kinds of point features. Each cluster has a
point_count property with the number of points in it, and a
point_count_abbreviated property with the same number shortened for labels,
such as 1.2k. Single points keep their own properties and have no
point_count.
Use feature.has("point_count") as a layer filter to draw each kind with its
own layers:
val state = rememberMapState { val pointCount = feature["point_count"].asNumber()
// Clusters: bigger and warmer as they hold more points CircleLayer( id = "earthquake-clusters", source = earthquakes, filter = feature.has("point_count"), radius = step(pointCount, const(14.dp), 25 to const(20.dp), 100 to const(28.dp)), color = step( pointCount, const(Color(0xFF4FC3F7)), 25 to const(Color(0xFFFFB74D)), 100 to const(Color(0xFFE57373)), ), )
// The number of points on each cluster SymbolLayer( id = "earthquake-cluster-counts", source = earthquakes, filter = feature.has("point_count"), textField = feature["point_count_abbreviated"].convertToString(), // A font that the base style's glyphs provide textFont = const(listOf("Noto Sans Regular")), )
// Points that are not in a cluster CircleLayer( id = "earthquake-points", source = earthquakes, filter = !feature.has("point_count"), radius = const(4.dp), color = const(Color(0xFF4FC3F7)), strokeWidth = const(1.dp), strokeColor = const(Color.White), )}MaplibreMap(state = state)step picks a radius and a color for each range of
point_count. Expressions in Kotlin explains
step and filters. textFont must name a font that the base style provides. The
OpenFreeMap styles provide Noto Sans Regular.
To add up a value from the points in each cluster, such as a total magnitude,
set GeoJsonOptions.clusterProperties.
Zoom into a cluster on click
Section titled “Zoom into a cluster on click”When the user clicks a cluster, you can zoom the map in until the cluster
splits. GeoJsonSourceHandle.getClusterExpansionZoom returns that
zoom level. In the map content, LocalMapState gives the map’s
state. Get the source’s handle from mapState.style.sources, then move the
camera to the cluster:
val state = rememberMapState { val mapState = checkNotNull(LocalMapState.current) val scope = rememberCoroutineScope()
CircleLayer( id = "earthquake-clusters", source = earthquakes, filter = feature.has("point_count"), onClick = { features -> val cluster = features.first() scope.launch { val handle = mapState.style.sources[earthquakes] ?: return@launch val zoom = handle.getClusterExpansionZoom(cluster) ?: return@launch val center = (cluster.geometry as? Point)?.coordinates ?: return@launch mapState.animateCamera(CameraUpdate(center = center, zoom = zoom)) } ClickResult.Consume }, )}MaplibreMap(state = state)The handle functions suspend, so the click handler launches a coroutine.
Handle gestures and clicks
covers layer click handlers.
GeoJsonSourceHandle.getClusterLeaves returns the points inside a
cluster, for example to list them in a panel.