mckinsey / vizro

Vizro is a toolkit for creating modular data visualization applications.
https://vizro.readthedocs.io/en/stable/
Apache License 2.0
2.66k stars 138 forks source link

Relationship between control's targets and its actions outputs #800

Closed gtauzin closed 1 week ago

gtauzin commented 1 week ago

Question

Hi, First of all thank you so much for the work you put in vizro, it seems to be absolutely amazing!

I am making my first dashboards and I am getting confused about the relationship between a control's targets and its actions' outputs. The example below showcases the points that confuse me, but my questions are:

Code/Examples

The code below can be ran on PyCafe

from vizro import Vizro
import pandas as pd
import vizro.plotly.express as px
import vizro.models as vm
from vizro.models.types import capture

from vizro.managers import data_manager

def load_iris_data(number_of_points=10, fake_param=False): 
    iris = px.data.iris()
    return iris.sample(number_of_points) 

data_manager["iris"] = load_iris_data 

@capture("action")
def get_min_nop(value):
    return value * 10

page = vm.Page(
    title="Update the chart on page refresh",
    components=[
        vm.Graph(id="graph", figure=px.box("iris", x="species", y="petal_width", color="species")) 
    ],
    controls=[
        vm.Parameter(
            # The fact that the target is the previous control makes it impossible to
            # put it first in the control list and then to have it displayed first on the left panel
            targets=["graph.data_frame.fake_param"], # Introducing a fake target is the only way I could put this control first
            selector=vm.Slider(
                id="dropdown-ms",
                min=0, max=3, step=1, value=0, title="Min slider",
                actions=[
                    vm.Action(
                        function=get_min_nop(), 
                        inputs=["dropdown-ms.value"], 
                        outputs=["slider-nop.min"], # Is it not an issue that it's not a subset of the targets?
                    )
                ],
            ),
        ),
        vm.Parameter(
            targets=["graph.data_frame.number_of_points"], 
            selector=vm.Slider(
                id="slider-nop",
                min=10, max=100, step=10, value=10,
            ),
        ),
    ],
)

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

Vizro().build(dashboard).run()

Thank you!

Which package?

vizro

Code of Conduct

petar-qb commented 1 week ago

Hi @gtauzin 👋

I see what caused ambiguities (thanks for pointing out to this). I'm happy to improve the configuration UX (and the documentation) to make it more clear for all other users as well.

How should I think about a control's targets? I understand that when there are no actions, the selectors' value is directly mapped to the targets. But when there are actions, it seems to be ignored. Is this correct?

Yes, that's correct. Here's related content I posted in the previous Issue -> https://github.com/mckinsey/vizro/issues/799? So, control's "column" and "targets" are just the "configuration shortcut" for users so they don't have to write the entire configuration like this:

vm.Filter(
            column="species",
            targets=["graph"],
            selector=vm.Dropdown(
                options=[{"label": i, "value": i} for i in ["setosa", "versicolor", "virginica"]],
                actions=[
                    vm.Action(
                        function=_filter(filter_column="species", targets=["graph"], filter_function=_filter_isin)
                    )
                ]
            )
        )

but can only do something like:

vm.Filter(column="species")

and the rest of the configuration is internally calculated by Vizro. So, how Vizro calculates the entire model based on the single column="species" input, you can find in the src/vizro/models/_controls/filter.py.


Also, it's worth to mention the main difference between the predefined and custom action here.

Here are some examples of the already predefined actions: _filter - _filter(filter_column="species", targets=["graph"], filter_function=_filter_isin), export_data - export_data(targets=['graph']), filter_interaction - filter_interaction(targets=['graph'])

The get_min_nop is the great example of the custom action.

The main difference between predefined and custom actions is that for the predefined actions its inputs and outputs are calculated based on the function arguments (like "targets"). So, you don't have to add "outputs" to the export_data or list its "inputs" like "all filter selectors on the page that target exported chart" (all this is internally calculated by Vizro). In case of the custom actions, Vizro can't automatically calculate its "inputs" and "outputs" because Vizro doesn't know the "nature" of this action.

So, to summarise it: Based on some controls's arguments (like parameter "targets" argument), Vizro creates the predefined action vm.Action(function=_parameter(targets=["graph"])) and assigns it to the parameter selector.actions (if some other actions are not already assigned to the parameter selector component). Based on the action's argument "targets" (_parameter(targets=["graph"])), Vizro calculates the action's outputs (-> outputs=["graph.figure"]). So, the predefined actions (actions that have arguments but don't have "inputs" and "outputs" defined) are converted to the actions that have "inputs" and "outputs" defined (Vizro calculates this based on the function arguments like "targets").

"Yes, we can say that any predefined action is also a custom action, but custom actions are not the predefined actions. 😄"


Whey you say: "targets are ignored", you're correct. Targets are "ignored" if some other action is assigned to the "parameter.selector.actions" (this assigned action overwrites the default one). The "targets" parameter argument matters only if you don't set the "parameter.selector.actions" (in that case Vizro will add a _parameter action and assign it to the "parameter.selector.actions").

...switching the order of both controls will raise an error as the previous control has not yet been defined

Can you send me an example of this? Somehow I can't reproduce it..

gtauzin commented 1 week ago

Thank you so much @petar-qb for the detailed answer, it's much clearer now :)

Regarding the ordering, sorry my example was indeed not clear.

I have made another one in PyCafe:

from vizro import Vizro
import pandas as pd
import vizro.plotly.express as px
import vizro.models as vm
from vizro.models.types import capture

from vizro.managers import data_manager

def load_iris_data(number_of_points=10): 
    iris = px.data.iris()
    return iris.sample(number_of_points) 

data_manager["iris"] = load_iris_data 

@capture("action")
def get_min_nop(value):
    return value * 10

page = vm.Page(
    title="Update the chart on page refresh",
    components=[
        vm.Graph(id="graph", figure=px.box("iris", x="species", y="petal_width", color="species")) 
    ],
    controls=[
        vm.Parameter(
            # I want this first so I have to "find" a target that is different from the
            # one of the other controls and is already fully defined: for readability
            # I would have liked to use `targets=["slider-nop.min"]`
            # targets=["slider-nop.min"], # Using this will raise
            # Alternatively, as there is a custom action  and targets is ignored I would have liked to use
            # targets=None, # This would let me do whatever w.r.t. ordering controls
            targets=["graph.x"], # Introducing a fake target is the only way I could put this control first
            selector=vm.Slider(
                id="dropdown-ms",
                min=0, max=3, step=1, value=0, title="Min slider",
                actions=[
                    vm.Action(
                        function=get_min_nop(), 
                        inputs=["dropdown-ms.value"], 
                        outputs=["slider-nop.min"],
                    )
                ],
            ),
        ),
        vm.Parameter(
            targets=["graph.data_frame.number_of_points"], 
            selector=vm.Slider(
                id="slider-nop",
                min=10, max=100, step=10, value=10,
            ),
        ),
    ],
)

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

Vizro().build(dashboard).run()

I think what confuse me is that when I have a control with a custom action, the targets is ignored, but it still plays a role as it needs to be valid and can influence the validity of the order of the controls as well. In my example, for readability purposes, I first tried to set targets=["slider-nop.min"] as it is the action's output and thus seemed more natural. This is valid only when it is in 2nd position, because in 1st, "slider-nop" is not yet defined. To put it in first, I can't write targets=None, as None is not a valid target, so I have to get creative and ask myself: "What is a valid, already defined target that I can put here so that the validation does not raise?". In the example I randomly ended putting targets=["graph.x"]. Do I understand this properly? How would you advise I handle such a case?

Maybe a side question: Is there any way I can move/reorganize the selectors on the dashboard? Like changing the order, the position, the component they are on, etc...

Thanks again!

petar-qb commented 1 week ago

I think what confuse me is that when I have a control with a custom action, the targets is ignored, but it still plays a role as it needs to be valid and can influence the validity of the order of the controls as well.

Ah I see! You're confused because "targets" argument is still validated even it's not practically used. That's already noticed bad UX and will be solved with https://github.com/mckinsey/vizro/pull/363 PR. So, "targets" argument is actually only used as a "configuration shortcut" for users when vm.Filter and vm.Parameter components are defined. Based on this "targets" argument, outputs for private predefined _filter and _parameter action are calculated.

However, let's see how can you make your app working. Generally, the order of the components and actions in Vizro should not make any issues (except this weird targets issue you mentioned). Also, the vm.Filter and the vm.Parameter controls are made to be used just as shortcuts for _filter and _parameter actions. If you want to add a custom action to the control on the left side of the screen, then there's no need to use vm.Filter or vm.Parameter, but you should use the e.g. vm.Slider directly. (P.S. don't forget to explicitly add the vm.Slider as allowed type - see below)

from vizro import Vizro
import pandas as pd
import vizro.plotly.express as px
import vizro.models as vm
from vizro.models.types import capture

from vizro.managers import data_manager

def load_iris_data(number_of_points=10):
    iris = px.data.iris()
    return iris.sample(number_of_points)

data_manager["iris"] = load_iris_data

@capture("action")
def get_min_nop(value):
    return value * 10

# TODO - Important: Explicitly add the type to the Page.controls
vm.Page.add_type("controls", vm.Slider)

page = vm.Page(
    title="Update the chart on page refresh",
    components=[
        vm.Graph(id="graph", figure=px.box("iris", x="species", y="petal_width", color="species"))
    ],
    controls=[
        vm.Slider(
            id="dropdown-ms",
            min=0, max=3, step=1, value=0, title="Min slider",
                actions=[
                vm.Action(
                    function=get_min_nop(),
                    inputs=["dropdown-ms.value"],
                    outputs=["slider-nop.min"],
                )
            ],
        ),
        vm.Parameter(
            targets=["graph.data_frame.number_of_points"],
            selector=vm.Slider(
                id="slider-nop",
                min=10, max=100, step=10, value=10,
            ),
        ),
    ],
)

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

Vizro().build(dashboard).run()
gtauzin commented 1 week ago

Thanks @petar-qb! This looks very neat.