diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 2b1830fe70b..140d81239c3 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -2100,19 +2100,36 @@ 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 = [ - 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) + # 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: val = update_obj[key] - validator = plotly_obj._validators[key] + validator = plotly_obj._get_prop_validator(key) if isinstance(validator, CompoundValidator): @@ -2453,6 +2470,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): """ @@ -3324,7 +3356,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+)') @@ -3338,15 +3377,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): @@ -3488,6 +3530,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 6641a91e07d..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 @@ -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,10 +156,55 @@ 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): + + 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') + 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): + + self.layout.update({'xaxis1': {'title': 'xaxis 1'}, + 'xaxis2': {'title': 'xaxis 2'}, + 'yaxis3': {'title': 'yaxis 3'}, + 'geo4': {'bgcolor': 'blue'}, + 'ternary5': {'sum': 120}, + 'scene6': {'dragmode': 'zoom'}, + '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') + 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))