Skip to content
Draft
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
42 changes: 40 additions & 2 deletions vizro-core/examples/scratch_dev/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from vizro import Vizro
from vizro.models.types import capture
from vizro.managers import data_manager
from vizro.actions import update_figures


df = px.data.iris()
Expand Down Expand Up @@ -42,7 +43,7 @@

page_1_1 = vm.Page(
id="page_1_1",
title="Apply controls on button click",
title="Apply the filter on the parameter change",
components=[
vm.Graph(
id="p11_graph",
Expand All @@ -66,7 +67,44 @@
)


dashboard = vm.Dashboard(pages=[page_0_1, page_1_1])
# ====== **NEW** Apply controls on button click ======

vm.Page.add_type("controls", vm.Button)

page_2_1 = vm.Page(
id="page_2_1",
title="Apply controls on button click",
components=[
vm.Graph(
id="p21_graph",
figure=px.scatter(
df, x="sepal_width", y="sepal_length", color="species", color_discrete_map=SPECIES_COLORS
),
),
vm.Text(id="p21_text", text="Placeholder"),
],
controls=[
vm.Filter(
column="species",
targets=["p21_graph"],
selector=vm.RadioItems(
title="Filter that does NOT auto-apply, but is taken into account when its target Graph is updated.",
actions=vm.Action(function=capture("action")(lambda _trigger: _trigger)(), outputs="p21_text"),
),
),
vm.Parameter(
targets=["p21_graph.x"],
selector=vm.RadioItems(
title="Parameter that does NOT auto-apply, but is taken into account when its target Graph is updated.",
options=["sepal_width", "sepal_length"],
actions=vm.Action(function=capture("action")(lambda _trigger: _trigger)(), outputs="p21_text"),
),
),
vm.Button(text="Apply controls", actions=update_figures()),
],
)

dashboard = vm.Dashboard(pages=[page_0_1, page_1_1, page_2_1])

if __name__ == "__main__":
Vizro().build(dashboard).run()
10 changes: 9 additions & 1 deletion vizro-core/src/vizro/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,13 @@
from vizro.actions._filter_interaction import filter_interaction
from vizro.actions._notifications import show_notification, update_notification
from vizro.actions._set_control import set_control
from vizro.actions._update_figures import update_figures

__all__ = ["export_data", "filter_interaction", "set_control", "show_notification", "update_notification"]
__all__ = [
"export_data",
"filter_interaction",
"set_control",
"show_notification",
"update_figures",
"update_notification",
]
41 changes: 0 additions & 41 deletions vizro-core/src/vizro/actions/_filter_action.py

This file was deleted.

43 changes: 0 additions & 43 deletions vizro-core/src/vizro/actions/_on_page_load.py

This file was deleted.

51 changes: 0 additions & 51 deletions vizro-core/src/vizro/actions/_parameter_action.py

This file was deleted.

86 changes: 86 additions & 0 deletions vizro-core/src/vizro/actions/_update_figures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from collections.abc import Iterable
from typing import Any, Literal, cast

from dash import ctx
from pydantic import Field

import vizro.models as vm
from vizro.actions._abstract_action import _AbstractAction
from vizro.actions._actions_utils import _get_modified_page_figures
from vizro.managers import model_manager
from vizro.managers._model_manager import FIGURE_MODELS
from vizro.models._models_utils import _log_call
from vizro.models.types import FigureType, ModelID, _Controls


# TODO AM QQ: Should we rename "update_figures"? It updates controls too.
# Consider combining words update/recreate/refresh with figures/models/controls.
class update_figures(_AbstractAction):
"""Exports data of target charts, tables and figures.

Args:
targets (list[ModelID]): List of target component ids that will be rebuilt. If none are given then target
all components on the page.

Example:
```python
import vizro.actions as va

vm.Button(
text="Recreate first graph",
actions=va.update_figures(targets=["graph_id_1"]),
)
```
"""

type: Literal["update_figures"] = "update_figures"

targets: list[ModelID] = Field(default=[], description="Target component IDs.")

@_log_call
def pre_build(self):
# Set targets to all figures on the page if not already set.

# TODO AM-PP OQ: This implementation enables users to manually specify filter targets outside the container.
# TODO-AV2 A 4: work out where this duplicated get_all_targets_on_page logic should live.

root_model = model_manager._get_model_page(self)

figure_ids_on_page = [
model.id for model in cast(Iterable[FigureType], model_manager._get_models(FIGURE_MODELS, root_model))
]
dynamic_filter_ids_on_page = [
filter.id
for filter in cast(Iterable[vm.Filter], model_manager._get_models(vm.Filter, root_model=root_model))
if filter._dynamic
]

if not self.targets:
self.targets = figure_ids_on_page
elif invalid_targets := set(self.targets) - set(figure_ids_on_page + dynamic_filter_ids_on_page):
raise ValueError(f"targets {invalid_targets} are not valid figures on the page.")

def function(self, _controls: _Controls) -> dict[ModelID, Any]:
"""Recreates targeted charts by applying controls.

Returns:
Dict mapping target chart ids to modified figures e.g. {"my_scatter": Figure(...)}.

"""
# TODO-AV2 A 1: _controls is not currently used but instead taken out of the Dash context. This
# will change in future once the structure of _controls has been worked out and we know how to pass ids through.
# See https://github.com/mckinsey/vizro/pull/880
return _get_modified_page_figures(
ctds_filter=ctx.args_grouping["external"]["_controls"]["filters"],
ctds_parameter=ctx.args_grouping["external"]["_controls"]["parameters"],
ctds_filter_interaction=ctx.args_grouping["external"]["_controls"]["filter_interaction"],
targets=self.targets,
)

@property
def outputs(self): # type: ignore[override]
# Special handling for controls as otherwise the control's default action output would alter the selector value.
return {
target: f"{target}.selector" if isinstance(model_manager[target], (vm.Filter, vm.Parameter)) else target
for target in self.targets
}
13 changes: 9 additions & 4 deletions vizro-core/src/vizro/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,15 @@
# To resolve ForwardRefs we need to import a few more things that are not part of the vizro.models namespace.
# We rebuild all the models even if it's not strictly necessary so that if pydantic changes how model_rebuild works
# we won't end up with unresolved references.
from vizro.actions import export_data, filter_interaction, set_control, show_notification, update_notification
from vizro.actions._filter_action import _filter
from vizro.actions._on_page_load import _on_page_load
from vizro.actions._parameter_action import _parameter

from vizro.actions import (
export_data,
filter_interaction,
set_control,
show_notification,
update_figures,
update_notification,
)

from ._components.form._text_area import TextArea
from ._components.form._user_input import UserInput
Expand Down
12 changes: 3 additions & 9 deletions vizro-core/src/vizro/models/_controls/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pydantic import Field, PrivateAttr, model_validator

from vizro._constants import FILTER_ACTION_PREFIX
from vizro.actions._filter_action import _filter
from vizro.actions import update_figures
from vizro.managers import data_manager, model_manager
from vizro.managers._data_manager import DataSourceName, _DynamicData
from vizro.managers._model_manager import FIGURE_MODELS
Expand Down Expand Up @@ -285,15 +285,9 @@ def pre_build(self):
else:
self._filter_function = _filter_isin

# TODO AM-PP: If [] or None is set make that the actions are not overwritten. Could be tricky, but doable.
if not self.selector.actions:
self.selector.actions = [
_filter(
id=f"{FILTER_ACTION_PREFIX}_{self.id}",
column=self.column,
filter_function=self._filter_function,
targets=self.targets,
),
]
self.selector.actions = update_figures(id=f"{FILTER_ACTION_PREFIX}_{self.id}", targets=self.targets)

# A set of properties unique to selector (inner object) that are not present in html.Div (outer build wrapper).
# Creates _action_outputs and _action_inputs for forwarding properties to the underlying selector.
Expand Down
7 changes: 4 additions & 3 deletions vizro-core/src/vizro/models/_controls/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pydantic import AfterValidator, Field, PrivateAttr, model_validator

from vizro._constants import PARAMETER_ACTION_PREFIX
from vizro.actions._parameter_action import _parameter
from vizro.actions import update_figures
from vizro.managers import model_manager
from vizro.models import VizroBaseModel
from vizro.models._controls._controls_utils import (
Expand Down Expand Up @@ -183,9 +183,10 @@ def pre_build(self):

# Extending `self.targets` with `filter_targets` instead of redefining it to avoid triggering the
# pydantic validator like `check_dot_notation` on the `self.targets` again.
# We do the update to ensure that `self.targets` is consistent with the targets passed to `_parameter`.
# We do the update to ensure that `self.targets` is consistent with the target ids passed to `_parameter`.
self.targets.extend(list(filter_targets))
self.selector.actions = [_parameter(id=f"{PARAMETER_ACTION_PREFIX}_{self.id}", targets=self.targets)]
targets_ids = [target.partition(".")[0] for target in self.targets]
self.selector.actions = update_figures(id=f"{PARAMETER_ACTION_PREFIX}_{self.id}", targets=targets_ids)

@_log_call
def build(self):
Expand Down
5 changes: 3 additions & 2 deletions vizro-core/src/vizro/models/_models_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from dash.development.base_component import Component
from pydantic import ValidationInfo

from vizro._constants import ON_PAGE_LOAD_ACTION_PREFIX
from vizro.managers import model_manager
from vizro.models.types import CapturedCallable, _SupportsCapturedCallable

Expand Down Expand Up @@ -127,7 +128,6 @@ def make_actions_chain(self):
Table and AgGrid. Even though it's a model validator it is also run on assignment e.g. selector.actions = ...
"""
from vizro.actions import export_data, filter_interaction
from vizro.actions._on_page_load import _on_page_load

converted_actions = []

Expand Down Expand Up @@ -164,7 +164,8 @@ def make_actions_chain(self):
action._first_in_chain_trigger = model_action_trigger

# The actions chain guard should be called only for on page load.
action._prevent_initial_call_of_guard = not isinstance(action, _on_page_load)
# TODO AM-PP OQ: Revisit this and make the implementation better if possible.
action._prevent_initial_call_of_guard = not action.id.startswith(ON_PAGE_LOAD_ACTION_PREFIX)

# Temporary workaround for lookups in filter_interaction and set_control. This should become unnecessary once
# the model manager supports `parent_model` access for all Vizro models.
Expand Down
Loading
Loading