mckinsey / vizro

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

How to custom action for selector feature #770

Open yaoyi92 opened 1 day ago

yaoyi92 commented 1 day ago

Question

Dear all,

I hope to use click data on the scatter plot to control the input of another figure. I can use the control component to achieve the function of control the figure. I am also able to extract clickData from the action. However, not sure how to pass the information in the custom action to another figure.

This works

        vm.Parameter(
            targets=["my_bio_figure.material"],
            selector=vm.Dropdown(options=list(df_perovskite.material), value="YTcO3", multi=False),
        )

This doesn't work

                    actions=[vm.Action(function=select_interaction(), inputs=["scatter_chart.clickData"],outputs=["my_bio_figure.material"])]

The full code


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

import dash_bio as dashbio
import dash_bio.utils.ngl_parser as ngl_parser

import ase.db
from pymatgen.io.ase import AseAtomsAdaptor
import nvcs
from pymatgen.core import Structure
import nglview
from vizro.actions import filter_interaction

df_perovskite = pd.read_csv("cubic.csv")
structure_db = ase.db.connect("perovskites.db")
data_path = "https://raw.githubusercontent.com/plotly/datasets/master/Dash_Bio/Molecular/"

@capture("figure")
def custom_bio_molecule_figure(data_frame, material):

    atoms = structure_db.get_atoms(material)
    pmg_structure = AseAtomsAdaptor().get_structure(atoms)
    sites_displayed = nvcs.structure._get_displayed(pmg_structure)
    pmg_structure_displayed = Structure.from_sites([si.site for si in sites_displayed])
    atoms_displayed = AseAtomsAdaptor().get_atoms(pmg_structure_displayed)

    ngl_ase_adaptor = nglview.ASEStructure(atoms_displayed)
    #data_list = [ngl_ase_adaptor.get_structure_string()]
    content = ngl_ase_adaptor.get_structure_string()
    data = {
        'filename': material,
        'ext': 'pdb',
        'selectedValue': '1',
        'chain': 'ALL',
        'aaRange': 'ALL',
        'chosen': {'atoms':'', 'residues':''},
        'color': '#e41a1c',
        'config': {'type': 'text/plain', 'input': content},
        'resetView': True,
        'uploaded': True
    }
    data_list = [data]
    print(data_list)
    molstyles_dict = {
        "representations": ["ball+stick", 'unitcell'],
    }

    return dashbio.NglMoleculeViewer(
        id="ngl_molecule_viewer_id",
        data=data_list,
        molStyles=molstyles_dict,
    )

@capture("action")
def select_interaction(clickData):
    """Returns the input value."""
    material = clickData['points'][0]['customdata'][0]
    print(material)
    #print(clickData["custom_data"])
    #return clickData["custom_data"]
    return material

page = vm.Page(
    title="Perovskites",
    layout=vm.Layout(grid=[[0, 0, 1],
                           [0, 0, 2]]),
    components=[vm.AgGrid(figure=dash_ag_grid(data_frame=df_perovskite)),
                vm.Graph(
                    id = "scatter_chart",
                    figure = px.scatter(df_perovskite, x="lattice_constant (AA)", y="bulk_modulus (eV/AA^3)", custom_data = ["material"]), 
                    actions=[vm.Action(function=select_interaction(), inputs=["scatter_chart.clickData"],outputs=["my_bio_figure.material"])]
                    ),
                vm.Figure(id="my_bio_figure", figure=custom_bio_molecule_figure(data_frame=pd.DataFrame(), material="YTcO3")),
                ],
    controls=[
        vm.Parameter(
            targets=["scatter_chart.x"],
            selector=vm.Dropdown(options=list(df_perovskite.columns), value="lattice_constant (AA)", multi=False),
        ),
        vm.Parameter(
            targets=["scatter_chart.y"],
            selector=vm.Dropdown(options=list(df_perovskite.columns), value="bulk_modulus (eV/AA^3)", multi=False),
        ),
        vm.Parameter(
            targets=["my_bio_figure.material"],
            selector=vm.Dropdown(options=list(df_perovskite.material), value="YTcO3", multi=False),
        ),
    ]
)

dashboard = vm.Dashboard(pages=[page], theme="vizro_light")

if __name__ == "__main__":
    Vizro().build(dashboard).run()

Thank you very much for your help.

Code/Examples

No response

Which package?

vizro

Code of Conduct

petar-qb commented 1 day ago

Hi @yaoyi92 👋 and thanks for the great question! So, you want to parametrise custom graph creation function by clicking a data point on the source chart (instead of selecting a value from the vm.Parameter directly). This is the upcoming feature we call "parameter_interaction" (similar to filter_interaction action) and it's in our development roadmap.

Even though it's currently unavailable to implement this behaviour completely natively through the Vizro configuration (e.g. by defining a "parameter_interaction" action on the source chart), it's possible to achieve the same in another way.

Here's an example where by clicking on the ag_grid "Graph_type" column cell, you parametrise the custom graph argument called "graph_type". The example is hosted on Py Cafe and you can access its source code by clicking on the "✏️ EDITOR" button in the top right corner.

The solution is made by utilising pure dash callback mechanism and the clicked value actually goes over the vm.Parameter control that is hidden with the CSS from the assets/custom.css (you can comment out this CSS to see how it actually works). Your case is pretty similar (as I understood), just instead of implementing a callback that propagates the clicked cell value from the ag_grid, you should propagate the clicked data point from the scatter graph.

yaoyi92 commented 17 hours ago

Dear @petar-qb , nice to hear it's a feature under development.

Thank you for the solution. Using your strategy I can connect click on aggrid to the figure. However, I am still not sure how to connect to the scatter plot. The difficulty I am facing now is I am not sure how to get the underlying scatter plot id.

               vm.Graph(
                    id = "scatter_chart",
                    figure = px.scatter(df_perovskite, x="lattice_constant (AA)", y="bulk_modulus (eV/AA^3)", custom_data = ["material"]), 
                    #actions=[vm.Action(function=select_interaction(), inputs=["scatter_chart.clickData"],outputs=["my_bio_figure.material"])]
                    ),

I got TypeError: got an unexpected keyword argument 'id' if I try to add an id in the px.scatter arguments. Do we have an id for the px.scatter?

    figure = px.scatter(df_perovskite, x="lattice_constant (AA)", y="bulk_modulus (eV/AA^3)", custom_data = ["material"], id="underlying_scatter_chart"), 
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/yiy/miniconda3/envs/molecular_simulation/lib/python3.12/site-packages/vizro/models/types.py", line 379, in wrapped
    captured_callable: CapturedCallable = CapturedCallable(func, *args, **kwargs)
                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/yiy/miniconda3/envs/molecular_simulation/lib/python3.12/site-packages/vizro/models/types.py", line 94, in __init__
    self.__bound_arguments = inspect.signature(function).bind_partial(*args, **kwargs).arguments
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/yiy/miniconda3/envs/molecular_simulation/lib/python3.12/inspect.py", line 3266, in bind_partial
    return self._bind(args, kwargs, partial=True)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/yiy/miniconda3/envs/molecular_simulation/lib/python3.12/inspect.py", line 3248, in _bind
    raise TypeError(
TypeError: got an unexpected keyword argument 'id'
yaoyi92 commented 16 hours ago

Another approach I tried is to use the action to change the control vm.Parameter.

actions=[vm.Action(function=select_interaction(), inputs=["scatter_chart.clickData"],outputs=["selector_material_id.value"])]

with

@capture("action")
def select_interaction(clickData):
    """Returns the input value."""
    material = clickData['points'][0]['customdata'][0]
    print(material)
    return material

The dropdown value will change in this case, but it seems the value won't propagate to the final target of targets=["my_bio_figure.material"],

petar-qb commented 4 hours ago

Hi @yaoyi92 👋

Thanks for pointing out to this unintuitive UX with IDs. We will explain it better in the docs. So, vm.Graph has only one ID (outer ID) that is used as input or output of the action (there's no underlying figure ID for graphs). However, objects like vm.Table and vm.AgGrid have two IDs, the outer and and underlying ID. Outer ID represents the ID of the html div that wraps the real table object (that has the underlying ID) and it's mostly used as the action's output (when you return entire new object from the action). And, underlying ID is used mostly as the action's input when you have to fetch clickedCell or any similar property from the table object.

As the source of your app interaction is vm.Graph, it means you have to use its outer ID (the only one that exists). Also, we have to use the pure dash callback to propagate clicked graph point to vm.Parameter and that's the limitation we want to overcome natively in Vizro.

Here's another example that shows how you can make interaction from vm.Graph to the custom vm.Figure function argument -> https://py.cafe/app/petar-qb/vizro-graph-parameter-interaction