From e2fe50fd88a9cc30f0725fc4f36cd5b82373ff4d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 11 Mar 2020 12:30:31 +0100 Subject: [PATCH 1/3] Add ability to define selected glyphs on bokeh plots --- holoviews/plotting/bokeh/callbacks.py | 2 +- holoviews/plotting/bokeh/chart.py | 12 ++++++++++++ holoviews/plotting/bokeh/geometry.py | 9 +++++++++ holoviews/plotting/bokeh/path.py | 4 ++++ holoviews/plotting/bokeh/plot.py | 18 +++++++++++++++++- holoviews/plotting/bokeh/tabular.py | 12 +++++++----- .../tests/plotting/bokeh/testcallbacks.py | 6 ++++++ .../tests/plotting/bokeh/testpointplot.py | 6 ++++++ 8 files changed, 62 insertions(+), 7 deletions(-) diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index 961515e497..476500754c 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -978,7 +978,7 @@ def _process_msg(self, msg): if isinstance(el, Table): # Ensure that explicitly applied selection does not # trigger new events - sel = el.opts.get('style').kwargs.get('selection') + sel = el.opts.get('plot').kwargs.get('selected') if sel is not None and list(sel) == msg['index']: return {} return self._transform(msg) diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 0080b5d734..7548f2bba5 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -32,6 +32,10 @@ class PointPlot(LegendPlot, ColorbarPlot): jitter = param.Number(default=None, bounds=(0, None), doc=""" The amount of jitter to apply to offset the points along the x-axis.""") + selected = param.List(default=None, doc=""" + The current selection as a list of integers corresponding + to the selected items.""") + # Deprecated parameters color_index = param.ClassSelector(default=None, class_=(basestring, int), @@ -509,6 +513,10 @@ def _init_glyph(self, plot, mapping, properties): class ErrorPlot(ColorbarPlot): + selected = param.List(default=None, doc=""" + The current selection as a list of integers corresponding + to the selected items.""") + selection_display = BokehOverlaySelectionDisplay() style_opts = ([ @@ -715,6 +723,10 @@ class SideSpikesPlot(SpikesPlot): SpikesPlot with useful defaults for plotting adjoined rug plot. """ + selected = param.List(default=None, doc=""" + The current selection as a list of integers corresponding + to the selected items.""") + xaxis = param.ObjectSelector(default='top-bare', objects=['top', 'bottom', 'bare', 'top-bare', 'bottom-bare', None], doc=""" diff --git a/holoviews/plotting/bokeh/geometry.py b/holoviews/plotting/bokeh/geometry.py index 5728cf9085..d8ea055926 100644 --- a/holoviews/plotting/bokeh/geometry.py +++ b/holoviews/plotting/bokeh/geometry.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, division, unicode_literals import numpy as np +import param from ..mixins import GeomMixin from .element import ColorbarPlot, LegendPlot @@ -14,6 +15,10 @@ class SegmentPlot(GeomMixin, ColorbarPlot): (x, y) node of the line. """ + selected = param.List(default=None, doc=""" + The current selection as a list of integers corresponding + to the selected items.""") + selection_display = BokehOverlaySelectionDisplay() style_opts = base_properties + line_properties + ['cmap'] @@ -34,6 +39,10 @@ def _get_factors(self, element, ranges): class RectanglesPlot(GeomMixin, LegendPlot, ColorbarPlot): + selected = param.List(default=None, doc=""" + The current selection as a list of integers corresponding + to the selected items.""") + selection_display = BokehOverlaySelectionDisplay() style_opts = (base_properties + line_properties + fill_properties + diff --git a/holoviews/plotting/bokeh/path.py b/holoviews/plotting/bokeh/path.py index f4ba74df8d..4447c73918 100644 --- a/holoviews/plotting/bokeh/path.py +++ b/holoviews/plotting/bokeh/path.py @@ -155,6 +155,10 @@ def get_batched_data(self, element, ranges=None): class ContourPlot(PathPlot): + selected = param.List(default=None, doc=""" + The current selection as a list of integers corresponding + to the selected items.""") + show_legend = param.Boolean(default=False, doc=""" Whether to show legend for the plot.""") diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index 861e061096..9038622b02 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -105,7 +105,15 @@ def _init_datasource(self, data): Initializes a data source to be passed into the bokeh glyph. """ data = self._postprocess_data(data) - return ColumnDataSource(data=data) + cds = ColumnDataSource(data=data) + if hasattr(self, 'selected'): + from .callbacks import Selection1DCallback + cds.selected.indices = self.selected + for cb in self.callbacks: + if isinstance(cb, Selection1DCallback): + for s in cb.streams: + s.update(index=self.selected) + return cds def _postprocess_data(self, data): @@ -153,6 +161,14 @@ def _update_datasource(self, source, data): else: source.data.update(data) + if hasattr(self, 'selected'): + from .callbacks import Selection1DCallback + source.selected.indices = self.selected + for cb in self.callbacks: + if isinstance(cb, Selection1DCallback): + for s in cb.streams: + s.update(index=self.selected) + def _update_callbacks(self, plot): """ Iterates over all subplots and updates existing CustomJS diff --git a/holoviews/plotting/bokeh/tabular.py b/holoviews/plotting/bokeh/tabular.py index d036d566a5..bd19fa1d23 100644 --- a/holoviews/plotting/bokeh/tabular.py +++ b/holoviews/plotting/bokeh/tabular.py @@ -29,11 +29,15 @@ class TablePlot(BokehPlot, GenericElementPlot): height = param.Number(default=300) + selected = param.List(default=None, doc=""" + The current selection as a list of integers corresponding + to the selected items.""") + width = param.Number(default=400) selection_display = TabularSelectionDisplay() - style_opts = ['row_headers', 'selectable', 'selected', 'editable', + style_opts = ['row_headers', 'selectable', 'editable', 'sortable', 'fit_columns', 'scroll_to_selection', 'index_position', 'visible'] @@ -69,8 +73,8 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): source = self._init_datasource(data) self.handles['source'] = self.handles['cds'] = source self.handles['selected'] = source.selected - if 'selected' in style: - source.selected.indices = list(style['selected']) + if self.selected is not None: + source.selected.indices = self.selected columns = self._get_columns(element, data) style['reorderable'] = False @@ -140,6 +144,4 @@ def update_frame(self, key, ranges=None, plot=None): data, _, style = self.get_data(element, ranges, style) columns = self._get_columns(element, data) self.handles['table'].columns = columns - if 'selected' in style: - source.selected.indices = list(style['selected']) self._update_datasource(source, data) diff --git a/holoviews/tests/plotting/bokeh/testcallbacks.py b/holoviews/tests/plotting/bokeh/testcallbacks.py index ed04bd83d4..5f44768eef 100644 --- a/holoviews/tests/plotting/bokeh/testcallbacks.py +++ b/holoviews/tests/plotting/bokeh/testcallbacks.py @@ -120,6 +120,12 @@ def test_callback_cleanup(self): self.assertFalse(bool(stream._subscribers)) self.assertFalse(bool(Callback._callbacks)) + def test_selection1d_syncs_to_selected(self): + points = Points([(0, 0), (1, 1), (2, 2)]).opts(selected=[0, 2]) + stream = Selection1D(source=points) + plot = bokeh_renderer.get_plot(points) + self.assertEqual(stream.index, [0, 2]) + class TestResetCallback(CallbackTestCase): diff --git a/holoviews/tests/plotting/bokeh/testpointplot.py b/holoviews/tests/plotting/bokeh/testpointplot.py index 46cd974c9b..e64e021431 100644 --- a/holoviews/tests/plotting/bokeh/testpointplot.py +++ b/holoviews/tests/plotting/bokeh/testpointplot.py @@ -322,6 +322,12 @@ def test_points_datetime_hover(self): hover = plot.handles['hover'] self.assertEqual(hover.tooltips, [('x', '@{x}'), ('y', '@{y}'), ('date', '@{date_dt_strings}')]) + def test_points_selected(self): + points = Points([(0, 0), (1, 1), (2, 2)]).opts(selected=[0, 2]) + plot = bokeh_renderer.get_plot(points) + cds = plot.handles['cds'] + self.assertEqual(cds.selected.indices, [0, 2]) + ########################### # Styling mapping # ########################### From 4a4bb4ac298daa33e372da712d02dc775cb25811 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 11 Mar 2020 12:50:23 +0100 Subject: [PATCH 2/3] Fix when selected is None --- holoviews/plotting/bokeh/plot.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index 9038622b02..a7e4280bab 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -100,19 +100,22 @@ def get_data(self, element, ranges, style): raise NotImplementedError + def _update_selected(self, cds): + from .callbacks import Selection1DCallback + cds.selected.indices = self.selected + for cb in self.callbacks: + if isinstance(cb, Selection1DCallback): + for s in cb.streams: + s.update(index=self.selected) + def _init_datasource(self, data): """ Initializes a data source to be passed into the bokeh glyph. """ data = self._postprocess_data(data) cds = ColumnDataSource(data=data) - if hasattr(self, 'selected'): - from .callbacks import Selection1DCallback - cds.selected.indices = self.selected - for cb in self.callbacks: - if isinstance(cb, Selection1DCallback): - for s in cb.streams: - s.update(index=self.selected) + if hasattr(self, 'selected') and self.selected is not None: + self._update_selected(cds) return cds @@ -161,13 +164,9 @@ def _update_datasource(self, source, data): else: source.data.update(data) - if hasattr(self, 'selected'): - from .callbacks import Selection1DCallback - source.selected.indices = self.selected - for cb in self.callbacks: - if isinstance(cb, Selection1DCallback): - for s in cb.streams: - s.update(index=self.selected) + if hasattr(self, 'selected') and self.selected is not None: + self._update_selected(source) + def _update_callbacks(self, plot): """ From 98a0f0dc5817f439b8457d04ad8670f5015b7841 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 11 Mar 2020 13:25:44 +0100 Subject: [PATCH 3/3] Fixed flake --- holoviews/tests/plotting/bokeh/testcallbacks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/tests/plotting/bokeh/testcallbacks.py b/holoviews/tests/plotting/bokeh/testcallbacks.py index 5f44768eef..fc2fdbb8b9 100644 --- a/holoviews/tests/plotting/bokeh/testcallbacks.py +++ b/holoviews/tests/plotting/bokeh/testcallbacks.py @@ -123,7 +123,7 @@ def test_callback_cleanup(self): def test_selection1d_syncs_to_selected(self): points = Points([(0, 0), (1, 1), (2, 2)]).opts(selected=[0, 2]) stream = Selection1D(source=points) - plot = bokeh_renderer.get_plot(points) + bokeh_renderer.get_plot(points) self.assertEqual(stream.index, [0, 2])