ArcGIS API for JavaScript

Interactive dot density maps for the web

At version 4.11 of the ArcGIS API for JavaScript (ArcGIS JS API), we added support for DotDensityRenderer, which visualizes the density of a population or count in polygon layers. Last week I wrote an introduction to these visualizations. In this post, I’ll demonstrate how to add custom behavior to the Legend, to transform it into a data exploration tool allowing users to explore dot density maps in more depth.

In Dot density maps for the web, I demonstrated how to create this app, which visualizes population density by race/ethnicity in the United States.

1 dot represents 100 people at a city scale.
One dot represents 100 people at a city scale.

While the visual is fun to explore on its own, you may want to focus on one data category at a time. This can be difficult in highly dense areas for a couple of reasons:

1. One category could clearly dominate and occlude all others, or
2. Several categories could be evenly represented, making it difficult to discern which ones are present.

Let’s take a look at various ways you can make dot density visualizations more interactive, allowing users to explore individual data categories.

Copy the same layer for each attribute

You could allow the user to explore individual categories by creating up to eight separate layers that visualize each field with a different color. You can even add a LayerList widget with an embedded Legend to toggle layer visibility:


view.ui.add( new LayerList({ 
  view: view, 
  listItemCreatedFunction: function(event) {
    const item = event.item;
    item.panel = {
      content: ["legend"],
      open: true
    };
  }
}), "top-left");

This allows the user to not only view one category at a time, but any combination of categories at a single time. This can be appropriate for some purposes, but it also makes the LayerList/Legend long, redundant, and frankly kind of awkward to use when as many as eight layers are included.

Population density by race in the United States. Each category is represented by a separate layer and can be toggled using the LayerList widget.

Another drawback of this approach is performance; the geometries of each layer (the same data) are downloaded to the browser a total of eight times! This isn’t ideal considering the geometries are all the same.

Display each attribute in different views

You could do a similar visualization, but modify it by adding each layer to a different view with the same extent…

Population density by race. Each category is visualized in a different view.
Population Density by race in the United States. Each category is visualized as a separate layer in the same view.

This app has similar performance issues because it draws eight separate views to render data from the same layer. However, it does allow us to see population density for each category at a single glance. I even synced the views so you can navigate to any area in the United States and see the full breakdown of the data.

This is particularly useful if you want to clearly see categories with smaller populations in high density areas. For example, the “Other race” and “Two or more races” categories in the image above would be difficult (if not impossible) to see in the original visualization. By displaying it in a different view, we can more easily see the whole picture of the data at once for this area.

Create an interactive legend

There may some use cases for the other two visualization styles, but I found it more fun (and performant) to explore all this data in the same view with a single layer. That’s why I settled on adding custom interaction to the Legend for exploring each category.

The app below doesn’t look much different from the original app. However, as you hover over each Legend element, the visual will update to emphasize the category touched by the pointer.

Population density in the U.S. by race. The user can explore individual categories by altering a single renderer on one layer instance.

If you click one of the elements, you can freeze the emphasis on that category to resume normal interaction with the view.

Explore one category at a time by clicking the legend element representing that category.

Notice the dots pertaining to non-selected categories are still present, but deemphasized – they’re just given an opacity value of 0.2. This allows you to get a better view of the selected category (even the small population ones) in the context of all others all while rendering a single layer of data to maximize performance.

Click here to view my demo of this app at the 2019 Dev Summit plenary session.

How it works

To add interaction to the Legend, you need to hook up the desired event listeners on the Legend container. The Legend uses dot density attribute labels in the alt text of the DOM element representing the dot color and the innerText of the element containing the label text.

If the pointer hits an element containing alt text or innerText in the Legend, I store that string in a variable called selectedText.

The Legend has a activeLayerInfos property that contains information about how the Legend is rendered for each layer. For every interaction with the Legend, I check the selectedText against the activeLayerInfos labels. If the selected text matches any of the activeLayerInfos, then I call the showSelectedField function.


legendContainer.addEventListener("mousemove", legendEventListener);
legendContainer.addEventListener("click", legendEventListener);

let mousemoveEnabled = true;
function legendEventListener (event:any) {
  const selectedText = event.target.alt || event.target.innerText;
  const legendInfos: Array<any> = legend.activeLayerInfos.getItemAt(0).legendElements[0].infos;
  // if selectedText matches any text in the legend infos, then update the renderer
  const matchFound = legendInfos.filter( (info:any) => info.label === selectedText ).length > 0;
  
  if (matchFound){
    showSelectedField(selectedText);
    
    // additional logic for freezing view if mousemove 
    // was disabled would be included here
  } else {
    layer.renderer = dotDensityRenderer;
  }
}

This function modifies the renderer so all categories whose labels don’t match the selected label are assigned the same color, but with an opacity value of 0.2.


function showSelectedField (label: string) {
  const oldRenderer = layer.renderer as DotDensityRenderer;
  const newRenderer = oldRenderer.clone();
  const attributes = newRenderer.attributes.map( attribute => {
    attribute.color.a = attribute.label === label ? 1 : 0.2;
    return attribute;
  });
  newRenderer.attributes = attributes;
  layer.renderer = newRenderer;
}

If the selection doesn’t include any text or text that doesn’t match any label from the renderer’s attributes, then I set the original renderer back on the layer.

As you can see, with just a little bit of customization, you can create highly interactive dot density visualizations for data exploration purposes. This is just one way you can do that. Stay tuned for more posts exploring dot density in the ArcGIS JS API.

About the author

Kristian is a Product Engineer for the ArcGIS API for JavaScript, focusing on mapping and visualization.

Connect:

Next Article

Environment Maps to be Retired

Read this article