ArcGIS Maps SDK for JavaScript

Labeling support added to point clustering

The ArcGIS API for JavaScript (ArcGIS JS API) version 4.16 added support for labeling point clusters. This has been one of the most popular enhancement requests since clustering was released.

Clustering is a method of merging nearby and overlapping features into a single symbol to reduce cluttered features in the view. The size of the cluster icon indicates the number of features in each cluster relative to other clusters. Adding a label can provide more precise context to the features represented by the cluster.

For example, check out this example of earthquakes that occurred along the Aleutian Islands in Alaska over the last 30 days.

Earthquakes clustered.
Earthquakes that occurred along the Aleutian Islands in June 2020. The points are clustered so you can easily see where more earthquakes occur.

Clustering helps you immediately see where more points are densely located than others. When clustering is not enabled, it’s difficult to discern areas with a lot of overlapping points.

Notice how the cluster of 1,000+ earthquakes in the previous image is not discernible when the same layer is not clustered.

Earthquakes along the Aleutian Islands from the last 30 days. Not clustered.
Earthquakes that occurred along the Aleutian Islands in June 2020. In this visualization, each earthquake is represented with one point. When points aren't clustered, overlapping points make it difficult to see areas with a high number of earthquakes.

In fact the area where those 1,000+ earthquakes occurred is so small, most users will incorrectly assume there are fewer earthquakes in that location.

Show cluster counts

Labeling clusters is very similar to labeling individual features in a layer. Just like the FeatureLayer.labelingInfo property of the FeatureLayer, FeatureReductionCluster now has a labelingInfo property that allows you to configure cluster labels.

That means you have full control over the label’s font, symbol, placement, and text. The following code will display cluster counts in the center of the cluster.

const clusterConfig = {
  type: "cluster",
  clusterRadius: "100px",
  // increase the min cluster size
  // to fit cluster labels
  clusterMinSize: "24px",
  clusterMaxSize: "60px",
  // {cluster_count} is an aggregate field containing
  // the number of features comprised by the cluster
  labelingInfo: [
    {
      // allows for slight label overlap
      deconflictionStrategy: "none",
      labelExpressionInfo: {
        // Arcade expression formatting the cluster count
        expression: "Text($feature.cluster_count, '#,###')"
      },
      symbol: {
        type: "text",
        color: "#004a5d",
        font: {
          weight: "bold",
          family: "Noto Sans",
          size: "12px"
        }
      },
      labelPlacement: "center-center"
    }
  ]
};

layer.featureReduction = clusterConfig;

Here are a few properties introduced at version 4.16 of the ArcGIS JS API worth mentioning.

clusterMinSize/clusterMaxSize. Now you can configure the size of your smallest and largest clusters. All other cluster sizes interpolate linearly. The default size (12px) of the smallest cluster is a bit too small to house a readable label. Therefore, you may find yourself bumping this value up to something like 18-24px to get something that looks better with the label. Any time you change the clusterMaxSize you should also consider adjusting the clusterRadius.

deconflictionStrategy – Labeling in the ArcGIS JS API has always supported label deconfliction. Otherwise, most labels would be illegible. However, when it comes to displaying labels for clusters, it’s generally preferable to turn off label deconfliction so all cluster counts display. Now you have the option to disable label deconfliction. This looks better than removing labels when there’s only a little overlap.

side by side view of clusters with conflicting and deconflicting labels
The image on the left shows how label deconfliction removes labels that overlap, or "conflict". This is typically desirable for layer labeling, but looks strange in cluster labels.

Advanced Labels

Given there is no limitation to the labelingInfo spec for clusters compared to layer labeling, you have the flexibility to style labels however you want and present summary information related to the renderer beyond the cluster count. You can add as many label classes as you want to create a rich experience for the end user.

Check out the following sample, which adds six label classes to the FeatureReductionCluster labelingInfo. Note the various scale constraints, label placements, and symbol configurations for each.

Expand to view the code for all six label classes
const clusterConfig = {
  type: "cluster",
  // larger radii look better with multiple label classes
  // smaller radii looks better visually
  clusterRadius: "120px",
  labelsVisible: true,
  labelingInfo: [
    {
      symbol: {
        type: "text",
        haloColor,
        haloSize: "1px",
        color,
        font: {
          family: "Noto Sans",
          size: "11px"
        },
        xoffset: 0,
        yoffset: "-15px"
      },
      labelPlacement: "center-center",
      labelExpressionInfo: {
        expression: "Text($feature.cluster_count, '#,### plants')"
      },
      where: `cluster_avg_capacity_mw > ${clusterLabelThreshold}`
    },
    {
      symbol: {
        type: "text",
        haloColor,
        haloSize: "2px",
        color,
        font: {
          weight: "bold",
          family: "Noto Sans",
          size: "18px"
        },
        xoffset: 0,
        yoffset: 0
      },
      labelPlacement: "center-center",
      labelExpressionInfo: {
        expression: "$feature.cluster_type_fuel1"
      },
      where: `cluster_avg_capacity_mw > ${clusterLabelThreshold}`
    },
    {
      symbol: {
        type: "text",
        haloColor,
        haloSize: "1px",
        color,
        font: {
          weight: "bold",
          family: "Noto Sans",
          size: "12px"
        },
        xoffset: 0,
        yoffset: "15px"
      },
      deconflictionStrategy: "none",
      labelPlacement: "center-center",
      labelExpressionInfo: {
        expression: `
          var value = $feature.cluster_avg_capacity_mw;
          var num = Count(Text(Round(value)));

          Decode(num,
            4, Text(value / Pow(10, 3), "##.0k"),
            5, Text(value / Pow(10, 3), "##k"),
            6, Text(value / Pow(10, 3), "##k"),
            7, Text(value / Pow(10, 6), "##.0m"),
            Text(value, "#,###")
          );
        `
      },
      where: `cluster_avg_capacity_mw > ${clusterLabelThreshold}`
    },
    {
      symbol: {
        type: "text",
        haloColor,
        haloSize: "1px",
        color,
        font: {
          family: "Noto Sans",
          size: "11px"
        },
        xoffset: 0,
        yoffset: "-15px"
      },
      labelPlacement: "above-right",
      labelExpressionInfo: {
        expression: "Text($feature.cluster_count, '#,### plants')"
      },
      where: `cluster_avg_capacity_mw <= ${clusterLabelThreshold}`
    },
    {
      symbol: {
        type: "text",
        haloColor,
        haloSize: "2px",
        color,
        font: {
          weight: "bold",
          family: "Noto Sans",
          size: "18px"
        }
      },
      labelPlacement: "above-right",
      labelExpressionInfo: {
        expression: "$feature.cluster_type_fuel1"
      },
      where: `cluster_avg_capacity_mw <= ${clusterLabelThreshold}`
    },
    {
      symbol: {
        type: "text",
        haloColor,
        haloSize: "1px",
        color,
        font: {
          weight: "bold",
          family: "Noto Sans",
          size: "12px"
        },
        xoffset: 0,
        yoffset: 0
      },
      labelPlacement: "center-center",
      labelExpressionInfo: {
        expression: `
          var value = $feature.cluster_avg_capacity_mw;
          var num = Count(Text(Round(value)));

          Decode(num,
            4, Text(value / Pow(10, 3), "##.0k"),
            5, Text(value / Pow(10, 3), "##k"),
            6, Text(value / Pow(10, 3), "##k"),
            7, Text(value / Pow(10, 6), "##.0m"),
            Text(value, "#,###")
          );
        `
      },
      where: `cluster_avg_capacity_mw <= ${clusterLabelThreshold}`
    }
  ]
};
Global power plants clustered with several labels summarizing the points within each cluster.
Global power plants clustered and labeled with summary information including the predominant fuel type, number of power plants, and average wattage produced by plants in the cluster.

While the labels help make this visualization easy to explore, it takes a lot of time to get the labels right. In fact, placing this much information in labels may not be necessary in most scenarios because of the issue I describe below.

Also, when you label clusters with summary information, you should also consider adding complementary labels on individual features that don’t belong to a cluster. The sample above demonstrates this as well.

Suggested default labels

Since cluster count may not be the only label of importance to the user, we added the getLabelSchemes method in the new esri/smartMapping/labels/clusters module to generate suggested primary and secondary label classes for use in clustering.

All label classes are based on the fields used by the layer’s renderer. This can especially come in handy with complex renderers that refer to multiple fields and Arcade expressions.

Simply reference the layer and view in the method. Then you can apply any of the primary or secondary label schemes on the layer’s featureReduction property.

// Sets a suggested popupTemplate on the layer based on its renderer
clusterLabelCreator.getLabelSchemes({
  layer: featureLayer,
  view: view
}).then(function(labelSchemes){
  // labelSchemes has a `primaryScheme` and `secondarySchemes`
  const featureReduction = featureLayer.featureReduction.clone();
  const { labelingInfo, clusterMinSize } = labelSchemes.primaryScheme;
  featureReduction.labelingInfo = labelingInfo;
  featureReduction.clusterMinSize = clusterMinSize;

  featureLayer.featureReduction = featureReduction;
}).catch(function(error){
  console.error(error);
});

The suggested label classes generated for number fields will contain a TextSymbol with a color and halo that look good for most renderer configurations. Label expressions will look something like the following to improve the formatting of larger clusters.

// clusterField can be either
// {cluster_count} or
// {cluster_avg_FIELDNAME}
// where FEILDNAME is the name of
// a number field used by the renderer
$feature["${clusterField}"];
var value = $feature["${clusterField}"];
var num = Count(Text(Round(value)));
var label = When(
  num < 4, Text(value, "#.#"),
  num == 4, Text(value / Pow(10, 3), "#.0k"),
  num <= 6, Text(value / Pow(10, 3), "#k"),
  num == 7, Text(value / Pow(10, 6), "#.0m"),
  num > 7, Text(value / Pow(10, 6), "#m"),
  Text(value, "#,###")
);
return label;

Check out the Point clustering – generate suggested configuration sample to see how this works in a live app.

Places of worship in India clustered.
Places of worship in India. The color represents the predominant religion of places of worship in each cluster. The size and label of each cluster indicate the total number of points in the cluster. Note the formatting of larger clusters.

Experiment with alternate label schemes and configurations

While the primary labeling scheme usually involves placing the cluster count in the center of the cluster, you may be given a primary scheme that involves another attribute. For example, when generating label classes for any layer rendered with a size visual variable, the default scheme will suggest labeling clusters with the average value of the field or expression used in the size variable. This should provide clarity to the end user since the size variable referenced in the renderer is reused as the cluster size variable.

For example, given a UniqueValueRenderer containing a size variable in the power plants layer, the primary scheme will show the average capacity of power plants.

power plants clustered and labeled with the average capacity of power plants in the center of the cluster
The labels on these clusters may appear to be counts, but in reality they represent the average capacity of power plants in each cluster. This is the default because the size of the cluster represents average capacity, not feature count.

If the layer’s renderer did not include a size variable, then cluster size would indicate the count. Therefore, the suggested primary labelingInfo would display the count.

power plants clustered and labeled with the number of power plants in the center of the cluster
When no size variable is used on the renderer, the cluster size will always indicate the number of features in the cluster. So labeling these clusters with the count makes more sense.

Given the renderer is a UniqueValueRenderer, you could use the predominant fuel type provided in the secondary label schemes to label each cluster.

clusters with labels showing the predominant fuel type of power plants in each cluster.
You can opt to label clusters with a secondary label scheme indicating the predominant type of features within the clusters. Since the power plants layer colors each plant based on the fuel type, the color of each cluster indicates the predominant type.

Or you can use all available schemes to show summary information about each cluster. This requires some rearranging using the LabelClass label placement and TextSymbol offset properties.

clusters labeled with multiple label schemes
You can add more than one label class to clusters to provide additional summary information about features within the cluster.

A final thought

In previous posts about clustering, I stated the following, and it is worth repeating:

Clustering options available via featureReduction do not perform complex statistical analyses. Therefore, the clustering visualizations described above should not be interpreted as precise, statistically significant “clusters” of data. Rather, they should merely be approached as a nice summary of the data, providing you with a preview to potentially identify spatial patterns that may or may not reveal significant storylines.

Also, be sure to check out the newly documented configuration best practices in the FeatureReductionCluster class description and the updated clustering samples:

About the author

Kristian is a Principal Product Engineer at Esri specializing in data visualization. He works on the ArcGIS Maps SDK for JavaScript, ArcGIS Arcade, and Map Viewer in ArcGIS Online. His goal is to help developers be successful, efficient, and confident in building web applications with the JavaScript Maps SDK, especially when it comes to visualizing data. Prior to joining Esri, he worked as a GIS Specialist for an environmental consulting company. He enjoys cartography, GIS analysis, and building GIS applications for genealogy.

Connect:
1 Comment
Oldest
Newest
Inline Feedbacks
View all comments

Next Article

Tighten Up Your Edits with Editing Constraints in ArcGIS Online

Read this article