From 100530688d19abc48c6d512ea6c71d9f076a914a Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 17 Jul 2018 10:30:05 -0400 Subject: [PATCH 1/7] Remove custom fullmatch function (it was pretty slow) Change regular expressions to match full strings and just use .match. --- plotly/basedatatypes.py | 33 +++++++++------------------------ plotly/basewidget.py | 4 ++-- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 2b1830fe70b..e38ee56dd00 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -31,20 +31,11 @@ Undefined = object() -# back-port of fullmatch from Py3.4+ -def fullmatch(regex, string, flags=0): - """Emulate python-3.4 re.fullmatch().""" - if 'pattern' in dir(regex): - regex_string = regex.pattern - else: - regex_string = regex - return re.match("(?:" + regex_string + r")\Z", string, flags=flags) - - class BaseFigure(object): """ Base class for all figure types (both widget and non-widget) """ + _bracket_re = re.compile('^(.*)\[(\d+)\]$') # Constructor # ----------- @@ -752,11 +743,9 @@ def _str_to_dict_path(key_path_str): # Split out bracket indexes. # e.g. ['foo', 'bar[1]'] -> ['foo', 'bar', '1'] - bracket_re = re.compile('(.*)\[(\d+)\]') key_path2 = [] for key in key_path: - match = fullmatch(bracket_re, key) - #match = bracket_re.fullmatch(key) + match = BaseFigure._bracket_re.match(key) if match: key_path2.extend(match.groups()) else: @@ -3326,7 +3315,7 @@ class BaseLayoutType(BaseLayoutHierarchyType): # # ### Create subplot property regular expression ### _subplotid_prop_names = ['xaxis', 'yaxis', 'geo', 'ternary', 'scene'] _subplotid_prop_re = re.compile( - '(' + '|'.join(_subplotid_prop_names) + ')(\d+)') + '^(' + '|'.join(_subplotid_prop_names) + ')(\d+)$') @property def _subplotid_validators(self): @@ -3387,16 +3376,14 @@ def _process_kwargs(self, **kwargs): unknown_kwargs = { k: v for k, v in kwargs.items() - if not fullmatch(self._subplotid_prop_re, k) - # if not self._subplotid_prop_re.fullmatch(k) + if not self._subplotid_prop_re.match(k) } super(BaseLayoutHierarchyType, self)._process_kwargs(**unknown_kwargs) subplot_kwargs = { k: v for k, v in kwargs.items() - if fullmatch(self._subplotid_prop_re, k) - #if self._subplotid_prop_re.fullmatch(k) + if self._subplotid_prop_re.match(k) } for prop, value in subplot_kwargs.items(): @@ -3416,8 +3403,7 @@ def _set_subplotid_prop(self, prop, value): # Get regular expression match # ---------------------------- # Note: we already tested that match exists in the constructor - # match = self._subplotid_prop_re.fullmatch(prop) - match = fullmatch(self._subplotid_prop_re, prop) + match = self._subplotid_prop_re.match(prop) subplot_prop = match.group(1) suffix_digit = int(match.group(2)) @@ -3478,7 +3464,7 @@ def _strip_subplot_suffix_of_1(self, prop): # Handle subplot suffix digit of 1 # -------------------------------- # Remove digit of 1 from subplot id (e.g.. xaxis1 -> xaxis) - match = fullmatch(self._subplotid_prop_re, prop) + match = self._subplotid_prop_re.match(prop) if match: subplot_prop = match.group(1) @@ -3531,7 +3517,7 @@ def __setitem__(self, prop, value): # Check for subplot assignment # ---------------------------- - match = fullmatch(self._subplotid_prop_re, prop) + match = self._subplotid_prop_re.match(prop) if match is None: # Set as ordinary property super(BaseLayoutHierarchyType, self).__setitem__(prop, value) @@ -3545,8 +3531,7 @@ def __setattr__(self, prop, value): """ # Check for subplot assignment # ---------------------------- - # match = self._subplotid_prop_re.fullmatch(prop) - match = fullmatch(self._subplotid_prop_re, prop) + match = self._subplotid_prop_re.match(prop) if match is None: # Set as ordinary property super(BaseLayoutHierarchyType, self).__setattr__(prop, value) diff --git a/plotly/basewidget.py b/plotly/basewidget.py index af404b6923b..f9354c16351 100644 --- a/plotly/basewidget.py +++ b/plotly/basewidget.py @@ -9,7 +9,7 @@ import ipywidgets as widgets from traitlets import List, Unicode, Dict, observe, Integer -from .basedatatypes import BaseFigure, BasePlotlyType, fullmatch +from .basedatatypes import BaseFigure, BasePlotlyType from .callbacks import (BoxSelector, LassoSelector, InputDeviceState, Points) from .serializers import custom_serializers @@ -550,7 +550,7 @@ def _handler_js2py_layoutDelta(self, change): # may include axes that weren't explicitly defined by the user. for proppath in delta_transform: prop = proppath[0] - match = fullmatch(self.layout._subplotid_prop_re, prop) + match = self.layout._subplotid_prop_re.match(prop) if match and prop not in self.layout: # We need to create a subplotid object self.layout[prop] = {} From fbf4eb7b075badb3f0df908c7ecdfb6f13a01df8 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 17 Jul 2018 10:30:31 -0400 Subject: [PATCH 2/7] Add _str_to_dict_path fastpath for common case --- plotly/basedatatypes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index e38ee56dd00..175aa50ac4a 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -734,7 +734,12 @@ def _str_to_dict_path(key_path_str): ------- tuple[str | int] """ - if isinstance(key_path_str, tuple): + if isinstance(key_path_str, string_types) and \ + '.' not in key_path_str and \ + '[' not in key_path_str: + # Fast path for common case that avoids regular expressions + return (key_path_str,) + elif isinstance(key_path_str, tuple): # Nothing to do return key_path_str else: From 739a23de6f336fe8f2462a4a8ecef6bde273d97f Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 17 Jul 2018 10:45:16 -0400 Subject: [PATCH 3/7] Use dict rather than graph_objs when build up dendrogram --- plotly/figure_factory/_dendrogram.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plotly/figure_factory/_dendrogram.py b/plotly/figure_factory/_dendrogram.py index c51bf555310..72fc36c7d36 100644 --- a/plotly/figure_factory/_dendrogram.py +++ b/plotly/figure_factory/_dendrogram.py @@ -288,11 +288,12 @@ def get_dendrogram_traces(self, X, colorscale, distfun, linkagefun, hovertext): hovertext_label = None if hovertext: hovertext_label = hovertext[i] - trace = graph_objs.Scatter( + trace = dict( + type='scatter', x=np.multiply(self.sign[self.xaxis], xs), y=np.multiply(self.sign[self.yaxis], ys), mode='lines', - marker=graph_objs.scatter.Marker(color=colors[color_key]), + marker=dict(color=colors[color_key]), text=hovertext_label, hoverinfo='text' ) From fd9ac4b9f26fb4c9e680a80af3f23903a9482a16 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 17 Jul 2018 11:24:21 -0400 Subject: [PATCH 4/7] Store trace index in the trace object This lets us avoid the _index_is calls that are expensive for large trace arrays. --- plotly/basedatatypes.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 175aa50ac4a..0adbe20a065 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -134,7 +134,7 @@ class is a subclass of both BaseFigure and widgets.DOMWidget. self._data_defaults = [{} for _ in data] # ### Reparent trace objects ### - for trace in data: + for trace_ind, trace in enumerate(data): # By setting the trace's parent to be this figure, we tell the # trace object to use the figure's _data and _data_defaults # dicts to get/set it's properties, rather than using the trace @@ -144,6 +144,9 @@ class is a subclass of both BaseFigure and widgets.DOMWidget. # We clear the orphan props since the trace no longer needs then trace._orphan_props.clear() + # Set trace index + trace._trace_ind = trace_ind + # Layout # ------ # ### Construct layout validator ### @@ -454,6 +457,7 @@ def data(self, new_data): old_trace = self.data[i] old_trace._orphan_props.update(deepcopy(old_trace._props)) old_trace._parent = None + old_trace._trace_ind = None # ### Compute trace props / defaults after removal ### traces_props_post_removal = [t for t in self._data] @@ -518,6 +522,10 @@ def data(self, new_data): # Update trace objects tuple self._data_objs = list(new_data) + # Update trace indexes + for trace_ind, trace in enumerate(self._data_objs): + trace._trace_ind = trace_ind + # Restyle # ------- def plotly_restyle(self, restyle_data, trace_indexes=None, **kwargs): @@ -1059,6 +1067,10 @@ def add_traces(self, data, rows=None, cols=None): # Validate traces data = self._data_validator.validate_coerce(data) + # Set trace indexes + for ind, new_trace in enumerate(data): + new_trace._trace_ind = ind + len(self.data) + # Validate rows / cols n = len(data) BaseFigure._validate_rows_cols('rows', n, rows) @@ -1206,14 +1218,9 @@ def _get_child_props(self, child): """ # Try to find index of child as a trace # ------------------------------------- - try: - trace_index = BaseFigure._index_is(self.data, child) - except ValueError as _: - trace_index = None - - # Child is a trace - # ---------------- - if trace_index is not None: + if isinstance(child, BaseTraceType): + # ### Child is a trace ### + trace_index = child._trace_ind return self._data[trace_index] # Child is the layout @@ -3621,6 +3628,9 @@ def __init__(self, plotly_name, **kwargs): # ### Callbacks to be called on selection ### self._select_callbacks = [] + # ### Trace index in figure ### + self._trace_ind = None + # uid # --- # All trace types must have a top-level UID From 58f201ac0642620eae85305dae7704e1f4657192 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 17 Jul 2018 12:14:29 -0400 Subject: [PATCH 5/7] Fig error when setting nested property on Frame hierarchy --- plotly/basedatatypes.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 0adbe20a065..fed6dae0964 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -3902,6 +3902,11 @@ def _send_prop_set(self, prop_path_str, val): # propagated to parents pass + def _restyle_child(self, child, key_path_str, val): + # Note: Frames are not supported by FigureWidget, and updates are not + # propagated to parents + pass + def on_change(self, callback, *args): raise NotImplementedError( 'Change callbacks are not supported on Frames') From a484c3b2d617ae17e5154ea395d3a57fa8e44685 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 17 Jul 2018 12:14:58 -0400 Subject: [PATCH 6/7] Additional optimizations to use trace._trace_ind rather than _index_is --- plotly/basedatatypes.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index fed6dae0964..01da41403e6 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -689,7 +689,7 @@ def _restyle_child(self, child, key_path_str, val): # Compute trace index # ------------------- - trace_index = BaseFigure._index_is(self.data, child) + trace_index = child._trace_ind # Not in batch mode # ----------------- @@ -1248,16 +1248,10 @@ def _get_child_prop_defaults(self, child): ------- dict """ - # Try to find index of child as a trace - # ------------------------------------- - try: - trace_index = BaseFigure._index_is(self.data, child) - except ValueError as _: - trace_index = None - # Child is a trace # ---------------- - if trace_index is not None: + if isinstance(child, BaseTraceType): + trace_index = child._trace_ind return self._data_defaults[trace_index] # Child is the layout @@ -3092,7 +3086,7 @@ def _prop_set_child(self, child, prop_path_str, val): # -------------------------------- child_prop_val = getattr(self, child.plotly_name) if isinstance(child_prop_val, (list, tuple)): - child_ind = BaseFigure._index_is(child_prop_val, child) + child_ind = child._trace_ind obj_path = '{child_name}.{child_ind}.{prop}'.format( child_name=child.plotly_name, child_ind=child_ind, @@ -3597,6 +3591,7 @@ class BaseTraceHierarchyType(BasePlotlyType): def __init__(self, plotly_name, **kwargs): super(BaseTraceHierarchyType, self).__init__(plotly_name, **kwargs) + def _send_prop_set(self, prop_path_str, val): if self.parent: # ### Inform parent of restyle operation ### From 2a5af22da20423081c676339f5a669ee8b9b6864 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 17 Jul 2018 13:07:19 -0400 Subject: [PATCH 7/7] Revert incorrect removal of _index_is in BasePlotlyType --- plotly/basedatatypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 01da41403e6..44d10635afc 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -3086,7 +3086,7 @@ def _prop_set_child(self, child, prop_path_str, val): # -------------------------------- child_prop_val = getattr(self, child.plotly_name) if isinstance(child_prop_val, (list, tuple)): - child_ind = child._trace_ind + child_ind = BaseFigure._index_is(child_prop_val, child) obj_path = '{child_name}.{child_ind}.{prop}'.format( child_name=child.plotly_name, child_ind=child_ind,