Clustering groups points into clusters based on their spatial proximity to one another. Clusters can reveal information that is difficult to visualize when there are hundreds or thousands of points in a map that may overlap or cover each other. Adopt clustering if you want to discover patterns in your data, such as:
- Identify where students live in a city so that the local government can plan appropriate educational and accommodation resources. Clustering allows you to visualize this data without revealing the individual student addresses.
- Visualize the most common type of environmental complaints that are registered across a city so that authorities can plan how to deploy their environmental officers.
- Ensure that police units are evenly spread throughout city so that a rapid response time can be maintained for emergency calls from the public.
In the map below, for example, various power plants of north-west Europe are clustered together so that you can easily see the count of power plants in each region. The size of each cluster is proportional to the number of points in each cluster and is recalculated when the map is zoomed.
Clustering can be defined on a point feature layer authored and published using the ArcGIS Online Map Viewer or ArcGIS Pro. If you add a feature layer from an ArcGIS feature service, web map, or mobile map package to your app, any predefined clustering displays automatically.
If the published point feature layer is not enabled with clustering, or if you want to cluster point graphics, you can use this SDK to define clustering by following the steps detailed in Enable clustering. You can enhance the visual representation of the clusters and display summary statistics of the clustered point attributes (not just the number of points) by using a variety of more complex renderers, labels, and popups, as described in Enhanced statistics and renderers.
Enable clustering
To cluster point-based features or graphics in your app, you need to create a ClusteringFeatureReduction
object that takes a renderer. The renderer determines the cluster's color and shape, but the cluster's size is determined by the number of points in the cluster. So, a cluster with more points is bigger than a cluster with fewer points. You can cluster point features or graphics as follows:
- Create a
SimpleRenderer
using aSimpleMarkerSymbol
. - Create a
ClusteringFeatureReduction
using the simple renderer. - Pass the
ClusteringFeatureReduction
to theFeatureLayer.featureReduction
or theGraphicsOverlay.featureReduction
.
val simpleMarkerSymbol = SimpleMarkerSymbol(color = Color.cyan)
val simpleRenderer = SimpleRenderer(symbol = simpleMarkerSymbol)
val clusteringFeatureReduction = ClusteringFeatureReduction(renderer = simpleRenderer)
featureLayer.featureReduction = clusteringFeatureReduction
// Alternatively, set the feature reduction on the graphics overlay.
graphicsOverlay.featureReduction = clusteringFeatureReduction
The map below contains a single point symbol for the 8000+ power plants in the United States. On the right side, the points are aggregated into clusters using the default clustering values. Every time the mapview's viewpoint changes the clusters may be recalculated and their symbols redrawn.
Label clusters
The number of points in the cluster is stored in a field called "cluster_count" and is updated every time the cluster redraws. You can display this information in the clusters label, as follows:
- Create a
LabelDefinition
that uses aSimpleLabelExpression
of "[cluster_count]" and a blackTextSymbol
. - Set label definition placement.
- Add the label definition to the
ClusteringFeatureReduction
.
// Create a label definition with a simple label expression.
val simpleLabelExpression = SimpleLabelExpression(simpleExpression = "[cluster_count]")
val textSymbol = TextSymbol(
text = "",
color = Color.black,
size = 12.0f,
horizontalAlignment = HorizontalAlignment.Center,
verticalAlignment = VerticalAlignment.Middle
)
val labelDefinition = LabelDefinition(
labelExpression = simpleLabelExpression,
textSymbol = textSymbol
).apply {
placement = LabelingPlacement.PointCenterCenter
}
// Add the label definition to the feature reduction.
clusteringFeatureReduction.labelDefinitions.add(labelDefinition)
The map below shows clusters labeled with a SimpleLabelExpression
.
Configure clusters
The process of clustering groups points into clusters based on an area of influence defined in screen space by the ClusteringFeatureReduction.radius
. This radius has a default value which is 60 device-independent pixels (dpi), but you can set it to a higher value to aggregate points from a larger area or to a lower value to aggregate points from a smaller area. The size of each cluster symbol on the map is proportional to the number of features within the cluster and is recalculated every time the viewpoint changes.
clusterRadius = clusterRadiusOptions[index]
clusteringFeatureReduction.radius = clusterRadius.toDouble()
The map below shows points clustered with a clustering radius of 120.
Enhanced statistics and renderers
You can enhance the information displayed to your users by presenting statistical information, such as the average, max, and mode value of the cluster's point attributes. First, you need to aggregate, or summarize, the cluster's point data, and then you can:
- Apply advanced renderers, such as class break and unique value renderers.
- Configure labels that use simple or Arcade expressions.
- Design user friendly popup text.
Aggregate the point data
First, examine the attributes in your feature layer or graphics overlay and decide which attributes you want to summarize. Next, specify how the attribute values are summarized and store the results in an aggregate field, as follows:
- Create a new
AggregateField
for each statistical operation you want to perform on an attribute. Give it aname
. - Set the
statistic
as the attribute (from the feature layer or graphic overlay) you want to analyze.Field Name - State the
AggregateStatisticType
, such as count, average, mode, and max, that will be applied to thestatistic
.Field Name - Add a collection of your aggregate fields to your
ClusteringFeatureReduction
so they can be used by its renderer, label, and popup definitions.
The code below creates and adds 3 AggregateField
objects to the ClusteringFeatureReduction
. In the first one, for example, the value of the "fuel1" field that appears most often (mode
value) is stored in the "most_common_fuel" aggregate field, the average
value of the "capacity_mw" field is stored in the "average_capacity_mw" aggregate field and so on.
// Create a new clustering feature reduction using the class breaks renderer.
return ClusteringFeatureReduction(classBreaksRenderer).apply {
// Set the feature reduction's aggregate fields. Note that the field names must match the names of fields in the feature layer's dataset.
// The aggregate fields summarize values based on the defined aggregate statistic type.
aggregateFields.add(
AggregateField(
name = "most_common_fuel",
statisticFieldName = "fuel1",
statisticType = AggregateStatisticType.Mode
)
)
aggregateFields.add(
AggregateField(
name = "average_capacity_mw",
statisticFieldName = "capacity_mw",
statisticType = AggregateStatisticType.Average
)
)
aggregateFields.add(
AggregateField(
name = "total_capacity_mw",
statisticFieldName = "capacity_mw",
statisticType = AggregateStatisticType.Sum
)
)
Apply a class breaks renderer
You can apply a renderer to the cluster to display the value of the aggregate fields. Class breaks renderers are useful if you want to apply different colors to ranges of values. You can apply a class breaks renderer to the cluster as follows:
- For each range of values, create a
ClassBreak
and assign aSimpleMarkerSymbol
with a unique color. - Create a
ClassBreaksRenderer
for the aggregate field and provide the collection of class breaks. - Pass the class breaks renderer to the constructor of the
ClusteringFeatureReduction
and pass that toFeatureLayer.featureReduction
orGraphicsOverlay.featureReduction
.
// Create a class breaks renderer to apply to the custom feature reduction.
val classBreaksRenderer = ClassBreaksRenderer().apply {
// Define the field to use for the class breaks renderer.
// Note that this field name must match the name of an aggregate field contained in the clustering feature reduction's aggregate fields property.
fieldName = "total_capacity_mw"
// Add a class break for each intended value range and define a symbol to display for features in that range.
val classBreak1 = ClassBreak(
description = "<5000",
label = "<5000",
minValue = 0.0,
maxValue = 5000.0,
symbol = SimpleMarkerSymbol(color = Color.cyan),
alternateSymbols = listOf(
SimpleMarkerSymbol(color = Color.fromRgba(r = 255, g = 192, b = 203))
)
)
val classBreak2 = ClassBreak(
description = ">5000",
label = ">5000",
minValue = 5001.0,
maxValue = 10000.0,
symbol = SimpleMarkerSymbol(color = Color.fromRgba(r = 62, g = 180, b = 137)),
alternateSymbols = listOf(SimpleMarkerSymbol(color = Color.fromRgba(r = 255, g = 192, b = 203)))
)
val classBreak3 = ClassBreak(
description = ">10000",
label = ">10000",
minValue = 10001.0,
maxValue = 15000.0,
symbol = SimpleMarkerSymbol(color = Color.green),
alternateSymbols = listOf(SimpleMarkerSymbol(color = Color.fromRgba(r = 255, g = 192, b = 203)))
)
val classBreak4 = ClassBreak(
description = ">15000",
label = ">15000",
minValue = 15001.0,
maxValue = 50000.0,
symbol = SimpleMarkerSymbol(color = Color.fromRgba(r = 255, g = 165, b = 0)),
alternateSymbols = listOf(SimpleMarkerSymbol(color = Color.fromRgba(r = 255, g = 192, b = 203)))
)
val classBreak5 = ClassBreak(
description = ">50000",
label = ">50000",
minValue = 50001.0,
maxValue = 1000000.0,
symbol = SimpleMarkerSymbol(color = Color.red),
alternateSymbols = listOf(SimpleMarkerSymbol(color = Color.fromRgba(r = 255, g = 192, b = 203)))
)
classBreaks.addAll(listOf(classBreak1, classBreak2, classBreak3, classBreak4, classBreak5))
// Define a default symbol to use for features that do not fall within any of the ranges defined by the class breaks.
defaultSymbol = SimpleMarkerSymbol().apply { color = Color.red }
}
The map below shows the clusters rendered with a class breaks renderer and reflects the range of total
values. For example, the red color indicates that the sum of the capacity
of all the points in a cluster is greater than 50000 MW.
Configure label expressions
As shown previously, you can label each cluster with the number of points in the cluster. However, now that you have created some aggregate fields, you can use these to label the clusters by configuring a SimpleLabelExpression
or an ArcadeLabelExpression
.
In the map below, the most
aggregate field value of each cluster is displayed in a label by using a SimpleLabelExpression
. For more information about how to format simple expression strings, see Simple expressions.
val classBreaksRenderer = ClassBreaksRenderer()
val clusteringFeatureReduction = ClusteringFeatureReduction(renderer = classBreaksRenderer)
val commonFuelLabelExpression =
SimpleLabelExpression(simpleExpression = "[most_common_fuel]")
In the map below, the total
aggregate field value of each cluster is reformated, concatenated with the "MW", and displayed in a label by using an ArcadeLabelExpression
. For more information about how to format Arcade expression strings, see Arcade expressions.
val labelExpression =
ArcadeLabelExpression(arcadeString = "Concatenate([Text(\$feature.total_capacity_mw, '###,###') + ' MW'])")
Configure popup expressions
You can configure your app to display a popup when the user clicks on a cluster. By default, the cluster's popup displays its "point_count" value; the number of points in the cluster. If the ClusteringFeatureReduction
has aggregate fields, the cluster popup will display the aggregate field values. However, if you want to present a more user-friendly text, you can create an Arcade expression, as follows:
- Create a
PopupDefinition
using theClusteringFeatureReduction
. - Assign the popup definition to the clustering feature reduction.
- Specify a title and remove all elements from the popup definition.
- Create a
PopupExpression
, provide an Arcade expression, and specify that the return type is string. In this example, the Arcade expression reformats and concatenates a few aggregate fields representing the average and total capacities, along with some helpful text. - Create an
ExpressionPopupElement
using the popup expression. - Add the expression popup element to the popup definition's collection of elements.
// Set the popup definition for the custom feature reduction.
clusteringFeatureReduction.popupDefinition = PopupDefinition(clusteringFeatureReduction).apply {
this.title = "Cluster summary"
this.elements.clear()
}
val popupExpression1 = PopupExpression().apply {
expression = "var result = Concatenate(['The power plants in this cluster have an average capacity of ' +" +
"Text(\$feature.average_capacity_mw, '#.00') + ' megawatts and produce a total of ' +" +
"Text(\$feature.total_capacity_mw, '###,###.00') + ' megawatts of power.']);" +
"return { type : 'text', text : result };"
returnType = PopupExpressionReturnType.String
}
val expressionPopupElement1 = ExpressionPopupElement(popupExpression = popupExpression1)
// Optionally, create additional popup expressions.
val popupExpression2 = PopupExpression().apply {
expression = "var result = Concatenate(['Most power plants in this cluster generate power from ' + " +
"\$feature.most_common_fuel + '.']);" +
"return { type : 'text', text : result };"
}
val expressionPopupElement2 = ExpressionPopupElement(popupExpression = popupExpression2)
clusteringFeatureReduction.popupDefinition?.elements?.addAll(listOf(expressionPopupElement1, expressionPopupElement2))
For more information about how to add popups to your app, see the PopupView component and example code that is provided by the ArcGIS Maps SDK for Kotlin Toolkit
The map below shows a popup configured with a PopupExpression
.