From 573f953e1b721e14105383c9e8c856fd6f942036 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 14 Oct 2019 18:00:20 -0400 Subject: [PATCH] Add "overwrite" kwarg to all update* figure methods. (#1726) When True, update operations will overwrite the prior version of all properties. When False (the default), updates are applied recursively and prior properties are retained if not updated. --- packages/python/plotly/codegen/figure.py | 7 +- .../python/plotly/plotly/basedatatypes.py | 58 ++++++++++--- .../plotly/plotly/graph_objs/_figure.py | 85 +++++++++++++++---- .../plotly/plotly/graph_objs/_figurewidget.py | 85 +++++++++++++++---- .../test_core/test_graph_objs/test_figure.py | 33 +++++++ .../test_core/test_graph_objs/test_update.py | 48 +++++++++++ .../test_update_objects/test_update_layout.py | 43 ++++++++++ .../test_update_subplots.py | 9 ++ .../test_update_objects/test_update_traces.py | 15 ++++ 9 files changed, 336 insertions(+), 47 deletions(-) diff --git a/packages/python/plotly/codegen/figure.py b/packages/python/plotly/codegen/figure.py index ab1e1c9b74..d18f790bed 100644 --- a/packages/python/plotly/codegen/figure.py +++ b/packages/python/plotly/codegen/figure.py @@ -313,6 +313,7 @@ def update_{plural_name}( self, patch=None, selector=None, + overwrite=False, row=None, col=None{secondary_y_1}, **kwargs): \"\"\" @@ -330,6 +331,10 @@ def update_{plural_name}( properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all {singular_name} objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of {singular_name} objects to select. To select {singular_name} objects by row and column, the Figure @@ -348,7 +353,7 @@ def update_{plural_name}( \"\"\" for obj in self.select_{plural_name}( selector=selector, row=row, col=col{secondary_y_2}): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self""" ) diff --git a/packages/python/plotly/plotly/basedatatypes.py b/packages/python/plotly/plotly/basedatatypes.py index a423ae1197..bf2f9d3ba8 100644 --- a/packages/python/plotly/plotly/basedatatypes.py +++ b/packages/python/plotly/plotly/basedatatypes.py @@ -438,7 +438,7 @@ def _ipython_display_(self): else: print (repr(self)) - def update(self, dict1=None, **kwargs): + def update(self, dict1=None, overwrite=False, **kwargs): """ Update the properties of the figure with a dict and/or with keyword arguments. @@ -450,6 +450,10 @@ def update(self, dict1=None, **kwargs): ---------- dict1 : dict Dictionary of properties to be updated + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. kwargs : Keyword/value pair of properties to be updated @@ -484,10 +488,11 @@ def update(self, dict1=None, **kwargs): if d: for k, v in d.items(): update_target = self[k] - if update_target == (): - # existing data or frames property is empty - # In this case we accept the v as is. + if update_target == () or overwrite: if k == "data": + # Overwrite all traces as special due to + # restrictions on trace assignment + self.data = () self.add_traces(v) else: # Accept v @@ -843,7 +848,14 @@ def for_each_trace(self, fn, selector=None, row=None, col=None, secondary_y=None return self def update_traces( - self, patch=None, selector=None, row=None, col=None, secondary_y=None, **kwargs + self, + patch=None, + selector=None, + row=None, + col=None, + secondary_y=None, + overwrite=False, + **kwargs ): """ Perform a property update operation on all traces that satisfy the @@ -877,6 +889,10 @@ def update_traces( created using plotly.subplots.make_subplots. See the docstring for the specs argument to make_subplots for more info on creating subplots with secondary y-axes. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. **kwargs Additional property updates to apply to each selected trace. If a property is specified in both patch and in **kwargs then the @@ -890,10 +906,10 @@ def update_traces( for trace in self.select_traces( selector=selector, row=row, col=col, secondary_y=secondary_y ): - trace.update(patch, **kwargs) + trace.update(patch, overwrite=overwrite, **kwargs) return self - def update_layout(self, dict1=None, **kwargs): + def update_layout(self, dict1=None, overwrite=False, **kwargs): """ Update the properties of the figure's layout with a dict and/or with keyword arguments. @@ -905,6 +921,10 @@ def update_layout(self, dict1=None, **kwargs): ---------- dict1 : dict Dictionary of properties to be updated + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. kwargs : Keyword/value pair of properties to be updated @@ -913,7 +933,7 @@ def update_layout(self, dict1=None, **kwargs): BaseFigure The Figure object that the update_layout method was called on """ - self.layout.update(dict1, **kwargs) + self.layout.update(dict1, overwrite=overwrite, **kwargs) return self def _select_layout_subplots_by_prefix( @@ -2697,7 +2717,7 @@ def _is_dict_list(v): return isinstance(v, list) and len(v) > 0 and isinstance(v[0], dict) @staticmethod - def _perform_update(plotly_obj, update_obj): + def _perform_update(plotly_obj, update_obj, overwrite=False): """ Helper to support the update() methods on :class:`BaseFigure` and :class:`BasePlotlyType` @@ -2747,6 +2767,12 @@ def _perform_update(plotly_obj, update_obj): # ------------------------ for key in update_obj: val = update_obj[key] + + if overwrite: + # Don't recurse and assign property as-is + plotly_obj[key] = val + continue + validator = plotly_obj._get_prop_validator(key) if isinstance(validator, CompoundValidator) and isinstance(val, dict): @@ -3530,7 +3556,7 @@ def _raise_on_invalid_property_error(self, *args): ) ) - def update(self, dict1=None, **kwargs): + def update(self, dict1=None, overwrite=False, **kwargs): """ Update the properties of an object with a dict and/or with keyword arguments. @@ -3542,6 +3568,10 @@ def update(self, dict1=None, **kwargs): ---------- dict1 : dict Dictionary of properties to be updated + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. kwargs : Keyword/value pair of properties to be updated @@ -3552,11 +3582,11 @@ def update(self, dict1=None, **kwargs): """ if self.figure: with self.figure.batch_update(): - BaseFigure._perform_update(self, dict1) - BaseFigure._perform_update(self, kwargs) + BaseFigure._perform_update(self, dict1, overwrite=overwrite) + BaseFigure._perform_update(self, kwargs, overwrite=overwrite) else: - BaseFigure._perform_update(self, dict1) - BaseFigure._perform_update(self, kwargs) + BaseFigure._perform_update(self, dict1, overwrite=overwrite) + BaseFigure._perform_update(self, kwargs, overwrite=overwrite) return self diff --git a/packages/python/plotly/plotly/graph_objs/_figure.py b/packages/python/plotly/plotly/graph_objs/_figure.py index a3890db018..d62ac17caf 100644 --- a/packages/python/plotly/plotly/graph_objs/_figure.py +++ b/packages/python/plotly/plotly/graph_objs/_figure.py @@ -15081,7 +15081,9 @@ def for_each_coloraxis(self, fn, selector=None, row=None, col=None): return self - def update_coloraxes(self, patch=None, selector=None, row=None, col=None, **kwargs): + def update_coloraxes( + self, patch=None, selector=None, overwrite=False, row=None, col=None, **kwargs + ): """ Perform a property update operation on all coloraxis objects that satisfy the specified selection criteria @@ -15097,6 +15099,10 @@ def update_coloraxes(self, patch=None, selector=None, row=None, col=None, **kwar properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all coloraxis objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of coloraxis objects to select. To select coloraxis objects by row and column, the Figure @@ -15113,7 +15119,7 @@ def update_coloraxes(self, patch=None, selector=None, row=None, col=None, **kwar Returns the Figure object that the method was called on """ for obj in self.select_coloraxes(selector=selector, row=row, col=col): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self @@ -15175,7 +15181,9 @@ def for_each_geo(self, fn, selector=None, row=None, col=None): return self - def update_geos(self, patch=None, selector=None, row=None, col=None, **kwargs): + def update_geos( + self, patch=None, selector=None, overwrite=False, row=None, col=None, **kwargs + ): """ Perform a property update operation on all geo objects that satisfy the specified selection criteria @@ -15191,6 +15199,10 @@ def update_geos(self, patch=None, selector=None, row=None, col=None, **kwargs): properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all geo objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of geo objects to select. To select geo objects by row and column, the Figure @@ -15207,7 +15219,7 @@ def update_geos(self, patch=None, selector=None, row=None, col=None, **kwargs): Returns the Figure object that the method was called on """ for obj in self.select_geos(selector=selector, row=row, col=col): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self @@ -15269,7 +15281,9 @@ def for_each_mapbox(self, fn, selector=None, row=None, col=None): return self - def update_mapboxes(self, patch=None, selector=None, row=None, col=None, **kwargs): + def update_mapboxes( + self, patch=None, selector=None, overwrite=False, row=None, col=None, **kwargs + ): """ Perform a property update operation on all mapbox objects that satisfy the specified selection criteria @@ -15285,6 +15299,10 @@ def update_mapboxes(self, patch=None, selector=None, row=None, col=None, **kwarg properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all mapbox objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of mapbox objects to select. To select mapbox objects by row and column, the Figure @@ -15301,7 +15319,7 @@ def update_mapboxes(self, patch=None, selector=None, row=None, col=None, **kwarg Returns the Figure object that the method was called on """ for obj in self.select_mapboxes(selector=selector, row=row, col=col): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self @@ -15363,7 +15381,9 @@ def for_each_polar(self, fn, selector=None, row=None, col=None): return self - def update_polars(self, patch=None, selector=None, row=None, col=None, **kwargs): + def update_polars( + self, patch=None, selector=None, overwrite=False, row=None, col=None, **kwargs + ): """ Perform a property update operation on all polar objects that satisfy the specified selection criteria @@ -15379,6 +15399,10 @@ def update_polars(self, patch=None, selector=None, row=None, col=None, **kwargs) properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all polar objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of polar objects to select. To select polar objects by row and column, the Figure @@ -15395,7 +15419,7 @@ def update_polars(self, patch=None, selector=None, row=None, col=None, **kwargs) Returns the Figure object that the method was called on """ for obj in self.select_polars(selector=selector, row=row, col=col): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self @@ -15457,7 +15481,9 @@ def for_each_scene(self, fn, selector=None, row=None, col=None): return self - def update_scenes(self, patch=None, selector=None, row=None, col=None, **kwargs): + def update_scenes( + self, patch=None, selector=None, overwrite=False, row=None, col=None, **kwargs + ): """ Perform a property update operation on all scene objects that satisfy the specified selection criteria @@ -15473,6 +15499,10 @@ def update_scenes(self, patch=None, selector=None, row=None, col=None, **kwargs) properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all scene objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of scene objects to select. To select scene objects by row and column, the Figure @@ -15489,7 +15519,7 @@ def update_scenes(self, patch=None, selector=None, row=None, col=None, **kwargs) Returns the Figure object that the method was called on """ for obj in self.select_scenes(selector=selector, row=row, col=col): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self @@ -15551,7 +15581,9 @@ def for_each_ternary(self, fn, selector=None, row=None, col=None): return self - def update_ternaries(self, patch=None, selector=None, row=None, col=None, **kwargs): + def update_ternaries( + self, patch=None, selector=None, overwrite=False, row=None, col=None, **kwargs + ): """ Perform a property update operation on all ternary objects that satisfy the specified selection criteria @@ -15567,6 +15599,10 @@ def update_ternaries(self, patch=None, selector=None, row=None, col=None, **kwar properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all ternary objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of ternary objects to select. To select ternary objects by row and column, the Figure @@ -15583,7 +15619,7 @@ def update_ternaries(self, patch=None, selector=None, row=None, col=None, **kwar Returns the Figure object that the method was called on """ for obj in self.select_ternaries(selector=selector, row=row, col=col): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self @@ -15645,7 +15681,9 @@ def for_each_xaxis(self, fn, selector=None, row=None, col=None): return self - def update_xaxes(self, patch=None, selector=None, row=None, col=None, **kwargs): + def update_xaxes( + self, patch=None, selector=None, overwrite=False, row=None, col=None, **kwargs + ): """ Perform a property update operation on all xaxis objects that satisfy the specified selection criteria @@ -15661,6 +15699,10 @@ def update_xaxes(self, patch=None, selector=None, row=None, col=None, **kwargs): properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all xaxis objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of xaxis objects to select. To select xaxis objects by row and column, the Figure @@ -15677,7 +15719,7 @@ def update_xaxes(self, patch=None, selector=None, row=None, col=None, **kwargs): Returns the Figure object that the method was called on """ for obj in self.select_xaxes(selector=selector, row=row, col=col): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self @@ -15768,7 +15810,14 @@ def for_each_yaxis(self, fn, selector=None, row=None, col=None, secondary_y=None return self def update_yaxes( - self, patch=None, selector=None, row=None, col=None, secondary_y=None, **kwargs + self, + patch=None, + selector=None, + overwrite=False, + row=None, + col=None, + secondary_y=None, + **kwargs ): """ Perform a property update operation on all yaxis objects @@ -15785,6 +15834,10 @@ def update_yaxes( properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all yaxis objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of yaxis objects to select. To select yaxis objects by row and column, the Figure @@ -15815,6 +15868,6 @@ def update_yaxes( for obj in self.select_yaxes( selector=selector, row=row, col=col, secondary_y=secondary_y ): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self diff --git a/packages/python/plotly/plotly/graph_objs/_figurewidget.py b/packages/python/plotly/plotly/graph_objs/_figurewidget.py index af6758944b..d4b5239abb 100644 --- a/packages/python/plotly/plotly/graph_objs/_figurewidget.py +++ b/packages/python/plotly/plotly/graph_objs/_figurewidget.py @@ -15081,7 +15081,9 @@ def for_each_coloraxis(self, fn, selector=None, row=None, col=None): return self - def update_coloraxes(self, patch=None, selector=None, row=None, col=None, **kwargs): + def update_coloraxes( + self, patch=None, selector=None, overwrite=False, row=None, col=None, **kwargs + ): """ Perform a property update operation on all coloraxis objects that satisfy the specified selection criteria @@ -15097,6 +15099,10 @@ def update_coloraxes(self, patch=None, selector=None, row=None, col=None, **kwar properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all coloraxis objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of coloraxis objects to select. To select coloraxis objects by row and column, the Figure @@ -15113,7 +15119,7 @@ def update_coloraxes(self, patch=None, selector=None, row=None, col=None, **kwar Returns the Figure object that the method was called on """ for obj in self.select_coloraxes(selector=selector, row=row, col=col): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self @@ -15175,7 +15181,9 @@ def for_each_geo(self, fn, selector=None, row=None, col=None): return self - def update_geos(self, patch=None, selector=None, row=None, col=None, **kwargs): + def update_geos( + self, patch=None, selector=None, overwrite=False, row=None, col=None, **kwargs + ): """ Perform a property update operation on all geo objects that satisfy the specified selection criteria @@ -15191,6 +15199,10 @@ def update_geos(self, patch=None, selector=None, row=None, col=None, **kwargs): properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all geo objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of geo objects to select. To select geo objects by row and column, the Figure @@ -15207,7 +15219,7 @@ def update_geos(self, patch=None, selector=None, row=None, col=None, **kwargs): Returns the Figure object that the method was called on """ for obj in self.select_geos(selector=selector, row=row, col=col): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self @@ -15269,7 +15281,9 @@ def for_each_mapbox(self, fn, selector=None, row=None, col=None): return self - def update_mapboxes(self, patch=None, selector=None, row=None, col=None, **kwargs): + def update_mapboxes( + self, patch=None, selector=None, overwrite=False, row=None, col=None, **kwargs + ): """ Perform a property update operation on all mapbox objects that satisfy the specified selection criteria @@ -15285,6 +15299,10 @@ def update_mapboxes(self, patch=None, selector=None, row=None, col=None, **kwarg properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all mapbox objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of mapbox objects to select. To select mapbox objects by row and column, the Figure @@ -15301,7 +15319,7 @@ def update_mapboxes(self, patch=None, selector=None, row=None, col=None, **kwarg Returns the Figure object that the method was called on """ for obj in self.select_mapboxes(selector=selector, row=row, col=col): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self @@ -15363,7 +15381,9 @@ def for_each_polar(self, fn, selector=None, row=None, col=None): return self - def update_polars(self, patch=None, selector=None, row=None, col=None, **kwargs): + def update_polars( + self, patch=None, selector=None, overwrite=False, row=None, col=None, **kwargs + ): """ Perform a property update operation on all polar objects that satisfy the specified selection criteria @@ -15379,6 +15399,10 @@ def update_polars(self, patch=None, selector=None, row=None, col=None, **kwargs) properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all polar objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of polar objects to select. To select polar objects by row and column, the Figure @@ -15395,7 +15419,7 @@ def update_polars(self, patch=None, selector=None, row=None, col=None, **kwargs) Returns the Figure object that the method was called on """ for obj in self.select_polars(selector=selector, row=row, col=col): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self @@ -15457,7 +15481,9 @@ def for_each_scene(self, fn, selector=None, row=None, col=None): return self - def update_scenes(self, patch=None, selector=None, row=None, col=None, **kwargs): + def update_scenes( + self, patch=None, selector=None, overwrite=False, row=None, col=None, **kwargs + ): """ Perform a property update operation on all scene objects that satisfy the specified selection criteria @@ -15473,6 +15499,10 @@ def update_scenes(self, patch=None, selector=None, row=None, col=None, **kwargs) properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all scene objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of scene objects to select. To select scene objects by row and column, the Figure @@ -15489,7 +15519,7 @@ def update_scenes(self, patch=None, selector=None, row=None, col=None, **kwargs) Returns the Figure object that the method was called on """ for obj in self.select_scenes(selector=selector, row=row, col=col): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self @@ -15551,7 +15581,9 @@ def for_each_ternary(self, fn, selector=None, row=None, col=None): return self - def update_ternaries(self, patch=None, selector=None, row=None, col=None, **kwargs): + def update_ternaries( + self, patch=None, selector=None, overwrite=False, row=None, col=None, **kwargs + ): """ Perform a property update operation on all ternary objects that satisfy the specified selection criteria @@ -15567,6 +15599,10 @@ def update_ternaries(self, patch=None, selector=None, row=None, col=None, **kwar properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all ternary objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of ternary objects to select. To select ternary objects by row and column, the Figure @@ -15583,7 +15619,7 @@ def update_ternaries(self, patch=None, selector=None, row=None, col=None, **kwar Returns the Figure object that the method was called on """ for obj in self.select_ternaries(selector=selector, row=row, col=col): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self @@ -15645,7 +15681,9 @@ def for_each_xaxis(self, fn, selector=None, row=None, col=None): return self - def update_xaxes(self, patch=None, selector=None, row=None, col=None, **kwargs): + def update_xaxes( + self, patch=None, selector=None, overwrite=False, row=None, col=None, **kwargs + ): """ Perform a property update operation on all xaxis objects that satisfy the specified selection criteria @@ -15661,6 +15699,10 @@ def update_xaxes(self, patch=None, selector=None, row=None, col=None, **kwargs): properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all xaxis objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of xaxis objects to select. To select xaxis objects by row and column, the Figure @@ -15677,7 +15719,7 @@ def update_xaxes(self, patch=None, selector=None, row=None, col=None, **kwargs): Returns the Figure object that the method was called on """ for obj in self.select_xaxes(selector=selector, row=row, col=col): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self @@ -15768,7 +15810,14 @@ def for_each_yaxis(self, fn, selector=None, row=None, col=None, secondary_y=None return self def update_yaxes( - self, patch=None, selector=None, row=None, col=None, secondary_y=None, **kwargs + self, + patch=None, + selector=None, + overwrite=False, + row=None, + col=None, + secondary_y=None, + **kwargs ): """ Perform a property update operation on all yaxis objects @@ -15785,6 +15834,10 @@ def update_yaxes( properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all yaxis objects are selected. + overwrite: bool + If True, overwrite existing properties. If False, apply updates + to existing properties recursively, preserving existing + properties that are not specified in the update operation. row, col: int or None (default None) Subplot row and column index of yaxis objects to select. To select yaxis objects by row and column, the Figure @@ -15815,6 +15868,6 @@ def update_yaxes( for obj in self.select_yaxes( selector=selector, row=row, col=col, secondary_y=secondary_y ): - obj.update(patch, **kwargs) + obj.update(patch, overwrite=overwrite, **kwargs) return self diff --git a/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_figure.py b/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_figure.py index 04051bffc4..57bd060b59 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_figure.py +++ b/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_figure.py @@ -5,6 +5,11 @@ class FigureTest(TestCaseNoTemplate): + def setUp(self): + import plotly.io as pio + + pio.templates.default = None + def test_instantiation(self): native_figure = {"data": [], "layout": {}, "frames": []} @@ -166,3 +171,31 @@ def test_pop_invalid_key(self): fig = go.Figure(layout=go.Layout(width=1000)) with self.assertRaises(KeyError): fig.pop("bogus") + + def test_update_overwrite_layout(self): + fig = go.Figure(layout=go.Layout(width=1000)) + + # By default, update works recursively so layout.width should remain + fig.update(layout={"title": {"text": "Fig Title"}}) + fig.layout.pop("template") + self.assertEqual( + fig.layout.to_plotly_json(), {"title": {"text": "Fig Title"}, "width": 1000} + ) + + # With overwrite=True, all existing layout properties should be + # removed + fig.update(overwrite=True, layout={"title": {"text": "Fig2 Title"}}) + fig.layout.pop("template") + self.assertEqual(fig.layout.to_plotly_json(), {"title": {"text": "Fig2 Title"}}) + + def test_update_overwrite_data(self): + fig = go.Figure( + data=[go.Bar(marker_color="blue"), go.Bar(marker_color="yellow")] + ) + + fig.update(overwrite=True, data=[go.Marker(y=[1, 3, 2], line_color="yellow")]) + + self.assertEqual( + fig.to_plotly_json()["data"], + [{"type": "scatter", "y": [1, 3, 2], "line": {"color": "yellow"}}], + ) diff --git a/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_update.py b/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_update.py index 4783faa96a..188a9a3498 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_update.py +++ b/packages/python/plotly/plotly/tests/test_core/test_graph_objs/test_update.py @@ -157,3 +157,51 @@ def test_update_initialize_nonempty_list_with_list_extends(self): } self.assertEqual(layout.to_plotly_json(), expected) + + def test_overwrite_compound_prop(self): + layout = go.Layout(title_font_family="Courier") + + # First update with default (recursive) behavior + layout.update(title={"text": "Fig Title"}) + expected = {"title": {"text": "Fig Title", "font": {"family": "Courier"}}} + self.assertEqual(layout.to_plotly_json(), expected) + + # Update with overwrite behavior + layout.update(title={"text": "Fig Title2"}, overwrite=True) + expected = {"title": {"text": "Fig Title2"}} + self.assertEqual(layout.to_plotly_json(), expected) + + def test_overwrite_tuple_prop(self): + layout = go.Layout( + annotations=[ + go.layout.Annotation(text="one"), + go.layout.Annotation(text="two"), + ] + ) + + layout.update( + overwrite=True, + annotations=[ + go.layout.Annotation(width=10), + go.layout.Annotation(width=20), + go.layout.Annotation(width=30), + go.layout.Annotation(width=40), + go.layout.Annotation(width=50), + ], + ) + + expected = { + "annotations": [ + {"width": 10}, + {"width": 20}, + {"width": 30}, + {"width": 40}, + {"width": 50}, + ] + } + + self.assertEqual(layout.to_plotly_json(), expected) + + # Remove all annotations + layout.update(overwrite=True, annotations=None) + self.assertEqual(layout.to_plotly_json(), {}) diff --git a/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_layout.py b/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_layout.py index 595aa6ae8b..3a637167c6 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_layout.py +++ b/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_layout.py @@ -3,6 +3,11 @@ class TestUpdateLayout(TestCase): + def setUp(self): + import plotly.io as pio + + pio.templates.default = None + def test_update_layout_kwargs(self): # Create initial figure fig = go.Figure() @@ -26,3 +31,41 @@ def test_update_layout_dict(self): fig.update_layout(dict(title=dict(font=dict(family="Courier New")))) orig_fig.layout.update(title_font_family="Courier New") self.assertEqual(fig, orig_fig) + + def test_update_layout_overwrite(self): + fig = go.Figure( + layout=go.Layout( + annotations=[ + go.layout.Annotation(text="one"), + go.layout.Annotation(text="two"), + ] + ) + ) + + fig.update_layout( + overwrite=True, + annotations=[ + go.layout.Annotation(width=10), + go.layout.Annotation(width=20), + go.layout.Annotation(width=30), + go.layout.Annotation(width=40), + go.layout.Annotation(width=50), + ], + ) + + expected = { + "annotations": [ + {"width": 10}, + {"width": 20}, + {"width": 30}, + {"width": 40}, + {"width": 50}, + ] + } + + fig.layout.pop("template") + self.assertEqual(fig.layout.to_plotly_json(), expected) + + # Remove all annotations + fig.update_layout(overwrite=True, annotations=None) + self.assertEqual(fig.layout.annotations, ()) diff --git a/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_subplots.py b/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_subplots.py index 19f81db2e8..14a4758b10 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_subplots.py +++ b/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_subplots.py @@ -563,3 +563,12 @@ def test_update_by_type_and_grid_and_selector(self): row=1, selector={"title.text": "A"}, ) + + def test_update_subplot_overwrite(self): + fig = go.Figure(layout_xaxis_title_text="Axis title") + fig.update_xaxes(overwrite=True, title={"font": {"family": "Courier"}}) + + self.assertEqual( + fig.layout.xaxis.to_plotly_json(), + {"title": {"font": {"family": "Courier"}}}, + ) diff --git a/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_traces.py b/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_traces.py index 3bd16adac2..b1c8487cea 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_traces.py +++ b/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_traces.py @@ -362,3 +362,18 @@ def test_update_traces_by_grid_and_selector(self): ) self.assert_update_traces([9], {"marker.size": 6}, col=1, secondary_y=True) + + def test_update_traces_overwrite(self): + fig = go.Figure( + data=[go.Scatter(marker_line_color="red"), go.Bar(marker_line_color="red")] + ) + + fig.update_traces(overwrite=True, marker={"line": {"width": 10}}) + + self.assertEqual( + fig.to_plotly_json()["data"], + [ + {"type": "scatter", "marker": {"line": {"width": 10}}}, + {"type": "bar", "marker": {"line": {"width": 10}}}, + ], + )