Skip to content

Commit 5753e13

Browse files
authored
Assorted performance improvements (#1061)
* Remove custom fullmatch function (it was pretty slow) Change regular expressions to match full strings and just use .match. * Add _str_to_dict_path fastpath for common case * Use dict rather than graph_objs when build up dendrogram * Store trace index in the trace object. This lets us avoid the _index_is calls that are expensive for large trace arrays. * Fix fig error when setting nested property on Frame hierarchy * Additional optimizations to use trace._trace_ind rather than _index_is
1 parent 43105ce commit 5753e13

File tree

3 files changed

+48
-47
lines changed

3 files changed

+48
-47
lines changed

plotly/basedatatypes.py

+43-43
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,11 @@
3131
Undefined = object()
3232

3333

34-
# back-port of fullmatch from Py3.4+
35-
def fullmatch(regex, string, flags=0):
36-
"""Emulate python-3.4 re.fullmatch()."""
37-
if 'pattern' in dir(regex):
38-
regex_string = regex.pattern
39-
else:
40-
regex_string = regex
41-
return re.match("(?:" + regex_string + r")\Z", string, flags=flags)
42-
43-
4434
class BaseFigure(object):
4535
"""
4636
Base class for all figure types (both widget and non-widget)
4737
"""
38+
_bracket_re = re.compile('^(.*)\[(\d+)\]$')
4839

4940
# Constructor
5041
# -----------
@@ -143,7 +134,7 @@ class is a subclass of both BaseFigure and widgets.DOMWidget.
143134
self._data_defaults = [{} for _ in data]
144135

145136
# ### Reparent trace objects ###
146-
for trace in data:
137+
for trace_ind, trace in enumerate(data):
147138
# By setting the trace's parent to be this figure, we tell the
148139
# trace object to use the figure's _data and _data_defaults
149140
# dicts to get/set it's properties, rather than using the trace
@@ -153,6 +144,9 @@ class is a subclass of both BaseFigure and widgets.DOMWidget.
153144
# We clear the orphan props since the trace no longer needs then
154145
trace._orphan_props.clear()
155146

147+
# Set trace index
148+
trace._trace_ind = trace_ind
149+
156150
# Layout
157151
# ------
158152
# ### Construct layout validator ###
@@ -463,6 +457,7 @@ def data(self, new_data):
463457
old_trace = self.data[i]
464458
old_trace._orphan_props.update(deepcopy(old_trace._props))
465459
old_trace._parent = None
460+
old_trace._trace_ind = None
466461

467462
# ### Compute trace props / defaults after removal ###
468463
traces_props_post_removal = [t for t in self._data]
@@ -527,6 +522,10 @@ def data(self, new_data):
527522
# Update trace objects tuple
528523
self._data_objs = list(new_data)
529524

525+
# Update trace indexes
526+
for trace_ind, trace in enumerate(self._data_objs):
527+
trace._trace_ind = trace_ind
528+
530529
# Restyle
531530
# -------
532531
def plotly_restyle(self, restyle_data, trace_indexes=None, **kwargs):
@@ -690,7 +689,7 @@ def _restyle_child(self, child, key_path_str, val):
690689

691690
# Compute trace index
692691
# -------------------
693-
trace_index = BaseFigure._index_is(self.data, child)
692+
trace_index = child._trace_ind
694693

695694
# Not in batch mode
696695
# -----------------
@@ -743,7 +742,12 @@ def _str_to_dict_path(key_path_str):
743742
-------
744743
tuple[str | int]
745744
"""
746-
if isinstance(key_path_str, tuple):
745+
if isinstance(key_path_str, string_types) and \
746+
'.' not in key_path_str and \
747+
'[' not in key_path_str:
748+
# Fast path for common case that avoids regular expressions
749+
return (key_path_str,)
750+
elif isinstance(key_path_str, tuple):
747751
# Nothing to do
748752
return key_path_str
749753
else:
@@ -752,11 +756,9 @@ def _str_to_dict_path(key_path_str):
752756

753757
# Split out bracket indexes.
754758
# e.g. ['foo', 'bar[1]'] -> ['foo', 'bar', '1']
755-
bracket_re = re.compile('(.*)\[(\d+)\]')
756759
key_path2 = []
757760
for key in key_path:
758-
match = fullmatch(bracket_re, key)
759-
#match = bracket_re.fullmatch(key)
761+
match = BaseFigure._bracket_re.match(key)
760762
if match:
761763
key_path2.extend(match.groups())
762764
else:
@@ -1065,6 +1067,10 @@ def add_traces(self, data, rows=None, cols=None):
10651067
# Validate traces
10661068
data = self._data_validator.validate_coerce(data)
10671069

1070+
# Set trace indexes
1071+
for ind, new_trace in enumerate(data):
1072+
new_trace._trace_ind = ind + len(self.data)
1073+
10681074
# Validate rows / cols
10691075
n = len(data)
10701076
BaseFigure._validate_rows_cols('rows', n, rows)
@@ -1212,14 +1218,9 @@ def _get_child_props(self, child):
12121218
"""
12131219
# Try to find index of child as a trace
12141220
# -------------------------------------
1215-
try:
1216-
trace_index = BaseFigure._index_is(self.data, child)
1217-
except ValueError as _:
1218-
trace_index = None
1219-
1220-
# Child is a trace
1221-
# ----------------
1222-
if trace_index is not None:
1221+
if isinstance(child, BaseTraceType):
1222+
# ### Child is a trace ###
1223+
trace_index = child._trace_ind
12231224
return self._data[trace_index]
12241225

12251226
# Child is the layout
@@ -1247,16 +1248,10 @@ def _get_child_prop_defaults(self, child):
12471248
-------
12481249
dict
12491250
"""
1250-
# Try to find index of child as a trace
1251-
# -------------------------------------
1252-
try:
1253-
trace_index = BaseFigure._index_is(self.data, child)
1254-
except ValueError as _:
1255-
trace_index = None
1256-
12571251
# Child is a trace
12581252
# ----------------
1259-
if trace_index is not None:
1253+
if isinstance(child, BaseTraceType):
1254+
trace_index = child._trace_ind
12601255
return self._data_defaults[trace_index]
12611256

12621257
# Child is the layout
@@ -3365,7 +3360,7 @@ class BaseLayoutType(BaseLayoutHierarchyType):
33653360
'polar']
33663361

33673362
_subplotid_prop_re = re.compile(
3368-
'(' + '|'.join(_subplotid_prop_names) + ')(\d+)')
3363+
'^(' + '|'.join(_subplotid_prop_names) + ')(\d+)$')
33693364

33703365
@property
33713366
def _subplotid_validators(self):
@@ -3429,16 +3424,14 @@ def _process_kwargs(self, **kwargs):
34293424
unknown_kwargs = {
34303425
k: v
34313426
for k, v in kwargs.items()
3432-
if not fullmatch(self._subplotid_prop_re, k)
3433-
# if not self._subplotid_prop_re.fullmatch(k)
3427+
if not self._subplotid_prop_re.match(k)
34343428
}
34353429
super(BaseLayoutHierarchyType, self)._process_kwargs(**unknown_kwargs)
34363430

34373431
subplot_kwargs = {
34383432
k: v
34393433
for k, v in kwargs.items()
3440-
if fullmatch(self._subplotid_prop_re, k)
3441-
#if self._subplotid_prop_re.fullmatch(k)
3434+
if self._subplotid_prop_re.match(k)
34423435
}
34433436

34443437
for prop, value in subplot_kwargs.items():
@@ -3458,8 +3451,7 @@ def _set_subplotid_prop(self, prop, value):
34583451
# Get regular expression match
34593452
# ----------------------------
34603453
# Note: we already tested that match exists in the constructor
3461-
# match = self._subplotid_prop_re.fullmatch(prop)
3462-
match = fullmatch(self._subplotid_prop_re, prop)
3454+
match = self._subplotid_prop_re.match(prop)
34633455
subplot_prop = match.group(1)
34643456
suffix_digit = int(match.group(2))
34653457

@@ -3520,7 +3512,7 @@ def _strip_subplot_suffix_of_1(self, prop):
35203512
# Handle subplot suffix digit of 1
35213513
# --------------------------------
35223514
# Remove digit of 1 from subplot id (e.g.. xaxis1 -> xaxis)
3523-
match = fullmatch(self._subplotid_prop_re, prop)
3515+
match = self._subplotid_prop_re.match(prop)
35243516

35253517
if match:
35263518
subplot_prop = match.group(1)
@@ -3580,7 +3572,7 @@ def __setitem__(self, prop, value):
35803572

35813573
# Check for subplot assignment
35823574
# ----------------------------
3583-
match = fullmatch(self._subplotid_prop_re, prop)
3575+
match = self._subplotid_prop_re.match(prop)
35843576
if match is None:
35853577
# Set as ordinary property
35863578
super(BaseLayoutHierarchyType, self).__setitem__(prop, value)
@@ -3594,8 +3586,7 @@ def __setattr__(self, prop, value):
35943586
"""
35953587
# Check for subplot assignment
35963588
# ----------------------------
3597-
# match = self._subplotid_prop_re.fullmatch(prop)
3598-
match = fullmatch(self._subplotid_prop_re, prop)
3589+
match = self._subplotid_prop_re.match(prop)
35993590
if match is None:
36003591
# Set as ordinary property
36013592
super(BaseLayoutHierarchyType, self).__setattr__(prop, value)
@@ -3649,6 +3640,7 @@ class BaseTraceHierarchyType(BasePlotlyType):
36493640

36503641
def __init__(self, plotly_name, **kwargs):
36513642
super(BaseTraceHierarchyType, self).__init__(plotly_name, **kwargs)
3643+
36523644
def _send_prop_set(self, prop_path_str, val):
36533645
if self.parent:
36543646
# ### Inform parent of restyle operation ###
@@ -3680,6 +3672,9 @@ def __init__(self, plotly_name, **kwargs):
36803672
# ### Callbacks to be called on selection ###
36813673
self._select_callbacks = []
36823674

3675+
# ### Trace index in figure ###
3676+
self._trace_ind = None
3677+
36833678
# uid
36843679
# ---
36853680
# All trace types must have a top-level UID
@@ -3951,6 +3946,11 @@ def _send_prop_set(self, prop_path_str, val):
39513946
# propagated to parents
39523947
pass
39533948

3949+
def _restyle_child(self, child, key_path_str, val):
3950+
# Note: Frames are not supported by FigureWidget, and updates are not
3951+
# propagated to parents
3952+
pass
3953+
39543954
def on_change(self, callback, *args):
39553955
raise NotImplementedError(
39563956
'Change callbacks are not supported on Frames')

plotly/basewidget.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import ipywidgets as widgets
1111
from traitlets import List, Unicode, Dict, observe, Integer
12-
from .basedatatypes import BaseFigure, BasePlotlyType, fullmatch
12+
from .basedatatypes import BaseFigure, BasePlotlyType
1313
from .callbacks import (BoxSelector, LassoSelector,
1414
InputDeviceState, Points)
1515
from .serializers import custom_serializers
@@ -550,7 +550,7 @@ def _handler_js2py_layoutDelta(self, change):
550550
# may include axes that weren't explicitly defined by the user.
551551
for proppath in delta_transform:
552552
prop = proppath[0]
553-
match = fullmatch(self.layout._subplotid_prop_re, prop)
553+
match = self.layout._subplotid_prop_re.match(prop)
554554
if match and prop not in self.layout:
555555
# We need to create a subplotid object
556556
self.layout[prop] = {}

plotly/figure_factory/_dendrogram.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,12 @@ def get_dendrogram_traces(self, X, colorscale, distfun, linkagefun, hovertext):
288288
hovertext_label = None
289289
if hovertext:
290290
hovertext_label = hovertext[i]
291-
trace = graph_objs.Scatter(
291+
trace = dict(
292+
type='scatter',
292293
x=np.multiply(self.sign[self.xaxis], xs),
293294
y=np.multiply(self.sign[self.yaxis], ys),
294295
mode='lines',
295-
marker=graph_objs.scatter.Marker(color=colors[color_key]),
296+
marker=dict(color=colors[color_key]),
296297
text=hovertext_label,
297298
hoverinfo='text'
298299
)

0 commit comments

Comments
 (0)