Skip to content

Commit

Permalink
Add TimePicker from Bokeh (#7013)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahuang11 authored Aug 26, 2024
1 parent 6b44a18 commit 577f8d9
Show file tree
Hide file tree
Showing 9 changed files with 331 additions and 4 deletions.
104 changes: 104 additions & 0 deletions examples/reference/widgets/TimePicker.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 datetime as dt\n",
"import panel as pn\n",
"\n",
"pn.extension()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``TimePicker`` widget allows entering a time value as text or `datetime.time`. \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",
"* **``value``** (str | datetime.time): The current value\n",
"* **``start``** (str | datetime.time): Inclusive lower bound of the allowed time selection\n",
"* **``end``** (str | datetime.time): Inclusive upper bound of the allowed time selection\n",
"\n",
" ```\n",
" +---+------------------------------------+------------+\n",
" | H | Hours (24 hours) | 00 to 23 |\n",
" | h | Hours | 1 to 12 |\n",
" | G | Hours, 2 digits with leading zeros | 1 to 12 |\n",
" | i | Minutes | 00 to 59 |\n",
" | S | Seconds, 2 digits | 00 to 59 |\n",
" | s | Seconds | 0, 1 to 59 |\n",
" | K | AM/PM | AM or PM |\n",
" +---+------------------------------------+------------+\n",
" ```\n",
" See also https://flatpickr.js.org/formatting/#date-formatting-tokens.\n",
"\n",
"\n",
"\n",
"##### Display\n",
"\n",
"* **``disabled``** (boolean): Whether the widget is editable\n",
"* **``name``** (str): The title of the widget\n",
"* **``format``** (str): Formatting specification for the display of the picked date.\n",
"* **``hour_increment``** (int): Defines the granularity of hour value increments in the UI. Default is 1.\n",
"* **``minute_increment``** (int): Defines the granularity of minute value increments in the UI. Default is 1.\n",
"* **``second_increment``** (int): Defines the granularity of second value increments in the UI. Default is 1.\n",
"* **``seconds``** (bool): Allows to select seconds. By default, only hours and minutes are selectable, and AM/PM depending on the `clock` option. Default is False.\n",
"* **``clock``** (bool): Whether to use 12 hour or 24 hour clock.\n",
"___"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `TimePicker` widget allows selecting a time of day."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"time_picker = pn.widgets.TimePicker(name='Time Picker', value=dt.datetime.now().time(), format='H:i K')\n",
"time_picker"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Either `datetime.time` or `str` can be used as input and `TimePicker` can be bounded by a start and end time."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"time_picker = pn.widgets.TimePicker(name='Time Picker', value=\"08:28\", start='00:00', end='12:00')\n",
"time_picker"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
1 change: 1 addition & 0 deletions panel/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .markup import HTML, JSON, PDF # noqa
from .reactive_html import ReactiveHTML # noqa
from .state import State # noqa
from .time_picker import TimePicker # noqa
from .trend import TrendIndicator # noqa
from .widgets import ( # noqa
Audio, Button, CheckboxButtonGroup, CustomMultiSelect, CustomSelect,
Expand Down
1 change: 1 addition & 0 deletions panel/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export {Terminal} from "./terminal"
export {TextAreaInput} from "./textarea_input"
export {TextInput} from "./text_input"
export {TextToSpeech} from "./text_to_speech"
export {TimePicker} from "./time_picker"
export {ToggleIcon} from "./toggle_icon"
export {TooltipIcon} from "./tooltip_icon"
export {TrendIndicator} from "./trend"
Expand Down
7 changes: 7 additions & 0 deletions panel/models/time_picker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from bokeh.models import TimePicker as BkTimePicker


class TimePicker(BkTimePicker):
"""
A custom Panel version of the Bokeh TimePicker model which fixes timezones.
"""
67 changes: 67 additions & 0 deletions panel/models/time_picker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {TimePicker as BkTimePicker, TimePickerView as BkTimePickerView} from "@bokehjs/models/widgets/time_picker"
import type * as p from "@bokehjs/core/properties"
import type flatpickr from "flatpickr"

export class TimePickerView extends BkTimePickerView {
declare model: TimePicker

private _offset_time(value: string | number): number {
const baseDate = new Date(value)
const timeZoneOffset = baseDate.getTimezoneOffset() * 60 * 1000
return baseDate.getTime() + timeZoneOffset
}

private _setDate(date: string | number): void {
date = this._offset_time(date)
this.picker.setDate(date)
}

protected override get flatpickr_options(): flatpickr.Options.Options {
// on init
const options = super.flatpickr_options
if (options.defaultDate != null) { options.defaultDate = this._offset_time(options.defaultDate as string) }
return options
}

override connect_signals(): void {
super.connect_signals()

const {value} = this.model.properties
this.connect(value.change, () => {
const {value} = this.model
if (value != null && typeof value === "number") {
// we need to handle it when programmatically changed thru Python, e.g.
// time_picker.value = "4:08" or time_picker.value = "datetime.time(4, 8)"
// else, when changed in the UI, e.g. by typing in the input field
// no special handling is needed
this._setDate(value)
}
})
}

}

export namespace TimePicker {
export type Attrs = p.AttrsOf<Props>
export type Props = BkTimePicker.Props & {
}
}

export interface TimePicker extends TimePicker.Attrs { }

export class TimePicker extends BkTimePicker {
declare properties: TimePicker.Props

constructor(attrs?: Partial<TimePicker.Attrs>) {
super(attrs)
}

static override __module__ = "panel.models.time_picker"

static {
this.prototype.default_view = TimePickerView

this.define<TimePicker.Props>(({ }) => ({
}))
}
}
58 changes: 58 additions & 0 deletions panel/tests/ui/widgets/test_time_picker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import datetime

import pytest

from panel.tests.util import serve_component, wait_until
from panel.widgets import TimePicker

pytestmark = pytest.mark.ui


def test_time_picker(page):

time_picker = TimePicker(value="18:08", format="H:i")

serve_component(page, time_picker)

# test init corrected timezone
locator = page.locator("#input")
assert locator.get_attribute("value") == "18:08:00"

# test UI change
locator = page.locator("input.bk-input.form-control.input")
locator.click()
wait_until(lambda: page.locator("input.numInput.flatpickr-hour").is_visible())
locator = page.locator("input.numInput.flatpickr-hour")
locator.press("ArrowDown")
locator.press("Enter")
wait_until(lambda: time_picker.value == datetime.time(17, 8))

# test str value change
time_picker.value = "04:08"
wait_until(lambda: time_picker.value == "04:08")
locator = page.locator("#input")
assert locator.get_attribute("value") == "04:08:00"

# test datetime.time value change
time_picker.value = datetime.time(18, 8)
wait_until(lambda: time_picker.value == datetime.time(18, 8))
locator = page.locator("#input")
assert locator.get_attribute("value") == "18:08:00"


@pytest.mark.parametrize("timezone_id", [
"America/New_York",
"Europe/Berlin",
"UTC",
])
def test_time_picker_timezone_different(page, timezone_id):
context = page.context.browser.new_context(
timezone_id=timezone_id,
)
page = context.new_page()

time_picker = TimePicker(value="18:08", format="H:i")
serve_component(page, time_picker)

locator = page.locator("#input")
assert locator.get_attribute("value") == "18:08:00"
20 changes: 18 additions & 2 deletions panel/tests/widgets/test_input.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import date, datetime
from datetime import date, datetime, time as dt_time
from pathlib import Path

import numpy as np
Expand All @@ -10,7 +10,7 @@
from panel.widgets import (
ArrayInput, Checkbox, DatePicker, DateRangePicker, DatetimeInput,
DatetimePicker, DatetimeRangeInput, DatetimeRangePicker, FileInput,
FloatInput, IntInput, LiteralInput, StaticText, TextInput,
FloatInput, IntInput, LiteralInput, StaticText, TextInput, TimePicker,
)


Expand Down Expand Up @@ -185,6 +185,22 @@ def test_datetime_range_picker(document, comm):
datetime_range_picker._process_events({'value': '2018-09-10 00:00:01'})


def test_time_picker(document, comm):
time_picker = TimePicker(name='Time Picker', value=dt_time(hour=18), format='H:i K')
assert time_picker.value == dt_time(hour=18)
assert time_picker.format == 'H:i K'
assert time_picker.start is None
assert time_picker.end is None


def test_time_picker_str(document, comm):
time_picker = TimePicker(name='Time Picker', value="08:28", start='00:00', end='12:00')
assert time_picker.value == "08:28"
assert time_picker.format == 'H:i'
assert time_picker.start == "00:00"
assert time_picker.end == "12:00"


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

Expand Down
2 changes: 2 additions & 0 deletions panel/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
DatetimeInput, DatetimePicker, DatetimeRangeInput, DatetimeRangePicker,
FileDropper, FileInput, FloatInput, IntInput, LiteralInput, NumberInput,
PasswordInput, Spinner, StaticText, Switch, TextAreaInput, TextInput,
TimePicker,
)
from .misc import FileDownload, JSONEditor, VideoStream # noqa
from .player import DiscretePlayer, Player # noqa
Expand Down Expand Up @@ -136,6 +137,7 @@
"TextEditor",
"TextInput",
"TextToSpeech",
"TimePicker",
"Toggle",
"ToggleGroup",
"ToggleIcon",
Expand Down
Loading

0 comments on commit 577f8d9

Please sign in to comment.