Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Implement AgGrid model #289

Merged
merged 42 commits into from
Feb 27, 2024
Merged

Conversation

maxschulz-COL
Copy link
Contributor

@maxschulz-COL maxschulz-COL commented Jan 25, 2024

Description

This PR is an iteration of discussion in #260. Here we decided to implement one model per Callable type.

  • Example Dashboard contains main features
  • Formatting of Data inspired by @AnnMarieW and the new features in AGGrid 31.0.0
  • no styling applied
  • no tests
  • no docs

Styling, docs and tests will be in separate PRs.

Notable activated features

  • Columns are resizeable
  • Columns are sortable
  • Columns are filterable (with an apply and reset button)
  • Activated pagination
  • Deactivated Row animation

Cell formatting

  • Defined 4 additional data types for easy column formatting - will be explained in the docs: dollar, euro, percent, numeric - see third page of dev example and Screenshot
  • Filters adjust to cell content type: numeric, date, or string/other
  • (to be discussed) Auto-format any detected pd.datetime column into a valid dateString (required by AGGrid to function for dates out of the box)

Screenshot

image

Notice

  • I acknowledge and agree that, by checking this box and clicking "Submit Pull Request":

    • I submit this contribution under the Apache 2.0 license and represent that I am entitled to do so on behalf of myself, my employer, or relevant third parties, as applicable.
    • I certify that (a) this contribution is my original creation and / or (b) to the extent it is not my original creation, I am authorized to submit this contribution on behalf of the original creator(s) or their licensees.
    • I certify that the use of this contribution as authorized by the Apache 2.0 license does not violate the intellectual property rights of anyone else.
    • I have not referenced individuals, products or companies in any commits, directly or indirectly.
    • I have not added data or restricted code in any commits, directly or indirectly.

@maxschulz-COL maxschulz-COL mentioned this pull request Feb 20, 2024
4 tasks
Copy link
Contributor

@petar-qb petar-qb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left just a few general comments I'm interested in and I can review this PR in detail maybe tomorrow 😄.

vizro-core/src/vizro/actions/_actions_utils.py Outdated Show resolved Hide resolved
vizro-core/src/vizro/models/_components/graph.py Outdated Show resolved Hide resolved
Comment on lines 86 to 109
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like _filter_interaction method is pretty similar in Graph/Table/Grid model. What do you think about the idea of implementing _extract_filtering_info method per model instead (e.g. it can just return real selected value and maybe the column that has to be filtered), and conduct filtering inside the _actions_utils._apply_filter_interaction?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is an interesting idea! I think we should definitely discuss it. I guess the pros are:

  • simplified and shortened code in the respective models
  • avoiding repretition

The cons I can think of are:

  • potentially wrong abstraction level given that e.g. the graph currently filters multiple columns, which the table/grid don't
  • like this we are giving the user less control over the filtering

Open to both really :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it is not easy to make this kind of abstraction in general, hence I call it - "actions_info" - the biggest open question we currently have.

If we make this kind of abstraction properly, we could say that the "filter_interaction" is just the same as the "_filter" action, which sounds perfectly correct to me.

From top of my head: All "actions_info" we need to set for every Vizro model (e.g. Button, Card, Dropdown, Graph, Grid...) is:

  • _get_input_components - e.g. {model_id: State(...), derived_viewport_data: State(...), active_cell: State(...)} OR {model_id: State(...), value: State(...)};
  • _extract_value_from_input - some function,
  • _get_output_components - e.g. {model_id: Output(...)}

It means that every Vizro model will have implemented these methods in a form of (class methods, or some actions_info: Dict[str, any] argument). Also, these methods/arguments could be overwritten in case of custom component creation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest we leave it as is for now and come back to it later. This is still a big open question indeed, and I think "Duplication is cheaper than the wrong abstraction" applies in this case.

Generally I think I agree with @petar-qb's idea, but there's still a lot of moving pieces here so hard to tell right now what the right solution is. So for now I think just focus on:

  • getting these models working correctly
  • initial actions v2 work to make those action CapturedCallable classes

And after that we can figure out what to do with all this actions info stuff.

Let's unresolve this conversation after merging though so we can easily refer back to it.

vizro-core/src/vizro/models/_components/graph.py Outdated Show resolved Hide resolved
Copy link
Contributor

@antonymilne antonymilne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looks good, although I didn't actually play around with using it yet 👍 Thanks for all the work on different versions of this, I know it's not been an easy task!

I left some initial comments so you don't wait too long for a review, but I don't think I will have much more to say after this round anyway, so long as we're confident it works ok.

What happens if a user does AgGrid(figure=dash_data_table(...) or Table(figure=dash_ag_grid(...)? I guess it renders and all works well apart from the filter interaction ?

vizro-core/src/vizro/tables/dash_aggrid.py Outdated Show resolved Hide resolved
vizro-core/src/vizro/tables/dash_aggrid.py Outdated Show resolved Hide resolved
vizro-core/src/vizro/tables/dash_aggrid.py Outdated Show resolved Hide resolved
vizro-core/src/vizro/tables/dash_aggrid.py Outdated Show resolved Hide resolved
vizro-core/src/vizro/tables/dash_aggrid.py Outdated Show resolved Hide resolved
@_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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR, but @petar-qb do you remember we had a conversation a while ago about trying to simplify all this stuff? I think we should try and do that still, especially now it's duplicated for AGGrid and Table.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really can't remember what we talked about exactly..
At least we can extract the implementation of this method and reuse it in the vm.AgGrid and vm.Table.

Also, I think we should get rid of "if self.actions:" statement in pre_build method and always conduct fetching of underlying id. In that way we can avoid raising a dash exception (something like: "Input of callback X doesn't exist."). This exception will be raised:

  • if user provides an ID for the underlying table,
  • if the underlying table is an input (dash.State) of the custom action,
  • if there is no "actions" defined for the vm.Table / vm.AgGrid

We should double check this assumption and if it's true we should also change one line in the build method.
From:

                        dash_table.DataTable(**({"id": self._callable_object_id} if self.actions else {})), id=self.id

To:

                        dash_table.DataTable(id=self._callable_object_id), id=self.id

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My idea was that we can remove basically all of this, like so:

Index: vizro-core/src/vizro/models/_components/table.py
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/vizro-core/src/vizro/models/_components/table.py b/vizro-core/src/vizro/models/_components/table.py
--- a/vizro-core/src/vizro/models/_components/table.py	(revision 4ea5b5f35cf9db4217537038d4f36ed4e8fc3ee4)
+++ b/vizro-core/src/vizro/models/_components/table.py	(date 1705597769502)
@@ -48,7 +48,9 @@
     # Convenience wrapper/syntactic sugar.
     def __call__(self, **kwargs):
         kwargs.setdefault("data_frame", data_manager._get_component_data(self.id))
-        return self.figure(**kwargs)
+        figure = self.figure(**kwargs)
+        figure.id = self._callable_object_id
+        return figure
 
     # Convenience wrapper/syntactic sugar.
     def __getitem__(self, arg_name: str):
@@ -60,31 +62,14 @@
 
     @_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()
-
-            # 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
+        self._callable_object_id = self.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",

Definitely not for this PR, but just wanted to make sure we didn't forget it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, thanks for remembering 😄. This change really reduces the complexity of the table model ⚡.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok will keep this un-resolved but will not implement in this PR

vizro-core/src/vizro/models/_components/aggrid.py Outdated Show resolved Hide resolved
vizro-core/src/vizro/models/_components/aggrid.py Outdated Show resolved Hide resolved
Comment on lines 86 to 109
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest we leave it as is for now and come back to it later. This is still a big open question indeed, and I think "Duplication is cheaper than the wrong abstraction" applies in this case.

Generally I think I agree with @petar-qb's idea, but there's still a lot of moving pieces here so hard to tell right now what the right solution is. So for now I think just focus on:

  • getting these models working correctly
  • initial actions v2 work to make those action CapturedCallable classes

And after that we can figure out what to do with all this actions info stuff.

Let's unresolve this conversation after merging though so we can easily refer back to it.

Copy link
Contributor

@huong-li-nguyen huong-li-nguyen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I won't review this in detail as others already do :)

As long as I remember - it would be amazing ⭐ if you could add the AG-grid example inside the features dashboard and add the yaml version of it (if applicable) as well. You can also replace the data table inside the demo dashboard with the ag-grid one already👍

I'll leave that up to you though, whether you want to add it in this PR or your docs PR.

Copy link
Contributor

@antonymilne antonymilne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments on the tests to look at, but it's minor stuff so feel free to merge when you've dealt with them 🙂

@maxschulz-COL maxschulz-COL changed the title Feature: AGGrid implementation - separate models [FEAT] Implement AgGrid model allowing Feb 23, 2024
Copy link
Contributor

@petar-qb petar-qb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've already approved this PR, but here's the final review. Everything is on its place except a few minor things I commented below.

@huong-li-nguyen huong-li-nguyen changed the title [FEAT] Implement AgGrid model allowing [Feat] Implement AgGrid model allowing Feb 26, 2024
@maxschulz-COL maxschulz-COL changed the title [Feat] Implement AgGrid model allowing [Feat] Implement AgGrid model Feb 27, 2024
@petar-qb
Copy link
Contributor

The initial bug is solved with the latest changes ec85e0c you pushed. Awesome work Max! 🤖

@maxschulz-COL maxschulz-COL merged commit 1dd4a75 into main Feb 27, 2024
33 checks passed
@maxschulz-COL maxschulz-COL deleted the feat/aggrid_separate_models branch February 27, 2024 20:43
Comment on lines +111 to +116
# 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very clean solution! I just think that the following two lines:

        if hasattr(self, "_callable_object_id"):
            dash_ag_grid_conf["id"] = self._callable_object_id

don't make any difference as the id is already copied to the dash_ag_grid_conf in the previous lines.

Also, it seems like it makes sense to push similar approach for the vm.Table - dash_data_table solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants