Skip to content

Commit 08724b1

Browse files
committed
Merge main with the feature branch
2 parents 221ad18 + 77c898b commit 08724b1

File tree

6 files changed

+159
-72
lines changed

6 files changed

+159
-72
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<!--
2+
A new scriv changelog fragment.
3+
4+
Uncomment the section that is right (remove the HTML comment wrapper).
5+
-->
6+
7+
<!--
8+
### Highlights ✨
9+
10+
- A bullet item for the Highlights ✨ category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
11+
12+
-->
13+
<!--
14+
### Removed
15+
16+
- A bullet item for the Removed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
17+
18+
-->
19+
### Added
20+
21+
- `DatePicker` filters update automatically when underlying dynamic data changes. See the [user guide on dynamic filters](https://vizro.readthedocs.io/en/stable/pages/user-guides/data/#filters) for more information. ([#1039](https://github.com/mckinsey/vizro/pull/1039))
22+
23+
<!--
24+
### Changed
25+
26+
- A bullet item for the Changed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
27+
28+
-->
29+
<!--
30+
### Deprecated
31+
32+
- A bullet item for the Deprecated category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
33+
34+
-->
35+
<!--
36+
### Fixed
37+
38+
- A bullet item for the Fixed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
39+
40+
-->
41+
<!--
42+
### Security
43+
44+
- A bullet item for the Security category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
45+
46+
-->

vizro-core/docs/pages/user-guides/data.md

+14-8
Original file line numberDiff line numberDiff line change
@@ -355,14 +355,14 @@ When the page is refreshed, the behavior of a dynamic filter is as follows:
355355

356356
- The filter's selector updates its available values:
357357
- For [categorical selectors](selectors.md#categorical-selectors), `options` updates to give all unique values found in `column` across all the data sources of components in `targets`.
358-
- For [numerical selectors](selectors.md#numerical-selectors), `min` and `max` update to give the overall minimum and maximum values found in `column` across all the data sources of components in `targets`.
358+
- For [numerical selectors](selectors.md#numerical-selectors) and [temporal selectors](selectors.md#temporal-selectors), `min` and `max` update to give the overall minimum and maximum values found in `column` across all the data sources of components in `targets`.
359359
- The value selected on screen by a dashboard user _does not_ change. If the selected value is not already present in the new set of available values then the `options` or `min` and `max` are modified to include it. In this case, the filtering operation might result in an empty DataFrame.
360360
- Even though the values present in a data source can change, the schema should not: `column` should remain present and of the same type in the data sources. The `targets` of the filter and selector type cannot change while the dashboard is running. For example, a `vm.Dropdown` selector cannot turn into `vm.RadioItems`.
361361

362-
For example, let us add two filters to the [dynamic data example](#dynamic-data) above:
362+
For example, to add three filters to the [dynamic data example](#dynamic-data) above:
363363

364364
!!! example "Dynamic filters"
365-
```py hl_lines="10 20 21"
365+
```py hl_lines="10 11 21 22 23"
366366
from vizro import Vizro
367367
import pandas as pd
368368
import vizro.plotly.express as px
@@ -372,7 +372,8 @@ For example, let us add two filters to the [dynamic data example](#dynamic-data)
372372

373373
def load_iris_data():
374374
iris = pd.read_csv("iris.csv")
375-
return iris.sample(5) # (1)!
375+
iris["date_column"] = pd.date_range(start=pd.to_datetime("2025-01-01"), periods=len(iris), freq="D") # (1)!
376+
return iris.sample(5) # (2)!
376377

377378
data_manager["iris"] = load_iris_data
378379

@@ -382,8 +383,9 @@ For example, let us add two filters to the [dynamic data example](#dynamic-data)
382383
vm.Graph(figure=px.box("iris", x="species", y="petal_width", color="species"))
383384
],
384385
controls=[
385-
vm.Filter(column="species"), # (2)!
386-
vm.Filter(column="sepal_length"), # (3)!
386+
vm.Filter(column="species"), # (3)!
387+
vm.Filter(column="sepal_length"), # (4)!
388+
vm.Filter(column="date_column"), # (5)!
387389
],
388390
)
389391

@@ -392,25 +394,29 @@ For example, let us add two filters to the [dynamic data example](#dynamic-data)
392394
Vizro().build(dashboard).run()
393395
```
394396

397+
1. Add a new column `"date_column"` to the `"iris"` data source. This column is used to demonstrate usage of a temporal dynamic filter.
395398
1. We sample only 5 rather than 50 points so that changes to the available values in the filtered columns are more apparent when the page is refreshed.
396399
1. This filter implicitly controls the dynamic data source `"iris"`, which supplies the `data_frame` to the targeted `vm.Graph`. On page refresh, Vizro reloads this data, finds all the unique values in the `"species"` column and sets the categorical selector's `options` accordingly.
397400
1. Similarly, on page refresh, Vizro finds the minimum and maximum values of the `"sepal_length"` column in the reloaded data and sets new `min` and `max` values for the numerical selector accordingly.
401+
1. Similarly, on page refresh, Vizro finds the minimum and maximum values of the `"date_column"` column in the reloaded data and sets new `min` and `max` values for the temporal selector accordingly.
398402

399-
Consider a filter that depends on dynamic data, where you do **not** want the available values to change when the dynamic data changes. You should manually specify the `selector`'s `options` field (categorical selector) or `min` and `max` fields (numerical selector). In the above example, this could be achieved as follows:
403+
Consider a filter that depends on dynamic data, where you do **not** want the available values to change when the dynamic data changes. You should manually specify the `selector`'s `options` field (categorical selector) or `min` and `max` fields (numerical and temporal selector). In the above example, this could be achieved as follows:
400404

401405
```python title="Override selector options to make a dynamic filter static"
402406
controls = [
403407
vm.Filter(column="species", selector=vm.Dropdown(options=["setosa", "versicolor", "virginica"])),
404408
vm.Filter(column="sepal_length", selector=vm.RangeSlider(min=4.3, max=7.9)),
409+
vm.Filter(column="date_column", selector=vm.DatePickerRange(min="2025-01-01", max="2025-05-29")),
405410
]
406411
```
407412

408-
If you [use a specific selector](filters.md#change-selector) for a dynamic filter without manually specifying `options` (categorical selector) or `min` and `max` (numerical selector) then the selector remains dynamic. For example:
413+
If you [use a specific selector](filters.md#change-selector) for a dynamic filter without manually specifying `options` (categorical selector) or `min` and `max` (numerical and temporal selector) then the selector remains dynamic. For example:
409414

410415
```python title="Dynamic filter with specific selector is still dynamic"
411416
controls = [
412417
vm.Filter(column="species", selector=vm.Checklist()),
413418
vm.Filter(column="sepal_length", selector=vm.Slider()),
419+
vm.Filter(column="date_column", selector=vm.DatePicker(range=False)),
414420
]
415421
```
416422

+3-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
# Choose between 0-50
2-
setosa: 5
3-
versicolor: 10
4-
virginica: 15
5-
6-
# Choose between: 4.3 to 7.4
7-
min: 5
8-
max: 7
9-
10-
# Choose between: 2020-01-01 to 2020-05-29
11-
date_min: 2024-01-01
12-
date_max: 2024-05-29
1+
# Choose between: 2024-01-01 to 2024-05-29
2+
date_min: 2024-03-05
3+
date_max: 2024-03-10

vizro-core/src/vizro/models/_components/form/date_picker.py

+24-7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from vizro.models import Action, VizroBaseModel
1212
from vizro.models._action._actions_chain import _action_validator_factory
1313
from vizro.models._components.form._form_utils import validate_date_picker_range, validate_max, validate_range_value
14+
from vizro.models._models_utils import _log_call
1415

1516

1617
class DatePicker(VizroBaseModel):
@@ -42,7 +43,7 @@ class DatePicker(VizroBaseModel):
4243
value: Annotated[
4344
Optional[Union[list[date], date]],
4445
# TODO[MS]: check here and similar if the early exit clause in below validator or similar is
45-
# necessary given we don't validate on default
46+
# necessary given we don't validate on default
4647
AfterValidator(validate_range_value),
4748
Field(default=None, description="Default date/dates for date picker."),
4849
]
@@ -72,16 +73,22 @@ class DatePicker(VizroBaseModel):
7273
]
7374
]
7475

75-
_input_property: str = PrivateAttr("value")
76+
_dynamic: bool = PrivateAttr(False)
7677

77-
def build(self):
78-
init_value = self.value or ([self.min, self.max] if self.range else self.min) # type: ignore[list-item]
78+
_input_property: str = PrivateAttr("value")
7979

80+
def __call__(self, min, max, current_value=None):
81+
# TODO: Refactor value calculation logic after the Dash persistence bug is fixed and "Select All" PR is merged.
82+
# The underlying component's value calculation will need to account for:
83+
# - Changes introduced by Pydantic V2.
84+
# - The way how the new Vizro solution is built on top of the Dash persistence bugfix.
85+
# - Whether the current value is included in the updated options.
86+
# - The way how the validate_options_dict validator and tests are improved.
8087
defaults = {
8188
"id": self.id,
82-
"minDate": self.min,
83-
"value": init_value,
84-
"maxDate": self.max,
89+
"minDate": min,
90+
"value": self.value or ([min, max] if self.range else min),
91+
"maxDate": max,
8592
"persistence": True,
8693
"persistence_type": "session",
8794
"type": "range" if self.range else "default",
@@ -96,3 +103,13 @@ def build(self):
96103
dmc.DatePickerInput(**(defaults | self.extra)),
97104
],
98105
)
106+
107+
def _build_dynamic_placeholder(self):
108+
if not self.value:
109+
self.value = [self.min, self.max] if self.range else self.min # type: ignore[list-item]
110+
111+
return self.__call__(self.min, self.max)
112+
113+
@_log_call
114+
def build(self):
115+
return self._build_dynamic_placeholder() if self._dynamic else self.__call__(self.min, self.max)

vizro-core/src/vizro/models/_controls/filter.py

+24-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from collections.abc import Iterable
4+
from contextlib import suppress
45
from typing import Annotated, Any, Literal, Optional, Union, cast
56

67
import pandas as pd
@@ -23,7 +24,7 @@
2324
Slider,
2425
)
2526
from vizro.models._models_utils import _log_call
26-
from vizro.models.types import FigureType, MultiValueType, SelectorType
27+
from vizro.models.types import FigureType, MultiValueType, SelectorType, SingleValueType
2728

2829
# Ideally we might define these as NumericalSelectorType = Union[RangeSlider, Slider] etc., but that will not work
2930
# with isinstance checks.
@@ -46,11 +47,6 @@
4647
"categorical": SELECTORS["numerical"] + SELECTORS["temporal"],
4748
}
4849

49-
# TODO: Remove DYNAMIC_SELECTORS along with its validation check when support dynamic mode for the DatePicker selector.
50-
# Tuple of filter selectors that support dynamic mode
51-
DYNAMIC_SELECTORS = (Dropdown, Checklist, RadioItems, Slider, RangeSlider)
52-
DynamicNonCategoricalSelectorType = Union[Slider, RangeSlider]
53-
5450

5551
def _filter_between(series: pd.Series, value: Union[list[float], list[str]]) -> pd.Series:
5652
if is_datetime64_any_dtype(series):
@@ -126,7 +122,7 @@ def __call__(self, target_to_data_frame: dict[ModelID, pd.DataFrame], current_va
126122
self.selector = cast(CategoricalSelectorType, self.selector)
127123
return self.selector(options=self._get_options(targeted_data, current_value))
128124
else:
129-
self.selector = cast(DynamicNonCategoricalSelectorType, self.selector)
125+
self.selector = cast(NumericalTemporalSelectorType, self.selector)
130126
_min, _max = self._get_min_max(targeted_data, current_value)
131127
# "current_value" is propagated only to support dcc.Input and dcc.Store components in numerical selectors
132128
# to work with a dynamic selector. This can be removed when dash persistence bug is fixed.
@@ -178,7 +174,7 @@ def pre_build(self):
178174
# The filter is dynamic iff mentioned attributes ("options"/"min"/"max") are not explicitly provided and
179175
# filter targets at least one figure that uses dynamic data source. Note that min or max = 0 are Falsey values
180176
# but should still count as manually set.
181-
if isinstance(self.selector, DYNAMIC_SELECTORS) and (
177+
if (
182178
not getattr(self.selector, "options", [])
183179
and getattr(self.selector, "min", None) is None
184180
and getattr(self.selector, "max", None) is None
@@ -296,7 +292,16 @@ def _validate_column_type(self, targeted_data: pd.DataFrame) -> Literal["numeric
296292
)
297293

298294
@staticmethod
299-
def _get_min_max(targeted_data: pd.DataFrame, current_value=None) -> tuple[float, float]:
295+
def _get_min_max(
296+
targeted_data: pd.DataFrame,
297+
current_value: Optional[Union[SingleValueType, MultiValueType]] = None,
298+
) -> Union[tuple[float, float], tuple[pd.Timestamp, pd.Timestamp]]:
299+
# Try to convert the current value to a datetime object. If it fails (like for Slider), it will be left as is.
300+
# By default, DatePicker produces inputs in the following format: "YYYY-MM-DD".
301+
# "ISO8601" is used to enable the conversion process for custom DatePicker components and custom formats.
302+
with suppress(ValueError):
303+
current_value = pd.to_datetime(current_value, format="ISO8601")
304+
300305
targeted_data = pd.concat([targeted_data, pd.Series(current_value)]).stack().dropna() # noqa: PD013
301306

302307
_min = targeted_data.min(axis=None)
@@ -312,7 +317,16 @@ def _get_min_max(targeted_data: pd.DataFrame, current_value=None) -> tuple[float
312317
return _min, _max
313318

314319
@staticmethod
315-
def _get_options(targeted_data: pd.DataFrame, current_value=None) -> list[Any]:
320+
def _get_options(
321+
targeted_data: pd.DataFrame,
322+
current_value: Optional[Union[SingleValueType, MultiValueType]] = None,
323+
) -> list[Any]:
324+
# Try to convert the current value to a datetime object. If it fails (like for Slider), it will be left as is.
325+
# By default, DatePicker produces inputs in the following format: "YYYY-MM-DD".
326+
# "ISO8601" is used to enable the conversion process for custom DatePicker components and custom formats.
327+
with suppress(ValueError):
328+
current_value = pd.to_datetime(current_value, format="ISO8601")
329+
316330
# The dropna() isn't strictly required here but will be in future pandas versions when the behavior of stack
317331
# changes. See https://pandas.pydata.org/docs/whatsnew/v2.1.0.html#whatsnew-210-enhancements-new-stack.
318332
targeted_data = pd.concat([targeted_data, pd.Series(current_value)]).stack().dropna() # noqa: PD013

0 commit comments

Comments
 (0)