Skip to content

Commit

Permalink
feat: UI Calendar Component (deephaven#918)
Browse files Browse the repository at this point in the history
Closes deephaven#905 for
implementation.
Closes deephaven#851 for
docs.
  • Loading branch information
dgodinez-dh authored Oct 3, 2024
1 parent 863655b commit 90b27b1
Show file tree
Hide file tree
Showing 20 changed files with 1,086 additions and 46 deletions.
129 changes: 129 additions & 0 deletions plugins/ui/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,135 @@ list_view5 = ui.list_view(

```

###### ui.calendar

A calendar that can be used to select a date.

The calendar accepts the following date types as inputs:

- `None`
- `LocalDate`
- `ZoneDateTime`
- `Instant`
- `int`
- `str`
- `datetime.datetime`
- `numpy.datetime64`
- `pandas.Timestamp`

The input will be converted to one of three Java date types:

1. `LocalDate`: A LocalDate is a date without a time zone in the ISO-8601 system, such as "2007-12-03" or "2057-01-28".
2. `Instant`: An Instant represents an unambiguous specific point on the timeline, such as 2021-04-12T14:13:07 UTC.
3. `ZonedDateTime`: A ZonedDateTime represents an unambiguous specific point on the timeline with an associated time zone, such as 2021-04-12T14:13:07 America/New_York.

The format of the calendar and the type of the value passed to the `on_change` handler
is determined by the type of the following props in order of precedence:

1. `value`
2. `default_value`
3. `focused_value`
4. `default_focused_value`

If none of these are provided, the `on_change` handler passes a range of `Instant`.

```py
import deephaven.ui as ui
ui.calendar(
value: Date | None = None,
default_value: Date | None = None,
focused_value: Date | None = None,
default_focused_value: Date | None = None,
min_value: Date | None = None,
max_value: Date | None = None,
on_change: Callable[[Date], None] | None = None,
**props: Any
) -> CalendarElement
```

###### Parameters

| Parameter | Type | Description |
| ----------------------- | -------------------------------- | ---------------------------------------------------------------------------------------- |
| `value` | `Date \| None` | The current value (controlled). |
| `default_value` | `Date \| None` | The default value (uncontrolled). |
| `focused_value` | `Date \| None` | The focused value (controlled). |
| `default_focused_value` | `Date \| None` | The default focused value (uncontrolled). |
| `min_value` | `Date \| None` | The minimum allowed date that a user may select. |
| `max_value` | `Date \| None` | The maximum allowed date that a user may select. |
| `on_change` | `Callable[[Date], None] \| None` | Handler that is called when the value changes. |
| `**props` | `Any` | Any other [Calendar](https://react-spectrum.adobe.com/react-spectrum/Calendar.html) prop |

```py

import deephaven.ui as ui
from deephaven.time import to_j_local_date, dh_today, to_j_instant, to_j_zdt

zoned_date_time = to_j_zdt("1995-03-22T11:11:11.23142 America/New_York")
instant = to_j_instant("2022-01-01T00:00:00 ET")
local_date = to_j_local_date(dh_today())

# simple calendar that takes ui.items and is uncontrolled
calendar1 = ui.calendar(
default_value=local_date
)

# simple calendar that takes list view items directly and is controlled
# the on_change handler is passed an instant
date, set_date = ui.use_state(instant)

calendar2 = ui.calendar(
value=date,
on_change=set_date
)

# this creates a calendar in the specified time zone
# the on_change handler is passed a zoned date time
date, set_date = ui.use_state(None)

calendar3 = ui.calendar(
default_value=zoned_date_time,
on_change=set_date
)

# this creates a calendar in UTC
# the on_change handler is passed an instant
date, set_date = ui.use_state(None)

calendar4 = ui.calendar(
default_value=instant,
on_change=set_date
)

# this creates a calendar
# the on_change handler is passed a local date
date, set_date = ui.use_state(None)

calendar5 = ui.calendar(
default_value=local_date,
on_change=set_date
)

# this creates a calendar the on_change handler is passed an instant
date, set_date = ui.use_state(None)

calendar7 = ui.calendar(
on_change=set_date
)

# this create a calendar, a min and max value
min_value = to_j_local_date("2022-01-01")
max_value = to_j_local_date("2022-12-31")
unavailable_dates = [to_j_local_date("2022-03-15"), to_j_local_date("2022-03-17")]
date, set_date = ui.use_state(to_j_local_date("2022-03-16"))
calendar8 = ui.calendar(
value=date,
min_value=min_value,
max_value=max_value,
on_change=set_date
)
```

###### ui.date_field

A date field that can be used to select a date.
Expand Down
249 changes: 249 additions & 0 deletions plugins/ui/docs/components/calendar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# Calendar

Calendars display a grid of days in one or more months and allow users to select a single date.

## Example

```python
from deephaven import ui

my_calendar_basic = ui.calendar(aria_label="Event Date")
```

## Date types

The calendar accepts the following date types as inputs:

- `None`
- `LocalDate`
- `ZoneDateTime`
- `Instant`
- `int`
- `str`
- `datetime.datetime`
- `numpy.datetime64`
- `pandas.Timestamp`

The input will be converted to one of three Java date types:

1. `LocalDate`: A LocalDate is a date without a time zone in the ISO-8601 system, such as "2007-12-03" or "2057-01-28".
2. `Instant`: An Instant represents an unambiguous specific point on the timeline, such as 2021-04-12T14:13:07 UTC.
3. `ZonedDateTime`: A ZonedDateTime represents an unambiguous specific point on the timeline with an associated time zone, such as 2021-04-12T14:13:07 America/New_York.

The format of the calendar and the type of the value passed to the `on_change` handler
is determined by the type of the following props in order of precedence:

1. `value`
2. `default_value`
3. `focused_value`
4. `default_focused_value`

If none of these are provided, the `on_change` handler passes a range of `Instant`.

```python
from deephaven import ui
from deephaven.time import to_j_local_date, dh_today, to_j_instant, to_j_zdt

zoned_date_time = to_j_zdt("1995-03-22T11:11:11.23142 America/New_York")
instant = to_j_instant("2022-01-01T00:00:00 ET")
local_date = to_j_local_date(dh_today())


@ui.component
def calendar_test(value):
date, set_date = ui.use_state(value)
return [ui.calendar(on_change=set_date, value=date), ui.text(str(date))]


zoned_calendar = calendar_test(zoned_date_time)
instant_calendar = calendar_test(instant)
local_calendar = calendar_test(local_date)
```

## Value

A Calendar has no selection by default. An initial, uncontrolled value can be provided to the Calendar using the `default_value` prop. Alternatively, a controlled value can be provided using the `value` prop.

```python
from deephaven import ui


@ui.component
def example():
value, set_value = ui.use_state("2020-02-03")
return ui.flex(
ui.calendar(
aria_label="Calendar (uncontrolled)",
default_value="2020-02-03",
),
ui.calendar(
aria_label="Calendar (controlled)", value=value, on_change=set_value
),
gap="size-300",
wrap=True,
)


my_example = example()
```

## Labeling

An `aria_label` must be provided to the Calendar for accessibility. If it is labeled by a separate element, an `aria_labelledby` prop must be provided using the id of the labeling element instead.

## Events

Calendar accepts an `on_change` prop which is triggered whenever a date is selected by the user.

```python
from deephaven import ui


@ui.component
def event_example():
value, set_value = ui.use_state("2020-02-03")
return ui.calendar(
aria_label="Calendar (controlled)", value=value, on_change=set_value
)


my_event_example = event_example()
```

## Validation

By default, Calendar allows selecting any date. The `min_value` and `max_value` props can also be used to prevent the user from selecting dates outside a certain range.

This example only accepts dates after today.

```python
from deephaven import ui
from deephaven.time import dh_today


my_calendar_min_value_example = ui.calendar(
aria_label="Appointment Date", min_value=dh_today()
)
```

## Controlling the focused date

By default, the selected date is focused when a Calendar first mounts. If no `value` or `default_value` prop is provided, then the current date is focused. However, Calendar supports controlling which date is focused using the `focused_value` and `on_focus_change` props. This also determines which month is visible. The `default_focused_value` prop allows setting the initial focused date when the Calendar first mounts, without controlling it.

This example focuses July 1, 2021 by default. The user may change the focused date, and the `on_focus_change` event updates the state. Clicking the button resets the focused date back to the initial value.

```python
from deephaven import ui
from deephaven.time import to_j_local_date

default_date = to_j_local_date("2021-07-01")


@ui.component
def focused_example():
value, set_value = ui.use_state(default_date)
return ui.flex(
ui.action_button(
"Reset focused date", on_press=lambda: set_value(default_date)
),
ui.calendar(focused_value=value, on_focus_change=set_value),
direction="column",
align_items="start",
gap="size-200",
)


my_focused_example = focused_example()
```

## Disabled state

The `is_disabled` prop disables the calendar to prevent user interaction. This is useful when the calendar should be visible but not available for selection.

```python
from deephaven import ui


my_calendar_is_disabled_example = ui.calendar(
is_disabled=True,
)
```

## Read only

The `is_read_only` prop makes the calendar's value immutable. Unlike `is_disabled`, the calendar remains focusable.

```python
from deephaven import ui


my_calendar_is_read_only_example = ui.calendar(
is_read_only=True,
)
```

## Visible Months

By default, the Calendar displays a single month. The `visible_Months` prop allows displaying up to 3 months at a time.

```python
from deephaven import ui


my_calendar_visible_months_example = ui.calendar(
visible_months=3,
)
```

## Page Behavior

By default, when pressing the next or previous buttons, pagination will advance by the `visible_months` value. This behavior can be changed to page by single months instead, by setting `page_behavior` to `single`.

```python
from deephaven import ui


my_calendar_page_behavior_example = ui.calendar(
visible_months=3, page_behavior="single"
)
```

## Time table filtering

Calendars can be used to filter tables with time columns.

```python
from deephaven.time import dh_now
from deephaven import time_table, ui


@ui.component
def date_table_filter(table, start_date, end_date, time_col="Timestamp"):
after_date, set_after_date = ui.use_state(start_date)
before_date, set_before_date = ui.use_state(end_date)
return [
ui.flex(
ui.calendar(
aria_label="Start Date", value=after_date, on_change=set_after_date
),
ui.calendar(
aria_label="End Date", value=before_date, on_change=set_before_date
),
),
table.where(f"{time_col} >= after_date && {time_col} < before_date"),
]


SECONDS_IN_DAY = 86400
today = dh_now()
_table = time_table("PT1s").update_view(
["Timestamp=today.plusSeconds(SECONDS_IN_DAY*i)", "Row=i"]
)
date_filter = date_table_filter(_table, today, today.plusSeconds(SECONDS_IN_DAY * 10))
```

## API Reference

```{eval-rst}
.. dhautofunction:: deephaven.ui.calendar
```
Loading

0 comments on commit 90b27b1

Please sign in to comment.