Skip to content

Commit 0a36c9d

Browse files
authored
Fixes for numbered subplot bugs in GH1050 (#1057)
* Fix for #1050. Can't create numbered subplots in update * Added missing `mapbox` and `polar` subplot id support (See #1050) * Handle .update on subplots with number 1 (e.g. xaxis1) * Convert plotly objects to dicts before iterating over the in update. This prevents unspecified values from being treated as None and overwriting everything
1 parent 213602d commit 0a36c9d

File tree

2 files changed

+111
-7
lines changed

2 files changed

+111
-7
lines changed

Diff for: plotly/basedatatypes.py

+55-6
Original file line numberDiff line numberDiff line change
@@ -2100,19 +2100,36 @@ def _perform_update(plotly_obj, update_obj):
21002100
return
21012101
elif isinstance(plotly_obj, BasePlotlyType):
21022102

2103+
# Handle initializing subplot ids
2104+
# -------------------------------
2105+
# This should be valid even if xaxis2 hasn't been initialized:
2106+
# >>> layout.update(xaxis2={'title': 'xaxis 2'})
2107+
if isinstance(plotly_obj, BaseLayoutType):
2108+
for key in update_obj:
2109+
if key not in plotly_obj:
2110+
match = fullmatch(plotly_obj._subplotid_prop_re, key)
2111+
if match:
2112+
# We need to create a subplotid object
2113+
plotly_obj[key] = {}
2114+
21032115
# Handle invalid properties
21042116
# -------------------------
21052117
invalid_props = [
2106-
k for k in update_obj if k not in plotly_obj._validators
2118+
k for k in update_obj if k not in plotly_obj
21072119
]
21082120

21092121
plotly_obj._raise_on_invalid_property_error(*invalid_props)
21102122

2123+
# Convert update_obj to dict
2124+
# --------------------------
2125+
if isinstance(update_obj, BasePlotlyType):
2126+
update_obj = update_obj.to_plotly_json()
2127+
21112128
# Process valid properties
21122129
# ------------------------
21132130
for key in update_obj:
21142131
val = update_obj[key]
2115-
validator = plotly_obj._validators[key]
2132+
validator = plotly_obj._get_prop_validator(key)
21162133

21172134
if isinstance(validator, CompoundValidator):
21182135

@@ -2453,6 +2470,21 @@ def _prop_defaults(self):
24532470
else:
24542471
return self.parent._get_child_prop_defaults(self)
24552472

2473+
def _get_prop_validator(self, prop):
2474+
"""
2475+
Return the validator associated with the specified property
2476+
2477+
Parameters
2478+
----------
2479+
prop: str
2480+
A property that exists in this object
2481+
2482+
Returns
2483+
-------
2484+
BaseValidator
2485+
"""
2486+
return self._validators[prop]
2487+
24562488
@property
24572489
def parent(self):
24582490
"""
@@ -3324,7 +3356,14 @@ class BaseLayoutType(BaseLayoutHierarchyType):
33243356
# generated properties/validators as needed for xaxis2, yaxis3, etc.
33253357

33263358
# # ### Create subplot property regular expression ###
3327-
_subplotid_prop_names = ['xaxis', 'yaxis', 'geo', 'ternary', 'scene']
3359+
_subplotid_prop_names = ['xaxis',
3360+
'yaxis',
3361+
'geo',
3362+
'ternary',
3363+
'scene',
3364+
'mapbox',
3365+
'polar']
3366+
33283367
_subplotid_prop_re = re.compile(
33293368
'(' + '|'.join(_subplotid_prop_names) + ')(\d+)')
33303369

@@ -3338,15 +3377,18 @@ def _subplotid_validators(self):
33383377
dict
33393378
"""
33403379
from .validators.layout import (XAxisValidator, YAxisValidator,
3341-
GeoValidator, TernaryValidator,
3342-
SceneValidator)
3380+
GeoValidator, TernaryValidator,
3381+
SceneValidator, MapboxValidator,
3382+
PolarValidator)
33433383

33443384
return {
33453385
'xaxis': XAxisValidator,
33463386
'yaxis': YAxisValidator,
33473387
'geo': GeoValidator,
33483388
'ternary': TernaryValidator,
3349-
'scene': SceneValidator
3389+
'scene': SceneValidator,
3390+
'mapbox': MapboxValidator,
3391+
'polar': PolarValidator
33503392
}
33513393

33523394
def __init__(self, plotly_name, **kwargs):
@@ -3488,6 +3530,13 @@ def _strip_subplot_suffix_of_1(self, prop):
34883530

34893531
return prop
34903532

3533+
def _get_prop_validator(self, prop):
3534+
"""
3535+
Custom _get_prop_validator that handles subplot properties
3536+
"""
3537+
prop = self._strip_subplot_suffix_of_1(prop)
3538+
return super(BaseLayoutHierarchyType, self)._get_prop_validator(prop)
3539+
34913540
def __getattr__(self, prop):
34923541
"""
34933542
Custom __getattr__ that handles dynamic subplot properties

Diff for: plotly/tests/test_core/test_graph_objs/test_layout_subplots.py

+56-1
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ def test_initial_access_subplots(self):
1616
self.assertEqual(self.layout.yaxis, go.layout.YAxis())
1717
self.assertEqual(self.layout['geo'], go.layout.Geo())
1818
self.assertEqual(self.layout.scene, go.layout.Scene())
19+
self.assertEqual(self.layout.mapbox, go.layout.Mapbox())
20+
self.assertEqual(self.layout.polar, go.layout.Polar())
1921

2022
# Subplot ids of 1 should be mapped to the same object as the base
2123
# subplot. Notice we're using assertIs not assertEqual here
2224
self.assertIs(self.layout.xaxis, self.layout.xaxis1)
2325
self.assertIs(self.layout.yaxis, self.layout.yaxis1)
2426
self.assertIs(self.layout.geo, self.layout.geo1)
2527
self.assertIs(self.layout.scene, self.layout.scene1)
28+
self.assertIs(self.layout.mapbox, self.layout.mapbox1)
29+
self.assertIs(self.layout.polar, self.layout.polar1)
2630

2731
@raises(AttributeError)
2832
def test_initial_access_subplot2(self):
@@ -137,6 +141,12 @@ def test_subplot_objs_have_proper_type(self):
137141
self.layout.scene6 = {}
138142
self.assertIsInstance(self.layout.scene6, go.layout.Scene)
139143

144+
self.layout.mapbox7 = {}
145+
self.assertIsInstance(self.layout.mapbox7, go.layout.Mapbox)
146+
147+
self.layout.polar8 = {}
148+
self.assertIsInstance(self.layout.polar8, go.layout.Polar)
149+
140150
def test_subplot_1_in_constructor(self):
141151
layout = go.Layout(xaxis1=go.layout.XAxis(title='xaxis 1'))
142152
self.assertEqual(layout.xaxis1.title, 'xaxis 1')
@@ -146,10 +156,55 @@ def test_subplot_props_in_constructor(self):
146156
yaxis3=go.layout.YAxis(title='yaxis 3'),
147157
geo4=go.layout.Geo(bgcolor='blue'),
148158
ternary5=go.layout.Ternary(sum=120),
149-
scene6=go.layout.Scene(dragmode='zoom'))
159+
scene6=go.layout.Scene(dragmode='zoom'),
160+
mapbox7=go.layout.Mapbox(zoom=2),
161+
polar8=go.layout.Polar(sector=[0, 90]))
150162

151163
self.assertEqual(layout.xaxis2.title, 'xaxis 2')
152164
self.assertEqual(layout.yaxis3.title, 'yaxis 3')
153165
self.assertEqual(layout.geo4.bgcolor, 'blue')
154166
self.assertEqual(layout.ternary5.sum, 120)
155167
self.assertEqual(layout.scene6.dragmode, 'zoom')
168+
self.assertEqual(layout.mapbox7.zoom, 2)
169+
self.assertEqual(layout.polar8.sector, (0, 90))
170+
171+
def test_create_subplot_with_update(self):
172+
173+
self.layout.update(
174+
xaxis1=go.layout.XAxis(title='xaxis 1'),
175+
xaxis2=go.layout.XAxis(title='xaxis 2'),
176+
yaxis3=go.layout.YAxis(title='yaxis 3'),
177+
geo4=go.layout.Geo(bgcolor='blue'),
178+
ternary5=go.layout.Ternary(sum=120),
179+
scene6=go.layout.Scene(dragmode='zoom'),
180+
mapbox7=go.layout.Mapbox(zoom=2),
181+
polar8=go.layout.Polar(sector=[0, 90]))
182+
183+
self.assertEqual(self.layout.xaxis1.title, 'xaxis 1')
184+
self.assertEqual(self.layout.xaxis2.title, 'xaxis 2')
185+
self.assertEqual(self.layout.yaxis3.title, 'yaxis 3')
186+
self.assertEqual(self.layout.geo4.bgcolor, 'blue')
187+
self.assertEqual(self.layout.ternary5.sum, 120)
188+
self.assertEqual(self.layout.scene6.dragmode, 'zoom')
189+
self.assertEqual(self.layout.mapbox7.zoom, 2)
190+
self.assertEqual(self.layout.polar8.sector, (0, 90))
191+
192+
def test_create_subplot_with_update_dict(self):
193+
194+
self.layout.update({'xaxis1': {'title': 'xaxis 1'},
195+
'xaxis2': {'title': 'xaxis 2'},
196+
'yaxis3': {'title': 'yaxis 3'},
197+
'geo4': {'bgcolor': 'blue'},
198+
'ternary5': {'sum': 120},
199+
'scene6': {'dragmode': 'zoom'},
200+
'mapbox7': {'zoom': 2},
201+
'polar8': {'sector': [0, 90]}})
202+
203+
self.assertEqual(self.layout.xaxis1.title, 'xaxis 1')
204+
self.assertEqual(self.layout.xaxis2.title, 'xaxis 2')
205+
self.assertEqual(self.layout.yaxis3.title, 'yaxis 3')
206+
self.assertEqual(self.layout.geo4.bgcolor, 'blue')
207+
self.assertEqual(self.layout.ternary5.sum, 120)
208+
self.assertEqual(self.layout.scene6.dragmode, 'zoom')
209+
self.assertEqual(self.layout.mapbox7.zoom, 2)
210+
self.assertEqual(self.layout.polar8.sector, (0, 90))

0 commit comments

Comments
 (0)