from_template does not work with the output of to_template
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
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 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
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.
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:
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.
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 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.
Awesome, thanks for taking a look into that json issue, and in the meantime I did confirm I can use that workaround successfully!