Skip to content

Commit

Permalink
Align src with main
Browse files Browse the repository at this point in the history
  • Loading branch information
antonymilne committed Dec 3, 2024
1 parent dd50d91 commit 539884f
Show file tree
Hide file tree
Showing 20 changed files with 638 additions and 134 deletions.
12 changes: 12 additions & 0 deletions vizro-core/examples/scratch_dev/data.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Choose between 0-50
setosa: 5
versicolor: 10
virginica: 15

# Choose between: 4.3 to 7.4
min: 5
max: 7

# Choose between: 2020-01-01 to 2020-05-29
date_min: 2024-01-01
date_max: 2024-05-29
56 changes: 38 additions & 18 deletions vizro-core/src/vizro/actions/_actions_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,18 @@ def _get_component_actions(component) -> list[Action]:


def _apply_filter_controls(
data_frame: pd.DataFrame, ctds_filters: list[CallbackTriggerDict], target: ModelID
data_frame: pd.DataFrame, ctds_filter: list[CallbackTriggerDict], target: ModelID
) -> pd.DataFrame:
"""Applies filters from a vm.Filter model in the controls.
Args:
data_frame: unfiltered DataFrame.
ctds_filters: list of CallbackTriggerDict for filters.
ctds_filter: list of CallbackTriggerDict for filters.
target: id of targeted Figure.
Returns: filtered DataFrame.
"""
for ctd in ctds_filters:
for ctd in ctds_filter:
selector_value = ctd["value"]
selector_value = selector_value if isinstance(selector_value, list) else [selector_value]
selector_actions = _get_component_actions(model_manager[ctd["id"]])
Expand Down Expand Up @@ -162,12 +162,12 @@ def _update_nested_figure_properties(


def _get_parametrized_config(
ctd_parameters: list[CallbackTriggerDict], target: ModelID, data_frame: bool
ctds_parameter: list[CallbackTriggerDict], target: ModelID, data_frame: bool
) -> dict[str, Any]:
"""Convert parameters into a keyword-argument dictionary.
Args:
ctd_parameters: list of CallbackTriggerDicts for vm.Parameter.
ctds_parameter: list of CallbackTriggerDicts for vm.Parameter.
target: id of targeted figure.
data_frame: whether to return only DataFrame parameters starting "data_frame." or only non-DataFrame parameters.
Expand All @@ -185,7 +185,7 @@ def _get_parametrized_config(
config = deepcopy(model_manager[target].figure._arguments)
del config["data_frame"]

for ctd in ctd_parameters:
for ctd in ctds_parameter:
# TODO: needs to be refactored so that it is independent of implementation details
parameter_value = ctd["value"]

Expand Down Expand Up @@ -221,25 +221,25 @@ def _apply_filters(
# Takes in just one target, so dataframe is filtered repeatedly for every target that uses it.
# Potentially this could be de-duplicated but it's not so important since filtering is a relatively fast
# operation (compared to data loading).
filtered_data = _apply_filter_controls(data_frame=data, ctds_filters=ctds_filter, target=target)
filtered_data = _apply_filter_controls(data_frame=data, ctds_filter=ctds_filter, target=target)
filtered_data = _apply_filter_interaction(
data_frame=filtered_data, ctds_filter_interaction=ctds_filter_interaction, target=target
)
return filtered_data


def _get_unfiltered_data(
ctds_parameters: list[CallbackTriggerDict], targets: list[ModelID]
ctds_parameter: list[CallbackTriggerDict], targets: list[ModelID]
) -> dict[ModelID, pd.DataFrame]:
# Takes in multiple targets to ensure that data can be loaded efficiently using _multi_load and not repeated for
# every single target.
# Getting unfiltered data requires data frame parameters. We pass in all ctd_parameters and then find the
# Getting unfiltered data requires data frame parameters. We pass in all ctds_parameter and then find the
# data_frame ones by passing data_frame=True in the call to _get_paramaterized_config. Static data is also
# handled here and will just have empty dictionary for its kwargs.
multi_data_source_name_load_kwargs: list[tuple[DataSourceName, dict[str, Any]]] = []
for target in targets:
dynamic_data_load_params = _get_parametrized_config(
ctd_parameters=ctds_parameters, target=target, data_frame=True
ctds_parameter=ctds_parameter, target=target, data_frame=True
)
data_source_name = model_manager[target]["data_frame"]
multi_data_source_name_load_kwargs.append((data_source_name, dynamic_data_load_params["data_frame"]))
Expand All @@ -250,25 +250,45 @@ def _get_unfiltered_data(
def _get_modified_page_figures(
ctds_filter: list[CallbackTriggerDict],
ctds_filter_interaction: list[dict[str, CallbackTriggerDict]],
ctds_parameters: list[CallbackTriggerDict],
ctds_parameter: list[CallbackTriggerDict],
targets: list[ModelID],
) -> dict[ModelID, Any]:
from vizro.models import Filter

outputs: dict[ModelID, Any] = {}

control_targets = []
figure_targets = []
for target in targets:
if isinstance(model_manager[target], Filter):
control_targets.append(target)
else:
figure_targets.append(target)

# TODO-NEXT: Add fetching unfiltered data for the Filter.targets as well, once dynamic filters become "targetable"
# from other actions too. For example, in future, if Parameter is targeting only a single Filter.
# Currently, it only works for the on_page_load because Filter.targets are indeed the part of the actions' targets.
# More about the limitation: https://github.com/mckinsey/vizro/pull/879/files#r1863535516
target_to_data_frame = _get_unfiltered_data(ctds_parameter=ctds_parameter, targets=figure_targets)

# TODO: the structure here would be nicer if we could get just the ctds for a single target at one time,
# so you could do apply_filters on a target a pass only the ctds relevant for that target.
# Consider restructuring ctds to a more convenient form to make this possible.

for target, unfiltered_data in _get_unfiltered_data(ctds_parameters, targets).items():
for target, unfiltered_data in target_to_data_frame.items():
filtered_data = _apply_filters(unfiltered_data, ctds_filter, ctds_filter_interaction, target)
outputs[target] = model_manager[target](
data_frame=filtered_data,
**_get_parametrized_config(ctd_parameters=ctds_parameters, target=target, data_frame=False),
**_get_parametrized_config(ctds_parameter=ctds_parameter, target=target, data_frame=False),
)

# TODO NEXT: will need to pass unfiltered_data into Filter.__call__.
# This dictionary is filtered for correct targets already selected in Filter.__call__ or that could be done here
# instead.
# {target: data_frame for target, data_frame in unfiltered_data.items() if target in self.targets}
for target in control_targets:
ctd_filter = [item for item in ctds_filter if item["id"] == model_manager[target].selector.id]

# This only covers the case of cross-page actions when Filter in an output, but is not an input of the action.
current_value = ctd_filter[0]["value"] if ctd_filter else None

# target_to_data_frame contains all targets, including some which might not be relevant for the filter in
# question. We filter to use just the relevant targets in Filter.__call__.
outputs[target] = model_manager[target](target_to_data_frame=target_to_data_frame, current_value=current_value)

return outputs
2 changes: 1 addition & 1 deletion vizro-core/src/vizro/actions/_filter_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ def _filter(
return _get_modified_page_figures(
ctds_filter=ctx.args_grouping["external"]["filters"],
ctds_filter_interaction=ctx.args_grouping["external"]["filter_interaction"],
ctds_parameters=ctx.args_grouping["external"]["parameters"],
ctds_parameter=ctx.args_grouping["external"]["parameters"],
targets=targets,
)
2 changes: 1 addition & 1 deletion vizro-core/src/vizro/actions/_on_page_load_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ def _on_page_load(targets: list[ModelID], **inputs: dict[str, Any]) -> dict[Mode
return _get_modified_page_figures(
ctds_filter=ctx.args_grouping["external"]["filters"],
ctds_filter_interaction=ctx.args_grouping["external"]["filter_interaction"],
ctds_parameters=ctx.args_grouping["external"]["parameters"],
ctds_parameter=ctx.args_grouping["external"]["parameters"],
targets=targets,
)
2 changes: 1 addition & 1 deletion vizro-core/src/vizro/actions/_parameter_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ def _parameter(targets: list[str], **inputs: dict[str, Any]) -> dict[ModelID, An
return _get_modified_page_figures(
ctds_filter=ctx.args_grouping["external"]["filters"],
ctds_filter_interaction=ctx.args_grouping["external"]["filter_interaction"],
ctds_parameters=ctx.args_grouping["external"]["parameters"],
ctds_parameter=ctx.args_grouping["external"]["parameters"],
targets=target_ids,
)
2 changes: 1 addition & 1 deletion vizro-core/src/vizro/actions/filter_interaction_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ def filter_interaction(targets: Optional[list[ModelID]] = None, **inputs: dict[s
return _get_modified_page_figures(
ctds_filter=ctx.args_grouping["external"]["filters"],
ctds_filter_interaction=ctx.args_grouping["external"]["filter_interaction"],
ctds_parameters=ctx.args_grouping["external"]["parameters"],
ctds_parameter=ctx.args_grouping["external"]["parameters"],
targets=targets or [],
)
3 changes: 3 additions & 0 deletions vizro-core/src/vizro/models/_components/form/_form_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ def validate_value(cls, value, values):
[entry["value"] for entry in values["options"]] if isinstance(values["options"][0], dict) else values["options"]
)

if hasattr(value, "__iter__") and ALL_OPTION in value:
return value

if value and not is_value_contained(value, possible_values):
raise ValueError("Please provide a valid value from `options`.")

Expand Down
17 changes: 14 additions & 3 deletions vizro-core/src/vizro/models/_components/form/checklist.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class Checklist(VizroBaseModel):
title: str = Field("", description="Title to be displayed")
actions: list[Action] = []

_dynamic: bool = PrivateAttr(False)

# Component properties for actions and interactions
_input_property: str = PrivateAttr("value")

Expand All @@ -46,9 +48,8 @@ class Checklist(VizroBaseModel):
_validate_options = root_validator(allow_reuse=True, pre=True)(validate_options_dict)
_validate_value = validator("value", allow_reuse=True, always=True)(validate_value)

@_log_call
def build(self):
full_options, default_value = get_options_and_default(options=self.options, multi=True)
def __call__(self, options):
full_options, default_value = get_options_and_default(options=options, multi=True)

return html.Fieldset(
children=[
Expand All @@ -62,3 +63,13 @@ def build(self):
),
]
)

def _build_dynamic_placeholder(self):
if self.value is None:
self.value = [get_options_and_default(self.options, multi=True)[1]]

return self.__call__(self.options)

@_log_call
def build(self):
return self._build_dynamic_placeholder() if self._dynamic else self.__call__(self.options)
38 changes: 34 additions & 4 deletions vizro-core/src/vizro/models/_components/form/dropdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pydantic import Field, PrivateAttr, StrictBool, root_validator, validator

import dash_bootstrap_components as dbc
import dash_mantine_components as dmc

from vizro.models import Action, VizroBaseModel
from vizro.models._action._actions_chain import _action_validator_factory
Expand Down Expand Up @@ -65,6 +66,10 @@ class Dropdown(VizroBaseModel):
title: str = Field("", description="Title to be displayed")
actions: list[Action] = []

# Consider making the _dynamic public later. The same property could also be used for all other components.
# For example: vm.Graph could have a dynamic that is by default set on True.
_dynamic: bool = PrivateAttr(False)

# Component properties for actions and interactions
_input_property: str = PrivateAttr("value")

Expand All @@ -82,9 +87,8 @@ def validate_multi(cls, multi, values):
raise ValueError("Please set multi=True if providing a list of default values.")
return multi

@_log_call
def build(self):
full_options, default_value = get_options_and_default(options=self.options, multi=self.multi)
def __call__(self, options):
full_options, default_value = get_options_and_default(options=options, multi=self.multi)
option_height = _calculate_option_height(full_options)

return html.Div(
Expand All @@ -95,9 +99,35 @@ def build(self):
options=full_options,
value=self.value if self.value is not None else default_value,
multi=self.multi,
persistence=True,
optionHeight=option_height,
persistence=True,
persistence_type="session",
),
]
)

def _build_dynamic_placeholder(self):
# Setting self.value is kind of Dropdown pre_build method. It sets self.value only the first time if it's None.
# We cannot create pre_build for the Dropdown because it has to be called after vm.Filter.pre_build, but nothing
# guarantees that. We can call Filter.selector.pre_build() from the Filter.pre_build() method if we decide that.
# TODO: move this to pre_build once we have better control of the ordering.
if self.value is None:
_, default_value = get_options_and_default(self.options, self.multi)
self.value = default_value

# TODO-NEXT: Replace this with the "universal Vizro placeholder" component.
return html.Div(
children=[
html.Legend(children=self.title, className="form-label") if self.title else None,
dmc.DateRangePicker(
id=self.id,
value=self.value,
persistence=True,
persistence_type="session",
),
]
)

@_log_call
def build(self):
return self._build_dynamic_placeholder() if self._dynamic else self.__call__(self.options)
17 changes: 14 additions & 3 deletions vizro-core/src/vizro/models/_components/form/radio_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class RadioItems(VizroBaseModel):
title: str = Field("", description="Title to be displayed")
actions: list[Action] = []

_dynamic: bool = PrivateAttr(False)

# Component properties for actions and interactions
_input_property: str = PrivateAttr("value")

Expand All @@ -47,9 +49,8 @@ class RadioItems(VizroBaseModel):
_validate_options = root_validator(allow_reuse=True, pre=True)(validate_options_dict)
_validate_value = validator("value", allow_reuse=True, always=True)(validate_value)

@_log_call
def build(self):
full_options, default_value = get_options_and_default(options=self.options, multi=False)
def __call__(self, options):
full_options, default_value = get_options_and_default(options=options, multi=False)

return html.Fieldset(
children=[
Expand All @@ -63,3 +64,13 @@ def build(self):
),
]
)

def _build_dynamic_placeholder(self):
if self.value is None:
self.value = get_options_and_default(self.options, multi=False)[1]

return self.__call__(self.options)

@_log_call
def build(self):
return self._build_dynamic_placeholder() if self._dynamic else self.__call__(self.options)
Loading

0 comments on commit 539884f

Please sign in to comment.