Skip to content

Commit 6f9621a

Browse files
authored
Merge pull request #678 from plotly/speed_up_graph_objs
Speed up graph objs
2 parents 7064d74 + 28c6fe0 commit 6f9621a

File tree

9 files changed

+2208
-2128
lines changed

9 files changed

+2208
-2128
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## [Unreleased]
66

7+
### Added
8+
- `memoize` decorator added to `plotly.utils`
9+
710
### Changed
811
- a `Grid` from `plotly.grid_objs` now accepts a `pandas.Dataframe` as its argument.
12+
- computationally-intensive `graph_reference` functions are memoized.
913

1014
## [2.0.0] - 2017-01-25
1115

plotly/graph_objs/graph_objs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
from plotly import exceptions, graph_reference
3535
from plotly.graph_objs import graph_objs_tools
3636

37+
_subplot_regex = re.compile(r'(?P<digits>\d+$)')
38+
3739

3840
class PlotlyBase(object):
3941
"""
@@ -506,7 +508,7 @@ def _get_subplot_attributes(self):
506508

507509
def _get_subplot_key(self, key):
508510
"""Some keys can have appended integers, this handles that."""
509-
match = re.search(r'(?P<digits>\d+$)', key)
511+
match = _subplot_regex.search(key)
510512
if match:
511513
root_key = key[:match.start()]
512514
if (root_key in self._get_subplot_attributes() and

plotly/graph_reference.py

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,8 @@ def get_attributes_dicts(object_name, parent_object_names=()):
229229
return attributes_dicts
230230

231231

232-
def get_valid_attributes(object_name, parent_object_names=()):
232+
@utils.memoize()
233+
def _get_valid_attributes(object_name, parent_object_names):
233234
attributes = get_attributes_dicts(object_name, parent_object_names)
234235
# These are for documentation and quick lookups. They're just strings.
235236
valid_attributes = set()
@@ -245,6 +246,11 @@ def get_valid_attributes(object_name, parent_object_names=()):
245246
return valid_attributes
246247

247248

249+
def get_valid_attributes(object_name, parent_object_names=()):
250+
# Enforce that parent_object_names is hashable (a tuple).
251+
return _get_valid_attributes(object_name, tuple(parent_object_names))
252+
253+
248254
def get_deprecated_attributes(object_name, parent_object_names=()):
249255
attributes = get_attributes_dicts(object_name, parent_object_names)
250256
# These are for documentation and quick lookups. They're just strings.
@@ -340,21 +346,10 @@ def attribute_path_to_object_names(attribute_container_path):
340346
return tuple(object_names)
341347

342348

343-
def get_role(object_name, attribute, value=None, parent_object_names=()):
344-
"""
345-
Values have types associated with them based on graph_reference.
346-
347-
'data' type values are always kept
348-
'style' values are kept if they're sequences (but not strings)
349-
350-
:param (str) object_name: The name of the object containing 'attribute'.
351-
:param (str) attribute: The attribute we want the `role` of.
352-
:param (*) value: If the value is an array, the return can be different.
353-
:param parent_object_names: An iterable of obj names from graph reference.
354-
:returns: (str) This will be 'data', 'style', or 'info'.
355-
356-
"""
357-
if object_name in TRACE_NAMES and attribute == 'type':
349+
@utils.memoize()
350+
def _get_role(object_name, attribute, value_type, parent_object_names=()):
351+
"""Private, more easily memoized version of get_role."""
352+
if attribute == 'type' and object_name in TRACE_NAMES:
358353
return 'info'
359354
attributes_dicts = get_attributes_dicts(object_name, parent_object_names)
360355
matches = []
@@ -372,12 +367,8 @@ def get_role(object_name, attribute, value=None, parent_object_names=()):
372367
for match in matches:
373368
role = match['role']
374369
array_ok = match.get('arrayOk')
375-
if value is not None and array_ok:
376-
iterable = hasattr(value, '__iter__')
377-
stringy = isinstance(value, six.string_types)
378-
dicty = isinstance(value, dict)
379-
if iterable and not stringy and not dicty:
380-
role = 'data'
370+
if array_ok and value_type == 'array':
371+
role = 'data'
381372
roles.append(role)
382373

383374
# TODO: this is ambiguous until the figure is in place...
@@ -388,6 +379,36 @@ def get_role(object_name, attribute, value=None, parent_object_names=()):
388379
return role
389380

390381

382+
def get_role(object_name, attribute, value=None, parent_object_names=()):
383+
"""
384+
Values have types associated with them based on graph_reference.
385+
386+
'data' type values are always kept
387+
'style' values are kept if they're sequences (but not strings)
388+
389+
:param (str) object_name: The name of the object containing 'attribute'.
390+
:param (str) attribute: The attribute we want the `role` of.
391+
:param (*) value: If the value is an array, the return can be different.
392+
:param parent_object_names: An iterable of obj names from graph reference.
393+
:returns: (str) This will be 'data', 'style', or 'info'.
394+
395+
"""
396+
if value is None:
397+
value_type = 'none'
398+
elif isinstance(value, dict):
399+
value_type = 'dict'
400+
elif isinstance(value, six.string_types):
401+
value_type = 'string'
402+
elif hasattr(value, '__iter__'):
403+
value_type = 'array'
404+
else:
405+
value_type = 'unknown'
406+
407+
# Enforce that parent_object_names is hashable (a tuple).
408+
return _get_role(object_name, attribute, value_type,
409+
tuple(parent_object_names))
410+
411+
391412
def _is_valid_sub_path(path, parent_paths):
392413
"""
393414
Check if a sub path is valid given an iterable of parent paths.

0 commit comments

Comments
 (0)