Skip to content

Commit 3a972e1

Browse files
author
Jon M. Mease
committed
Added back support for subplots and anchor properties with suffix of 1
(e.g. xaxis1 is now accepted and converted into xaxis)
1 parent 13ba045 commit 3a972e1

2 files changed

Lines changed: 201 additions & 10 deletions

File tree

_plotly_utils/basevalidators.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,15 +172,51 @@ def __init__(self, plotly_name, parent_name, values, array_ok=False, coerce_numb
172172

173173
# compile regexes
174174
self.val_regexs = []
175+
176+
# regex replacement that runs before the matching regex
177+
# So far, this is only used to cast x1 -> x for anchor-style
178+
# enumeration properties
179+
self.regex_replacements = []
175180
for v in self.values:
176181
if v and isinstance(v, str) and v[0] == '/' and v[-1] == '/':
177182
# String is regex with leading and trailing '/' character
178-
self.val_regexs.append(re.compile(v[1:-1]))
183+
regex_str = v[1:-1]
184+
self.val_regexs.append(re.compile(regex_str))
185+
self.regex_replacements.append(
186+
EnumeratedValidator.build_regex_replacement(regex_str))
179187
else:
180188
self.val_regexs.append(None)
189+
self.regex_replacements.append(None)
181190

182191
self.array_ok = array_ok
183192

193+
@staticmethod
194+
def build_regex_replacement(regex_str):
195+
# regex_str = r"^y([2-9]|[1-9][0-9]+)?$"
196+
197+
# Remove id of 1 from subplotid-style anchors. The regular
198+
# expressions forbid a suffix of 1. But we want just want to convert
199+
# to by removing the 1 (e.g. turn x1 -> x).
200+
#
201+
# To be cautious, we only perform this conversion for enumerated
202+
# values that match the anchor-style regex
203+
match = re.match(r"\^(\w)\(\[2\-9\]\|\[1\-9\]\[0\-9\]\+\)\?\$",
204+
regex_str)
205+
206+
if match:
207+
anchor_char = match.group(1)
208+
return '^' + anchor_char + '1$', anchor_char
209+
else:
210+
return None
211+
212+
213+
def perform_replacemenet(self, v):
214+
for repl_args in self.regex_replacements:
215+
if repl_args:
216+
v = re.sub(repl_args[0], repl_args[1], v)
217+
218+
return v
219+
184220
def description(self):
185221

186222
# Separate regular values from regular expressions
@@ -238,12 +274,15 @@ def validate_coerce(self, v):
238274
# Pass None through
239275
pass
240276
elif self.array_ok and is_array(v):
277+
v = [self.perform_replacemenet(v_el) for v_el in v]
278+
241279
invalid_els = [e for e in v if (not self.in_values(e))]
242280
if invalid_els:
243281
self.raise_invalid_elements(invalid_els)
244282

245283
v = copy_to_contiguous_readonly_numpy_array(v)
246284
else:
285+
v = self.perform_replacemenet(v)
247286
if not self.in_values(v):
248287
self.raise_invalid_val(v)
249288
return v
@@ -821,8 +860,8 @@ def description(self):
821860

822861
desc = """\
823862
The '{plotly_name}' property is an identifier of a particular subplot, of type '{base}', that
824-
may be specified as the string '{base}' optionally followed by an integer > 1
825-
(e.g. '{base}', '{base}2', '{base}3', etc.)
863+
may be specified as the string '{base}' optionally followed by an integer >= 1
864+
(e.g. '{base}', '{base}1', '{base}2', '{base}3', etc.)
826865
""".format(plotly_name=self.plotly_name, base=self.base)
827866
return desc
828867

@@ -832,12 +871,17 @@ def validate_coerce(self, v):
832871
elif not isinstance(v, str):
833872
self.raise_invalid_val(v)
834873
else:
835-
if not re.fullmatch(self.regex, v):
874+
match = re.fullmatch(self.regex, v)
875+
if not match:
836876
is_valid = False
837877
else:
838-
digit_str = re.fullmatch(self.regex, v).group(1)
839-
if len(digit_str) > 0 and int(digit_str) in [0, 1]:
878+
digit_str = match.group(1)
879+
if len(digit_str) > 0 and int(digit_str) == 0:
840880
is_valid = False
881+
elif len(digit_str) > 0 and int(digit_str) == 1:
882+
# Remove 1 suffix (e.g. x1 -> x)
883+
v = self.base
884+
is_valid = True
841885
else:
842886
is_valid = True
843887

plotly/basedatatypes.py

Lines changed: 151 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,19 @@ def __init__(self, data=None, layout_plotly=None, frames=None):
2626

2727
layout = layout_plotly
2828

29+
# Subplots
30+
# --------
31+
self._grid_str = None
32+
self._grid_ref = None
33+
2934
# Handle case where data is a Figure or Figure-like dict
3035
# ------------------------------------------------------
3136
if isinstance(data, BaseFigure):
37+
# Bring over subplot fields
38+
self._grid_str = data._grid_str
39+
self._grid_ref = data._grid_ref
40+
41+
# Extract data, layout, and frames
3242
data, layout, frames = data.data, data.layout, data.frames
3343

3444
elif (isinstance(data, dict) and
@@ -37,6 +47,7 @@ def __init__(self, data=None, layout_plotly=None, frames=None):
3747
data.get('layout', None),
3848
data.get('frames', None))
3949

50+
4051
# Traces
4152
# ------
4253
from plotly.validators import DataValidator
@@ -113,6 +124,8 @@ def __init__(self, data=None, layout_plotly=None, frames=None):
113124
# -------
114125
self._log_plotly_commands = False
115126

127+
128+
116129
# Magic Methods
117130
# -------------
118131
def __setitem__(self, prop, value):
@@ -181,6 +194,77 @@ def update(self, dict1=None, **dict2):
181194
for k, v in d.items():
182195
BaseFigure._perform_update(self[k], v)
183196

197+
# Subplots
198+
# --------
199+
def print_grid(self):
200+
"""
201+
Print a visual layout of the figure's axes arrangement.
202+
This is only valid for figures that are created
203+
with plotly.tools.make_subplots.
204+
"""
205+
if self._grid_str is None:
206+
raise Exception("Use plotly.tools.make_subplots "
207+
"to create a subplot grid.")
208+
print(self._grid_str)
209+
210+
def append_trace(self, trace, row, col):
211+
"""
212+
Add a trace to your figure bound to axes at the row, col index.
213+
The row, col index is generated from figures created with
214+
plotly.tools.make_subplots and can be viewed with
215+
Figure.print_grid.
216+
:param (dict) trace: The data trace to be bound.
217+
:param (int) row: Subplot row index (see Figure.print_grid).
218+
:param (int) col: Subplot column index (see Figure.print_grid).
219+
Example:
220+
# stack two subplots vertically
221+
fig = tools.make_subplots(rows=2)
222+
This is the format of your plot grid:
223+
[ (1,1) x1,y1 ]
224+
[ (2,1) x2,y2 ]
225+
fig.append_trace(Scatter(x=[1,2,3], y=[2,1,2]), 1, 1)
226+
fig.append_trace(Scatter(x=[1,2,3], y=[2,1,2]), 2, 1)
227+
"""
228+
try:
229+
grid_ref = self._grid_ref
230+
except AttributeError:
231+
raise Exception("In order to use Figure.append_trace, "
232+
"you must first use "
233+
"plotly.tools.make_subplots "
234+
"to create a subplot grid.")
235+
if row <= 0:
236+
raise Exception("Row value is out of range. "
237+
"Note: the starting cell is (1, 1)")
238+
if col <= 0:
239+
raise Exception("Col value is out of range. "
240+
"Note: the starting cell is (1, 1)")
241+
try:
242+
ref = grid_ref[row - 1][col - 1]
243+
except IndexError:
244+
raise Exception("The (row, col) pair sent is out of "
245+
"range. Use Figure.print_grid to view the "
246+
"subplot grid. ")
247+
if 'scene' in ref[0]:
248+
trace['scene'] = ref[0]
249+
if ref[0] not in self['layout']:
250+
raise Exception("Something went wrong. "
251+
"The scene object for ({r},{c}) "
252+
"subplot cell "
253+
"got deleted.".format(r=row, c=col))
254+
else:
255+
xaxis_key = "xaxis{ref}".format(ref=ref[0][1:])
256+
yaxis_key = "yaxis{ref}".format(ref=ref[1][1:])
257+
if (xaxis_key not in self['layout']
258+
or yaxis_key not in self['layout']):
259+
raise Exception("Something went wrong. "
260+
"An axis object for ({r},{c}) subplot "
261+
"cell got deleted."
262+
.format(r=row, c=col))
263+
trace['xaxis'] = ref[0]
264+
trace['yaxis'] = ref[1]
265+
266+
self.add_traces([trace])
267+
184268
# Data
185269
# ----
186270
@property
@@ -1443,7 +1527,9 @@ def __getitem__(self, prop):
14431527

14441528
return res
14451529
else:
1446-
if prop not in self._validators:
1530+
if (prop not in self._validators and
1531+
prop not in self._props and
1532+
prop not in self._prop_defaults):
14471533
raise KeyError(prop)
14481534

14491535
if prop in self._compound_props:
@@ -1765,11 +1851,17 @@ def _set_subplotid_prop(self, prop, value):
17651851
match = self._subplotid_prop_re.fullmatch(prop)
17661852
subplot_prop = match.group(1)
17671853
suffix_digit = int(match.group(2))
1768-
if suffix_digit in [0, 1]:
1769-
raise TypeError('Subplot properties may only be suffixed by an integer > 1\n'
1854+
if suffix_digit == 0:
1855+
raise TypeError('Subplot properties may only be suffixed by an '
1856+
'integer >= 1\n'
17701857
'Received {k}'.format(k=prop))
17711858

1772-
# Add validator
1859+
# Handle suffix_digit == 1
1860+
# In this case we remove suffix digit (e.g. xaxis1 -> xaxis)
1861+
if suffix_digit == 1:
1862+
prop = subplot_prop
1863+
1864+
# Add validator
17731865
if prop not in self._validators:
17741866
validator = self._subplotid_validators[subplot_prop](plotly_name=prop)
17751867
self._validators[prop] = validator
@@ -1778,13 +1870,68 @@ def _set_subplotid_prop(self, prop, value):
17781870
self._subplotid_props[prop] = self._set_compound_prop(prop, value)
17791871

17801872
def __getattr__(self, item):
1873+
1874+
# Handle subplot suffix of 1
1875+
# Remove digit of 1 from subplot id (e.g.. xaxis1 -> xaxis)
1876+
match = self._subplotid_prop_re.fullmatch(item)
1877+
if match:
1878+
subplot_prop = match.group(1)
1879+
suffix_digit = int(match.group(2))
1880+
if subplot_prop and suffix_digit == 1:
1881+
item = subplot_prop
1882+
17811883
# Check for subplot access (e.g. xaxis2)
17821884
# Validate then call self._get_prop(item)
17831885
if item in self._subplotid_props:
17841886
return self._subplotid_props[item]
1887+
elif item in self._validators:
1888+
return self[item]
17851889

17861890
raise AttributeError("'Layout' object has no attribute '{item}'".format(item=item))
17871891

1892+
def __getitem__(self, item):
1893+
1894+
# Handle subplot suffix of 1
1895+
# Remove digit of 1 from subplot id (e.g.. xaxis1 -> xaxis)
1896+
match = self._subplotid_prop_re.fullmatch(item)
1897+
if match:
1898+
subplot_prop = match.group(1)
1899+
suffix_digit = int(match.group(2))
1900+
if subplot_prop and suffix_digit == 1:
1901+
item = subplot_prop
1902+
1903+
# Check for subplot access (e.g. xaxis2)
1904+
# Validate then call self._get_prop(item)
1905+
if item in self._subplotid_props:
1906+
return self._subplotid_props[item]
1907+
elif item in self._validators:
1908+
return super().__getitem__(item)
1909+
1910+
raise AttributeError("'Layout' object has no attribute '{item}'".format(item=item))
1911+
1912+
def __contains__(self, prop):
1913+
if prop in self._validators:
1914+
return True
1915+
else:
1916+
# Check for subplot with suffix 1
1917+
match = self._subplotid_prop_re.fullmatch(prop)
1918+
if (match and
1919+
match.group(1) in self._validators and
1920+
match.group(2) == '1'):
1921+
return True
1922+
else:
1923+
return False
1924+
1925+
def __setitem__(self, prop, value):
1926+
# Check for subplot assignment (e.g. xaxis2)
1927+
# Call _set_compound_prop with the xaxis validator
1928+
match = self._subplotid_prop_re.fullmatch(prop)
1929+
if match is None:
1930+
# Try setting as ordinary property
1931+
super().__setitem__(prop, value)
1932+
else:
1933+
self._set_subplotid_prop(prop, value)
1934+
17881935
def __setattr__(self, prop, value):
17891936
# Check for subplot assignment (e.g. xaxis2)
17901937
# Call _set_compound_prop with the xaxis validator

0 commit comments

Comments
 (0)