I have a feature layer that is using proportional symbols, but I need to symbols to scale in size with the map scale. Visual variables does not work (probably because of the field values), so I need to find another way. I tried setting a new renderer based on the level of detail (seems like the easiest solution), but the symbols do not change, even after redrawing the layer. I first defined 3 uniquevalue renders and set the layer initially to the renderer with the smaller symbols:
var uvrJsonSmall = {
"type": "uniqueValue",
"field1": "Density",
"legendOptions": {
"showLegend": true,
//"title": "Site Density",
"customValues": [1,2,3]
},
"defaultSymbol": propSym,
"uniqueValueInfos": [{
"value": "Sparse",
"symbol": {
"color": [0, 153, 0, 150],
"size": 4,
"outline": {
"color": [0, 51, 0, 200],
"width": 1,
"type": "esriSLS",
"style": "esriSLSSolid"
},
"type": "esriSMS",
"style": "esriSMSCircle"
}
}, {
"value": "Moderate",
"symbol": {
"color": [0, 153, 0, 150],
"size": 7,
"outline": {
"color": [0, 51, 0, 200],
"width": 1,
"type": "esriSLS",
"style": "esriSLSSolid"
},
"type": "esriSMS",
"style": "esriSMSCircle"
}
}, {
"value": "Dense",
"symbol": {
"color": [0, 153, 0, 150],
"size": 10,
"outline": {
"color": [0, 51, 0, 200],
"width": 1,
"type": "esriSLS",
"style": "esriSLSSolid"
},
"type": "esriSMS",
"style": "esriSMSCircle"
}
}]
}
var uvrJsonMedium = {
"type": "uniqueValue",
"field1": "Density",
"legendOptions": {
"showLegend": true,
//"title": "Site Density",
"customValues": [1,2,3]
},
"defaultSymbol": propSym,
"uniqueValueInfos": [{
"value": "Sparse",
"symbol": {
"color": [0, 153, 0, 150],
"size": 10,
"outline": {
"color": [0, 51, 0, 200],
"width": 1,
"type": "esriSLS",
"style": "esriSLSSolid"
},
"type": "esriSMS",
"style": "esriSMSCircle"
}
}, {
"value": "Moderate",
"symbol": {
"color": [0, 153, 0, 150],
"size": 16,
"outline": {
"color": [0, 51, 0, 200],
"width": 1,
"type": "esriSLS",
"style": "esriSLSSolid"
},
"type": "esriSMS",
"style": "esriSMSCircle"
}
}, {
"value": "Dense",
"symbol": {
"color": [0, 153, 0, 150],
"size": 24,
"outline": {
"color": [0, 51, 0, 200],
"width": 1,
"type": "esriSLS",
"style": "esriSLSSolid"
},
"type": "esriSMS",
"style": "esriSMSCircle"
}
}]
}
var uvrJsonLarge = {
"type": "uniqueValue",
"field1": "Density",
"legendOptions": {
"showLegend": true,
//"title": "Site Density",
"customValues": [1,2,3]
},
"defaultSymbol": propSym,
"uniqueValueInfos": [{
"value": "Sparse",
"symbol": {
"color": [0, 153, 0, 150],
"size": 14,
"outline": {
"color": [0, 51, 0, 200],
"width": 1,
"type": "esriSLS",
"style": "esriSLSSolid"
},
"type": "esriSMS",
"style": "esriSMSCircle"
}
}, {
"value": "Moderate",
"symbol": {
"color": [0, 153, 0, 150],
"size": 20,
"outline": {
"color": [0, 51, 0, 200],
"width": 1,
"type": "esriSLS",
"style": "esriSLSSolid"
},
"type": "esriSMS",
"style": "esriSMSCircle"
}
}, {
"value": "Dense",
"symbol": {
"color": [0, 153, 0, 150],
"size": 32,
"outline": {
"color": [0, 51, 0, 200],
"width": 1,
"type": "esriSLS",
"style": "esriSLSSolid"
},
"type": "esriSMS",
"style": "esriSMSCircle"
}
}]
}
if (resultLayer.graphics.length != QueryGraphics.length){
for(var i = 1; i < QueryGraphics.length; i++) {
//console.log("query graphic ", QueryGraphics[i]);
resultLayer.add(QueryGraphics[i]);
}
}
propRendSm = new UniqueValueRenderer(uvrJsonSmall);
propRendMed = new UniqueValueRenderer(uvrJsonMedium);
propRendLg = new UniqueValueRenderer(uvrJsonLarge);
resultLayer.setRenderer(propRendSm);
currentRend = 'small';
map.addLayer(resultLayer);
resultLayer.on('load', function() {
console.log("result layer ", resultLayer);
//resultLayer.redraw();
_showSpecDensLegend()
})
Then on 'zoom-end' I can update the renderer based on the zoom level. This works ( the layer renderer is updated), but the symbols don't update in the map.
map.on('zoom-end', lang.hitch(this, function() {
level = map.getLevel();
if(resultLayer) {
console.log("result layer")
if(level < 11 && currentRend != 'small') {
resultLayer.setRenderer(propRendSm);
resultLayer.redraw();
currentRend = 'small';
}
if(level === 11 && currentRend != 'medium') {
console.log("setting medium symbols");
resultLayer.setRenderer(propRendMed);
console.log("result layer renderer ", resultLayer.renderer); //I can see the renderer has been updated
resultLayer.redraw();
currentRend = 'medium';
}
if(level > 11 && currentRend != 'large') {
console.log("setting large symbols");
resultLayer.setRenderer(propRendLg);
resultLayer.redraw();
currentRend = 'large';
}
}
}
Redrawing the layer doesn't seem to be doing anything. Is there something I am missing, or is there a better way of scaling the symbols?
Solved! Go to Solution.
let sizesAndScales = `
var sizes = {
small: {
Sparse: 2,
Moderate: 5,
Dense: 8
},
medium: {
Sparse: 8,
Moderate: 14,
Dense: 20
},
large: {
Sparse: 18,
Moderate: 24,
Dense: 32
}
}
var scale = When(
$view.scale > 1155581, "small",
$view.scale <= 1155581 && $view.scale > 288895, "medium",
$view.scale <= 288895, "large",
"no scale"
);
if($feature["Density"] != "Sparse" && $feature["Density"] != "Moderate" && $feature["Density"] != "Dense"){
return null;
}
return sizes[scale][$feature["Density"]];
`
let uvRend = new UniqueValueRenderer ({
type: "uniqueValue",
field1: "Density",
//defaultSymbol: propSym,
uniqueValueInfos: [
{
value: "Sparse",
symbol: {
type: "esriSMS",
style: "esriSMSCircle",
color: [0, 153, 0, 150],
outline: {
width: 0.5,
color: [0, 51, 0, 200],
type: "esriSLS",
style: "esriSLSSolid",
},
size: 6
}
},
{
value: "Moderate",
symbol: {
type: "esriSMS",
style: "esriSMSCircle",
color: [0, 153, 0, 150],
outline: {
width: 0.5,
color: [0, 51, 0, 200],
type: "esriSLS",
style: "esriSLSSolid",
},
size: 10
}
},
{
value: "Dense",
symbol: {
type: "esriSMS",
style: "esriSMSCircle",
color: [0, 153, 0, 150],
outline: {
width: 0.5,
color: [0, 51, 0, 200],
type: "esriSLS",
style: "esriSLSSolid",
},
size: 14
}
}
]
});
let arcade = dom.byId("arcade-symbols").text;
console.log("arcade expression ", arcade);
let visualParams = {
type: "sizeInfo",
field: "Density",
valueExpression: sizesAndScales,
minDataValue: 1,
minSize: 1,
maxDataValue: 32,
maxSize: 32,
legendOptions: {
showLegend: true
}
}
resultLayer = new FeatureLayer(featureCollection, {
id: "Species Results",
opacity: 1,
outFields: ["*"]
});
uvRend.setVisualVariables([visualParams]);
resultLayer.renderer = uvRend;
map.addLayer(resultLayer);
Kristian,
Yes, I substituted the correct syntax for 3x, but I am getting an undefined error for the arcade global variable $feature. My code is not in the same index file, but is in a .js file. I tried creating a size function from within the .js file and also from within the main index (as in your example) and then referenced it. When I did this I didn't get an error, but the symbols were the same size so I don't think I am getting the actual values I need for the size property. Here is what I have so far:
I am looking at the arcade docs now for js3x. I know I am close, just need to figure out how to get the $feature variable to be defined.
Hey Franklin,
You actually can use visual variables to your advantage here, just in a roundabout way. Here's a test app that demos this: https://codepen.io/kekenes/pen/GRWZpXY?editors=1000
Basically, you need to use Arcade to manage the sizes and scales for each unique value. This expression will return the desired feature size depending on the value and the scale level. In essence, it treats the desired size as if it were a data value. Then you need to set the minDataValue/minSize and maxDataValue/maxSize to matching limits. I set them to the overall min and max sizes across all scales, though you could theoretically do something like 1 and 100 and it would work the same.
layer.renderer = {
type: "unique-value",
field: "DOM_CROP_ACRES",
uniqueValueInfos: [
{
value: "Corn",
symbol: {
type: "simple-marker",
color: "rgb(237, 81, 81)",
outline: {
width: 0.5,
color: "rgba(0,0,0,0.5)"
},
size: 4
}
},
{
value: "Wheat",
symbol: {
type: "simple-marker",
color: "rgb(20, 158, 206)",
outline: {
width: 0.5,
color: "rgba(0,0,0,0.5)"
},
size: 7
}
},
{
value: "Soybeans",
symbol: {
type: "simple-marker",
color: "rgb(167, 198, 54)",
outline: {
width: 0.5,
color: "rgba(0,0,0,0.5)"
},
size: 10
}
}
],
visualVariables: [
{
type: "size",
valueExpression: `
// sizes for each value
// at generic scale levels
var sizesAndScales = {
small: {
Corn: 2,
Wheat: 5,
Soybeans: 8
},
medium: {
Corn: 8,
Wheat: 14,
Soybeans: 20
},
large: {
Corn: 18,
Wheat: 24,
Soybeans: 32
}
}
// match scale levels with actual
// scale values
var scale = When(
$view.scale > 9244650, "small",
$view.scale <= 9244650 && $view.scale > 2311162, "medium",
$view.scale <= 2311162, "large",
"no scale"
);
// I only need to make this check because there
// are more values than three example values I wanted
// to use for the demo
if($feature["DOM_CROP_ACRES"] != "Corn" && $feature["DOM_CROP_ACRES"] != "Wheat" && $feature["DOM_CROP_ACRES"] != "Soybeans"){
return null;
}
// return desired size based on value and scale level
return sizesAndScales[scale][$feature["DOM_CROP_ACRES"]];
`,
// must be equal to or smaller
// than the smallest size in the expression
minSize: 1,
minDataValue: 1,
// must be equal to or larger
// than the largest size in the expression
maxSize: 32,
maxDataValue: 32,
// legend not needed for this VV
legendOptions: {
showLegend: false
}
}
]
};
Let me know if this doesn't solve your issue.
Kristian
Thanks for the suggestion, but I don't think this will work in my case. 1st, I am using javascript 3x and I noticed that you are using 4x in your example. Having said that, I did try it and it doesn't work. I don't get an error, but the layer won't set renderer as configured, I just get a generic symbol. Also there might be an issue with the field I am trying to base the proportional symbols on. The field only has three values: 1, 2, and 3. I think this might be causing an issue with the 'minDataValue' and 'maxDataValue' property since they are looking for values < or > only. If I set the values to 1 and 3, then this won't work, but that is my only option. I will keep working with it, I think it's still better than what I was trying to do, so maybe I can find a way to use your example and make it work with 3x.
I do have a question. In your example, you have the 'minDataValue' and 'maxDataValue' properties set to 1 and 32 respectively, but the field you are basing your symbols on is obviously a text field. What field is the min and max looking at? These properties are a little confusing to me.
Franklin,
This should work in your scenario...
> 1st, I am using javascript 3x and I noticed that you are using 4x in your example
This method will work regardless if the app is in 3x or 4x. Both APIs support Arcade and size visual variables. The syntax for setting a visual variable is slightly different in 3x than it is in 4x. For example, type is "sizeInfo" in 3x, and "size" in 4x. You may also have to use renderer.setVisualVariables() instead of setting a property directly.
>The field only has three values: 1, 2, and 3.
The field type doesn't matter in this case. As I stated in the first reply, the Arcade expression in the size variable need to return a size value, not a data value for this scenario to work. That's why the minDataValue and maxDataValue properties are set to 1-32...they are representing sizes, not data values.
I used a text field in my example because your code snippet uses text values... "sparse", "moderate", "dense"... It will work in 3x. I'll attempt it in that API.
Here it is working in a 3x app: https://codepen.io/kekenes/pen/VwpKKmQ?editors=1000
To clarify all my earlier statements. In order for this to work for a text field (or any field), you need to return a size value in the Arcade expression. This makes for an awkward sizeInfo configuration because the minDataValue/maxDataValue and minSize/maxSize pairs always need to be equal. It's an unorthodox way for handling this scenario, but works...
let sizesAndScales = `
var sizes = {
small: {
Sparse: 2,
Moderate: 5,
Dense: 8
},
medium: {
Sparse: 8,
Moderate: 14,
Dense: 20
},
large: {
Sparse: 18,
Moderate: 24,
Dense: 32
}
}
var scale = When(
$view.scale > 1155581, "small",
$view.scale <= 1155581 && $view.scale > 288895, "medium",
$view.scale <= 288895, "large",
"no scale"
);
if($feature["Density"] != "Sparse" && $feature["Density"] != "Moderate" && $feature["Density"] != "Dense"){
return null;
}
return sizes[scale][$feature["Density"]];
`
let uvRend = new UniqueValueRenderer ({
type: "uniqueValue",
field1: "Density",
//defaultSymbol: propSym,
uniqueValueInfos: [
{
value: "Sparse",
symbol: {
type: "esriSMS",
style: "esriSMSCircle",
color: [0, 153, 0, 150],
outline: {
width: 0.5,
color: [0, 51, 0, 200],
type: "esriSLS",
style: "esriSLSSolid",
},
size: 6
}
},
{
value: "Moderate",
symbol: {
type: "esriSMS",
style: "esriSMSCircle",
color: [0, 153, 0, 150],
outline: {
width: 0.5,
color: [0, 51, 0, 200],
type: "esriSLS",
style: "esriSLSSolid",
},
size: 10
}
},
{
value: "Dense",
symbol: {
type: "esriSMS",
style: "esriSMSCircle",
color: [0, 153, 0, 150],
outline: {
width: 0.5,
color: [0, 51, 0, 200],
type: "esriSLS",
style: "esriSLSSolid",
},
size: 14
}
}
]
});
let arcade = dom.byId("arcade-symbols").text;
console.log("arcade expression ", arcade);
let visualParams = {
type: "sizeInfo",
field: "Density",
valueExpression: sizesAndScales,
minDataValue: 1,
minSize: 1,
maxDataValue: 32,
maxSize: 32,
legendOptions: {
showLegend: true
}
}
resultLayer = new FeatureLayer(featureCollection, {
id: "Species Results",
opacity: 1,
outFields: ["*"]
});
uvRend.setVisualVariables([visualParams]);
resultLayer.renderer = uvRend;
map.addLayer(resultLayer);
Kristian,
Yes, I substituted the correct syntax for 3x, but I am getting an undefined error for the arcade global variable $feature. My code is not in the same index file, but is in a .js file. I tried creating a size function from within the .js file and also from within the main index (as in your example) and then referenced it. When I did this I didn't get an error, but the symbols were the same size so I don't think I am getting the actual values I need for the size property. Here is what I have so far:
I am looking at the arcade docs now for js3x. I know I am close, just need to figure out how to get the $feature variable to be defined.
Check out the sizeVvisualVariables parameters in Kristian's CodePen... it uses the parameter 'valueExpression', not 'expression'. Also not sure what your expression is as we can't see above the arcade code in the dom element called 'arcade-symbols'.
It looks like you're using a JS function instead of Arcade. If you reference Arcade in a separate script tag, then make sure to set the type to "plain/text" or something else that tells the browser it's not JavaScript "esri/arcade" would even work. `$feature` has no meaning in JavaScript. That's why you're getting an undefined message.
Within the context of JavaScript, the Arcade expression needs to be a string value much like you see it in my first example. I've just updated this codepen to make this more clear: https://codepen.io/kekenes/pen/VwpKKmQ?editors=1000
Ok, thanks to both of you I finally got it to work. Esri docs can be very confusing and sometimes just plain wrong. I was going by the 'visualVariable' example for the js3x 'uniqueValueRender' doc here: https://developers.arcgis.com/javascript/3/jsapi/uniquevaluerenderer-amd.html which used 'expression' as the parameter. Once I changed it to 'valueExpression' and made a few other tweaks it works. It took me a minute to understand exactly how it is working (still don't really get the minDataValue and maxDataValue params, but I was able to put all of the code in the .js file, make sure to textualize the arcade and now I have 'scaled' proportional symbols. I will edit the above code block with the working solution.
The minDataValue/maxDataValue weirdness is because your scenario is a bit unorthodox (to scale symbol sizes based on string values). The more typical case looks like the following:
var sizeVisVar = {
type: "sizeInfo",
field: "EDUCBASECY", //total adult population 25 years and older
minDataValue: 10000, //counties with fewer than 10,000 adults will be assigned the min size
maxDataValue: 1000000, //counties with more than 1,000,000 adults will be assigned the max size
valueUnit: "unknown",
minSize: {
type: "sizeInfo",
valueExpression: "$view.scale",
//The min size of the symbol varies depending on the map scale
stops: [
{ value: 250000000, size: 6 },
{ value: 6000000, size: 6 }, //at a 1:6,000,000 scale, the min size is 6px
{ value: 1000000, size: 4 },
{ value: 200000, size: 2 },
{ value: 1000, size: 2 }
]
},
maxSize: {
type: "sizeInfo",
valueExpression: "$view.scale",
//The max size of the symbol varies depending on the map scale
stops: [
{ value: 250000000, size: 50 },
{ value: 6000000, size: 50 }, //at a 1:6,000,000 scale, the max size is 50px
{ value: 1000000, size: 40 },
{ value: 200000, size: 20 },
{ value: 1000, size: 10 }
]
}
};
Hopefully this snippet makes more sense. In this case, the Arcade expression only returns "$view.scale". Then the minSize/maxSize variables manage which sizes to assign to features at certain scales.
Because your scenario involves string values, you have to manage all of that yourself inside an Arcade expression. So rather than return a data value (like 10,000 people) you need to return a size (like 32px). Then you trick the size variable into assuming it's a data value, by setting minSize = 1 and minDataValue = 1; and maxSize = 32 and maxDataValue = 32.