Select to view content in your preferred language

Field Maps Tips & Tricks: Using Arcade to Find Intersecting Polygons

5158
8
07-21-2023 08:31 AM
KerriRasmussen
Esri Contributor
10 8 5,158

Before UC Sarah Saint-Ruth and I presented a webinar on Intuitive Field Workflows with ArcGIS Field Maps and Arcade. During the webinar we received a question asking if it were possible to use Arcade to summarize polygon areas that fall within another polygon in a pop-up? The specific example that was asked about was, can you summarize land use types that fall within a tax parcel? Yes- you can! Both layers will need to be vector layers for this expression to work. Below is a sample expression and the results as seen in the pop-up. And a big thank you to my colleague Chris Dunn for authoring these awesome expressions.

We will start with the Intersects() function to find the geometry of areas that are intersecting. Next, we will create a dictionary, this will allow us to iterate through the different land use types that are intersecting our parcel and if we have more than one area of the same land use type, we can then add those areas together, instead of overwriting the pervious sum. That iteration process as seen in the for loop below the dictionary. We also calculate the geographic area for each one of the land use areas, in this case we are using square meters. Using an If statement, we will check for the types of land use that fall inside that parcel and if that type is inside the parcel, we will count the total area for that land use type. And finally, we return fields with our land use type and the corresponding area totals.

 

 

 

 

 

//Return land_polys
var land_polys = Intersects($feature, FeatureSetByName($map, "Land Use"))

//Hide element if no data is found
if (count(land_polys)<1){
  return null
} 

//Create empty array for land use codes and dictionary for field values
var land_uses = []
var land_d = Dictionary()

//Iterate and add to dictionary, adding to value if key exists
for (var l in land_polys) {
  
  //Intersect the selected feature with the current iterated feature
  var land = Intersection($feature, l)

  //Set variables for land use and area
  var l_use = l.LU
  var l_area = AreaGeodetic(land, "square meters")

  //Check for land use key in dict
  if (HasKey(land_d, l_use)==true) {
    //If the land use code is in the dictionary, get the area from the dictionary and add the current area to it
    var current_area = land_d[l_use]
    var total_area = current_area + l_area
    land_d[l_use] = total_area
  
  } else {
    //If the land use code is not in the dictionary, add it with the current area
    land_d[l_use] = l_area
    Push(land_uses,
    {"fieldName":l_use})
  }
}

return {
    type: 'fields',
    title: 'Land Use Sq Meters',
    //description : '',
    fieldInfos: land_uses,
    attributes : land_d
  }

 

 

 

 

 

Pop-up using arcade for land use table and chart.Pop-up using arcade for land use table and chart.

 

As you can see from the pop-up, we have 3 land use types that fall within this parcel, and we can see the total sq meters for each of the 3 types. We also have a pie chart displaying the data below the fields. The code is the same as the code above, with the exception of the else and return statements. In this case, we are returning a pie chart instead of the fields as shown above.

 

 

 

 

 

//Return land_polys
var land_polys = Intersects($feature, FeatureSetByName($map, "Land Use"))

//Hide element if no data is found
if (count(land_polys)<1){
  return null
}

//Create empty array for land use codes and dictionary for field values
var land_uses = []
var land_d = Dictionary()

//Iterate and add to dictionary, adding to value if key exists
for (var l in land_polys) {
  
  //Intersect the selected feature with the current iterated feature
  var land = Intersection($feature, l)

  //Set variables for land use and area
  var l_use = l.LU
  var l_area = AreaGeodetic(land, "square meters")

  //Check for land use key in dict
  if (HasKey(land_d, l_use)==true) {
    //If the land use code is in the dictionary, get the area from the dictionary and add the current area to it
    var current_area = land_d[l_use]
    var total_area = current_area + l_area
    land_d[l_use] = total_area
  
  } else {
    //If the land use code is not in the dictionary, add it with the current area
    land_d[l_use] = l_area
    Push(land_uses, l_use)
  }
}

return {
    type: 'media',
    // title : 'The title for all media in the element',
    // description : '',
    attributes : land_d,  // replace this dictionary with your own key-value pairs,
    mediaInfos: [{
        type : 'piechart', //linechart | barchart | piechart | columnchart
        title : 'Land Use Breakdown',
        // caption : '',
        altText : '', //altText will be read by screen readers
        value : {
          fields: land_uses,  // choose what attributes to use in the chart
          //normalizeField : '',  // the name of the attribute to normalize by or value
        }
      }]
  }

 

 

 

 

 

This is a great use case for Arcade and shows the power of spatial expressions. Arcade can also be used to calculate the number of points in a polygon and just like we did here, you can summarize by the type of point. And bonus, this expression will work both online and offline! Arcade goes beyond just attribute queries and allows you to take advantage of the spatial aspect of your data. And while these types of expressions are incredibly useful and let’s face it, pretty cool, I do recommend using them sparingly. Using lots of Arcade expressions in your maps can impact map performance, especially if you are using spatial expressions. So be adventurous when it comes to Arcade, but also make sure you use it wisely.  With great power, comes great responsibility.

 

8 Comments
LindsayRaabe_FPCWA
MVP Regular Contributor

Great examples! These will be very useful. I've got layers that return intersecting plantations from Local Government Area, but our staff have also asked to get a summary of all plantations in an LGA in the popup as well. I should be able to adapt this code do achieve that. 

LindsayRaabe_FPCWA
MVP Regular Contributor

So I've been experimenting with your code. I've managed to get it to return a list of Plantations within an LGA, but I'm guessing it's returning them ordered by first feature found - any chance we could sort the results alphabetically? 

I've also experimented with adding text to the end of the area value returned, but each location I've tried adding text to the end of the returned value (Lines 21, 32 or 43) has just resulted in the Summing or Value return not working properly. I'm currently stuck with the total number. (i.e. want to format the returned area as "100 Ha" to represent Hectares instead of just "100"). 

KerriRasmussen
Esri Contributor

Hi @LindsayRaabe_FPCWA ! Would you mind sharing your expression so I can do some troubleshooting? 

LindsayRaabe_FPCWA
MVP Regular Contributor

Hi @KerriRasmussen. My code is below. You'll see that I've swapped most references to land use to Plantation Name (the layer and list field are both called "Plantation" with my Area field being "AreaHa"). You'll see that I've swapped out the Area calculation for summing an area field within my attribute table (to provide consistent results with other reports). 

LindsayRaabe_FPCWA_1-1690769269929.png

I've also included a negative buffer at the beginning to prevent features that slightly overlap LGA boundaries being counted where they shouldn't be. 

LindsayRaabe_FPCWA_8-1690769545145.png

 

//Return Plantations
var BufferedFeature = Buffer($feature, -10, 'meters')
var Plantations = Intersects(BufferedFeature, FeatureSetByName($map, "FPC Plantations"))

//Hide element if no data is found
if (count(Plantations)<1){
  return null
}

//Create empty array for LGA use codes and dictionary for field values
var ptns = []
var ptns_d = Dictionary()

//Iterate and add to dictionary, adding to value if key exists
for (var p in Plantations) {
  
  //Intersect the selected feature with the current iterated feature
  var LGA = Intersection($feature, p)

  //Set variables for LGA use and area
  var p_name = p.Plantation
  var p_area = Round(p.AreaHa,0) //Round(AreaGeodetic(LGA, "hectares"),0)

  //Check for LGA use key in dict
  if (HasKey(ptns_d, p_name)==true) {
    //If the LGA use code is in the dictionary, get the area from the dictionary and add the current area to it
    var current_area = ptns_d[p_name]
    var total_area = current_area + p_area
    ptns_d[p_name] = total_area
  
  } else {
    //If the LGA use code is not in the dictionary, add it with the current area
    ptns_d[p_name] = p_area
    Push(ptns,
    {"fieldName":p_name})
  }
}

return {
    type: 'fields',
    title: 'Plantations in LGA',
    description : 'Plantation Name | Plantation Area (Hectares)',
    fieldInfos: ptns,
    attributes : ptns_d
  }

 

An additional comment on my above issues around the " Ha" suffix to the results is that where I have Rounded the figures to 0 decimals, I was trying to do it to 1 decimal, but found some results would still come out with 6(?) decimals. Also, that Rounding had to happen by feature (Line 22) instead of on the overall outcome (Line 33) as that also had unpredictable results with some lines being rounded and some not. Rounding to 0 decimals was the only way of getting consistent results for all records returned.

LindsayRaabe_FPCWA_12-1690769678249.png

LindsayRaabe_FPCWA_13-1690769754681.png

It would be good to pad with 0's as well if rounded to 1 decimal, but fear I'd run into a similar problem to the above " Ha" suffix and the summing of values by formatting the text result #.# but haven't tried it. 

KerriRasmussen
Esri Contributor

Hi @LindsayRaabe_FPCWA! Thank you for sending along your expression, it was very helpful. First, to take care of alphabetizing your results list, you will want to change line 3 from: 

var Plantations = Intersects(BufferedFeature, FeatureSetByName($map, "FPC Plantations"))

to

var Plantations = OrderBy(Intersects(BufferedFeature, FeatureSetByName($map, "FPC Plantations")), 'p_name')

Using the OrderBy() function will put your results list in alphabetical order.

Second, when it comes to rounding, you will want to use the AreaGeodetic() function to find the area of the polygon, like you see in line 21 of the original expression. You can swap out "square-meter" for "hectarces", so that line would look like: 

var p_area = Round(AreaGeodetic(LGA, "hectares"),0)

This is telling the AreaGeodetic() function what units to use while calculating the area for the polygon. Using the area field that is calculated automatically can cause issues when using arcade, it's better to use the AreaGeodetic() function when making calculations using areas.

And lastly, to add the "ha" label, you will want to add the following to the line about your results section (so line 38). 

ptns_ha = Dictionary()

for (var k in ptns_d) {
  ptns_ha[k] = ptns_d[k] + " ha"
}

This will add the "ha" string to the end of your result. I hope this helps.

Kerri

LindsayRaabe_FPCWA
MVP Regular Contributor

@KerriRasmussen Thank you for your response! I've implemented the sorting and that works perfectly. I'm struggling with the addition of the " Ha" suffix bit though (not sure where I'm putting it).

LindsayRaabe_FPCWA_0-1690850006124.png

Regarding the rounding solution, I just wanted to clarify something on my original post. The AreaHa field I'm using is actually a static area figure (calculated in ArcGIS Pro using a different projection) and not an auto-calculated field (like Shape_Area). This was our preference so that the areas returned in the popup match what is calculated and reported on in our plantation database. 

When I try your solution rounding to 1 decimal, it still gives the same results as per below, which is the same as if I use my static attributes (despite them only containing 1 decimal to start with - sample data at bottom). 

LindsayRaabe_FPCWA_1-1690850143565.png

LindsayRaabe_FPCWA_2-1690850412963.png

 

ChrisDunn1
Esri Contributor

Hi @LindsayRaabe_FPCWA ,

For the step where you add the "ha", you just need to use the new dictionary, ptns_ha, as the attribute parameter in your field list (line 48) instead of ptns_d. I also think you'll need a "var" before the ptns_ha on line 38 to declare it as a variable (our bad for forgetting that in the example).

I think you can actually add a step to round your results as part of that dictionary creation as well, so your new code should look like this:

var ptns_ha = Dictionary()

for (var k in ptns_d) {
  var round_area = Text(ptns_d[k], '#.0')
  ptns_ha[k] = round_area + " ha"
}

 

That should round your area values and add the "ha" to the end.

Chris

LindsayRaabe_FPCWA
MVP Regular Contributor

@ChrisDunn1 Perfect! My complete code below. I tested you're suggestion with the Rounding included at Line 22 in addition to your rounding step, and without the rounding at Line 22 (and just using yours). It looks like it works best with just yours in place (returns different results - I suspect double rounding skewing the results with both steps). 

 

 

//Refer to https://community.esri.com/t5/arcgis-field-maps-blog/field-maps-tips-amp-tricks-using-arcade-to-find/bc-p/1314058 for details on the below script
//Requires that the FPC Plantations layer is also present in the map or lookups won't work

//Return Plantations
var BufferedFeature = Buffer($feature, -10, 'meters')
var Plantations = OrderBy(Intersects(BufferedFeature, FeatureSetByName($map, "FPC Plantations")), 'Plantation')

//Hide element if no data is found
if (count(Plantations)<1){
  return null
}

//Create empty array for LGA use codes and dictionary for field values
var ptns = []
var ptns_d = Dictionary()

//Iterate and add to dictionary, adding to value if key exists
for (var p in Plantations) {
  
  //Intersect the selected feature with the current iterated feature
  var LGA = Intersection($feature, p)

  //Set variables for LGA use and area
  var p_name = p.Plantation
  var p_area = p.AreaHa //AreaGeodetic(LGA, "hectares") <--- use this to calculate and area instead of pulling an area from the attribute table. Set rounding at Line 45

  //Check for LGA use key in dict
  if (HasKey(ptns_d, p_name)==true) {
    //If the LGA use code is in the dictionary, get the area from the dictionary and add the current area to it
    var current_area = ptns_d[p_name]
    var total_area = current_area + p_area
    ptns_d[p_name] = total_area
  
  } else {
    //If the LGA use code is not in the dictionary, add it with the current area
    ptns_d[p_name] = p_area
    Push(ptns,
    {"fieldName":p_name})
  }
}

var ptns_ha = Dictionary()

for (var k in ptns_d) {
  var round_area = Text(ptns_d[k], '#') //Rounds the number to whole figures. To round to a decimal, use #.0 which also pads the rounded figures with ".0" when value rounds to a whole number
  ptns_ha[k] = round_area + " ha"
}

return {
    type: 'fields',
    title: 'Plantations in LGA',
    description : 'Plantation Name | Plantation Area (Hectares)',
    fieldInfos: ptns,
    attributes : ptns_ha
  }

 

 

Incorrect sums:

LindsayRaabe_FPCWA_2-1690954394059.png

Correct sums:

LindsayRaabe_FPCWA_3-1690954402450.png

Thanks again both @KerriRasmussen and @ChrisDunn1 for your help. This stuff is really useful and I can see it being used more and more in our ArcGIS Online space!