"""
Sliders allow you to select a value from a defined range of values by
moving one or more handle(s).
- The `value` will update when a handle is dragged.
- The `value_throttled`will update when a handle is released.
"""
import datetime as dt
import param
import numpy as np
from bokeh.models import CustomJS
from bokeh.models.formatters import TickFormatter
from bokeh.models.widgets import (
DateSlider as _BkDateSlider, DateRangeSlider as _BkDateRangeSlider,
RangeSlider as _BkRangeSlider, Slider as _BkSlider)
from ..config import config
from ..io import state
from ..util import (
datetime_as_utctimestamp, edit_readonly, param_reprs,
value_as_datetime, value_as_date
)
from ..viewable import Layoutable
from ..layout import Column, Row
from .base import Widget, CompositeWidget
from .input import IntInput, FloatInput, StaticText
class _SliderBase(Widget):
bar_color = param.Color(default="#e6e6e6", doc="""
Color of the slider bar as a hexidecimal RGB value.""")
direction = param.ObjectSelector(default='ltr', objects=['ltr', 'rtl'], doc="""
Whether the slider should go from left-to-right ('ltr') or
right-to-left ('rtl').""")
name = param.String(default=None, doc="""
The name of the widget. Also used as the label of the widget. If not set,
the widget has no label.""")
orientation = param.ObjectSelector(default='horizontal', objects=['horizontal', 'vertical'],
doc="""
Whether the slider should be oriented horizontally or
vertically.""")
show_value = param.Boolean(default=True, doc="""
Whether to show the widget value as a label or not.""")
tooltips = param.Boolean(default=True, doc="""
Whether the slider handle should display tooltips.""")
_widget_type = _BkSlider
__abstract = True
def __init__(self, **params):
if 'value' in params and 'value_throttled' in self.param:
params['value_throttled'] = params['value']
super().__init__(**params)
def __repr__(self, depth=0):
return '{cls}({params})'.format(cls=type(self).__name__,
params=', '.join(param_reprs(self, ['value_throttled'])))
def _process_property_change(self, msg):
if config.throttled:
if "value" in msg:
del msg["value"]
if "value_throttled" in msg:
msg["value"] = msg["value_throttled"]
return super()._process_property_change(msg)
def _update_model(self, events, msg, root, model, doc, comm):
if 'value_throttled' in msg:
del msg['value_throttled']
return super()._update_model(events, msg, root, model, doc, comm)
class ContinuousSlider(_SliderBase):
format = param.ClassSelector(class_=(str, TickFormatter,), doc="""
A custom format string or Bokeh TickFormatter.""")
_supports_embed = True
__abstract = True
def __init__(self, **params):
if 'value' not in params:
params['value'] = params.get('start', self.start)
super().__init__(**params)
def _get_embed_state(self, root, values=None, max_opts=3):
ref = root.ref['id']
w_model, parent = self._models[ref]
_, _, doc, comm = state._views[ref]
# Compute sampling
start, end, step = w_model.start, w_model.end, w_model.step
if values is None:
span = end-start
dtype = int if isinstance(step, int) else float
if (span/step) > (max_opts-1):
step = dtype(span/(max_opts-1))
values = [dtype(v) for v in np.arange(start, end+step, step)]
elif any(v < start or v > end for v in values):
raise ValueError('Supplied embed states for %s widget outside '
'of valid range.' % type(self).__name__)
# Replace model
layout_opts = {k: v for k, v in self.param.values().items()
if k in Layoutable.param and k != 'name'}
dw = DiscreteSlider(options=values, name=self.name, **layout_opts)
dw.link(self, value='value')
self._models.pop(ref)
index = parent.children.index(w_model)
with config.set(embed=True):
w_model = dw._get_model(doc, root, parent, comm)
link = CustomJS(code=dw._jslink.code['value'], args={
'source': w_model.children[1], 'target': w_model.children[0]})
parent.children[index] = w_model
w_model = w_model.children[1]
w_model.js_on_change('value', link)
return (dw, w_model, values, lambda x: x.value, 'value', 'cb_obj.value')
class FloatSlider(ContinuousSlider):
"""
The FloatSlider widget allows selecting a floating-point value
within a set of bounds using a slider.
Reference: https://panel.holoviz.org/reference/widgets/FloatSlider.html
:Example:
>>> FloatSlider(value=0.5, start=0.0, end=1.0, step=0.1, name="Float value")
"""
value = param.Number(default=0.0, doc="""
The selected floating-point value of the slider. Updated when the handle is dragged.
""")
value_throttled = param.Number(default=None, constant=True, doc="""
The value of the slider. Updated when the handle is released.""")
start = param.Number(default=0.0, doc="""
The lower bound.""")
end = param.Number(default=1.0, doc="""
The upper bound.""")
step = param.Number(default=0.1, doc="""
The step size.""")
_rename = {'name': 'title'}
class IntSlider(ContinuousSlider):
"""
The IntSlider widget allows selecting an integer value within a
set of bounds using a slider.
Reference: https://panel.holoviz.org/reference/widgets/IntSlider.html
:Example:
>>> IntSlider(value=5, start=0, end=10, step=1, name="Integer Value")
"""
value = param.Integer(default=0, doc="""
The selected integer value of the slider. Updated when the handle is dragged.""")
start = param.Integer(default=0, doc="""
The lower bound.""")
end = param.Integer(default=1, doc="""
The upper bound.""")
step = param.Integer(default=1, doc="""
The step size.""")
value_throttled = param.Integer(default=None, constant=True, doc="""
The value of the slider. Updated when the handle is released""")
_rename = {'name': 'title'}
def _process_property_change(self, msg):
msg = super()._process_property_change(msg)
if 'value' in msg:
msg['value'] = msg['value'] if msg['value'] is None else int(msg['value'])
if 'value_throttled' in msg:
throttled = msg['value_throttled']
msg['value_throttled'] = throttled if throttled is None else int(throttled)
return msg
class DateSlider(_SliderBase):
"""
The DateSlider widget allows selecting a value within a set of
bounds using a slider. Supports datetime.datetime, datetime.date
and np.datetime64 values. The step size is fixed at 1 day.
Reference: https://panel.holoviz.org/reference/widgets/DateSlider.html
:Example:
>>> import datetime as dt
>>> DateSlider(
... value=dt.datetime(2025, 1, 1),
... start=dt.datetime(2025, 1, 1),
... end=dt.datetime(2025, 1, 7),
... name="A datetime value"
... )
"""
value = param.Date(default=None, doc="""
The selected date value of the slider. Updated when the slider
handle is dragged. Supports datetime.datetime, datetime.date
or np.datetime64 types.""")
value_throttled = param.Date(default=None, constant=True, doc="""
The value of the slider. Updated when the slider handle is released.""")
start = param.Date(default=None, doc="""
The lower bound.""")
end = param.Date(default=None, doc="""
The upper bound.""")
as_datetime = param.Boolean(default=False, doc="""
Whether to store the date as a datetime.""")
_rename = {'name': 'title', 'as_datetime': None}
_source_transforms = {'value': None, 'value_throttled': None, 'start': None, 'end': None}
_widget_type = _BkDateSlider
def __init__(self, **params):
if 'value' not in params:
params['value'] = params.get('start', self.start)
super().__init__(**params)
def _process_param_change(self, msg):
msg = super()._process_param_change(msg)
if 'value' in msg:
value = msg['value']
if isinstance(value, dt.datetime):
value = datetime_as_utctimestamp(value)
msg['value'] = value
return msg
def _process_property_change(self, msg):
msg = super()._process_property_change(msg)
transform = value_as_datetime if self.as_datetime else value_as_date
if 'value' in msg:
msg['value'] = transform(msg['value'])
if 'value_throttled' in msg:
msg['value_throttled'] = transform(msg['value_throttled'])
return msg
class DiscreteSlider(CompositeWidget, _SliderBase):
"""
The DiscreteSlider widget allows selecting a value from a discrete
list or dictionary of values using a slider.
Reference: https://panel.holoviz.org/reference/widgets/DiscreteSlider.html
:Example:
>>> DiscreteSlider(
... value=0,
... options=list([0, 1, 2, 4, 8, 16, 32, 64]),
... name="A discrete value",
... )
"""
value = param.Parameter(doc="""
The selected value of the slider. Updated when the handle is
dragged. Must be one of the options.""")
value_throttled = param.Parameter(constant=True, doc="""
The value of the slider. Updated when the handle is released.""")
options = param.ClassSelector(default=[], class_=(dict, list), doc="""
A list or dictionary of valid options.""")
formatter = param.String(default='%.3g', doc="""
A custom format string. Separate from format parameter since
formatting is applied in Python, not via the bokeh TickFormatter.""")
_source_transforms = {'value': None, 'value_throttled': None, 'options': None}
_rename = {'formatter': None}
_supports_embed = True
_text_link = """
var labels = {labels}
target.text = labels[source.value]
"""
_style_params = [p for p in list(Layoutable.param) if p != 'name'] + ['orientation']
def __init__(self, **params):
self._syncing = False
super().__init__(**params)
if 'formatter' not in params and all(isinstance(v, (int, np.int_)) for v in self.values):
self.formatter = '%d'
if self.value is None and None not in self.values and self.options:
self.value = self.values[0]
elif self.value not in self.values and not (self.value is None or self.options):
raise ValueError('Value %s not a valid option, '
'ensure that the supplied value '
'is one of the declared options.'
% self.value)
self._text = StaticText(margin=(5, 0, 0, 5), style={'white-space': 'nowrap'})
self._slider = None
self._composite = Column(self._text, self._slider)
self._update_options()
self.param.watch(self._update_options, ['options', 'formatter', 'name'])
self.param.watch(self._update_value, 'value')
self.param.watch(self._update_value, 'value_throttled')
self.param.watch(self._update_style, self._style_params)
def _update_options(self, *events):
values, labels = self.values, self.labels
if not self.options and self.value is None:
value = 0
label = (f'{self.name}: ' if self.name else '') + '-'
elif self.value not in values:
value = 0
self.value = values[0]
label = labels[value]
else:
value = values.index(self.value)
label = labels[value]
disabled = len(values) in (0, 1)
end = 1 if disabled else len(self.options)-1
self._slider = IntSlider(
start=0, end=end, value=value, tooltips=False,
show_value=False, margin=(0, 5, 5, 5),
orientation=self.orientation,
_supports_embed=False, disabled=disabled
)
self._update_style()
js_code = self._text_link.format(
labels='['+', '.join([repr(l) for l in labels])+']'
)
self._jslink = self._slider.jslink(self._text, code={'value': js_code})
self._slider.param.watch(self._sync_value, 'value')
self._slider.param.watch(self._sync_value, 'value_throttled')
self._text.value = label
self._composite[1] = self._slider
def _update_value(self, event):
"""
This will update the IntSlider (behind the scene)
based on changes to the DiscreteSlider (front).
_syncing options is to avoid infinite loop.
event.name is either value or value_throttled.
"""
values = self.values
if getattr(self, event.name) not in values:
with param.edit_constant(self):
setattr(self, event.name, values[0])
return
index = self.values.index(getattr(self, event.name))
if event.name == 'value':
self._text.value = self.labels[index]
if self._syncing:
return
try:
self._syncing = True
with param.edit_constant(self._slider):
setattr(self._slider, event.name, index)
finally:
self._syncing = False
def _update_style(self, *events):
style = {p: getattr(self, p) for p in self._style_params}
margin = style.pop('margin')
if isinstance(margin, tuple):
if len(margin) == 2:
t = b = margin[0]
r = l = margin[1]
else:
t, r, b, l = margin
else:
t = r = b = l = margin
text_margin = (t, 0, 0, l)
slider_margin = (0, r, b, l)
text_style = {k: v for k, v in style.items()
if k not in ('style', 'orientation')}
self._text.param.update(margin=text_margin, **text_style)
self._slider.param.update(margin=slider_margin, **style)
if self.width:
style['width'] = self.width + l + r
col_style = {k: v for k, v in style.items()
if k != 'orientation'}
self._composite.param.update(**col_style)
def _sync_value(self, event):
"""
This will update the DiscreteSlider (front)
based on changes to the IntSlider (behind the scene).
_syncing options is to avoid infinite loop.
event.name is either value or value_throttled.
"""
if self._syncing:
return
try:
self._syncing = True
with param.edit_constant(self):
setattr(self, event.name, self.values[event.new])
finally:
self._syncing = False
def _get_embed_state(self, root, values=None, max_opts=3):
model = self._composite[1]._models[root.ref['id']][0]
if values is None:
values = self.values
elif any(v not in self.values for v in values):
raise ValueError("Supplieed embed states were not found "
"in the %s widgets' values list." % type(self).__name__)
return self, model, values, lambda x: x.value, 'value', 'cb_obj.value'
@property
def labels(self):
"""The list of labels to display"""
title = (self.name + ': ' if self.name else '')
if isinstance(self.options, dict):
return [title + ('%s' % o) for o in self.options]
else:
return [title + ('%s' % (o if isinstance(o, str) else (self.formatter % o)))
for o in self.options]
@property
def values(self):
"""The list of option values"""
return list(self.options.values()) if isinstance(self.options, dict) else self.options
class _RangeSliderBase(_SliderBase):
value = param.Tuple(length=2, doc="""
The selected range of the slider. Updated when a handle is dragged.""")
value_start = param.Parameter(readonly=True, doc="""The lower value of the selected range.""")
value_end = param.Parameter(readonly=True, doc="""The upper value of the selected range.""")
__abstract = True
def __init__(self, **params):
if 'value' not in params:
params['value'] = (params.get('start', self.start),
params.get('end', self.end))
params['value_start'], params['value_end'] = params['value']
with edit_readonly(self):
super().__init__(**params)
@param.depends('value', watch=True)
def _sync_values(self):
vs, ve = self.value
with edit_readonly(self):
self.param.update(value_start=vs, value_end=ve)
def _process_property_change(self, msg):
msg = super()._process_property_change(msg)
if 'value' in msg:
msg['value'] = tuple(msg['value'])
if 'value_throttled' in msg:
msg['value_throttled'] = tuple(msg['value_throttled'])
return msg
class RangeSlider(_RangeSliderBase):
"""
The RangeSlider widget allows selecting a floating-point range
using a slider with two handles.
Reference: https://panel.holoviz.org/reference/widgets/RangeSlider.html
:Example:
>>> RangeSlider(
... value=(1.0, 1.5), start=0.0, end=2.0, step=0.25, name="A tuple of floats"
... )
"""
value = param.Range(default=(0, 1), doc=
"""The selected range as a tuple of values. Updated when a handle is
dragged.""")
value_throttled = param.Range(default=None, constant=True, doc="""
The selected range as a tuple of floating point values. Updated when a handle is
released""")
value_start = param.Number(default=0, readonly=True, doc="""
The lower value of the selected range.""")
value_end = param.Number(default=1, readonly=True, doc="""
The upper value of the selected range.""")
start = param.Number(default=0, doc="""
The lower bound.""")
end = param.Number(default=1, doc="""
The upper bound.""")
step = param.Number(default=0.1, doc="""
The step size.""")
format = param.ClassSelector(class_=(str, TickFormatter,), doc="""
A format string or bokeh TickFormatter.""")
_rename = {'name': 'title', 'value_start': None, 'value_end': None}
_widget_type = _BkRangeSlider
def __init__(self, **params):
super().__init__(**params)
values = [self.value[0], self.value[1], self.start, self.end]
if (all(v is None or isinstance(v, int) for v in values) and
'step' not in params):
self.step = 1
class IntRangeSlider(RangeSlider):
"""
The IntRangeSlider widget allows selecting an integer range using
a slider with two handles.
Reference: https://panel.holoviz.org/reference/widgets/IntRangeSlider.html
:Example:
>>> IntRangeSlider(
... value=(2, 4), start=0, end=10, step=2, name="A tuple of integers"
... )
"""
start = param.Integer(default=0, doc="""
The lower bound.""")
end = param.Integer(default=1, doc="""
The uppper bound.""")
step = param.Integer(default=1, doc="""
The step size""")
def _process_property_change(self, msg):
msg = super()._process_property_change(msg)
if 'value' in msg:
msg['value'] = tuple([v if v is None else int(v)
for v in msg['value']])
if 'value_throttled' in msg:
msg['value_throttled'] = tuple([v if v is None else int(v)
for v in msg['value_throttled']])
return msg
class DateRangeSlider(_RangeSliderBase):
"""
The DateRangeSlider widget allows selecting a date range using a
slider with two handles. Supports datetime.datetime, datetime.data
and np.datetime64 ranges.
Reference: https://panel.holoviz.org/reference/widgets/DateRangeSlider.html
:Example:
>>> import datetime as dt
>>> DateRangeSlider(
... value=(dt.datetime(2025, 1, 9), dt.datetime(2025, 1, 16)),
... start=dt.datetime(2025, 1, 1),
... end=dt.datetime(2025, 1, 31),
... step=2,
... name="A tuple of datetimes"
... )
"""
value = param.Tuple(default=(None, None), length=2, doc=
"""The selected range as a tuple of values. Updated when one of the handles is
dragged. Supports datetime.datetime, datetime.date, and np.datetime64 ranges.""")
value_start = param.Date(default=None, readonly=True, doc="""
The lower value of the selected range.""")
value_end = param.Date(default=None, readonly=True, doc="""
The upper value of the selected range.""")
value_throttled = param.Tuple(default=None, length=2, constant=True, doc="""
The selected range as a tuple of values. Updated one of the handles is released. Supports
datetime.datetime, datetime.date and np.datetime64 ranges""")
start = param.Date(default=None, doc="""
The lower bound.""")
end = param.Date(default=None, doc="""
The upper bound.""")
step = param.Number(default=1, doc="""
The step size. Default is 1 (day).""")
_source_transforms = {'value': None, 'value_throttled': None,
'start': None, 'end': None, 'step': None}
_rename = {'name': 'title', 'value_start': None, 'value_end': None}
_widget_type = _BkDateRangeSlider
def _process_param_change(self, msg):
msg = super()._process_param_change(msg)
if msg.get('value') == (None, None):
del msg['value']
elif 'value' in msg:
v1, v2 = msg['value']
if isinstance(v1, dt.datetime):
v1 = datetime_as_utctimestamp(v1)
if isinstance(v2, dt.datetime):
v2 = datetime_as_utctimestamp(v2)
msg['value'] = (v1, v2)
if msg.get('value_throttled') == (None, None):
del msg['value_throttled']
return msg
def _process_property_change(self, msg):
msg = super()._process_property_change(msg)
if 'value' in msg:
v1, v2 = msg['value']
msg['value'] = (value_as_datetime(v1), value_as_datetime(v2))
if 'value_throttled' in msg:
v1, v2 = msg['value_throttled']
msg['value_throttled'] = (value_as_datetime(v1), value_as_datetime(v2))
return msg
class _EditableContinuousSlider(CompositeWidget):
"""
The EditableFloatSlider extends the FloatSlider by adding a text
input field to manually edit the value and potentially override
the bounds.
"""
editable = param.Boolean(default=True, doc="""
Whether the value is editable via the text input.""")
show_value = param.Boolean(default=False, readonly=True, precedence=-1, doc="""
Whether to show the widget value.""")
_composite_type = Column
_slider_widget = None
_input_widget = None
__abstract = True
def __init__(self, **params):
if not 'width' in params and not 'sizing_mode' in params:
params['width'] = 300
super().__init__(**params)
self._label = StaticText(margin=0, align='end')
self._slider = self._slider_widget(
value=self.value, margin=(0, 0, 5, 0), sizing_mode='stretch_width'
)
self._slider.param.watch(self._sync_value, 'value')
self._slider.param.watch(self._sync_value, 'value_throttled')
self._value_edit = self._input_widget(
margin=0, align='end', css_classes=['slider-edit']
)
self._value_edit.param.watch(self._sync_value, 'value')
self._value_edit.param.watch(self._sync_value, 'value_throttled')
self._value_edit.jscallback(args={'slider': self._slider}, value="""
if (cb_obj.value < slider.start)
slider.start = cb_obj.value
else if (cb_obj.value > slider.end)
slider.end = cb_obj.value
""")
label = Row(self._label, self._value_edit)
self._composite.extend([label, self._slider])
self._update_editable()
self._update_layout()
self._update_name()
self._update_slider()
self._update_value()
@param.depends('width', 'height', 'sizing_mode', watch=True)
def _update_layout(self):
self._value_edit.sizing_mode = self.sizing_mode
if self.sizing_mode not in ('stretch_width', 'stretch_both'):
w = (self.width or 300)//4
self._value_edit.width = w
@param.depends('editable', watch=True)
def _update_editable(self):
self._value_edit.disabled = not self.editable
@param.depends('name', watch=True)
def _update_name(self):
if self.name:
label = f'{self.name}:'
margin = (0, 10, 0, 0)
else:
label = ''
margin = (0, 0, 0, 0)
self._label.param.update(**{'margin': margin, 'value': label})
@param.depends('start', 'end', 'step', 'bar_color', 'direction',
'show_value', 'tooltips', 'format', watch=True)
def _update_slider(self):
self._slider.param.update(**{
'format': self.format,
'start': self.start,
'end': self.end,
'step': self.step,
'bar_color': self.bar_color,
'direction': self.direction,
'show_value': self.show_value,
'tooltips': self.tooltips
})
self._value_edit.step = self.step
@param.depends('value', watch=True)
def _update_value(self):
self._slider.value = self.value
self._value_edit.value = self.value
def _sync_value(self, event):
with param.edit_constant(self):
self.param.update(**{event.name: event.new})
class EditableFloatSlider(_EditableContinuousSlider, FloatSlider):
"""
The EditableFloatSlider widget allows selecting selecting a
numeric floating-point value within a set of bounds using a slider
and for more precise control offers an editable number input box.
Reference: https://panel.holoviz.org/reference/widgets/EditableFloatSlider.html
:Example:
>>> EditableFloatSlider(
... value=1.0, start=0.0, end=2.0, step=0.25, name="A float value"
... )
"""
_slider_widget = FloatSlider
_input_widget = FloatInput
class EditableIntSlider(_EditableContinuousSlider, IntSlider):
"""
The EditableIntSlider widget allows selecting selecting an integer
value within a set of bounds using a slider and for more precise
control offers an editable integer input box.
Reference: https://panel.holoviz.org/reference/widgets/EditableIntSlider.html
:Example:
>>> EditableIntSlider(
... value=2, start=0, end=5, step=1, name="An integer value"
... )
"""
_slider_widget = IntSlider
_input_widget = IntInput
class EditableRangeSlider(CompositeWidget, _SliderBase):
"""
The EditableRangeSlider widget allows selecting a floating-point
range using a slider with two handles and for more precise control
also offers a set of number input boxes.
Reference: https://panel.holoviz.org/reference/widgets/EditableRangeSlider.html
:Example:
>>> EditableRangeSlider(
... value=(1.0, 1.5), start=0.0, end=2.0, step=0.25, name="A tuple of floats"
... )
"""
value = param.Range(default=(0, 1), doc="Current range value. Updated when a handle is dragged")
value_throttled = param.Range(default=None, constant=True, doc="""
The value of the slider. Updated when the handle is released.""")
start = param.Number(default=0., doc="Lower bound of the range.")
end = param.Number(default=1., doc="Upper bound of the range.")
step = param.Number(default=0.1, doc="Slider and number input step.")
editable = param.Tuple(default=(True, True), doc="""
Whether the lower and upper values are editable.""")
format = param.ClassSelector(default='0.0[0000]', class_=(str, TickFormatter,), doc="""
Allows defining a custom format string or bokeh TickFormatter.""")
show_value = param.Boolean(default=False, readonly=True, precedence=-1, doc="""
Whether to show the widget value.""")
_composite_type = Column
def __init__(self, **params):
if not 'width' in params and not 'sizing_mode' in params:
params['width'] = 300
super().__init__(**params)
self._label = StaticText(margin=0, align='end')
self._slider = RangeSlider(margin=(0, 0, 5, 0), show_value=False)
self._slider.param.watch(self._sync_value, 'value')
self._slider.param.watch(self._sync_value, 'value_throttled')
self._start_edit = FloatInput(min_width=50, margin=0, format=self.format,
css_classes=['slider-edit'])
self._end_edit = FloatInput(min_width=50, margin=(0, 0, 0, 10), format=self.format,
css_classes=['slider-edit'])
self._start_edit.param.watch(self._sync_start_value, 'value')
self._start_edit.param.watch(self._sync_start_value, 'value_throttled')
self._end_edit.param.watch(self._sync_end_value, 'value')
self._end_edit.param.watch(self._sync_end_value, 'value_throttled')
sep = StaticText(value='...', margin=(0, 2, 0, 2), align='end')
edit = Row(self._label, self._start_edit, sep, self._end_edit,
sizing_mode='stretch_width', margin=0)
self._composite.extend([edit, self._slider])
self._slider.jscallback(args={'start': self._start_edit, 'end': self._end_edit}, value="""
let [min, max] = cb_obj.value
start.value = min
end.value = max
""")
self._start_edit.jscallback(args={'slider': self._slider}, value="""
if (cb_obj.value < slider.start) {
slider.start = cb_obj.value
} else if (cb_obj.value > slider.end) {
slider.end = cb_obj.value
}
""")
self._end_edit.jscallback(args={'slider': self._slider}, value="""
if (cb_obj.value < slider.start) {
slider.start = cb_obj.value
} else if (cb_obj.value > slider.end) {
slider.end = cb_obj.value
}
""")
self._update_editable()
self._update_layout()
self._update_name()
self._update_slider()
self._update_value()
@param.depends('editable', watch=True)
def _update_editable(self):
self._start_edit.disabled = not self.editable[0]
self._end_edit.disabled = not self.editable[1]
@param.depends('name', watch=True)
def _update_name(self):
if self.name:
label = f'{self.name}:'
margin = (0, 10, 0, 0)
else:
label = ''
margin = (0, 0, 0, 0)
self._label.param.update(**{'margin': margin, 'value': label})
@param.depends('width', 'height', 'sizing_mode', watch=True)
def _update_layout(self):
self._start_edit.sizing_mode = self.sizing_mode
self._end_edit.sizing_mode = self.sizing_mode
if self.sizing_mode not in ('stretch_width', 'stretch_both'):
w = (self.width or 300)//4
self._start_edit.width = w
self._end_edit.width = w
@param.depends('start', 'end', 'step', 'bar_color', 'direction',
'show_value', 'tooltips', 'name', 'format', watch=True)
def _update_slider(self):
self._slider.param.update(**{
'format': self.format,
'start': self.start,
'end': self.end,
'step': self.step,
'bar_color': self.bar_color,
'direction': self.direction,
'show_value': self.show_value,
'tooltips': self.tooltips,
})
self._start_edit.step = self.step
self._end_edit.step = self.step
@param.depends('value', watch=True)
def _update_value(self):
self._slider.value = self.value
self._start_edit.value = self.value[0]
self._end_edit.value = self.value[1]
def _sync_value(self, event):
with param.edit_constant(self):
self.param.update(**{event.name: event.new})
def _sync_start_value(self, event):
if event.name == 'value':
end = self.value[1] if self.value else self.end
else:
end = self.value_throttled[1] if self.value_throttled else self.end
with param.edit_constant(self):
self.param.update(
**{event.name: (event.new, end)}
)
def _sync_end_value(self, event):
if event.name == 'value':
start = self.value[0] if self.value else self.start
else:
start = self.value_throttled[0] if self.value_throttled else self.start
with param.edit_constant(self):
self.param.update(
**{event.name: (start, event.new)}
)