Arcade Expression is not working properly. Keeps showing estimated population as "N/A"

259
6
3 weeks ago
lisaswarnNJDOH
Emerging Contributor

Hello, 

I am in pop up configuration, and my expression was working properly but now, it is not, instead showing N/A

below is my code: 

function safeRound(val) {
  if (IsEmpty(val)) { return null; }
  var n = Number(val);
  if (IsEmpty(n)) { return null; }
  return Round(n);
}

function ordinal(n) {
  var d = n % 100;
  if (d >= 11 && d <= 13) {
    return Text(n) + "th";
  }
  var r = n % 10;
  if (r == 1) {
    return Text(n) + "st";
  }
  if (r == 2) {
    return Text(n) + "nd";
  }
  if (r == 3) {
    return Text(n) + "rd";
  }
  return Text(n) + "th";
}

// simple title case: capitalizes first letter of each word
function titleCase(s) {
  var words = Split(s, " ");
  var out = [];
  for (var i in words) {
    var w = words[i];
    if (Count(w) == 0) {
      Push(out, w);
      continue;
    }
    var hyphens = Split(w, "-");
    var rebuilt = [];
    for (var j in hyphens) {
      var piece = hyphens[j];
      var first = Upper(Left(piece, 1));
      var rest = "";
      if (Count(piece) > 1) {
        rest = Lower(Right(piece, Count(piece) - 1));
      }
      Push(rebuilt, first + rest);
    }
    Push(out, Concatenate(rebuilt, "-"));
  }
  return Concatenate(out, " ");
}

// safe estimated population fetch; expects fields named like <BASE>_EstPop
function getEstPop(base) {
  var fieldName = base + "_EstPop";
  var raw = DefaultValue($feature, fieldName, "N/A");
  if (raw == "N/A" || IsEmpty(raw)) {
    return "N/A";
  }
  var num = Number(raw);
  if (IsEmpty(num)) {
    return "N/A";
  }
  return Text(Round(num));
}

// selection-sort descending without Remove
function sortDesc(items) {
  var out = [];
  var working = items;
  while (Count(working) > 0) {
    var maxIdx = 0;
    for (var i = 1; i < Count(working); i++) {
      if (working[i].score > working[maxIdx].score) {
        maxIdx = i;
      }
    }
    Push(out, working[maxIdx]);
    var next = [];
    for (var j = 0; j < Count(working); j++) {
      if (j != maxIdx) {
        Push(next, working[j]);
      }
    }
    working = next;
  }
  return out;
}

// buckets as arrays of objects
var veryHigh = [];
var high = [];
var medium = [];

//Health Outcomes
// TEETHLOST
var t1 = safeRound($feature['CDC_PLACES_2175.TEETHLOST_percentile']);
var est1 = getEstPop("TEETHLOST");
if (t1 != null) {
  if (t1 >= 90) { Push(veryHigh, { label: "All teeth lost", score: t1, estPop: est1 }); }
  else if (t1 >= 80) { Push(high, { label: "All teeth lost", score: t1, estPop: est1 }); }
  else if (t1 >= 70) { Push(medium, { label: "All teeth lost", score: t1, estPop: est1 }); }
}

// ARTHRITIS
var t3 = safeRound($feature['CDC_PLACES_2175.ARTHRITIS_percentile']);
var est3 = getEstPop("ARTHRITIS");
if (t3 != null) {
  if (t3 >= 90) { Push(veryHigh, { label: "Arthritis", score: t3, estPop: est3 }); }
  else if (t3 >= 80) { Push(high, { label: "Arthritis", score: t3, estPop: est3 }); }
  else if (t3 >= 70) { Push(medium, { label: "Arthritis", score: t3, estPop: est3 }); }
}

// CANCER
var t6 = safeRound($feature['CDC_PLACES_2175.CANCER_percentile']);
var est6 = getEstPop("CANCER");
if (t6 != null) {
  if (t6 >= 90) { Push(veryHigh, { label: "Cancer (non-skin) or melanoma", score: t6, estPop: est6 }); }
  else if (t6 >= 80) { Push(high, { label: "Cancer (non-skin) or melanoma", score: t6, estPop: est6 }); }
  else if (t6 >= 70) { Push(medium, { label: "Cancer (non-skin) or melanoma", score: t6, estPop: est6 }); }
}

// COPD
var t10 = safeRound($feature['CDC_PLACES_2175.COPD_percentile']);
var est10 = getEstPop("COPD");
if (t10 != null) {
  if (t10 >= 90) { Push(veryHigh, { label: "COPD", score: t10, estPop: est10 }); }
  else if (t10 >= 80) { Push(high, { label: "COPD", score: t10, estPop: est10 }); }
  else if (t10 >= 70) { Push(medium, { label: "COPD", score: t10, estPop: est10 }); }
}

// CHD
var t11 = safeRound($feature['CDC_PLACES_2175.CHD_percentile']);
var est11 = getEstPop("CHD");
if (t11 != null) {
  if (t11 >= 90) { Push(veryHigh, { label: "Coronary heart disease", score: t11, estPop: est11 }); }
  else if (t11 >= 80) { Push(high, { label: "Coronary heart disease", score: t11, estPop: est11 }); }
  else if (t11 >= 70) { Push(medium, { label: "Coronary heart disease", score: t11, estPop: est11 }); }
}

// CASTHMA
var t12 = safeRound($feature['CDC_PLACES_2175.CASTHMA_percentile']);
var est12 = getEstPop("CASTHMA");
if (t12 != null) {
  if (t12 >= 90) { Push(veryHigh, { label: "Current asthma", score: t12, estPop: est12 }); }
  else if (t12 >= 80) { Push(high, { label: "Current asthma", score: t12, estPop: est12 }); }
  else if (t12 >= 70) { Push(medium, { label: "Current asthma", score: t12, estPop: est12 }); }
}

// DEPRESSION
var t15 = safeRound($feature['CDC_PLACES_2175.DEPRESSION_percentile']);
var est15 = getEstPop("DEPRESSION");
if (t15 != null) {
  if (t15 >= 90) { Push(veryHigh, { label: "Depression", score: t15, estPop: est15 }); }
  else if (t15 >= 80) { Push(high, { label: "Depression", score: t15, estPop: est15 }); }
  else if (t15 >= 70) { Push(medium, { label: "Depression", score: t15, estPop: est15 }); }
}

// DIABETES
var t16 = safeRound($feature['CDC_PLACES_2175.DIABETES_percentile']);
var est16 = getEstPop("DIABETES");
if (t16 != null) {
  if (t16 >= 90) { Push(veryHigh, { label: "Diabetes", score: t16, estPop: est16 }); }
  else if (t16 >= 80) { Push(high, { label: "Diabetes", score: t16, estPop: est16 }); }
  else if (t16 >= 70) { Push(medium, { label: "Diabetes", score: t16, estPop: est16 }); }
}


// BPHIGH
var t22 = safeRound($feature['CDC_PLACES_2175.BPHIGH_percentile']);
var est22 = getEstPop("BPHIGH");
if (t22 != null) {
  if (t22 >= 90) { Push(veryHigh, { label: "High blood pressure", score: t22, estPop: est22 }); }
  else if (t22 >= 80) { Push(high, { label: "High blood pressure", score: t22, estPop: est22 }); }
  else if (t22 >= 70) { Push(medium, { label: "High blood pressure", score: t22, estPop: est22 }); }
}

// HIGHCHOL
var t23 = safeRound($feature['CDC_PLACES_2175.HIGHCHOL_percentile']);
var est23 = getEstPop("HIGHCHOL");
if (t23 != null) {
  if (t23 >= 90) { Push(veryHigh, { label: "High cholesterol", score: t23, estPop: est23 }); }
  else if (t23 >= 80) { Push(high, { label: "High cholesterol", score: t23, estPop: est23 }); }
  else if (t23 >= 70) { Push(medium, { label: "High cholesterol", score: t23, estPop: est23 }); }
}

// OBESITY
var t30 = safeRound($feature['CDC_PLACES_2175.OBESITY_percentile']);
var est30 = getEstPop("OBESITY");
if (t30 != null) {
  if (t30 >= 90) { Push(veryHigh, { label: "Obesity", score: t30, estPop: est30 }); }
  else if (t30 >= 80) { Push(high, { label: "Obesity", score: t30, estPop: est30 }); }
  else if (t30 >= 70) { Push(medium, { label: "Obesity", score: t30, estPop: est30 }); }
}


// STROKE
var t37 = safeRound($feature['CDC_PLACES_2175.STROKE_percentile']);
var est37 = getEstPop("STROKE");
if (t37 != null) {
  if (t37 >= 90) { Push(veryHigh, { label: "Stroke", score: t37, estPop: est37 }); }
  else if (t37 >= 80) { Push(high, { label: "Stroke", score: t37, estPop: est37 }); }
  else if (t37 >= 70) { Push(medium, { label: "Stroke", score: t37, estPop: est37 }); }
}


// sort buckets descending
veryHigh = sortDesc(veryHigh);
high = sortDesc(high);
medium = sortDesc(medium);

// format back to strings with title case applied to labels, including percentile and est pop
function format(arr) {
  var out = [];
  for (var i in arr) {
    var o = arr[i];
    var pct = ordinal(o.score);
    var estText = When(o.estPop == "N/A", "N/A", o.estPop);
    Push(out, titleCase(o.label) + " (" + pct + " Percentile and Est Pop: " + estText + ")");
  }
  return out;
}

var vhStr = format(veryHigh);
var hStr = format(high);
var mStr = format(medium);

// assemble output with HTML breaks between categories
var parts = [];
if (Count(vhStr) > 0) {
  Push(parts, "<b>Very High (" + Count(vhStr) + "):</b> " + Concatenate(vhStr, ", "));
}
if (Count(hStr) > 0) {
  Push(parts, "<b>High (" + Count(hStr) + "):</b> " + Concatenate(hStr, ", "));
}
if (Count(mStr) > 0) {
  Push(parts, "<b>Medium (" + Count(mStr) + "):</b> " + Concatenate(mStr, ", "));
}

if (Count(parts) == 0) {
  return "No conditions at Medium or above";
}
return Concatenate(parts, "<br><br>");




0 Kudos
6 Replies
DanielFox1
Esri Regular Contributor

Hi @lisaswarnNJDOH 

If your layer has been republished or updated I would start by checking that all your fields are the same and match exactly. 

Does your data contain null or empty values for the selected features? You could use the Arcade Debugger in the pop-up configuration to inspect $feature values directly, try printing raw values to confirm they are populated. 

You can also add in a temporary line to the expression return $feature; to test if you can isolate the issue down further. 

I hope this helps in some way. 

lisaswarnNJDOH
Emerging Contributor

This is a layer I made, so no updates. The fields are correct because the rest of the code is working properly just not the estimated population. It was working perfectly yesterday and today I noticed it. There are null values for about 6 rows. 


Popup.jpg

0 Kudos
HaydenWelch
MVP Regular Contributor

Since you're referencing the '_EstPop' fields dynamically, they might not being loaded. You can fix that by including an Expects() call:

Expects($feature, 
  'TEETHLOST_EstPop',
  'ARTHRITIS_EstPop',
  'CANCER_EstPop',
  'COPD_EstPop',
  'CHD_EstPop',
  'CASTHMA_EstPop',
  'DEPRESSION_EstPop',
  'DIABETES_EstPop',
  'BPHIGH_EstPop',
  'HIGHCHOL_EstPop',
  'OBESITY_EstPop',
  'STROKE_EstPop'
)

 

https://developers.arcgis.com/arcade/function-reference/feature_functions/#expects

 

Now that we know that, we can use it at the top of the script and make everything more dynamic:

var outcome_messages = { 
  'TEETHLOST' : 'All teeth lost',
  'ARTHRITIS': 'Arthritis',
  'CANCER': 'Cancer (non-skin) or melanoma',
  'COPD': 'COPD',
  'CHD': 'Coronary heart disease',
  'CASTHMA': 'Current asthma',
  'DEPRESSION': 'Depression',
  'DIABETES': 'Diabetes',
  'BPHIGH': 'High blood pressure',
  'HIGHCHOL': 'High cholesterol',
  'OBESITY': 'Obesity',
  'STROKE': 'Stroke',
}

Expects($feature, 
  'TEETHLOST_EstPop',
  'CDC_PLACES_2175.TEETHLOST_percentile',
  'ARTHRITIS_EstPop',
  'CDC_PLACES_2175.ARTHRITIS_EstPop',
  'CANCER_EstPop',
  'CDC_PLACES_2175.CANCER_EstPop',
  'COPD_EstPop',
  'CDC_PLACES_2175.COPD_EstPop',
  'CHD_EstPop',
  'CDC_PLACES_2175.CHD_EstPop',
  'CASTHMA_EstPop',
  'CDC_PLACES_2175.CASTHMA_EstPop',
  'DEPRESSION_EstPop',
  'CDC_PLACES_2175.DEPRESSION_EstPop',
  'DIABETES_EstPop',
  'CDC_PLACES_2175.DIABETES_EstPop',
  'BPHIGH_EstPop',
  'CDC_PLACES_2175.BPHIGH_EstPop',
  'HIGHCHOL_EstPop',
  'CDC_PLACES_2175.HIGHCHOL_EstPop',
  'OBESITY_EstPop',
  'CDC_PLACES_2175.OBESITY_EstPop',
  'STROKE_EstPop',
  'CDC_PLACES_2175.STROKE_EstPop',
)

// function declarations (Proper can be used to replace the titleCase function)
// buckets as arrays of objects
var veryHigh = [];
var high = [];
var medium = [];

function getOutcomes(percentile, popField, message) {
    var label = { 
        label: message, 
        score: safeRound($feature[percentile]), 
        estPop: getEstPop(popField) 
    }

    if (t >= 90) { Push(veryHigh, label) }
    else if (t >= 80) { Push(high, label) }
    else if (t >= 70) { Push(medium, label) }
} 

//Health Outcomes
for ( var outome in outcome_messages ) {
    getOutcomes(
        `CDC_PLACES_2175.${outcome}_percentile`, 
        outcome, 
        outcome_messages[outcome]
    )
}

// Handle the buckets here

 

Additional note on using Proper, since it's a builtin you get massive performance boosts for using it. If a string is encountered more than 5 or so times, the interpreter will actually cache the result and avoid re-calculating it. Which is great if you have a lot of similar strings that are being titlecased

RPGIS
by MVP Regular Contributor
MVP Regular Contributor

There are also, in addition to what @HaydenWelch mentioned, there are a few ways in which your code can be rewritten to be simpler.

  1. Utilize the When function when there are multiple if-if else statements. The when function behaves in a similar manner and ultimately returns the same thing. You can specify the default value as null to basically do nothing if none of the when conditions are met.
  2. When there are multiple values being made but logically are the same then it is best to create a dictionary of those values rather than resulting to multiple statements.
  3. You can also use the example below in conjunction with Expects.
Expects($feature)
var Values = {}
for( var i in $feature ){ Values[i] = $feature[i] }

/* Another option
Expects($feature)
var Values = Dictionary($feature).attributes
*/

Just for fun I am trying to rewrite your code for suggestive purposes.

HaydenWelch
MVP Regular Contributor

I do like the When function in some cases, but I also feel like it can make your code harder to read later. I prefer to just have a bunch of if statements with no else/elifs after and let them fallthrough to a base return:

function f(a) {
    if (a > 10) { return 'A is Greater Than 10' }
    if (a == 9) { return 'A is 9' }
    if (a == 1) { return 'A is 1'}
    return 'A is an unknown value'
}

function g(a) {
    return When(
        (a > 10), 'A is Greater Than 10',
        (a == 9), 'A is 9',
        (a == 1), 'A is 1',
        'A is an unknown value'
    )
}

 

Those are identical, but it's basically up to the programmer which they think is more readable lol

RPGIS
by MVP Regular Contributor
MVP Regular Contributor

Here is what I came up with but without having any sample data it is the best that I could do. This is just a suggestive means but feel free to modify and ask any further questions.

function RoundN(val){ iif( TypeOf(Number(val))=='Number', Round(Number(val)), 0 ) }

function Risk(n){ iif( Number(n)>0, Round(n/10)*10, 0 ) }

function ordinal(n) {
	var r = n % 10
	var t = When( r==1, "st", r==2, "nd", r==3, "rd", "th" )
	iif( !IsEmpty(n), Concatenate(n,t), null )
	}

// Set the default values
var RootFieldNames = [
	"TEETHLOST","ARTHRITIS","CANCER","COPD",
	"CHD","CASTHMA","DEPRESSION","DIABETES",
	"BPHIGH","HIGHCHOL","OBESITY","STROKE"
	]

// Set the coomon names for all fields
var RootName = 'CDC_PLACES_2175'
var percName = '_percentile'
var popName = '_EstPop'

// Creates a new line
var nl = TextfFormatting.NewLine

/* Get all attributes in the feature
Loop through and populate the dictionary */
Expects($feature,'*')
var Values = {}
for( var i in $feature ){ Values[i] = $feature[i] }

/* Another option
Expects($feature)
var Values = Dictionary($feature).attributes */

/* Create the dictionaries to be populated
using the information gathered below */
var Convert = {}
var PopVals = {}
var PCTVals = {}

for( var r in RootFieldNames ){
	r = RootFieldNames[r]
	var pctname = RootName + '.' + r + PercFName
	var popname = r+PopFName
	
	//Get the field name and round if the field is in the 
	if( HasKey(pctname,Values) ){ PCTVals[r] = RoundN(Values[pctname]) }
	else if( HasKey(popname,Values) ){ PopVals[r] = PCTVals[r] = Values[popname] }
	
	//Populate the conversion dictionary with the default values
	Convert[r] = When(
		r=="TEETHLOST","All teeth lost",
		r=="CANCER","Cancer (non-skin) or melanoma",
		r=="CHD","Coronary heart disease",
		r=="CASTHMA","Coronary heart disease",
		r=="CHD","Current asthma",
		Includes(["ARTHRITIS","DEPRESSION","DIABETES","OBESITY","STROKE"],r),Proper(r),
		r=="BPHIGH","High blood pressure",
		r=="HIGHCHOL","High cholesterol",
		r=="COPD", r,
		null
		)
	}

// Set the score and limit dictionary
var Limits = { VeryHigh:[], High:[], Medium:[] }
var PctScores = { VeryHigh:[], High:[], Medium:[] }

// Loop through all keys in the conversion dictionary
for( var i in Convert ){
	var pop = PopVals[i]
	var pct = PCTVals[i]
	
	// Set value dictionary with the default value
	var Def = { label: Convert[i], score: pct, estPop: pop }
	
	// Get the rating of the measured risk percentage
	var R = When( Risk(pct)==90, 'VeryHigh', Risk(pct)==80, 'High', Risk(pct)==70, 'Medium', Null )
	
	/* If the rating is a key value in the limit dictionary
	then populate both the limit dictionary with the
	default value and the percentage score array with the
	percentages */
	if( HasKey(Limits,R) ){ 
		Push( Limits[R], Def )
		Push( PctScores[R], pct )
		}
	}

// Set the html array
var HTMLText = []

// Loop through the limit dictionary
for( var l in Limits ){
	var vals = Limits[l]
	
	if( Count(vals) > 0 ){
		
		// Get the score array and sort from highest to lowest of distinct values
		var valscores = Reverse( Sort( Distinct( PctScores[l] ) ) )
		
		// Set the html text
		var html = "<b>"+l+" (" + Count(vals) + "):</b>"+nl
		
		/* Set the empty paragraph html array
		Loop through the array of score values and
		reassign the index as the value */
		var pHTML = []
		for( var i in valscores ){
			i = valscores[i]
			// Loop through the array of score values and reassign the index as the value
			for( var v in vals ){ 
				v = vals[v]
				/* If the score matches the value
				update the paragraph html array with the text
				erase the value from the array
				break the loop */
				if( v.score == i ){ 
					var Text = '<p>style="text-indent:15%">' + Proper(v.label) + " (" + ordinal(v.score) + " Percentile and Est Pop: " + v.estPop + ")</p>"
					Push( pHTML, Text )
					Erase(vals,v)
					break
					}
				}
			}
		/* If the count of the array below is greater than 0
		then conatenate the list of values and insert a new line */
		if( Count( pHTML ) > 0 ){ 
			pHTML = Concatenate(pHTML,nl)
			Push(HTMLText, html+pHTML)
			}
		}
	else{ Console('There were no risks in this catergory') }
	}
/* Return the concatenated html if the count of the html list
is greater than 0, otherwise return the default text */
iif( Count(HTMLText) > 0, Concatenate(HTMLText,nl), "No conditions at Medium or above" )