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 TimePicker from Bokeh #7013

Merged
merged 9 commits into from
Aug 26, 2024
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/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
Loading