Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bokeh widgets editable #1247

Merged
merged 1 commit into from
Apr 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 42 additions & 23 deletions holoviews/plotting/bokeh/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'])
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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='<b>%s</b>' % 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:
Expand All @@ -109,16 +116,22 @@ 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='<b>%s</b>' % dim.pprint_value_string(start))
widget = Slider(value=start, start=start,
end=end, step=step, title=None)
else:
values = (dim.values if dim.values else
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='<b>%s</b>' % (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],
Expand All @@ -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)
Expand Down Expand Up @@ -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 = '<b>%s</b>' % 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)
Expand Down
43 changes: 34 additions & 9 deletions tests/testbokehwidgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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, '<b>%s</b>' % 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)
Expand All @@ -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, '<b>%s</b>' % 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)
Expand All @@ -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)))
Expand All @@ -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)
Expand All @@ -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)))
Expand Down