From 380d263100a4a522982c0468e4821bbbccdc4b8d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 8 Apr 2017 18:06:58 +0100 Subject: [PATCH] Correctly sync shared datasources --- holoviews/plotting/bokeh/plot.py | 14 ++++-- holoviews/plotting/bokeh/util.py | 26 +++++++++++ tests/testplotinstantiation.py | 75 +++++++++++++++++++++++++++++++- 3 files changed, 110 insertions(+), 5 deletions(-) diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index dd8e5a5865..046321e492 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -17,7 +17,7 @@ from ..util import get_dynamic_mode, initialize_sampled from .renderer import BokehRenderer from .util import (bokeh_version, layout_padding, pad_plots, - filter_toolboxes, make_axis) + filter_toolboxes, make_axis, update_shared_sources) if bokeh_version >= '0.12': from bokeh.layouts import gridplot @@ -153,6 +153,8 @@ def sync_sources(self): and 'source' in x.handles) data_sources = self.traverse(get_sources, [filter_fn]) grouped_sources = groupby(sorted(data_sources, key=lambda x: x[0]), lambda x: x[0]) + shared_sources = [] + source_cols = {} for _, group in grouped_sources: group = list(group) if len(group) > 1: @@ -169,6 +171,10 @@ def sync_sources(self): else: renderer.update(source=new_source) plot.handles['source'] = new_source + shared_sources.append(new_source) + source_cols[id(new_source)] = [c for c in new_source.data] + self.handles['shared_sources'] = shared_sources + self.handles['source_cols'] = source_cols @@ -441,7 +447,7 @@ def _make_axes(self, plot): plot = Column(*models) return plot - + @update_shared_sources def update_frame(self, key, ranges=None): """ Update the internal state of the Plot to represent the given @@ -450,7 +456,7 @@ def update_frame(self, key, ranges=None): """ ranges = self.compute_ranges(self.layout, key, ranges) for coord in self.layout.keys(full_grid=True): - subplot = self.subplots.get(coord, None) + subplot = self.subplots.get(wrap_tuple(coord), None) if subplot is not None: subplot.update_frame(key, ranges) title = self._get_title(key) @@ -692,7 +698,7 @@ def initialize_plot(self, plots=None, ranges=None): return self.handles['plot'] - + @update_shared_sources def update_frame(self, key, ranges=None): """ Update the internal state of the Plot to represent the given diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py index ec7e993066..b2d9b87171 100644 --- a/holoviews/plotting/bokeh/util.py +++ b/holoviews/plotting/bokeh/util.py @@ -612,3 +612,29 @@ def filter_batched_data(data, mapping): del data[v] except: pass + + +def update_shared_sources(f): + """ + Context manager to ensures data sources shared between multiple + plots are cleared and updated appropriately avoiding warnings and + allowing empty frames on subplots. Expects a list of + shared_sources and a mapping of the columns expected columns for + each source in the plots handles. + """ + def wrapper(self, *args, **kwargs): + source_cols = self.handles.get('source_cols', {}) + shared_sources = self.handles.get('shared_sources', []) + for source in shared_sources: + source.data.clear() + + ret = f(self, *args, **kwargs) + + for source in shared_sources: + expected = source_cols[id(source)] + found = [c for c in expected if c in source.data] + empty = np.full_like(source.data[found[0]], np.NaN) if found else [] + patch = {c: empty for c in expected if c not in source.data} + source.data.update(patch) + return ret + return wrapper diff --git a/tests/testplotinstantiation.py b/tests/testplotinstantiation.py index 8d6e27f7d4..ed31bf74f4 100644 --- a/tests/testplotinstantiation.py +++ b/tests/testplotinstantiation.py @@ -11,7 +11,7 @@ import param import numpy as np -from holoviews import (Dimension, Overlay, DynamicMap, Store, +from holoviews import (Dimension, Overlay, DynamicMap, Store, Dataset, NdOverlay, GridSpace, HoloMap, Layout, Cycle) from holoviews.core.util import pd from holoviews.element import (Curve, Scatter, Image, VLine, Points, @@ -1224,6 +1224,79 @@ def test_shared_axes_disable(self): self.assertEqual((x_range.start, x_range.end), (-.5, .5)) self.assertEqual((y_range.start, y_range.end), (-.5, .5)) + def test_layout_shared_source_synced_update(self): + hmap = HoloMap({i: Dataset({chr(65+j): np.random.rand(i+2) + for j in range(4)}, kdims=['A', 'B', 'C', 'D']) + for i in range(3)}) + + # Create two holomaps of points sharing the same data source + hmap1= hmap.map(lambda x: Points(x.clone(kdims=['A', 'B'])), Dataset) + hmap2 = hmap.map(lambda x: Points(x.clone(kdims=['D', 'C'])), Dataset) + + # Pop key (1,) for one of the HoloMaps and make Layout + hmap2.pop((1,)) + layout = (hmap1 + hmap2)(plot=dict(shared_datasource=True)) + + # Get plot + plot = bokeh_renderer.get_plot(layout) + + # Check plot created shared data source and recorded expected columns + sources = plot.handles.get('shared_sources', []) + source_cols = plot.handles.get('source_cols', {}) + self.assertEqual(len(sources), 1) + source = sources[0] + data = source.data + cols = source_cols[id(source)] + self.assertEqual(set(cols), {'A', 'B', 'C', 'D'}) + + # Ensure the source contains the expected columns + self.assertEqual(set(data.keys()), {'A', 'B', 'C', 'D'}) + + # Update to key (1,) and check the source contains data + # corresponding to hmap1 and filled in NaNs for hmap2, + # which was popped above + plot.update((1,)) + self.assertEqual(data['A'], hmap1[1].dimension_values(0)) + self.assertEqual(data['B'], hmap1[1].dimension_values(1)) + self.assertEqual(data['C'], np.full_like(hmap1[1].dimension_values(0), np.NaN)) + self.assertEqual(data['D'], np.full_like(hmap1[1].dimension_values(0), np.NaN)) + + def test_grid_shared_source_synced_update(self): + hmap = HoloMap({i: Dataset({chr(65+j): np.random.rand(i+2) + for j in range(4)}, kdims=['A', 'B', 'C', 'D']) + for i in range(3)}) + + # Create two holomaps of points sharing the same data source + hmap1= hmap.map(lambda x: Points(x.clone(kdims=['A', 'B'])), Dataset) + hmap2 = hmap.map(lambda x: Points(x.clone(kdims=['D', 'C'])), Dataset) + + # Pop key (1,) for one of the HoloMaps and make GridSpace + hmap2.pop(1) + grid = GridSpace({0: hmap1, 2: hmap2}, kdims=['X'])(plot=dict(shared_datasource=True)) + + # Get plot + plot = bokeh_renderer.get_plot(grid) + + # Check plot created shared data source and recorded expected columns + sources = plot.handles.get('shared_sources', []) + source_cols = plot.handles.get('source_cols', {}) + self.assertEqual(len(sources), 1) + source = sources[0] + data = source.data + cols = source_cols[id(source)] + self.assertEqual(set(cols), {'A', 'B', 'C', 'D'}) + + # Ensure the source contains the expected columns + self.assertEqual(set(data.keys()), {'A', 'B', 'C', 'D'}) + + # Update to key (1,) and check the source contains data + # corresponding to hmap1 and filled in NaNs for hmap2, + # which was popped above + plot.update((1,)) + self.assertEqual(data['A'], hmap1[1].dimension_values(0)) + self.assertEqual(data['B'], hmap1[1].dimension_values(1)) + self.assertEqual(data['C'], np.full_like(hmap1[1].dimension_values(0), np.NaN)) + self.assertEqual(data['D'], np.full_like(hmap1[1].dimension_values(0), np.NaN)) class TestPlotlyPlotInstantiation(ComparisonTestCase):