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

Add DateRangePicker widget #6027

Merged
merged 2 commits into from
Dec 11, 2023
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
104 changes: 104 additions & 0 deletions examples/reference/widgets/DateRangePicker.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"\n",
"pn.extension()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``DateRangePicker`` widget allows selecting a date range using a text box and the browser's date-picking utility.\n",
"\n",
"Discover more on using widgets to add interactivity to your applications in the [how-to guides on interactivity](../how_to/interactivity/index.md). Alternatively, learn [how to set up callbacks and (JS-)links between parameters](../../how_to/links/index.md) or [how to use them as part of declarative UIs with Param](../../how_to/param/index.html).\n",
"\n",
"#### Parameters:\n",
"\n",
"For details on other options for customizing the component see the [layout](../../how_to/layout/index.md) and [styling](../../how_to/styling/index.md) how-to guides.\n",
"\n",
"##### Core\n",
"\n",
"* **``end``** (date): The latest selectable date\n",
"* **``start``** (date): The earliest selectable date\n",
"* **``value``** (tuple): Tuple of upper and lower bounds of the selected range expressed as date types\n",
"\n",
"##### Display\n",
"\n",
"* **``disabled``** (boolean): Whether the widget is editable\n",
"* **``name``** (str): The title of the widget\n",
"* **``disabled_dates``** (list): dates to make unavailable for selection; others will be available\n",
"* **``enabled_dates``** (list): dates to make available for selection; others will be unavailable\n",
"\n",
"___"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"``DateRangePicker`` uses a browser-dependent calendar widget to select the date range:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"date_range_picker = pn.widgets.DateRangePicker(name='Date Range Picker')\n",
"\n",
"date_range_picker"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"``DateRangePicker.value`` returns a tuple of date values type that can be read out or set like other widgets:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"date_range_picker.value"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Controls\n",
"\n",
"The `DateRangePicker` widget exposes a number of options which can be changed from both Python and Javascript. Try out \n",
"the effect of these parameters interactively:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pn.Row(date_range_picker.controls(jslink=True), date_range_picker)"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
44 changes: 39 additions & 5 deletions panel/tests/widgets/test_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

from panel import config
from panel.widgets import (
ArrayInput, Checkbox, DatePicker, DatetimeInput, DatetimePicker,
DatetimeRangeInput, DatetimeRangePicker, FileInput, FloatInput, IntInput,
LiteralInput, StaticText, TextInput,
ArrayInput, Checkbox, DatePicker, DateRangePicker, DatetimeInput,
DatetimePicker, DatetimeRangeInput, DatetimeRangePicker, FileInput,
FloatInput, IntInput, LiteralInput, StaticText, TextInput,
)


Expand Down Expand Up @@ -58,6 +58,42 @@ def test_date_picker(document, comm):
assert widget.value == '2018-09-04'


def test_daterange_picker(document, comm):
date_range_picker = DateRangePicker(name='DateRangePicker',
value=(date(2018, 9, 2), date(2018, 9, 3)),
start=date(2018, 9, 1),
end=date(2018, 9, 10))

widget = date_range_picker.get_root(document, comm=comm)

assert isinstance(widget, date_range_picker._widget_type)
assert widget.title == 'DateRangePicker'
assert widget.value == ('2018-09-02', '2018-09-03')
assert widget.min_date == '2018-09-01'
assert widget.max_date == '2018-09-10'

date_range_picker._process_events({'value': ('2018-09-03', '2018-09-04')})
assert date_range_picker.value == (date(2018, 9, 3), date(2018, 9, 4))

date_range_picker._process_events({'value': ('2018-09-05', '2018-09-08')})
assert date_range_picker.value == (date(2018, 9, 5), date(2018, 9, 8))

value = date_range_picker._process_param_change({'value': (date(2018, 9, 4), date(2018, 9, 5))})
assert value['value'] == ('2018-09-04', '2018-09-05')

value = date(2018, 9, 4)
assert date_range_picker._convert_date_to_string(value) == '2018-09-04'
assert date_range_picker._convert_string_to_date(date_range_picker._convert_date_to_string(value)) == value

# Check start value
with pytest.raises(ValueError):
date_range_picker._process_events({'value': ('2018-08-31', '2018-09-01')})

# Check end value
with pytest.raises(ValueError):
date_range_picker._process_events({'value': ('2018-09-10', '2018-09-11')})


def test_datetime_picker(document, comm):
datetime_picker = DatetimePicker(
name='DatetimePicker', value=datetime(2018, 9, 2, 1, 5),
Expand Down Expand Up @@ -94,7 +130,6 @@ def test_datetime_picker(document, comm):
datetime_picker._process_events({'value': '2018-09-10 00:00:01'})



def test_datetime_range_picker(document, comm):
datetime_range_picker = DatetimeRangePicker(
name='DatetimeRangePicker', value=(datetime(2018, 9, 2, 1, 5), datetime(2018, 9, 2, 1, 6)),
Expand Down Expand Up @@ -130,7 +165,6 @@ def test_datetime_range_picker(document, comm):
datetime_range_picker._process_events({'value': '2018-09-10 00:00:01'})



def test_file_input(document, comm):
file_input = FileInput(accept='.txt')

Expand Down
9 changes: 5 additions & 4 deletions panel/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@
TooltipIcon, Tqdm, Trend,
)
from .input import ( # noqa
ArrayInput, Checkbox, ColorPicker, DatePicker, DatetimeInput,
DatetimePicker, DatetimeRangeInput, DatetimeRangePicker, FileInput,
FloatInput, IntInput, LiteralInput, NumberInput, PasswordInput, Spinner,
StaticText, Switch, TextAreaInput, TextInput,
ArrayInput, Checkbox, ColorPicker, DatePicker, DateRangePicker,
DatetimeInput, DatetimePicker, DatetimeRangeInput, DatetimeRangePicker,
FileInput, FloatInput, IntInput, LiteralInput, NumberInput, PasswordInput,
Spinner, StaticText, Switch, TextAreaInput, TextInput,
)
from .misc import FileDownload, JSONEditor, VideoStream # noqa
from .player import DiscretePlayer, Player # noqa
Expand Down Expand Up @@ -83,6 +83,7 @@
"CrossSelector",
"DataFrame",
"DatePicker",
"DateRangePicker",
"DateRangeSlider",
"DatetimeRangeSlider",
"DateSlider",
Expand Down
91 changes: 87 additions & 4 deletions panel/widgets/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@
from bokeh.models.formatters import TickFormatter
from bokeh.models.widgets import (
Checkbox as _BkCheckbox, ColorPicker as _BkColorPicker,
DatePicker as _BkDatePicker, Div as _BkDiv, FileInput as _BkFileInput,
NumericInput as _BkNumericInput, PasswordInput as _BkPasswordInput,
Spinner as _BkSpinner, Switch as _BkSwitch, TextInput as _BkTextInput,
DatePicker as _BkDatePicker, DateRangePicker as _BkDateRangePicker,
Div as _BkDiv, FileInput as _BkFileInput, NumericInput as _BkNumericInput,
PasswordInput as _BkPasswordInput, Spinner as _BkSpinner,
Switch as _BkSwitch, TextInput as _BkTextInput,
)

from ..config import config
Expand Down Expand Up @@ -286,7 +287,7 @@ def _process_param_change(self, msg):

class DatePicker(Widget):
"""
The `DatePicker` allows selecting selecting a `date` value using a text box
The `DatePicker` allows selecting a `date` value using a text box
and a date-picking utility.

Reference: https://panel.holoviz.org/reference/widgets/DatePicker.html
Expand Down Expand Up @@ -339,6 +340,88 @@ def _process_property_change(self, msg):
return msg


class DateRangePicker(Widget):
"""
The `DateRangePicker` allows selecting a `date` range using a text box
and a date-picking utility.

Reference: https://panel.holoviz.org/reference/widgets/DateRangePicker.html

:Example:

>>> DateRangePicker(
... value=(date(2025,1,1), date(2025,1,5)),
... start=date(2025,1,1), end=date(2025,12,31),
... name='Date range'
... )
"""

value = param.DateRange(default=None, doc="""
The current value""")

start = param.CalendarDate(default=None, doc="""
Inclusive lower bound of the allowed date selection""")

end = param.CalendarDate(default=None, doc="""
Inclusive upper bound of the allowed date selection""")

disabled_dates = param.List(default=None, item_type=(date, str))

enabled_dates = param.List(default=None, item_type=(date, str))

width = param.Integer(default=300, allow_None=True, doc="""
Width of this component. If sizing_mode is set to stretch
or scale mode this will merely be used as a suggestion.""")

description = param.String(default=None, doc="""
An HTML string describing the function of this component.""")

_source_transforms: ClassVar[Mapping[str, str | None]] = {}

_rename: ClassVar[Mapping[str, str | None]] = {
'start': 'min_date', 'end': 'max_date'
}

_widget_type: ClassVar[Type[Model]] = _BkDateRangePicker

def __init__(self, **params):
super().__init__(**params)
self._update_value_bounds()

@param.depends('start', 'end', watch=True)
def _update_value_bounds(self):
self.param.value.bounds = (self.start, self.end)
self.param.value._validate(self.value)

def _process_property_change(self, msg):
msg = super()._process_property_change(msg)
for p in ('start', 'end', 'value'):
if p not in msg:
continue
value = msg[p]
if isinstance(value, tuple):
msg[p] = tuple(self._convert_string_to_date(v) for v in value)
return msg

def _process_param_change(self, msg):
msg = super()._process_param_change(msg)
if 'value' in msg:
msg['value'] = tuple(self._convert_date_to_string(v) for v in msg['value'])
if 'min_date' in msg:
msg['min_date'] = self._convert_date_to_string(msg['min_date'])
if 'max_date' in msg:
msg['max_date'] = self._convert_date_to_string(msg['max_date'])
return msg

@staticmethod
def _convert_string_to_date(v):
return datetime.strptime(v, '%Y-%m-%d').date()

@staticmethod
def _convert_date_to_string(v):
return v.strftime('%Y-%m-%d')


class _DatetimePickerBase(Widget):

disabled_dates = param.List(default=None, item_type=(date, str), doc="""
Expand Down
Loading