ArcGIS Online

Five under-appreciated Arcade functions

Arcade is an expression language that allows you to create custom content in popups, format labels, create new data values for visualizations, and enforce rules in various workflows. A lot has already been written about Arcade, what it is, and why you would use it.

I’ve seen a lot of Arcade expressions from the Esri user community. Over the years, I’ve noticed common patterns that can be simplified by using out-of-the box Arcade functions. Most of the time, expression authors aren’t aware of the functionality they’re missing out on. In this post, I’ll demonstrate five under-appreciated Arcade functions and how they can improve your expressions.

  1. iif
  2. Text
  3. Decode
  4. DefaultValue
  5. Number

1. iif

The iif function returns a value if a conditional expression evaluates to true, and returns an alternate value if the condition evaluates to false. This is particularly useful in simple scenarios such as writing an expression to indicate when a value has reached a threshold.

Commonly, expressions use if-else blocks to conditionally return values.

var category = "";

if($feature.value > 100){
    category = "high";
} else {
    category = "low";
}
return category;

However, the iif function simplifies this into two lines without the need for multiple returns or variable assignments…

// if the value is more than 100, return "high"
// otherwise, return "low"
var category = IIF($feature.value > 100, "high", "low");
return category;

…or to one line (thanks to implicit returns).

IIF($feature.value > 100, "high", "low");

There is nothing wrong with using an if-else block to return conditional content. In fact, you should use if statements if you must evaluate other statements that follow the same conditions, including assigning values to variables. In the following example, the iif function is not appropriate because multiple variable assignments take place if the condition is true.

if($feature.value > 100){
  percentAbove = $feature.value - 100;
  category = "high";
} else {
  category = "low";
}
return `${category} (${percentAbove}%)`;

However, when all you need to do is return a value based on a simple condition, iif will condense and simplify your expression.

2. Text

The Text function converts a value of any Arcade data type to an equivalent Text value. Many find this function useful when formatting dates, but I’ve seen a lot of expressions that implement extra logic for formatting numbers that could be avoided simply by leveraging Text.

This is especially true when it comes to rounding. You may ask: why should I use Text for number rounding when Arcade already has a Round function? For example, this pattern is very common:

Round(($feature.diplomas/$feature.AdultPopulation) * 100, 1) + “%”;
// returns "37.8%";

However, Text provides you with a % option so you don’t have to do the math of converting a decimal number (0-1) to a percentage.

Text($feature.diplomas/$feature.AdultPopulation, "#.#%");
// returns "37.8%";

But does Text really make your expression better in this case? Yes! The two approaches are distinct because Round returns a number, and Text returns a text value. Here’s why this distinction is significant:

Round changes the precision of the numbers you use for calculations within an expression without actually formatting the value. Text allows you to format a number based on a formatting pattern. While Text helps with rounding numbers, it also allows you to specify separators for large numbers. In short, Round may round your decimal numbers; however, Text will round and add digit separators to large numbers.

Despite all that, the biggest benefit to using Text is the formatting defined in Text also conforms to the locale of the app in which the expression executes. Round does not give you that benefit.

Check out the following examples and note the differences. Text always returns a cleaner and more correct result.

var v = 2891.378268263982736;
Round(v, 2);
// returns 2891.38 in English, no digit separator
// returns 2891.38 in Spanish, no digit separator (incorrect format)

Text(v, "#,###.##");
// returns "2,891.38" in English, with a digit separator
// returns "2.891,38" in Spanish (correct format)

I’ve also seen people use iif to format values above and below zero, like this:

var previous = $feature.pop2020;  // 3020
var current = $feature.pop2023;   // 3333
var change = (current - previous) / previous;

IIF(change > 0, "⬆", "⬇") + Round(change*100, 2) + "%";
// returns "⬆10.4%" in English
// returns "⬆10.4%" in Spanish (incorrect)

As noted in the potential results, this will yield bad formatting in many non-English locales. The format pattern parameter of Text actually provides an option that allows you to specify a different format for positive or negative values. Simply separate the two patterns with a semi-colon and Arcade will do the work for you. There is no need to use iif at all.

var previous = $feature.pop2020;  // 3020
var current = $feature.pop2023;   // 3333
var change = (current - previous) / previous;

Text(change, "⬆#.#%;⬇#.#%");
// returns "⬆10.4%" in English
// returns "⬆10,4%" in Spanish (correct)

The following images demonstrate how using Round will result in popup values looking incorrect depending on the locale.

Popup content "formatted" using the Round function. Round only changes the precision of floating point values, which are then cast to text as is without any real formatting taking place.
Popup content "formatted" using the Round function. Round only changes the precision of floating point values, which are then cast to text as is without any real formatting taking place. This will always be the result no matter the locale of the browser.
Popup content formatted using the Text function and displayed in an English browser. When Text is used to format numbers, digit separators may be used. The formatting also conforms to the locale of the browser.
Popup content formatted using the Text function and displayed in an English browser. When Text is used to format numbers, digit separators may be used. The formatting also conforms to the locale of the browser.
Popup content formatted using the Text function and displayed in a Spanish browser. When Text is used to format numbers, digit separators may be used. The formatting also conforms to the locale of the browser.
Popup content formatted using the Text function and displayed in a Spanish browser. When Text is used to format numbers, digit separators may be used. The formatting also conforms to the locale of the browser.

3. Decode

Decode finds a match for a coded value from a set of codes and returns an equivalent description of the matching code. This function can be used to evaluate any expression to a value and compare the result to a list of expected values and return a matching description, category, or other value.

For example, the TechCode attribute in the following example is a coded value. I commonly see users return the description of the code using a sequence of if statements, like this:

if($feature.TechCode == 30){
  return "Copper Wireline";
}
if($feature.TechCode == 40){
  return "Cable Modem";
}
if($feature.TechCode == 50){
  return "Fiber";
}
if($feature.TechCode == 60){
  return "Satellite";
}
if($feature.TechCode == 70){
  return "Terrestrial Fixed Wireless";
}
if($feature.TechCode == 90){
  return "Electric Power Line";
}

Of course, this is valid syntax and returns a correct result. You could alternatively condense this expression using the When function:

When(
  $feature.TechCode == 30, "Copper Wireline",
  $feature.TechCode == 40, "Cable Modem",
  $feature.TechCode == 50, "Fiber",
  $feature.TechCode == 60, "Satellite",
  $feature.TechCode == 70, "Terrestrial Fixed Wireless",
  $feature.TechCode == 90, "Electric Power Line",
  "Other"
);

This is also valid and returns a correct result. However, because the code comes from a single field, you can simply use Decode to return the description:

Decode($feature.TechCode,
  30, "Copper Wireline",
  40, "Cable Modem",
  50, "Fiber",
  60, "Satellite",
  70, "Terrestrial Fixed Wireless",
  90, "Electric Power Line",
  "Other"
);

This is more compact, and (in my view) easier to read. Decode is preferred for evaluating a single expression and comparing the result to a list of known values. When is preferred for evaluating multiple expressions in a prioritized sequence, and returning a value once one of the statements is true.

Decode can also be used in clever ways such as comparing multiple numeric attributes from competing categories and returning the alias or description of the field to visualize predominance.

For example, in an election dataset, you may have columns that represent the total count of votes for individual candidates, but no column that indicates the winner (because one hasn’t been declared yet). You can use Decode to return the current leader in an election as results update:

var votesPerParty = [
  $feature.Republican,
  $feature.Democrat,
  $feature.Green,
  $feature.Libertarian,
  $feature.Independent,
];

// Finds the largest number,
// matches it with the field it originates from
// and returns the alias of the field indicating the current leader
var leader = Decode(Max(votesPerParty),
  $feature.Republican, "Republican",
  $feature.Democrat, "Democrat",
  $feature.Green, "Green",
  $feature.Libertarian, "Libertarian",
  $feature.Independent, "Independent",
  "Other"
);

return leader;

4. DefaultValue

DefaultValue returns a value if an empty value (null or '') is detected. Users commonly want to display default text in case a value does not exist. Many people use a combination of iif and IsEmpty to account for empty values.

// returns a default value of 0 if the data field is empty
IIF(!IsEmpty($feature.fieldName), $feature.fieldName, 0);

This works, but the DefaultValue function does all the lifting for you. All you have to do is specify which value you want to check, and the default value to return if it is empty.

The equivalent to the previous expression is easier to read:

// returns a default value of 0 if the data field is empty
DefaultValue($feature.fieldName, 0);

In popups, it can be helpful to display a message that no data is available so the user knows the data is collected in the layer, but isn’t available for the selected feature.

DefaultValue($feature.status, "No data available");
// returns "No data available" if the status field is not populated

I recently came across the following expression that could have been cut in half, had the expression author used DefaultValue. Note how the return value in the if and the else are nearly identical. The only difference is displaying the value of one attribute or another if the first is empty.

var percentageValue = Round(($feature.RecreationalVisits_int/$feature.TotalRecreationVisits)*100, 2) + "%";
var unitType = Lower($feature.UNIT_TYPE);
var parkName = $feature.PARKNAME;

if(IsEmpty(parkName)){
  return {
    text: "{UNIT_NAME} is a " + unitType + " that had <b>" +
    "{RecreationalVisits_int}</b> recreational visitors in 2022. " +
    "This accounts for <b>" + percentageValue + "</b> of visits " +
    "to all national parks in 2022."
  }
} else {
  return {
    text: "{PARKNAME} is a " + unitType + " that had <b>" +
    "{RecreationalVisits_int}</b> recreational visitors in 2022. " +
    "This accounts for <b>" + percentageValue + "</b> of visits " +
    "to all national parks in 2022."
  }
}

This is the updated expression leveraging DefaultValue for the parkName variable, which results in less text duplication in the expression. I also use Text to clean up the number formatting and make the values locale aware.

var percentageValue = Text($feature.RecreationalVisits_int/$feature.TotalRecreationVisits, "#.##%");
var unitType = Lower($feature.UNIT_TYPE);
var parkName = DefaultValue($feature.PARKNAME, $feature.UNIT_NAME);

return {
  text : parkName + " is a " + unitType + " that had <b>" +
    Text($feature.RecreationalVisits_int, "#,###") +
    "</b> recreational visitors in 2022. This accounts for <b>" +
    percentageValue + "</b> of visits to all national parks in 2022."
};

5. Number

The Number function converts an input value of any type to a number. People often request a function to return the the number of milliseconds since Jan. 1, 1970 similar to JavaScript’s Date.getTime() method. This function already exists! Number will do this if you pass a date value as a parameter to the function.

Number(Date())
// returns epoch value

It’s also common for people to store binary values as a feature attribute. Some attributes are either true or false, so the value will either be stored as a 0 (false) or 1 (true).

I’ve seen some expressions do the following when calculating a value in editing workflows:

IIF($feature.status == "completed", 1, 0);

While this works, you can also leverage Number to return a 1 or a 0 when evaluating a Boolean expression.

Number($feature.status == "completed");
// returns 1 if true, 0 if false

This is actually how the ArcGIS Maps SDK for JavaScript calculates the sum of each category when visualizing clusters as pie charts.

Aggregate field calculated for use in a cluster's pie chart. The expression here evaluates for all features in the cluster. If the complaint type is "Blocked Driveway", the expression returns 1 for the feature. All values are summed resulting in the total count of blocked driveway complaints within the cluster.
Aggregate field calculated for use in a cluster's pie chart. The expression here evaluates for all features in the cluster. If the complaint type is "Blocked Driveway", the expression returns 1 for the feature. All values are summed resulting in the total count of blocked driveway complaints within the cluster.

Number can also be helpful if you need to parse a value from text and convert to a number for a calculation or number visualization.

Number("38.7%", "#.#%");
// returns 0.387

Number("1,000 people", "# people");
// returns 1000

The most under-appreciated function may be your own

If you find yourself writing duplicate code within one expression, you will likely benefit from writing a custom function. Functions allow you to write would-be duplicate code once and give it an identifier. This significantly reduces errors that creep in with copy/paste and can greatly reduce the size of an expression.

In one extreme case, I was shown an expression longer than 900 lines in length that was eventually condensed to 22 lines just by defining a custom function and calling it in a for loop.

Conclusion

As with any other scripting or expression language, there are multiple ways to solve a task or come to a correct result. However, the functions described in this post (iif, Text, Decode, DefaultValue, Number) can simplify your expressions, improve their readability, and reduce bugs. Defining your own functions can also save you time, reduce the length of your expressions, and reduce bugs introduced in copy/paste operations.

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:
0 Comments
Inline Feedbacks
View all comments

Next Article

Stuttgart 3D - Creating Foundational Content for an Even Smarter City

Read this article