diff --git a/vizro-core/changelog.d/20240821_165515_petar_pejovic_return_empty_data_frame_from_tables_build.md b/vizro-core/changelog.d/20240821_165515_petar_pejovic_return_empty_data_frame_from_tables_build.md new file mode 100644 index 000000000..e99e94ad9 --- /dev/null +++ b/vizro-core/changelog.d/20240821_165515_petar_pejovic_return_empty_data_frame_from_tables_build.md @@ -0,0 +1,46 @@ + + + + + + +### Changed + +- Improve page loading time for `AgGrid`, `Table` and `Figure`. ([#644](https://github.com/mckinsey/vizro/pull/644)) + + + +### Fixed + +- Fix persistence of `columnSize` and `selectedRows` for `AgGrid`. ([#644](https://github.com/mckinsey/vizro/pull/644)) + + diff --git a/vizro-core/examples/scratch_dev/app.py b/vizro-core/examples/scratch_dev/app.py index a024b808a..69a6ed34a 100644 --- a/vizro-core/examples/scratch_dev/app.py +++ b/vizro-core/examples/scratch_dev/app.py @@ -1,67 +1,139 @@ """Dev app to try things out.""" +import pandas as pd import vizro.models as vm import vizro.plotly.express as px -from charts.charts import page2 +from vizro import Vizro +from vizro.actions import filter_interaction +from vizro.figures import kpi_card from vizro.managers import data_manager +from vizro.models.types import capture +from vizro.tables import dash_ag_grid, dash_data_table -df = px.data.iris() +data = pd.DataFrame( + { + "Column 1": [1, 2, 3, 4, 5, 6], + "Column 2": ["A", "B", "C", "VeryLongStringInputCell_VeryLongStringInputCell", "D", "E"], + "Column 3": [10.5, 20.1, 30.2, 40.3, 50.4, 60.5], + } +) + +iris_dataset = px.data.iris() +giant_dataset = pd.concat([iris_dataset] * 10000, ignore_index=True) + + +def load_data(): + """Load data.""" + return data + + +data_manager["my_data"] = load_data + + +@capture("ag_grid") +def custom_dash_ag_grid(data_frame, **kwargs): + """Custom AgGrid.""" + # print(f"dash_ag_grid -> len: {len(data_frame)}") + return dash_ag_grid(data_frame, **kwargs)() + + +@capture("table") +def custom_dash_data_table(data_frame, **kwargs): + """Custom DataTable.""" + # print(f"dash_data_table -> len: {len(data_frame)}") + return dash_data_table(data_frame, **kwargs)() + + +@capture("graph") +def custom_px_scatter(data_frame, **kwargs): + """Custom Scatter plot.""" + # print(f"graph -> len: {len(data_frame)}") + return px.scatter(data_frame, **kwargs) -data_manager["iris"] = px.data.iris() -page = vm.Page( - title="Test", +@capture("figure") +def custom_kpi_card(data_frame, **kwargs): + """Custom KPI card.""" + # print(f"kpi_card -> len: {len(data_frame)}") + return kpi_card(data_frame, **kwargs)() + + +page_grid = vm.Page( + title="Example Page", layout=vm.Layout( - grid=[[0, 1], [2, 3], [4, 5]], + grid=[ + [0, 1], + [2, 3], + ] ), components=[ - vm.Card( - text=""" - ### What is Vizro? - - Vizro is a toolkit for creating modular data visualization applications. - """ + vm.AgGrid( + id="outer_ag_grid_id", + figure=custom_dash_ag_grid( + id="inner_ag_grid_id", + data_frame="my_data", + columnDefs=[{"field": col, "checkboxSelection": True, "filter": True} for col in data.columns], + columnSize="autoSize", + defaultColDef={"resizable": True}, + persistence=True, + persistence_type="local", + persisted_props=["selectedRows", "filterModel"], + dashGridOptions={ + "rowSelection": "multiple", + "suppressRowClickSelection": True, + # Turn pagination on to see results: + # "pagination": True, + # "paginationPageSize": 3, + }, + ), + actions=[ + vm.Action( + function=filter_interaction(targets=["graph_id"]), + ) + ], ), - vm.Card( - text=""" - ### Github - - Checkout Vizro's github page. - """, - href="https://github.com/mckinsey/vizro", + vm.Table( + id="outer_table_id", + figure=custom_dash_data_table( + id="inner_table_id", + data_frame="my_data", + row_selectable="multi", + filter_action="native", + persistence=True, + persistence_type="local", + # Turn pagination on to see results: + # page_action="native", + # page_size=3, + ), ), - vm.Card( - text=""" - ### Docs - - Visit the documentation for codes examples, tutorials and API reference. - """, - href="https://vizro.readthedocs.io/", + vm.Graph( + id="graph_id", + figure=custom_px_scatter(data_frame="my_data", x="Column 1", y="Column 3"), ), - vm.Card( - text=""" - ### Nav Link - - Click this for page 2. - """, - href="/page2", + vm.Figure( + id="figure_id", + figure=custom_kpi_card(data_frame="my_data", value_column="Column 1"), ), - vm.Graph(id="scatter_chart", figure=px.scatter("iris", x="sepal_length", y="petal_width", color="species")), - vm.Graph(id="hist_chart", figure=px.histogram("iris", x="sepal_width", color="species")), - ], - controls=[ - vm.Filter(column="species", selector=vm.Dropdown(value=["ALL"])), - vm.Filter(column="petal_length"), - vm.Filter(column="sepal_width"), ], + controls=[vm.Filter(column="Column 1", selector=vm.RangeSlider(step=1))], ) -dashboard = vm.Dashboard(pages=[page, page2]) +columnDefs = [{"field": "petal_length"}] -if __name__ == "__main__": - from vizro import Vizro +dashboard = vm.Dashboard( + pages=[ + vm.Page( + title="Page_1", + components=[vm.Card(text="Dummy page just for testing")], + ), + page_grid, + vm.Page( + title="Page_3", + components=[vm.AgGrid(figure=dash_ag_grid(data_frame=giant_dataset, columnDefs=columnDefs))], + ), + ] +) - string = dashboard._to_python(extra_imports={"from dash_ag_grid import AgGrid"}) - print(string) # noqa - Vizro().build(dashboard).run() +if __name__ == "__main__": + Vizro().build(dashboard).run(debug=True) diff --git a/vizro-core/src/vizro/_vizro.py b/vizro-core/src/vizro/_vizro.py index 8d92ab3bd..fd6c9f285 100644 --- a/vizro-core/src/vizro/_vizro.py +++ b/vizro-core/src/vizro/_vizro.py @@ -32,7 +32,19 @@ def __init__(self, **kwargs): [Dash documentation](https://dash.plotly.com/reference#dash.dash) for possible arguments. """ - self.dash = dash.Dash(**kwargs, use_pages=True, pages_folder="", title="Vizro") + # Setting suppress_callback_exceptions=True for the following reasons: + # 1. Prevents the following Dash exception when using html.Div as placeholders in build methods: + # "Property 'cellClicked' was used with component ID '__input_ag_grid_id' in one of the Input + # items of a callback. This ID is assigned to a dash_html_components.Div component in the layout, + # which does not support this property." + # 2. Improves performance by bypassing layout validation. + self.dash = dash.Dash( + **kwargs, + pages_folder="", + suppress_callback_exceptions=True, + title="Vizro", + use_pages=True, + ) # Include Vizro assets (in the static folder) as external scripts and stylesheets. We extend self.dash.config # objects so the user can specify additional external_scripts and external_stylesheets via kwargs. diff --git a/vizro-core/src/vizro/models/_components/ag_grid.py b/vizro-core/src/vizro/models/_components/ag_grid.py index 80cbc02a4..5b9ea0961 100644 --- a/vizro-core/src/vizro/models/_components/ag_grid.py +++ b/vizro-core/src/vizro/models/_components/ag_grid.py @@ -106,13 +106,18 @@ def build(self): return dcc.Loading( children=[ html.H3(self.title) if self.title else None, - # The pagination setting (and potentially others) of the initially built AgGrid (in the build method - # here) must have the same setting as the object that is built by the on-page-load mechanism using - # with the user settings and rendered finally. Otherwise the grid is not rendered correctly. - # Additionally, we cannot remove the DF from the ag grid object before returning it (to save sending - # data over the network), because it breaks filter persistence settings on page change. - # Hence be careful when editing the line below. - html.Div(self.__call__(), id=self.id, className="table-container"), + # The Div component with `id=self._input_component_id` is rendered during the build phase. + # This placeholder component is quickly replaced by the actual AgGrid object, which is generated using + # a filtered data_frame and parameterized arguments as part of the on_page_load mechanism. + # To prevent pagination and persistence issues while maintaining a lightweight component initial load, + # this method now returns a html.Div object instead of the previous dag.AgGrid. The actual AgGrid is + # then rendered by the on_page_load mechanism. + # The `id=self._input_component_id` is set to avoid the "Non-existing object" Dash exception. + html.Div( + id=self.id, + children=[html.Div(id=self._input_component_id)], + className="table-container", + ), ], color="grey", parent_className="loading-container", diff --git a/vizro-core/src/vizro/models/_components/figure.py b/vizro-core/src/vizro/models/_components/figure.py index f4cf0c52e..042ffceb9 100644 --- a/vizro-core/src/vizro/models/_components/figure.py +++ b/vizro-core/src/vizro/models/_components/figure.py @@ -52,11 +52,14 @@ def __getitem__(self, arg_name: str): @_log_call def build(self): return dcc.Loading( + # Refer to the vm.AgGrid build method for details on why we return the + # html.Div(id=self.id) instead of actual figure object with the original data_frame. # Optimally, we would like to provide id=self.id directly here such that we can target the CSS - # of the children via ID as well, but the `id` doesn't seem to be passed on to the loading component. - # I've raised an issue on dash here: https://github.com/plotly/dash/issues/2878 + # of the children via ID as well, but the `id` doesn't seem to be passed on to the loading component. + # This limitation is handled with this PR -> https://github.com/plotly/dash/pull/2888. + # The PR is merged but is not released yet. Once it is released, we can try to refactor the following code. # In the meantime, we are adding an extra html.div here. - html.Div(self.__call__(), id=self.id, className="figure-container"), + html.Div(id=self.id, className="figure-container"), color="grey", parent_className="loading-container", overlay_style={"visibility": "visible", "opacity": 0.3}, diff --git a/vizro-core/src/vizro/models/_components/table.py b/vizro-core/src/vizro/models/_components/table.py index 2018aedb8..1d0b643f1 100644 --- a/vizro-core/src/vizro/models/_components/table.py +++ b/vizro-core/src/vizro/models/_components/table.py @@ -105,10 +105,13 @@ def build(self): return dcc.Loading( children=[ html.H3(self.title) if self.title else None, - # Please see vm.AgGrid build method as to why we are returning the call with the full data here - # Most of the comments may not apply to the data table, but in order to be consistent, we are - # handling the build method in the exact same way here - html.Div(self.__call__(), id=self.id, className="table-container"), + # Refer to the vm.AgGrid build method for details on why we return the + # html.Div(id=self._input_component_id) instead of actual figure object with the original data_frame. + html.Div( + id=self.id, + children=[html.Div(id=self._input_component_id)], + className="table-container", + ), ], color="grey", parent_className="loading-container", diff --git a/vizro-core/tests/unit/vizro/models/_components/test_ag_grid.py b/vizro-core/tests/unit/vizro/models/_components/test_ag_grid.py index dd2c1116f..be5174295 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_ag_grid.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_ag_grid.py @@ -18,6 +18,11 @@ from vizro.tables import dash_ag_grid +@pytest.fixture +def dash_ag_grid_with_id(): + return dash_ag_grid(id="underlying_table_id", data_frame=px.data.gapminder()) + + @pytest.fixture def dash_ag_grid_with_arguments(): return dash_ag_grid(data_frame=px.data.gapminder(), defaultColDef={"resizable": False, "sortable": False}) @@ -97,6 +102,18 @@ def test_getitem_unknown_args(self, standard_ag_grid): with pytest.raises(KeyError): ag_grid["unknown_args"] + def test_underlying_id_is_auto_generated(self, standard_ag_grid): + ag_grid = vm.AgGrid(id="text_ag_grid", figure=standard_ag_grid) + ag_grid.pre_build() + # ag_grid() is the same as ag_grid.__call__() + assert ag_grid().id == "__input_text_ag_grid" + + def test_underlying_id_is_provided(self, dash_ag_grid_with_id): + ag_grid = vm.AgGrid(figure=dash_ag_grid_with_id) + ag_grid.pre_build() + # ag_grid() is the same as ag_grid.__call__() + assert ag_grid().id == "underlying_table_id" + class TestAttributesAgGrid: # Testing at this low implementation level as mocking callback contexts skips checking for creation of these objects @@ -141,7 +158,7 @@ def test_ag_grid_build_mandatory_only(self, standard_ag_grid, gapminder): None, html.Div( id="text_ag_grid", - children=dash_ag_grid(data_frame=gapminder, id="__input_text_ag_grid")(), + children=[html.Div(id="__input_text_ag_grid")], className="table-container", ), ], @@ -162,7 +179,7 @@ def test_ag_grid_build_with_underlying_id(self, ag_grid_with_id_and_conf, filter None, html.Div( id="text_ag_grid", - children=dash_ag_grid(data_frame=gapminder, id="underlying_ag_grid_id")(), + children=[html.Div(id="underlying_ag_grid_id")], className="table-container", ), ], diff --git a/vizro-core/tests/unit/vizro/models/_components/test_figure.py b/vizro-core/tests/unit/vizro/models/_components/test_figure.py index 20bbc3b87..08adc456e 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_figure.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_figure.py @@ -96,15 +96,8 @@ def test_figure_build(self, standard_kpi_card, gapminder): expected_figure = dcc.Loading( html.Div( - kpi_card( - data_frame=gapminder, - value_column="lifeExp", - agg_func="mean", - title="Mean Lifeexp", - value_format="{value:.3f}", - )(), - className="figure-container", id="figure-id", + className="figure-container", ), color="grey", parent_className="loading-container", diff --git a/vizro-core/tests/unit/vizro/models/_components/test_table.py b/vizro-core/tests/unit/vizro/models/_components/test_table.py index 82e716fab..d613f7b2d 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_table.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_table.py @@ -97,6 +97,18 @@ def test_getitem_unknown_args(self, standard_dash_table): with pytest.raises(KeyError): table["unknown_args"] + def test_underlying_id_is_auto_generated(self, standard_dash_table): + table = vm.Table(id="table", figure=standard_dash_table) + table.pre_build() + # table() is the same as table.__call__() + assert table().id == "__input_table" + + def test_underlying_id_is_provided(self, dash_data_table_with_id): + table = vm.Table(figure=dash_data_table_with_id) + table.pre_build() + # table() is the same as table.__call__() + assert table().id == "underlying_table_id" + class TestAttributesTable: def test_table_filter_interaction_attributes(self, dash_data_table_with_id): @@ -141,8 +153,8 @@ def test_table_build_mandatory_only(self, standard_dash_table, gapminder): children=[ None, html.Div( - dash_data_table(id="__input_text_table", data_frame=gapminder)(), id="text_table", + children=[html.Div(id="__input_text_table")], className="table-container", ), ], @@ -162,8 +174,8 @@ def test_table_build_with_underlying_id(self, dash_data_table_with_id, filter_in children=[ None, html.Div( - dash_data_table(id="underlying_table_id", data_frame=gapminder)(), id="text_table", + children=[html.Div(id="underlying_table_id")], className="table-container", ), ], @@ -182,8 +194,8 @@ def test_table_build_with_title(self, standard_dash_table, gapminder): children=[ html.H3("Table Title"), html.Div( - dash_data_table(id="__input_text_table", data_frame=gapminder)(), id="text_table", + children=[html.Div(id="__input_text_table")], className="table-container", ), ], diff --git a/vizro-core/tests/unit/vizro/tables/test_dash_ag_grid.py b/vizro-core/tests/unit/vizro/tables/test_dash_ag_grid.py index 7dce3eb2f..3fb7be123 100644 --- a/vizro-core/tests/unit/vizro/tables/test_dash_ag_grid.py +++ b/vizro-core/tests/unit/vizro/tables/test_dash_ag_grid.py @@ -1,8 +1,6 @@ import dash_ag_grid as dag import pandas as pd -import vizro.models as vm from asserts import assert_component_equal -from dash import dcc, html from pandas import Timestamp from vizro.models.types import capture from vizro.tables import dash_ag_grid @@ -50,76 +48,20 @@ def test_dash_ag_grid(self): ) # skipping only dashGridOptions as this is mostly our defaults for data formats, and would crowd the tests - -class TestCustomDashAgGrid: def test_custom_dash_ag_grid(self): - """Tests whether a custom created grid callable can be correctly be built in vm.AgGrid.""" - id = "custom_ag_grid" - @capture("ag_grid") - def custom_ag_grid(data_frame): + def custom_dash_ag_grid(data_frame): return dag.AgGrid( columnDefs=[{"field": col} for col in data_frame.columns], rowData=data_frame.to_dict("records"), ) - grid_model = vm.AgGrid( - id=id, - figure=custom_ag_grid(data_frame=data), - ) - grid_model.pre_build() - custom_grid = grid_model.build() - - expected_grid = dcc.Loading( - [ - None, - html.Div( - dag.AgGrid(id="__input_custom_ag_grid", columnDefs=column_defs, rowData=row_data_date_raw), - id=id, - className="table-container", - ), - ], - color="grey", - parent_className="loading-container", - overlay_style={"visibility": "visible", "opacity": 0.3}, - ) - - assert_component_equal(custom_grid, expected_grid) + ag_grid = custom_dash_ag_grid(data_frame=data)() - def test_custom_dash_ag_grid_column_referral(self): - """Tests whether a custom created grid can be correctly built in vm.AgGrid. - - This test focuses on the case that the custom grid includes column referrals on presumed data knowledge. - """ - id = "custom_ag_grid" - - @capture("ag_grid") - def custom_ag_grid(data_frame): - data_frame["cat"] # access "existing" column - return dag.AgGrid( - columnDefs=[{"field": col} for col in data_frame.columns], - rowData=data_frame.to_dict("records"), - ) - - grid_model = vm.AgGrid( - id=id, - figure=custom_ag_grid(data_frame=data), - ) - grid_model.pre_build() - custom_grid = grid_model.build() - - expected_grid = dcc.Loading( - [ - None, - html.Div( - dag.AgGrid(id="__input_custom_ag_grid", columnDefs=column_defs, rowData=row_data_date_raw), - id=id, - className="table-container", - ), - ], - color="grey", - parent_className="loading-container", - overlay_style={"visibility": "visible", "opacity": 0.3}, + assert_component_equal( + ag_grid, + dag.AgGrid( + rowData=row_data_date_raw, + columnDefs=column_defs, + ), ) - - assert_component_equal(custom_grid, expected_grid) diff --git a/vizro-core/tests/unit/vizro/tables/test_dash_table.py b/vizro-core/tests/unit/vizro/tables/test_dash_table.py index 0301adfde..b5cca6969 100644 --- a/vizro-core/tests/unit/vizro/tables/test_dash_table.py +++ b/vizro-core/tests/unit/vizro/tables/test_dash_table.py @@ -1,7 +1,6 @@ import pandas as pd -import vizro.models as vm from asserts import assert_component_equal -from dash import dash_table, dcc, html +from dash import dash_table from vizro.models.types import capture from vizro.tables import dash_data_table @@ -52,86 +51,20 @@ def test_dash_data_table(self): ), ) - -class TestCustomDashDataTable: def test_custom_dash_data_table(self): - """Tests whether a custom created table callable can be correctly be built in vm.Table.""" - id = "custom_dash_data_table" - - @capture("table") - def custom_dash_data_table(data_frame): - return dash_table.DataTable( - columns=[{"name": col, "id": col} for col in data_frame.columns], - data=data_frame.to_dict("records"), - ) - - table = vm.Table( - id=id, - figure=custom_dash_data_table(data_frame=data), - ) - table.pre_build() - - custom_table = table.build() - - expected_table_object = dash_table.DataTable( - columns=columns, - data=data_in_table, - ) - expected_table_object.id = "__input_" + id - - expected_table = dcc.Loading( - children=[ - None, - html.Div( - expected_table_object, - id=id, - className="table-container", - ), - ], - color="grey", - parent_className="loading-container", - overlay_style={"visibility": "visible", "opacity": 0.3}, - ) - - assert_component_equal(custom_table, expected_table) - - def test_custom_dash_data_table_column_referral(self): - """Tests whether a custom created table callable can be correctly built in vm.Table. - - This test focuses on the case that the custom grid include column referrals on presumed data knowledge. - """ - id = "custom_dash_data_table" - @capture("table") def custom_dash_data_table(data_frame): - data_frame["cat"] # access "existing" column return dash_table.DataTable( columns=[{"name": col, "id": col} for col in data_frame.columns], data=data_frame.to_dict("records"), ) - table = vm.Table( - id=id, - figure=custom_dash_data_table(data_frame=data), - ) - table.pre_build() - - custom_table = table.build() - - expected_table_object = dash_table.DataTable( - columns=columns, - data=data_in_table, - ) - expected_table_object.id = "__input_" + id + table = custom_dash_data_table(data_frame=data)() - expected_table = dcc.Loading( - children=[ - None, - html.Div(expected_table_object, id=id, className="table-container"), - ], - color="grey", - parent_className="loading-container", - overlay_style={"visibility": "visible", "opacity": 0.3}, + assert_component_equal( + table, + dash_table.DataTable( + data=data_in_table, + columns=columns, + ), ) - - assert_component_equal(custom_table, expected_table)