Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release of the custom actions #178

Merged
merged 18 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!--
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. ([#178](https://github.com/mckinsey/vizro/pull/178))
petar-qb marked this conversation as resolved.
Show resolved Hide resolved
petar-qb marked this conversation as resolved.
Show resolved Hide resolved

<!--
### 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))

-->
168 changes: 165 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,171 @@ 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
petar-qb marked this conversation as resolved.
Show resolved Hide resolved

!!! 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 "Simple custom action"
=== "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 action with UI inputs and outputs"
=== "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
The return value of the custom action function is propagated to the dashboard components that are defined in the `outputs` argument of the [`Action`][vizro.models.Action] model.
If there is a single `output` defined, the function return value is directly assigned to the component property.
If there are multiple `outputs` defined, the return value is iterated and assigned to the respective component properties, in line with Python's flexibility in managing multiple return values.

!!! tip
There is still a possibility to match return values to multiple `outputs` in a desired order by using [`collections.abc.namedtuple`][https://docs.python.org/3/library/collections.html#namedtuple-factory-function-for-tuples-with-named-fields].
Here is an example on how would look like the custom action function from the previous example if we would like to match the return value to multiple `outputs` in a desired order:
```py
from collections import namedtuple
@capture("action")
def my_custom_action(scatter_chart_clickData: dict = None):
"""Custom action."""
if scatter_chart_clickData:
return_value = {
"card_id_children": f'Scatter chart clicked data:\n### Species: "{scatter_chart_clickData["points"][0]["customdata"][0]}"'}
}
return namedtuple("x", return_value.keys())(*return_value.values())
return_value = {"card_id_children": "### No data clicked."}
return namedtuple("x", return_value.keys())(*return_value.values())
```

---

!!! 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"]:
petar-qb marked this conversation as resolved.
Show resolved Hide resolved
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)
petar-qb marked this conversation as resolved.
Show resolved Hide resolved
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",
petar-qb marked this conversation as resolved.
Show resolved Hide resolved
"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)
Loading
Loading