Skip to content

Commit

Permalink
[Tidy] Return empty data frame from AgGrid, Table and Figure build me…
Browse files Browse the repository at this point in the history
…thods (#644)
  • Loading branch information
petar-qb authored Aug 27, 2024
1 parent 1893168 commit 84c0ffc
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 215 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
-->

<!--
### Highlights ✨
- A bullet item for the Highlights ✨ category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Removed
- A bullet item for the Removed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Added
- A bullet item for the Added category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->

### Changed

- Improve page loading time for `AgGrid`, `Table` and `Figure`. ([#644](https://github.com/mckinsey/vizro/pull/644))

<!--
### Deprecated
- A bullet item for the Deprecated category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->

### Fixed

- Fix persistence of `columnSize` and `selectedRows` for `AgGrid`. ([#644](https://github.com/mckinsey/vizro/pull/644))

<!--
### Security
- A bullet item for the Security category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
164 changes: 118 additions & 46 deletions vizro-core/examples/scratch_dev/app.py
Original file line number Diff line number Diff line change
@@ -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)
14 changes: 13 additions & 1 deletion vizro-core/src/vizro/_vizro.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 12 additions & 7 deletions vizro-core/src/vizro/models/_components/ag_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 6 additions & 3 deletions vizro-core/src/vizro/models/_components/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
11 changes: 7 additions & 4 deletions vizro-core/src/vizro/models/_components/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 19 additions & 2 deletions vizro-core/tests/unit/vizro/models/_components/test_ag_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
),
],
Expand All @@ -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",
),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 84c0ffc

Please sign in to comment.