Display clusters

View on GitHub

Display a web map with a point feature layer that has feature reduction enabled to aggregate points into clusters.

Image of display clusters

Use case

Feature clustering can be used to dynamically aggregate groups of points that are within proximity of each other in order to represent each group with a single symbol. Such grouping allows you to see patterns in the data that are difficult to visualize when a layer contains hundreds or thousands of points that overlap and cover each other.

How to use the sample

Pan and zoom the map to view how clustering is dynamically updated. Toggle clustering off to view the original point features that make up the clustered elements. When clustering is "On", you can tap on a clustered geoelement to view a list of contained geoelements.

How it works

  1. Create a map from a web map PortalItem.
  2. Get the cluster enabled layer from the map's operational layers.
  3. Get the FeatureReduction from the feature layer and set the enabled bool to enable or disable clustering on the feature layer.
  4. When the user taps on the map view, call identifyLayer(), passing in the layer, map tap location and tolerance.
  5. Select the AggregateGeoElement from the resulting IdentifyLayerResult and call getGeoElements() to retrieve the containing GeoElement objects.
  6. Display the list of contained GeoElement objects in a dialog.

Relevant API

  • AggregateGeoElement
  • FeatureLayer
  • FeatureReduction
  • GeoElement
  • IdentifyLayerResult

About the data

This sample uses a web map that displays the Esri Global Power Plants feature layer with feature reduction enabled. When enabled, the aggregate features symbology shows the color of the most common power plant type, and a size relative to the average plant capacity of the cluster.

Additional information

Graphics in a graphics overlay can also be aggregated into clusters. To do this, set the FeatureReduction property on the GraphicsOverlay to a new ClusteringFeatureReduction.

Tags

aggregate, bin, cluster, group, merge, normalize, reduce, summarize

Sample Code

display_clusters.dart
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
//
// Copyright 2024 Esri
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import 'package:arcgis_maps/arcgis_maps.dart';
import 'package:flutter/material.dart';

import '../../utils/sample_state_support.dart';

class DisplayClusters extends StatefulWidget {
  const DisplayClusters({super.key});

  @override
  State<DisplayClusters> createState() => _DisplayClustersState();
}

class _DisplayClustersState extends State<DisplayClusters>
    with SampleStateSupport {
  // Create a map view controller.
  final _mapViewController = ArcGISMapView.createController();
  late ArcGISMap _map;
  late FeatureLayer _featureLayer;
  // A flag to track whether feature clustering is enabled to display in the UI.
  var _featureReductionEnabled = false;
  // A flag for when the map view is ready and controls can be used.
  var _ready = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        top: false,
        child: Stack(
          children: [
            Column(
              children: [
                Expanded(
                  // Add a map view to the widget tree and set a controller.
                  child: ArcGISMapView(
                    controllerProvider: () => _mapViewController,
                    onMapViewReady: onMapViewReady,
                    onTap: onTap,
                  ),
                ),
                Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    // Add a button to toggle feature clustering.
                    ElevatedButton(
                      onPressed: toggleFeatureClustering,
                      child: const Text('Toggle feature clustering'),
                    ),
                    const SizedBox(
                      height: 10,
                    ),
                    // Display the current feature reduction state.
                    Text(
                      _featureReductionEnabled
                          ? 'Feature Reduction: On'
                          : 'Feature Reduction: Off',
                    ),
                  ],
                ),
              ],
            ),
            // Display a progress indicator and prevent interaction until state is ready.
            Visibility(
              visible: !_ready,
              child: SizedBox.expand(
                child: Container(
                  color: Colors.white30,
                  child: const Center(child: CircularProgressIndicator()),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  void onMapViewReady() async {
    // Get the power plants web map from the default portal.
    final portal = Portal.arcGISOnline();
    final portalItem = PortalItem.withPortalAndItemId(
      portal: portal,
      itemId: '8916d50c44c746c1aafae001552bad23',
    );
    // Load the portal item.
    await portalItem.load();
    // Create a map from the portal item.
    _map = ArcGISMap.withItem(portalItem);
    // Set the map to the map view controller.
    _mapViewController.arcGISMap = _map;
    // Load the map.
    await _map.load();
    // Get the power plant feature layer once the map has finished loading.
    if (_map.operationalLayers.isNotEmpty &&
        _map.operationalLayers.first is FeatureLayer) {
      // Get the first layer from the web map a feature layer.
      _featureLayer = _map.operationalLayers.first as FeatureLayer;
      if (_featureLayer.featureReduction != null) {
        // Set the ready state variable to true to enable the sample UI.
        // Set the feature reduction flag to the current state of the feature
        setState(() {
          _ready = true;
          _featureReductionEnabled = _featureLayer.featureReduction!.enabled;
        });
      } else {
        showWarningDialog(
          'Feature layer does not have feature reduction enabled.',
        );
      }
    } else {
      showWarningDialog('Unable to access a feature layer on the web map.');
    }
  }

  void onTap(Offset localPosition) async {
    // Clear any existing selected features.
    _featureLayer.clearSelection();
    // Perform an identify result on the map view controller, using the feature layer and tapped location.
    final identifyLayerResult = await _mapViewController.identifyLayer(
      _featureLayer,
      screenPoint: localPosition,
      tolerance: 12.0,
    );
    // Get the aggregate geoelements from the identify result.
    final aggregateGeoElements =
        identifyLayerResult.geoElements.whereType<AggregateGeoElement>();
    if (aggregateGeoElements.isEmpty) return;
    // Select the first aggregate geoelement.
    final aggregateGeoElement = aggregateGeoElements.first;
    aggregateGeoElement.isSelected = true;

    // Get the list of geoelements associated with the aggregate geoelement.
    final geoElements = await aggregateGeoElement.getGeoElements();
    // Display a dialog with information about the geoelements.
    showResultsDialog(geoElements);
  }

  void showResultsDialog(List<GeoElement> geoElements) {
    // Create a dialog that lists the count and names of the provided list of geoelements.
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('Contained GeoElements'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                'Total GeoElements: ${geoElements.length}',
                style: Theme.of(context).textTheme.titleMedium,
              ),
              const SizedBox(height: 10),
              SizedBox(
                height: 200,
                child: SingleChildScrollView(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: geoElements
                        .map(
                          (geoElement) => Text(
                            geoElement.attributes['name'] ??
                                'Geoelement: ${geoElements.indexOf(geoElement)}}',
                          ),
                        )
                        .toList(),
                  ),
                ),
              ),
            ],
          ),
        );
      },
    );
  }

  void toggleFeatureClustering() {
    if (_featureLayer.featureReduction != null) {
      // Toggle the feature reduction.
      final featureReduction = _featureLayer.featureReduction!;
      featureReduction.enabled = !featureReduction.enabled;
      setState(() => _featureReductionEnabled = featureReduction.enabled);
    }
  }

  void showWarningDialog(String message) {
    // Show a dialog with the provided message.
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('Warning'),
          content: Text(
            '$message Could not load sample.',
          ),
        );
      },
    );
  }
}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.