mckinsey / vizro

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

Custom action targeting custom graph inputs / graph function parameters #567

Closed szt-ableton closed 4 months ago

szt-ableton commented 4 months ago

Question

Hey team, I have a question regarding the use of custom actions. I would like to be able to click on a cell on my AgGrid element, and for the value of the click to update a parameter in the function that builds my graph. However, I am not sure how to target a parameter within a function that builds the Graph component.

Below is an example - where I am able to control the text in a card element ( as in a standard example published by you). Instead, I would like to control specific inputs to my function (the commented out mainfig.question example). To be more clear, basically, I would like this custom action to work as a parameter - being able to change the question input to my custom graph.

How is it possible to target custom graph inputs through custom actions? If that is not possible, is it possible to target the selected parameter somehow?

Thank you so much for the amazing work on this tool, and for the help in advance!

Code/Examples


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
import pandas as pd
from vizro.tables import dash_ag_grid
import vizro.plotly.express as px

import json  # Testing

data = pd.read_csv('./develop/lsurvey.csv')
questions = pd.DataFrame(data['question'].drop_duplicates())
segments = pd.DataFrame(list(data[data['question_type']=='single_choice']['question'].unique()) + ['none'], columns = ['segment'])

df = px.data.iris()

@capture("action")
def my_custom_action(clickedcell: dict):
    """Custom action."""
    # text = json.dumps(clickedcell)

    # return lambda: f"You clicked on cell with value: {text}"  # Return an update function
    return (
        json.dumps(clickedcell["value"])
        if clickedcell
        else "Click on a point on the above graph(func)."
    )

@capture("graph")
def build_selected_figure(data_frame, question, segmentation):
    fig=px.scatter_matrix(
                     data_frame = data_frame
        , dimensions=["sepal_length", "sepal_width", "petal_length", "petal_width"]
        , color="species"
        , title = question
        ),

    return fig

page = vm.Page(
    title="Example of a custom action with UI inputs and outputs",
    path="custom_actions",
    id="custom_actions",
    layout=vm.Layout(
        grid=[[0, 1], [2, 2]],
        row_gap="25px",
    ),
    components=[
        vm.AgGrid(
            id="cell-selection-simple-click-callback",
            figure=dash_ag_grid(
                id="cell-selection-simple-click-callback-grid",
                data_frame=data,
                rowData=data.to_dict("records"),
                columnDefs=[{"field": i} for i in data.columns],
                defaultColDef={"filter": True},
                columnSize="sizeToFit",
                getRowId="params.data.State",
                dashGridOptions={"animateRows": False},
            ),
            actions=[
                vm.Action(
                    function=my_custom_action(),
                    inputs=["cell-selection-simple-click-callback-grid.cellClicked"],
                    #outputs=["mainfig.question"],
                    outputs=[mycard.children]
                ),
            ],
        ),
        vm.Card(id="my_card", text="Click on a point on the above graph."),
        vm.Graph(id = 'mainfig'
                 , figure=build_selected_figure(data_frame = df, question = "How satisfied are you with the product overall?", segmentation = 'none'),
        ),
    ],
    controls=[
        vm.Parameter(
          id="segment",
            targets=['mainfig.question'],
            selector=CustomRadioItems(
                # options= all_segments,
                options = questions,
                title='questionz',
            ),
        ),
)

dashboard = vm.Dashboard(pages=[page])
Vizro().build(dashboard).run()

Other information

No response

Which package?

vizro

Package version

No response

Python version

No response

OS

No response

Code of Conduct

antonymilne commented 4 months ago

Hello @szt-ableton and thank you for your comments and for asking this excellent question!

What you describe is something we would call a parameter interaction. It's not natively built into vizro yet as an action but will be in future. You can already achieve what you need a couple of ways though.

Approach 1 below does what you described: use a custom action to update a graph when you click on a cell of the AgGrid. I've added cellClicked data into a vm.Card just so you can easily see what information is available there.

Approach 2 does things a bit differently:

Currently I think the easiest/only way to do Approach 2 is by using a pure Dash callback rather than action (but let's discuss this when you're back from holiday @petar-qb).

In my mind there are actually a couple of advantages of Approach 2:

This is a topic we're very actively discussing now so I am very curious why you would like to take the first approach instead of updating a parameter - please can you explain?

FYI @Joseph-Perkins @maxschulz-COL


Approach 1: update target graph directly

import json

from dash import Output, Input, callback
from dash.exceptions import PreventUpdate

import vizro.models as vm

import vizro.plotly.express as px
from vizro import Vizro
from vizro.models.types import capture
from vizro.tables import dash_ag_grid

df = px.data.iris()
graph = vm.Graph(id="graph", figure=px.box(df, x="petal_width", color="species"))

@capture("action")
def f(data):
    # Put the JSON dump of data in ``` to show it as code in markdown
    code_markdown = f"```\n{json.dumps(data, indent=2)}\n```"
    # graph() is basically a shortcut for saying "re-run the vm.Graph's figure function but with x=data["colId"]
    # The data used will be *unfiltered* df
    return graph(x=data["colId"]), code_markdown

page = vm.Page(
    title="Demo",
    components=[
        vm.AgGrid(
            figure=dash_ag_grid(id="grid", data_frame=df),
            actions=[vm.Action(function=f(), inputs=["grid.cellClicked"], outputs=["graph.figure", "card.children"])],
        ),
        graph,
        vm.Card(id="card", text="Click on a cell on the grid and the box plot will show that column"),
    ],
    controls=[
        vm.Parameter(targets=["graph.x"], selector=vm.RadioItems(id="radio", options=list(df.columns[:4]))),
        vm.Filter(column="species"),
    ],
)
dashboard = vm.Dashboard(pages=[page])

Approach 2: update parameter

import json

from dash import Output, Input, callback
from dash.exceptions import PreventUpdate

import vizro.models as vm

import vizro.plotly.express as px
from vizro import Vizro
from vizro.models.types import capture
from vizro.tables import dash_ag_grid

df = px.data.iris()
graph = vm.Graph(id="graph", figure=px.box(df, x="petal_width", color="species"))

@callback(Output("radio", "value"), Output("card", "children"), Input("grid", "cellClicked"))
def g(data):
    if data is None:
        raise PreventUpdate
    code_markdown = f"```\n{json.dumps(data, indent=2)}\n```"
    return data["colId"], code_markdown

page = vm.Page(
    title="Hi",
    components=[
        vm.AgGrid(
            figure=dash_ag_grid(id="grid", data_frame=df),
        ),
        graph,
        vm.Card(id="card", text="Click on a cell on the grid and the box plot will show that column"),
    ],
    controls=[
        vm.Parameter(targets=["graph.x"], selector=vm.RadioItems(id="radio", options=list(df.columns[:4]))),
        vm.Filter(column="species"),
    ],
)
dashboard = vm.Dashboard(pages=[page])

Vizro().build(dashboard).run(debug=True)
szt-ableton commented 4 months ago

Hey @antonymilne

thank you so much for the fast and clear answer, it saved me a lot of confusion! Hats off to this project!

As for your question - I did consider manipulating the parameter directly (asked if its possible in the question as well). The reason I focused on Approach 1 is twofold - and none of them about any strong preference:

  1. I simply did not find examples in the vizro docs / youtube videos where parameters were manipulated, so assumed this would be the trickier way, and so investigated it less then custom actions.
  2. The parameter I am filtering for is actually quite a long list of strings (survey questions). I wanted to avoid cluttering the navigation bar, as I also have other filters coming on top of this. (I still need to use a modified dropdown menu instead of radio, where I need to add in padding and enough space for the longer string menu items - otherwise my texts overlap. Maybe this is a future feature improvement? :) )

However, I do see that the arguments you bring for Approach 2 are valid, so I will go with that one! (to avoid overwriting other params and maybe for transparency as well)

Thanks a lot again and wish you some fun bringing these great features to the people!

antonymilne commented 4 months ago

@szt-ableton Great, thank you for the extra information - that's all very useful feedback!

Just on the dropdown point, this is great to know and we actually had another ask for something similar last week. Are you able to share the code for your custom dropdown menu just to see how you solved it? @lingyielia suggested for the other user to use optionHeight. if this is a common problem then indeed we might be able to build something in to make this easier to format nicely (maybe it could even automatically choose a right height depending on number of characters). wdyt @huong-li-nguyen?

@maxschulz-COL @Joseph-Perkins this is a great example we should talk about when we discuss the question "should all interactions be done through controls". My current feeling is definitely yes, though maybe eventually we should add a visible option to Parameter/Filter to make it easier to hide (already easy through CSS though). This would also be how we handle URL query parameters that we don't want to render on screen.

szt-ableton commented 4 months ago

@antonymilne I've also used optionHeight (plus dropdown_build_obj[self.id].style = {"min-width": "150px", "padding": "20px 20px"} for the menu itself.)

antonymilne commented 4 months ago

Perfect, thank you! I'll close this issue then but please do feel free to open another if you have any more questions 🙂

antonymilne commented 4 months ago

FYI @szt-ableton we've merged a change that means optionsHeight should change itself automatically depending on the length of the options. https://github.com/mckinsey/vizro/pull/574. This will be in the next release, 0.1.19, which is probably later this week.

szt-ableton commented 4 months ago

amaaziing!