Managed expression - Arcade

712
6
02-07-2022 11:36 PM
Labels (1)
elpinguino
Occasional Contributor III

Hi, I'm starting to apply Arcade more in Map Viewer pop-ups and symbology. I can't figure out what I've done wrong here. Everything looks ok when I test this, except for when I have 5 "To do"s, the output doesn't show as "To do" as it should. It instead shows as "In progress".

I think the error is in line 15-17.

/*Statuses: To do, In progress, Complete as possible, 100% complete
To do - If any To dos, output To do
In progress - If there is an In progress, output In progress
If no To dos, In progress or not all Completes, output Complete as possible
100% Complete - If all equal Complete, output 100% Complete
*/

var checklist = $feature["Checklist_Status"]
var consent = $feature.ConsentFormStatus
var occupier = $feature.OccupierComplete
var owner = $feature.OwnerComplete
var water = $feature.WaterIntakeFormStatus

//To do - If any To dos, output To do
If (checklist == "ToDo" && consent == "To do" && occupier == "To do" && owner == "To do" && water == "To do"){
    return "To do"
    }

//In progress - If there is an In progress, output In progress
    else if (checklist != "ToDo" || consent != "To do" || occupier != "To do" || owner != "To do" || water != "Pending"){
        return "In progress"
    }
        
//100% Complete - If all equal Complete, output 100% Complete
    else if (checklist == "Complete" && (consent == "Complete - Eversign" || consent == "Complete - Manual" || consent == "Complete - Survey123") && (occupier == "Yes" || occupier == "Owner is occupier") && owner == "Yes" && (water == "Complete - Eversign" || water == "Complete - Manual" || water == "Complete - Survey123")){
        return "100% Complete"
    }    
    
    else {
    return "Complete as possible"
}

 I'm hoping to apply this as a field in the pop-up, as an expression for symbology and dynamic content in ExB's text widget.

Also, as I'm still early in my Arcade journey, if you notice there's a better way I could be doing this, please point it out.

Thank you in advance for your help.

Tags (1)
0 Kudos
6 Replies
jcarlson
MVP Esteemed Contributor

A lot of what you're doing in here can be made much simpler using an array and some of the array-specific functions.

First, we create an array, rather than a series of separate variables, to hold our attributes.

 

var checklist = $feature["Checklist_Status"]
var consent = $feature.ConsentFormStatus
var occupier = $feature.OccupierComplete
var owner = $feature.OwnerComplete
var water = $feature.WaterIntakeFormStatus

var status_array = [
    checklist,
    consent,
    occupier,
    owner,
    water
]

 

Now we can work with that array. There are a few ways we can approach this.

To Dos

For the "to do" items, you are checking your values for any "to do" items.

Side note: you've written your expression with lots of '&&', which means AND. Your "to do" section will only evaluate to true if all items have a "to do" value. Your comment above that section says "any", however, so it's not clear which you're looking to do. I'll address both.

Normally, I would suggest using Includes for this, but I can see that one of the values is different from the others. You could just use the function twice in that case, like this:

 

If(Includes(status_array, 'ToDo') || Includes(status_array, 'To do')){
    return 'To do'
}

 

But I'd also like to make mention of the functions Any and All. You'll have to create a custom function for it, but it allows us to be a little more precise. This expression will return 'To Do' if any of our values in the array have a defined "to do" value.

 

function hasToDos(value){
    // Check if value exists in array of valid values
    Includes(['ToDo', 'To do'], value)
}

If(Any(status_array, hasToDos)){
    return 'To Do'
}

 

With that custom function, we can easily alter our expression to instead check for all values being a "to do". We just swap out Any for All! This expression will return 'To Do' only if all the values match our list of defined "to do" values.

 

function hasToDos(value){
    // Check if value exists in array of valid values
    Includes(['ToDo', 'To do'], value)
}

If(All(status_array, hasToDos)){
    return 'To Do'
}

 

 

In Progress

If I understand your expression correctly, "In Progress" is equal to any of the values not being set to "to do"? If so, then we've got it easy. We can just re-use our existing function with a slight tweak. By including a "!" in our expression, we can negate a conditional statement. The inverse of "all to dos" is equivalent to "not all to dos". (As opposed to the None function, which is "all not", very different from "not all"!)

 

Else If(!All(status_array, hasToDos)){
    return 'In Progress'
}

 

The one thing that's throwing me off here is that your list of values has the water status of "pending" here. Why does that attribute get checked differently than everything else?

 

Complete

Your "complete" status seems to be a bit more involved than the other two. As with your "to do" section, your conditions are all joined the "&&", so these will all need to be true. You can use line breaks pretty liberally in Arcade, so for longer lines like this, I suggest breaking them up a bit.

 

Else If(checklist == "Complete" &&
(consent == "Complete - Eversign" || consent == "Complete - Manual" || consent == "Complete - Survey123") &&
(occupier == "Yes" || occupier == "Owner is occupier") &&
owner == "Yes" &&
(water == "Complete - Eversign" || water == "Complete - Manual" || water == "Complete - Survey123")){
    return '100% Complete'
}

 

But in this case, it's still a bit much to keep track of. Within this large conditional statement, there are a few different things happening. You essentially have a series of Includes functions nested within a large All.

If we can get each main condition to evaluate to a true/false, then we can easily check for full completion. Also, since we're checking those forms for the same three values, we can establish those values separately and re-use them to keep things concise.

 

// Complete form values
var form_completes = [
    'Complete - Eversign',
    'Complete - Manual',
    'Complete - Survey123'
]

// This array will evaluate to a series of true/false values
var completions = [
    checklist == 'Yes',
    Includes(form_completes, consent),
    Includes(['Yes', 'Owner is occupier'], occupier),
    owner == 'Yes',
    Includes(form_completes, water)
]

// Simple function to return the array values
function isTrue(value){return value}

// Check if completions are all true
Else If(All(completions, isTrue)){
    return '100% Complete'
}

 

This expression may be taller than the original, but in my opinion, that's easier to follow than having excessively long single lines.

Evaluation Order

Something critical to note here: your if statements will evaluate in order. Suppose all of your fields would evaluate to "100% Complete". This would also fit the criteria for "In Progress"! And since the "In Progress" check comes first, you'll basically never see a "Complete" value output from this expression.

Easy fix: just list your conditions in order of specificity. If your fields would not evaluate to being "Complate", then it's fine to return an "In Progress". If we check completion first, we're good.

The Whole Thing

See if this works for you:

 

var checklist = $feature["Checklist_Status"]
var consent = $feature.ConsentFormStatus
var occupier = $feature.OccupierComplete
var owner = $feature.OwnerComplete
var water = $feature.WaterIntakeFormStatus

var status_array = [
    checklist,
    consent,
    occupier,
    owner,
    water
]

// Check if value exists in array of valid values
function hasToDos(value){
    Includes(['ToDo', 'To do'], value)
}

// Complete form values
var form_completes = [
    'Complete - Eversign',
    'Complete - Manual',
    'Complete - Survey123'
]

// This array will evaluate to a series of true/false values
var completions = [
    checklist == 'Yes',
    Includes(form_completes, consent),
    Includes(['Yes', 'Owner is occupier'], occupier),
    owner == 'Yes',
    Includes(form_completes, water)
]

// Simple function to return the array values
function isTrue(value){return value}

// Determine status!

If(Any(status_array, hasToDos)){  // Or use "All" function, if that's what you're trying to do
    return 'To Do'
} Else If(All(completions, isTrue)){
    return '100% Complete'
} Else If(!All(status_array, hasToDos)){
    return 'In Progress'
} Else {
    return 'Complete as possible'
}

 

- Josh Carlson
Kendall County GIS
elpinguino
Occasional Contributor III

Hi @jcarlson . Thank you for the thorough reply. This is good learnings for me. Really appreciate you taking the time to break it down.

To answer some of your questions:

There are inconsistencies in the lists because different fields were added in at different times, and I hadn't planned to be writing something like this. Thus WaterIntakeForm - Pending instead of In progress. The system is in use during the day too, so any changes I have to make are after hours, so I'm trying to minimise what I change on the front end and have to migrate on the backend.

For the To dos, I've changed it to All. We want to make it clear whether anything has been started on that parcel or not.

I picked up fairly quickly everything you explained. The var completions array/ Includes I don't totally have my head wrapped around.

I added in a few things to var form_completes.

There is one more thing I've tried to fix. I think I have, but have a look anyway.

There are a few parcels where we just won't be able to get all the forms or collect details because we don't even have contact details for the landholder and no one is home when we go by. It's important we distinguish that we have tried to make contact, but just can't. This would be the output Complete as possible. So there couldn't be any To dos in it nor Yes or Complete. Below is a screenshot of the issue.

elpinguino_0-1644374492832.png

Everything else I've come across so far works though. Thank you.

This is how I've adjusted the script. After looking at what you wrote, I replicated the hasToDos function in the hasImpossible function, but I have 'Not able to. No contact details.' and 'Not possible to complete' under both var form_completes and the hasImpossible function. So the above screenshot now appears as 'Complete as possible'. Will doubling up cause issues in the script? Is that bad practice?

 

var checklist = $feature["Checklist_Status"]
var consent = $feature.ConsentFormStatus
var occupier = $feature.OccupierComplete
var owner = $feature.OwnerComplete
var water = $feature.WaterIntakeFormStatus

var status_array = [
    checklist,
    consent,
    occupier,
    owner,
    water
]

// Check if value exists in array of valid values
function hasToDos(value){
    Includes(['ToDo', 'To do'], value)
}

// Check if value exists in array of valid values
function hasImpossible(value){
    Includes(['Not applicable', 'Not required','Not able to. No contact details.', 'Not possible to complete'], value)
}

// Complete form values plus don't need forms
var form_completes = [
    'Complete - Eversign',
    'Complete - Manual',
    'Complete - Survey123',
    "Complete via Eversign",
    'Not applicable',
    'Not required'
]

// This array will evaluate to a series of true/false values
var completions = [
    checklist == 'Yes',
    Includes(form_completes, consent),
    Includes(['Yes', 'Owner is occupier'], occupier),
    owner == 'Yes',
    Includes(form_completes, water)
]

// Simple function to return the array values
function isTrue(value){return value}

// Determine status!

If(All(status_array, hasToDos)){  // Or use "Any" function, if that's what you're trying to do
    return 'To do'
} Else If(All(completions, isTrue)){
    return '100% Complete'
} Else If(All(status_array, hasImpossible)){
    return 'Complete as possible'
} Else If(!All(status_array, hasToDos)){
    return 'In progress'
} Else {
    return 'Complete as possible'
}

 

 

0 Kudos
elpinguino
Occasional Contributor III

Hi @jcarlson . I wrote a long reply to this earlier in the day, but it looks like it didn't post. Thank you so much for the thorough explanation. I really appreciate it. This is good learnings for me!

It all looks pretty good. I understand mostly everything. Could I write this from scratch myself tomorrow if asked to? Not quite, but when I look at it, I think I can replicate it.

To answer some of your questions:

To do should be all. This is because I want to make it clear what parcels have not been touched yet. I have changed this in the script.

Why aren't all the lists matching? Well, I didn't realise we'd be adding in all these forms, checks, etc. So we added it in one by one. I didn't realise I'd be writing something like this. The system is live now. I'm having to make these updates after hours and to minimise moving things around, losing data or confusing people, I'm trying to avoid changing things. It's messy.

There is one combo that isn't producing like I want it to, but I think I've found a way around it. You'll see in the script that I've added Not applicable and Not required into var form_completes. I've also added it into a new function I've made though function hasImpossible. These are for all the list options where it isn't possible to be complete. You'll see in the below screenshot, it was outputting In progress. It now outputs Complete as possible with the new function. I wonder if this is going to cause trouble down the track since I have them in two different areas.

elpinguino_0-1644385961933.png

 

This is the script I'm using now, and I'm going to do some further testing. Thanks again for explaining in such detail how to do all this. I've saved the post in a safe place so I can refer to it when I try to write more stuff in the future.

var checklist = $feature["Checklist_Status"]
var consent = $feature.ConsentFormStatus
var occupier = $feature.OccupierComplete
var owner = $feature.OwnerComplete
var water = $feature.WaterIntakeFormStatus

var status_array = [
    checklist,
    consent,
    occupier,
    owner,
    water
]

// Check if value exists in array of valid values
function hasToDos(value){
    Includes(['ToDo', 'To do'], value)
}

// Check if value exists in array of valid values
function hasImpossible(value){
    Includes(['Not applicable', 'Not required','Not able to. No contact details.', 'Not possible to complete'], value)
}

// Complete form values plus don't need forms
var form_completes = [
    'Complete - Eversign',
    'Complete - Manual',
    'Complete - Survey123',
    "Complete via Eversign",
    'Not applicable',
    'Not required'
]

// This array will evaluate to a series of true/false values
var completions = [
    checklist == 'Yes',
    Includes(form_completes, consent),
    Includes(['Yes', 'Owner is occupier'], occupier),
    owner == 'Yes',
    Includes(form_completes, water)
]

// Simple function to return the array values
function isTrue(value){return value}

// Determine status!

If(All(status_array, hasToDos)){  // Or use "Any" function, if that's what you're trying to do
    return 'To do'
} Else If(All(completions, isTrue)){
    return '100% Complete'
} Else If(All(status_array, hasImpossible)){
    return 'Complete as possible'
} Else If(!All(status_array, hasToDos)){
    return 'In progress'
} Else {
    return 'Complete as possible'
}

Should I remove line 58? Do the changes I've made make sense?

0 Kudos
elpinguino
Occasional Contributor III

Hi @jcarlson , you taught me how to set this up almost a year ago. It's made a huuuge difference to our system. We are going through a redesign of the system now where owner and occupier are in one layer and consent, water intake and checklist are in another. I've been looking at how to modify this expression to use featuresetbyname (similar to in this article - https://learn.arcgis.com/en/projects/access-attributes-from-another-layer-with-arcade/). I am getting stuck around Step 10 where we filter down to increase loading speed. In the article they use country. I'd be using OperationName. Are you able to give any tips or pointers?

0 Kudos
jcarlson
MVP Esteemed Contributor

It really depends on what you're doing with the data. I like FeatureSetByPortalItem because it works off of the ItemID of the layer, not the map's table of contents. It also doesn't require the other layer to be in the map at all.

Are the layers in entirely different services, or just different layers of the same service? $datastore can be used for other layers in the same service, too. Are any of the layers linked by a relationship class?

- Josh Carlson
Kendall County GIS
0 Kudos
elpinguino
Occasional Contributor III

Hi @jcarlson , we are using the arcade expression to power the symbology in stoplight colours like red for to do, yellow for in progress and green for complete.

Both layers are in the map and in the same service. Yes they have a relationship. Layer1 is the parent, and Layer2 is the child. Layer1 is to use the arcade expression/symbology.

I asked one other person who has helped me with arcade before because I realised it was the American weekend still when I wrote, and he said  FeatureSetByName, FeatureSetById etc are not supported in Arcade when using an expression to define symbology. I didn't realise that before posting here and didn't provide you the full context. Now that you know are you in agreement or do you know a way around that?

0 Kudos