Skip to content

Commit

Permalink
Custom actions release
Browse files Browse the repository at this point in the history
  • Loading branch information
petar-qb committed Nov 23, 2023
1 parent 529c26b commit d62a72c
Show file tree
Hide file tree
Showing 18 changed files with 388 additions and 128 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
-->

### Highlights ✨

- Release of the `custom actions`. Visit the [user guide on actions](https://vizro.readthedocs.io/en/stable/pages/user_guides/actions/) to learn more.
- TODO: Add PR link above.

<!--
### Removed
- 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))
-->
<!--
### Added
- A bullet item for the Added 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))
-->
<!--
### Changed
- 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))
-->
<!--
### Deprecated
- 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))
-->
<!--
### Fixed
- 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))
-->
<!--
### Security
- 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))
-->
149 changes: 146 additions & 3 deletions vizro-core/docs/pages/user_guides/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,8 @@ Here is an example of how to configure a chart interaction when the source is a

[Table]: ../../assets/user_guides/actions/actions_table_filter_interaction.png

## Predefined actions customization
Many predefined actions are customizable which helps to achieve more specific desired goal. For specific options, please
## Pre-defined actions customization
Many pre-defined actions are customizable which helps to achieve more specific desired goal. For specific options, please
refer to the [API reference][vizro.actions] on this topic.

## Actions chaining
Expand Down Expand Up @@ -384,9 +384,152 @@ The order of action execution is guaranteed, and the next action in the list wil

[Graph3]: ../../assets/user_guides/actions/actions_chaining.png


## Custom actions

!!! success "Coming soon!"
In case you want to create complex functionality with the [`Action`][vizro.models.Action] model, and if there is no already pre-defined `action function` available, you can create your own `custom action`.
Like other actions, custom actions could also be added as element inside the actions chain, and it can be triggered with one of many dashboard components.

### Simple custom action example (without UI inputs and outputs)

Custom actions feature enables you to implement your own `action function`, and for this, simply do the following:

- define a function
- decorate it with the `@capture("action")` decorator
- add it as a `function` argument inside the [`Action`][vizro.models.Action] model

The following example shows how to create a custom action that postpone execution of the next action in the chain for `N` seconds.

??? example "Custom Dash DataTable"
=== "app.py"
```py
import vizro.models as vm
import vizro.plotly.express as px
from vizro import Vizro
from vizro.actions import export_data
from vizro.models.types import capture
from time import sleep


@capture("action")
def my_custom_action(sleep_n_seconds: int = 0):
"""Custom action."""
sleep(sleep_n_seconds)


df = px.data.iris()

page = vm.Page(
title="Example of a simple custom action",
components=[
vm.Graph(
id="scatter_chart",
figure=px.scatter(df, x="sepal_length", y="petal_width", color="species")
),
vm.Button(
text="Export data",
actions=[
vm.Action(function=export_data()),
vm.Action(
function=my_custom_action(
sleep_n_seconds=2
)
),
vm.Action(function=export_data(file_format="xlsx")),
]
)
],
controls=[vm.Filter(column="species", selector=vm.Dropdown(title="Species"))],
)

dashboard = vm.Dashboard(pages=[page])

Vizro().build(dashboard).run()
```
=== "app.yaml"
```yaml
# Custom action are currently only possible via python configuration
```


### Custom action with UI inputs and outputs
In case the custom action needs to interact with the dashboard, it is possible to define `inputs` and `outputs` for the custom action.

- `inputs` represents dashboard components properties which values are passed to the custom action function as function arguments. It can be defined as a list of strings in the following format: `"<component_id>.<property>"`, which enables propagation of the visible values from the app into the function as its arguments in the following format: `"<component_id>_<property>"`.
- `outputs` represents dashboard components properties that are affected with custom action function return value. Similar to `inputs`, it can be defined aa a list of strings in the following format: `"<component_id>.<property>"`.

The following example shows how to create a custom action that prints the data of the clicked point in the graph to the console.

??? example "Custom Dash DataTable"
=== "app.py"
```py
import vizro.models as vm
import vizro.plotly.express as px
from vizro import Vizro
from vizro.actions import filter_interaction
from vizro.models.types import capture


@capture("action")
def my_custom_action(scatter_chart_clickData: dict = None):
"""Custom action."""
if scatter_chart_clickData:
return f'Scatter chart clicked data:\n### Species: "{scatter_chart_clickData["points"][0]["customdata"][0]}"'
return "### No data clicked."


df = px.data.iris()

page = vm.Page(
title="Example of a custom action with UI inputs and outputs",
layout=vm.Layout(
grid=[
[0, 0, 0, 1],
[0, 0, 0, -1],
[0, 0, 0, -1],
[0, 0, 0, -1],
[2, 2, 2, -1],
[2, 2, 2, -1],
[2, 2, 2, -1],
[2, 2, 2, -1],
],
row_gap="25px",
),
components=[
vm.Graph(
id="scatter_chart",
figure=px.scatter(df, x="sepal_length", y="petal_width", color="species", custom_data=["species"]),
actions=[
vm.Action(function=filter_interaction(targets=["scatter_chart_2"])),
vm.Action(
function=my_custom_action(),
inputs=["scatter_chart.clickData"],
outputs=["card_id.children"]
),
],
),
vm.Card(id="card_id", text="### No data clicked."),
vm.Graph(
id="scatter_chart_2",
figure=px.scatter(df, x="sepal_length", y="petal_width", color="species"),
),
],
controls=[vm.Filter(column="species", selector=vm.Dropdown(title="Species"))],
)

dashboard = vm.Dashboard(pages=[page])

Vizro().build(dashboard).run()
```
=== "app.yaml"
```yaml
# Custom action are currently only possible via python configuration
```

#### Custom action return value
TO BE ADDED!

---

!!! warning

Expand Down
13 changes: 7 additions & 6 deletions vizro-core/src/vizro/actions/_actions_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

from __future__ import annotations

from collections import defaultdict
from collections import defaultdict, namedtuple
from copy import deepcopy
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, TypedDict, Union
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union

import pandas as pd

Expand Down Expand Up @@ -84,8 +84,9 @@ def _apply_graph_filter_interaction(

customdata = ctd_click_data["value"]["points"][0]["customdata"]

# TODO: BUG: fix if there is filter_interaction and other actions assigned to the same 'vm.Graph.actions' .
for action in source_graph_actions:
if target not in action.function["targets"]:
if action.function._function.__name__ != "filter_interaction" or target not in action.function["targets"]:
continue
for custom_data_idx, column in enumerate(custom_data_columns):
data_frame = data_frame[data_frame[column].isin([customdata[custom_data_idx]])]
Expand Down Expand Up @@ -248,7 +249,7 @@ def _get_modified_page_figures(
ctds_parameters: List[CallbackTriggerDict],
ctd_theme: CallbackTriggerDict,
targets: Optional[List[ModelID]] = None,
) -> Dict[ModelID, Any]:
) -> Tuple[Any, ...]:
if not targets:
targets = []
filtered_data = _get_filtered_data(
Expand All @@ -262,12 +263,12 @@ def _get_modified_page_figures(
parameters=ctds_parameters,
)

outputs = {}
outputs: Dict[ModelID, Any] = {}
for target in targets:
outputs[target] = model_manager[target]( # type: ignore[operator]
data_frame=filtered_data[target], **parameterized_config[target]
)
if hasattr(outputs[target], "update_layout"):
outputs[target].update_layout(template="vizro_dark" if ctd_theme["value"] else "vizro_light")

return outputs
return namedtuple("outputs", outputs.keys())(**outputs)
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,9 @@ def _get_export_data_callback_outputs(action_id: ModelID) -> Dict[str, List[Stat
targets = _get_components_with_data(action_id=action_id)

return {
f"download-dataframe_{target}": Output(
f"download_dataframe_{target}": Output(
component_id={
"type": "download-dataframe",
"type": "download_dataframe",
"action_id": action_id,
"target_id": target,
},
Expand All @@ -231,7 +231,7 @@ def _get_export_data_callback_components(action_id: ModelID) -> List[dcc.Downloa
return [
dcc.Download(
id={
"type": "download-dataframe",
"type": "download_dataframe",
"action_id": action_id,
"target_id": target,
},
Expand Down
4 changes: 2 additions & 2 deletions vizro-core/src/vizro/actions/_filter_action.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Pre-defined action function "_filter" to be reused in `action` parameter of VizroBaseModels."""

from typing import Any, Callable, Dict, List
from typing import Any, Callable, Dict, List, Tuple

import pandas as pd
from dash import ctx
Expand All @@ -18,7 +18,7 @@ def _filter(
targets: List[ModelID],
filter_function: Callable[[pd.Series, Any], pd.Series],
**inputs: Dict[str, Any],
) -> Dict[ModelID, Any]:
) -> Tuple[Any, ...]:
"""Filters targeted charts/components on page by interaction with `Filter` control.
Args:
Expand Down
4 changes: 2 additions & 2 deletions vizro-core/src/vizro/actions/_on_page_load_action.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Pre-defined action function "_on_page_load" to be reused in `action` parameter of VizroBaseModels."""

from typing import Any, Dict
from typing import Any, Dict, Tuple

from dash import ctx

Expand All @@ -13,7 +13,7 @@


@capture("action")
def _on_page_load(page_id: ModelID, **inputs: Dict[str, Any]) -> Dict[ModelID, Any]:
def _on_page_load(page_id: ModelID, **inputs: Dict[str, Any]) -> Tuple[Any, ...]:
"""Applies controls to charts on page once the page is opened (or refreshed).
Args:
Expand Down
4 changes: 2 additions & 2 deletions vizro-core/src/vizro/actions/_parameter_action.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Pre-defined action function "_parameter" to be reused in `action` parameter of VizroBaseModels."""

from typing import Any, Dict, List
from typing import Any, Dict, List, Tuple

from dash import ctx

Expand All @@ -12,7 +12,7 @@


@capture("action")
def _parameter(targets: List[str], **inputs: Dict[str, Any]) -> Dict[ModelID, Any]:
def _parameter(targets: List[str], **inputs: Dict[str, Any]) -> Tuple[Any, ...]:
"""Modifies parameters of targeted charts/components on page.
Args:
Expand Down
13 changes: 7 additions & 6 deletions vizro-core/src/vizro/actions/export_data_action.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Pre-defined action function "export_data" to be reused in `action` parameter of VizroBaseModels."""

from typing import Any, Dict, List, Optional
from collections import namedtuple
from typing import Any, Dict, List, Optional, Tuple

from dash import ctx, dcc
from typing_extensions import Literal
Expand All @@ -18,7 +19,7 @@ def export_data(
targets: Optional[List[ModelID]] = None,
file_format: Literal["csv", "xlsx"] = "csv",
**inputs: Dict[str, Any],
) -> Dict[str, Any]:
) -> Tuple[Any, ...]:
"""Exports visible data of target charts/components on page after being triggered.
Args:
Expand All @@ -38,7 +39,7 @@ def export_data(
targets = [
output["id"]["target_id"]
for output in ctx.outputs_list
if isinstance(output["id"], dict) and output["id"]["type"] == "download-dataframe"
if isinstance(output["id"], dict) and output["id"]["type"] == "download_dataframe"
]
for target in targets:
if target not in model_manager:
Expand All @@ -50,16 +51,16 @@ def export_data(
ctds_filter_interaction=ctx.args_grouping["filter_interaction"],
)

callback_outputs = {}
outputs = {}
for target_id in targets:
if file_format == "csv":
writer = data_frames[target_id].to_csv
elif file_format == "xlsx":
writer = data_frames[target_id].to_excel
# Invalid file_format should be caught by Action validation

callback_outputs[f"download-dataframe_{target_id}"] = dcc.send_data_frame(
outputs[f"download_dataframe_{target_id}"] = dcc.send_data_frame(
writer=writer, filename=f"{target_id}.{file_format}", index=False
)

return callback_outputs
return namedtuple("outputs", outputs.keys())(**outputs)
4 changes: 2 additions & 2 deletions vizro-core/src/vizro/actions/filter_interaction_action.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Pre-defined action function "filter_interaction" to be reused in `action` parameter of VizroBaseModels."""

from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple

from dash import ctx

Expand All @@ -15,7 +15,7 @@
def filter_interaction(
targets: Optional[List[ModelID]] = None,
**inputs: Dict[str, Any],
) -> Dict[ModelID, Any]:
) -> Tuple[Any, ...]:
"""Filters targeted charts/components on page by clicking on data points or table cells of the source chart.
To set up filtering on specific columns of the target graph(s), include these columns in the 'custom_data'
Expand Down
Loading

0 comments on commit d62a72c

Please sign in to comment.