Aggregation

Earthquakes aggregated to 30-mile hexbins.

What is aggregation?

Aggregation allows you to summarize (or aggregate) large datasets with many features to layers with fewer features. This is typically done by summarizing points within polygons where each polygon visualizes the number of points contained in the polygon.

While the examples in this topic focus on point-to-polygon aggregation, each of the principles described also apply to point-to-polyline and polygon-to-polygon aggregation.

Why is aggregation useful?

You may reasonably ask: Why should I aggregate points to polygons when point clustering takes care of aggregation for me on the fly?

There are two scenarios where aggregating points to a polygon layer is advantageous to point clustering:

  1. The point dataset is too large to cluster client-side. Some point datasets are so large they cannot reasonably be loaded to the browser and visualized with good performance. Aggregating points to a polygon layer allows you to represent the data in a performant way.
  2. Data can be summarized within irregular polygon boundaries. You may be required to summarize point data to meaningful, predefined polygon boundaries, such as counties, congressional districts, school districts, or police precincts. Clustering is always handled in screen space without regard for geopolitical boundaries. There are scenarios where summarizing by predefined irregular polygons is required for policy makers.

How aggregation works

Point-to-polygon aggregation is a data preprocessing step usually done before visualizing data in a web map. There are a variety of tools that allow you to do this, but the examples below were prepared using the Aggregate Points analysis tool in the ArcGIS Online Map Viewer.

To aggregate points in ArcGIS Online, you must select the point layer to aggregate, and a polygon layer used to calculate summary statistics. This creates a new feature service. By default, the count of points intersecting each polygon will be included in the output table. You may optionally select fields in the point layer to summarize with various statistics such as the average of a numeric field, or the predominant value of a string field. You can also group numeric statistics by the values of a string field.

Examples

Hexbins

The following example demonstrates how to visualize earthquake points aggregated to hexbins. This layer was created using the Aggregate Points analysis tool in the ArcGIS Online Map Viewer. The output layer contains the total count of earthquakes intersecting each bin.

This layer is styled with a continuous color ramp using a color visual variable. Typically, you should not visualize total counts in polygons using color without some form of data normalization. However, it is permissible in this case because the areas of each hexagon are equal, effectively normalizing the data by area.

Earthquakes aggregated to 30-mile hexbins. The popup uses Arcade to display summary information about the earthquakes represented by each hexbin.

This map was configured in the ArcGIS Online map viewer. In addition to the hexbin layer generated by the Aggregate Points tool, it includes the earthquakes point layer with visibility controlled by scale. This allows us to use Arcade to summarize earthquake statistics in the popup.

For example, to show the size of the largest earthquake, you could write the following expression.

Use dark colors for code blocks
  
1
2
var earthquakes = Intersects(FeatureSetByName($map, "earthquake points"), $feature);
Text( Max(earthquakes, "mag"), "#.#" );

Then reference the expression in the popup content. This can be done in code, or directly in ArcGIS Online.

Setting a maxScale on the hexbin layer and an equivalent minScale on the point layer allows you to create a smooth transition as the user zooms to larger scales. When the user zooms past a certain scale, the point layer can be safely loaded without performance concerns. Depending on the resolution of the hexbins, it visually doesn't make sense to view the bins at large scales.

ArcGIS JS API
Use dark colors for code blocks
46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 46 47 48 49 49 49 49 49 49 49 49 49 49 49
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
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <title>Hexbin aggregation</title>

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
    </style>

    <link rel="stylesheet" href="https://js.arcgis.com/4.22/esri/themes/light/main.css" />
    <script src="https://js.arcgis.com/4.22/"></script>

    <script>
      require([
        "esri/views/MapView",
        "esri/WebMap",
        "esri/widgets/Expand",
        "esri/widgets/Legend"
      ], (
      ) => {
        const view = new MapView({
          map: new WebMap({
            portalItem: {
              // autocasts as new PortalItem()
              id: "4ac45bfebc8647b39efad59cdf0be15a"
          container: "viewDiv"
        view.ui.add(new Expand({
          content: new Legend({ view })
        }), "top-right");
        const earthquakeLayer = view.map.layers.find(layer => layer.title === "earthquake points");
        earthquakeLayer.minScale = 577790;
        earthquakeLayer.maxScale = 0;
        earthquakeLayer.visible = true;
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
  </body>
</html>

Irregular polygons

This example demonstrates how to summarize points within related irregular polygons. In this app, a point layer representing homicide locations is aggregated to a polygon layer representing police precincts in Chicago. Summarizing the data by precinct makes sense when comparing statistics by police precinct jurisdiction.

Because of differences in polygon shape and size, we should visualize the total count using graduated symbols (i.e. icons with a size visual variable). Using color would result in over-emphasizing large areas that may have fewer totals than smaller, more densely populated areas.

Total homicides reported (2008-2017) by police precinct in Chicago, U.S.A. The original homicide point layer was aggregated to a polygon layer representing police precincts.

During the point to polygon aggregation analysis, homicide counts were grouped by the status of the case (i.e. open, closed by arrest, or closed without arrest). These stats are persisted in the output table so they can be easily referenced in the popup.

aggregation-popup

Because the summary statistics are included as attributes in the table, you can also render based on any of the grouped statistics. For example, rather than showing the total count of all homicides, you can show the total count of unsolved homicides.

Unsolved homicides reported (2008-2017) by police precinct in Chicago, U.S.A. This app uses the same layer as the previous example, but now renders the data based off a subset of the data (unsolved cases vs. total cases).

Predominance

Using the Aggregate Points tool in ArcGIS Online, you can also create a field for the most common occurrence of a string value. This allows us to create a predominance map where size still indicates total count, but color represents the most common value of a particular category. The example below uses color to represent the predominant race or ethnicity of the victims in each precinct.

Total homicides reported (2008-2017) by police precinct in Chicago, U.S.A. The color of each icon indicates the predominant race or ethnicity of the victims in each precinct.

This style is configured with a UniqueValueRenderer and a size visual variable.

Browse features

By default, the clustering popup has a "Browse features" action that allows you to select individual features within a cluster to view their location and attribute information. You can implement the same behavior in a polygon layer representing aggregated point data.

In the example below, click any hexbin to explore individual features belonging to the bin.

Earthquakes aggregated to 30-mile hexbins. Click the "browse features" action in any of the popups to view the location and information associated with specific earthquake events.

To implement this browsing experience, you must do the following:

Create a "browse features" action for the aggregate layer's popup template.

ArcGIS JS API
Use dark colors for code blocks
60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 61 62 63 64 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65
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
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <title>Hexbin aggregation</title>

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
    </style>

    <link rel="stylesheet" href="https://js.arcgis.com/4.22/esri/themes/light/main.css" />
    <script src="https://js.arcgis.com/4.22/"></script>

    <script>
      require([
        "esri/views/MapView",
        "esri/WebMap",
        "esri/widgets/Expand",
        "esri/widgets/Legend",
        "esri/core/watchUtils"
      ], (
      ) => {
        (async ()=>{
          view = new MapView({
            map: new WebMap({
              portalItem: {
                // autocasts as new PortalItem()
                id: "4ac45bfebc8647b39efad59cdf0be15a"
            constraints: {
              snapToZoom: false,
              minScale: 30000000
            container: "viewDiv"
          await view.when();
          const hexbinLayer = view.map.layers.find(layer => layer.title === "Hexbins");
          const earthquakeLayer = view.map.layers.find(layer => layer.title === "earthquake points");
          earthquakeLayer.minScale = 577790;
          earthquakeLayer.maxScale = 0;
          earthquakeLayer.visible = true;
          let selectedFeatureHandle = null;
          let selectedFeature = null;
          let selectedAggregateFeature = null;
          hexbinLayer.popupTemplate.actions = [{
            title: "Browse features",
            id: "browse",
            className: "esri-icon-table"
          }];
          view.ui.add(new Expand({
            content: new Legend({ view })
          }), "top-right");
          view.popup.on("trigger-action", (event) => {
            const id = event.action.id;
            if(id === "browse"){
          // Clear view graphics and popup when cluster is no longer selected
          watchUtils.whenFalse(view.popup, "visible", clear);
          view.watch("scale", (scale) => {
            if(scale < earthquakeLayer.minScale){
          async function browseFeatures(aggregateGraphic){
            const layer = view.map.layers.find(layer => layer.title === "earthquake points");
            const query = layer.createQuery();
            query.returnGeometry = true;
            const { features } = await layer.queryFeatures(query);
            selectedFeatureHandle = view.popup.watch("selectedFeature", async (feature) => {
              if(selectedAggregateFeature.getObjectId() === feature.getObjectId()){
                return;
                type: "simple-marker",
                color: "rgb(50,50,50)",
                size: 8,
                style: "x",
                outline: {
                  color: "rgb(50,50,50)",
                  width: 4
              if(selectedFeature && view.graphics.includes(selectedFeature)){
                selectedFeature = null;
              if(selectedAggregateFeature.getObjectId() !== feature.getObjectId()){
          function clear(){
            view.popup.features = view.popup.features.filter( feature => feature == selectedAggregateFeature );
            if(selectedFeatureHandle){
              selectedFeatureHandle = null;
              selectedFeature = null;
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
  </body>
</html>

When browsing is activated by the user, execute a function to query features in the point layer that intersect the selected feature. Then add the fetched features to the view's popup.

ArcGIS JS API
Use dark colors for code blocks
95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 95 96 97 98 99 100 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101 101
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
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <title>Hexbin aggregation</title>

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
    </style>

    <link rel="stylesheet" href="https://js.arcgis.com/4.22/esri/themes/light/main.css" />
    <script src="https://js.arcgis.com/4.22/"></script>

    <script>
      require([
        "esri/views/MapView",
        "esri/WebMap",
        "esri/widgets/Expand",
        "esri/widgets/Legend",
        "esri/core/watchUtils"
      ], (
      ) => {
        (async ()=>{
          view = new MapView({
            map: new WebMap({
              portalItem: {
                // autocasts as new PortalItem()
                id: "4ac45bfebc8647b39efad59cdf0be15a"
            constraints: {
              snapToZoom: false,
              minScale: 30000000
            container: "viewDiv"
          await view.when();
          const hexbinLayer = view.map.layers.find(layer => layer.title === "Hexbins");
          const earthquakeLayer = view.map.layers.find(layer => layer.title === "earthquake points");
          earthquakeLayer.minScale = 577790;
          earthquakeLayer.maxScale = 0;
          earthquakeLayer.visible = true;
          let selectedFeatureHandle = null;
          let selectedFeature = null;
          let selectedAggregateFeature = null;
            title: "Browse features",
            id: "browse",
            className: "esri-icon-table"
          view.ui.add(new Expand({
            content: new Legend({ view })
          }), "top-right");
          view.popup.on("trigger-action", (event) => {
            const id = event.action.id;
            if(id === "browse"){
          // Clear view graphics and popup when cluster is no longer selected
          watchUtils.whenFalse(view.popup, "visible", clear);
          view.watch("scale", (scale) => {
            if(scale < earthquakeLayer.minScale){
          async function browseFeatures(aggregateGraphic){
            const layer = view.map.layers.find(layer => layer.title === "earthquake points");
            const query = layer.createQuery();
            query.returnGeometry = true;
            query.geometry = aggregateGraphic.geometry;
            const { features } = await layer.queryFeatures(query);
            view.popup.features = [aggregateGraphic].concat(features);
            selectedFeatureHandle = view.popup.watch("selectedFeature", async (feature) => {
              if(selectedAggregateFeature.getObjectId() === feature.getObjectId()){
                return;
                type: "simple-marker",
                color: "rgb(50,50,50)",
                size: 8,
                style: "x",
                outline: {
                  color: "rgb(50,50,50)",
                  width: 4
              if(selectedFeature && view.graphics.includes(selectedFeature)){
                selectedFeature = null;
              if(selectedAggregateFeature.getObjectId() !== feature.getObjectId()){
          function clear(){
            view.popup.features = view.popup.features.filter( feature => feature == selectedAggregateFeature );
            if(selectedFeatureHandle){
              selectedFeatureHandle = null;
              selectedFeature = null;
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
  </body>
</html>

When a new feature is selected, add it to the view and clear any previous graphics from the view.

ArcGIS JS API
Use dark colors for code blocks
104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127 127
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
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <title>Hexbin aggregation</title>

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
    </style>

    <link rel="stylesheet" href="https://js.arcgis.com/4.22/esri/themes/light/main.css" />
    <script src="https://js.arcgis.com/4.22/"></script>

    <script>
      require([
        "esri/views/MapView",
        "esri/WebMap",
        "esri/widgets/Expand",
        "esri/widgets/Legend",
        "esri/core/watchUtils"
      ], (
      ) => {
        (async ()=>{
          view = new MapView({
            map: new WebMap({
              portalItem: {
                // autocasts as new PortalItem()
                id: "4ac45bfebc8647b39efad59cdf0be15a"
            constraints: {
              snapToZoom: false,
              minScale: 30000000
            container: "viewDiv"
          await view.when();
          const hexbinLayer = view.map.layers.find(layer => layer.title === "Hexbins");
          const earthquakeLayer = view.map.layers.find(layer => layer.title === "earthquake points");
          earthquakeLayer.minScale = 577790;
          earthquakeLayer.maxScale = 0;
          earthquakeLayer.visible = true;
          let selectedFeatureHandle = null;
          let selectedFeature = null;
          let selectedAggregateFeature = null;
            title: "Browse features",
            id: "browse",
            className: "esri-icon-table"
          view.ui.add(new Expand({
            content: new Legend({ view })
          }), "top-right");
          view.popup.on("trigger-action", (event) => {
            const id = event.action.id;
            if(id === "browse"){
          // Clear view graphics and popup when cluster is no longer selected
          watchUtils.whenFalse(view.popup, "visible", clear);
          view.watch("scale", (scale) => {
            if(scale < earthquakeLayer.minScale){
          async function browseFeatures(aggregateGraphic){
            const layer = view.map.layers.find(layer => layer.title === "earthquake points");
            const query = layer.createQuery();
            query.returnGeometry = true;
            const { features } = await layer.queryFeatures(query);
            selectedFeatureHandle = view.popup.watch("selectedFeature", async (feature) => {
              if(selectedAggregateFeature.getObjectId() === feature.getObjectId()){
                return;
              }
              feature.symbol = {
                type: "simple-marker",
                color: "rgb(50,50,50)",
                size: 8,
                style: "x",
                outline: {
                  color: "rgb(50,50,50)",
                  width: 4
                }
              }
              if(selectedFeature && view.graphics.includes(selectedFeature)){
                view.graphics.remove(selectedFeature);
                selectedFeature = null;
              }
              if(selectedAggregateFeature.getObjectId() !== feature.getObjectId()){
                view.graphics.add(feature);
                selectedFeature = feature;
              }
            });
          function clear(){
            view.popup.features = view.popup.features.filter( feature => feature == selectedAggregateFeature );
            if(selectedFeatureHandle){
              selectedFeatureHandle = null;
              selectedFeature = null;
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
  </body>
</html>

The same technique can also be applied to an aggregated layer of irregular polygons. Browse the features in a precinct by clicking the action in the popup and selecting one of the homicide locations.

Homicides aggregated to police precincts. Click the "browse features" action in any of the popups to view the location and information associated with specific earthquake events.

API support

The following table describes the geometry and view types that are suited well for each visualization technique.

2D3DPointsLinesPolygonsMeshClient-sideServer-side
Clustering
Heatmap
Opacity
Bloom
Aggregation
Thinning11123
Visible scale range
Full supportPartial supportNo support
  • 1. Feature reduction selection not supported
  • 2. Only by feature reduction selection
  • 3. Only by scale-driven filter

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