Add Button.value Event parameter
Add test
Small fix
1 |
"""
|
|
2 |
Defines the Widget base class which provides bi-directional
|
|
3 |
communication between the rendered dashboard and the Widget
|
|
4 |
parameters.
|
|
5 |
"""
|
|
6 | 7 |
from __future__ import absolute_import, division, unicode_literals |
7 |
|
|
8 | 7 |
from six import string_types |
9 |
|
|
10 | 7 |
import param |
11 | 7 |
import numpy as np |
12 |
|
|
13 | 7 |
from bokeh.models import CustomJS |
14 | 7 |
from bokeh.models.formatters import TickFormatter |
15 | 7 |
from bokeh.models.widgets import ( |
16 |
DateSlider as _BkDateSlider, DateRangeSlider as _BkDateRangeSlider, |
|
17 |
RangeSlider as _BkRangeSlider, Slider as _BkSlider) |
|
18 |
|
|
19 | 7 |
from ..config import config |
20 | 7 |
from ..io import state |
21 | 7 |
from ..util import unicode_repr, value_as_datetime, value_as_date |
22 | 7 |
from ..viewable import Layoutable |
23 | 7 |
from .base import Widget, CompositeWidget |
24 | 7 |
from ..layout import Column |
25 | 7 |
from .input import StaticText |
26 |
|
|
27 |
|
|
28 |
|
|
29 | 7 |
class _SliderBase(Widget): |
30 |
|
|
31 | 7 |
bar_color = param.Color(default="#e6e6e6", doc=""" |
32 |
Color of the slider bar as a hexidecimal RGB value.""") |
|
33 |
|
|
34 | 7 |
direction = param.ObjectSelector(default='ltr', objects=['ltr', 'rtl'], |
35 |
doc=""" |
|
36 |
Whether the slider should go from left-to-right ('ltr') or
|
|
37 |
right-to-left ('rtl')""") |
|
38 |
|
|
39 | 7 |
orientation = param.ObjectSelector(default='horizontal', |
40 |
objects=['horizontal', 'vertical'], doc=""" |
|
41 |
Whether the slider should be oriented horizontally or
|
|
42 |
vertically.""") |
|
43 |
|
|
44 | 7 |
show_value = param.Boolean(default=True, doc=""" |
45 |
Whether to show the widget value.""") |
|
46 |
|
|
47 | 7 |
tooltips = param.Boolean(default=True, doc=""" |
48 |
Whether the slider handle should display tooltips.""") |
|
49 |
|
|
50 | 7 |
_widget_type = _BkSlider |
51 |
|
|
52 | 7 |
__abstract = True |
53 |
|
|
54 | 7 |
def __init__(self, **params): |
55 | 7 |
if 'value' in params and 'value_throttled' in self.param: |
56 | 7 |
params['value_throttled'] = params['value'] |
57 | 7 |
super(_SliderBase, self).__init__(**params) |
58 |
|
|
59 |
|
|
60 | 7 |
class ContinuousSlider(_SliderBase): |
61 |
|
|
62 | 7 |
format = param.ClassSelector(class_=string_types+(TickFormatter,), doc=""" |
63 |
Allows defining a custom format string or bokeh TickFormatter.""") |
|
64 |
|
|
65 | 7 |
_supports_embed = True |
66 |
|
|
67 | 7 |
__abstract = True |
68 |
|
|
69 | 7 |
def __init__(self, **params): |
70 | 7 |
if 'value' not in params: |
71 | 7 |
params['value'] = params.get('start', self.start) |
72 | 7 |
super(ContinuousSlider, self).__init__(**params) |
73 |
|
|
74 | 7 |
def _get_embed_state(self, root, values=None, max_opts=3): |
75 | 7 |
ref = root.ref['id'] |
76 | 7 |
w_model, parent = self._models[ref] |
77 | 7 |
_, _, doc, comm = state._views[ref] |
78 |
|
|
79 |
# Compute sampling
|
|
80 | 7 |
start, end, step = w_model.start, w_model.end, w_model.step |
81 | 7 |
if values is None: |
82 | 7 |
span = end-start |
83 | 7 |
dtype = int if isinstance(step, int) else float |
84 | 7 |
if (span/step) > (max_opts-1): |
85 | 7 |
step = dtype(span/(max_opts-1)) |
86 | 7 |
values = [dtype(v) for v in np.arange(start, end+step, step)] |
87 | 7 |
elif any(v < start or v > end for v in values): |
88 | 7 |
raise ValueError('Supplied embed states for %s widget outside ' |
89 |
'of valid range.' % type(self).__name__) |
|
90 |
|
|
91 |
# Replace model
|
|
92 | 7 |
layout_opts = {k: v for k, v in self.param.get_param_values() |
93 |
if k in Layoutable.param and k != 'name'} |
|
94 | 7 |
dw = DiscreteSlider(options=values, name=self.name, **layout_opts) |
95 | 7 |
dw.link(self, value='value') |
96 | 7 |
self._models.pop(ref) |
97 | 7 |
index = parent.children.index(w_model) |
98 | 7 |
with config.set(embed=True): |
99 | 7 |
w_model = dw._get_model(doc, root, parent, comm) |
100 | 7 |
link = CustomJS(code=dw._jslink.code['value'], args={ |
101 |
'source': w_model.children[1], 'target': w_model.children[0]}) |
|
102 | 7 |
parent.children[index] = w_model |
103 | 7 |
w_model = w_model.children[1] |
104 | 7 |
w_model.js_on_change('value', link) |
105 |
|
|
106 | 7 |
return (dw, w_model, values, lambda x: x.value, 'value', 'cb_obj.value') |
107 |
|
|
108 |
|
|
109 | 7 |
class FloatSlider(ContinuousSlider): |
110 |
|
|
111 | 7 |
start = param.Number(default=0.0) |
112 |
|
|
113 | 7 |
end = param.Number(default=1.0) |
114 |
|
|
115 | 7 |
value = param.Number(default=0.0) |
116 |
|
|
117 | 7 |
value_throttled = param.Number(default=None, constant=True) |
118 |
|
|
119 | 7 |
step = param.Number(default=0.1) |
120 |
|
|
121 |
|
|
122 | 7 |
class IntSlider(ContinuousSlider): |
123 |
|
|
124 | 7 |
value = param.Integer(default=0) |
125 |
|
|
126 | 7 |
value_throttled = param.Integer(default=None, constant=True) |
127 |
|
|
128 | 7 |
start = param.Integer(default=0) |
129 |
|
|
130 | 7 |
end = param.Integer(default=1) |
131 |
|
|
132 | 7 |
step = param.Integer(default=1) |
133 |
|
|
134 | 7 |
def _process_property_change(self, msg): |
135 | 7 |
msg = super(_SliderBase, self)._process_property_change(msg) |
136 | 7 |
if 'value' in msg: |
137 | 7 |
msg['value'] = msg['value'] if msg['value'] is None else int(msg['value']) |
138 | 7 |
if 'value_throttled' in msg: |
139 | 7 |
throttled = msg['value_throttled'] |
140 | 7 |
msg['value_throttled'] = throttled if throttled is None else int(throttled) |
141 | 7 |
return msg |
142 |
|
|
143 |
|
|
144 | 7 |
class DateSlider(_SliderBase): |
145 |
|
|
146 | 7 |
value = param.Date(default=None) |
147 |
|
|
148 | 7 |
value_throttled = param.Date(default=None, constant=True) |
149 |
|
|
150 | 7 |
start = param.Date(default=None) |
151 |
|
|
152 | 7 |
end = param.Date(default=None) |
153 |
|
|
154 | 7 |
_source_transforms = {'value': None, 'value_throttled': None, 'start': None, 'end': None} |
155 |
|
|
156 | 7 |
_widget_type = _BkDateSlider |
157 |
|
|
158 | 7 |
def __init__(self, **params): |
159 | 7 |
if 'value' not in params: |
160 | 7 |
params['value'] = params.get('start', self.start) |
161 | 7 |
super(DateSlider, self).__init__(**params) |
162 |
|
|
163 | 7 |
def _process_property_change(self, msg): |
164 | 7 |
msg = super(_SliderBase, self)._process_property_change(msg) |
165 | 7 |
if 'value' in msg: |
166 | 7 |
msg['value'] = value_as_date(msg['value']) |
167 | 7 |
if 'value_throttled' in msg: |
168 | 7 |
msg['value_throttled'] = value_as_date(msg['value_throttled']) |
169 | 7 |
return msg |
170 |
|
|
171 |
|
|
172 | 7 |
class DiscreteSlider(CompositeWidget, _SliderBase): |
173 |
|
|
174 | 7 |
options = param.ClassSelector(default=[], class_=(dict, list)) |
175 |
|
|
176 | 7 |
value = param.Parameter() |
177 |
|
|
178 | 7 |
value_throttled = param.Parameter(constant=True) |
179 |
|
|
180 | 7 |
formatter = param.String(default='%.3g') |
181 |
|
|
182 | 7 |
_source_transforms = {'value': None, 'value_throttled': None, 'options': None} |
183 |
|
|
184 | 7 |
_rename = {'formatter': None} |
185 |
|
|
186 | 7 |
_supports_embed = True |
187 |
|
|
188 | 7 |
_text_link = """ |
189 |
var labels = {labels}
|
|
190 |
target.text = labels[source.value]
|
|
191 |
"""
|
|
192 |
|
|
193 | 7 |
_style_params = [p for p in list(Layoutable.param) if p != 'name'] + ['orientation'] |
194 |
|
|
195 | 7 |
def __init__(self, **params): |
196 | 7 |
self._syncing = False |
197 | 7 |
super(DiscreteSlider, self).__init__(**params) |
198 | 7 |
if 'formatter' not in params and all(isinstance(v, (int, np.int_)) for v in self.values): |
199 | 7 |
self.formatter = '%d' |
200 | 7 |
if self.value is None and None not in self.values and self.options: |
201 | 7 |
self.value = self.values[0] |
202 | 7 |
elif self.value not in self.values: |
203 |
raise ValueError('Value %s not a valid option, ' |
|
204 |
'ensure that the supplied value '
|
|
205 |
'is one of the declared options.'
|
|
206 |
% self.value) |
|
207 |
|
|
208 | 7 |
self._text = StaticText(margin=(5, 0, 0, 5), style={'white-space': 'nowrap'}) |
209 | 7 |
self._slider = None |
210 | 7 |
self._composite = Column(self._text, self._slider) |
211 | 7 |
self._update_options() |
212 | 7 |
self.param.watch(self._update_options, ['options', 'formatter']) |
213 | 7 |
self.param.watch(self._update_value, 'value') |
214 | 7 |
self.param.watch(self._update_value, 'value_throttled') |
215 | 7 |
self.param.watch(self._update_style, self._style_params) |
216 |
|
|
217 | 7 |
def _update_options(self, *events): |
218 | 7 |
values, labels = self.values, self.labels |
219 | 7 |
if self.value not in values: |
220 |
value = 0 |
|
221 |
self.value = values[0] |
|
222 |
else: |
|
223 | 7 |
value = values.index(self.value) |
224 |
|
|
225 | 7 |
self._slider = IntSlider( |
226 |
start=0, end=len(self.options)-1, value=value, tooltips=False, |
|
227 |
show_value=False, margin=(0, 5, 5, 5), |
|
228 |
orientation=self.orientation, |
|
229 |
_supports_embed=False |
|
230 |
)
|
|
231 | 7 |
self._update_style() |
232 | 7 |
js_code = self._text_link.format( |
233 |
labels='['+', '.join([unicode_repr(l) for l in labels])+']' |
|
234 |
)
|
|
235 | 7 |
self._jslink = self._slider.jslink(self._text, code={'value': js_code}) |
236 | 7 |
self._slider.param.watch(self._sync_value, 'value') |
237 | 7 |
self._slider.param.watch(self._sync_value, 'value_throttled') |
238 | 7 |
self._text.value = labels[value] |
239 | 7 |
self._composite[1] = self._slider |
240 |
|
|
241 | 7 |
def _update_value(self, event): |
242 |
"""
|
|
243 |
This will update the IntSlider (behind the scene)
|
|
244 |
based on changes to the DiscreteSlider (front).
|
|
245 |
|
|
246 |
_syncing options is to avoid infinite loop.
|
|
247 |
|
|
248 |
event.name is either value or value_throttled.
|
|
249 |
"""
|
|
250 |
|
|
251 | 7 |
values = self.values |
252 | 7 |
if getattr(self, event.name) not in values: |
253 |
with param.edit_constant(self): |
|
254 |
setattr(self, event.name, values[0]) |
|
255 |
return
|
|
256 | 7 |
index = self.values.index(getattr(self, event.name)) |
257 | 7 |
if self._syncing: |
258 | 7 |
return
|
259 | 7 |
try: |
260 | 7 |
self._syncing = True |
261 | 7 |
with param.edit_constant(self._slider): |
262 | 7 |
setattr(self._slider, event.name, index) |
263 |
finally: |
|
264 | 7 |
self._syncing = False |
265 |
|
|
266 | 7 |
def _update_style(self, *events): |
267 | 7 |
style = {p: getattr(self, p) for p in self._style_params} |
268 | 7 |
margin = style.pop('margin') |
269 | 7 |
if isinstance(margin, tuple): |
270 | 7 |
if len(margin) == 2: |
271 | 7 |
t = b = margin[0] |
272 | 7 |
r = l = margin[1] |
273 |
else: |
|
274 | 7 |
t, r, b, l = margin |
275 |
else: |
|
276 |
t = r = b = l = margin |
|
277 | 7 |
text_margin = (t, 0, 0, l) |
278 | 7 |
slider_margin = (0, r, b, l) |
279 | 7 |
text_style = {k: v for k, v in style.items() |
280 |
if k not in ('style', 'orientation')} |
|
281 | 7 |
self._text.param.set_param(margin=text_margin, **text_style) |
282 | 7 |
self._slider.param.set_param(margin=slider_margin, **style) |
283 | 7 |
if self.width: |
284 | 7 |
style['width'] = self.width + l + r |
285 | 7 |
col_style = {k: v for k, v in style.items() |
286 |
if k != 'orientation'} |
|
287 | 7 |
self._composite.param.set_param(**col_style) |
288 |
|
|
289 | 7 |
def _sync_value(self, event): |
290 |
"""
|
|
291 |
This will update the DiscreteSlider (front)
|
|
292 |
based on changes to the IntSlider (behind the scene).
|
|
293 |
|
|
294 |
_syncing options is to avoid infinite loop.
|
|
295 |
|
|
296 |
event.name is either value or value_throttled.
|
|
297 |
"""
|
|
298 |
|
|
299 | 7 |
if self._syncing: |
300 | 7 |
return
|
301 | 7 |
try: |
302 | 7 |
self._syncing = True |
303 | 7 |
with param.edit_constant(self): |
304 | 7 |
setattr(self, event.name, self.values[event.new]) |
305 |
finally: |
|
306 | 7 |
self._syncing = False |
307 |
|
|
308 | 7 |
def _get_embed_state(self, root, values=None, max_opts=3): |
309 |
model = self._composite[1]._models[root.ref['id']][0] |
|
310 |
if values is None: |
|
311 |
values = self.values |
|
312 |
elif any(v not in self.values for v in values): |
|
313 |
raise ValueError("Supplieed embed states were not found " |
|
314 |
"in the %s widgets' values list." % type(self).__name__) |
|
315 |
return self, model, values, lambda x: x.value, 'value', 'cb_obj.value' |
|
316 |
|
|
317 | 7 |
@property
|
318 | 2 |
def labels(self): |
319 | 7 |
title = (self.name + ': ' if self.name else '') |
320 | 7 |
if isinstance(self.options, dict): |
321 | 7 |
return [title + ('<b>%s</b>' % o) for o in self.options] |
322 |
else: |
|
323 | 7 |
return [title + ('<b>%s</b>' % (o if isinstance(o, string_types) else (self.formatter % o))) |
324 |
for o in self.options] |
|
325 | 7 |
@property
|
326 | 2 |
def values(self): |
327 | 7 |
return list(self.options.values()) if isinstance(self.options, dict) else self.options |
328 |
|
|
329 |
|
|
330 | 7 |
class RangeSlider(_SliderBase): |
331 |
|
|
332 | 7 |
format = param.ClassSelector(class_=string_types+(TickFormatter,), doc=""" |
333 |
Allows defining a custom format string or bokeh TickFormatter.""") |
|
334 |
|
|
335 | 7 |
value = param.Range(default=(0, 1)) |
336 |
|
|
337 | 7 |
value_throttled = param.Range(default=None, constant=True) |
338 |
|
|
339 | 7 |
start = param.Number(default=0) |
340 |
|
|
341 | 7 |
end = param.Number(default=1) |
342 |
|
|
343 | 7 |
step = param.Number(default=0.1) |
344 |
|
|
345 | 7 |
_widget_type = _BkRangeSlider |
346 |
|
|
347 | 7 |
def __init__(self, **params): |
348 | 7 |
if 'value' not in params: |
349 | 7 |
params['value'] = (params.get('start', self.start), |
350 |
params.get('end', self.end)) |
|
351 | 7 |
super(RangeSlider, self).__init__(**params) |
352 | 7 |
values = [self.value[0], self.value[1], self.start, self.end] |
353 | 7 |
if (all(v is None or isinstance(v, int) for v in values) and |
354 |
'step' not in params): |
|
355 | 7 |
self.step = 1 |
356 |
|
|
357 | 7 |
def _process_property_change(self, msg): |
358 | 7 |
msg = super(RangeSlider, self)._process_property_change(msg) |
359 | 7 |
if 'value' in msg: |
360 | 7 |
msg['value'] = tuple(msg['value']) |
361 | 7 |
if 'value_throttled' in msg: |
362 | 7 |
msg['value_throttled'] = tuple(msg['value_throttled']) |
363 | 7 |
return msg |
364 |
|
|
365 |
|
|
366 | 7 |
class IntRangeSlider(RangeSlider): |
367 |
|
|
368 | 7 |
start = param.Integer(default=0) |
369 |
|
|
370 | 7 |
end = param.Integer(default=1) |
371 |
|
|
372 | 7 |
step = param.Integer(default=1) |
373 |
|
|
374 | 7 |
def _process_property_change(self, msg): |
375 | 7 |
msg = super(RangeSlider, self)._process_property_change(msg) |
376 | 7 |
if 'value' in msg: |
377 |
msg['value'] = tuple([v if v is None else int(v) |
|
378 |
for v in msg['value']]) |
|
379 | 7 |
if 'value_throttled' in msg: |
380 |
msg['value_throttled'] = tuple([v if v is None else int(v) |
|
381 |
for v in msg['value_throttled']]) |
|
382 | 7 |
return msg |
383 |
|
|
384 |
|
|
385 | 7 |
class DateRangeSlider(_SliderBase): |
386 |
|
|
387 | 7 |
value = param.Tuple(default=(None, None), length=2) |
388 |
|
|
389 | 7 |
value_throttled = param.Tuple(default=None, length=2, constant=True) |
390 |
|
|
391 | 7 |
start = param.Date(default=None) |
392 |
|
|
393 | 7 |
end = param.Date(default=None) |
394 |
|
|
395 | 7 |
step = param.Number(default=1) |
396 |
|
|
397 | 7 |
_source_transforms = {'value': None, 'value_throttled': None, |
398 |
'start': None, 'end': None, 'step': None} |
|
399 |
|
|
400 | 7 |
_widget_type = _BkDateRangeSlider |
401 |
|
|
402 | 7 |
def __init__(self, **params): |
403 | 7 |
if 'value' not in params: |
404 | 7 |
params['value'] = (params.get('start', self.start), |
405 |
params.get('end', self.end)) |
|
406 | 7 |
super(DateRangeSlider, self).__init__(**params) |
407 |
|
|
408 | 7 |
def _process_property_change(self, msg): |
409 | 7 |
msg = super(DateRangeSlider, self)._process_property_change(msg) |
410 | 7 |
if 'value' in msg: |
411 | 7 |
v1, v2 = msg['value'] |
412 | 7 |
msg['value'] = (value_as_datetime(v1), value_as_datetime(v2)) |
413 | 7 |
if 'value_throttled' in msg: |
414 | 7 |
v1, v2 = msg['value_throttled'] |
415 | 7 |
msg['value_throttled'] = (value_as_datetime(v1), value_as_datetime(v2)) |
416 | 7 |
return msg |
Read our documentation on viewing source code .