From 78a4434bf37ccee9d10c941f880bc9e3020e1998 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 15 Jul 2018 08:10:11 -0400 Subject: [PATCH 1/4] Fix for #1050. Can't create numbered subplots in update --- plotly/basedatatypes.py | 12 ++++++++ .../test_graph_objs/test_layout_subplots.py | 28 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 2b1830fe70b..8d0a1b06056 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -2100,6 +2100,18 @@ def _perform_update(plotly_obj, update_obj): return elif isinstance(plotly_obj, BasePlotlyType): + # Handle initializing subplot ids + # ------------------------------- + # This should be valid even if xaxis2 hasn't been initialized: + # >>> layout.update(xaxis2={'title': 'xaxis 2'}) + if isinstance(plotly_obj, BaseLayoutType): + for key in update_obj: + if key not in plotly_obj: + match = fullmatch(plotly_obj._subplotid_prop_re, key) + if match: + # We need to create a subplotid object + plotly_obj[key] = {} + # Handle invalid properties # ------------------------- invalid_props = [ diff --git a/plotly/tests/test_core/test_graph_objs/test_layout_subplots.py b/plotly/tests/test_core/test_graph_objs/test_layout_subplots.py index 6641a91e07d..2214108c4a2 100644 --- a/plotly/tests/test_core/test_graph_objs/test_layout_subplots.py +++ b/plotly/tests/test_core/test_graph_objs/test_layout_subplots.py @@ -153,3 +153,31 @@ def test_subplot_props_in_constructor(self): self.assertEqual(layout.geo4.bgcolor, 'blue') self.assertEqual(layout.ternary5.sum, 120) self.assertEqual(layout.scene6.dragmode, 'zoom') + + def test_create_subplot_with_update(self): + + self.layout.update(xaxis2=go.layout.XAxis(title='xaxis 2'), + yaxis3=go.layout.YAxis(title='yaxis 3'), + geo4=go.layout.Geo(bgcolor='blue'), + ternary5=go.layout.Ternary(sum=120), + scene6=go.layout.Scene(dragmode='zoom')) + + self.assertEqual(self.layout.xaxis2.title, 'xaxis 2') + self.assertEqual(self.layout.yaxis3.title, 'yaxis 3') + self.assertEqual(self.layout.geo4.bgcolor, 'blue') + self.assertEqual(self.layout.ternary5.sum, 120) + self.assertEqual(self.layout.scene6.dragmode, 'zoom') + + def test_create_subplot_with_update_dict(self): + + self.layout.update({'xaxis2': {'title': 'xaxis 2'}, + 'yaxis3': {'title': 'yaxis 3'}, + 'geo4': {'bgcolor': 'blue'}, + 'ternary5': {'sum': 120}, + 'scene6': {'dragmode': 'zoom'}}) + + self.assertEqual(self.layout.xaxis2.title, 'xaxis 2') + self.assertEqual(self.layout.yaxis3.title, 'yaxis 3') + self.assertEqual(self.layout.geo4.bgcolor, 'blue') + self.assertEqual(self.layout.ternary5.sum, 120) + self.assertEqual(self.layout.scene6.dragmode, 'zoom') From 80db49aa060fd7c7bc99169d12395b10caa52080 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 15 Jul 2018 08:31:57 -0400 Subject: [PATCH 2/4] Added missing `mapbox` and `polar` subplot id support (See #1050) --- plotly/basedatatypes.py | 18 +++++++++--- .../test_graph_objs/test_layout_subplots.py | 28 +++++++++++++++++-- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 8d0a1b06056..c8358bda94e 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -3336,7 +3336,14 @@ class BaseLayoutType(BaseLayoutHierarchyType): # generated properties/validators as needed for xaxis2, yaxis3, etc. # # ### Create subplot property regular expression ### - _subplotid_prop_names = ['xaxis', 'yaxis', 'geo', 'ternary', 'scene'] + _subplotid_prop_names = ['xaxis', + 'yaxis', + 'geo', + 'ternary', + 'scene', + 'mapbox', + 'polar'] + _subplotid_prop_re = re.compile( '(' + '|'.join(_subplotid_prop_names) + ')(\d+)') @@ -3350,15 +3357,18 @@ def _subplotid_validators(self): dict """ from .validators.layout import (XAxisValidator, YAxisValidator, - GeoValidator, TernaryValidator, - SceneValidator) + GeoValidator, TernaryValidator, + SceneValidator, MapboxValidator, + PolarValidator) return { 'xaxis': XAxisValidator, 'yaxis': YAxisValidator, 'geo': GeoValidator, 'ternary': TernaryValidator, - 'scene': SceneValidator + 'scene': SceneValidator, + 'mapbox': MapboxValidator, + 'polar': PolarValidator } def __init__(self, plotly_name, **kwargs): diff --git a/plotly/tests/test_core/test_graph_objs/test_layout_subplots.py b/plotly/tests/test_core/test_graph_objs/test_layout_subplots.py index 2214108c4a2..eef2be47345 100644 --- a/plotly/tests/test_core/test_graph_objs/test_layout_subplots.py +++ b/plotly/tests/test_core/test_graph_objs/test_layout_subplots.py @@ -16,6 +16,8 @@ def test_initial_access_subplots(self): self.assertEqual(self.layout.yaxis, go.layout.YAxis()) self.assertEqual(self.layout['geo'], go.layout.Geo()) self.assertEqual(self.layout.scene, go.layout.Scene()) + self.assertEqual(self.layout.mapbox, go.layout.Mapbox()) + self.assertEqual(self.layout.polar, go.layout.Polar()) # Subplot ids of 1 should be mapped to the same object as the base # subplot. Notice we're using assertIs not assertEqual here @@ -23,6 +25,8 @@ def test_initial_access_subplots(self): self.assertIs(self.layout.yaxis, self.layout.yaxis1) self.assertIs(self.layout.geo, self.layout.geo1) self.assertIs(self.layout.scene, self.layout.scene1) + self.assertIs(self.layout.mapbox, self.layout.mapbox1) + self.assertIs(self.layout.polar, self.layout.polar1) @raises(AttributeError) def test_initial_access_subplot2(self): @@ -137,6 +141,12 @@ def test_subplot_objs_have_proper_type(self): self.layout.scene6 = {} self.assertIsInstance(self.layout.scene6, go.layout.Scene) + self.layout.mapbox7 = {} + self.assertIsInstance(self.layout.mapbox7, go.layout.Mapbox) + + self.layout.polar8 = {} + self.assertIsInstance(self.layout.polar8, go.layout.Polar) + def test_subplot_1_in_constructor(self): layout = go.Layout(xaxis1=go.layout.XAxis(title='xaxis 1')) self.assertEqual(layout.xaxis1.title, 'xaxis 1') @@ -146,13 +156,17 @@ def test_subplot_props_in_constructor(self): yaxis3=go.layout.YAxis(title='yaxis 3'), geo4=go.layout.Geo(bgcolor='blue'), ternary5=go.layout.Ternary(sum=120), - scene6=go.layout.Scene(dragmode='zoom')) + scene6=go.layout.Scene(dragmode='zoom'), + mapbox7=go.layout.Mapbox(zoom=2), + polar8=go.layout.Polar(sector=[0, 90])) self.assertEqual(layout.xaxis2.title, 'xaxis 2') self.assertEqual(layout.yaxis3.title, 'yaxis 3') self.assertEqual(layout.geo4.bgcolor, 'blue') self.assertEqual(layout.ternary5.sum, 120) self.assertEqual(layout.scene6.dragmode, 'zoom') + self.assertEqual(layout.mapbox7.zoom, 2) + self.assertEqual(layout.polar8.sector, (0, 90)) def test_create_subplot_with_update(self): @@ -160,13 +174,17 @@ def test_create_subplot_with_update(self): yaxis3=go.layout.YAxis(title='yaxis 3'), geo4=go.layout.Geo(bgcolor='blue'), ternary5=go.layout.Ternary(sum=120), - scene6=go.layout.Scene(dragmode='zoom')) + scene6=go.layout.Scene(dragmode='zoom'), + mapbox7=go.layout.Mapbox(zoom=2), + polar8=go.layout.Polar(sector=[0, 90])) self.assertEqual(self.layout.xaxis2.title, 'xaxis 2') self.assertEqual(self.layout.yaxis3.title, 'yaxis 3') self.assertEqual(self.layout.geo4.bgcolor, 'blue') self.assertEqual(self.layout.ternary5.sum, 120) self.assertEqual(self.layout.scene6.dragmode, 'zoom') + self.assertEqual(self.layout.mapbox7.zoom, 2) + self.assertEqual(self.layout.polar8.sector, (0, 90)) def test_create_subplot_with_update_dict(self): @@ -174,10 +192,14 @@ def test_create_subplot_with_update_dict(self): 'yaxis3': {'title': 'yaxis 3'}, 'geo4': {'bgcolor': 'blue'}, 'ternary5': {'sum': 120}, - 'scene6': {'dragmode': 'zoom'}}) + 'scene6': {'dragmode': 'zoom'}, + 'mapbox7': {'zoom': 2}, + 'polar8': {'sector': [0, 90]}}) self.assertEqual(self.layout.xaxis2.title, 'xaxis 2') self.assertEqual(self.layout.yaxis3.title, 'yaxis 3') self.assertEqual(self.layout.geo4.bgcolor, 'blue') self.assertEqual(self.layout.ternary5.sum, 120) self.assertEqual(self.layout.scene6.dragmode, 'zoom') + self.assertEqual(self.layout.mapbox7.zoom, 2) + self.assertEqual(self.layout.polar8.sector, (0, 90)) From 424bf6b75801caa0e9a3d0de68f33fbf591f6fac Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 17 Jul 2018 08:44:49 -0400 Subject: [PATCH 3/4] Handle .update on subplots with number 1 (e.g. xaxis1) --- plotly/basedatatypes.py | 26 +++++++++++++++++-- .../test_graph_objs/test_layout_subplots.py | 23 +++++++++------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index c8358bda94e..7d8d7593150 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -2115,7 +2115,7 @@ def _perform_update(plotly_obj, update_obj): # Handle invalid properties # ------------------------- invalid_props = [ - k for k in update_obj if k not in plotly_obj._validators + k for k in update_obj if k not in plotly_obj ] plotly_obj._raise_on_invalid_property_error(*invalid_props) @@ -2124,7 +2124,7 @@ def _perform_update(plotly_obj, update_obj): # ------------------------ for key in update_obj: val = update_obj[key] - validator = plotly_obj._validators[key] + validator = plotly_obj._get_prop_validator(key) if isinstance(validator, CompoundValidator): @@ -2465,6 +2465,21 @@ def _prop_defaults(self): else: return self.parent._get_child_prop_defaults(self) + def _get_prop_validator(self, prop): + """ + Return the validator associated with the specified property + + Parameters + ---------- + prop: str + A property that exists in this object + + Returns + ------- + BaseValidator + """ + return self._validators[prop] + @property def parent(self): """ @@ -3510,6 +3525,13 @@ def _strip_subplot_suffix_of_1(self, prop): return prop + def _get_prop_validator(self, prop): + """ + Custom _get_prop_validator that handles subplot properties + """ + prop = self._strip_subplot_suffix_of_1(prop) + return super(BaseLayoutHierarchyType, self)._get_prop_validator(prop) + def __getattr__(self, prop): """ Custom __getattr__ that handles dynamic subplot properties diff --git a/plotly/tests/test_core/test_graph_objs/test_layout_subplots.py b/plotly/tests/test_core/test_graph_objs/test_layout_subplots.py index eef2be47345..a96bb076c24 100644 --- a/plotly/tests/test_core/test_graph_objs/test_layout_subplots.py +++ b/plotly/tests/test_core/test_graph_objs/test_layout_subplots.py @@ -170,14 +170,17 @@ def test_subplot_props_in_constructor(self): def test_create_subplot_with_update(self): - self.layout.update(xaxis2=go.layout.XAxis(title='xaxis 2'), - yaxis3=go.layout.YAxis(title='yaxis 3'), - geo4=go.layout.Geo(bgcolor='blue'), - ternary5=go.layout.Ternary(sum=120), - scene6=go.layout.Scene(dragmode='zoom'), - mapbox7=go.layout.Mapbox(zoom=2), - polar8=go.layout.Polar(sector=[0, 90])) - + self.layout.update( + xaxis1=go.layout.XAxis(title='xaxis 1'), + xaxis2=go.layout.XAxis(title='xaxis 2'), + yaxis3=go.layout.YAxis(title='yaxis 3'), + geo4=go.layout.Geo(bgcolor='blue'), + ternary5=go.layout.Ternary(sum=120), + scene6=go.layout.Scene(dragmode='zoom'), + mapbox7=go.layout.Mapbox(zoom=2), + polar8=go.layout.Polar(sector=[0, 90])) + + self.assertEqual(self.layout.xaxis1.title, 'xaxis 1') self.assertEqual(self.layout.xaxis2.title, 'xaxis 2') self.assertEqual(self.layout.yaxis3.title, 'yaxis 3') self.assertEqual(self.layout.geo4.bgcolor, 'blue') @@ -188,7 +191,8 @@ def test_create_subplot_with_update(self): def test_create_subplot_with_update_dict(self): - self.layout.update({'xaxis2': {'title': 'xaxis 2'}, + self.layout.update({'xaxis1': {'title': 'xaxis 1'}, + 'xaxis2': {'title': 'xaxis 2'}, 'yaxis3': {'title': 'yaxis 3'}, 'geo4': {'bgcolor': 'blue'}, 'ternary5': {'sum': 120}, @@ -196,6 +200,7 @@ def test_create_subplot_with_update_dict(self): 'mapbox7': {'zoom': 2}, 'polar8': {'sector': [0, 90]}}) + self.assertEqual(self.layout.xaxis1.title, 'xaxis 1') self.assertEqual(self.layout.xaxis2.title, 'xaxis 2') self.assertEqual(self.layout.yaxis3.title, 'yaxis 3') self.assertEqual(self.layout.geo4.bgcolor, 'blue') From ecf5d9102d052a2fb8a747d266954a5b3c944c3b Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 17 Jul 2018 09:06:50 -0400 Subject: [PATCH 4/4] Convert plotly objects to dicts before iterating over the in update. This prevents unspecified values from being treated as None and overwriting everything --- plotly/basedatatypes.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 7d8d7593150..140d81239c3 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -2120,6 +2120,11 @@ def _perform_update(plotly_obj, update_obj): plotly_obj._raise_on_invalid_property_error(*invalid_props) + # Convert update_obj to dict + # -------------------------- + if isinstance(update_obj, BasePlotlyType): + update_obj = update_obj.to_plotly_json() + # Process valid properties # ------------------------ for key in update_obj: