arcgis-python-api icon indicating copy to clipboard operation
arcgis-python-api copied to clipboard

from_template does not work with the output of to_template

Open pricealexandra opened this issue 2 months ago • 7 comments

Describe the bug We are trying to apply the styling of one layer to another via the python API. If we are understanding this correctly, we should be able to do that with the from_template functionality, but we have not been able to get it working.

To Reproduce Steps to reproduce the behavior:

renderer_manager_1 = webmap.content.renderer(0)
style_template = renderer_manager.to_template()
renderer_manager_2 = webmap.content.renderer(1)
renderer_manager_2.from_template(style_template)

error:

FileNotFoundError: [Errno 2] No such file or directory: 'simple_renderer_1760132073101.json'

Expected behavior We would expect the output of to_template to work as the input of from_template, or for the documentation to describe any necessary intermediate steps.

Platform (please complete the following information):

  • OS: mac sequoia
  • Browser: chrome
  • Python API Version: 2.4.1.3

Additional context This is in a Jupyter notebook, in case that matters

pricealexandra avatar Oct 10 '25 21:10 pricealexandra

Hello! I’d like to contribute to this issue. It looks like a good learning opportunity. Could you please assign me so I can start working on it?

JanaKocakova avatar Oct 10 '25 21:10 JanaKocakova

@JanaKocakova If you have any ideas you are welcome to contribute.

@pricealexandra It seems the file path cannot be found. The to_template saves as a resource to the webmap so you can always try to find the file using the `item.resources.get("<file_name>"). We will work on enhancing the example for this.

cc @ManushiM

nanaeaubry avatar Oct 13 '25 06:10 nanaeaubry

got it, thanks for the suggestion! I got a bit further with the following code:

renderer_manager_1 = webmap.content.renderer(0)
style_template_filename = renderer_manager_1.to_template()
style_template = web_map_item.resources.get(style_template_filename, try_json=False)
renderer_manager_2 = webmap.content.renderer(1)
renderer_manager_2.from_template(style_template)

I wasn't sure about using try_json in the .get call -- is from_template expecting a file path to a json file or is it expecting a string in json format?

With try_json as False the error I get is:

JSONDecodeError                           Traceback (most recent call last)
Cell In[12], line 6
      4 print(dir(style_template))
      5 renderer_manager_2 = webmap.content.renderer(1)
----> 6 renderer_manager_2.from_template(style_template)
      7 # webmap.update()
      8 # web_map_item

File ~/.pyenv/versions/3.12.6/envs/esri-map-spike/lib/python3.12/site-packages/arcgis/map/renderers.py:315, in RendererManager.from_template(self, template)
    312 if template.endswith(".json"):
    313     # Read the file
    314     with open(template, "r") as f:
--> 315         data = json.load(f)
    317     # Pass the renderer into a dataclass depending on type
    318     if data["type"] == "simple":

File ~/.pyenv/versions/3.12.6/lib/python3.12/json/__init__.py:293, in load(fp, cls, object_hook, parse_float, parse_int, parse_constant, object_pairs_hook, **kw)
    274 def load(fp, *, cls=None, object_hook=None, parse_float=None,
    275         parse_int=None, parse_constant=None, object_pairs_hook=None, **kw):
    276     """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing
    277     a JSON document) to a Python object.
    278 
   (...)    291     kwarg; otherwise ``JSONDecoder`` is used.
    292     """
--> 293     return loads(fp.read(),
    294         cls=cls, object_hook=object_hook,
    295         parse_float=parse_float, parse_int=parse_int,
    296         parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, **kw)

File ~/.pyenv/versions/3.12.6/lib/python3.12/json/__init__.py:346, in loads(s, cls, object_hook, parse_float, parse_int, parse_constant, object_pairs_hook, **kw)
    341     s = s.decode(detect_encoding(s), 'surrogatepass')
    343 if (cls is None and object_hook is None and
    344         parse_int is None and parse_float is None and
    345         parse_constant is None and object_pairs_hook is None and not kw):
--> 346     return _default_decoder.decode(s)
    347 if cls is None:
    348     cls = JSONDecoder

File ~/.pyenv/versions/3.12.6/lib/python3.12/json/decoder.py:340, in JSONDecoder.decode(self, s, _w)
    338 end = _w(s, end).end()
    339 if end != len(s):
--> 340     raise JSONDecodeError("Extra data", s, end)
    341 return obj

JSONDecodeError: Extra data: line 1 column 7 (char 6)

With try_json as True, I get a different error:

ValueError                                Traceback (most recent call last)
Cell In[13], line 6
      4 print(dir(style_template))
      5 renderer_manager_2 = webmap.content.renderer(1)
----> 6 renderer_manager_2.from_template(style_template)
      7 # webmap.update()
      8 # web_map_item

File ~/.pyenv/versions/3.12.6/envs/esri-map-spike/lib/python3.12/site-packages/arcgis/map/renderers.py:339, in RendererManager.from_template(self, template)
    337         raise ValueError("The renderer type provided is not supported.")
    338 else:
--> 339     raise ValueError("The template must be a json file.")
    340 return self.renderer

ValueError: The template must be a json file.

pricealexandra avatar Oct 14 '25 23:10 pricealexandra

So from our tests here is an example we have that works:

Here is a small pseudo code snippet:

renderer_path = "C:\\<your_path>\\renderers.json"
m = Map(item=<existing_item>)

renderer_manager_first_layer = m.content.renderer(0)
renderer_manager_first_layer.from_template(renderer_path)

Here is the longer version:

renderer_path = "C:\\<your_path>\\renderers.json"
fl = FeatureLayer(
    "https://servicesdev.arcgis.com/01ClFLufh9nZafWR/arcgis/rest/services/tblfc_gdb/FeatureServer/0",
    gis=self.gis,
)
df = fl.query(where="POP < 250000").sdf

m = Map()

m.content.add(df)

renderer_manager = m.content.renderer(0)
assert renderer_manager

# Read the renderer from the file
updated = renderer_manager.from_template(renderer_path)
assert updated
assert updated.type == "classBreaks"

here is the json file in question to give you an idea of what we are reading in:

renderers.json


Another way to update the renderer if you would like is to use the dataclass for the renderer.

m1 = Map(item=<existing_item>)
rend_manager_m1 = m1.content.renderer(0)

class_breaks_rend = rend_manager_m1.renderer

m2 = Map(item=<existing_item>)
rend_manager_m2 = m2.content.renderer(<idx>)
rend_manager_m2.renderer = class_breaks_rend

This is all very new workflow so if you have any enhancements you feel would benefit this please feel free to let us know. We will work on the to_template and from_template methods to make them more flexible as well.

nanaeaubry avatar Oct 15 '25 12:10 nanaeaubry

Thanks so much for your patience working this out with me -- I'm a ESRI newbie so I'm sure some of these questions reflect that. I went to compare the renderer file you linked in the example above with the one I have -- I just used something like the following:

renderer_manager_1 = webmap.content.renderer(0)
style_template_filename = renderer_manager_1.to_template()
style_template = web_map_item.resources.get(style_template_filename, try_json=True)
with open("output.json", "w") as f:
    json.dump(style_template, f)

When I look at output.json, I only see " 6.99 \u2013 13.3\", \"symbol\": {\"color\": [185, 255, 110, 255], \"style\": \"esriSLSSolid\", \"type\": \"esriSLS\", \"width\": 2.25}}], \"field\": \"rmean\", \"type\": \"classBreaks\", \"visualVariables\": []}"

That's obviously not a full/complete json file which would explain why I've been having trouble getting from_template to work with it. Do you think there's an issue with how I'm caling to_template that would cause this?

pricealexandra avatar Oct 16 '25 16:10 pricealexandra

@pricealexandra That's why we are here so no worries! It helps us better understand how these methods are being used and perceived so all you are showing is very helpful.

However, no that is not a normal json and you are calling to_template correctly. This seems to be a bug on our end so I will dig into it and post any updates here.

For now the best workaround would be to use the Renderer Dataclasses directly as in this example:

m1 = Map(item=<existing_item>)
rend_manager_m1 = m1.content.renderer(0)

class_breaks_rend = rend_manager_m1.renderer

m2 = Map(item=<existing_item>)
rend_manager_m2 = m2.content.renderer(<idx>)
rend_manager_m2.renderer = class_breaks_rend

The renderer method returns a dataclass. You can edit this as needed and then assign it to the renderer property of another layer in the same map or on another map as shown above.

nanaeaubry avatar Oct 16 '25 16:10 nanaeaubry

Awesome, thanks for taking a look into that json issue, and in the meantime I did confirm I can use that workaround successfully!

pricealexandra avatar Oct 16 '25 21:10 pricealexandra