diff --git a/holoviews/plotting/bokeh/bokehwidgets.js b/holoviews/plotting/bokeh/bokehwidgets.js index df0c39469f..eec4065e2e 100644 --- a/holoviews/plotting/bokeh/bokehwidgets.js +++ b/holoviews/plotting/bokeh/bokehwidgets.js @@ -25,20 +25,8 @@ var BokehMethods = { var data = this.frames[current]; } if (data !== undefined) { - if (data.root !== undefined) { - var doc = Bokeh.index[data.root].model.document; - } - $.each(data.data, function(i, value) { - if (data.root !== undefined) { - var ds = doc.get_model_by_id(value.id); - } else { - var ds = Bokeh.Collections(value.type).get(value.id); - } - if (ds != undefined) { - ds.set(value.data); - ds.trigger('change'); - } - }); + var doc = Bokeh.index[data.root].model.document; + doc.apply_json_patch(data.patch); } }, dynamic_update : function(current){ diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index b2fd77f440..d3d847ea5c 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -4,14 +4,10 @@ import param from ...core.data import ArrayColumns -from .renderer import bokeh_version -from .util import models_to_json, bokeh_version +from .util import compute_static_patch, models_to_json from bokeh.models import CustomJS, TapTool, ColumnDataSource -if bokeh_version < '0.11': - from bokeh.protocol import serialize_json -else: - from bokeh.core.json_encoder import serialize_json +from bokeh.core.json_encoder import serialize_json class Callback(param.ParameterizedFunction): @@ -71,20 +67,11 @@ class Callback(param.ParameterizedFunction): function callback(msg){ if (msg.msg_type == "execute_result") { var data = JSON.parse(msg.content.data['text/plain'].slice(1, -1)); - if (data.root !== undefined) { - var doc = Bokeh.index[data.root].model.document; + if (data !== undefined) { + console.log(data.root) + var doc = Bokeh.index[data.root].model.document; + doc.apply_json_patch(data.patch); } - $.each(data.data, function(i, value) { - if (data.root !== undefined) { - var ds = doc.get_model_by_id(value.id); - } else { - var ds = Bokeh.Collections(value.type).get(value.id); - } - if (ds != undefined) { - ds.set(value.data); - ds.trigger('change'); - } - }); } else { console.log("Python callback returned unexpected message:", msg) } @@ -145,14 +132,13 @@ def update(self, data, chained=False): return self.serialize(objects) - def serialize(self, objects): + def serialize(self, models): """ Serializes any Bokeh plot objects passed to it as a list. """ - data = dict(data=models_to_json(objects)) - if bokeh_version >= '0.11': - plot = self.plots[0] - data['root'] = plot.state._id + plot = self.plots[0] + patch = compute_static_patch(plot.document, models) + data = dict(root=plot.state._id, patch=patch) return serialize_json(data) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 44ad4a147a..1ab310d164 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -23,7 +23,7 @@ from ..util import dynamic_update from .callbacks import Callbacks from .plot import BokehPlot -from .util import bokeh_version, mpl_to_bokeh, convert_datetime, update_plot +from .util import mpl_to_bokeh, convert_datetime, update_plot # Define shared style properties for bokeh plots @@ -280,9 +280,7 @@ def _plot_properties(self, key, plot, element): if self.show_title: plot_props['title'] = self._format_title(key, separator=' ') if self.bgcolor: - bg_attr = 'background_fill' - if bokeh_version > '0.11': bg_attr += '_color' - plot_props[bg_attr] = self.bgcolor + plot_props['background_fill_color'] = self.bgcolor if self.border is not None: for p in ['left', 'right', 'top', 'bottom']: plot_props['min_border_'+p] = self.border @@ -675,10 +673,7 @@ def _process_legend(self): if legend_fontsize: plot.legend[0].label_text_font_size = legend_fontsize - if bokeh_version < '0.11': - plot.legend.orientation = self.legend_position - else: - plot.legend.location = self.legend_position + plot.legend.location = self.legend_position legends = plot.legend[0].legends new_legends = [] for label, l in legends: diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index 12793554f6..895773cc04 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -10,7 +10,7 @@ AdjointLayout, NdLayout, Empty, GridSpace, HoloMap) from ...core import traversal from ...core.options import Compositor -from ...core.util import basestring +from ...core.util import basestring, wrap_tuple from ...element import Histogram from ..plot import DimensionedPlot, GenericCompositePlot, GenericLayoutPlot from ..util import get_dynamic_mode, initialize_sampled @@ -257,7 +257,7 @@ def initialize_plot(self, ranges=None, plots=[]): passed_plots = list(plots) for i, coord in enumerate(self.layout.keys(full_grid=True)): r = i % self.cols - subplot = self.subplots.get(coord, None) + subplot = self.subplots.get(wrap_tuple(coord), None) if subplot is not None: plot = subplot.initialize_plot(ranges=ranges, plots=passed_plots) plots[r].append(plot) diff --git a/holoviews/plotting/bokeh/renderer.py b/holoviews/plotting/bokeh/renderer.py index 17a883bd37..a748427f97 100644 --- a/holoviews/plotting/bokeh/renderer.py +++ b/holoviews/plotting/bokeh/renderer.py @@ -3,7 +3,7 @@ from ...core import Store, HoloMap from ..renderer import Renderer, MIME_TYPES from .widgets import BokehScrubberWidget, BokehSelectionWidget -from .util import bokeh_version, models_to_json +from .util import compute_static_patch import param from param.parameterized import bothmethod @@ -12,12 +12,12 @@ from bokeh.embed import notebook_div from bokeh.io import load_notebook from bokeh.resources import CDN, INLINE +from bokeh.io import _CommsHandle +from bokeh.util.notebook import get_comms -if bokeh_version < '0.11': - from bokeh.protocol import serialize_json -else: - from bokeh.core.json_encoder import serialize_json - from bokeh.model import _ModelInDocument as add_to_document +from bokeh.core.json_encoder import serialize_json +from bokeh.model import _ModelInDocument as add_to_document +from bokeh.document import Document class BokehRenderer(Renderer): @@ -60,25 +60,21 @@ def __call__(self, obj, fmt=None): elif fmt == 'json': plotobjects = [h for handles in plot.traverse(lambda x: x.current_handles) for h in handles] - data = dict(data=[]) - if bokeh_version >= '0.11': - data['root'] = plot.state._id - data['data'] = models_to_json(plotobjects) + patch = compute_static_patch(plot.document, plotobjects) + data = dict(root=plot.state._id, patch=patch) return self._apply_post_render_hooks(serialize_json(data), obj, fmt), info def figure_data(self, plot, fmt='html', **kwargs): - if bokeh_version >= '0.11': - doc_handler = add_to_document(plot.state) - with doc_handler: - doc = doc_handler._doc - comms_target = str(uuid.uuid4()) - doc.last_comms_target = comms_target - div = notebook_div(plot.state, comms_target) - plot.document = doc - return div - else: - return notebook_div(plot.state) + doc_handler = add_to_document(plot.state) + with doc_handler: + doc = doc_handler._doc + comms_target = str(uuid.uuid4()) + doc.last_comms_target = comms_target + div = notebook_div(plot.state, comms_target) + plot.document = doc + doc.add_root(plot.state) + return div @classmethod diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py index e237f72cb7..1597ee3d6b 100644 --- a/holoviews/plotting/bokeh/util.py +++ b/holoviews/plotting/bokeh/util.py @@ -11,12 +11,9 @@ import bokeh bokeh_version = LooseVersion(bokeh.__version__) -if bokeh_version < '0.11': - from bokeh.enums import Palette - from bokeh.plotting import Plot -else: - from bokeh.core.enums import Palette - from bokeh.models.plots import Plot +from bokeh.core.enums import Palette +from bokeh.document import Document +from bokeh.models.plots import Plot from bokeh.models import GlyphRenderer from bokeh.plotting import Figure @@ -139,18 +136,51 @@ def models_to_json(models): continue else: ids.append(plotobj.ref['id']) - if bokeh_version < '0.11': - json = plotobj.vm_serialize(changed_only=True) - else: - json = plotobj.to_json(False) - json.pop('tool_events', None) - json.pop('renderers', None) - json_data.append({'id': plotobj.ref['id'], - 'type': plotobj.ref['type'], - 'data': json}) + json = plotobj.to_json(False) + json.pop('tool_events', None) + json.pop('renderers', None) + json_data.append({'id': plotobj.ref['id'], + 'type': plotobj.ref['type'], + 'data': json}) return json_data +def refs(json): + """ + Finds all the references to other objects in the json + representation of a bokeh Document. + """ + result = {} + for obj in json['roots']['references']: + result[obj['id']] = obj + return result + + +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. + """ + json = document.to_json() + references = refs(json) + requested_updates = [m.ref['id'] for m in models] + + value_refs = {} + events = [] + for ref_id, obj in references.items(): + if ref_id not in requested_updates: + continue + for key, val in obj['attributes'].items(): + event = Document._event_for_attribute_change(references, + obj, key, val, + value_refs) + events.append(event) + return dict(events=events, references=list(value_refs.values())) + + def hsv_to_rgb(hsv): """ Vectorized HSV to RGB conversion, adapted from: diff --git a/holoviews/plotting/bokeh/widgets.py b/holoviews/plotting/bokeh/widgets.py index e743220fa2..470fdad079 100644 --- a/holoviews/plotting/bokeh/widgets.py +++ b/holoviews/plotting/bokeh/widgets.py @@ -1,14 +1,12 @@ import json -from .util import bokeh_version -from ..widgets import NdWidget, SelectionWidget, ScrubberWidget - import param import bokeh from bokeh.io import Document -if bokeh_version >= '0.11': - from bokeh.io import _CommsHandle - from bokeh.util.notebook import get_comms +from bokeh.io import _CommsHandle +from bokeh.util.notebook import get_comms + +from ..widgets import NdWidget, SelectionWidget, ScrubberWidget class BokehWidget(NdWidget): @@ -39,15 +37,13 @@ def _plot_figure(self, idx, fig_format='json'): first call and """ self.plot.update(idx) - if self.embed or fig_format == 'html' or bokeh_version < '0.11': - return self.renderer.html(self.plot, fig_format) + if self.embed or fig_format == 'html': + html = self.renderer.html(self.plot, fig_format) + return html else: - doc = self.plot.document - if hasattr(doc, 'last_comms_handle'): handle = doc.last_comms_handle else: - doc.add_root(self.plot.state) handle = _CommsHandle(get_comms(doc.last_comms_target), doc, doc.to_json()) doc.last_comms_handle = handle