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
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.
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.
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.
Article Discussion: