diff --git a/holoviews/plotting/bokeh/widgets.py b/holoviews/plotting/bokeh/widgets.py index 9a7a00e39a..39b51658d1 100644 --- a/holoviews/plotting/bokeh/widgets.py +++ b/holoviews/plotting/bokeh/widgets.py @@ -8,7 +8,7 @@ import numpy as np from bokeh.io import _CommsHandle from bokeh.util.notebook import get_comms -from bokeh.models.widgets import Select, Slider, AutocompleteInput, TextInput +from bokeh.models.widgets import Select, Slider, AutocompleteInput, TextInput, Div from bokeh.layouts import layout, gridplot, widgetbox, row, column from ...core import Store, NdMapping, OrderedDict @@ -27,14 +27,9 @@ class BokehServerWidgets(param.Parameterized): and dropdown widgets letting you select non-numeric values. """ - basejs = param.String(default=None, precedence=-1, doc=""" - Defines the local CSS file to be loaded for this widget.""") - - extensionjs = param.String(default=None, precedence=-1, doc=""" - Optional javascript extension file for a particular backend.""") - - css = param.String(default=None, precedence=-1, doc=""" - Defines the local CSS file to be loaded for this widget.""") + editable = param.Boolean(default=False, doc=""" + Whether the slider text fields should be editable. Disabled + by default for a more compact widget layout.""") position = param.ObjectSelector(default='right', objects=['right', 'left', 'above', 'below']) @@ -43,9 +38,18 @@ class BokehServerWidgets(param.Parameterized): objects=['fixed', 'stretch_both', 'scale_width', 'scale_height', 'scale_both']) - width = param.Integer(default=200, doc=""" + width = param.Integer(default=250, doc=""" Width of the widget box in pixels""") + basejs = param.String(default=None, precedence=-1, doc=""" + Defines the local CSS file to be loaded for this widget.""") + + extensionjs = param.String(default=None, precedence=-1, doc=""" + Optional javascript extension file for a particular backend.""") + + css = param.String(default=None, precedence=-1, doc=""" + Defines the local CSS file to be loaded for this widget.""") + def __init__(self, plot, renderer=None, **params): super(BokehServerWidgets, self).__init__(**params) self.plot = plot @@ -75,7 +79,7 @@ def __init__(self, plot, renderer=None, **params): @classmethod - def create_widget(self, dim, holomap=None): + def create_widget(self, dim, holomap=None, editable=False): """" Given a Dimension creates bokeh widgets to select along that dimension. For numeric data a slider widget is created which @@ -92,8 +96,11 @@ def create_widget(self, dim, holomap=None): if all(isnumeric(v) for v in dim.values): values = dim.values labels = [unicode(dim.pprint_value(v)) for v in dim.values] - label = AutocompleteInput(value=labels[0], completions=labels, - title=dim.pprint_label) + if editable: + label = AutocompleteInput(value=labels[0], completions=labels, + title=dim.pprint_label) + else: + label = Div(text='%s' % dim.pprint_value_string(labels[0])) widget = Slider(value=0, end=len(dim.values)-1, title=None, step=1) mapping = list(zip(values, labels)) else: @@ -109,7 +116,10 @@ def create_widget(self, dim, holomap=None): step = 1 else: step = 10**((round(math.log10(dim_range))-3)) - label = TextInput(value=str(start), title=dim.pprint_label) + if editable: + label = TextInput(value=str(start), title=dim.pprint_label) + else: + label = Div(text='%s' % dim.pprint_value_string(start)) widget = Slider(value=start, start=start, end=end, step=step, title=None) else: @@ -117,8 +127,11 @@ def create_widget(self, dim, holomap=None): list(unique_array(holomap.dimension_values(dim.name)))) labels = [dim.pprint_value(v) for v in values] if isinstance(values[0], np.datetime64) or isnumeric(values[0]): - label = AutocompleteInput(value=labels[0], completions=labels, - title=dim.pprint_label) + if editable: + label = AutocompleteInput(value=labels[0], completions=labels, + title=dim.pprint_label) + else: + label = Div(text='%s' % (dim.pprint_value_string(labels[0]))) widget = Slider(value=0, end=len(values)-1, title=None, step=1) else: widget = Select(title=dim.pprint_label, value=values[0], @@ -136,8 +149,8 @@ def get_widgets(self): mappings = {} for dim in self.mock_obj.kdims: holomap = None if self.plot.dynamic else self.mock_obj - widget, label, mapping = self.create_widget(dim, holomap) - if label is not None: + widget, label, mapping = self.create_widget(dim, holomap, self.editable) + if label is not None and not isinstance(label, Div): label.on_change('value', partial(self.on_change, dim, 'label')) widget.on_change('value', partial(self.on_change, dim, 'widget')) widgets[dim.pprint_label] = (label, widget) @@ -184,21 +197,27 @@ def update(self): label, widget = self.widgets[dim_label] if widget_type == 'label': if isinstance(label, AutocompleteInput): - value = self.reverse_lookups[dim_label][new] + value = [new] widget.value = value else: widget.value = float(new) elif label: - if isinstance(label, AutocompleteInput): - text = self.lookups[dim_label][new] + lookups = self.lookups.get(dim_label) + if not self.editable: + if lookups: + new = list(lookups.keys())[widget.value] + label.text = '%s' % dim.pprint_value_string(new) + elif isinstance(label, AutocompleteInput): + text = lookups[new] label.value = text else: label.value = dim.pprint_value(new) key = [] for dim, (label, widget) in self.widgets.items(): - if label and isinstance(label, AutocompleteInput): - val = list(self.lookups[dim].keys())[widget.value] + lookups = self.lookups.get(dim) + if label and lookups: + val = list(lookups.keys())[widget.value] else: val = widget.value key.append(val) diff --git a/tests/testbokehwidgets.py b/tests/testbokehwidgets.py index fc7afdc616..391c83185b 100644 --- a/tests/testbokehwidgets.py +++ b/tests/testbokehwidgets.py @@ -10,7 +10,7 @@ from holoviews.plotting.bokeh.widgets import BokehServerWidgets from holoviews.plotting.bokeh.util import bokeh_version - from bokeh.models.widgets import Select, Slider, AutocompleteInput, TextInput + from bokeh.models.widgets import Select, Slider, AutocompleteInput, TextInput, Div except: BokehServerWidgets = None @@ -23,7 +23,7 @@ def setUp(self): def test_bokeh_server_dynamic_range_int(self): dim = Dimension('x', range=(3, 11)) - widget, label, mapping = BokehServerWidgets.create_widget(dim) + widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=True) self.assertIsInstance(widget, Slider) self.assertEqual(widget.value, 3) self.assertEqual(widget.start, 3) @@ -36,7 +36,7 @@ def test_bokeh_server_dynamic_range_int(self): def test_bokeh_server_dynamic_range_float(self): dim = Dimension('x', range=(3.1, 11.2)) - widget, label, mapping = BokehServerWidgets.create_widget(dim) + widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=True) self.assertIsInstance(widget, Slider) self.assertEqual(widget.value, 3.1) self.assertEqual(widget.start, 3.1) @@ -47,10 +47,22 @@ def test_bokeh_server_dynamic_range_float(self): self.assertEqual(label.value, '3.1') self.assertIs(mapping, None) + def test_bokeh_server_dynamic_range_not_editable(self): + dim = Dimension('x', range=(3.1, 11.2)) + widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=False) + self.assertIsInstance(widget, Slider) + self.assertEqual(widget.value, 3.1) + self.assertEqual(widget.start, 3.1) + self.assertEqual(widget.end, 11.2) + self.assertEqual(widget.step, 0.01) + self.assertIsInstance(label, Div) + self.assertEqual(label.text, '%s' % dim.pprint_value_string(3.1)) + self.assertIs(mapping, None) + def test_bokeh_server_dynamic_values_int(self): values = list(range(3, 11)) dim = Dimension('x', values=values) - widget, label, mapping = BokehServerWidgets.create_widget(dim) + widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=True) self.assertIsInstance(widget, Slider) self.assertEqual(widget.value, 0) self.assertEqual(widget.start, 0) @@ -61,10 +73,23 @@ def test_bokeh_server_dynamic_values_int(self): self.assertEqual(label.value, '3') self.assertEqual(mapping, [(v, dim.pprint_value(v)) for v in values]) - def test_bokeh_server_dynamic_values_float(self): + def test_bokeh_server_dynamic_values_float_not_editable(self): + values = list(np.linspace(3.1, 11.2, 7)) + dim = Dimension('x', values=values) + widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=False) + self.assertIsInstance(widget, Slider) + self.assertEqual(widget.value, 0) + self.assertEqual(widget.start, 0) + self.assertEqual(widget.end, 6) + self.assertEqual(widget.step, 1) + self.assertIsInstance(label, Div) + self.assertEqual(label.text, '%s' % dim.pprint_value_string(3.1)) + self.assertEqual(mapping, [(v, dim.pprint_value(v)) for v in values]) + + def test_bokeh_server_dynamic_values_float_editable(self): values = list(np.linspace(3.1, 11.2, 7)) dim = Dimension('x', values=values) - widget, label, mapping = BokehServerWidgets.create_widget(dim) + widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=True) self.assertIsInstance(widget, Slider) self.assertEqual(widget.value, 0) self.assertEqual(widget.start, 0) @@ -78,7 +103,7 @@ def test_bokeh_server_dynamic_values_float(self): def test_bokeh_server_dynamic_values_str(self): values = [chr(65+i) for i in range(10)] dim = Dimension('x', values=values) - widget, label, mapping = BokehServerWidgets.create_widget(dim) + widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=True) self.assertIsInstance(widget, Select) self.assertEqual(widget.value, 'A') self.assertEqual(widget.options, list(zip(values, values))) @@ -89,7 +114,7 @@ def test_bokeh_server_dynamic_values_str(self): def test_bokeh_server_static_numeric_values(self): dim = Dimension('x') ndmap = NdMapping({i: None for i in range(3, 12)}, kdims=['x']) - widget, label, mapping = BokehServerWidgets.create_widget(dim, ndmap) + widget, label, mapping = BokehServerWidgets.create_widget(dim, ndmap, editable=True) self.assertIsInstance(widget, Slider) self.assertEqual(widget.value, 0) self.assertEqual(widget.start, 0) @@ -104,7 +129,7 @@ def test_bokeh_server_dynamic_values_str(self): keys = [chr(65+i) for i in range(10)] ndmap = NdMapping({i: None for i in keys}, kdims=['x']) dim = Dimension('x') - widget, label, mapping = BokehServerWidgets.create_widget(dim, ndmap) + widget, label, mapping = BokehServerWidgets.create_widget(dim, ndmap, editable=True) self.assertIsInstance(widget, Select) self.assertEqual(widget.value, 'A') self.assertEqual(widget.options, list(zip(keys, keys)))