From 64a4eb9cafe32966333b2c1369fa91c132c605d2 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Wed, 24 Jan 2024 13:15:28 +0100 Subject: [PATCH 01/34] Initial commit with rendering Grid --- vizro-core/examples/_dev/app.py | 190 ++++++++++++++---- .../src/vizro/models/_components/grid.py | 0 vizro-core/src/vizro/tables/__init__.py | 3 +- vizro-core/src/vizro/tables/dash_aggrid.py | 20 ++ 4 files changed, 176 insertions(+), 37 deletions(-) create mode 100644 vizro-core/src/vizro/models/_components/grid.py create mode 100644 vizro-core/src/vizro/tables/dash_aggrid.py diff --git a/vizro-core/examples/_dev/app.py b/vizro-core/examples/_dev/app.py index 1ceb7de01..ae6fca88f 100644 --- a/vizro-core/examples/_dev/app.py +++ b/vizro-core/examples/_dev/app.py @@ -1,54 +1,172 @@ -"""Rough example used by developers.""" +"""Example to show dashboard configuration.""" +from typing import List + +import pandas as pd +from dash import State, dash_table import vizro.models as vm import vizro.plotly.express as px from vizro import Vizro -from vizro.tables import dash_data_table +from vizro.actions import export_data, filter_interaction +from vizro.models.types import capture +from vizro.tables import dash_ag_grid, dash_data_table df = px.data.gapminder() +df_mean = ( + df.groupby(by=["continent", "year"]).agg({"lifeExp": "mean", "pop": "mean", "gdpPercap": "mean"}).reset_index() +) -table_and_container = vm.Page( - title="Table and Container", - components=[ - vm.Container( - title="Container w/ Table", - components=[ - vm.Table( - title="Table Title", - figure=dash_data_table( - id="dash_data_table_country", - data_frame=df, - page_size=30, - ), - ) - ], +df_transformed = df.copy() +df_transformed["lifeExp"] = df.groupby(by=["continent", "year"])["lifeExp"].transform("mean") +df_transformed["gdpPercap"] = df.groupby(by=["continent", "year"])["gdpPercap"].transform("mean") +df_transformed["pop"] = df.groupby(by=["continent", "year"])["pop"].transform("sum") +df_concat = pd.concat([df_transformed.assign(color="Continent Avg."), df.assign(color="Country")], ignore_index=True) + + +def my_custom_table(data_frame=None, id: str = None, chosen_columns: List[str] = None): + """Custom table.""" + columns = [{"name": i, "id": i} for i in chosen_columns] + defaults = { + "style_as_list_view": True, + "style_data": {"border_bottom": "1px solid var(--border-subtle-alpha-01)", "height": "40px"}, + "style_header": { + "border_bottom": "1px solid var(--state-overlays-selected-hover)", + "border_top": "1px solid var(--main-container-bg-color)", + "height": "32px", + }, + } + return dash_table.DataTable(data=data_frame.to_dict("records"), columns=columns, id=id, **defaults) + + +my_custom_table.action_info = { + "filter_interaction_input": lambda x: { + "active_cell": State(component_id=x._callable_object_id, component_property="active_cell"), + "derived_viewport_data": State( + component_id=x._callable_object_id, + component_property="derived_viewport_data", ), - vm.Container( - title="Another Container", - components=[ - vm.Graph( - figure=px.scatter( - df, - title="Graph_2", - x="gdpPercap", - y="lifeExp", - size="pop", - color="continent", + } +} + +my_custom_table = capture("table")(my_custom_table) + + +def create_benchmark_analysis(): + """Function returns a page to perform analysis on country level.""" + # Apply formatting to table columns + columns = [ + {"id": "country", "name": "country"}, + {"id": "continent", "name": "continent"}, + {"id": "year", "name": "year"}, + {"id": "lifeExp", "name": "lifeExp", "type": "numeric", "format": {"specifier": ",.1f"}}, + {"id": "gdpPercap", "name": "gdpPercap", "type": "numeric", "format": {"specifier": "$,.2f"}}, + {"id": "pop", "name": "pop", "type": "numeric", "format": {"specifier": ",d"}}, + ] + + page_country = vm.Page( + title="Benchmark Analysis", + # description="Discovering how the metrics differ for each country and export data for further investigation", + # layout=vm.Layout(grid=[[0, 1]] * 5 + [[2, -1]], col_gap="32px", row_gap="60px"), + components=[ + vm.Table( + id="table_country_new", + title="Click on a cell in country column:", + figure=dash_ag_grid( + id="dash_ag_grid_country", + data_frame=df, + ), + actions=[vm.Action(function=filter_interaction(targets=["line_country"]))], + ), + vm.Table( + id="table_country", + title="Click on a cell in country column:", + figure=dash_data_table( + id="dash_data_table_country", + data_frame=df, + columns=columns, + style_data_conditional=[ + { + "if": {"filter_query": "{gdpPercap} < 1045", "column_id": "gdpPercap"}, + "backgroundColor": "#ff9222", + }, + { + "if": { + "filter_query": "{gdpPercap} >= 1045 && {gdpPercap} <= 4095", + "column_id": "gdpPercap", + }, + "backgroundColor": "#de9e75", + }, + { + "if": { + "filter_query": "{gdpPercap} > 4095 && {gdpPercap} <= 12695", + "column_id": "gdpPercap", + }, + "backgroundColor": "#aaa9ba", + }, + { + "if": {"filter_query": "{gdpPercap} > 12695", "column_id": "gdpPercap"}, + "backgroundColor": "#00b4ff", + }, + ], + sort_action="native", + style_cell={"textAlign": "left"}, + ), + actions=[vm.Action(function=filter_interaction(targets=["line_country"]))], + ), + vm.Graph( + id="line_country", + figure=px.line( + df_concat, + title="Country vs. Continent", + x="year", + y="gdpPercap", + color="color", + labels={"year": "Year", "data": "Data", "gdpPercap": "GDP per capita"}, + color_discrete_map={"Country": "#afe7f9", "Continent": "#003875"}, + markers=True, + hover_name="country", + ), + ), + vm.Button( + text="Export data", + actions=[ + vm.Action( + function=export_data( + targets=["line_country"], + ) ), + ], + ), + vm.Table( # the custom table works with its own set of states defined above + id="custom_table", + title="Custom Dash DataTable", + figure=my_custom_table( + id="custom_dash_table_callable_id", + data_frame=df, + chosen_columns=["country", "continent", "lifeExp", "pop", "gdpPercap"], ), - ], - ), - ], - controls=[vm.Filter(column="continent")], -) + actions=[vm.Action(function=filter_interaction(targets=["line_country"]))], + ), + ], + controls=[ + vm.Filter(column="continent", selector=vm.Dropdown(value="Europe", multi=False, title="Select continent")), + vm.Filter(column="year", selector=vm.RangeSlider(title="Select timeframe", step=1, marks=None)), + vm.Parameter( + targets=["line_country.y"], + selector=vm.Dropdown( + options=["lifeExp", "gdpPercap", "pop"], multi=False, value="gdpPercap", title="Choose y-axis" + ), + ), + ], + ) + return page_country dashboard = vm.Dashboard( - title="Dashboard Title", pages=[ - table_and_container, + create_benchmark_analysis(), ], ) if __name__ == "__main__": - Vizro().build(dashboard).run() + Vizro(assets_folder="../assets").build(dashboard).run() \ No newline at end of file diff --git a/vizro-core/src/vizro/models/_components/grid.py b/vizro-core/src/vizro/models/_components/grid.py new file mode 100644 index 000000000..e69de29bb diff --git a/vizro-core/src/vizro/tables/__init__.py b/vizro-core/src/vizro/tables/__init__.py index 5a813ad8c..0c91e3346 100644 --- a/vizro-core/src/vizro/tables/__init__.py +++ b/vizro-core/src/vizro/tables/__init__.py @@ -1,4 +1,5 @@ +from vizro.tables.dash_aggrid import dash_ag_grid from vizro.tables.dash_table import dash_data_table # Please keep alphabetically ordered -__all__ = ["dash_data_table"] +__all__ = ["dash_ag_grid", "dash_data_table"] diff --git a/vizro-core/src/vizro/tables/dash_aggrid.py b/vizro-core/src/vizro/tables/dash_aggrid.py new file mode 100644 index 000000000..da54ea069 --- /dev/null +++ b/vizro-core/src/vizro/tables/dash_aggrid.py @@ -0,0 +1,20 @@ +import dash_ag_grid as dag +from dash import State + +from vizro.models.types import capture + + +def dash_ag_grid(data_frame=None, **kwargs): + """Custom AgGrid.""" + return dag.AgGrid( + rowData=data_frame.to_dict("records"), columnDefs=[{"field": col} for col in data_frame.columns], **kwargs + ) + + +dash_ag_grid.action_info = { + "filter_interaction_input": lambda x: { + "cellClicked": State(component_id=x._callable_object_id, component_property="cellClicked"), + } +} + +dash_ag_grid = capture("table")(dash_ag_grid) \ No newline at end of file From 67b12e67e90bec47c72255d053c781f2d80ae5b6 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Thu, 25 Jan 2024 13:31:01 +0100 Subject: [PATCH 02/34] First implementation of separate model approach --- vizro-core/examples/_dev/app.py | 39 +++++- .../src/vizro/actions/_actions_utils.py | 20 +-- .../_callback_mapping_utils.py | 23 +--- vizro-core/src/vizro/models/__init__.py | 6 +- .../src/vizro/models/_components/__init__.py | 3 +- .../src/vizro/models/_components/graph.py | 43 +++++- .../src/vizro/models/_components/grid.py | 122 ++++++++++++++++++ .../src/vizro/models/_components/table.py | 43 +++++- vizro-core/src/vizro/models/types.py | 7 +- vizro-core/src/vizro/tables/dash_aggrid.py | 11 +- 10 files changed, 255 insertions(+), 62 deletions(-) diff --git a/vizro-core/examples/_dev/app.py b/vizro-core/examples/_dev/app.py index ae6fca88f..fc7e503a7 100644 --- a/vizro-core/examples/_dev/app.py +++ b/vizro-core/examples/_dev/app.py @@ -22,6 +22,8 @@ df_transformed["pop"] = df.groupby(by=["continent", "year"])["pop"].transform("sum") df_concat = pd.concat([df_transformed.assign(color="Continent Avg."), df.assign(color="Country")], ignore_index=True) +gapminder_2007 = df.query("year == 2007") + def my_custom_table(data_frame=None, id: str = None, chosen_columns: List[str] = None): """Custom table.""" @@ -64,11 +66,11 @@ def create_benchmark_analysis(): ] page_country = vm.Page( - title="Benchmark Analysis", + title="Table Test", # description="Discovering how the metrics differ for each country and export data for further investigation", # layout=vm.Layout(grid=[[0, 1]] * 5 + [[2, -1]], col_gap="32px", row_gap="60px"), components=[ - vm.Table( + vm.Grid( id="table_country_new", title="Click on a cell in country column:", figure=dash_ag_grid( @@ -162,11 +164,36 @@ def create_benchmark_analysis(): return page_country -dashboard = vm.Dashboard( - pages=[ - create_benchmark_analysis(), +chart_interaction = vm.Page( + title="Chart interaction", + components=[ + vm.Graph( + figure=px.box( + gapminder_2007, + x="continent", + y="lifeExp", + color="continent", + custom_data=["continent"], + ), + actions=[vm.Action(function=filter_interaction(targets=["scatter_relation_2007"]))], + ), + vm.Graph( + id="scatter_relation_2007", + figure=px.scatter( + gapminder_2007, + x="gdpPercap", + y="lifeExp", + size="pop", + color="continent", + ), + ), ], ) + +dashboard = vm.Dashboard( + pages=[create_benchmark_analysis(), chart_interaction], +) + if __name__ == "__main__": - Vizro(assets_folder="../assets").build(dashboard).run() \ No newline at end of file + Vizro(assets_folder="../assets").build(dashboard).run() diff --git a/vizro-core/src/vizro/actions/_actions_utils.py b/vizro-core/src/vizro/actions/_actions_utils.py index 7d4b3ba8d..7ca6c78c7 100644 --- a/vizro-core/src/vizro/actions/_actions_utils.py +++ b/vizro-core/src/vizro/actions/_actions_utils.py @@ -139,19 +139,13 @@ def _apply_filter_interaction( target: str, ) -> pd.DataFrame: for ctd_filter_interaction in ctds_filter_interaction: - if "clickData" in ctd_filter_interaction: - data_frame = _apply_graph_filter_interaction( - data_frame=data_frame, - target=target, - ctd_filter_interaction=ctd_filter_interaction, - ) - - if "active_cell" in ctd_filter_interaction and "derived_viewport_data" in ctd_filter_interaction: - data_frame = _apply_table_filter_interaction( - data_frame=data_frame, - target=target, - ctd_filter_interaction=ctd_filter_interaction, - ) + #TODO: make this more robust, such that it doesn't require the modelID + triggered_model = model_manager[ctd_filter_interaction["modelID"]["id"]] + data_frame = triggered_model._filter_interaction( + data_frame=data_frame, + target=target, + ctd_filter_interaction=ctd_filter_interaction, + ) return data_frame diff --git a/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py b/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py index 96b022108..b1255f448 100644 --- a/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py +++ b/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py @@ -7,7 +7,7 @@ from vizro.actions import _parameter, export_data, filter_interaction from vizro.managers import model_manager from vizro.managers._model_manager import ModelID -from vizro.models import Action, Page, Table +from vizro.models import Action, Page from vizro.models._controls import Filter, Parameter from vizro.models.types import ControlType @@ -51,25 +51,8 @@ def _get_inputs_of_figure_interactions( inputs = [] for action in figure_interactions_on_page: triggered_model = model_manager._get_action_trigger(action_id=ModelID(str(action.id))) - if isinstance(triggered_model, Table): - inputs.append( - { - "active_cell": State( - component_id=triggered_model._callable_object_id, component_property="active_cell" - ), - "derived_viewport_data": State( - component_id=triggered_model._callable_object_id, - component_property="derived_viewport_data", - ), - } - ) - else: - inputs.append( - { - "clickData": State(component_id=triggered_model.id, component_property="clickData"), - } - ) - + if hasattr(triggered_model, "_get_figure_interaction_input"): + inputs.append(triggered_model._get_figure_interaction_input()) return inputs diff --git a/vizro-core/src/vizro/models/__init__.py b/vizro-core/src/vizro/models/__init__.py index d25d374b2..212f7df02 100644 --- a/vizro-core/src/vizro/models/__init__.py +++ b/vizro-core/src/vizro/models/__init__.py @@ -1,7 +1,7 @@ # Keep this import at the top to avoid circular imports since it's used in every model. from ._base import VizroBaseModel # noqa: I001 from ._action import Action -from ._components import Card, Container, Graph, Table +from ._components import Card, Container, Graph, Grid, Table from ._components.form import Button, Checklist, Dropdown, RadioItems, RangeSlider, Slider from ._controls import Filter, Parameter from ._navigation.accordion import Accordion @@ -12,7 +12,7 @@ from ._layout import Layout from ._page import Page -Container.update_forward_refs(Button=Button, Card=Card, Graph=Graph, Table=Table, Layout=Layout) +Container.update_forward_refs(Button=Button, Card=Card, Graph=Graph, Grid=Grid, Table=Table, Layout=Layout) Page.update_forward_refs( Accordion=Accordion, Button=Button, @@ -20,6 +20,7 @@ Container=Container, Filter=Filter, Graph=Graph, + Grid=Grid, Parameter=Parameter, Table=Table, ) @@ -40,6 +41,7 @@ "Dropdown", "Filter", "Graph", + "Grid", "Layout", "NavBar", "NavLink", diff --git a/vizro-core/src/vizro/models/_components/__init__.py b/vizro-core/src/vizro/models/_components/__init__.py index 743ae87e0..6122367be 100644 --- a/vizro-core/src/vizro/models/_components/__init__.py +++ b/vizro-core/src/vizro/models/_components/__init__.py @@ -3,6 +3,7 @@ from vizro.models._components.card import Card from vizro.models._components.container import Container from vizro.models._components.graph import Graph +from vizro.models._components.grid import Grid from vizro.models._components.table import Table -__all__ = ["Button", "Card", "Container", "Graph", "Table"] +__all__ = ["Button", "Card", "Container", "Graph", "Grid", "Table"] diff --git a/vizro-core/src/vizro/models/_components/graph.py b/vizro-core/src/vizro/models/_components/graph.py index 03f8e8b58..881733090 100644 --- a/vizro-core/src/vizro/models/_components/graph.py +++ b/vizro-core/src/vizro/models/_components/graph.py @@ -1,7 +1,7 @@ import logging -from typing import List, Literal +from typing import Dict, List, Literal -from dash import ctx, dcc +from dash import State, ctx, dcc from dash.exceptions import MissingCallbackContextException from plotly import graph_objects as go @@ -10,9 +10,13 @@ except ImportError: # pragma: no cov from pydantic import Field, PrivateAttr, validator +import pandas as pd + import vizro.plotly.express as px from vizro import _themes as themes -from vizro.managers import data_manager +from vizro.actions._actions_utils import CallbackTriggerDict, _get_component_actions +from vizro.managers import data_manager, model_manager +from vizro.managers._model_manager import ModelID from vizro.models import Action, VizroBaseModel from vizro.models._action._actions_chain import _action_validator_factory from vizro.models._components._components_utils import _process_callable_data_frame @@ -70,6 +74,39 @@ def __getitem__(self, arg_name: str): return self.type return self.figure[arg_name] + # Interaction methods + def _get_figure_interaction_input(self): + """Requiried properties when using pre-defined `filter_interaction`""" + return { + "clickData": State(component_id=self.id, component_property="clickData"), + "modelID": State(component_id=self.id, component_property="id"), + } + + def _filter_interaction( + self, data_frame: pd.DataFrame, target: str, ctd_filter_interaction: Dict[str, CallbackTriggerDict] + ) -> pd.DataFrame: + """Function to be carried out for pre-defined `filter_interaction`""" + ctd_click_data = ctd_filter_interaction["clickData"] + if not ctd_click_data["value"]: + return data_frame + + source_graph_id: ModelID = ctd_click_data["id"] + source_graph_actions = _get_component_actions(model_manager[source_graph_id]) + try: + custom_data_columns = model_manager[source_graph_id]["custom_data"] + except KeyError as exc: + raise KeyError(f"No `custom_data` argument found for source graph with id {source_graph_id}.") from exc + + customdata = ctd_click_data["value"]["points"][0]["customdata"] + + for action in source_graph_actions: + if action.function._function.__name__ != "filter_interaction" or target not in action.function["targets"]: + continue + for custom_data_idx, column in enumerate(custom_data_columns): + data_frame = data_frame[data_frame[column].isin([customdata[custom_data_idx]])] + + return data_frame + @_log_call def build(self): # The empty figure here is just a placeholder designed to be replaced by the actual figure when the filters diff --git a/vizro-core/src/vizro/models/_components/grid.py b/vizro-core/src/vizro/models/_components/grid.py index e69de29bb..ba5d9a484 100644 --- a/vizro-core/src/vizro/models/_components/grid.py +++ b/vizro-core/src/vizro/models/_components/grid.py @@ -0,0 +1,122 @@ +import logging +from typing import Dict, List, Literal + +import pandas as pd +from dash import State, dash_table, dcc, html + +try: + from pydantic.v1 import Field, PrivateAttr, validator +except ImportError: # pragma: no cov + from pydantic import Field, PrivateAttr, validator + +import vizro.tables as vt +from vizro.actions._actions_utils import CallbackTriggerDict, _get_component_actions, _get_parent_vizro_model +from vizro.managers import data_manager +from vizro.models import Action, VizroBaseModel +from vizro.models._action._actions_chain import _action_validator_factory +from vizro.models._components._components_utils import _process_callable_data_frame +from vizro.models._models_utils import _log_call +from vizro.models.types import CapturedCallable + +logger = logging.getLogger(__name__) + + +class Grid(VizroBaseModel): + """Wrapper for table components to visualize in dashboard. + + Args: + type (Literal["grid"]): Defaults to `"grid"`. + figure (CapturedCallable): Grid like object to be displayed. Current choices include: + [`dash-ag-grid.AgGrid`](https://dash.plotly.com/dash-ag-grid). + title (str): Title of the table. Defaults to `""`. + actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`. + """ + + type: Literal["grid"] = "grid" + figure: CapturedCallable = Field(..., import_path=vt, description="Grid to be visualized on dashboard") + title: str = Field("", description="Title of the grid") + actions: List[Action] = [] + + _callable_object_id: str = PrivateAttr() + + # Component properties for actions and interactions + _output_property: str = PrivateAttr("children") + + # validator + set_actions = _action_validator_factory("cellClicked") + _validate_callable = validator("figure", allow_reuse=True, always=True)(_process_callable_data_frame) + + # Convenience wrapper/syntactic sugar. + def __call__(self, **kwargs): + kwargs.setdefault("data_frame", data_manager._get_component_data(self.id)) + return self.figure(**kwargs) + + # Convenience wrapper/syntactic sugar. + def __getitem__(self, arg_name: str): + # See table implementation for more details. + if arg_name == "type": + return self.type + return self.figure[arg_name] + + # Interaction methods + def _get_figure_interaction_input(self): + """Requiried properties when using pre-defined `filter_interaction`""" + return { + "cellClicked": State(component_id=self._callable_object_id, component_property="cellClicked"), + "modelID": State(component_id=self.id, component_property="id"), + } + + def _filter_interaction( + self, data_frame: pd.DataFrame, target: str, ctd_filter_interaction: Dict[str, CallbackTriggerDict] + ) -> pd.DataFrame: + """Function to be carried out for pre-defined `filter_interaction`""" + ctd_cellClicked = ctd_filter_interaction["cellClicked"] + if not ctd_cellClicked["value"]: + return data_frame + + # ctd_active_cell["id"] represents the underlying table id, so we need to fetch its parent Vizro Table actions. + source_table_actions = _get_component_actions(_get_parent_vizro_model(ctd_cellClicked["id"])) + + for action in source_table_actions: + if action.function._function.__name__ != "filter_interaction" or target not in action.function["targets"]: + continue + column = ctd_cellClicked["value"]["colId"] + clicked_data = ctd_cellClicked["value"]["value"] + data_frame = data_frame[data_frame[column].isin([clicked_data])] + + return data_frame + + @_log_call + def pre_build(self): + if self.actions: + kwargs = self.figure._arguments.copy() + + # This workaround is needed because the underlying table object requires a data_frame + kwargs["data_frame"] = pd.DataFrame() + + # The underlying table object is pre-built, so we can fetch its ID. + underlying_table_object = self.figure._function(**kwargs) + + if not hasattr(underlying_table_object, "id"): + raise ValueError( + "Underlying `Table` callable has no attribute 'id'. To enable actions triggered by the `Table`" + " a valid 'id' has to be provided to the `Table` callable." + ) + + self._callable_object_id = underlying_table_object.id + + def build(self): + return dcc.Loading( + html.Div( + [ + html.H3(self.title, className="table-title") if self.title else None, + html.Div( + dash_table.DataTable(**({"id": self._callable_object_id} if self.actions else {})), id=self.id + ), + ], + className="table-container", + id=f"{self.id}_outer", + ), + color="grey", + parent_className="loading-container", + ) diff --git a/vizro-core/src/vizro/models/_components/table.py b/vizro-core/src/vizro/models/_components/table.py index 5ee771261..32ab90d84 100644 --- a/vizro-core/src/vizro/models/_components/table.py +++ b/vizro-core/src/vizro/models/_components/table.py @@ -1,8 +1,8 @@ import logging -from typing import List, Literal +from typing import Dict, List, Literal -from dash import dash_table, dcc, html -from pandas import DataFrame +import pandas as pd +from dash import State, dash_table, dcc, html try: from pydantic.v1 import Field, PrivateAttr, validator @@ -10,6 +10,7 @@ from pydantic import Field, PrivateAttr, validator import vizro.tables as vt +from vizro.actions._actions_utils import CallbackTriggerDict, _get_component_actions, _get_parent_vizro_model from vizro.managers import data_manager from vizro.models import Action, VizroBaseModel from vizro.models._action._actions_chain import _action_validator_factory @@ -58,13 +59,47 @@ def __getitem__(self, arg_name: str): return self.type return self.figure[arg_name] + # Interaction methods + def _get_figure_interaction_input(self): + """Requiried properties when using pre-defined `filter_interaction`""" + return { + "active_cell": State(component_id=self._callable_object_id, component_property="active_cell"), + "derived_viewport_data": State( + component_id=self._callable_object_id, + component_property="derived_viewport_data", + ), + "modelID": State(component_id=self.id, component_property="id"), + } + + def _filter_interaction( + self, data_frame: pd.DataFrame, target: str, ctd_filter_interaction: Dict[str, CallbackTriggerDict] + ) -> pd.DataFrame: + """Function to be carried out for pre-defined `filter_interaction`""" + ctd_active_cell = ctd_filter_interaction["active_cell"] + ctd_derived_viewport_data = ctd_filter_interaction["derived_viewport_data"] + if not ctd_active_cell["value"] or not ctd_derived_viewport_data["value"]: + return data_frame + + # ctd_active_cell["id"] represents the underlying table id, so we need to fetch its parent Vizro Table actions. + source_table_actions = _get_component_actions(_get_parent_vizro_model(ctd_active_cell["id"])) + + for action in source_table_actions: + if action.function._function.__name__ != "filter_interaction" or target not in action.function["targets"]: + continue + column = ctd_active_cell["value"]["column_id"] + derived_viewport_data_row = ctd_active_cell["value"]["row"] + clicked_data = ctd_derived_viewport_data["value"][derived_viewport_data_row][column] + data_frame = data_frame[data_frame[column].isin([clicked_data])] + + return data_frame + @_log_call def pre_build(self): if self.actions: kwargs = self.figure._arguments.copy() # This workaround is needed because the underlying table object requires a data_frame - kwargs["data_frame"] = DataFrame() + kwargs["data_frame"] = pd.DataFrame() # The underlying table object is pre-built, so we can fetch its ID. underlying_table_object = self.figure._function(**kwargs) diff --git a/vizro-core/src/vizro/models/types.py b/vizro-core/src/vizro/models/types.py index db81a78f0..4bcd086f3 100644 --- a/vizro-core/src/vizro/models/types.py +++ b/vizro-core/src/vizro/models/types.py @@ -293,6 +293,7 @@ def wrapped(*args, **kwargs): return CapturedCallable(func, *args, **kwargs) return wrapped + #TODO: should we have "grid" - also add this elif self._mode == "table": @functools.wraps(func) @@ -362,15 +363,15 @@ class OptionsDictType(TypedDict): [`Parameter`][vizro.models.Parameter].""" ComponentType = Annotated[ - Union["Button", "Card", "Container", "Graph", "Table"], + Union["Button", "Card", "Container", "Graph", "Grid", "Table"], Field( discriminator="type", description="Component that makes up part of the layout on the page.", ), ] """Discriminated union. Type of component that makes up part of the layout on the page: -[`Button`][vizro.models.Button], [`Card`][vizro.models.Card], [`Table`][vizro.models.Table] or -[`Graph`][vizro.models.Graph].""" +[`Button`][vizro.models.Button], [`Card`][vizro.models.Card], [`Table`][vizro.models.Table], +[`Graph`][vizro.models.Graph] or [`Grid`][vizro.models.Grid].""" NavPagesType = Union[List[str], Dict[str, List[str]]] "List of page IDs or a mapping from name of a group to a list of page IDs (for hierarchical sub-navigation)." diff --git a/vizro-core/src/vizro/tables/dash_aggrid.py b/vizro-core/src/vizro/tables/dash_aggrid.py index da54ea069..3ec3a06de 100644 --- a/vizro-core/src/vizro/tables/dash_aggrid.py +++ b/vizro-core/src/vizro/tables/dash_aggrid.py @@ -1,20 +1,11 @@ import dash_ag_grid as dag -from dash import State from vizro.models.types import capture +@capture("table") def dash_ag_grid(data_frame=None, **kwargs): """Custom AgGrid.""" return dag.AgGrid( rowData=data_frame.to_dict("records"), columnDefs=[{"field": col} for col in data_frame.columns], **kwargs ) - - -dash_ag_grid.action_info = { - "filter_interaction_input": lambda x: { - "cellClicked": State(component_id=x._callable_object_id, component_property="cellClicked"), - } -} - -dash_ag_grid = capture("table")(dash_ag_grid) \ No newline at end of file From 61d190ed48fdab8498ec53a0e56e7f5180a20861 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:43:20 +0000 Subject: [PATCH 03/34] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- vizro-core/src/vizro/actions/_actions_utils.py | 2 +- vizro-core/src/vizro/models/types.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vizro-core/src/vizro/actions/_actions_utils.py b/vizro-core/src/vizro/actions/_actions_utils.py index 7ca6c78c7..e549054d5 100644 --- a/vizro-core/src/vizro/actions/_actions_utils.py +++ b/vizro-core/src/vizro/actions/_actions_utils.py @@ -139,7 +139,7 @@ def _apply_filter_interaction( target: str, ) -> pd.DataFrame: for ctd_filter_interaction in ctds_filter_interaction: - #TODO: make this more robust, such that it doesn't require the modelID + # TODO: make this more robust, such that it doesn't require the modelID triggered_model = model_manager[ctd_filter_interaction["modelID"]["id"]] data_frame = triggered_model._filter_interaction( data_frame=data_frame, diff --git a/vizro-core/src/vizro/models/types.py b/vizro-core/src/vizro/models/types.py index 4bcd086f3..498bf927b 100644 --- a/vizro-core/src/vizro/models/types.py +++ b/vizro-core/src/vizro/models/types.py @@ -293,7 +293,7 @@ def wrapped(*args, **kwargs): return CapturedCallable(func, *args, **kwargs) return wrapped - #TODO: should we have "grid" - also add this + # TODO: should we have "grid" - also add this elif self._mode == "table": @functools.wraps(func) From c186d916bf33f7bd5b1821b01d896b6c22a81088 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Mon, 19 Feb 2024 20:03:11 +0100 Subject: [PATCH 04/34] Initial PR ready implementation --- vizro-core/examples/_dev/app.py | 246 +++++++----------- vizro-core/pyproject.toml | 1 + .../src/vizro/actions/_actions_utils.py | 14 +- .../src/vizro/models/_components/graph.py | 8 +- .../src/vizro/models/_components/grid.py | 12 +- .../src/vizro/models/_components/table.py | 10 +- vizro-core/src/vizro/models/types.py | 20 +- vizro-core/src/vizro/tables/_utils.py | 13 + vizro-core/src/vizro/tables/dash_aggrid.py | 73 +++++- vizro-core/src/vizro/tables/dash_table.py | 14 +- 10 files changed, 213 insertions(+), 198 deletions(-) create mode 100644 vizro-core/src/vizro/tables/_utils.py diff --git a/vizro-core/examples/_dev/app.py b/vizro-core/examples/_dev/app.py index fc7e503a7..10b46c71a 100644 --- a/vizro-core/examples/_dev/app.py +++ b/vizro-core/examples/_dev/app.py @@ -1,14 +1,12 @@ """Example to show dashboard configuration.""" -from typing import List +import numpy as np import pandas as pd -from dash import State, dash_table import vizro.models as vm import vizro.plotly.express as px from vizro import Vizro from vizro.actions import export_data, filter_interaction -from vizro.models.types import capture from vizro.tables import dash_ag_grid, dash_data_table df = px.data.gapminder() @@ -22,169 +20,105 @@ df_transformed["pop"] = df.groupby(by=["continent", "year"])["pop"].transform("sum") df_concat = pd.concat([df_transformed.assign(color="Continent Avg."), df.assign(color="Country")], ignore_index=True) -gapminder_2007 = df.query("year == 2007") - -def my_custom_table(data_frame=None, id: str = None, chosen_columns: List[str] = None): - """Custom table.""" - columns = [{"name": i, "id": i} for i in chosen_columns] - defaults = { - "style_as_list_view": True, - "style_data": {"border_bottom": "1px solid var(--border-subtle-alpha-01)", "height": "40px"}, - "style_header": { - "border_bottom": "1px solid var(--state-overlays-selected-hover)", - "border_top": "1px solid var(--main-container-bg-color)", - "height": "32px", - }, - } - return dash_table.DataTable(data=data_frame.to_dict("records"), columns=columns, id=id, **defaults) - - -my_custom_table.action_info = { - "filter_interaction_input": lambda x: { - "active_cell": State(component_id=x._callable_object_id, component_property="active_cell"), - "derived_viewport_data": State( - component_id=x._callable_object_id, - component_property="derived_viewport_data", - ), - } -} - -my_custom_table = capture("table")(my_custom_table) - - -def create_benchmark_analysis(): - """Function returns a page to perform analysis on country level.""" - # Apply formatting to table columns - columns = [ - {"id": "country", "name": "country"}, - {"id": "continent", "name": "continent"}, - {"id": "year", "name": "year"}, - {"id": "lifeExp", "name": "lifeExp", "type": "numeric", "format": {"specifier": ",.1f"}}, - {"id": "gdpPercap", "name": "gdpPercap", "type": "numeric", "format": {"specifier": "$,.2f"}}, - {"id": "pop", "name": "pop", "type": "numeric", "format": {"specifier": ",d"}}, - ] - - page_country = vm.Page( - title="Table Test", - # description="Discovering how the metrics differ for each country and export data for further investigation", - # layout=vm.Layout(grid=[[0, 1]] * 5 + [[2, -1]], col_gap="32px", row_gap="60px"), - components=[ - vm.Grid( - id="table_country_new", - title="Click on a cell in country column:", - figure=dash_ag_grid( - id="dash_ag_grid_country", - data_frame=df, - ), - actions=[vm.Action(function=filter_interaction(targets=["line_country"]))], - ), - vm.Table( - id="table_country", - title="Click on a cell in country column:", - figure=dash_data_table( - id="dash_data_table_country", - data_frame=df, - columns=columns, - style_data_conditional=[ - { - "if": {"filter_query": "{gdpPercap} < 1045", "column_id": "gdpPercap"}, - "backgroundColor": "#ff9222", - }, - { - "if": { - "filter_query": "{gdpPercap} >= 1045 && {gdpPercap} <= 4095", - "column_id": "gdpPercap", - }, - "backgroundColor": "#de9e75", - }, - { - "if": { - "filter_query": "{gdpPercap} > 4095 && {gdpPercap} <= 12695", - "column_id": "gdpPercap", - }, - "backgroundColor": "#aaa9ba", - }, - { - "if": {"filter_query": "{gdpPercap} > 12695", "column_id": "gdpPercap"}, - "backgroundColor": "#00b4ff", - }, - ], - sort_action="native", - style_cell={"textAlign": "left"}, - ), - actions=[vm.Action(function=filter_interaction(targets=["line_country"]))], - ), - vm.Graph( - id="line_country", - figure=px.line( - df_concat, - title="Country vs. Continent", - x="year", - y="gdpPercap", - color="color", - labels={"year": "Year", "data": "Data", "gdpPercap": "GDP per capita"}, - color_discrete_map={"Country": "#afe7f9", "Continent": "#003875"}, - markers=True, - hover_name="country", - ), +grid_interaction = vm.Page( + title="Grid and Table Interaction", + components=[ + vm.Grid( + id="table_country_new", + title="Click on a cell", + figure=dash_ag_grid( + id="dash_ag_grid_1", + data_frame=px.data.gapminder(), ), - vm.Button( - text="Export data", - actions=[ - vm.Action( - function=export_data( - targets=["line_country"], - ) - ), - ], + actions=[vm.Action(function=filter_interaction(targets=["line_country"]))], + ), + vm.Table( + id="table_country", + title="Click on a cell", + figure=dash_data_table( + id="dash_data_table_country", + data_frame=df, + columns=[{"id": col, "name": col} for col in df.columns], + sort_action="native", + style_cell={"textAlign": "left"}, ), - vm.Table( # the custom table works with its own set of states defined above - id="custom_table", - title="Custom Dash DataTable", - figure=my_custom_table( - id="custom_dash_table_callable_id", - data_frame=df, - chosen_columns=["country", "continent", "lifeExp", "pop", "gdpPercap"], - ), - actions=[vm.Action(function=filter_interaction(targets=["line_country"]))], + actions=[vm.Action(function=filter_interaction(targets=["line_country"]))], + ), + vm.Graph( + id="line_country", + figure=px.line( + df_concat, + title="Country vs. Continent", + x="year", + y="gdpPercap", + color="color", + labels={"year": "Year", "data": "Data", "gdpPercap": "GDP per capita"}, + color_discrete_map={"Country": "#afe7f9", "Continent": "#003875"}, + markers=True, + hover_name="country", ), - ], - controls=[ - vm.Filter(column="continent", selector=vm.Dropdown(value="Europe", multi=False, title="Select continent")), - vm.Filter(column="year", selector=vm.RangeSlider(title="Select timeframe", step=1, marks=None)), - vm.Parameter( - targets=["line_country.y"], - selector=vm.Dropdown( - options=["lifeExp", "gdpPercap", "pop"], multi=False, value="gdpPercap", title="Choose y-axis" + ), + vm.Button( + text="Export data", + actions=[ + vm.Action( + function=export_data( + targets=["line_country"], + ) ), + ], + ), + ], + controls=[ + vm.Filter(column="continent", selector=vm.Dropdown(value="Europe", multi=False, title="Select continent")), + vm.Filter(column="year", selector=vm.RangeSlider(title="Select timeframe", step=1, marks=None)), + vm.Parameter( + targets=["line_country.y"], + selector=vm.Dropdown( + options=["lifeExp", "gdpPercap", "pop"], multi=False, value="gdpPercap", title="Choose y-axis" ), - ], - ) - return page_country + ), + ], +) -chart_interaction = vm.Page( - title="Chart interaction", +df2 = px.data.stocks() +df2["date_as_datetime"] = pd.to_datetime(df2["date"]) +df2["date_str"] = df2["date"].astype("str") +df2["perc_from_float"] = np.random.rand(len(df2)) +df2["random"] = np.random.uniform(-100000.000, 100000.000, len(df2)) + +grid_standard = vm.Page( + title="Grid Default", components=[ - vm.Graph( - figure=px.box( - gapminder_2007, - x="continent", - y="lifeExp", - color="continent", - custom_data=["continent"], + vm.Grid( + figure=dash_ag_grid( + id="dash_ag_grid_2", + data_frame=df2, ), - actions=[vm.Action(function=filter_interaction(targets=["scatter_relation_2007"]))], ), - vm.Graph( - id="scatter_relation_2007", - figure=px.scatter( - gapminder_2007, - x="gdpPercap", - y="lifeExp", - size="pop", - color="continent", + ], +) + +grid_custom = vm.Page( + title="Grid Custom", + components=[ + vm.Grid( + figure=dash_ag_grid( + id="dash_ag_grid_3", + data_frame=df2, + columnDefs=[ + {"field": "AAPL", "headerName": "Format Dollar", "cellDataType": "dollar"}, + {"field": "AAPL", "headerName": "Format Euro", "cellDataType": "euro"}, + {"field": "random", "headerName": "Format Numeric", "cellDataType": "numeric"}, + {"field": "perc_from_float", "headerName": "Format Percent", "cellDataType": "percent"}, + { + "field": "perc_from_float", + "headerName": "custom format", + "valueFormatter": {"function": "d3.format('.^30')(params.value)"}, + }, + ], ), ), ], @@ -192,7 +126,7 @@ def create_benchmark_analysis(): dashboard = vm.Dashboard( - pages=[create_benchmark_analysis(), chart_interaction], + pages=[grid_interaction, grid_standard, grid_custom], ) if __name__ == "__main__": diff --git a/vizro-core/pyproject.toml b/vizro-core/pyproject.toml index 475303f9b..6c039e5f0 100644 --- a/vizro-core/pyproject.toml +++ b/vizro-core/pyproject.toml @@ -17,6 +17,7 @@ classifiers = [ dependencies = [ "dash>=2.14.1", # 2.14.1 needed for compatibility with werkzeug "dash_bootstrap_components", + "dash-ag-grid>=31.0.0", "pandas", "pydantic>=1.10.13", # must be synced with pre-commit mypy hook manually "dash_mantine_components", diff --git a/vizro-core/src/vizro/actions/_actions_utils.py b/vizro-core/src/vizro/actions/_actions_utils.py index e549054d5..6c26c5bba 100644 --- a/vizro-core/src/vizro/actions/_actions_utils.py +++ b/vizro-core/src/vizro/actions/_actions_utils.py @@ -139,13 +139,13 @@ def _apply_filter_interaction( target: str, ) -> pd.DataFrame: for ctd_filter_interaction in ctds_filter_interaction: - # TODO: make this more robust, such that it doesn't require the modelID - triggered_model = model_manager[ctd_filter_interaction["modelID"]["id"]] - data_frame = triggered_model._filter_interaction( - data_frame=data_frame, - target=target, - ctd_filter_interaction=ctd_filter_interaction, - ) + if "modelID" in ctd_filter_interaction: + triggered_model = model_manager[ctd_filter_interaction["modelID"]["id"]] + data_frame = triggered_model._filter_interaction( + data_frame=data_frame, + target=target, + ctd_filter_interaction=ctd_filter_interaction, + ) return data_frame diff --git a/vizro-core/src/vizro/models/_components/graph.py b/vizro-core/src/vizro/models/_components/graph.py index 881733090..e5ab02ee5 100644 --- a/vizro-core/src/vizro/models/_components/graph.py +++ b/vizro-core/src/vizro/models/_components/graph.py @@ -75,17 +75,17 @@ def __getitem__(self, arg_name: str): return self.figure[arg_name] # Interaction methods - def _get_figure_interaction_input(self): - """Requiried properties when using pre-defined `filter_interaction`""" + def _get_figure_interaction_input(self) -> Dict[str, State]: + """Required properties when using pre-defined `filter_interaction`.""" return { "clickData": State(component_id=self.id, component_property="clickData"), - "modelID": State(component_id=self.id, component_property="id"), + "modelID": State(component_id=self.id, component_property="id"), # required, to determine triggered model } def _filter_interaction( self, data_frame: pd.DataFrame, target: str, ctd_filter_interaction: Dict[str, CallbackTriggerDict] ) -> pd.DataFrame: - """Function to be carried out for pre-defined `filter_interaction`""" + """Function to be carried out for pre-defined `filter_interaction`.""" ctd_click_data = ctd_filter_interaction["clickData"] if not ctd_click_data["value"]: return data_frame diff --git a/vizro-core/src/vizro/models/_components/grid.py b/vizro-core/src/vizro/models/_components/grid.py index ba5d9a484..c781dfb94 100644 --- a/vizro-core/src/vizro/models/_components/grid.py +++ b/vizro-core/src/vizro/models/_components/grid.py @@ -22,11 +22,11 @@ class Grid(VizroBaseModel): - """Wrapper for table components to visualize in dashboard. + """Wrapper for `dash-ag-grid.AgGrid` to visualize grids in dashboard. Args: type (Literal["grid"]): Defaults to `"grid"`. - figure (CapturedCallable): Grid like object to be displayed. Current choices include: + figure (CapturedCallable): Grid like object to be displayed. For more information see: [`dash-ag-grid.AgGrid`](https://dash.plotly.com/dash-ag-grid). title (str): Title of the table. Defaults to `""`. actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`. @@ -59,17 +59,17 @@ def __getitem__(self, arg_name: str): return self.figure[arg_name] # Interaction methods - def _get_figure_interaction_input(self): - """Requiried properties when using pre-defined `filter_interaction`""" + def _get_figure_interaction_input(self) -> Dict[str, State]: + """Required properties when using pre-defined `filter_interaction`.""" return { "cellClicked": State(component_id=self._callable_object_id, component_property="cellClicked"), - "modelID": State(component_id=self.id, component_property="id"), + "modelID": State(component_id=self.id, component_property="id"), # required, to determine triggered model } def _filter_interaction( self, data_frame: pd.DataFrame, target: str, ctd_filter_interaction: Dict[str, CallbackTriggerDict] ) -> pd.DataFrame: - """Function to be carried out for pre-defined `filter_interaction`""" + """Function to be carried out for pre-defined `filter_interaction`.""" ctd_cellClicked = ctd_filter_interaction["cellClicked"] if not ctd_cellClicked["value"]: return data_frame diff --git a/vizro-core/src/vizro/models/_components/table.py b/vizro-core/src/vizro/models/_components/table.py index 32ab90d84..3b5cebf08 100644 --- a/vizro-core/src/vizro/models/_components/table.py +++ b/vizro-core/src/vizro/models/_components/table.py @@ -22,11 +22,11 @@ class Table(VizroBaseModel): - """Wrapper for table components to visualize in dashboard. + """Wrapper for `dash_table.DataTable` to visualize tables in dashboard. Args: type (Literal["table"]): Defaults to `"table"`. - figure (CapturedCallable): Table like object to be displayed. Current choices include: + figure (CapturedCallable): Table like object to be displayed. For more information see: [`dash_table.DataTable`](https://dash.plotly.com/datatable). title (str): Title of the table. Defaults to `""`. actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`. @@ -60,15 +60,15 @@ def __getitem__(self, arg_name: str): return self.figure[arg_name] # Interaction methods - def _get_figure_interaction_input(self): - """Requiried properties when using pre-defined `filter_interaction`""" + def _get_figure_interaction_input(self) -> Dict[str, State]: + """Required properties when using pre-defined `filter_interaction`.""" return { "active_cell": State(component_id=self._callable_object_id, component_property="active_cell"), "derived_viewport_data": State( component_id=self._callable_object_id, component_property="derived_viewport_data", ), - "modelID": State(component_id=self.id, component_property="id"), + "modelID": State(component_id=self.id, component_property="id"), # required, to determine triggered model } def _filter_interaction( diff --git a/vizro-core/src/vizro/models/types.py b/vizro-core/src/vizro/models/types.py index 498bf927b..66f74547d 100644 --- a/vizro-core/src/vizro/models/types.py +++ b/vizro-core/src/vizro/models/types.py @@ -293,7 +293,6 @@ def wrapped(*args, **kwargs): return CapturedCallable(func, *args, **kwargs) return wrapped - # TODO: should we have "grid" - also add this elif self._mode == "table": @functools.wraps(func) @@ -309,9 +308,26 @@ def wrapped(*args, **kwargs): raise ValueError(f"{func.__name__} must supply a value to data_frame argument.") from exc return captured_callable + return wrapped + elif self._mode == "grid": + + @functools.wraps(func) + def wrapped(*args, **kwargs): + if "data_frame" not in inspect.signature(func).parameters: + raise ValueError(f"{func.__name__} must have data_frame argument to use capture('grid').") + + captured_callable: CapturedCallable = CapturedCallable(func, *args, **kwargs) + + try: + captured_callable["data_frame"] + except KeyError as exc: + raise ValueError(f"{func.__name__} must supply a value to data_frame argument.") from exc + return captured_callable + return wrapped raise ValueError( - "Valid modes of the capture decorator are @capture('graph'), @capture('action') or @capture('table')." + "Valid modes of the capture decorator are @capture('graph'), @capture('action'), @capture('table') or " + "@capture('grid')." ) diff --git a/vizro-core/src/vizro/tables/_utils.py b/vizro-core/src/vizro/tables/_utils.py new file mode 100644 index 000000000..ec2791917 --- /dev/null +++ b/vizro-core/src/vizro/tables/_utils.py @@ -0,0 +1,13 @@ +"""Contains utilities for the implementation of table callables.""" +from collections import defaultdict +from typing import Any, Dict, Mapping + + +def _set_defaults_nested(supplied: Mapping[str, Any], defaults: Mapping[str, Any]) -> Dict[str, Any]: + supplied = defaultdict(dict, supplied) + for default_key, default_value in defaults.items(): + if isinstance(default_value, Mapping): + supplied[default_key] = _set_defaults_nested(supplied[default_key], default_value) + else: + supplied.setdefault(default_key, default_value) + return dict(supplied) diff --git a/vizro-core/src/vizro/tables/dash_aggrid.py b/vizro-core/src/vizro/tables/dash_aggrid.py index 3ec3a06de..5fc2aff95 100644 --- a/vizro-core/src/vizro/tables/dash_aggrid.py +++ b/vizro-core/src/vizro/tables/dash_aggrid.py @@ -1,11 +1,74 @@ import dash_ag_grid as dag +import pandas as pd from vizro.models.types import capture +from vizro.tables._utils import _set_defaults_nested +FORMAT_CURRENCY_EU = """d3.formatLocale({ + "decimal": ",", + "thousands": "\u00a0", + "grouping": [3], + "currency": ["", "\u00a0€"], + "percent": "\u202f%", + "nan": "" +})""" -@capture("table") +DATA_TYPE_DEFINITIONS = { + "number": { + "baseDataType": "number", + "extendsDataType": "number", + "columnTypes": ["numberColumn", "rightAligned"], + "appendColumnTypes": True, + "valueFormatter": {"function": "params.value == null ? 'NaN' : String(params.value)"}, + }, + "dollar": { + "baseDataType": "number", + "extendsDataType": "number", + "valueFormatter": {"function": "d3.format('($,.2f')(params.value)"}, + }, + "euro": { + "baseDataType": "number", + "extendsDataType": "number", + "valueFormatter": {"function": f"{FORMAT_CURRENCY_EU}.format('$,.2f')(params.value)"}, + }, + "percent": { + "baseDataType": "number", + "extendsDataType": "number", + "valueFormatter": {"function": "d3.format(',.1%')(params.value)"}, + }, + "numeric": { + "baseDataType": "number", + "extendsDataType": "number", + "valueFormatter": {"function": "d3.format(',.1f')(params.value)"}, + }, +} + + +@capture("grid") def dash_ag_grid(data_frame=None, **kwargs): - """Custom AgGrid.""" - return dag.AgGrid( - rowData=data_frame.to_dict("records"), columnDefs=[{"field": col} for col in data_frame.columns], **kwargs - ) + """Implementation of `dash-ag-grid.AgGrid` with sensible defaults.""" + defaults = { + "columnDefs": [{"field": col} for col in data_frame.columns], + "rowData": data_frame.apply( + lambda x: x.dt.strftime("%Y-%m-%d") # set date columns to `dateString` for AGGrid filtering to function + if pd.api.types.is_datetime64_any_dtype(x) + else x + ).to_dict("records"), + "defaultColDef": { + # "editable": True, #do not set, as this may confuse some users + "resizable": True, + "sortable": True, + "filter": True, + "filterParams": { + "buttons": ["apply", "reset"], + "closeOnApply": True, + }, + }, + "dashGridOptions": { + "dataTypeDefinitions": DATA_TYPE_DEFINITIONS, + "animateRows": False, + "pagination": True, + }, + } + kwargs = _set_defaults_nested(kwargs, defaults) + return dag.AgGrid(**kwargs) diff --git a/vizro-core/src/vizro/tables/dash_table.py b/vizro-core/src/vizro/tables/dash_table.py index f28315834..0b44c6440 100644 --- a/vizro-core/src/vizro/tables/dash_table.py +++ b/vizro-core/src/vizro/tables/dash_table.py @@ -1,21 +1,9 @@ """Module containing the standard implementation of `dash_table.DataTable`.""" -from collections import defaultdict -from typing import Any, Dict, Mapping - import pandas as pd from dash import dash_table from vizro.models.types import capture - - -def _set_defaults_nested(supplied: Mapping[str, Any], defaults: Mapping[str, Any]) -> Dict[str, Any]: - supplied = defaultdict(dict, supplied) - for default_key, default_value in defaults.items(): - if isinstance(default_value, Mapping): - supplied[default_key] = _set_defaults_nested(supplied[default_key], default_value) - else: - supplied.setdefault(default_key, default_value) - return dict(supplied) +from vizro.tables._utils import _set_defaults_nested @capture("table") From 923be6785cd365349f708d44d54c98bf485f3cb6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 09:03:09 +0000 Subject: [PATCH 05/34] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- vizro-core/examples/_dev/app.py | 1 - vizro-core/src/vizro/models/_components/grid.py | 1 + vizro-core/src/vizro/tables/_utils.py | 1 + vizro-core/src/vizro/tables/dash_aggrid.py | 8 +++++--- vizro-core/src/vizro/tables/dash_table.py | 1 + 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/vizro-core/examples/_dev/app.py b/vizro-core/examples/_dev/app.py index 10b46c71a..bf5958ac8 100644 --- a/vizro-core/examples/_dev/app.py +++ b/vizro-core/examples/_dev/app.py @@ -2,7 +2,6 @@ import numpy as np import pandas as pd - import vizro.models as vm import vizro.plotly.express as px from vizro import Vizro diff --git a/vizro-core/src/vizro/models/_components/grid.py b/vizro-core/src/vizro/models/_components/grid.py index c781dfb94..35cb42316 100644 --- a/vizro-core/src/vizro/models/_components/grid.py +++ b/vizro-core/src/vizro/models/_components/grid.py @@ -30,6 +30,7 @@ class Grid(VizroBaseModel): [`dash-ag-grid.AgGrid`](https://dash.plotly.com/dash-ag-grid). title (str): Title of the table. Defaults to `""`. actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`. + """ type: Literal["grid"] = "grid" diff --git a/vizro-core/src/vizro/tables/_utils.py b/vizro-core/src/vizro/tables/_utils.py index ec2791917..63fe98c68 100644 --- a/vizro-core/src/vizro/tables/_utils.py +++ b/vizro-core/src/vizro/tables/_utils.py @@ -1,4 +1,5 @@ """Contains utilities for the implementation of table callables.""" + from collections import defaultdict from typing import Any, Dict, Mapping diff --git a/vizro-core/src/vizro/tables/dash_aggrid.py b/vizro-core/src/vizro/tables/dash_aggrid.py index 5fc2aff95..c9f829f9e 100644 --- a/vizro-core/src/vizro/tables/dash_aggrid.py +++ b/vizro-core/src/vizro/tables/dash_aggrid.py @@ -50,9 +50,11 @@ def dash_ag_grid(data_frame=None, **kwargs): defaults = { "columnDefs": [{"field": col} for col in data_frame.columns], "rowData": data_frame.apply( - lambda x: x.dt.strftime("%Y-%m-%d") # set date columns to `dateString` for AGGrid filtering to function - if pd.api.types.is_datetime64_any_dtype(x) - else x + lambda x: ( + x.dt.strftime("%Y-%m-%d") # set date columns to `dateString` for AGGrid filtering to function + if pd.api.types.is_datetime64_any_dtype(x) + else x + ) ).to_dict("records"), "defaultColDef": { # "editable": True, #do not set, as this may confuse some users diff --git a/vizro-core/src/vizro/tables/dash_table.py b/vizro-core/src/vizro/tables/dash_table.py index 0b44c6440..41cc43ded 100644 --- a/vizro-core/src/vizro/tables/dash_table.py +++ b/vizro-core/src/vizro/tables/dash_table.py @@ -1,4 +1,5 @@ """Module containing the standard implementation of `dash_table.DataTable`.""" + import pandas as pd from dash import dash_table From 69a7b0737eb3a536ef198f01e3b64ad32612b1b0 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Tue, 20 Feb 2024 10:09:08 +0100 Subject: [PATCH 06/34] Linting --- vizro-core/src/vizro/models/_components/table.py | 2 +- vizro-core/src/vizro/models/types.py | 4 ++-- vizro-core/src/vizro/tables/dash_aggrid.py | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/vizro-core/src/vizro/models/_components/table.py b/vizro-core/src/vizro/models/_components/table.py index 1648716a9..25ce2c5d2 100644 --- a/vizro-core/src/vizro/models/_components/table.py +++ b/vizro-core/src/vizro/models/_components/table.py @@ -75,7 +75,7 @@ def _get_figure_interaction_input(self) -> Dict[str, State]: def _filter_interaction( self, data_frame: pd.DataFrame, target: str, ctd_filter_interaction: Dict[str, CallbackTriggerDict] ) -> pd.DataFrame: - """Function to be carried out for pre-defined `filter_interaction`""" + """Function to be carried out for pre-defined `filter_interaction`.""" ctd_active_cell = ctd_filter_interaction["active_cell"] ctd_derived_viewport_data = ctd_filter_interaction["derived_viewport_data"] if not ctd_active_cell["value"] or not ctd_derived_viewport_data["value"]: diff --git a/vizro-core/src/vizro/models/types.py b/vizro-core/src/vizro/models/types.py index bfa82aae7..834fbc24d 100644 --- a/vizro-core/src/vizro/models/types.py +++ b/vizro-core/src/vizro/models/types.py @@ -242,8 +242,8 @@ class capture: """ - def __init__(self, mode: Literal["graph", "action", "table"]): - """Instantiates the decorator to capture a function call. Valid modes are "graph", "table" and "action".""" + def __init__(self, mode: Literal["graph", "action", "table", "grid"]): + """Decorator to capture a function call. Valid modes are "graph", "table", "action" and "grid".""" self._mode = mode def __call__(self, func, /): diff --git a/vizro-core/src/vizro/tables/dash_aggrid.py b/vizro-core/src/vizro/tables/dash_aggrid.py index c9f829f9e..f06b6a8a2 100644 --- a/vizro-core/src/vizro/tables/dash_aggrid.py +++ b/vizro-core/src/vizro/tables/dash_aggrid.py @@ -1,3 +1,5 @@ +"""Module containing the standard implementation of `dash-ag-grid.AgGrid`.""" + import dash_ag_grid as dag import pandas as pd From d81b1c873906dccd0e59aab6e0b79a18672a8404 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Tue, 20 Feb 2024 10:31:50 +0100 Subject: [PATCH 07/34] Small updates to grid model --- vizro-core/src/vizro/models/_components/grid.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/vizro-core/src/vizro/models/_components/grid.py b/vizro-core/src/vizro/models/_components/grid.py index 35cb42316..3bdaa3b11 100644 --- a/vizro-core/src/vizro/models/_components/grid.py +++ b/vizro-core/src/vizro/models/_components/grid.py @@ -92,19 +92,17 @@ def pre_build(self): if self.actions: kwargs = self.figure._arguments.copy() - # This workaround is needed because the underlying table object requires a data_frame + # taken from table implementation - see there for details kwargs["data_frame"] = pd.DataFrame() + underlying_grid_object = self.figure._function(**kwargs) - # The underlying table object is pre-built, so we can fetch its ID. - underlying_table_object = self.figure._function(**kwargs) - - if not hasattr(underlying_table_object, "id"): + if not hasattr(underlying_grid_object, "id"): raise ValueError( - "Underlying `Table` callable has no attribute 'id'. To enable actions triggered by the `Table`" - " a valid 'id' has to be provided to the `Table` callable." + "Underlying `Grid` callable has no attribute 'id'. To enable actions triggered by the `Grid`" + " a valid 'id' has to be provided to the `Grid` callable." ) - self._callable_object_id = underlying_table_object.id + self._callable_object_id = underlying_grid_object.id def build(self): return dcc.Loading( From 8703d0e817b19a9733fcacdd931b07970c23ddf7 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Tue, 20 Feb 2024 10:48:21 +0100 Subject: [PATCH 08/34] Remove ununsed files --- .../src/vizro/actions/_actions_utils.py | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/vizro-core/src/vizro/actions/_actions_utils.py b/vizro-core/src/vizro/actions/_actions_utils.py index e1d09eac1..e330ddb41 100644 --- a/vizro-core/src/vizro/actions/_actions_utils.py +++ b/vizro-core/src/vizro/actions/_actions_utils.py @@ -69,31 +69,6 @@ def _apply_filters(data_frame: pd.DataFrame, ctds_filters: List[CallbackTriggerD return data_frame -def _apply_graph_filter_interaction( - data_frame: pd.DataFrame, target: str, ctd_filter_interaction: Dict[str, CallbackTriggerDict] -) -> pd.DataFrame: - ctd_click_data = ctd_filter_interaction["clickData"] - if not ctd_click_data["value"]: - return data_frame - - source_graph_id: ModelID = ctd_click_data["id"] - source_graph_actions = _get_component_actions(model_manager[source_graph_id]) - try: - custom_data_columns = model_manager[source_graph_id]["custom_data"] - except KeyError as exc: - raise KeyError(f"No `custom_data` argument found for source graph with id {source_graph_id}.") from exc - - customdata = ctd_click_data["value"]["points"][0]["customdata"] - - for action in source_graph_actions: - if action.function._function.__name__ != "filter_interaction" or target not in action.function["targets"]: - continue - for custom_data_idx, column in enumerate(custom_data_columns): - data_frame = data_frame[data_frame[column].isin([customdata[custom_data_idx]])] - - return data_frame - - def _get_parent_vizro_model(_underlying_callable_object_id: str) -> VizroBaseModel: from vizro.models import VizroBaseModel @@ -108,28 +83,6 @@ def _get_parent_vizro_model(_underlying_callable_object_id: str) -> VizroBaseMod ) -def _apply_table_filter_interaction( - data_frame: pd.DataFrame, target: str, ctd_filter_interaction: Dict[str, CallbackTriggerDict] -) -> pd.DataFrame: - ctd_active_cell = ctd_filter_interaction["active_cell"] - ctd_derived_viewport_data = ctd_filter_interaction["derived_viewport_data"] - if not ctd_active_cell["value"] or not ctd_derived_viewport_data["value"]: - return data_frame - - # ctd_active_cell["id"] represents the underlying table id, so we need to fetch its parent Vizro Table actions. - source_table_actions = _get_component_actions(_get_parent_vizro_model(ctd_active_cell["id"])) - - for action in source_table_actions: - if action.function._function.__name__ != "filter_interaction" or target not in action.function["targets"]: - continue - column = ctd_active_cell["value"]["column_id"] - derived_viewport_data_row = ctd_active_cell["value"]["row"] - clicked_data = ctd_derived_viewport_data["value"][derived_viewport_data_row][column] - data_frame = data_frame[data_frame[column].isin([clicked_data])] - - return data_frame - - def _apply_filter_interaction( data_frame: pd.DataFrame, ctds_filter_interaction: List[Dict[str, CallbackTriggerDict]], target: str ) -> pd.DataFrame: From e642da13e2101a494530faa687b400ef2e1d8d3e Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Wed, 21 Feb 2024 11:26:40 +0100 Subject: [PATCH 09/34] Change name from Grid to AGGrid --- vizro-core/examples/_dev/app.py | 12 +++++----- vizro-core/src/vizro/models/__init__.py | 10 +++++---- .../src/vizro/models/_components/__init__.py | 4 ++-- .../models/_components/{grid.py => aggrid.py} | 20 ++++++++--------- vizro-core/src/vizro/models/types.py | 22 +++++++++---------- vizro-core/src/vizro/tables/dash_aggrid.py | 2 +- 6 files changed, 36 insertions(+), 34 deletions(-) rename vizro-core/src/vizro/models/_components/{grid.py => aggrid.py} (86%) diff --git a/vizro-core/examples/_dev/app.py b/vizro-core/examples/_dev/app.py index bf5958ac8..d0aa2040a 100644 --- a/vizro-core/examples/_dev/app.py +++ b/vizro-core/examples/_dev/app.py @@ -21,9 +21,9 @@ grid_interaction = vm.Page( - title="Grid and Table Interaction", + title="AGGrid and Table Interaction", components=[ - vm.Grid( + vm.AGGrid( id="table_country_new", title="Click on a cell", figure=dash_ag_grid( @@ -89,9 +89,9 @@ df2["random"] = np.random.uniform(-100000.000, 100000.000, len(df2)) grid_standard = vm.Page( - title="Grid Default", + title="AGGrid Default", components=[ - vm.Grid( + vm.AGGrid( figure=dash_ag_grid( id="dash_ag_grid_2", data_frame=df2, @@ -101,9 +101,9 @@ ) grid_custom = vm.Page( - title="Grid Custom", + title="AGGrid Custom", components=[ - vm.Grid( + vm.AGGrid( figure=dash_ag_grid( id="dash_ag_grid_3", data_frame=df2, diff --git a/vizro-core/src/vizro/models/__init__.py b/vizro-core/src/vizro/models/__init__.py index 31a36728c..5f7c9ba22 100644 --- a/vizro-core/src/vizro/models/__init__.py +++ b/vizro-core/src/vizro/models/__init__.py @@ -1,7 +1,7 @@ # Keep this import at the top to avoid circular imports since it's used in every model. from ._base import VizroBaseModel # noqa: I001 from ._action import Action -from ._components import Card, Container, Graph, Grid, Table, Tabs +from ._components import Card, Container, Graph, AGGrid, Table, Tabs from ._components.form import Button, Checklist, Dropdown, RadioItems, RangeSlider, Slider from ._controls import Filter, Parameter from ._navigation.accordion import Accordion @@ -13,15 +13,17 @@ from ._page import Page Tabs.update_forward_refs(Container=Container) -Container.update_forward_refs(Button=Button, Card=Card, Graph=Graph, Grid=Grid, Layout=Layout, Table=Table, Tabs=Tabs) +Container.update_forward_refs( + AGGrid=AGGrid, Button=Button, Card=Card, Graph=Graph, Layout=Layout, Table=Table, Tabs=Tabs +) Page.update_forward_refs( Accordion=Accordion, + AGGrid=AGGrid, Button=Button, Card=Card, Container=Container, Filter=Filter, Graph=Graph, - Grid=Grid, Parameter=Parameter, Table=Table, Tabs=Tabs, @@ -35,6 +37,7 @@ __all__ = [ "Accordion", "Action", + "AGGrid", "Button", "Card", "Container", @@ -43,7 +46,6 @@ "Dropdown", "Filter", "Graph", - "Grid", "Layout", "NavBar", "NavLink", diff --git a/vizro-core/src/vizro/models/_components/__init__.py b/vizro-core/src/vizro/models/_components/__init__.py index 7a350bf61..578b88bce 100644 --- a/vizro-core/src/vizro/models/_components/__init__.py +++ b/vizro-core/src/vizro/models/_components/__init__.py @@ -1,11 +1,11 @@ """Components that are placed according to the `Layout` of the `Page`.""" +from vizro.models._components.aggrid import AGGrid from vizro.models._components.button import Button from vizro.models._components.card import Card from vizro.models._components.container import Container from vizro.models._components.graph import Graph -from vizro.models._components.grid import Grid from vizro.models._components.table import Table from vizro.models._components.tabs import Tabs -__all__ = ["Button", "Card", "Container", "Graph", "Grid", "Table", "Tabs"] +__all__ = ["AGGrid", "Button", "Card", "Container", "Graph", "Table", "Tabs"] diff --git a/vizro-core/src/vizro/models/_components/grid.py b/vizro-core/src/vizro/models/_components/aggrid.py similarity index 86% rename from vizro-core/src/vizro/models/_components/grid.py rename to vizro-core/src/vizro/models/_components/aggrid.py index 3bdaa3b11..b936be70e 100644 --- a/vizro-core/src/vizro/models/_components/grid.py +++ b/vizro-core/src/vizro/models/_components/aggrid.py @@ -21,21 +21,21 @@ logger = logging.getLogger(__name__) -class Grid(VizroBaseModel): +class AGGrid(VizroBaseModel): """Wrapper for `dash-ag-grid.AgGrid` to visualize grids in dashboard. Args: type (Literal["grid"]): Defaults to `"grid"`. - figure (CapturedCallable): Grid like object to be displayed. For more information see: + figure (CapturedCallable): AGGrid like object to be displayed. For more information see: [`dash-ag-grid.AgGrid`](https://dash.plotly.com/dash-ag-grid). title (str): Title of the table. Defaults to `""`. actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`. """ - type: Literal["grid"] = "grid" - figure: CapturedCallable = Field(..., import_path=vt, description="Grid to be visualized on dashboard") - title: str = Field("", description="Title of the grid") + type: Literal["aggrid"] = "aggrid" + figure: CapturedCallable = Field(..., import_path=vt, description="AGGrid to be visualized on dashboard") + title: str = Field("", description="Title of the AGGrid") actions: List[Action] = [] _callable_object_id: str = PrivateAttr() @@ -94,15 +94,15 @@ def pre_build(self): # taken from table implementation - see there for details kwargs["data_frame"] = pd.DataFrame() - underlying_grid_object = self.figure._function(**kwargs) + underlying_aggrid_object = self.figure._function(**kwargs) - if not hasattr(underlying_grid_object, "id"): + if not hasattr(underlying_aggrid_object, "id"): raise ValueError( - "Underlying `Grid` callable has no attribute 'id'. To enable actions triggered by the `Grid`" - " a valid 'id' has to be provided to the `Grid` callable." + "Underlying `AGGrid` callable has no attribute 'id'. To enable actions triggered by the `AGGrid`" + " a valid 'id' has to be provided to the `AGGrid` callable." ) - self._callable_object_id = underlying_grid_object.id + self._callable_object_id = underlying_aggrid_object.id def build(self): return dcc.Loading( diff --git a/vizro-core/src/vizro/models/types.py b/vizro-core/src/vizro/models/types.py index 834fbc24d..77fcaafe2 100644 --- a/vizro-core/src/vizro/models/types.py +++ b/vizro-core/src/vizro/models/types.py @@ -216,8 +216,8 @@ class capture: """Captures a function call to create a [`CapturedCallable`][vizro.models.types.CapturedCallable]. This is used to add the functionality required to make graphs and actions work in a dashboard. - Typically, it should be used as a function decorator. There are four possible modes: `"graph"`, `"table"`, `"grid"` - and `"action"`. + Typically, it should be used as a function decorator. There are four possible modes: `"graph"`, `"table"`, + `"aggrid"` and `"action"`. Examples >>> @capture("graph") @@ -226,8 +226,8 @@ class capture: >>> @capture("table") >>> def table_function(): >>> ... - >>> @capture("grid") - >>> def grid_function(): + >>> @capture("aggrid") + >>> def aggrid_function(): >>> ... >>> @capture("action") >>> def action_function(): @@ -242,8 +242,8 @@ class capture: """ - def __init__(self, mode: Literal["graph", "action", "table", "grid"]): - """Decorator to capture a function call. Valid modes are "graph", "table", "action" and "grid".""" + def __init__(self, mode: Literal["graph", "action", "table", "aggrid"]): + """Decorator to capture a function call. Valid modes are "graph", "table", "action" and "aggrid".""" self._mode = mode def __call__(self, func, /): @@ -313,12 +313,12 @@ def wrapped(*args, **kwargs): return captured_callable return wrapped - elif self._mode == "grid": + elif self._mode == "aggrid": @functools.wraps(func) def wrapped(*args, **kwargs): if "data_frame" not in inspect.signature(func).parameters: - raise ValueError(f"{func.__name__} must have data_frame argument to use capture('grid').") + raise ValueError(f"{func.__name__} must have data_frame argument to use capture('aggrid').") captured_callable: CapturedCallable = CapturedCallable(func, *args, **kwargs) @@ -331,7 +331,7 @@ def wrapped(*args, **kwargs): return wrapped raise ValueError( "Valid modes of the capture decorator are @capture('graph'), @capture('action'), @capture('table') or " - "@capture('grid')." + "@capture('aggrid')." ) @@ -374,7 +374,7 @@ class OptionsDictType(TypedDict): [`Parameter`][vizro.models.Parameter].""" ComponentType = Annotated[ - Union["Button", "Card", "Container", "Graph", "Grid", "Table", "Tabs"], + Union["AGGrid", "Button", "Card", "Container", "Graph", "Table", "Tabs"], Field( discriminator="type", description="Component that makes up part of the layout on the page.", @@ -382,7 +382,7 @@ class OptionsDictType(TypedDict): ] """Discriminated union. Type of component that makes up part of the layout on the page: [`Button`][vizro.models.Button], [`Card`][vizro.models.Card], [`Table`][vizro.models.Table], -[`Graph`][vizro.models.Graph] or [`Grid`][vizro.models.Grid].""" +[`Graph`][vizro.models.Graph] or [`AGGrid`][vizro.models.AGGrid].""" NavPagesType = Union[List[str], Dict[str, List[str]]] "List of page IDs or a mapping from name of a group to a list of page IDs (for hierarchical sub-navigation)." diff --git a/vizro-core/src/vizro/tables/dash_aggrid.py b/vizro-core/src/vizro/tables/dash_aggrid.py index f06b6a8a2..0256e2709 100644 --- a/vizro-core/src/vizro/tables/dash_aggrid.py +++ b/vizro-core/src/vizro/tables/dash_aggrid.py @@ -46,7 +46,7 @@ } -@capture("grid") +@capture("aggrid") def dash_ag_grid(data_frame=None, **kwargs): """Implementation of `dash-ag-grid.AgGrid` with sensible defaults.""" defaults = { From b92f0ebea455557646e91f6d81a6c8556fa2e1b0 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Thu, 22 Feb 2024 09:06:30 +0100 Subject: [PATCH 10/34] First batch of PR comments --- vizro-core/examples/_dev/app.py | 12 +++---- .../_callback_mapping_utils.py | 4 +-- vizro-core/src/vizro/models/__init__.py | 8 ++--- .../src/vizro/models/_components/__init__.py | 4 +-- .../src/vizro/models/_components/aggrid.py | 19 ++++++----- .../src/vizro/models/_components/graph.py | 3 +- .../src/vizro/models/_components/table.py | 3 +- vizro-core/src/vizro/models/types.py | 34 +++++-------------- vizro-core/src/vizro/tables/dash_aggrid.py | 10 +++--- 9 files changed, 42 insertions(+), 55 deletions(-) diff --git a/vizro-core/examples/_dev/app.py b/vizro-core/examples/_dev/app.py index d0aa2040a..d842ba7a9 100644 --- a/vizro-core/examples/_dev/app.py +++ b/vizro-core/examples/_dev/app.py @@ -21,9 +21,9 @@ grid_interaction = vm.Page( - title="AGGrid and Table Interaction", + title="AG Grid and Table Interaction", components=[ - vm.AGGrid( + vm.AgGrid( id="table_country_new", title="Click on a cell", figure=dash_ag_grid( @@ -89,9 +89,9 @@ df2["random"] = np.random.uniform(-100000.000, 100000.000, len(df2)) grid_standard = vm.Page( - title="AGGrid Default", + title="AG Grid Default", components=[ - vm.AGGrid( + vm.AgGrid( figure=dash_ag_grid( id="dash_ag_grid_2", data_frame=df2, @@ -101,9 +101,9 @@ ) grid_custom = vm.Page( - title="AGGrid Custom", + title="AG Grid Custom", components=[ - vm.AGGrid( + vm.AgGrid( figure=dash_ag_grid( id="dash_ag_grid_3", data_frame=df2, diff --git a/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py b/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py index 7bea98c56..d28d45846 100644 --- a/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py +++ b/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py @@ -47,8 +47,8 @@ def _get_inputs_of_figure_interactions( inputs = [] for action in figure_interactions_on_page: triggered_model = model_manager._get_action_trigger(action_id=ModelID(str(action.id))) - if hasattr(triggered_model, "_get_figure_interaction_input"): - inputs.append(triggered_model._get_figure_interaction_input()) + if hasattr(triggered_model, "_figure_interaction_input"): + inputs.append(triggered_model._figure_interaction_input) return inputs diff --git a/vizro-core/src/vizro/models/__init__.py b/vizro-core/src/vizro/models/__init__.py index 5f7c9ba22..981f91c37 100644 --- a/vizro-core/src/vizro/models/__init__.py +++ b/vizro-core/src/vizro/models/__init__.py @@ -1,7 +1,7 @@ # Keep this import at the top to avoid circular imports since it's used in every model. from ._base import VizroBaseModel # noqa: I001 from ._action import Action -from ._components import Card, Container, Graph, AGGrid, Table, Tabs +from ._components import Card, Container, Graph, AgGrid, Table, Tabs from ._components.form import Button, Checklist, Dropdown, RadioItems, RangeSlider, Slider from ._controls import Filter, Parameter from ._navigation.accordion import Accordion @@ -14,11 +14,11 @@ Tabs.update_forward_refs(Container=Container) Container.update_forward_refs( - AGGrid=AGGrid, Button=Button, Card=Card, Graph=Graph, Layout=Layout, Table=Table, Tabs=Tabs + AgGrid=AgGrid, Button=Button, Card=Card, Graph=Graph, Layout=Layout, Table=Table, Tabs=Tabs ) Page.update_forward_refs( Accordion=Accordion, - AGGrid=AGGrid, + AgGrid=AgGrid, Button=Button, Card=Card, Container=Container, @@ -37,7 +37,7 @@ __all__ = [ "Accordion", "Action", - "AGGrid", + "AgGrid", "Button", "Card", "Container", diff --git a/vizro-core/src/vizro/models/_components/__init__.py b/vizro-core/src/vizro/models/_components/__init__.py index 578b88bce..be0b8603b 100644 --- a/vizro-core/src/vizro/models/_components/__init__.py +++ b/vizro-core/src/vizro/models/_components/__init__.py @@ -1,6 +1,6 @@ """Components that are placed according to the `Layout` of the `Page`.""" -from vizro.models._components.aggrid import AGGrid +from vizro.models._components.aggrid import AgGrid from vizro.models._components.button import Button from vizro.models._components.card import Card from vizro.models._components.container import Container @@ -8,4 +8,4 @@ from vizro.models._components.table import Table from vizro.models._components.tabs import Tabs -__all__ = ["AGGrid", "Button", "Card", "Container", "Graph", "Table", "Tabs"] +__all__ = ["AgGrid", "Button", "Card", "Container", "Graph", "Table", "Tabs"] diff --git a/vizro-core/src/vizro/models/_components/aggrid.py b/vizro-core/src/vizro/models/_components/aggrid.py index b936be70e..1f176195e 100644 --- a/vizro-core/src/vizro/models/_components/aggrid.py +++ b/vizro-core/src/vizro/models/_components/aggrid.py @@ -21,21 +21,21 @@ logger = logging.getLogger(__name__) -class AGGrid(VizroBaseModel): +class AgGrid(VizroBaseModel): """Wrapper for `dash-ag-grid.AgGrid` to visualize grids in dashboard. Args: - type (Literal["grid"]): Defaults to `"grid"`. - figure (CapturedCallable): AGGrid like object to be displayed. For more information see: + type (Literal["ag_grid"]): Defaults to `"ag_grid"`. + figure (CapturedCallable): AgGrid like object to be displayed. For more information see: [`dash-ag-grid.AgGrid`](https://dash.plotly.com/dash-ag-grid). title (str): Title of the table. Defaults to `""`. actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`. """ - type: Literal["aggrid"] = "aggrid" - figure: CapturedCallable = Field(..., import_path=vt, description="AGGrid to be visualized on dashboard") - title: str = Field("", description="Title of the AGGrid") + type: Literal["ag_grid"] = "ag_grid" + figure: CapturedCallable = Field(..., import_path=vt, description="AgGrid to be visualized on dashboard") + title: str = Field("", description="Title of the AgGrid") actions: List[Action] = [] _callable_object_id: str = PrivateAttr() @@ -60,7 +60,8 @@ def __getitem__(self, arg_name: str): return self.figure[arg_name] # Interaction methods - def _get_figure_interaction_input(self) -> Dict[str, State]: + @property + def _figure_interaction_input(self): """Required properties when using pre-defined `filter_interaction`.""" return { "cellClicked": State(component_id=self._callable_object_id, component_property="cellClicked"), @@ -98,8 +99,8 @@ def pre_build(self): if not hasattr(underlying_aggrid_object, "id"): raise ValueError( - "Underlying `AGGrid` callable has no attribute 'id'. To enable actions triggered by the `AGGrid`" - " a valid 'id' has to be provided to the `AGGrid` callable." + "Underlying `AgGrid` callable has no attribute 'id'. To enable actions triggered by the `AgGrid`" + " a valid 'id' has to be provided to the `AgGrid` callable." ) self._callable_object_id = underlying_aggrid_object.id diff --git a/vizro-core/src/vizro/models/_components/graph.py b/vizro-core/src/vizro/models/_components/graph.py index 05e958850..aceaedf70 100644 --- a/vizro-core/src/vizro/models/_components/graph.py +++ b/vizro-core/src/vizro/models/_components/graph.py @@ -76,7 +76,8 @@ def __getitem__(self, arg_name: str): return self.figure[arg_name] # Interaction methods - def _get_figure_interaction_input(self) -> Dict[str, State]: + @property + def _figure_interaction_input(self): """Required properties when using pre-defined `filter_interaction`.""" return { "clickData": State(component_id=self.id, component_property="clickData"), diff --git a/vizro-core/src/vizro/models/_components/table.py b/vizro-core/src/vizro/models/_components/table.py index 25ce2c5d2..3b233fb2b 100644 --- a/vizro-core/src/vizro/models/_components/table.py +++ b/vizro-core/src/vizro/models/_components/table.py @@ -61,7 +61,8 @@ def __getitem__(self, arg_name: str): return self.figure[arg_name] # Interaction methods - def _get_figure_interaction_input(self) -> Dict[str, State]: + @property + def _figure_interaction_input(self): """Required properties when using pre-defined `filter_interaction`.""" return { "active_cell": State(component_id=self._callable_object_id, component_property="active_cell"), diff --git a/vizro-core/src/vizro/models/types.py b/vizro-core/src/vizro/models/types.py index 77fcaafe2..8ffc9f9a4 100644 --- a/vizro-core/src/vizro/models/types.py +++ b/vizro-core/src/vizro/models/types.py @@ -217,7 +217,7 @@ class capture: This is used to add the functionality required to make graphs and actions work in a dashboard. Typically, it should be used as a function decorator. There are four possible modes: `"graph"`, `"table"`, - `"aggrid"` and `"action"`. + `"ag_grid"` and `"action"`. Examples >>> @capture("graph") @@ -226,8 +226,8 @@ class capture: >>> @capture("table") >>> def table_function(): >>> ... - >>> @capture("aggrid") - >>> def aggrid_function(): + >>> @capture("ag_grid") + >>> def ag_grid_function(): >>> ... >>> @capture("action") >>> def action_function(): @@ -242,8 +242,8 @@ class capture: """ - def __init__(self, mode: Literal["graph", "action", "table", "aggrid"]): - """Decorator to capture a function call. Valid modes are "graph", "table", "action" and "aggrid".""" + def __init__(self, mode: Literal["graph", "action", "table", "ag_grid"]): + """Decorator to capture a function call. Valid modes are "graph", "table", "action" and "ag_grid".""" self._mode = mode def __call__(self, func, /): @@ -297,7 +297,7 @@ def wrapped(*args, **kwargs): return CapturedCallable(func, *args, **kwargs) return wrapped - elif self._mode == "table": + elif self._mode in ["table", "ag_grid"]: @functools.wraps(func) def wrapped(*args, **kwargs): @@ -312,26 +312,10 @@ def wrapped(*args, **kwargs): raise ValueError(f"{func.__name__} must supply a value to data_frame argument.") from exc return captured_callable - return wrapped - elif self._mode == "aggrid": - - @functools.wraps(func) - def wrapped(*args, **kwargs): - if "data_frame" not in inspect.signature(func).parameters: - raise ValueError(f"{func.__name__} must have data_frame argument to use capture('aggrid').") - - captured_callable: CapturedCallable = CapturedCallable(func, *args, **kwargs) - - try: - captured_callable["data_frame"] - except KeyError as exc: - raise ValueError(f"{func.__name__} must supply a value to data_frame argument.") from exc - return captured_callable - return wrapped raise ValueError( "Valid modes of the capture decorator are @capture('graph'), @capture('action'), @capture('table') or " - "@capture('aggrid')." + "@capture('ag_grid')." ) @@ -374,7 +358,7 @@ class OptionsDictType(TypedDict): [`Parameter`][vizro.models.Parameter].""" ComponentType = Annotated[ - Union["AGGrid", "Button", "Card", "Container", "Graph", "Table", "Tabs"], + Union["AgGrid", "Button", "Card", "Container", "Graph", "Table", "Tabs"], Field( discriminator="type", description="Component that makes up part of the layout on the page.", @@ -382,7 +366,7 @@ class OptionsDictType(TypedDict): ] """Discriminated union. Type of component that makes up part of the layout on the page: [`Button`][vizro.models.Button], [`Card`][vizro.models.Card], [`Table`][vizro.models.Table], -[`Graph`][vizro.models.Graph] or [`AGGrid`][vizro.models.AGGrid].""" +[`Graph`][vizro.models.Graph] or [`AgGrid`][vizro.models.AgGrid].""" NavPagesType = Union[List[str], Dict[str, List[str]]] "List of page IDs or a mapping from name of a group to a list of page IDs (for hierarchical sub-navigation)." diff --git a/vizro-core/src/vizro/tables/dash_aggrid.py b/vizro-core/src/vizro/tables/dash_aggrid.py index 0256e2709..0c45c6df2 100644 --- a/vizro-core/src/vizro/tables/dash_aggrid.py +++ b/vizro-core/src/vizro/tables/dash_aggrid.py @@ -1,4 +1,4 @@ -"""Module containing the standard implementation of `dash-ag-grid.AgGrid`.""" +"""Module containing the standard implementation of `dash_ag_grid.AgGrid`.""" import dash_ag_grid as dag import pandas as pd @@ -46,14 +46,14 @@ } -@capture("aggrid") -def dash_ag_grid(data_frame=None, **kwargs): - """Implementation of `dash-ag-grid.AgGrid` with sensible defaults.""" +@capture("ag_grid") +def dash_ag_grid(data_frame, **kwargs): + """Implementation of `dash_ag_grid.AgGrid` with sensible defaults.""" defaults = { "columnDefs": [{"field": col} for col in data_frame.columns], "rowData": data_frame.apply( lambda x: ( - x.dt.strftime("%Y-%m-%d") # set date columns to `dateString` for AGGrid filtering to function + x.dt.strftime("%Y-%m-%d") # set date columns to `dateString` for AG Grid filtering to function if pd.api.types.is_datetime64_any_dtype(x) else x ) From dab8c8ec874bd507f7648651f10b8eabac5dcf78 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Thu, 22 Feb 2024 09:40:29 +0100 Subject: [PATCH 11/34] Add changelog --- ...aximilian_schulz_aggrid_separate_models.md | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 vizro-core/changelog.d/20240222_094020_maximilian_schulz_aggrid_separate_models.md diff --git a/vizro-core/changelog.d/20240222_094020_maximilian_schulz_aggrid_separate_models.md b/vizro-core/changelog.d/20240222_094020_maximilian_schulz_aggrid_separate_models.md new file mode 100644 index 000000000..f1f65e73c --- /dev/null +++ b/vizro-core/changelog.d/20240222_094020_maximilian_schulz_aggrid_separate_models.md @@ -0,0 +1,48 @@ + + + + + + + + + From dd7c3dfedba9205c3d48c13054fbda102f6d5b4e Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Thu, 22 Feb 2024 13:10:33 +0100 Subject: [PATCH 12/34] Turn off pagination --- vizro-core/src/vizro/tables/dash_aggrid.py | 1 - 1 file changed, 1 deletion(-) diff --git a/vizro-core/src/vizro/tables/dash_aggrid.py b/vizro-core/src/vizro/tables/dash_aggrid.py index 0c45c6df2..45af842af 100644 --- a/vizro-core/src/vizro/tables/dash_aggrid.py +++ b/vizro-core/src/vizro/tables/dash_aggrid.py @@ -71,7 +71,6 @@ def dash_ag_grid(data_frame, **kwargs): "dashGridOptions": { "dataTypeDefinitions": DATA_TYPE_DEFINITIONS, "animateRows": False, - "pagination": True, }, } kwargs = _set_defaults_nested(kwargs, defaults) From 842e68de1ddfca3090b5eec5bb810f906942d21c Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Thu, 22 Feb 2024 14:37:23 +0100 Subject: [PATCH 13/34] Replace datatable placeholder with aggrid --- vizro-core/src/vizro/models/_components/aggrid.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/vizro-core/src/vizro/models/_components/aggrid.py b/vizro-core/src/vizro/models/_components/aggrid.py index 1f176195e..d40f10757 100644 --- a/vizro-core/src/vizro/models/_components/aggrid.py +++ b/vizro-core/src/vizro/models/_components/aggrid.py @@ -1,8 +1,9 @@ import logging from typing import Dict, List, Literal +import dash_ag_grid as dag import pandas as pd -from dash import State, dash_table, dcc, html +from dash import State, dcc, html try: from pydantic.v1 import Field, PrivateAttr, validator @@ -110,9 +111,7 @@ def build(self): html.Div( [ html.H3(self.title, className="table-title") if self.title else None, - html.Div( - dash_table.DataTable(**({"id": self._callable_object_id} if self.actions else {})), id=self.id - ), + html.Div(dag.AgGrid(**({"id": self._callable_object_id} if self.actions else {})), id=self.id), ], className="table-container", id=f"{self.id}_outer", From 1ce60f6f789ade0bb627d7573bdb21d86410530a Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 10:41:38 +0100 Subject: [PATCH 14/34] PR comment on Errors --- vizro-core/src/vizro/actions/_actions_utils.py | 13 ++++++------- .../_callback_mapping/_callback_mapping_utils.py | 11 +++++++++-- vizro-core/src/vizro/models/_components/aggrid.py | 2 +- vizro-core/src/vizro/models/_components/graph.py | 2 +- vizro-core/src/vizro/models/_components/table.py | 2 +- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/vizro-core/src/vizro/actions/_actions_utils.py b/vizro-core/src/vizro/actions/_actions_utils.py index e330ddb41..8b6105321 100644 --- a/vizro-core/src/vizro/actions/_actions_utils.py +++ b/vizro-core/src/vizro/actions/_actions_utils.py @@ -87,13 +87,12 @@ def _apply_filter_interaction( data_frame: pd.DataFrame, ctds_filter_interaction: List[Dict[str, CallbackTriggerDict]], target: str ) -> pd.DataFrame: for ctd_filter_interaction in ctds_filter_interaction: - if "modelID" in ctd_filter_interaction: - triggered_model = model_manager[ctd_filter_interaction["modelID"]["id"]] - data_frame = triggered_model._filter_interaction( - data_frame=data_frame, - target=target, - ctd_filter_interaction=ctd_filter_interaction, - ) + triggered_model = model_manager[ctd_filter_interaction["modelID"]["id"]] + data_frame = triggered_model._filter_interaction( + data_frame=data_frame, + target=target, + ctd_filter_interaction=ctd_filter_interaction, + ) return data_frame diff --git a/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py b/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py index d28d45846..408f7280a 100644 --- a/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py +++ b/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py @@ -47,8 +47,15 @@ def _get_inputs_of_figure_interactions( inputs = [] for action in figure_interactions_on_page: triggered_model = model_manager._get_action_trigger(action_id=ModelID(str(action.id))) - if hasattr(triggered_model, "_figure_interaction_input"): - inputs.append(triggered_model._figure_interaction_input) + required_attributes = ["_filter_interaction_input", "_filter_interaction"] + for attribute in required_attributes: + if not hasattr(triggered_model, attribute): + raise ValueError(f"Model {triggered_model.id} does not have required attribute `{attribute}`.") + if "modelID" not in triggered_model._filter_interaction_input: + raise ValueError( + f"Model {triggered_model.id} does not have required State `modelID` in `_filter_interaction_input`." + ) + inputs.append(triggered_model._filter_interaction_input) return inputs diff --git a/vizro-core/src/vizro/models/_components/aggrid.py b/vizro-core/src/vizro/models/_components/aggrid.py index d40f10757..f811fe6af 100644 --- a/vizro-core/src/vizro/models/_components/aggrid.py +++ b/vizro-core/src/vizro/models/_components/aggrid.py @@ -62,7 +62,7 @@ def __getitem__(self, arg_name: str): # Interaction methods @property - def _figure_interaction_input(self): + def _filter_interaction_input(self): """Required properties when using pre-defined `filter_interaction`.""" return { "cellClicked": State(component_id=self._callable_object_id, component_property="cellClicked"), diff --git a/vizro-core/src/vizro/models/_components/graph.py b/vizro-core/src/vizro/models/_components/graph.py index aceaedf70..63ac4e2b8 100644 --- a/vizro-core/src/vizro/models/_components/graph.py +++ b/vizro-core/src/vizro/models/_components/graph.py @@ -77,7 +77,7 @@ def __getitem__(self, arg_name: str): # Interaction methods @property - def _figure_interaction_input(self): + def _filter_interaction_input(self): """Required properties when using pre-defined `filter_interaction`.""" return { "clickData": State(component_id=self.id, component_property="clickData"), diff --git a/vizro-core/src/vizro/models/_components/table.py b/vizro-core/src/vizro/models/_components/table.py index 3b233fb2b..4c23d6f8a 100644 --- a/vizro-core/src/vizro/models/_components/table.py +++ b/vizro-core/src/vizro/models/_components/table.py @@ -62,7 +62,7 @@ def __getitem__(self, arg_name: str): # Interaction methods @property - def _figure_interaction_input(self): + def _filter_interaction_input(self): """Required properties when using pre-defined `filter_interaction`.""" return { "active_cell": State(component_id=self._callable_object_id, component_property="active_cell"), From 574e0f1d840e815b21b6164d6bf05d75732c0591 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 12:09:28 +0100 Subject: [PATCH 15/34] Fix unit tests after model refactoring --- .../test_get_action_callback_mapping.py | 6 ++++-- .../unit/vizro/actions/test_export_data_action.py | 10 ++++++++-- .../vizro/actions/test_filter_interaction_action.py | 8 +++++++- .../tests/unit/vizro/models/test_models_utils.py | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/vizro-core/tests/unit/vizro/actions/_callback_mapping/test_get_action_callback_mapping.py b/vizro-core/tests/unit/vizro/actions/_callback_mapping/test_get_action_callback_mapping.py index f19c5d2a6..68e2fc542 100644 --- a/vizro-core/tests/unit/vizro/actions/_callback_mapping/test_get_action_callback_mapping.py +++ b/vizro-core/tests/unit/vizro/actions/_callback_mapping/test_get_action_callback_mapping.py @@ -135,10 +135,11 @@ def action_callback_inputs_expected(): dash.State("parameter_table_row_selectable", "value"), ], "filter_interaction": [ - {"clickData": dash.State("scatter_chart", "clickData")}, + {"clickData": dash.State("scatter_chart", "clickData"), "modelID": dash.State("scatter_chart", "id")}, { "active_cell": dash.State("underlying_table_id", "active_cell"), "derived_viewport_data": dash.State("underlying_table_id", "derived_viewport_data"), + "modelID": dash.State("vizro_table", "id"), }, ], "theme_selector": dash.State("theme_selector", "checked"), @@ -162,10 +163,11 @@ def export_data_inputs_expected(): ], "parameters": [], "filter_interaction": [ - {"clickData": dash.State("scatter_chart", "clickData")}, + {"clickData": dash.State("scatter_chart", "clickData"), "modelID": dash.State("scatter_chart", "id")}, { "active_cell": dash.State("underlying_table_id", "active_cell"), "derived_viewport_data": dash.State("underlying_table_id", "derived_viewport_data"), + "modelID": dash.State("vizro_table", "id"), }, ], "theme_selector": [], diff --git a/vizro-core/tests/unit/vizro/actions/test_export_data_action.py b/vizro-core/tests/unit/vizro/actions/test_export_data_action.py index 3101a4a55..773b8d4d9 100644 --- a/vizro-core/tests/unit/vizro/actions/test_export_data_action.py +++ b/vizro-core/tests/unit/vizro/actions/test_export_data_action.py @@ -51,8 +51,11 @@ def ctx_export_data(request): value={"points": [{"customdata": [continent_filter_interaction]}]}, str_id="box_chart", triggered=False, - ) - } + ), + "modelID": CallbackTriggerDict( + id="box_chart", property="id", value="box_chart", str_id="box_chart", triggered=False + ), + }, ) if country_table_filter_interaction: args_grouping_filter_interaction.append( @@ -74,6 +77,9 @@ def ctx_export_data(request): str_id="underlying_table_id", triggered=False, ), + "modelID": CallbackTriggerDict( + id="vizro_table", property="id", value="vizro_table", str_id="vizro_table", triggered=False + ), } ) mock_ctx = { diff --git a/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py b/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py index 595ac4d01..78b649596 100644 --- a/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py +++ b/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py @@ -23,7 +23,10 @@ def ctx_filter_interaction(request): value={"points": [{"customdata": [continent_filter_interaction]}]}, str_id="box_chart", triggered=False, - ) + ), + "modelID": CallbackTriggerDict( + id="box_chart", property="id", value="box_chart", str_id="box_chart", triggered=False + ), } ) if country_table_filter_interaction: @@ -46,6 +49,9 @@ def ctx_filter_interaction(request): str_id="underlying_table_id", triggered=False, ), + "modelID": CallbackTriggerDict( + id="vizro_table", property="id", value="vizro_table", str_id="vizro_table", triggered=False + ), } ) diff --git a/vizro-core/tests/unit/vizro/models/test_models_utils.py b/vizro-core/tests/unit/vizro/models/test_models_utils.py index f4e1310ea..395d99b9d 100644 --- a/vizro-core/tests/unit/vizro/models/test_models_utils.py +++ b/vizro-core/tests/unit/vizro/models/test_models_utils.py @@ -18,6 +18,6 @@ def test_set_components_validator(self, model_with_layout): def test_check_for_valid_component_types(self, model_with_layout): with pytest.raises( ValidationError, - match=re.escape("(allowed values: 'button', 'card', 'container', 'graph', 'table', 'tabs')"), + match=re.escape("(allowed values: 'ag_grid', 'button', 'card', 'container', 'graph', 'table', 'tabs')"), ): model_with_layout(title="Page Title", components=[vm.Checklist()]) From c019521380e81003bca08af78ed03f5e453b1445 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 12:10:28 +0100 Subject: [PATCH 16/34] Schema --- vizro-core/schemas/0.1.12.dev0.json | 44 ++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/vizro-core/schemas/0.1.12.dev0.json b/vizro-core/schemas/0.1.12.dev0.json index 68ea6e0ea..7057167e6 100644 --- a/vizro-core/schemas/0.1.12.dev0.json +++ b/vizro-core/schemas/0.1.12.dev0.json @@ -72,6 +72,40 @@ }, "additionalProperties": false }, + "AgGrid": { + "title": "AgGrid", + "description": "Wrapper for `dash-ag-grid.AgGrid` to visualize grids in dashboard.\n\nArgs:\n type (Literal[\"ag_grid\"]): Defaults to `\"ag_grid\"`.\n figure (CapturedCallable): AgGrid like object to be displayed. For more information see:\n [`dash-ag-grid.AgGrid`](https://dash.plotly.com/dash-ag-grid).\n title (str): Title of the table. Defaults to `\"\"`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "default": "", + "type": "string" + }, + "type": { + "title": "Type", + "default": "ag_grid", + "enum": ["ag_grid"], + "type": "string" + }, + "title": { + "title": "Title", + "description": "Title of the AgGrid", + "default": "", + "type": "string" + }, + "actions": { + "title": "Actions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + }, + "additionalProperties": false + }, "Button": { "title": "Button", "description": "Component provided to `Page` to trigger any defined `action` in `Page`.\n\nArgs:\n type (Literal[\"button\"]): Defaults to `\"button\"`.\n text (str): Text to be displayed on button. Defaults to `\"Click me!\"`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", @@ -168,7 +202,7 @@ }, "Table": { "title": "Table", - "description": "Wrapper for table components to visualize in dashboard.\n\nArgs:\n type (Literal[\"table\"]): Defaults to `\"table\"`.\n figure (CapturedCallable): Table like object to be displayed. Current choices include:\n [`dash_table.DataTable`](https://dash.plotly.com/datatable).\n title (str): Title of the table. Defaults to `\"\"`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", + "description": "Wrapper for `dash_table.DataTable` to visualize tables in dashboard.\n\nArgs:\n type (Literal[\"table\"]): Defaults to `\"table\"`.\n figure (CapturedCallable): Table like object to be displayed. For more information see:\n [`dash_table.DataTable`](https://dash.plotly.com/datatable).\n title (str): Title of the table. Defaults to `\"\"`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", "type": "object", "properties": { "id": { @@ -306,6 +340,7 @@ "discriminator": { "propertyName": "type", "mapping": { + "ag_grid": "#/definitions/AgGrid", "button": "#/definitions/Button", "card": "#/definitions/Card", "container": "#/definitions/Container", @@ -315,6 +350,9 @@ } }, "oneOf": [ + { + "$ref": "#/definitions/AgGrid" + }, { "$ref": "#/definitions/Button" }, @@ -969,6 +1007,7 @@ "discriminator": { "propertyName": "type", "mapping": { + "ag_grid": "#/definitions/AgGrid", "button": "#/definitions/Button", "card": "#/definitions/Card", "container": "#/definitions/Container", @@ -978,6 +1017,9 @@ } }, "oneOf": [ + { + "$ref": "#/definitions/AgGrid" + }, { "$ref": "#/definitions/Button" }, From 1d40cf3295be75cbe6f419bacd6575a0fa2621fe Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 12:19:07 +0100 Subject: [PATCH 17/34] Update requirements --- vizro-core/snyk/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/vizro-core/snyk/requirements.txt b/vizro-core/snyk/requirements.txt index 240498883..9ff9de875 100644 --- a/vizro-core/snyk/requirements.txt +++ b/vizro-core/snyk/requirements.txt @@ -1,5 +1,6 @@ dash>=2.14.1 dash_bootstrap_components +dash-ag-grid>=31.0.0 pandas pydantic>=1.10.13 dash_mantine_components From 56e9f252f264db087ef5d404a8915cedd5e7cc0c Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 13:36:02 +0100 Subject: [PATCH 18/34] Fix tests and take over most tests from Table --- vizro-core/tests/unit/vizro/conftest.py | 12 +- .../unit/vizro/models/_components/conftest.py | 10 ++ .../vizro/models/_components/test_aggrid.py | 147 ++++++++++++++++++ .../vizro/models/_components/test_table.py | 6 - 4 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 vizro-core/tests/unit/vizro/models/_components/conftest.py create mode 100644 vizro-core/tests/unit/vizro/models/_components/test_aggrid.py diff --git a/vizro-core/tests/unit/vizro/conftest.py b/vizro-core/tests/unit/vizro/conftest.py index 6ba8dae5b..72c215dc0 100644 --- a/vizro-core/tests/unit/vizro/conftest.py +++ b/vizro-core/tests/unit/vizro/conftest.py @@ -5,7 +5,7 @@ import vizro.models as vm import vizro.plotly.express as px from vizro import Vizro -from vizro.tables import dash_data_table +from vizro.tables import dash_ag_grid, dash_data_table @pytest.fixture @@ -26,6 +26,16 @@ def standard_px_chart(gapminder): ) +@pytest.fixture +def standard_ag_grid(gapminder): + return dash_ag_grid(data_frame=gapminder) + + +@pytest.fixture +def ag_grid_with_id(gapminder): + return dash_ag_grid(id="underlying_ag_grid_id", data_frame=gapminder) + + @pytest.fixture def standard_dash_table(gapminder): return dash_data_table(data_frame=gapminder) diff --git a/vizro-core/tests/unit/vizro/models/_components/conftest.py b/vizro-core/tests/unit/vizro/models/_components/conftest.py new file mode 100644 index 000000000..97939f484 --- /dev/null +++ b/vizro-core/tests/unit/vizro/models/_components/conftest.py @@ -0,0 +1,10 @@ +"""Fixtures to be shared across several tests.""" + +import pytest +import vizro.models as vm +from vizro.actions import filter_interaction + + +@pytest.fixture +def filter_interaction_action(): + return vm.Action(function=filter_interaction()) diff --git a/vizro-core/tests/unit/vizro/models/_components/test_aggrid.py b/vizro-core/tests/unit/vizro/models/_components/test_aggrid.py new file mode 100644 index 000000000..23e1d095f --- /dev/null +++ b/vizro-core/tests/unit/vizro/models/_components/test_aggrid.py @@ -0,0 +1,147 @@ +"""Unit tests for vizro.models.AgGrid.""" + +import dash_ag_grid as dag +import pytest +from asserts import assert_component_equal +from dash import dcc, html + +try: + from pydantic.v1 import ValidationError +except ImportError: # pragma: no cov + from pydantic import ValidationError + +import vizro.models as vm +import vizro.plotly.express as px +from vizro.managers import data_manager +from vizro.models._action._action import Action +from vizro.tables import dash_ag_grid + + +@pytest.fixture +def dash_ag_grid_with_arguments(): + return dash_ag_grid(data_frame=px.data.gapminder(), defaultColDef={"resizable": False, "sortable": False}) + + +@pytest.fixture +def dash_ag_grid_with_str_dataframe(): + return dash_ag_grid(data_frame="gapminder") + + +class TestDunderMethodsAgGrid: + def test_create_graph_mandatory_only(self, standard_ag_grid): + ag_grid = vm.AgGrid(figure=standard_ag_grid) + + assert hasattr(ag_grid, "id") + assert ag_grid.type == "ag_grid" + assert ag_grid.figure == standard_ag_grid + assert ag_grid.actions == [] + + @pytest.mark.parametrize("id", ["id_1", "id_2"]) + def test_create_ag_grid_mandatory_and_optional(self, standard_ag_grid, id): + ag_grid = vm.AgGrid(figure=standard_ag_grid, id=id, actions=[]) + + assert ag_grid.id == id + assert ag_grid.type == "ag_grid" + assert ag_grid.figure == standard_ag_grid + + def test_mandatory_figure_missing(self): + with pytest.raises(ValidationError, match="field required"): + vm.AgGrid() + + def test_failed_ag_grid_with_no_captured_callable(self, standard_go_chart): + with pytest.raises(ValidationError, match="must provide a valid CapturedCallable object"): + vm.AgGrid(figure=standard_go_chart) + + @pytest.mark.xfail(reason="This test is failing as we are not yet detecting different types of captured callables") + def test_failed_ag_grid_with_wrong_captured_callable(self, standard_px_chart): + with pytest.raises(ValidationError, match="must provide a valid ag_grid function vm.AgGrid"): + vm.AgGrid(figure=standard_px_chart) + + def test_getitem_known_args(self, dash_ag_grid_with_arguments): + ag_grid = vm.AgGrid(figure=dash_ag_grid_with_arguments) + assert ag_grid["defaultColDef"] == {"resizable": False, "sortable": False} + assert ag_grid["type"] == "ag_grid" + + def test_getitem_unknown_args(self, standard_ag_grid): + ag_grid = vm.AgGrid(figure=standard_ag_grid) + with pytest.raises(KeyError): + ag_grid["unknown_args"] + + def test_set_action_via_validator(self, standard_ag_grid, identity_action_function): + ag_grid = vm.AgGrid(figure=standard_ag_grid, actions=[Action(function=identity_action_function())]) + actions_chain = ag_grid.actions[0] + assert actions_chain.trigger.component_property == "cellClicked" + + +class TestProcessAgGridDataFrame: + def test_process_figure_data_frame_str_df(self, dash_ag_grid_with_str_dataframe, gapminder): + data_manager["gapminder"] = gapminder + ag_grid_with_str_df = vm.AgGrid(id="ag_grid", figure=dash_ag_grid_with_str_dataframe) + assert data_manager._get_component_data("ag_grid").equals(gapminder) + assert ag_grid_with_str_df["data_frame"] == "gapminder" + + def test_process_figure_data_frame_df(self, standard_ag_grid, gapminder): + ag_grid_with_str_df = vm.AgGrid(id="ag_grid", figure=standard_ag_grid) + assert data_manager._get_component_data("ag_grid").equals(gapminder) + with pytest.raises(KeyError, match="'data_frame'"): + ag_grid_with_str_df.figure["data_frame"] + + +class TestPreBuildAgGrid: + def test_pre_build_no_actions_no_underlying_ag_grid_id(self, standard_ag_grid): + ag_grid = vm.AgGrid(id="text_ag_grid", figure=standard_ag_grid) + ag_grid.pre_build() + + assert hasattr(ag_grid, "_callable_object_id") is False + + def test_pre_build_actions_no_underlying_ag_grid_id_exception(self, standard_ag_grid, filter_interaction_action): + ag_grid = vm.AgGrid(id="text_ag_grid", figure=standard_ag_grid, actions=[filter_interaction_action]) + with pytest.raises(ValueError, match="Underlying `AgGrid` callable has no attribute 'id'"): + ag_grid.pre_build() + + def test_pre_build_actions_underlying_ag_grid_id(self, ag_grid_with_id, filter_interaction_action): + ag_grid = vm.AgGrid(id="text_ag_grid", figure=ag_grid_with_id, actions=[filter_interaction_action]) + ag_grid.pre_build() + + assert ag_grid._callable_object_id == "underlying_ag_grid_id" + + +class TestBuildAgGrid: + def test_ag_grid_build_mandatory_only(self, standard_ag_grid): + ag_grid = vm.AgGrid(id="text_ag_grid", figure=standard_ag_grid) + ag_grid.pre_build() + ag_grid = ag_grid.build() + expected_ag_grid = dcc.Loading( + html.Div( + [ + None, + html.Div(dag.AgGrid(), id="text_ag_grid"), + ], + className="table-container", + id="text_ag_grid_outer", + ), + color="grey", + parent_className="loading-container", + ) + + assert_component_equal(ag_grid, expected_ag_grid) + + def test_ag_grid_build_with_underlying_id(self, ag_grid_with_id, filter_interaction_action): + ag_grid = vm.AgGrid(id="text_ag_grid", figure=ag_grid_with_id, actions=[filter_interaction_action]) + ag_grid.pre_build() + ag_grid = ag_grid.build() + + expected_ag_grid = dcc.Loading( + html.Div( + [ + None, + html.Div(dag.AgGrid(id="underlying_ag_grid_id"), id="text_ag_grid"), + ], + className="table-container", + id="text_ag_grid_outer", + ), + color="grey", + parent_className="loading-container", + ) + + assert_component_equal(ag_grid, expected_ag_grid) 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 2a7d431db..cae963fc4 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_table.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_table.py @@ -11,7 +11,6 @@ import vizro.models as vm import vizro.plotly.express as px -from vizro.actions import filter_interaction from vizro.managers import data_manager from vizro.models._action._action import Action from vizro.tables import dash_data_table @@ -27,11 +26,6 @@ def dash_table_with_str_dataframe(): return dash_data_table(data_frame="gapminder") -@pytest.fixture -def filter_interaction_action(): - return vm.Action(function=filter_interaction()) - - class TestDunderMethodsTable: def test_create_graph_mandatory_only(self, standard_dash_table): table = vm.Table(figure=standard_dash_table) From 3d9abc80b0654f092f07f3cf10c6fa4047a71a83 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 13:55:03 +0100 Subject: [PATCH 19/34] Add attribute tests for Graph, Table and AgGrid --- .../tests/unit/vizro/models/_components/test_aggrid.py | 9 +++++++++ .../tests/unit/vizro/models/_components/test_graph.py | 8 ++++++++ .../tests/unit/vizro/models/_components/test_table.py | 9 +++++++++ 3 files changed, 26 insertions(+) diff --git a/vizro-core/tests/unit/vizro/models/_components/test_aggrid.py b/vizro-core/tests/unit/vizro/models/_components/test_aggrid.py index 23e1d095f..215235e70 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_aggrid.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_aggrid.py @@ -73,6 +73,15 @@ def test_set_action_via_validator(self, standard_ag_grid, identity_action_functi assert actions_chain.trigger.component_property == "cellClicked" +class TestAttributesAgGrid: + def test_ag_grid_filter_interaction_attributes(self, ag_grid_with_id): + ag_grid = vm.AgGrid(figure=ag_grid_with_id, title="Gapminder", actions=[]) + assert hasattr(ag_grid, "_filter_interaction") + ag_grid.pre_build() + assert hasattr(ag_grid, "_filter_interaction_input") + assert "modelID" in ag_grid._filter_interaction_input + + class TestProcessAgGridDataFrame: def test_process_figure_data_frame_str_df(self, dash_ag_grid_with_str_dataframe, gapminder): data_manager["gapminder"] = gapminder diff --git a/vizro-core/tests/unit/vizro/models/_components/test_graph.py b/vizro-core/tests/unit/vizro/models/_components/test_graph.py index 438bc79f6..1ecf66e1f 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_graph.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_graph.py @@ -107,6 +107,14 @@ def test_set_action_via_validator(self, standard_px_chart, identity_action_funct assert actions_chain.trigger.component_property == "clickData" +class TestAttributesGraph: + def test_graph_filter_interaction_attributes(self, standard_px_chart): + graph = vm.Graph(figure=standard_px_chart) + assert hasattr(graph, "_filter_interaction") + assert hasattr(graph, "_filter_interaction_input") + assert "modelID" in graph._filter_interaction_input + + class TestProcessFigureDataFrame: def test_process_figure_data_frame_str_df(self, standard_px_chart_with_str_dataframe, gapminder): data_manager["gapminder"] = gapminder 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 cae963fc4..6fa03ea88 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_table.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_table.py @@ -72,6 +72,15 @@ def test_set_action_via_validator(self, standard_dash_table, identity_action_fun assert actions_chain.trigger.component_property == "active_cell" +class TestAttributesTable: + def test_table_filter_interaction_attributes(self, dash_data_table_with_id): + table = vm.Table(figure=dash_data_table_with_id, title="Gapminder", actions=[]) + assert hasattr(table, "_filter_interaction") + table.pre_build() + assert hasattr(table, "_filter_interaction_input") + assert "modelID" in table._filter_interaction_input + + class TestProcessTableDataFrame: def test_process_figure_data_frame_str_df(self, dash_table_with_str_dataframe, gapminder): data_manager["gapminder"] = gapminder From 9c1751ffd6cc708d38df05125c535a6773a774ad Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 14:38:22 +0100 Subject: [PATCH 20/34] Introduce tests for dash_ag_grid function --- .../unit/vizro/tables/test_dash_ag_grid.py | 24 +++++++++++++++++++ .../unit/vizro/tables/test_dash_table.py | 21 ---------------- .../tests/unit/vizro/tables/test_utils.py | 21 ++++++++++++++++ 3 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 vizro-core/tests/unit/vizro/tables/test_dash_ag_grid.py create mode 100644 vizro-core/tests/unit/vizro/tables/test_utils.py 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 new file mode 100644 index 000000000..f00b9725b --- /dev/null +++ b/vizro-core/tests/unit/vizro/tables/test_dash_ag_grid.py @@ -0,0 +1,24 @@ +import pandas as pd +from vizro.tables import dash_ag_grid + +data = pd.DataFrame( + { + "cat": ["a", "b", "c"], + "int": [4, 5, 6], + "float": [7.3, 8.2, 9.1], + "date": pd.to_datetime(["2021/01/01", "2021/01/02", "2021/01/03"]), + } +) + + +class TestDashAgGrid: + def test_dash_ag_grid(self): + grid = dash_ag_grid(data_frame=data)() + assert grid.columnDefs == [{"field": "cat"}, {"field": "int"}, {"field": "float"}, {"field": "date"}] + assert grid.rowData == [ + {"cat": "a", "int": 4, "float": 7.3, "date": "2021-01-01"}, + {"cat": "b", "int": 5, "float": 8.2, "date": "2021-01-02"}, + {"cat": "c", "int": 6, "float": 9.1, "date": "2021-01-03"}, + ] + # we could test other properties such as defaultColDef, + # but this would just test our chosen defaults, and no functionality really 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 4b0be62e1..e69de29bb 100644 --- a/vizro-core/tests/unit/vizro/tables/test_dash_table.py +++ b/vizro-core/tests/unit/vizro/tables/test_dash_table.py @@ -1,21 +0,0 @@ -import pytest -from vizro.tables.dash_table import _set_defaults_nested - - -@pytest.fixture -def default_dictionary(): - return {"a": {"b": {"c": 1, "d": 2}}, "e": 3} - - -@pytest.mark.parametrize( - "input, expected", - [ - ({}, {"a": {"b": {"c": 1, "d": 2}}, "e": 3}), # nothing supplied - ({"e": 10}, {"a": {"b": {"c": 1, "d": 2}}, "e": 10}), # flat main key - ({"a": {"b": {"c": 11, "d": 12}}}, {"a": {"b": {"c": 11, "d": 12}}, "e": 3}), # updated multiple nested keys - ({"a": {"b": {"c": 1, "d": {"f": 42}}}}, {"a": {"b": {"c": 1, "d": {"f": 42}}}, "e": 3}), # add new dict - ({"a": {"b": {"c": 5}}}, {"a": {"b": {"c": 5, "d": 2}}, "e": 3}), # arbitrary nesting - ], -) -def test_set_defaults_nested(default_dictionary, input, expected): - assert _set_defaults_nested(input, default_dictionary) == expected diff --git a/vizro-core/tests/unit/vizro/tables/test_utils.py b/vizro-core/tests/unit/vizro/tables/test_utils.py new file mode 100644 index 000000000..cf06e5ef2 --- /dev/null +++ b/vizro-core/tests/unit/vizro/tables/test_utils.py @@ -0,0 +1,21 @@ +import pytest +from vizro.tables._utils import _set_defaults_nested + + +@pytest.fixture +def default_dictionary(): + return {"a": {"b": {"c": 1, "d": 2}}, "e": 3} + + +@pytest.mark.parametrize( + "input, expected", + [ + ({}, {"a": {"b": {"c": 1, "d": 2}}, "e": 3}), # nothing supplied + ({"e": 10}, {"a": {"b": {"c": 1, "d": 2}}, "e": 10}), # flat main key + ({"a": {"b": {"c": 11, "d": 12}}}, {"a": {"b": {"c": 11, "d": 12}}, "e": 3}), # updated multiple nested keys + ({"a": {"b": {"c": 1, "d": {"f": 42}}}}, {"a": {"b": {"c": 1, "d": {"f": 42}}}, "e": 3}), # add new dict + ({"a": {"b": {"c": 5}}}, {"a": {"b": {"c": 5, "d": 2}}, "e": 3}), # arbitrary nesting + ], +) +def test_set_defaults_nested(default_dictionary, input, expected): + assert _set_defaults_nested(input, default_dictionary) == expected From f427e27cabf9e3a4bce6710187e94f8967ce1ce1 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 15:36:36 +0100 Subject: [PATCH 21/34] Add test for filter interaction of ag_grid --- .../tests/unit/vizro/actions/conftest.py | 5 +- .../vizro/actions/test_export_data_action.py | 2 +- .../actions/test_filter_interaction_action.py | 97 ++++++++++++++----- 3 files changed, 77 insertions(+), 27 deletions(-) diff --git a/vizro-core/tests/unit/vizro/actions/conftest.py b/vizro-core/tests/unit/vizro/actions/conftest.py index 8c8549a39..f34fec6d8 100644 --- a/vizro-core/tests/unit/vizro/actions/conftest.py +++ b/vizro-core/tests/unit/vizro/actions/conftest.py @@ -59,7 +59,9 @@ def managers_one_page_two_graphs_one_button(box_chart, scatter_chart): @pytest.fixture -def managers_one_page_two_graphs_one_table_one_button(box_chart, scatter_chart, dash_data_table_with_id): +def managers_one_page_two_graphs_one_table_one_aggrid_one_button( + box_chart, scatter_chart, dash_data_table_with_id, ag_grid_with_id +): """Instantiates a simple model_manager and data_manager with a page, two graph models and the button component.""" vm.Page( id="test_page", @@ -68,6 +70,7 @@ def managers_one_page_two_graphs_one_table_one_button(box_chart, scatter_chart, vm.Graph(id="box_chart", figure=box_chart), vm.Graph(id="scatter_chart", figure=scatter_chart), vm.Table(id="vizro_table", figure=dash_data_table_with_id), + vm.AgGrid(id="ag_grid", figure=ag_grid_with_id), vm.Button(id="button"), ], ) diff --git a/vizro-core/tests/unit/vizro/actions/test_export_data_action.py b/vizro-core/tests/unit/vizro/actions/test_export_data_action.py index 773b8d4d9..f3a1f5585 100644 --- a/vizro-core/tests/unit/vizro/actions/test_export_data_action.py +++ b/vizro-core/tests/unit/vizro/actions/test_export_data_action.py @@ -291,7 +291,7 @@ def test_multiple_targets_with_filter_and_filter_interaction( assert result == expected - @pytest.mark.usefixtures("managers_one_page_two_graphs_one_table_one_button") + @pytest.mark.usefixtures("managers_one_page_two_graphs_one_table_one_aggrid_one_button") @pytest.mark.parametrize( "ctx_export_data, target_scatter_filter_and_filter_interaction, target_box_filtered_pop", [ diff --git a/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py b/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py index 78b649596..a10b734ea 100644 --- a/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py +++ b/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py @@ -11,7 +11,7 @@ @pytest.fixture def ctx_filter_interaction(request): """Mock dash.ctx that represents a click on a continent data-point and table selected cell.""" - continent_filter_interaction, country_table_filter_interaction = request.param + continent_filter_interaction, country_table_filter_interaction, country_aggrid_filter_interaction = request.param args_grouping_filter_interaction = [] if continent_filter_interaction: @@ -54,7 +54,27 @@ def ctx_filter_interaction(request): ), } ) - + if country_aggrid_filter_interaction: + args_grouping_filter_interaction.append( + { + "cellClicked": CallbackTriggerDict( + id="underlying_ag_grid_id", + property="cellClicked", + value={ + "value": country_aggrid_filter_interaction, + "colId": "country", + "rowIndex": 0, + "rowId": "0", + "timestamp": 1708697920849, + }, + str_id="underlying_ag_grid_id", + triggered=False, + ), + "modelID": CallbackTriggerDict( + id="ag_grid", property="id", value="ag_grid", str_id="ag_grid", triggered=False + ), + } + ) mock_ctx = { "args_grouping": { "external": { @@ -97,9 +117,9 @@ def target_box_filtered_continent(request, gapminder_2007, box_params): return px.box(data, **box_params).update_layout(margin_t=24) -@pytest.mark.usefixtures("managers_one_page_two_graphs_one_table_one_button") +@pytest.mark.usefixtures("managers_one_page_two_graphs_one_table_one_aggrid_one_button") class TestFilterInteraction: - @pytest.mark.parametrize("ctx_filter_interaction", [("Africa", None), ("Europe", None)], indirect=True) + @pytest.mark.parametrize("ctx_filter_interaction", [("Africa", None, None), ("Europe", None, None)], indirect=True) def test_filter_interaction_without_targets_temporary_behavior( # temporary fix, see below test self, ctx_filter_interaction ): @@ -116,9 +136,9 @@ def test_filter_interaction_without_targets_temporary_behavior( # temporary fix @pytest.mark.parametrize( "ctx_filter_interaction,target_scatter_filtered_continent,target_box_filtered_continent", [ - (("Africa", None), ("Africa", None), ("Africa", None)), - (("Europe", None), ("Europe", None), ("Europe", None)), - (("Americas", None), ("Americas", None), ("Americas", None)), + (("Africa", None, None), ("Africa", None), ("Africa", None)), + (("Europe", None, None), ("Europe", None), ("Europe", None)), + (("Americas", None, None), ("Americas", None), ("Americas", None)), ], indirect=True, ) @@ -137,9 +157,9 @@ def test_filter_interaction_without_targets_desired_behavior( @pytest.mark.parametrize( "ctx_filter_interaction,target_scatter_filtered_continent", [ - (("Africa", None), ("Africa", None)), - (("Europe", None), ("Europe", None)), - (("Americas", None), ("Americas", None)), + (("Africa", None, None), ("Africa", None)), + (("Europe", None, None), ("Europe", None)), + (("Americas", None, None), ("Americas", None)), ], indirect=True, ) @@ -158,9 +178,9 @@ def test_filter_interaction_with_one_target(self, ctx_filter_interaction, target @pytest.mark.parametrize( "ctx_filter_interaction,target_scatter_filtered_continent,target_box_filtered_continent", [ - (("Africa", None), ("Africa", None), ("Africa", None)), - (("Europe", None), ("Europe", None), ("Europe", None)), - (("Americas", None), ("Americas", None), ("Americas", None)), + (("Africa", None, None), ("Africa", None), ("Africa", None)), + (("Europe", None, None), ("Europe", None), ("Europe", None)), + (("Americas", None, None), ("Americas", None), ("Americas", None)), ], indirect=True, ) @@ -180,7 +200,7 @@ def test_filter_interaction_with_two_target( @pytest.mark.xfail # This (or similar code) should raise a Value/Validation error explaining next steps @pytest.mark.parametrize("target", ["scatter_chart", ["scatter_chart"]]) - @pytest.mark.parametrize("ctx_filter_interaction", [("Africa", None), ("Europe", None)], indirect=True) + @pytest.mark.parametrize("ctx_filter_interaction", [("Africa", None, None), ("Europe", None, None)], indirect=True) def test_filter_interaction_with_invalid_targets(self, target, ctx_filter_interaction): with pytest.raises(ValueError, match="Target invalid_target not found in model_manager."): # Add action to relevant component - here component[0] is the source_chart @@ -191,9 +211,9 @@ def test_filter_interaction_with_invalid_targets(self, target, ctx_filter_intera @pytest.mark.parametrize( "ctx_filter_interaction,target_scatter_filtered_continent", [ - ((None, "Algeria"), (None, "Algeria")), - ((None, "Albania"), (None, "Albania")), - ((None, "Argentina"), (None, "Argentina")), + ((None, "Algeria", None), (None, "Algeria")), + ((None, "Albania", None), (None, "Albania")), + ((None, "Argentina", None), (None, "Argentina")), ], indirect=True, ) @@ -214,9 +234,9 @@ def test_table_filter_interaction_with_one_target(self, ctx_filter_interaction, @pytest.mark.parametrize( "ctx_filter_interaction, target_scatter_filtered_continent, target_box_filtered_continent", [ - ((None, "Algeria"), (None, "Algeria"), (None, "Algeria")), - ((None, "Albania"), (None, "Albania"), (None, "Albania")), - ((None, "Argentina"), (None, "Argentina"), (None, "Argentina")), + ((None, "Algeria", None), (None, "Algeria"), (None, "Algeria")), + ((None, "Albania", None), (None, "Albania"), (None, "Albania")), + ((None, "Argentina", None), (None, "Argentina"), (None, "Argentina")), ], indirect=True, ) @@ -224,11 +244,11 @@ def test_table_filter_interaction_with_two_targets( self, ctx_filter_interaction, target_scatter_filtered_continent, target_box_filtered_continent ): model_manager["box_chart"].actions = [ - vm.Action(id="test_action", function=filter_interaction(targets=["scatter_chart", "box_chart"])) + vm.Action(function=filter_interaction(targets=["scatter_chart", "box_chart"])) ] model_manager["vizro_table"].actions = [ - vm.Action(function=filter_interaction(targets=["scatter_chart", "box_chart"])) + vm.Action(id="test_action", function=filter_interaction(targets=["scatter_chart", "box_chart"])) ] model_manager["vizro_table"].pre_build() @@ -241,9 +261,36 @@ def test_table_filter_interaction_with_two_targets( @pytest.mark.parametrize( "ctx_filter_interaction, target_scatter_filtered_continent, target_box_filtered_continent", [ - (("Africa", "Algeria"), ("Africa", "Algeria"), ("Africa", "Algeria")), - (("Europe", "Albania"), ("Europe", "Albania"), ("Europe", "Albania")), - (("Americas", "Argentina"), ("Americas", "Argentina"), ("Americas", "Argentina")), + ((None, None, "Algeria"), (None, "Algeria"), (None, "Algeria")), + ((None, None, "Albania"), (None, "Albania"), (None, "Albania")), + ((None, None, "Argentina"), (None, "Argentina"), (None, "Argentina")), + ], + indirect=True, + ) + def test_aggrid_filter_interaction_with_two_targets( + self, ctx_filter_interaction, target_scatter_filtered_continent, target_box_filtered_continent + ): + model_manager["box_chart"].actions = [ + vm.Action(function=filter_interaction(targets=["scatter_chart", "box_chart"])) + ] + + model_manager["ag_grid"].actions = [ + vm.Action(id="test_action", function=filter_interaction(targets=["scatter_chart", "box_chart"])) + ] + model_manager["ag_grid"].pre_build() + + # Run action by picking the above added action function and executing it with () + result = model_manager["test_action"].function() + expected = {"scatter_chart": target_scatter_filtered_continent, "box_chart": target_box_filtered_continent} + + assert result == expected + + @pytest.mark.parametrize( + "ctx_filter_interaction, target_scatter_filtered_continent, target_box_filtered_continent", + [ + (("Africa", "Algeria", None), ("Africa", "Algeria"), ("Africa", "Algeria")), + (("Europe", "Albania", None), ("Europe", "Albania"), ("Europe", "Albania")), + (("Americas", "Argentina", None), ("Americas", "Argentina"), ("Americas", "Argentina")), ], indirect=True, ) From a7e8e8cd13be32df23ad33846d60988484d322dc Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 16:29:35 +0100 Subject: [PATCH 22/34] Add tests for dash_data_table --- vizro-core/src/vizro/tables/dash_table.py | 2 +- .../actions/test_filter_interaction_action.py | 1 + .../unit/vizro/tables/test_dash_table.py | 29 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/vizro-core/src/vizro/tables/dash_table.py b/vizro-core/src/vizro/tables/dash_table.py index 41cc43ded..bdcbb58ba 100644 --- a/vizro-core/src/vizro/tables/dash_table.py +++ b/vizro-core/src/vizro/tables/dash_table.py @@ -11,7 +11,7 @@ def dash_data_table(data_frame: pd.DataFrame, **kwargs): """Standard `dash_table.DataTable`.""" defaults = { - "columns": [{"name": i, "id": i} for i in data_frame.columns], + "columns": [{"name": col, "id": col} for col in data_frame.columns], "style_as_list_view": True, "style_data": {"border_bottom": "1px solid var(--border-subtle-alpha-01)", "height": "40px"}, "style_header": { diff --git a/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py b/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py index a10b734ea..a47f84911 100644 --- a/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py +++ b/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py @@ -270,6 +270,7 @@ def test_table_filter_interaction_with_two_targets( def test_aggrid_filter_interaction_with_two_targets( self, ctx_filter_interaction, target_scatter_filtered_continent, target_box_filtered_continent ): + # to not overcrowd these tests with duplication, we use one general case here for the AG Grid model_manager["box_chart"].actions = [ vm.Action(function=filter_interaction(targets=["scatter_chart", "box_chart"])) ] 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 e69de29bb..ffcd164c9 100644 --- a/vizro-core/tests/unit/vizro/tables/test_dash_table.py +++ b/vizro-core/tests/unit/vizro/tables/test_dash_table.py @@ -0,0 +1,29 @@ +import pandas as pd +from vizro.tables import dash_data_table + +data = pd.DataFrame( + { + "cat": ["a", "b", "c"], + "int": [4, 5, 6], + "float": [7.3, 8.2, 9.1], + "date": pd.to_datetime(["2021/01/01", "2021/01/02", "2021/01/03"]), + } +) + + +class TestDashDataTable: + def test_dash_data_table(self): + table = dash_data_table(data_frame=data)() + assert table.columns == [ + {"id": "cat", "name": "cat"}, + {"id": "int", "name": "int"}, + {"id": "float", "name": "float"}, + {"id": "date", "name": "date"}, + ] + assert table.data == [ + {"cat": "a", "date": pd.Timestamp("2021-01-01 00:00:00"), "float": 7.3, "int": 4}, + {"cat": "b", "date": pd.Timestamp("2021-01-02 00:00:00"), "float": 8.2, "int": 5}, + {"cat": "c", "date": pd.Timestamp("2021-01-03 00:00:00"), "float": 9.1, "int": 6}, + ] + # we could test other properties such as style_header, + # but this would just test our chosen defaults, and no functionality really From 33dcb407bdf6b3c0b97ac062f1f1fe10115ea944 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 16:37:17 +0100 Subject: [PATCH 23/34] Rename aggrid.py file --- vizro-core/src/vizro/models/_components/__init__.py | 2 +- .../src/vizro/models/_components/{aggrid.py => ag_grid.py} | 0 .../tests/unit/vizro/actions/test_filter_interaction_action.py | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) rename vizro-core/src/vizro/models/_components/{aggrid.py => ag_grid.py} (100%) diff --git a/vizro-core/src/vizro/models/_components/__init__.py b/vizro-core/src/vizro/models/_components/__init__.py index be0b8603b..07e95fabc 100644 --- a/vizro-core/src/vizro/models/_components/__init__.py +++ b/vizro-core/src/vizro/models/_components/__init__.py @@ -1,6 +1,6 @@ """Components that are placed according to the `Layout` of the `Page`.""" -from vizro.models._components.aggrid import AgGrid +from vizro.models._components.ag_grid import AgGrid from vizro.models._components.button import Button from vizro.models._components.card import Card from vizro.models._components.container import Container diff --git a/vizro-core/src/vizro/models/_components/aggrid.py b/vizro-core/src/vizro/models/_components/ag_grid.py similarity index 100% rename from vizro-core/src/vizro/models/_components/aggrid.py rename to vizro-core/src/vizro/models/_components/ag_grid.py diff --git a/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py b/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py index a47f84911..cd5638724 100644 --- a/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py +++ b/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py @@ -271,6 +271,7 @@ def test_aggrid_filter_interaction_with_two_targets( self, ctx_filter_interaction, target_scatter_filtered_continent, target_box_filtered_continent ): # to not overcrowd these tests with duplication, we use one general case here for the AG Grid + # Functionality should be similar enough to the Dash Datatable that this should suffice model_manager["box_chart"].actions = [ vm.Action(function=filter_interaction(targets=["scatter_chart", "box_chart"])) ] From c3ffc76d9cda863b85637fdbd79d99e434073ce5 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 16:54:53 +0100 Subject: [PATCH 24/34] Rename _dash_files files --- vizro-core/src/vizro/tables/__init__.py | 4 ++-- .../src/vizro/tables/{dash_aggrid.py => _dash_ag_grid.py} | 0 vizro-core/src/vizro/tables/{dash_table.py => _dash_table.py} | 0 .../models/_components/{test_aggrid.py => test_ag_grid.py} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename vizro-core/src/vizro/tables/{dash_aggrid.py => _dash_ag_grid.py} (100%) rename vizro-core/src/vizro/tables/{dash_table.py => _dash_table.py} (100%) rename vizro-core/tests/unit/vizro/models/_components/{test_aggrid.py => test_ag_grid.py} (100%) diff --git a/vizro-core/src/vizro/tables/__init__.py b/vizro-core/src/vizro/tables/__init__.py index 0c91e3346..3ec9ce4d2 100644 --- a/vizro-core/src/vizro/tables/__init__.py +++ b/vizro-core/src/vizro/tables/__init__.py @@ -1,5 +1,5 @@ -from vizro.tables.dash_aggrid import dash_ag_grid -from vizro.tables.dash_table import dash_data_table +from vizro.tables._dash_ag_grid import dash_ag_grid +from vizro.tables._dash_table import dash_data_table # Please keep alphabetically ordered __all__ = ["dash_ag_grid", "dash_data_table"] diff --git a/vizro-core/src/vizro/tables/dash_aggrid.py b/vizro-core/src/vizro/tables/_dash_ag_grid.py similarity index 100% rename from vizro-core/src/vizro/tables/dash_aggrid.py rename to vizro-core/src/vizro/tables/_dash_ag_grid.py diff --git a/vizro-core/src/vizro/tables/dash_table.py b/vizro-core/src/vizro/tables/_dash_table.py similarity index 100% rename from vizro-core/src/vizro/tables/dash_table.py rename to vizro-core/src/vizro/tables/_dash_table.py diff --git a/vizro-core/tests/unit/vizro/models/_components/test_aggrid.py b/vizro-core/tests/unit/vizro/models/_components/test_ag_grid.py similarity index 100% rename from vizro-core/tests/unit/vizro/models/_components/test_aggrid.py rename to vizro-core/tests/unit/vizro/models/_components/test_ag_grid.py From 11c9cf56e021ace6295c651c6df604d0b0267558 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 16:57:01 +0100 Subject: [PATCH 25/34] Refactor constant names --- vizro-core/src/vizro/tables/_dash_ag_grid.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vizro-core/src/vizro/tables/_dash_ag_grid.py b/vizro-core/src/vizro/tables/_dash_ag_grid.py index 45af842af..38ca9276d 100644 --- a/vizro-core/src/vizro/tables/_dash_ag_grid.py +++ b/vizro-core/src/vizro/tables/_dash_ag_grid.py @@ -6,7 +6,7 @@ from vizro.models.types import capture from vizro.tables._utils import _set_defaults_nested -FORMAT_CURRENCY_EU = """d3.formatLocale({ +_FORMAT_CURRENCY_EU = """d3.formatLocale({ "decimal": ",", "thousands": "\u00a0", "grouping": [3], @@ -15,7 +15,7 @@ "nan": "" })""" -DATA_TYPE_DEFINITIONS = { +_DATA_TYPE_DEFINITIONS = { "number": { "baseDataType": "number", "extendsDataType": "number", @@ -31,7 +31,7 @@ "euro": { "baseDataType": "number", "extendsDataType": "number", - "valueFormatter": {"function": f"{FORMAT_CURRENCY_EU}.format('$,.2f')(params.value)"}, + "valueFormatter": {"function": f"{_FORMAT_CURRENCY_EU}.format('$,.2f')(params.value)"}, }, "percent": { "baseDataType": "number", @@ -69,7 +69,7 @@ def dash_ag_grid(data_frame, **kwargs): }, }, "dashGridOptions": { - "dataTypeDefinitions": DATA_TYPE_DEFINITIONS, + "dataTypeDefinitions": _DATA_TYPE_DEFINITIONS, "animateRows": False, }, } From 602d0fe8371f2d6394a79ef9bb0c4b443375dfcf Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 18:05:51 +0100 Subject: [PATCH 26/34] Delete unnecessary assert in attribute tests --- vizro-core/tests/unit/vizro/models/_components/test_ag_grid.py | 2 +- vizro-core/tests/unit/vizro/models/_components/test_graph.py | 2 +- vizro-core/tests/unit/vizro/models/_components/test_table.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 215235e70..3f2828b32 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 @@ -74,9 +74,9 @@ def test_set_action_via_validator(self, standard_ag_grid, identity_action_functi class TestAttributesAgGrid: + # Testing at this low implementation level as mocking callback contexts skips checking for creation of these objects def test_ag_grid_filter_interaction_attributes(self, ag_grid_with_id): ag_grid = vm.AgGrid(figure=ag_grid_with_id, title="Gapminder", actions=[]) - assert hasattr(ag_grid, "_filter_interaction") ag_grid.pre_build() assert hasattr(ag_grid, "_filter_interaction_input") assert "modelID" in ag_grid._filter_interaction_input diff --git a/vizro-core/tests/unit/vizro/models/_components/test_graph.py b/vizro-core/tests/unit/vizro/models/_components/test_graph.py index 1ecf66e1f..e8e91b8be 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_graph.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_graph.py @@ -108,9 +108,9 @@ def test_set_action_via_validator(self, standard_px_chart, identity_action_funct class TestAttributesGraph: + # Testing at this low implementation level as mocking callback contexts skips checking for creation of these objects def test_graph_filter_interaction_attributes(self, standard_px_chart): graph = vm.Graph(figure=standard_px_chart) - assert hasattr(graph, "_filter_interaction") assert hasattr(graph, "_filter_interaction_input") assert "modelID" in graph._filter_interaction_input 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 6fa03ea88..53699274a 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_table.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_table.py @@ -75,13 +75,13 @@ def test_set_action_via_validator(self, standard_dash_table, identity_action_fun class TestAttributesTable: def test_table_filter_interaction_attributes(self, dash_data_table_with_id): table = vm.Table(figure=dash_data_table_with_id, title="Gapminder", actions=[]) - assert hasattr(table, "_filter_interaction") table.pre_build() assert hasattr(table, "_filter_interaction_input") assert "modelID" in table._filter_interaction_input class TestProcessTableDataFrame: + # Testing at this low implementation level as mocking callback contexts skips checking for creation of these objects def test_process_figure_data_frame_str_df(self, dash_table_with_str_dataframe, gapminder): data_manager["gapminder"] = gapminder table_with_str_df = vm.Table(id="table", figure=dash_table_with_str_dataframe) From 634b1e8edc7e0773c6026025866aa4326d07bf54 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 18:15:58 +0100 Subject: [PATCH 27/34] Use assert_component_equal in _dash_* tests --- .../unit/vizro/tables/test_dash_ag_grid.py | 21 +++++++++---- .../unit/vizro/tables/test_dash_table.py | 31 ++++++++++++------- 2 files changed, 35 insertions(+), 17 deletions(-) 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 f00b9725b..bcec1167a 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,4 +1,6 @@ +import dash_ag_grid as dag import pandas as pd +from asserts import assert_component_equal from vizro.tables import dash_ag_grid data = pd.DataFrame( @@ -14,11 +16,18 @@ class TestDashAgGrid: def test_dash_ag_grid(self): grid = dash_ag_grid(data_frame=data)() - assert grid.columnDefs == [{"field": "cat"}, {"field": "int"}, {"field": "float"}, {"field": "date"}] - assert grid.rowData == [ - {"cat": "a", "int": 4, "float": 7.3, "date": "2021-01-01"}, - {"cat": "b", "int": 5, "float": 8.2, "date": "2021-01-02"}, - {"cat": "c", "int": 6, "float": 9.1, "date": "2021-01-03"}, - ] + assert_component_equal( + grid, + dag.AgGrid( + columnDefs=[{"field": "cat"}, {"field": "int"}, {"field": "float"}, {"field": "date"}], + rowData=[ + {"cat": "a", "int": 4, "float": 7.3, "date": "2021-01-01"}, + {"cat": "b", "int": 5, "float": 8.2, "date": "2021-01-02"}, + {"cat": "c", "int": 6, "float": 9.1, "date": "2021-01-03"}, + ], + # defaultColDef={"resizable": True, "sortable": True}, + ), + keys_to_strip={"defaultColDef", "dashGridOptions"}, + ) # we could test other properties such as defaultColDef, # but this would just test our chosen defaults, and no functionality really 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 ffcd164c9..82bec2abf 100644 --- a/vizro-core/tests/unit/vizro/tables/test_dash_table.py +++ b/vizro-core/tests/unit/vizro/tables/test_dash_table.py @@ -1,4 +1,6 @@ import pandas as pd +from asserts import assert_component_equal +from dash import dash_table from vizro.tables import dash_data_table data = pd.DataFrame( @@ -14,16 +16,23 @@ class TestDashDataTable: def test_dash_data_table(self): table = dash_data_table(data_frame=data)() - assert table.columns == [ - {"id": "cat", "name": "cat"}, - {"id": "int", "name": "int"}, - {"id": "float", "name": "float"}, - {"id": "date", "name": "date"}, - ] - assert table.data == [ - {"cat": "a", "date": pd.Timestamp("2021-01-01 00:00:00"), "float": 7.3, "int": 4}, - {"cat": "b", "date": pd.Timestamp("2021-01-02 00:00:00"), "float": 8.2, "int": 5}, - {"cat": "c", "date": pd.Timestamp("2021-01-03 00:00:00"), "float": 9.1, "int": 6}, - ] + assert_component_equal( + table, + dash_table.DataTable( + columns=[ + {"id": "cat", "name": "cat"}, + {"id": "int", "name": "int"}, + {"id": "float", "name": "float"}, + {"id": "date", "name": "date"}, + ], + data=[ + {"cat": "a", "int": 4, "float": 7.3, "date": pd.Timestamp("2021-01-01 00:00:00")}, + {"cat": "b", "int": 5, "float": 8.2, "date": pd.Timestamp("2021-01-02 00:00:00")}, + {"cat": "c", "int": 6, "float": 9.1, "date": pd.Timestamp("2021-01-03 00:00:00")}, + ], + ), + keys_to_strip={"style_as_list_view", "style_data", "style_header"}, + ) + # we could test other properties such as style_header, # but this would just test our chosen defaults, and no functionality really From cabdafae2626a50a5dc348281d8e9dde41c4f09b Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 18:25:32 +0100 Subject: [PATCH 28/34] Reshuffle tests to make the category clearer --- .../vizro/models/_components/test_ag_grid.py | 16 +++++++++------- .../unit/vizro/models/_components/test_table.py | 16 +++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) 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 3f2828b32..b5cf058df 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 @@ -27,7 +27,7 @@ def dash_ag_grid_with_str_dataframe(): return dash_ag_grid(data_frame="gapminder") -class TestDunderMethodsAgGrid: +class TestAgGridInstantiation: def test_create_graph_mandatory_only(self, standard_ag_grid): ag_grid = vm.AgGrid(figure=standard_ag_grid) @@ -57,6 +57,13 @@ def test_failed_ag_grid_with_wrong_captured_callable(self, standard_px_chart): with pytest.raises(ValidationError, match="must provide a valid ag_grid function vm.AgGrid"): vm.AgGrid(figure=standard_px_chart) + def test_set_action_via_validator(self, standard_ag_grid, identity_action_function): + ag_grid = vm.AgGrid(figure=standard_ag_grid, actions=[Action(function=identity_action_function())]) + actions_chain = ag_grid.actions[0] + assert actions_chain.trigger.component_property == "cellClicked" + + +class TestDunderMethodsAgGrid: def test_getitem_known_args(self, dash_ag_grid_with_arguments): ag_grid = vm.AgGrid(figure=dash_ag_grid_with_arguments) assert ag_grid["defaultColDef"] == {"resizable": False, "sortable": False} @@ -67,11 +74,6 @@ def test_getitem_unknown_args(self, standard_ag_grid): with pytest.raises(KeyError): ag_grid["unknown_args"] - def test_set_action_via_validator(self, standard_ag_grid, identity_action_function): - ag_grid = vm.AgGrid(figure=standard_ag_grid, actions=[Action(function=identity_action_function())]) - actions_chain = ag_grid.actions[0] - assert actions_chain.trigger.component_property == "cellClicked" - class TestAttributesAgGrid: # Testing at this low implementation level as mocking callback contexts skips checking for creation of these objects @@ -101,7 +103,7 @@ def test_pre_build_no_actions_no_underlying_ag_grid_id(self, standard_ag_grid): ag_grid = vm.AgGrid(id="text_ag_grid", figure=standard_ag_grid) ag_grid.pre_build() - assert hasattr(ag_grid, "_callable_object_id") is False + assert not hasattr(ag_grid, "_callable_object_id") def test_pre_build_actions_no_underlying_ag_grid_id_exception(self, standard_ag_grid, filter_interaction_action): ag_grid = vm.AgGrid(id="text_ag_grid", figure=standard_ag_grid, actions=[filter_interaction_action]) 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 53699274a..17429685e 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_table.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_table.py @@ -26,7 +26,7 @@ def dash_table_with_str_dataframe(): return dash_data_table(data_frame="gapminder") -class TestDunderMethodsTable: +class TestTableInstantiation: def test_create_graph_mandatory_only(self, standard_dash_table): table = vm.Table(figure=standard_dash_table) @@ -56,6 +56,13 @@ def test_failed_table_with_wrong_captured_callable(self, standard_px_chart): with pytest.raises(ValidationError, match="must provide a valid table function vm.Table"): vm.Table(figure=standard_px_chart) + def test_set_action_via_validator(self, standard_dash_table, identity_action_function): + table = vm.Table(figure=standard_dash_table, actions=[Action(function=identity_action_function())]) + actions_chain = table.actions[0] + assert actions_chain.trigger.component_property == "active_cell" + + +class TestDunderMethodsTable: def test_getitem_known_args(self, dash_table_with_arguments): table = vm.Table(figure=dash_table_with_arguments) assert table["style_header"] == {"border": "1px solid green"} @@ -66,11 +73,6 @@ def test_getitem_unknown_args(self, standard_dash_table): with pytest.raises(KeyError): table["unknown_args"] - def test_set_action_via_validator(self, standard_dash_table, identity_action_function): - table = vm.Table(figure=standard_dash_table, actions=[Action(function=identity_action_function())]) - actions_chain = table.actions[0] - assert actions_chain.trigger.component_property == "active_cell" - class TestAttributesTable: def test_table_filter_interaction_attributes(self, dash_data_table_with_id): @@ -100,7 +102,7 @@ def test_pre_build_no_actions_no_underlying_table_id(self, standard_dash_table): table = vm.Table(id="text_table", figure=standard_dash_table) table.pre_build() - assert hasattr(table, "_callable_object_id") is False + assert not hasattr(table, "_callable_object_id") def test_pre_build_actions_no_underlying_table_id_exception(self, standard_dash_table, filter_interaction_action): table = vm.Table(id="text_table", figure=standard_dash_table, actions=[filter_interaction_action]) From fb6f2066bef10ac0745d51dce01282e53dc3b288 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Fri, 23 Feb 2024 18:48:47 +0100 Subject: [PATCH 29/34] Remaining PR comments --- vizro-core/src/vizro/models/_components/ag_grid.py | 1 + vizro-core/src/vizro/models/_components/graph.py | 1 + vizro-core/src/vizro/models/_components/table.py | 1 + 3 files changed, 3 insertions(+) diff --git a/vizro-core/src/vizro/models/_components/ag_grid.py b/vizro-core/src/vizro/models/_components/ag_grid.py index f811fe6af..5ff91f08f 100644 --- a/vizro-core/src/vizro/models/_components/ag_grid.py +++ b/vizro-core/src/vizro/models/_components/ag_grid.py @@ -73,6 +73,7 @@ def _filter_interaction( self, data_frame: pd.DataFrame, target: str, ctd_filter_interaction: Dict[str, CallbackTriggerDict] ) -> pd.DataFrame: """Function to be carried out for pre-defined `filter_interaction`.""" + # data_frame is the DF of the target, ie the data to be filtered, hence we cannot get the DF from this model ctd_cellClicked = ctd_filter_interaction["cellClicked"] if not ctd_cellClicked["value"]: return data_frame diff --git a/vizro-core/src/vizro/models/_components/graph.py b/vizro-core/src/vizro/models/_components/graph.py index 63ac4e2b8..0a7cc46bf 100644 --- a/vizro-core/src/vizro/models/_components/graph.py +++ b/vizro-core/src/vizro/models/_components/graph.py @@ -88,6 +88,7 @@ def _filter_interaction( self, data_frame: pd.DataFrame, target: str, ctd_filter_interaction: Dict[str, CallbackTriggerDict] ) -> pd.DataFrame: """Function to be carried out for pre-defined `filter_interaction`.""" + # data_frame is the DF of the target, ie the data to be filtered, hence we cannot get the DF from this model ctd_click_data = ctd_filter_interaction["clickData"] if not ctd_click_data["value"]: return data_frame diff --git a/vizro-core/src/vizro/models/_components/table.py b/vizro-core/src/vizro/models/_components/table.py index 4c23d6f8a..6a2194a19 100644 --- a/vizro-core/src/vizro/models/_components/table.py +++ b/vizro-core/src/vizro/models/_components/table.py @@ -77,6 +77,7 @@ def _filter_interaction( self, data_frame: pd.DataFrame, target: str, ctd_filter_interaction: Dict[str, CallbackTriggerDict] ) -> pd.DataFrame: """Function to be carried out for pre-defined `filter_interaction`.""" + # data_frame is the DF of the target, ie the data to be filtered, hence we cannot get the DF from this model ctd_active_cell = ctd_filter_interaction["active_cell"] ctd_derived_viewport_data = ctd_filter_interaction["derived_viewport_data"] if not ctd_active_cell["value"] or not ctd_derived_viewport_data["value"]: From 24e0e846c769f970cf7a45233a06b89529cf6d55 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Tue, 27 Feb 2024 11:01:26 +0100 Subject: [PATCH 30/34] PR comments Petar --- .../src/vizro/models/_components/ag_grid.py | 23 ++++++++-------- .../src/vizro/models/_components/table.py | 27 +++++++++---------- .../tests/unit/vizro/actions/conftest.py | 2 +- .../actions/test_filter_interaction_action.py | 4 +-- .../unit/vizro/tables/test_dash_ag_grid.py | 1 - 5 files changed, 26 insertions(+), 31 deletions(-) diff --git a/vizro-core/src/vizro/models/_components/ag_grid.py b/vizro-core/src/vizro/models/_components/ag_grid.py index 5ff91f08f..5916722e1 100644 --- a/vizro-core/src/vizro/models/_components/ag_grid.py +++ b/vizro-core/src/vizro/models/_components/ag_grid.py @@ -92,27 +92,26 @@ def _filter_interaction( @_log_call def pre_build(self): - if self.actions: - kwargs = self.figure._arguments.copy() + kwargs = self.figure._arguments.copy() - # taken from table implementation - see there for details - kwargs["data_frame"] = pd.DataFrame() - underlying_aggrid_object = self.figure._function(**kwargs) + # taken from table implementation - see there for details + kwargs["data_frame"] = pd.DataFrame() + underlying_aggrid_object = self.figure._function(**kwargs) - if not hasattr(underlying_aggrid_object, "id"): - raise ValueError( - "Underlying `AgGrid` callable has no attribute 'id'. To enable actions triggered by the `AgGrid`" - " a valid 'id' has to be provided to the `AgGrid` callable." - ) + if not hasattr(underlying_aggrid_object, "id"): + raise ValueError( + "Underlying `AgGrid` callable has no attribute 'id'. To enable actions triggered by the `AgGrid`" + " a valid 'id' has to be provided to the `AgGrid` callable." + ) - self._callable_object_id = underlying_aggrid_object.id + self._callable_object_id = underlying_aggrid_object.id def build(self): return dcc.Loading( html.Div( [ html.H3(self.title, className="table-title") if self.title else None, - html.Div(dag.AgGrid(**({"id": self._callable_object_id} if self.actions else {})), id=self.id), + html.Div(dag.AgGrid(id=self._callable_object_id), id=self.id), ], className="table-container", id=f"{self.id}_outer", diff --git a/vizro-core/src/vizro/models/_components/table.py b/vizro-core/src/vizro/models/_components/table.py index 6a2194a19..88a2b1ca0 100644 --- a/vizro-core/src/vizro/models/_components/table.py +++ b/vizro-core/src/vizro/models/_components/table.py @@ -98,31 +98,28 @@ def _filter_interaction( @_log_call def pre_build(self): - if self.actions: - kwargs = self.figure._arguments.copy() + kwargs = self.figure._arguments.copy() - # This workaround is needed because the underlying table object requires a data_frame - kwargs["data_frame"] = pd.DataFrame() + # This workaround is needed because the underlying table object requires a data_frame + kwargs["data_frame"] = pd.DataFrame() - # The underlying table object is pre-built, so we can fetch its ID. - underlying_table_object = self.figure._function(**kwargs) + # The underlying table object is pre-built, so we can fetch its ID. + underlying_table_object = self.figure._function(**kwargs) - if not hasattr(underlying_table_object, "id"): - raise ValueError( - "Underlying `Table` callable has no attribute 'id'. To enable actions triggered by the `Table`" - " a valid 'id' has to be provided to the `Table` callable." - ) + if not hasattr(underlying_table_object, "id"): + raise ValueError( + "Underlying `Table` callable has no attribute 'id'. To enable actions triggered by the `Table`" + " a valid 'id' has to be provided to the `Table` callable." + ) - self._callable_object_id = underlying_table_object.id + self._callable_object_id = underlying_table_object.id def build(self): return dcc.Loading( html.Div( [ html.H3(self.title, className="table-title") if self.title else None, - html.Div( - dash_table.DataTable(**({"id": self._callable_object_id} if self.actions else {})), id=self.id - ), + html.Div(dash_table.DataTable(id=self._callable_object_id), id=self.id), ], className="table-container", id=f"{self.id}_outer", diff --git a/vizro-core/tests/unit/vizro/actions/conftest.py b/vizro-core/tests/unit/vizro/actions/conftest.py index f34fec6d8..fb570eb79 100644 --- a/vizro-core/tests/unit/vizro/actions/conftest.py +++ b/vizro-core/tests/unit/vizro/actions/conftest.py @@ -62,7 +62,7 @@ def managers_one_page_two_graphs_one_button(box_chart, scatter_chart): def managers_one_page_two_graphs_one_table_one_aggrid_one_button( box_chart, scatter_chart, dash_data_table_with_id, ag_grid_with_id ): - """Instantiates a simple model_manager and data_manager with a page, two graph models and the button component.""" + """Instantiates a simple model_manager and data_manager with: page, two graph, table, aggrid and a button component.""" vm.Page( id="test_page", title="My first dashboard", diff --git a/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py b/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py index cd5638724..b57131ad5 100644 --- a/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py +++ b/vizro-core/tests/unit/vizro/actions/test_filter_interaction_action.py @@ -270,8 +270,8 @@ def test_table_filter_interaction_with_two_targets( def test_aggrid_filter_interaction_with_two_targets( self, ctx_filter_interaction, target_scatter_filtered_continent, target_box_filtered_continent ): - # to not overcrowd these tests with duplication, we use one general case here for the AG Grid - # Functionality should be similar enough to the Dash Datatable that this should suffice + # To not overcrowd these tests with duplication, we use one general case here for the AG Grid + # Functionality should be similar enough to the Dash Datatable that this suffices model_manager["box_chart"].actions = [ vm.Action(function=filter_interaction(targets=["scatter_chart", "box_chart"])) ] 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 bcec1167a..481d038df 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 @@ -25,7 +25,6 @@ def test_dash_ag_grid(self): {"cat": "b", "int": 5, "float": 8.2, "date": "2021-01-02"}, {"cat": "c", "int": 6, "float": 9.1, "date": "2021-01-03"}, ], - # defaultColDef={"resizable": True, "sortable": True}, ), keys_to_strip={"defaultColDef", "dashGridOptions"}, ) From ec85e0c0dfcd5693d576e5fcc531f8bef88c3ca2 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Tue, 27 Feb 2024 17:13:49 +0100 Subject: [PATCH 31/34] Fix bug for missing ID when no actions but ID are defined --- vizro-core/src/vizro/models/_components/ag_grid.py | 11 +++++++---- vizro-core/src/vizro/models/_components/table.py | 10 ++++++---- vizro-core/tests/unit/vizro/actions/conftest.py | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/vizro-core/src/vizro/models/_components/ag_grid.py b/vizro-core/src/vizro/models/_components/ag_grid.py index 5916722e1..7782473e2 100644 --- a/vizro-core/src/vizro/models/_components/ag_grid.py +++ b/vizro-core/src/vizro/models/_components/ag_grid.py @@ -96,22 +96,25 @@ def pre_build(self): # taken from table implementation - see there for details kwargs["data_frame"] = pd.DataFrame() + underlying_aggrid_object = self.figure._function(**kwargs) - if not hasattr(underlying_aggrid_object, "id"): + if hasattr(underlying_aggrid_object, "id"): + self._callable_object_id = underlying_aggrid_object.id + + if self.actions and not hasattr(self,"_callable_object_id"): raise ValueError( "Underlying `AgGrid` callable has no attribute 'id'. To enable actions triggered by the `AgGrid`" " a valid 'id' has to be provided to the `AgGrid` callable." ) - self._callable_object_id = underlying_aggrid_object.id - def build(self): + dash_ag_grid_conf = {"id": self._callable_object_id} if hasattr(self, "_callable_object_id") else {} return dcc.Loading( html.Div( [ html.H3(self.title, className="table-title") if self.title else None, - html.Div(dag.AgGrid(id=self._callable_object_id), id=self.id), + html.Div(dag.AgGrid(**dash_ag_grid_conf), id=self.id), ], className="table-container", id=f"{self.id}_outer", diff --git a/vizro-core/src/vizro/models/_components/table.py b/vizro-core/src/vizro/models/_components/table.py index 88a2b1ca0..e8bd376c5 100644 --- a/vizro-core/src/vizro/models/_components/table.py +++ b/vizro-core/src/vizro/models/_components/table.py @@ -106,20 +106,22 @@ def pre_build(self): # The underlying table object is pre-built, so we can fetch its ID. underlying_table_object = self.figure._function(**kwargs) - if not hasattr(underlying_table_object, "id"): + if hasattr(underlying_table_object, "id"): + self._callable_object_id = underlying_table_object.id + + if self.actions and not hasattr(self,"_callable_object_id"): raise ValueError( "Underlying `Table` callable has no attribute 'id'. To enable actions triggered by the `Table`" " a valid 'id' has to be provided to the `Table` callable." ) - self._callable_object_id = underlying_table_object.id - def build(self): + dash_table_conf = {"id": self._callable_object_id} if hasattr(self,"_callable_object_id") else {} return dcc.Loading( html.Div( [ html.H3(self.title, className="table-title") if self.title else None, - html.Div(dash_table.DataTable(id=self._callable_object_id), id=self.id), + html.Div(dash_table.DataTable(**dash_table_conf), id=self.id), ], className="table-container", id=f"{self.id}_outer", diff --git a/vizro-core/tests/unit/vizro/actions/conftest.py b/vizro-core/tests/unit/vizro/actions/conftest.py index fb570eb79..413fb36eb 100644 --- a/vizro-core/tests/unit/vizro/actions/conftest.py +++ b/vizro-core/tests/unit/vizro/actions/conftest.py @@ -62,7 +62,7 @@ def managers_one_page_two_graphs_one_button(box_chart, scatter_chart): def managers_one_page_two_graphs_one_table_one_aggrid_one_button( box_chart, scatter_chart, dash_data_table_with_id, ag_grid_with_id ): - """Instantiates a simple model_manager and data_manager with: page, two graph, table, aggrid and a button component.""" + """Instantiates a simple model_manager and data_manager with: page, graphs, table, aggrid and button component.""" vm.Page( id="test_page", title="My first dashboard", From 29d9aaf0e7db511a4c93595333d3b353553d737e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:14:15 +0000 Subject: [PATCH 32/34] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- vizro-core/src/vizro/models/_components/ag_grid.py | 2 +- vizro-core/src/vizro/models/_components/table.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vizro-core/src/vizro/models/_components/ag_grid.py b/vizro-core/src/vizro/models/_components/ag_grid.py index 7782473e2..8c055d185 100644 --- a/vizro-core/src/vizro/models/_components/ag_grid.py +++ b/vizro-core/src/vizro/models/_components/ag_grid.py @@ -102,7 +102,7 @@ def pre_build(self): if hasattr(underlying_aggrid_object, "id"): self._callable_object_id = underlying_aggrid_object.id - if self.actions and not hasattr(self,"_callable_object_id"): + if self.actions and not hasattr(self, "_callable_object_id"): raise ValueError( "Underlying `AgGrid` callable has no attribute 'id'. To enable actions triggered by the `AgGrid`" " a valid 'id' has to be provided to the `AgGrid` callable." diff --git a/vizro-core/src/vizro/models/_components/table.py b/vizro-core/src/vizro/models/_components/table.py index e8bd376c5..3c90d2119 100644 --- a/vizro-core/src/vizro/models/_components/table.py +++ b/vizro-core/src/vizro/models/_components/table.py @@ -109,14 +109,14 @@ def pre_build(self): if hasattr(underlying_table_object, "id"): self._callable_object_id = underlying_table_object.id - if self.actions and not hasattr(self,"_callable_object_id"): + if self.actions and not hasattr(self, "_callable_object_id"): raise ValueError( "Underlying `Table` callable has no attribute 'id'. To enable actions triggered by the `Table`" " a valid 'id' has to be provided to the `Table` callable." ) def build(self): - dash_table_conf = {"id": self._callable_object_id} if hasattr(self,"_callable_object_id") else {} + dash_table_conf = {"id": self._callable_object_id} if hasattr(self, "_callable_object_id") else {} return dcc.Loading( html.Div( [ From f503f05074c2153095eb24d6d431bbd1f82530b4 Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Tue, 27 Feb 2024 21:02:59 +0100 Subject: [PATCH 33/34] Fix pagination bug --- vizro-core/examples/_dev/app.py | 1 + vizro-core/src/vizro/models/_components/ag_grid.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/vizro-core/examples/_dev/app.py b/vizro-core/examples/_dev/app.py index d842ba7a9..33c925215 100644 --- a/vizro-core/examples/_dev/app.py +++ b/vizro-core/examples/_dev/app.py @@ -95,6 +95,7 @@ figure=dash_ag_grid( id="dash_ag_grid_2", data_frame=df2, + dashGridOptions={"pagination": True}, ), ), ], diff --git a/vizro-core/src/vizro/models/_components/ag_grid.py b/vizro-core/src/vizro/models/_components/ag_grid.py index 8c055d185..8ac8f3724 100644 --- a/vizro-core/src/vizro/models/_components/ag_grid.py +++ b/vizro-core/src/vizro/models/_components/ag_grid.py @@ -1,7 +1,6 @@ import logging from typing import Dict, List, Literal -import dash_ag_grid as dag import pandas as pd from dash import State, dcc, html @@ -109,12 +108,17 @@ def pre_build(self): ) def build(self): - dash_ag_grid_conf = {"id": self._callable_object_id} if hasattr(self, "_callable_object_id") else {} + # The pagination setting (and potentially others) only work when the initially built AgGrid has the same + # setting as the object that is built on-page-load and rendered finally. + dash_ag_grid_conf = self.figure._arguments.copy() + dash_ag_grid_conf["data_frame"] = pd.DataFrame() + if hasattr(self, "_callable_object_id"): + dash_ag_grid_conf["id"] = self._callable_object_id return dcc.Loading( html.Div( [ html.H3(self.title, className="table-title") if self.title else None, - html.Div(dag.AgGrid(**dash_ag_grid_conf), id=self.id), + html.Div(self.figure._function(**dash_ag_grid_conf), id=self.id), ], className="table-container", id=f"{self.id}_outer", From f15ca91b82d6b22cb57d05932e132c34386b7dfe Mon Sep 17 00:00:00 2001 From: Maximilian Schulz Date: Tue, 27 Feb 2024 21:31:00 +0100 Subject: [PATCH 34/34] Add tests to reflect bugfix --- vizro-core/tests/unit/vizro/conftest.py | 5 +++++ .../unit/vizro/models/_components/test_ag_grid.py | 15 ++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/vizro-core/tests/unit/vizro/conftest.py b/vizro-core/tests/unit/vizro/conftest.py index 72c215dc0..42faf520b 100644 --- a/vizro-core/tests/unit/vizro/conftest.py +++ b/vizro-core/tests/unit/vizro/conftest.py @@ -36,6 +36,11 @@ def ag_grid_with_id(gapminder): return dash_ag_grid(id="underlying_ag_grid_id", data_frame=gapminder) +@pytest.fixture +def ag_grid_with_id_and_conf(gapminder): + return dash_ag_grid(id="underlying_ag_grid_id", data_frame=gapminder, dashGridOptions={"pagination": True}) + + @pytest.fixture def standard_dash_table(gapminder): return dash_data_table(data_frame=gapminder) 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 b5cf058df..59f0a812d 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 @@ -1,6 +1,6 @@ """Unit tests for vizro.models.AgGrid.""" -import dash_ag_grid as dag +import pandas as pd import pytest from asserts import assert_component_equal from dash import dcc, html @@ -126,7 +126,7 @@ def test_ag_grid_build_mandatory_only(self, standard_ag_grid): html.Div( [ None, - html.Div(dag.AgGrid(), id="text_ag_grid"), + html.Div(dash_ag_grid(data_frame=pd.DataFrame())(), id="text_ag_grid"), ], className="table-container", id="text_ag_grid_outer", @@ -137,8 +137,8 @@ def test_ag_grid_build_mandatory_only(self, standard_ag_grid): assert_component_equal(ag_grid, expected_ag_grid) - def test_ag_grid_build_with_underlying_id(self, ag_grid_with_id, filter_interaction_action): - ag_grid = vm.AgGrid(id="text_ag_grid", figure=ag_grid_with_id, actions=[filter_interaction_action]) + def test_ag_grid_build_with_underlying_id(self, ag_grid_with_id_and_conf, filter_interaction_action): + ag_grid = vm.AgGrid(id="text_ag_grid", figure=ag_grid_with_id_and_conf, actions=[filter_interaction_action]) ag_grid.pre_build() ag_grid = ag_grid.build() @@ -146,7 +146,12 @@ def test_ag_grid_build_with_underlying_id(self, ag_grid_with_id, filter_interact html.Div( [ None, - html.Div(dag.AgGrid(id="underlying_ag_grid_id"), id="text_ag_grid"), + html.Div( + dash_ag_grid( + id="underlying_ag_grid_id", data_frame=pd.DataFrame(), dashGridOptions={"pagination": True} + )(), + id="text_ag_grid", + ), ], className="table-container", id="text_ag_grid_outer",