diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index bc7b358364..e48fec092c 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -14,7 +14,8 @@ from ..core.util import (basestring, sanitize_identifier, group_sanitizer, label_sanitizer, max_range, find_range, dimension_sanitizer, OrderedDict, - bytes_to_unicode, unicode, dt64_to_dt, unique_array) + bytes_to_unicode, unicode, dt64_to_dt, unique_array, + builtins) from .options import Store, StoreOptions from .pprint import PrettyPrinter @@ -479,7 +480,7 @@ class LabelledData(param.Parameterized): _deep_indexable = False - def __init__(self, data, id=None, **params): + def __init__(self, data, id=None, plot_id=None, **params): """ All LabelledData subclasses must supply data to the constructor, which will be held on the .data attribute. @@ -488,6 +489,7 @@ def __init__(self, data, id=None, **params): """ self.data = data self.id = id + self._plot_id = plot_id or builtins.id(self) if isinstance(params.get('label',None), tuple): (alias, long_name) = params['label'] label_sanitizer.add_aliases(**{alias:long_name}) @@ -531,6 +533,7 @@ def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): if data is None and shared_data: data = self.data + settings['plot_id'] = self._plot_id # Apply name mangling for __ attribute pos_args = getattr(self, '_' + type(self).__name__ + '__pos_params', []) return clone_type(data, *args, **{k:v for k,v in settings.items() diff --git a/holoviews/core/layout.py b/holoviews/core/layout.py index 6d2c7ffa7a..3cee742b84 100644 --- a/holoviews/core/layout.py +++ b/holoviews/core/layout.py @@ -363,7 +363,7 @@ def __init__(self, items=None, identifier=None, parent=None, **kwargs): self.__dict__['_max_cols'] = 4 if items and all(isinstance(item, Dimensioned) for item in items): items = self._process_items(items) - params = {p: kwargs.pop(p) for p in list(self.params().keys())+['id'] if p in kwargs} + params = {p: kwargs.pop(p) for p in list(self.params().keys())+['id', 'plot_id'] if p in kwargs} AttrTree.__init__(self, items, identifier, parent, **kwargs) Dimensioned.__init__(self, self.data, **params) diff --git a/holoviews/core/ndmapping.py b/holoviews/core/ndmapping.py index f154cc82d1..bd868f69f2 100644 --- a/holoviews/core/ndmapping.py +++ b/holoviews/core/ndmapping.py @@ -766,6 +766,7 @@ def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): if data is None and shared_data: data = self.data + settings['plot_id'] = self._plot_id # Apply name mangling for __ attribute pos_args = getattr(self, '_' + type(self).__name__ + '__pos_params', []) with item_check(not shared_data and self._check_items): diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 27672ef9f0..3060f677c2 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -856,6 +856,7 @@ def clone(self, data=None, shared_data=True, new_type=None, link_inputs=True, """ if data is None and shared_data: data = self.data + overrides['plot_id'] = self._plot_id clone = super(UniformNdMapping, self).clone(overrides.pop('callback', self.callback), shared_data, new_type, *(data,) + args, **overrides) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index f8d3eca4e1..97a0999b0d 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -22,6 +22,11 @@ except: from collections import OrderedDict +try: + import __builtin__ as builtins +except: + import builtins as builtins + datetime_types = (np.datetime64, dt.datetime) try: diff --git a/holoviews/element/path.py b/holoviews/element/path.py index e79013dea2..1da810cbf2 100644 --- a/holoviews/element/path.py +++ b/holoviews/element/path.py @@ -146,6 +146,8 @@ def clone(self, *args, **overrides): containing the specified args and kwargs. """ settings = dict(self.get_param_values(), **overrides) + if not args: + settings['plot_id'] = self._plot_id return self.__class__(*args, **settings) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index bde1dec4ac..e6fc5e2da6 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -727,11 +727,14 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): # Get data and initialize data source empty = False if self.batched: + current_id = tuple(element.traverse(lambda x: x._plot_id, [Element])) data, mapping = self.get_batched_data(element, ranges, empty) else: data, mapping = self.get_data(element, ranges, empty) + current_id = element._plot_id if source is None: source = self._init_datasource(data) + self.handles['previous_id'] = current_id self.handles['source'] = source properties = self._glyph_properties(plot, style_element, source, ranges) @@ -804,15 +807,16 @@ def update_frame(self, key, ranges=None, plot=None, element=None, empty=False): # Cache frame object id to skip updating data if unchanged previous_id = self.handles.get('previous_id', None) if self.batched: - current_id = sum(element.traverse(lambda x: id(x.data), [Element])) + current_id = tuple(element.traverse(lambda x: x._plot_id, [Element])) else: - current_id = id(element.data) + current_id = element._plot_id self.handles['previous_id'] = current_id - self.static_source = self.dynamic and (current_id == previous_id) + self.static_source = (self.dynamic and (current_id == previous_id)) if self.batched: data, mapping = self.get_batched_data(element, ranges, empty) else: data, mapping = self.get_data(element, ranges, empty) + if not self.static_source: self._update_datasource(source, data) diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index a134b335ac..6e269acbff 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -147,7 +147,7 @@ def sync_sources(self): from the same object. """ get_sources = lambda x: (id(x.current_frame.data), x) - filter_fn = lambda x: (x.shared_datasource and x.current_frame and + filter_fn = lambda x: (x.shared_datasource and x.current_frame is not None and not isinstance(x.current_frame.data, np.ndarray) and 'source' in x.handles) data_sources = self.traverse(get_sources, [filter_fn]) diff --git a/holoviews/plotting/bokeh/tabular.py b/holoviews/plotting/bokeh/tabular.py index fe96c83009..b2cf0eb1af 100644 --- a/holoviews/plotting/bokeh/tabular.py +++ b/holoviews/plotting/bokeh/tabular.py @@ -73,11 +73,12 @@ def current_handles(self): if self.static and not self.dynamic: return handles + + element = self.current_frame previous_id = self.handles.get('previous_id', None) - current_id = id(self.current_frame.data) if self.current_frame else None + current_id = None if self.current_frame is None else element._plot_id for handle in self._update_handles: - if (handle == 'source' and self.dynamic and - current_id == previous_id): + if (handle == 'source' and self.dynamic and current_id == previous_id): continue if handle in self.handles: handles.append(self.handles[handle]) diff --git a/tests/testplotinstantiation.py b/tests/testplotinstantiation.py index 898e0c2a88..596ff85eb6 100644 --- a/tests/testplotinstantiation.py +++ b/tests/testplotinstantiation.py @@ -21,7 +21,7 @@ Scatter3D, Path, Polygons, Bars, Text, BoxWhisker, HLine) from holoviews.element.comparison import ComparisonTestCase -from holoviews.streams import PointerXY, PointerX +from holoviews.streams import Stream, PointerXY, PointerX from holoviews.operation import gridmatrix from holoviews.plotting import comms from holoviews.plotting.util import rgb2hex @@ -46,6 +46,7 @@ from holoviews.plotting.bokeh.util import bokeh_version bokeh_renderer = Store.renderers['bokeh'] from holoviews.plotting.bokeh.callbacks import Callback, PointerXCallback + from bokeh.document import Document from bokeh.models import ( Div, ColumnDataSource, FactorRange, Range1d, Row, Column, ToolbarBox, FixedTicker, FuncTickFormatter @@ -382,6 +383,41 @@ def test_layout_update_visible(self): self.assertFalse(subplot1.handles['glyph_renderer'].visible) self.assertTrue(subplot2.handles['glyph_renderer'].visible) + def test_static_source_optimization_not_skipping_new_element(self): + global data + data = np.ones((5, 5)) + def get_img(test): + global data + data *= test + return Image(data) + stream = Stream.define(str('Test'), test=1)() + dmap = DynamicMap(get_img, streams=[stream]) + plot = bokeh_renderer.get_plot(dmap, doc=Document()) + source = plot.handles['source'] + self.assertEqual(source.data['image'][0].mean(), 1) + stream.event(test=2) + self.assertFalse(plot.static_source) + self.assertEqual(source.data['image'][0].mean(), 2) + self.assertIn(source, plot.current_handles) + + def test_static_source_optimization(self): + global data + data = np.ones((5, 5)) + img = Image(data) + def get_img(test): + global data + data *= test + return img + stream = Stream.define(str('Test'), test=1)() + dmap = DynamicMap(get_img, streams=[stream]) + plot = bokeh_renderer.get_plot(dmap, doc=Document()) + source = plot.handles['source'] + self.assertEqual(source.data['image'][0].mean(), 1) + stream.event(test=2) + self.assertTrue(plot.static_source) + self.assertEqual(source.data['image'][0].mean(), 2) + self.assertNotIn(source, plot.current_handles) + def test_batched_plot(self): overlay = NdOverlay({i: Points(np.arange(i)) for i in range(1, 100)}) plot = bokeh_renderer.get_plot(overlay)