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 0
            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 0
            value = 0
221 0
            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 0
            with param.edit_constant(self):
254 0
                setattr(self, event.name, values[0])
255 0
            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 0
            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 0
        model = self._composite[1]._models[root.ref['id']][0]
310 0
        if values is None:
311 0
            values = self.values
312 0
        elif any(v not in self.values for v in values):
313 0
            raise ValueError("Supplieed embed states were not found "
314
                             "in the %s widgets' values list." % type(self).__name__)
315 0
        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 0
            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 0
            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 .

Loading