Skip to content

Commit

Permalink
feat: UI Component Range Calendar (#930)
Browse files Browse the repository at this point in the history
closes #904
closes #854

---------

Co-authored-by: margaretkennedy <82049573+margaretkennedy@users.noreply.github.com>
  • Loading branch information
dgodinez-dh and margaretkennedy authored Oct 11, 2024
1 parent 7f8c023 commit fde198c
Show file tree
Hide file tree
Showing 19 changed files with 964 additions and 11 deletions.
134 changes: 133 additions & 1 deletion plugins/ui/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1825,7 +1825,6 @@ date_range_picker1 = ui.date_range_picker(
# this creates a date picker with a granularity of seconds in UTC
# the on_change handler is passed a range of instants
dates2, set_dates2 = ui.use_state({"start": instant_start, "end": instant_end})

date_range_picker2 = ui.date_range_picker(
value=dates2,
on_change=set_dates2
Expand Down Expand Up @@ -2070,6 +2069,139 @@ ui.picker(
) -> PickerElement
```

###### ui.range_calendar

A calendar that can be used to select a range of dates.

The range is a dictionary with a `start` date and an `end` date; e.g., `{ "start": "2024-01-02", "end": "2024-01-05" }`

The range 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 range 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.range_calendar(
value: DateRange | None = None,
default_value: DateRange | 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[[DateRange], None] | None = None,
**props: Any
) -> RangeCalendarElement
```

###### Parameters

| Parameter | Type | Description |
| ----------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `value` | `DateRange \| None` | The current value (controlled). |
| `default_value` | `DateRange \| 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[[DateRange], None] \| None` | Handler that is called when the value changes. |
| `**props` | `Any` | Any other [RangeCalendar](https://react-spectrum.adobe.com/react-spectrum/RangeCalendar.html) prop |

```py

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

zdt_start = to_j_zdt("1995-03-22T11:11:11.23142 America/New_York")
zdt_end = to_j_zdt("1995-03-25T11:11:11.23142 America/New_York")
instant_start = to_j_instant("2022-01-01T00:00:00 ET")
instant_end = to_j_instant("2022-01-05T00:00:00 ET")
local_start = to_j_local_date("2024-05-06")
local_end = to_j_local_date("2024-05-10")

# simple range calendar that takes a range and is uncontrolled
range_calendar1 = ui.range_calendar(
default_value={"start": local_start, "end": local_end}
)

# simple range calendar that takes a range directly and is controlled
# the on_change handler is passed a range of instants
dates, set_dates = ui.use_state({"start": instant_start, "end": instant_end})

range_calendar2 = ui.range_calendar(
value=dates,
on_change=set_dates
)

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

range_calendar3 = ui.range_calendar(
default_value=zdt_start,
on_change=set_dates
)

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

range_calendar4 = ui.range_calendar(
default_value=instant_start,
on_change=set_dates
)

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

range_calendar5 = ui.range_calendar(
default_value=local_start,
on_change=set_dates
)

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

range_calendar7 = ui.range_calendar(
on_change=set_dates
)

# 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")
dates, set_dates = ui.use_state({"start": local_start, "end": local_end})
range_calendar8 = ui.range_calendar(
value=dates,
min_value=min_value,
max_value=max_value,
on_change=set_dates
)
```

###### Parameters

| Parameter | Type | Description |
Expand Down
251 changes: 251 additions & 0 deletions plugins/ui/docs/components/range_calendar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# Range Calendar

Range calendars display a grid of days in one or more months and allow users to select a contiguous range of dates.

## Example

```python
from deephaven import ui

my_range_calendar_basic = ui.range_calendar(aria_label="Trip Dates")
```

## Date types

The range is a dictionary with a `start` date and an `end` date; e.g., `{ "start": "2024-01-02", "end": "2024-01-05" }`.

The range 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 range 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, to_j_instant, to_j_zdt


@ui.component
def range_calendar_example(start, end):
dates, set_dates = ui.use_state({"start": start, "end": end})
return [
ui.range_calendar(on_change=set_dates, value=dates),
ui.text(str(dates["start"])),
ui.text(str(dates["end"])),
]


zdt_start = to_j_zdt("1995-03-22T11:11:11.23142 America/New_York")
zdt_end = to_j_zdt("1995-03-25T11:11:11.23142 America/New_York")
instant_start = to_j_instant("2022-01-01T00:00:00 ET")
instant_end = to_j_instant("2022-01-05T00:00:00 ET")
local_start = to_j_local_date("2024-05-06")
local_end = to_j_local_date("2024-05-10")

my_zoned_example = range_calendar_example(zdt_start, zdt_end)
my_instant_example = range_calendar_example(instant_start, instant_end)
my_local_example = range_calendar_example(local_start, local_end)
```

## Value

A range calendar has no selection by default. An initial, uncontrolled value can be provided to the range 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({"start": "2020-02-03", "end": "2020-02-08"})
return ui.flex(
ui.range_calendar(
aria_label="Date range (uncontrolled)",
default_value={"start": "2020-02-03", "end": "2020-02-08"},
),
ui.range_calendar(
aria_label="Date range (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

Range 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({"start": "2020-02-03", "end": "2020-02-08"})
return ui.range_calendar(
aria_label="Calendar (controlled)", value=value, on_change=set_value
)


my_event_example = event_example()
```

## Validation

By default, range calendar allows selecting any date range. 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_range_calendar_min_value_example = ui.range_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, range 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 range 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.range_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 range calendar to prevent user interaction. This is useful when the range calendar should be visible but not available for selection.

```python
from deephaven import ui


my_range_calendar_is_disabled_example = ui.range_calendar(
is_disabled=True,
)
```

## Read only

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

```python
from deephaven import ui


my_range_calendar_is_read_only_example = ui.range_calendar(
is_read_only=True,
)
```

## Visible Months

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

```python
from deephaven import ui


my_range_calendar_visible_months_example = ui.range_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_range_calendar_page_behavior_example = ui.range_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"):
dates, set_dates = ui.use_state({"start": start_date, "end": end_date})
start = dates["start"]
end = dates["end"]
return [
ui.range_calendar(value=dates, on_change=set_dates),
table.where(f"{time_col} >= start && {time_col} < end"),
]


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.range_calendar
```
Loading

0 comments on commit fde198c

Please sign in to comment.