Repository: holoviz/panel

Issue:
Add global configuration to always throttle sliders
#### Is your feature request related to a problem? Please describe.
When developing and working with parameterized classes, I often find myself only wanting the sliders to be throttled because of large datasets or long calculation time.

What I'm doing now is using `pn.Param` and setting `throttled=True` for each widget. An example of this could be this
``` python
import panel as pn
import param


class Example1(param.Parameterized):
    a = param.Range(default=(0.1, 0.4), bounds=(0, 2))
    b = param.Range(default=(0.1, 0.4), bounds=(0, 2))
    c = param.Range(default=(0.1, 0.4), bounds=(0, 2))
    d = param.Range(default=(0.1, 0.4), bounds=(0, 2))
    e = param.Range(default=(0.1, 0.4), bounds=(0, 2))

    def __init__(self, **params):
        super().__init__(**params)

        widgets = {
            "a": {"throttled": True},
            "b": {"throttled": True},
            "c": {"throttled": True},
            "d": {"throttled": True},
            "e": {"throttled": True},
        }

        self.widgets = pn.Param(
            self,
            widgets=widgets,
            show_name=False,
        )

    @param.depends("widgets")
    def values(self):
        return self.a, self.b, self.c, self.d, self.e


example1 = Example1()
pn.Column(example1.widgets, example1.values).servable()
```

This is very cumbersome to implement, which means I usually look it up from my previous code to get it to work. I also think it adds a lot of extra code, which takes away the focus for the actual problem I'm trying to solve.

Furthermore, I have a problem getting `watch=True` in `param.depends` to work with a slider that needs to be throttled. For example, the following code
``` python
import panel as pn
import param


class Example2(param.Parameterized):
    a = param.Range(default=(0.1, 0.4), bounds=(0, 2))
    b = param.String(None)

    def __init__(self, **params):
        super().__init__(**params)

        widgets = {
            "a": {"throttled": True},
        }

        self.a_slider = pn.Param(
            self,
            widgets=widgets,
            show_name=False,
        )

    @param.depends("a_slider", watch=True)
    def values(self):
        self.b = str(self.a_slider.value)


example2 = Example2()

pn.Column(example2.a_slider, example2.b).servable()
```
gives this error: `AttributeError: 'Example2' object has no attribute 'a_slider'`. I'm sure there is a solution to this problem, but I don't know if it is a good solution.

#### Describe the solution you'd like
I would like to have a global configuration that changes the sliders (and inputs) to always be throttled, something like `pn.config.throttled` or similar. This would make it possible to make my code significantly cleaner. Below is how Example1 will look:

``` python
import panel as pn
import param
pn.config.throttled = True


class Example1(param.Parameterized):
    a = param.Range(default=(0.1, 0.4), bounds=(0, 2))
    b = param.Range(default=(0.1, 0.4), bounds=(0, 2))
    c = param.Range(default=(0.1, 0.4), bounds=(0, 2))
    d = param.Range(default=(0.1, 0.4), bounds=(0, 2))
    e = param.Range(default=(0.1, 0.4), bounds=(0, 2))

    @param.depends("a", "b", "c", "d", "e")
    def values(self):
        return self.a, self.b, self.c, self.d, self.e


example1 = Example1()
pn.Column(example1, example1.values).servable()
```

At the same time, it would also make it easy to implement `watch=True`:

``` python
import panel as pn
import param
pn.config.throttled = True


class Example2(param.Parameterized):
    a = param.Range(default=(0.1, 0.4), bounds=(0, 2))
    b = param.String(None)

    @param.depends("a", watch=True)
    def values(self):
        self.b = str(self.a)


example2 = Example2()
pn.panel(example2).servable()
```

#### Additional context
I have an example of how `pn.config.throttled` could be implemented. The way I have done this is to delete `value` and renaming `value_throttled` to `value` both on property change. Right now, the code is only located on my local fork [here](https://github.com/Hoxbro/panel/pull/4/files).  Below is an example of how the implementation works for sliders and inputs with the code.

<details>
  <summary>code </summary>

``` python
import panel as pn
import datetime as dt


date_range_slider = pn.widgets.DateRangeSlider(
    name="Date Range Slider",
    start=dt.datetime(2017, 1, 1),
    end=dt.datetime(2019, 1, 1),
    value=(dt.datetime(2017, 1, 1), dt.datetime(2018, 1, 10)),
)

date_slider = pn.widgets.DateSlider(
    name="Date Slider",
    start=dt.datetime(2019, 1, 1),
    end=dt.datetime(2019, 6, 1),
    value=dt.datetime(2019, 2, 8),
)

discrete_slider = pn.widgets.DiscreteSlider(
    name="Discrete Slider", options=[2, 4, 8, 16, 32, 64, 128], value=32
)

int_range_slider = pn.widgets.IntRangeSlider(
    name="Integer Range Slider", start=0, end=10, value=(2, 8), step=2
)

int_slider = pn.widgets.IntSlider(
    name="Integer Slider", start=0, end=8, step=2, value=4
)

range_slider = pn.widgets.RangeSlider(
    name="Range Slider",
    start=0,
    end=4,
    value=(2, 3),
    step=0.01,
)

float_slider = pn.widgets.FloatSlider(name="Float slider")

int_input = pn.widgets.IntInput(name="Int input")

float_input = pn.widgets.FloatInput(name="Float input")

edit_float_slider = pn.widgets.EditableFloatSlider(name="Editable FloatSlider")

edit_int_slider = pn.widgets.EditableIntSlider(
    name="Editable IntSlider", start=0, end=8, step=2, value=4
)

edit_range_slider = pn.widgets.EditableRangeSlider(
    name="Editable RangeSlider", start=0, end=8, step=2, value=(1, 4)
)

widgets = [
    date_range_slider,
    date_slider,
    discrete_slider,
    int_range_slider,
    int_slider,
    range_slider,
    float_slider,
    int_input,
    float_input,
    edit_float_slider,
    edit_int_slider,
    edit_range_slider,
]

columns = [pn.Row(w, pn.bind((lambda x: x), w)) for w in widgets]
pn.Column(pn.config.param.throttled, *columns).servable()

```
</details>


https://user-images.githubusercontent.com/19758978/117197665-609f4280-ade8-11eb-90de-e922b67b3241.mp4


#### Describe alternatives you've considered
If this feature is decided not to be added to panel. I would like it to be possible for me to patch the implementation into my own code. This seems to be possible if I change the following line of codes from `super(_SliderBase, self)` to a `super()`.

https://github.com/holoviz/panel/blob/078c8b2efa03caa742139f5ce32e47bafcc8378f/panel/widgets/slider.py#L146

https://github.com/holoviz/panel/blob/078c8b2efa03caa742139f5ce32e47bafcc8378f/panel/widgets/slider.py#L177



Discussion:

Resolving patch:
diff --git a/panel/config.py b/panel/config.py
--- a/panel/config.py
+++ b/panel/config.py
@@ -106,6 +106,9 @@ class _config(_base_config):
         'scale_width', 'scale_height', 'scale_both', None], doc="""
         Specify the default sizing mode behavior of panels.""")
 
+    throttled = param.Boolean(default=False, doc="""
+        If sliders and inputs should be throttled until release of mouse.""")
+
     _comms = param.ObjectSelector(
         default='default', objects=['default', 'ipywidgets', 'vscode', 'colab'], doc="""
         Whether to render output in Jupyter with the default Jupyter
diff --git a/panel/widgets/input.py b/panel/widgets/input.py
--- a/panel/widgets/input.py
+++ b/panel/widgets/input.py
@@ -19,6 +19,7 @@
     FileInput as _BkFileInput, TextAreaInput as _BkTextAreaInput,
     NumericInput as _BkNumericInput)
 
+from ..config import config
 from ..layout import Column
 from ..util import param_reprs, as_unicode
 from .base import Widget, CompositeWidget
@@ -369,6 +370,14 @@ def _update_model(self, events, msg, root, model, doc, comm):
 
         return super()._update_model(events, msg, root, model, doc, comm)
 
+    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)
+
 
 class IntInput(_SpinnerBase, _IntInputBase):
 
diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py
--- a/panel/widgets/slider.py
+++ b/panel/widgets/slider.py
@@ -57,6 +57,14 @@ 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']
@@ -143,7 +151,7 @@ class IntSlider(ContinuousSlider):
     _rename = {'name': 'title'}
 
     def _process_property_change(self, msg):
-        msg = super(_SliderBase, self)._process_property_change(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:
@@ -174,7 +182,7 @@ def __init__(self, **params):
         super().__init__(**params)
 
     def _process_property_change(self, msg):
-        msg = super(_SliderBase, self)._process_property_change(msg)
+        msg = super()._process_property_change(msg)
         if 'value' in msg:
             msg['value'] = value_as_date(msg['value'])
         if 'value_throttled' in msg:
@@ -390,7 +398,7 @@ class IntRangeSlider(RangeSlider):
     step = param.Integer(default=1)
 
     def _process_property_change(self, msg):
-        msg = super(RangeSlider, self)._process_property_change(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']])
@@ -591,7 +599,9 @@ def __init__(self, **params):
                                       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')
@@ -609,7 +619,6 @@ def __init__(self, **params):
         } else if (cb_obj.value > slider.end) {
           slider.end = cb_obj.value
         }
-        slider.value = [cb_obj.value, slider.value[1]]
         """)
         self._end_edit.jscallback(args={'slider': self._slider}, value="""
         if (cb_obj.value < slider.start) {
@@ -617,7 +626,6 @@ def __init__(self, **params):
         } else if (cb_obj.value > slider.end) {
           slider.end = cb_obj.value
         }
-        slider.value = [slider.value[0], cb_obj.value]
         """)
         self._update_editable()
         self._update_layout()
@@ -676,13 +684,15 @@ def _sync_value(self, event):
             self.param.set_param(**{event.name: event.new})
 
     def _sync_start_value(self, event):
+        end = self.value[1] if event.name == 'value' else self.value_throttled[1]
         with param.edit_constant(self):
             self.param.set_param(
-                **{event.name: (event.new, self.value_throttled[1])}
+                **{event.name: (event.new, end)}
             )
 
     def _sync_end_value(self, event):
+        start = self.value[0] if event.name == 'value' else self.value_throttled[0]
         with param.edit_constant(self):
             self.param.set_param(
-                **{event.name: (self.value_throttled[0], event.new)}
+                **{event.name: (start, event.new)}
             )


Test patch:
diff --git a/panel/tests/widgets/test_input.py b/panel/tests/widgets/test_input.py
--- a/panel/tests/widgets/test_input.py
+++ b/panel/tests/widgets/test_input.py
@@ -2,10 +2,11 @@
 from datetime import datetime, date
 
 from bokeh.models.widgets import FileInput as BkFileInput
+from panel import config
 from panel.widgets import (
     Checkbox, DatePicker, DatetimeInput, DatetimePicker,
     DatetimeRangeInput, DatetimeRangePicker, FileInput,
-    LiteralInput, TextInput, StaticText,
+    LiteralInput, TextInput, StaticText, IntInput, FloatInput
 )
 
 
@@ -266,3 +267,59 @@ def test_datetime_range_input(document, comm):
     dt_input._start._process_events({'value': '2018-01-01 00:00:00'})
     assert dt_input.value == (datetime(2018, 1, 1), datetime(2018, 1, 3))
     assert label.text == 'Datetime'
+
+
+def test_int_input(document, comm):
+    int_input = IntInput(name='Int input')
+    widget = int_input.get_root(document, comm=comm)
+
+    assert isinstance(widget, int_input._widget_type)
+    assert widget.title == 'Int input'
+    assert widget.step == 1
+    assert widget.value == 0
+
+    int_input._process_events({'value': 2})
+    assert int_input.value == 2
+    int_input._process_events({'value_throttled': 2})
+    assert int_input.value_throttled == 2
+
+    int_input.value = 0
+    assert widget.value == 0
+
+    # Testing throttled mode
+    with config.set(throttled=True):
+        int_input._process_events({'value': 1})
+        assert int_input.value == 0  # no change
+        int_input._process_events({'value_throttled': 1})
+        assert int_input.value == 1
+
+        int_input.value = 2
+        assert widget.value == 2
+
+
+def test_float_input(document, comm):
+    float_input = FloatInput(value=0.4, name="Float input")
+    widget = float_input.get_root(document, comm=comm)
+
+    assert isinstance(widget, float_input._widget_type)
+    assert widget.title == 'Float input'
+    assert widget.step == 0.1
+    assert widget.value == 0.4
+
+    float_input._process_events({'value': 0.2})
+    assert float_input.value == 0.2
+    float_input._process_events({'value_throttled': 0.2})
+    assert float_input.value_throttled == 0.2
+
+    float_input.value = 0.3
+    assert widget.value == 0.3
+
+    # Testing throttled mode
+    with config.set(throttled=True):
+        float_input._process_events({'value': 0.4})
+        assert float_input.value == 0.3  # no change
+        float_input._process_events({'value_throttled': 0.4})
+        assert float_input.value == 0.4
+
+        float_input.value = 0.5
+        assert widget.value == 0.5
diff --git a/panel/tests/widgets/test_slider.py b/panel/tests/widgets/test_slider.py
--- a/panel/tests/widgets/test_slider.py
+++ b/panel/tests/widgets/test_slider.py
@@ -3,6 +3,7 @@
 
 from bokeh.models import Div as BkDiv, Slider as BkSlider, Column as BkColumn
 
+from panel import config
 from panel.widgets import (DateSlider, DateRangeSlider, DiscreteSlider,
                            FloatSlider, IntSlider, RangeSlider)
 
@@ -28,6 +29,16 @@ def test_float_slider(document, comm):
     slider.value = 0.3
     assert widget.value == 0.3
 
+    # Testing throttled mode
+    with config.set(throttled=True):
+        slider._process_events({'value': 0.4})
+        assert slider.value == 0.3  # no change
+        slider._process_events({'value_throttled': 0.4})
+        assert slider.value == 0.4
+
+        slider.value = 0.5
+        assert widget.value == 0.5
+
 
 def test_int_slider(document, comm):
 
@@ -55,6 +66,16 @@ def test_int_slider(document, comm):
     widget_2 = slider_2.get_root(document, comm=comm)
     assert widget_2.value == widget_2.start
 
+    # Testing throttled mode
+    with config.set(throttled=True):
+        slider._process_events({'value': 1})
+        assert slider.value == 0  # no change
+        slider._process_events({'value_throttled': 1})
+        assert slider.value == 1
+
+        slider.value = 2
+        assert widget.value == 2
+
 
 def test_range_slider(document, comm):
 
@@ -77,6 +98,16 @@ def test_range_slider(document, comm):
     slider.value = (0, 1)
     assert widget.value == (0, 1)
 
+    # Testing throttled mode
+    with config.set(throttled=True):
+        slider._process_events({'value': (1, 2)})
+        assert slider.value == (0, 1)  # no change
+        slider._process_events({'value_throttled': (1, 2)})
+        assert slider.value == (1, 2)
+
+        slider.value = (2, 3)
+        assert widget.value == (2, 3)
+
 
 def test_date_slider(document, comm):
     date_slider = DateSlider(name='DateSlider',
@@ -107,6 +138,17 @@ def test_date_slider(document, comm):
     date_slider.value = date(2018, 9, 6)
     assert widget.value == 1536192000000
 
+    # Testing throttled mode
+    epoch_time = lambda dt: (dt - epoch).total_seconds() * 1000
+    with config.set(throttled=True):
+        date_slider._process_events({'value': epoch_time(datetime(2021, 5, 15))})
+        assert date_slider.value == date(2018, 9, 6)  # no change
+        date_slider._process_events({'value_throttled': epoch_time(datetime(2021, 5, 15))})
+        assert date_slider.value == date(2021, 5, 15)
+
+        date_slider.value = date(2021, 5, 12)
+        assert widget.value == 1620777600000
+
 
 def test_date_range_slider(document, comm):
     date_slider = DateRangeSlider(name='DateRangeSlider',
@@ -134,6 +176,22 @@ def test_date_range_slider(document, comm):
     date_slider.value = (datetime(2018, 9, 4), datetime(2018, 9, 6))
     assert widget.value == (1536019200000, 1536192000000)
 
+    # Testing throttled mode
+    epoch_time = lambda dt: (dt - epoch).total_seconds() * 1000
+    epoch_times = lambda *dts: tuple(map(epoch_time, dts))
+    with config.set(throttled=True):
+        date_slider._process_events(
+            {'value': epoch_times(datetime(2021, 2, 15), datetime(2021, 5, 15))}
+        )
+        assert date_slider.value == (datetime(2018, 9, 4), datetime(2018, 9, 6))  # no change
+        date_slider._process_events(
+            {'value_throttled': epoch_times(datetime(2021, 2, 15), datetime(2021, 5, 15))}
+        )
+        assert date_slider.value == (datetime(2021, 2, 15), datetime(2021, 5, 15))
+
+        date_slider.value = (datetime(2021, 2, 12), datetime(2021, 5, 12))
+        assert widget.value == (1613088000000, 1620777600000)
+
 
 def test_discrete_slider(document, comm):
     discrete_slider = DiscreteSlider(name='DiscreteSlider', value=1,
@@ -151,7 +209,7 @@ def test_discrete_slider(document, comm):
     assert widget.step == 1
     assert label.text == 'DiscreteSlider: <b>1</b>'
 
-    widget.value = 2
+    # widget.value = 2
     discrete_slider._slider._process_events({'value': 2})
     assert discrete_slider.value == 10
     discrete_slider._slider._process_events({'value_throttled': 2})
@@ -160,6 +218,16 @@ def test_discrete_slider(document, comm):
     discrete_slider.value = 100
     assert widget.value == 3
 
+    # Testing throttled mode
+    with config.set(throttled=True):
+        discrete_slider._slider._process_events({'value': 0.1})
+        assert discrete_slider.value == 100  # no change
+        discrete_slider._slider._process_events({'value_throttled': 0.1})
+        assert discrete_slider.value == 0.1
+
+        discrete_slider.value = 1
+        assert widget.value == 1
+
 
 def test_discrete_slider_label_update(document, comm):
     discrete_slider = DiscreteSlider(name='DiscreteSlider', value=1,
@@ -191,7 +259,7 @@ def test_discrete_date_slider(document, comm):
     assert widget.step == 1
     assert label.text == 'DiscreteSlider: <b>2016-01-02</b>'
 
-    widget.value = 2
+    # widget.value = 2
     discrete_slider._slider._process_events({'value': 2})
     assert discrete_slider.value == dates['2016-01-03']
     discrete_slider._slider._process_events({'value_throttled': 2})
@@ -200,6 +268,16 @@ def test_discrete_date_slider(document, comm):
     discrete_slider.value = dates['2016-01-01']
     assert widget.value == 0
 
+    # Testing throttled mode
+    with config.set(throttled=True):
+        discrete_slider._slider._process_events({'value': 2})
+        assert discrete_slider.value == dates['2016-01-01']  # no change
+        discrete_slider._slider._process_events({'value_throttled': 2})
+        assert discrete_slider.value == dates['2016-01-03']
+
+        discrete_slider.value = dates['2016-01-02']
+        assert widget.value == 1
+
 
 def test_discrete_slider_options_dict(document, comm):
     options = OrderedDict([('0.1', 0.1), ('1', 1), ('10', 10), ('100', 100)])
@@ -218,7 +296,7 @@ def test_discrete_slider_options_dict(document, comm):
     assert widget.step == 1
     assert label.text == 'DiscreteSlider: <b>1</b>'
 
-    widget.value = 2
+    # widget.value = 2
     discrete_slider._slider._process_events({'value': 2})
     assert discrete_slider.value == 10
     discrete_slider._slider._process_events({'value_throttled': 2})
@@ -226,3 +304,13 @@ def test_discrete_slider_options_dict(document, comm):
 
     discrete_slider.value = 100
     assert widget.value == 3
+
+    # Testing throttled mode
+    with config.set(throttled=True):
+        discrete_slider._slider._process_events({'value': 2})
+        assert discrete_slider.value == options['100']  # no change
+        discrete_slider._slider._process_events({'value_throttled': 2})
+        assert discrete_slider.value == options['10']
+
+        discrete_slider.value = options['1']
+        assert widget.value == 1