Select to view content in your preferred language

Update Web Map Layer symbology

4688
8
Jump to solution
07-27-2021 01:30 PM
GPGeoespacialIDSM
Occasional Contributor

I have a map which has a layer symbolized in proportional circles based on the value of a certain field. This layer is a Feature Layer (hosted, view) that is the result of a join with a table that is also on the map. I have a script in python that updates the table that makes the join so that the data from this symbolized layer is also updated. So far, so good.

The problem is: whenever the data is updated, the symbology does not update automatically. The upper bound of the highest value class falls below the current highest value, causing this point not to be symbolized. See the print below to better understand, observing the values in the map and in the symbology classes. As you can see one point was updated to the value 195,505 which is out of the range of the actual symbology (which in turn doesn't update automatically):

 

print1.png

 

What I manually do in AGOL is decrease or increase the number of classes and then get back to 10 classes. Thus the classes' limits are updated I save the map and I'm good to go. How can I do it via python API?

The entire process is automatic via Python API and I would like to also update the symbology in my python script. As I understand from Methods for Updating Layer Symbology with the ArcGIS API for Python . I'd have to modify the web map JSON file and then update it. But am I supposed to "manually" calculate the limits of each class?

 

Ideas?

 

How can I

2 Solutions

Accepted Solutions
GPGeoespacialIDSM
Occasional Contributor

Ha!! Finally! I managed to update my map!! It worked. Here is the solution:

 

from arcgis import GIS
from arcgis.features import FeatureLayer
import json
# the gis module connection
gis = GIS("https://myorg.maps.arcgis.com", "user", "pass")

# the url of the feature of the webmap which symbology will be updated
url = "https://path/to/arcgis/rest/service/featureservice/to/update/inwebmap/FeatureServer/0"

# load as a feature layer
fl = FeatureLayer(url, gis)

# create a definition with symbology specifications
definition = {
    "type": "classBreaksDef",
    "classificationField": "casosAcumulado",
    "classificationMethod": "esriClassifyNaturalBreaks",
    "breakCount": 10
}

# renderer object. It contains new values for the "classMaxValue" key,
# which controls the symbology classes based on Jenks Natural Breaks
renderer = fl.generate_renderer(definition)


# load web map
wm = gis.content.search('title: web-map-title', itemtype= 'Web Map')[0]

# load the complete json definition of the web map
data = wm.get_data()

# iterate through 'classBreaksInfo' (each symbology class) and
# replace the 'classMaxValue' with new values. ["operationalLayers"][3] is 
# the index of the target layer within the web map (replace with your own)
for indice, item in enumerate(data['operationalLayers'][3]['layerDefinition']['drawingInfo']['renderer']['classBreakInfos']):
    item['classMaxValue'] = renderer['classBreakInfos'][indice]['classMaxValue']

# parse it a format that can be uploaded to your webmap
item_properties = {"text": json.dumps(data)}

# 'Commit' the updates to the Item (web map)
wm.update(item_properties=item_properties)

# be proud of yourself

 

Thanks, @emedina for your contribution. It helped a lot!

View solution in original post

emedina
Regular Contributor

Ah, great news! Glad to help! I was actually about to write back when I saw you posted. Here's an alternate solution using one of the other update methods. In this scenario, the update is done on the Feature Layer itself and is saved to its definition. This is the same process that would occur if you updated the symbology in the Visualization tab of the Item Details and saved the layer. The difference between this approach and the one you outlined is the change is global and not limited to the Web Map. I don't know what your needs are, but if you are using this layer in many Web Maps and need its appearance to be consistent this is the way to go:

 

 

from arcgis import GIS
from arcgis.features import FeatureLayer

gis = GIS("https://portal.com/portal", "username", "password")
url = "https://your/layer/url/FeatureServer/0"
fl = FeatureLayer(url, gis)

class_definition = {
  "type": "classBreaksDef",
  "classificationField": "<FIELD_YOU_WANT_TO_CLASSIFY_BY>",
  "classificationMethod": "esriClassifyNaturalBreaks",
  "breakCount": 4,
  "baseSymbol":
  {
    "size": 6,
    "type": "esriSMS",
    "style": "esriSMSCircle",
    "width": 2
  },
  "colorRamp":
  {
    "type": "algorithmic",
    "fromColor": [115,76,0,255],
    "toColor": [255,25,86,255],
    "algorithm": "esriHSVAlgorithm"
  }
}

renderer = fl.generate_renderer(class_definition)
update = {"drawingInfo": {"renderer": renderer}}
fl.manager.update_definition(update)

 

 

For figuring out the symbology options, see this documentation: https://developers.arcgis.com/documentation/common-data-types/symbol-objects.htm

Again, it is also helpful to make the changes manually in the Visualization tab and simply capturing the network traffic to learn how the REST API works. 

Hope this helps you and anyone else out there!

 

 

View solution in original post

8 Replies
emedina
Regular Contributor

Hi,

 

Begin by creating a renderer and use the result to update your symbology according to my article:

from arcgis import GIS
from arcgis.features import FeatureLayer

gis = GIS("https://portal.com/portal", "username", "password")
url = "https://your/layer/url/FeatureServer/0"
fl = FeatureLayer(url, gis)

definition = {
    "type": "classBreaksDef",
    "classificationField": "FIELDTOCLASSIFYBY",
    "classificationMethod": "esriClassifyNaturalBreaks",
    "breakCount": 5
}
renderer = fl.generate_renderer(definition)

 

See this documentation for more info on options: https://developers.arcgis.com/documentation/common-data-types/classification-objects.htm

Additionally, you can capture network traffic while adjusting your symbology in a browser to see what options to use - you'll find those in a "generateRenderer" call.

Hope this helps

GPGeoespacialIDSM
Occasional Contributor

Great. The renderer variable we just created has proper class sizes, according to updated data. Awesome. Now I'm not understanding how to actually update my web map symbology.I can get my actual definitions using:

 

 

 

from arcgis import GIS
from arcgis.features import FeatureLayer

gis = GIS("https://myorg.maps.arcgis.com", "user", "pass")

# web map
wm = gis.content.search('title: mymap', item_type= 'Web Map')[0]

#get definitions
data = wm.get_data()
# the renderer portion of it
original_renderer = data['operationalLayers'][3]['layerDefinition']['drawingInfo']['renderer']

 

 

 

But I dont understand how to update the web map layer using the approach of your article. Let's dive into what I understand and what I'm facing I'll comment your script below and really appreciate your potential help:

 

 

from arcgis import GIS
import json, sys

#judging by the parameter name you defined ("layer_name"), 
#it seems it is supposed to search for the operational layer??

def search_item(conn,layer_name):
    
    #but it is searching for a web map item... so, don't fully understand that
    search_results = conn.content.search(layer_name, item_type='Web Map')
    
    # this piece results in an empty list no matter if I enter the layer name 
    # or the web map name as "layer_name"
    proper_index = [i for i, s in enumerate(search_results) if '"'+layer_name+'"' in str(s)]
    
    # as the above list is empty, no progress on this function from here on. 
    # IndexError: list index out of range
    found_item = search_results[proper_index[0]]
    get_item = conn.content.get(found_item.id)
    return get_item


# this is supposed to update my operational layer within the web map
# using an updated version of a json file
def update_wm_layerdef(item):
    
    # get web map json
    item_data = item.get_data()

    print("*******************ORIGINAL DEFINITION*********************")
    print(json.dumps(item_data, indent=4, sort_keys=True))
    
    # Open JSON file containing symbology update
    # my "renderer" object created in your previous reply is not complete. 
    # It does not contain all info, just pieces of the renderer part. 
    # Is this a problem? Or anything ommited in my "renderer" object 
    # would remain untouched in the webmap? 
    with open('/path/to/webmaplyr.json') as json_data:
        data = json.load(json_data)
    
    # no more comments from here on
    # Set the item_properties to include the desired update
    item_properties = {"text": json.dumps(data)}

    # 'Commit' the updates to the Item
    item.update(item_properties=item_properties)

    # Print item_data to see that changes are reflected
    new_item_data = item.get_data()
    print("***********************NEW DEFINITION**********************")
    print(json.dumps(new_item_data, indent=4, sort_keys=True))


def main():
    conn = GIS("https://machine.domain.com/portal", 
               "admin", "password")
    
    # Search for item, get item data)
    item = search_item(conn, 'wm_lyrsym')
    update_wm_layerdef(item)


if __name__ == '__main__':
    sys.exit(main())

 

 

0 Kudos
emedina
Regular Contributor

Hi,

I'm not really sure what your specific issue is at this point. The article is an aid and I wouldn't expect it to apply to every case. For one, if you know the item id of the thing you want to update you can simply skip the search and get the item with the content manager's get function.

Also, I don't know if you want to be reading/writing JSON to a file - that's a decision you'll have to make. The JSON in this case is so small it probably doesn't matter if you just generate it within the script and use it directly. You need to take a look at the expected JSON (which I mentioned you can get by capturing traffic) to figure out the body of what you send should be.

GPGeoespacialIDSM
Occasional Contributor

It's pretty simple, in fact. The issue is the same as the first post. Then you were really helpful with:


Begin by creating a renderer and use the result to update your symbology according to my article

So I jumped to your article to learn how to update the symbology with the generated "renderer". That was my last reply. Summarizing: how do I update the symbology using the renderer I just made? Since the renderer is quite simple (not that long json), it seems it is not a matter of just item.update(item_properties=my_simple_json). What approach would you suggest to update my web map symbology using the generated renderer?

0 Kudos
GPGeoespacialIDSM
Occasional Contributor

Ha!! Finally! I managed to update my map!! It worked. Here is the solution:

 

from arcgis import GIS
from arcgis.features import FeatureLayer
import json
# the gis module connection
gis = GIS("https://myorg.maps.arcgis.com", "user", "pass")

# the url of the feature of the webmap which symbology will be updated
url = "https://path/to/arcgis/rest/service/featureservice/to/update/inwebmap/FeatureServer/0"

# load as a feature layer
fl = FeatureLayer(url, gis)

# create a definition with symbology specifications
definition = {
    "type": "classBreaksDef",
    "classificationField": "casosAcumulado",
    "classificationMethod": "esriClassifyNaturalBreaks",
    "breakCount": 10
}

# renderer object. It contains new values for the "classMaxValue" key,
# which controls the symbology classes based on Jenks Natural Breaks
renderer = fl.generate_renderer(definition)


# load web map
wm = gis.content.search('title: web-map-title', itemtype= 'Web Map')[0]

# load the complete json definition of the web map
data = wm.get_data()

# iterate through 'classBreaksInfo' (each symbology class) and
# replace the 'classMaxValue' with new values. ["operationalLayers"][3] is 
# the index of the target layer within the web map (replace with your own)
for indice, item in enumerate(data['operationalLayers'][3]['layerDefinition']['drawingInfo']['renderer']['classBreakInfos']):
    item['classMaxValue'] = renderer['classBreakInfos'][indice]['classMaxValue']

# parse it a format that can be uploaded to your webmap
item_properties = {"text": json.dumps(data)}

# 'Commit' the updates to the Item (web map)
wm.update(item_properties=item_properties)

# be proud of yourself

 

Thanks, @emedina for your contribution. It helped a lot!

emedina
Regular Contributor

Ah, great news! Glad to help! I was actually about to write back when I saw you posted. Here's an alternate solution using one of the other update methods. In this scenario, the update is done on the Feature Layer itself and is saved to its definition. This is the same process that would occur if you updated the symbology in the Visualization tab of the Item Details and saved the layer. The difference between this approach and the one you outlined is the change is global and not limited to the Web Map. I don't know what your needs are, but if you are using this layer in many Web Maps and need its appearance to be consistent this is the way to go:

 

 

from arcgis import GIS
from arcgis.features import FeatureLayer

gis = GIS("https://portal.com/portal", "username", "password")
url = "https://your/layer/url/FeatureServer/0"
fl = FeatureLayer(url, gis)

class_definition = {
  "type": "classBreaksDef",
  "classificationField": "<FIELD_YOU_WANT_TO_CLASSIFY_BY>",
  "classificationMethod": "esriClassifyNaturalBreaks",
  "breakCount": 4,
  "baseSymbol":
  {
    "size": 6,
    "type": "esriSMS",
    "style": "esriSMSCircle",
    "width": 2
  },
  "colorRamp":
  {
    "type": "algorithmic",
    "fromColor": [115,76,0,255],
    "toColor": [255,25,86,255],
    "algorithm": "esriHSVAlgorithm"
  }
}

renderer = fl.generate_renderer(class_definition)
update = {"drawingInfo": {"renderer": renderer}}
fl.manager.update_definition(update)

 

 

For figuring out the symbology options, see this documentation: https://developers.arcgis.com/documentation/common-data-types/symbol-objects.htm

Again, it is also helpful to make the changes manually in the Visualization tab and simply capturing the network traffic to learn how the REST API works. 

Hope this helps you and anyone else out there!

 

 

GPGeoespacialIDSM
Occasional Contributor

This scenario where I want a global symbology for a layer to be the same in other web maps might be useful indeed. Thanks a lot for your time looking at this issue.

0 Kudos
AndresCastillo
MVP Regular Contributor

Thank you @emedina for the great information to update the layer's symbology globally, and to observer the browser network logs for the generateRenderer call.

I checked, and seems this is also possible for referenced portal feature layers.

Note for others:

For Map Image Layers (MIL), the visualization tab is not an option due to the difference in the way this portal item works.

Yet, seems there is a way to update the symbology for a MIL, as per @emedina's blog section:

How to Update Portal/ArcGIS Online Item Symbology:

Also, check out the ArcGIS API for Python WebMap object, update_drawing_info method

0 Kudos