mckinsey / vizro

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

Dynamic AgGrid Table on Page Components #604

Closed BalaNagendraReddy closed 2 months ago

BalaNagendraReddy commented 2 months ago

Question

Based on the user selection from RadioItems or Checklist. I want to display that particular csv file in AgGrid format mainly using vm.AgGrid.

Sample Code.


@callback(Output("__table", "figure"), 
          Input("user-selection", "value"))
def display_ir_table(value):
    df = pd.read_csv(value)
    figure = dash_ag_grid(  
                data_frame=df,
                rowData = df.to_dict("records"),
                columnDefs= [{"field": i, 
                              'minWidth': 120, 
                              'maxWidth':200, 
                              "cellRenderer": "DynamicTooltip"} for i in df.columns],
                defaultColDef=defaultColDef,
                enableEnterpriseModules=True,
                dashGridOptions={"animateRows": False, 
                                "pagination": True,
                                "paginationPageSize": 100,
                                },
                className = "ag-theme-custom-theme",
            )
    return figure

vm.Page.add_type("controls", Selection)

# Define the page
page = vm.Page(
    title="MyPage",
    components=[
        vm.Tabs(
            tabs=[
                vm.Container(
                    title="Tab1",
                    components=[
                        # Based on the user selection, I want to read the file and display it here.   
                        vm.AgGrid(id = "table", figure = dash_ag_grid()), 
                    ],
                ),

                vm.Container(
                    title="Tab2",
                    components=[
                        vm.Card(text="My text here"),
                    ],
                ),
            ],
        ),
    ],
    controls=[
        Selection() 
    ],
)
petar-qb commented 2 months ago

Hi @BalaNagendraReddy and thanks for the question.

You seem to be looking for the "Parametrize data loading" feature that has been enabled since vizro>=0.1.17.

More on how to configure it can be found here in the Vizro docs.

BalaNagendraReddy commented 2 months ago

Hi @petar-qb ,

Thanks for your reply.

Expected Behavior: When a user selects an option from the radio buttons, the corresponding file should be loaded, and the vm.AgGrid DataTable should be updated with the relevant columns. This updated DataTable should then be passed to the appropriate page components.

Iam expecting to do this using callbacks.

petar-qb commented 2 months ago

Here is an example where you select between "Old iris dataset" and "New iris dataset" by selecting a RadioButton value. Based on the selected value in the RadioButton, a new dataset is loaded into the Graph. To develop this example, I used the "Parameterize Data Load" feature I mentioned in the comment above.

Let us know if this can help you develop your dashboard. 😃

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

from vizro.managers import data_manager

def mock_loading_old_iris_dataset():
    return px.data.iris()[:75]

def mock_loading_new_iris_dataset():
    return px.data.iris()[75:]

def load_iris_data(source_file="old"):
    if source_file == "old":
        return mock_loading_old_iris_dataset()
    else:
        return mock_loading_new_iris_dataset()

data_manager["iris"] = load_iris_data

page = vm.Page(
    title="Update the chart on page refresh",
    components=[
        vm.Graph(id="graph", figure=px.scatter("iris", x="sepal_length", y="petal_width", color="species"))
    ],
    controls=[
        vm.Parameter(
            targets=["graph.data_frame.source_file"],
            selector=vm.RadioItems(
                title="Select dataset",
                options=[
                    {"label": "Old Iris dataset", "value": "old"},
                    {"label": "New Iris dataset", "value": "new"},
                ],
                value="old"
            ),
        )
    ],
)

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

if __name__ == "__main__":
    Vizro().build(dashboard).run()
BalaNagendraReddy commented 2 months ago

Hi @petar-qb ,

I have nearly 30 options, and based on the selected input, the corresponding file should be displayed in Dash AG Grid format. For reference, I am attaching the code. I hope this clarifies my request.

import vizro.models as vm
import pandas as pd
import dash_bootstrap_components as dbc

from vizro import Vizro
from vizro.tables import dash_ag_grid
from dash import callback, Output, Input

from typing import Literal

dropdown_options = [{'label': 'File1', 'value': 'File1.csv'},{'label': 'File2', 'value': 'File2.csv' }, {'label': 'File3', 'value': 'file3.csv'}, {'label': 'File4', 'value': 'file4.csv'}]

class MyCard(vm.VizroBaseModel):
    """New custom component `CustomHTML`."""
    type: Literal["MyCard"] = "MyCard"

    def build(self):
        return dbc.Card(
            children= [
                dbc.DropdownMenu(
                    label= "Select File", 
                    children= dbc.RadioItems(options=dropdown_options, value=dropdown_options[0]['value'], id = "select-file"),
                    direction="down",
                    align_end = False,
                    className="m-1",
                    size ="sm",
                    toggle_style= {"fontSize": "12px","fontWeight": "bold", "borderRadius":"5px"},
                    toggleClassName="fst-italic border border-dark",
                    ),
                dbc.CardBody(id = "grid-table", children = [], style={'margin': '10px 10px 0px 10px'}),
                ],       
        )

@callback(Output("card", "children"), 
        Output("__input_grid-table", "figure"), 
          Input("select-file", "value"))
def display_grid_table(value):
    print(value)
    df = pd.read_csv(value)
    defaultColDef = {
        "flex": 1,
        "minWidth": 100,
        "floatingFilter": True,
        "initialWidth": 100,
        "wrapHeaderText": True,
        "autoHeaderHeight": True,
        "checkboxSelection": {
                    "function": 'params.column == params.columnApi.getAllDisplayedColumns()[0]'
                },
        "headerCheckboxSelection": {
            "function": 'params.column == params.columnApi.getAllDisplayedColumns()[0]'
        },
    }

    # Iam expecting the below table to be returned
    table = dash_ag_grid(  
                        id = "data_table",  
                        data_frame=df,
                        columnDefs= [{"field": i, 
                                        'minWidth': 120, 
                                        'maxWidth':200, 

                                        "cellRenderer": "DynamicTooltip"} for i in df.columns],
                        defaultColDef=defaultColDef,
                        enableEnterpriseModules=True,
                        dashGridOptions={"animateRows": False, 
                                        "pagination": True,
                                        "paginationPageSize": 100,
                                        "rowSelection": "multiple", 
                                        "suppressRowClickSelection": True,
                                        "enableCellTextSelection": True,
                                        "rowHeight": 25,
                                        "textAlignment": "center",
                                        "tooltipShowDelay": 10,
                                        },
                        className = "ag-theme-custom-theme",
            ),

    return f"selected File: {value}", table

vm.Page.add_type("components", MyCard)

page = vm.Page(
    title="Default Dash AG Grid",
    components=[
        MyCard(),
        vm.AgGrid(id = "grid-table", figure=dash_ag_grid(data_frame=pd.DataFrame())),
        vm.Card(id="card", text="Filename"),
        ]
)
dashboard = vm.Dashboard(pages=[page])

Vizro().build(dashboard).run()
petar-qb commented 2 months ago

Hi @BalaNagendraReddy

To make your dashboard to work you should apply a few tiny changes in your code. Here is the working example:

import vizro.models as vm
import pandas as pd
import dash_bootstrap_components as dbc

from vizro import Vizro
from vizro.tables import dash_ag_grid
from dash import callback, Output, Input

from typing import Literal

dropdown_options = [{'label': 'File1', 'value': 'File1.csv'}, {'label': 'File2', 'value': 'File2.csv'},
                    {'label': 'File3', 'value': 'file3.csv'}, {'label': 'File4', 'value': 'file4.csv'}]

class MyCard(vm.VizroBaseModel):
    """New custom component `CustomHTML`."""
    type: Literal["MyCard"] = "MyCard"

    def build(self):
        return dbc.Card(
            children=[
                dbc.DropdownMenu(
                    label="Select File",
                    children=dbc.RadioItems(options=dropdown_options, value=dropdown_options[0]['value'],
                                            id="select-file"),
                    direction="down",
                    align_end=False,
                    className="m-1",
                    size="sm",
                    toggle_style={"fontSize": "12px", "fontWeight": "bold", "borderRadius": "5px"},
                    toggleClassName="fst-italic border border-dark",
                ),
                dbc.CardBody(id="grid-table", children=[], style={'margin': '10px 10px 0px 10px'}),
            ],
        )

@callback(Output("card", "children"),
          Output("grid-table", "children"),
          Input("select-file", "value"))
def display_grid_table(value):
    print(value)
    df = pd.read_csv(value)
    defaultColDef = {
        "flex": 1,
        "minWidth": 100,
        "floatingFilter": True,
        "initialWidth": 100,
        "wrapHeaderText": True,
        "autoHeaderHeight": True,
        "checkboxSelection": {
            "function": 'params.column == params.columnApi.getAllDisplayedColumns()[0]'
        },
        "headerCheckboxSelection": {
            "function": 'params.column == params.columnApi.getAllDisplayedColumns()[0]'
        },
    }

    # Iam expecting the below table to be returned
    table = dash_ag_grid(
        id="data_table",
        data_frame=df,
        columnDefs=[{"field": i,
                     'minWidth': 120,
                     'maxWidth': 200,

                     "cellRenderer": "DynamicTooltip"} for i in df.columns],
        defaultColDef=defaultColDef,
        enableEnterpriseModules=True,
        dashGridOptions={"animateRows": False,
                         "pagination": True,
                         "paginationPageSize": 100,
                         "rowSelection": "multiple",
                         "suppressRowClickSelection": True,
                         "enableCellTextSelection": True,
                         "rowHeight": 25,
                         "textAlignment": "center",
                         "tooltipShowDelay": 10,
                         },
        className="ag-theme-custom-theme",
    )()

    return f"selected File: {value}", table

vm.Page.add_type("components", MyCard)

page = vm.Page(
    title="Default Dash AG Grid",
    components=[
        MyCard(),
        vm.AgGrid(id="grid-table", figure=dash_ag_grid(data_frame=pd.DataFrame())),
        vm.Card(id="card", text="Filename"),
    ]
)
dashboard = vm.Dashboard(pages=[page])

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

Also, you can change the dash_ag_grid className to "ag-theme-custom-theme ag-theme-quartz-dark ag-theme-vizro" to make it work with the theme changes.


However, there is a more native way to achieve this use-case in Vizro. Here's an example:

import pandas as pd
import vizro.models as vm
from vizro import Vizro
from vizro.tables import dash_ag_grid
from dash import callback, Output, Input

from vizro.managers import data_manager
from vizro.models.types import capture

def load_data_from_file_system(file_name=None):
    if file_name is None:
        return pd.DataFrame()
    return pd.read_csv(file_name)

data_manager["ag_grid_data_data_manager_key"] = load_data_from_file_system

@callback(
    Output("card", "children"),
    Input("file-selector-id", "value")
)
def update_card_text(file_name):
    return f'Selected file: "{file_name}"'

@capture("ag_grid")
def my_custom_dash_ag_grid(data_frame, **kwargs):
    kwargs["columnDefs"] = [
        {
            "field": col,
            'minWidth': 120,
            'maxWidth': 200,
            "cellRenderer": "DynamicTooltip"
        } for col in data_frame.columns
    ]

    vizro_ag_grid_object = dash_ag_grid(data_frame=data_frame, **kwargs)

    return vizro_ag_grid_object()

page = vm.Page(
    title="Default Dash AG Grid",
    layout=vm.Layout(grid=[
        [0, -1, -1, -1],
        *[[1, 1, 1, 1]] * 11,
    ]),
    components=[
        vm.Card(id="card", text="Filename"),
        vm.AgGrid(
            id="grid-table",
            figure=my_custom_dash_ag_grid(
                data_frame="ag_grid_data_data_manager_key",
                id="data_table",
                defaultColDef={
                    "flex": 1,
                    "minWidth": 100,
                    "floatingFilter": True,
                    "initialWidth": 100,
                    "wrapHeaderText": True,
                    "autoHeaderHeight": True,
                    "checkboxSelection": {
                        "function": 'params.column == params.columnApi.getAllDisplayedColumns()[0]'
                    },
                    "headerCheckboxSelection": {
                        "function": 'params.column == params.columnApi.getAllDisplayedColumns()[0]'
                    },
                },
                enableEnterpriseModules=True,
                dashGridOptions={
                    "animateRows": False,
                    "pagination": True,
                    "paginationPageSize": 100,
                    "rowSelection": "multiple",
                    "suppressRowClickSelection": True,
                    "enableCellTextSelection": True,
                    "rowHeight": 25,
                    "textAlignment": "center",
                    "tooltipShowDelay": 10,
                },
                # TODO: Consider changing/concatenating the classname with this one -> "ag-theme-quartz-dark ag-theme-vizro",
                className="ag-theme-custom-theme",
            )
        ),
    ],
    controls=[
        vm.Parameter(
            targets=["grid-table.data_frame.file_name"],
            # TODO: Or use this selector instead of the Dropdown
            # selector=vm.RadioItems(
            #     id="file-selector-id",
            #     title="Select a file",
            #     options=[
            #         {'label': 'File1', 'value': 'File1.csv'},
            #         {'label': 'File2', 'value': 'File2.csv'},
            #         {'label': 'File3', 'value': 'file3.csv'},
            #         {'label': 'File4', 'value': 'file4.csv'}
            #     ]
            # ),
            selector=vm.Dropdown(
                id="file-selector-id",
                title="Select a file",
                multi=False,
                options=[
                    {'label': 'File1', 'value': 'File1.csv'},
                    {'label': 'File2', 'value': 'File2.csv'},
                    {'label': 'File3', 'value': 'file3.csv'},
                    {'label': 'File4', 'value': 'file4.csv'}
                ]
            ),
        )
    ]

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

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

You can find more about:

BalaNagendraReddy commented 2 months ago

Hi @petar-qb ,

It worked as expected.

Thank you for your help in resolving the issue. Your expertise and quick action made a big difference, and I truly appreciate the time and effort you put into solving it.

Thanks again for your support!

BalaNagendraReddy commented 2 months ago

Hi @petar-qb ,

Description:

When selecting the first option in the dropdown, the grid column widths are set correctly as expected. However, after selecting an additional file to concatenate the data, the width of the first three columns (which are set to 150px) resets to the default width.

Steps to Reproduce:

Select the first option from the dropdown. Verify that the first three columns have a width of 150px. Select an additional file to concatenate. Observe that the width of the first three columns reverts to the default width.

Expected Behavior:

The width of the first three columns should remain at 150px even after adding more files.

Actual Behavior:

The column widths change to the default width after selecting additional files.

import vizro.models as vm
import pandas as pd
import dash_bootstrap_components as dbc

from vizro import Vizro
from vizro.tables import dash_ag_grid
from dash import callback, Output, Input

from typing import Literal

dropdown_options = [{'label': 'File1', 'value': 'file1.csv'}, {'label': 'File2', 'value': 'file2.csv'},
                    {'label': 'File3', 'value': 'file3.csv'}, {'label': 'File4', 'value': 'file4.csv'}]

class Dropdown(vm.VizroBaseModel):
    """New custom component `CustomHTML`."""
    type: Literal["MyCard"] = "MyCard"

    def build(self):
        return dbc.Card(
            children=[
                dbc.DropdownMenu(
                    label="Select File",
                    children=dbc.Checklist(options=dropdown_options, value=[], id="select-file"),
                    direction="down",
                    align_end=False,
                    className="m-1",
                    size="sm",
                    toggle_style={"fontSize": "12px", "fontWeight": "bold", "borderRadius": "5px"},
                    toggleClassName="fst-italic border border-dark",
                ),
            ],
        )

@callback(Output("grid-table", "children"),
          Input("select-file", "value"))
def display_grid_table(selected_files):
    all_files_df = pd.DataFrame()

    if selected_files:    
        for file_name in selected_files:
            df = pd.read_csv(file_name)
            all_files_df=pd.concat([all_files_df, df], ignore_index=True)

        cols = all_files_df.columns.tolist()
        first_three_cols = ['col1', 'col2', 'col3'] 
        rem_cols = [col for col in cols if col not in first_three_cols]

        format_df = all_files_df[first_three_cols + rem_cols]

        defaultColDef = {
            "flex": 1,
            "floatingFilter": True,
            "wrapHeaderText": True,
            "autoHeaderHeight": True,
        }

        # Iam expecting the below table to be returned
        table = dash_ag_grid(
            id="data-table",
            data_frame = format_df,
            rowData= format_df.to_dict("records"),
            columnDefs=[{"headerName": x, "field": x} for x in format_df.columns],
            columnSize="sizeToFit",
            defaultColDef=defaultColDef,
            columnSizeOptions={
            'defaultMinWidth': 70,
            'columnLimits': [{'key': 'col1', 'minWidth': 150}, {'key': 'col2', 'minWidth': 150}, {'key': 'col3', 'minWidth': 150}],
            },
            enableEnterpriseModules=True,
            dashGridOptions = {"animateRows": False, "domLayout": "autoHeight", "textAlignment": "center"},
            style = {"height": None},
        )()
        return table

vm.Page.add_type("components", Dropdown)

page = vm.Page(
    title="Default Dash AG Grid",
    layout = vm.Layout(grid = [[0, 1, 1, 1, 1]], row_min_height="250px", row_gap = "20px"),
    components=[
        Dropdown(),
        vm.AgGrid(id="grid-table", figure=dash_ag_grid(data_frame=pd.DataFrame())),
    ]
)
dashboard = vm.Dashboard(pages=[page])

Vizro().build(dashboard).run()
petar-qb commented 2 months ago

Hi @BalaNagendraReddy

Does it work as expected if you apply the following change? columnSize="sizeToFit" to -> columnSize="responsiveSizeToFit",

(edited) Also, what Vizro version do you use?

BalaNagendraReddy commented 2 months ago

Hi @petar-qb ,

Tried that option still facing same issue.

Iam using the vizro version of 0.1.18

petar-qb commented 2 months ago

Hi @BalaNagendraReddy

And what if you remove the line "flex": 1 from defaultColDef? Does it work then?

BalaNagendraReddy commented 2 months ago

Hi @petar-qb ,

Thank you it's working as expected, after removing the "flex": 1.