Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved bokeh json patch computation #807

Merged
merged 2 commits into from
Aug 15, 2016
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 59 additions & 27 deletions holoviews/plotting/bokeh/util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import itertools
from distutils.version import LooseVersion

from collections import defaultdict

import numpy as np

try:
Expand Down Expand Up @@ -40,12 +41,11 @@
# and can therefore be safely ignored. Axes currently fail saying
# LinearAxis.computed_bounds cannot be updated
IGNORED_MODELS = ['LinearAxis', 'LogAxis', 'DatetimeAxis',
'CategoricalAxis' 'BasicTicker', 'BasicTickFormatter',
'CategoricalAxis', 'BasicTicker', 'BasicTickFormatter',
'FixedTicker', 'FuncTickFormatter', 'LogTickFormatter']

# Where to look for the ignored models
LOCATIONS = ['new', 'below', 'right', 'left',
'renderers', 'above', 'attributes', 'plot', 'ticker']
# List of attributes that can safely be dropped from the references
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to get rid of this!

IGNORED_ATTRIBUTES = ['data', 'palette']

# Model priority order to ensure some types are updated before others
MODEL_PRIORITY = ['Range1d', 'Title', 'Image', 'LinearColorMapper',
Expand Down Expand Up @@ -176,22 +176,56 @@ def refs(json):
return result


def compute_static_patch(document, models, json=None):
def get_ids(obj):
"""
Returns a list of all ids in the supplied object. Useful for
determining if a json representation contains references to other
objects. Since only the references between objects are required
this allows determining whether a particular branch of the json
representation is required.
"""
ids = []
if isinstance(obj, list):
ids = [get_ids(o) for o in obj]
elif isinstance(obj, dict):
ids = [(v,) if k == 'id' else get_ids(v)
for k, v in obj.items()]
return list(itertools.chain(*ids))


def compute_static_patch(document, models):
"""
Computes a patch to update an existing document without
diffing the json first, making it suitable for static updates
between arbitrary frames. Note that this only supports changed
attributes and will break if new models have been added since
the plot was first created.

A static patch consists of two components:

1) The events: Contain references to particular model attributes
along with the updated value.
2) The references: Contain a list of all references required to
resolve the update events.

This function cleans up the events and references that are sent
to ensure that only the data that is required is sent. It does so
by a) filtering the events and references for the models that have
been requested to be updated and b) cleaning up the references to
ensure that only the references between objects are sent without
duplicating any of the data.
"""
references = refs(json if json else document.to_json())
requested_updates = [m.ref['id'] for m in models]
references = refs(document.to_json())
model_ids = [m.ref['id'] for m in models]

requested_updates = []
value_refs = {}
events = []
update_types = defaultdict(list)
for ref_id, obj in references.items():
if ref_id not in requested_updates:
if ref_id in model_ids:
requested_updates += get_ids(obj)
else:
continue
if obj['type'] in MODEL_PRIORITY:
priority = MODEL_PRIORITY.index(obj['type'])
Expand All @@ -203,38 +237,36 @@ def compute_static_patch(document, models, json=None):
value_refs)
events.append((priority, event))
update_types[obj['type']].append(key)
events = [delete_refs(e, LOCATIONS, IGNORED_MODELS)
events = [delete_refs(e, IGNORED_MODELS)
for _, e in sorted(events, key=lambda x: x[0])]
value_refs = {ref_id: delete_refs(val, LOCATIONS, IGNORED_MODELS)
events = [e for e in events if all(i in requested_updates for i in get_ids(e))]
value_refs = {ref_id: delete_refs(val, IGNORED_MODELS, IGNORED_ATTRIBUTES)
for ref_id, val in value_refs.items()}
references = [val for val in value_refs.values()
if val is not None]
if val not in [None, {}]]
return dict(events=events, references=references)


def delete_refs(obj, locs, delete):
def delete_refs(obj, models=[], attributes=[]):
"""
Delete all references to specific model types by recursively
traversing the object and looking for the models to be deleted in
the supplied locations.

Note: Can be deleted once bokeh stops raising errors when updating
LinearAxis.computed_bounds
Recursively traverses the object and looks for models and model
attributes to be deleted.
"""
if isinstance(obj, dict):
if 'type' in obj and obj['type'] in delete:
if 'type' in obj and obj['type'] in models:
return None
new_obj = {}
for k, v in list(obj.items()):
if k in locs:
ref = delete_refs(v, locs, delete)
if ref is not None:
new_obj[k] = ref
else:
new_obj[k] = v
# Drop unneccessary attributes, i.e. those that do not
# contain references to other objects.
if k in attributes or (k == 'attributes' and not get_ids(v)):
continue
ref = delete_refs(v, models, attributes)
if ref is not None:
new_obj[k] = ref
return new_obj
elif isinstance(obj, list):
objs = [delete_refs(v, locs, delete) for v in obj]
objs = [delete_refs(v, models, attributes) for v in obj]
return [o for o in objs if o is not None]
else:
return obj
Expand Down