Mapping

Create powerful popups in web apps with Arcade feature sets

In December 2018 the 3.27 ArcGIS API for JavaScript released support for accessing data from multiple data sources inside a single feature’s popup. This is accomplished using the Feature Set functions made available in ArcGIS Arcade version 1.5. Feature Sets allow you to query features from any layer in a map or feature service within the context of a feature’s popup and use those features’ attributes and geometries in calculations. The results of those calculations, otherwise not present in the feature’s attributes, can be displayed in the popup.

For example, you can use feature sets to compare the value of a feature’s attribute to the same attribute in neighboring features; summarize the number of points in another layer within a polygon feature; or compare a feature’s value to the mean of all features in the same layer.

In this post we’ll use feature sets in an Arcade expression to calculate the floor area ratio of buildings in land parcels. If you’re unfamiliar with Arcade, I encourage you to check out the following resources:

This app uses a feature set in an Arcade expression to calculate a land parcel's floor area ratio using the geometry of a building queried from a separate layer.

Floor Area Ratio

Floor area ratio (FAR) is defined as the ratio of the gross floor area of all buildings on a lot to the area of the lot. For example, if a lot has one building with one level that completely covers the lot, then the FAR of the lot is 1.0. If the lot has an FAR of 1.0, but the building has two levels, that indicates the lot is 50% covered.

The image below, which appears in an historic information report from the American Planning Association, depicts how FAR values of 1.0, 4.0, and 9.0 appear in various scenarios.

Illustrations of Floor Area Ratios. Source: A New Zoning Plan for the District of Columbia. Harold M. Lewis, 1956.

FAR is used in zoning to control the volume of buildings in particular zones. For example, a commercial zone may limit building up a parcel to an FAR of 5.0, thus allowing for taller buildings, while residential zones may limit building in parcels to an FAR of 0.3.

This measure has also been used in real estate applications as one of many factors for analyzing the price per square foot of a structure.

Check out the following app, which displays two layers in a map: one for parcels and one for buildings. The parcels don’t have FAR in the attributes, but we can calculate that using Arcade and display it to the user as they click a parcel. Go ahead and try it! Explore the map, clicking on some parcels to view the FAR value.

How it works

The following snippet shows the bulk of the JavaScript used in this application, which you’ll note isn’t too verbose.


const farArcade = `
  var buildingFootprints = Intersects($feature, FeatureSetByName($map, "Building Footprints"));
  
  var grossFloorArea = 0;
  for (var building in buildingFootprints){
    var floors = IIF(building.FLOORCOUNT == 0, 1, building.FLOORCOUNT);
    grossFloorArea += ( AreaGeodetic( Intersection(building, $feature), 'square-feet') * floors );
  }

  Round( ( grossFloorArea / AreaGeodetic($feature, 'square-feet') ), 1);
`;

// Loads the necessary resources for the expression including the
// geometry engine and permits async execution of the script
popupProfile.initialize( [ farArcade ] ).then(function(){
  // loads layers from a webmap with the given id
  arcgisUtils.createMap("da634028a734418f8a5416c675559c3a", "map")
    .then(function (response) {
      const map = response.map;
      const layerIds = map.graphicsLayerIds;
      const layer = map.getLayer(layerIds[1]);

      // Reference the expression in the popup template and set it on the parcels layer
      layer.setInfoTemplate( new PopupTemplate({
        description: "Floor Area Ratio (FAR): {expression/far}",
        expressionInfos: [{
          name: "far",
          title: "far",
          expression: farArcade
        }]
      }) );
    });
});

The Arcade expression is stored as a string and initialized using the esri/arcadeProfiles/popupProfile module. This initialization is critical because it inspects the script and loads all required dependencies for it to properly execute. Since geometry functions and feature set functions are used, the popup needs to load the geometryEngine and allow the expression to execute in an asynchronous environment. The async behavior is required because network requests will likely be made during the execution of the script.

Once the script is initialized, we can set it in the PopupTemplate of the parcels layer.


// Reference the expression in the popup template and set it on the parcels layer
layer.setInfoTemplate( new PopupTemplate({
  description: "Floor Area Ratio (FAR): {expression/far}",
  expressionInfos: [{
    name: "far",
    title: "far",
    expression: farArcade
  }]
}) );

Let’s take a close look at the expression itself. Remember, the goal is to calculate FAR for the parcel. Keep in mind the parcel layer doesn’t have this data, nor does it have data related to buildings or building area in the feature attributes.


// buildingFootprints represents the buildings that intersect the clicked parcel
var buildingFootprints = Intersects($feature, FeatureSetByName($map, "Building Footprints"));
var totalArea = 0;

// since a building may have multiple floors, we must multiply the floor area by the number
// of floors. Also note the building data shows some buildings in dense areas as crossing
// multiple polygons. To avoid miscalculation, we calculate the intersection of the building.
for (var building in buildingFootprints){
  var floors = IIF(building.FLOORCOUNT == 0, 1, building.FLOORCOUNT);
  totalArea += ( AreaGeodetic( Intersection(building, $feature), 'square-feet') * floors );
}
// Compute the ratio of the gross building area to the parcel area
Round( ( totalArea / AreaGeodetic($feature, 'square-feet') ), 1);

The first line of the expression executes a query that returns all buildings that intersect the clicked parcel.


// queries for all buildings that intersect the clicked feature
var buildingFootprints = Intersects($feature, FeatureSetByName($map, "Building Footprints"));

Two functions are used in this line: Intersects() and FeatureSetByName(). FeatureSetByName returns a FeatureSet, which is a representation of all the features of another layer in the map. In this case, the feature set refers to all features in the layer with the name “Building Footprints”.

Note my use of the word representation in the previous paragraph. At this point, the expression does not execute a query for ALL features in that layer. No query is made until the FeatureSet is used in another function parameter or statement. In the case of this expression, we use it immediately in the Intersects function. This is an example of chaining functions for improving performance.

Intersects builds and executes a query for all features in the input FeatureSet (i.e. the buildings layer) that intersect the input geometry (the clicked parcel), and returns those features as a FeatureSet. So in layman’s terms, we’re executing a query for all buildings that intersect the clicked parcel. You can observe this behavior when you inspect the network traffic upon clicking the feature.

Effectively chaining Arcade functions is crucial for maintaining reasonable performance in expressions that use feature sets. In this case, the parcel geometry will query for buildings that intersect it, ensuring that we'll work with 1 - 3 buildings in the majority of cases.

Once the building footprint features have been fetched, they are now available on the client, allowing the rest of this expression to execute client-side.


var grossFloorArea = 0;
for (var building in buildingFootprints){
  var floors = IIF(building.FLOORCOUNT == 0, 1, building.FLOORCOUNT);
  // multiply the square footage of the footprint by the number of floors
  grossFloorArea += ( AreaGeodetic( Intersection(building, $feature), 'square-feet') * floors );
}

Note the buildingFootprints variable is a FeatureSet of client-side features, as opposed to a reference to a service.

I then iterate through each feature in the FeatureSet and sum the gross floor area of all buildings (e.g. floor area * number of floors) intersecting the parcel.

Notice that I also calculate the intersection of the building and the parcel. This particular buildings dataset contains generalized buildings that span multiple parcels in more dense areas. Without calculating the intersection of the building to the parcel, the expression would return FAR values much higher than they are in reality. The parcels in this image, for example, intersect a single one-story building.

In the image above, the black polygon represents one building feature. If the Intersection of the building and the parcel weren't computed, the FAR value of each parcel in this image would be greatly exaggerated.

Although the building in the image above is only one story, the intersecting parcels would report a much larger FAR, often eclipsing 10.0 to 14.0. The Intersection function allows me to only account for the intersecting portion of the one-story building in the parcel, and thus get more realistic values for this area, usually between 0.6 and 1.0.

The final line of the expression completes the final FAR calculation, dividing the gross floor area by the area of the parcel.


Round( ( grossFloorArea / AreaGeodetic($feature, 'square-feet') ), 1);

Since FAR is typically expressed as a number rounded to one decimal place, we can use the Round function to help us with that as well.

Summary

Keep in mind this Arcade expression is just an approximation of FAR. It does not account for the shape of the buildings, what counts as a floor, or whether all floors have the same square footage. It makes a lot of general assumptions. However, this example does provide a glimpse into the power and complexity FeatureSets can bring to your apps.

Note also that the unwise use of feature sets in Arcade expressions can cause popup performance to suffer. Here are a few suggestions of good practices when working with feature sets in Arcade expressions:

If you’re interested in learning more about feature sets, I encourage you to read the following blogs by Paul Barker, who describes FeatureSets in detail with additional context.

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:

Next Article

Drawing a Blank? Understanding Drawing Alerts in ArcGIS Pro

Read this article