ArcGIS Maps SDK for JavaScript

Building dynamic web applications with the ArcGIS API for JavaScript

At the Esri Developer Summit plenary, I presented a dynamic web application that uses the powerful rendering and querying capabilities of the ArcGIS API for JavaScript to explore age and income data in the Los Angeles area.

This app allows you to see the relative proportion of various age groups around Los Angeles and the predominant income for each of these groups. You can view the six-minute demo of this app as it was presented at the Developer Summit in the following video. You can also view the source code here.

Today, I’m going to talk to you in a little more detail about how I created this app and some interesting areas of Los Angeles that I discovered when exploring this data.

Feel free to skip ahead using the links below. 😊

Let’s talk about the data
How the app was built

Interesting observations

Let’s talk about the data

Before I get into how the app was created, let’s first talk about the data. The primary layer that is used in this application contains population data for California block groups, geoenriched with age and income data. It contains over 23,000 features and over 350 fields. The fields that we are going to focus on in our application include fields for the number of males and females for every year of age from 0 to 85, household income for members of various age ranges, median household income, and the total population of each block group.

How the app was built

Los Angeles is a pretty unique city – the population is widespread across the whole city in different neighborhoods, not just centered around the downtown area.  Another thing that makes Los Angeles unique is its terrain. The city is pretty much surrounded either by hills on one side or the ocean on the other. The layout of Los Angeles – its neighborhoods and its terrain – plays a big role in its demographics.

Blend modes

To help us understand the terrain’s role in the demographics of Los Angeles, I created a webmap in the new Map Viewer that blends together the classic dark basemap with a world hillshade basemap. This allows us to see both the neighborhoods, the city’s roads and freeways, and its terrain at once.

layer blending on the basemap
The combination of the dark gray basemap layer with the dark hillshade creates a beautifully blended basemap..

When I added the demographic data layer to the map, I didn’t want it to cover this beautiful, informative basemap that I had just created. So, I decided to use layer blending yet again. Instead of displaying the layer as polygons that would just show the block group boundaries, I blended this layer with a tile layer of LA building footprints. This allows you to see where people live and work in the city, and areas that are less populated (or have less buildings) than others.

To add layer blending, I created a groupLayer with my FeatureLayer of population data, and my TileLayer of building footprints. Then, I set the blendMode on my FeatureLayer to "source-in", which means that this layer would only be drawn where it overlapped with the background layer (the building footprints).


const groupLayer = new GroupLayer({
  layers: [buildingFootprints, featureLayer],
});
featureLayer.blendMode = "source-in";

The resulting effect can be seen in the image below.

Blending together building footprints with our demographic data layer allows us to see where people live and work in Los Angeles.

Visualizing age distribution

One of our goals with this app was to allow users to explore and understand the age distribution of residents across Los Angeles County. To do this, I created a simple Arcade expression that would take a given age range and calculate the percentage of the population they represented. The layer is rendered from the results of that Arcade expression, using color visual variables to show areas where high percentages of that population exist.

We added some radio buttons using Esri’s new design system (Calcite Components) to allow users to select from a predefined age range, and we also added a Slider, to allow users to enter a custom age range. The Arcade expression will work at any age range, it will just update the fields it calculates based on the age range provided.


function createAgeRange(low, high) {
   let str = "var TOT = Sum(";

   for (let age = low; age <= high; age++) {
      str += "Number($feature.MAGE" + age + "_CY), Number($feature.FAGE" + age + "_CY)";
      if (age != high) {
         str += ",";
      }
   }
   str += ")\n Round(((TOT/$feature.TOTPOP_CY)*100),2)";
   return str;
 }

To make sure the information displayed on the map was reliable at each age range provided, we calculated the average and standard deviation for the layer at that age range and use those statistics to populate our visual variables stops. We set the middle stop to the average, and the highest and lowest stops to one standard deviation above and below the average, respectively.

Visualizing the percentage of teenagers in the area surrounding Downtown Los Angeles.

Above and below color ramp

The visualizations above show where high percentages of the given age range reside, but it can also be worthwhile to see areas where low percentages of that population exists. We can see this by switching to an above and below color ramp. This is easily done by updating the color value of the visual variable stops, but provides a unique map that allows you to see areas with both high and low percentages of a certain age group.

Using an above and below color ramp allows you to see new spatial patterns.

Querying for statistics

Since this layer contains so much data, we wanted to allow users to see more than just what is rendered in the layer. The age data fields that the layer contains (for men and women of all ages between 0 and 85) provide really nice information to set up an age pyramid. To do this, we created a moveable, resizable buffer using the SketchViewModel that would allow users to define the query geometry for the age pyramid. As soon as the buffer is updated, then a query is executed on the FeatureLayerView to get the statistics for each age field, and those results are plotted on a chart to create the age pyramid. Let’s dig a little deeper into this.

Creating the buffer using SketchViewModel

To create the buffer used to query for statistics, we start by creating two GraphicsLayers – these two layers are used together with the SketchViewModel to create the graphic drawn on the map. The bufferLayer has a "color-dodge" blendMode applied to create a lightening effect for the features included in the buffer.

// layers for the buffer graphic
const graphicsLayer = new GraphicsLayer();
const bufferLayer = new GraphicsLayer({
   legendEnabled: false,
   blendMode: "color-dodge"
});

When the app is initialized, we create the point and line graphics within the buffer based on a point relative to the center of the view.


const viewCenter = view.center.clone();
const centerScreenPoint = view.toScreen(viewCenter);
const centerPoint = view.toMap({
   x: centerScreenPoint.x,
   y: centerScreenPoint.y
});
const edgePoint = view.toMap({
   x: centerScreenPoint.x + 15,
   y: centerScreenPoint.y - 49
});

We use those two points and create a line connecting them to represent the radius of the buffer. Then, we can use the geometry engine to calculate the length of the line and create the buffer.


// get the length of the initial polyline and create a buffer
const length = geometryEngine.geodesicLength(polyline, unit);
const buffer = geometryEngine.geodesicBuffer(centerPoint, length, unit);

Then, we create graphics for each of the points, the line, and the buffer polygon that we created. We add the point, line and label graphics to the graphicsLayer, and add the buffer polygon graphic to the bufferLayer (which has the blendMode applied).

Once we have created all our graphics and added them to layers in the map, we can use the SketchViewModel to allow the point graphics to be updated in the map, and use their updated geometries to recalculate the line length and buffer whenever it is moved.


sketchViewModel.update([edgeGraphic, centerGraphic], {
   tool: "move"
});

Manipulate the points to resize or move the buffer.

Querying for statistics on the FeatureLayerView

Next step, creating the query to get the statistics of fields within the buffer. Since we are creating an age pyramid, we need to include statistics for the sum of the values of each field of males and females at each age. To calculate these statistics, we will perform a query on the FeatureLayerView with the outStatistics property set to an array of StatisticDefinitions for each field of males age 0-85 (MAGE0_CY to MAGE85_CY) and females age 0-85 (FAGE0_CY to FAGE85_CY). For each of those field names, the following definition is set to get the sum:


{
  onStatisticField: fieldName,
  outStatisticFieldName: fieldName + "_TOTAL",
  statisticType: "sum"
}

Once we have the statistic definitions defined through the statDefinitions variable, we can create our query. We want to perform this query on the client-side to avoid making unnecessary calls to the server, so we’ll use the featureLayerView. We’ll set the query.outStatistics to the statDefinitions we just defined, and apply the buffer from the SketchViewModel as our query geometry. Then, we can execute the query and use the results to populate our age pyramid that is created with Chart.js.


const query = featureLayerView.createQuery();
query.outStatistics = statDefinitions;
query.geometry = buffer;

return featureLayerView.queryFeatures(query)
   .then((results) => {
       // parse the results and pass them into the chart
   });

I just want to point out – this query generates statistics for 170 different fields! 🤯  Yet it still executes and updates the chart very quickly, all on the client-side.

Visualizing income predominance

When you switch to the “Income” tab of the app, we update the renderer to calculate and display the predominant income for members of each block group. Again, this is done through an Arcade expression. The Arcade expression looks something like this, where the fields array correlates to the fields for median income.


var fields = [FIELD_NAME_1, FIELD_NAME_2,
   // ADD MORE FIELDS AS NECESSARY
];

var maxValue = -Infinity;
var maxValueField = null;
for(var k in fields){
  if($feature[fieldsArray[k]] > maxValue){
    maxValue = $feature[fieldsArray[k]];
    maxValueField = fieldsArray[k];
  }
}
return maxValueField;

Once we’ve calculated the predominance, we use a UniqueValueRenderer to render the layer in different colors based on the different income ranges.

Los Angeles block groups, rendered by predominant income.

After creating the renderer, we create a donut chart using Chart.js that will serve as a legend for our map and provide some additional context. To create the chart, we query for statistics on the fields used to create our renderer to find the total number of people in each income bracket. Then, we perform a query on the FeatureLayerView, and use the results to populate our chart. We can set the colors of the chart to match the colors of the renderer so that it can also serve as a legend.

This donut chart serves as a legend (since the colors match the colors of the renderer) and provides more context for the number of people in each income bracket.

Whenever the layer view is updated, we can perform the query on the new layer view extent, and update the chart.


// when the view is updated, update the donut chart when on the income tab
featureLayerView.watch("updating", (value) => {
   if (!value) {
     if (activeTab == "income") {
       createDonutChart(incomeAge);
     }
   }
})

Similar to the age tab of the app, you can click through and see the predominant income for different age ranges. Clicking one of these radio buttons just updates the Arcade expression with the appropriate fields for that age range, and then the renderer updates. The donut chart is also repopulated with the new data.

Filter and effect

While the predominant income tells us what the majority of people in that block group are making, the median income will give us a better understanding of the bigger financial picture for that block group.

To filter by the median income, click the switch in the top right of the Income UI. A feature effect will immediately be applied to the layer, highlighting areas that fall within the filter range, and dimming features that fall outside of the filter. Let’s take a closer look at the code.

A filter is applied to show where the median income is between $75,000 and $90,000.

Here, we have the function to create the filter and effect. This is called whenever the filter is initially turned on, or when the slider values are updated. It takes the parameters of the minimum and maximum slider values and creates a filter where the median income is greater than the minimum slider value and less than the maximum. For features that are included in this filter, we apply a bloom and saturate effect, to highlight and add more color to these features. To the excluded features, we blur them and dim their brightness, to deemphasize them.


function createEffect(min, max) {
  featureLayerView.effect = {
     filter: {
        where: "MEDHINC_CY > " + min + " AND MEDHINC_CY < " + max
     },
     includedEffect: "bloom(150%, 1px, 0.2) saturate(200%)",
     excludedEffect: "blur(1px) brightness(65%)"
   }
}

As I mentioned, these effects are updated as soon as you move the slider. You can hit the Play button to quickly animate through the data.

Press play to animate through the median income of each block group.

Interesting observations

This app allowed me to really explore and understand the demographics of different neighborhoods in a new light. I’ll share some areas I found interesting, but encourage you to explore the map and play around with the data yourself!

When looking at the age tab, I found some unique population pyramids around the “College” age range. For example, here’s the population pyramid for UCLA’s campus.

The population pyramid here shows that the primary people living in this area are 18-23, and few people of other ages live here.

Another fascinating college age pyramid is this one, for Mount Saint Mary’s University, which is up in the hills just north of UCLA’s campus.

Mount St. Mary’s University is a small, Catholic all-girls school, so it makes sense why there is such a large spike for college-aged females in this area.

After looking at the percent of college aged students in each block group, I switched to “Millennials” and noticed a really unique pattern of millennials living primarily along the freeways in West Los Angeles. These areas have a higher amount of apartments and are generally cheaper places to live.

Switching to the above and below color ramp allows us to see even more patterns, and allows for easier comparison to nearby areas. For example, if we switch to the “Children” age range, we can see that Downtown Los Angeles has significantly less children than the surrounding neighborhoods.

We can also see some interesting patterns in the map after switching over to the Income tab. For instance, when switching between radio buttons to view the predominant income for different age groups, we see a pretty predictable story. Younger people start out making less money, they make more as they get older, and then as people start to reach retirement age, then they bring in less income. There are some unique areas that don’t follow this pattern – like the block group containing Skid Row in Downtown Los Angeles, where the predominant income is consistently less than $15,000 at each age range.

Notice how the block group containing skid row does remains <$15,000 regardless of the age range selected.

These are just some of the areas I found interesting while exploring this map. I encourage you to play around with the app and explore the data for yourself, you may be surprised at what you find! Feel free to share any of your observations in the comments below 😊

A big thank you to Jeremy Bartley, Jim Herries, and Kristian Ekenes who provided lots of input and feedback in the making of this app.

About the author

Anne Fitz is a Senior Product Engineer at Esri, working on the ArcGIS Maps SDK for JavaScript and ArcGIS Arcade. Her expertise primarily focuses on dynamic vector symbology (CIM symbols), data visualization, animation, ArcGIS Arcade integration, and widgets. Anne’s primary goal is to help developers succeed in building geospatial applications, providing them with the tools and knowledge they need to bring their ideas to life. She has been with Esri for over four years.

Connect:
0 Comments
Inline Feedbacks
View all comments

Next Article

Multi-Scale Contour Styling in ArcGIS Pro

Read this article