-
Notifications
You must be signed in to change notification settings - Fork 150
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
AGGrid implementation #260
Changes from 7 commits
96b6259
a74a28f
4878865
e27c329
f9e14cb
7d370c2
2c0e0a7
6a5e382
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
"""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.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() | ||
) | ||
|
||
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", | ||
), | ||
} | ||
} | ||
|
||
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"], | ||
), | ||
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( | ||
pages=[ | ||
create_benchmark_analysis(), | ||
], | ||
) | ||
|
||
if __name__ == "__main__": | ||
Vizro(assets_folder="../assets").build(dashboard).run() |
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -12,14 +12,26 @@ | |||||||||||||||||
import vizro.tables as vt | ||||||||||||||||||
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._action._actions_chain import _set_actions | ||||||||||||||||||
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__) | ||||||||||||||||||
|
||||||||||||||||||
|
||||||||||||||||||
def _get_table_type(figure): # this function can be applied also in pre-build | ||||||||||||||||||
kwargs = 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 = figure._function(**kwargs) | ||||||||||||||||||
Comment on lines
+24
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This should work by itself I think? Probably we should do some I'd also like to understand why this evaluation of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Posting the answer @petar-qb gave to me: Graphs and Tables in the Dash (so in the Vizro too) are handled differently. dcc.Graph has: The problem is that there's no outer dcc component wrapper for tables 😕. So, there is nothing like dcc.Table Dash inbuilt component that has Yes, Vizro has created some kind of wrapper Now, let's give an example on how callback inputs and outputs are created in the case of Tables. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @petar-qb this half makes sense to me. The half that doesn't make sense is:
Ideally what I'd like to do is this:
so that the No need resolve this conversation as part of this PR because it's outside the scope of the PR, but it would be great to have a chat about it - let me put something in the calendar 🙂 |
||||||||||||||||||
table_type = underlying_table_object.__class__.__name__ | ||||||||||||||||||
return underlying_table_object, table_type | ||||||||||||||||||
|
||||||||||||||||||
|
||||||||||||||||||
class Table(VizroBaseModel): | ||||||||||||||||||
"""Wrapper for table components to visualize in dashboard. | ||||||||||||||||||
|
||||||||||||||||||
|
@@ -37,14 +49,26 @@ class Table(VizroBaseModel): | |||||||||||||||||
actions: List[Action] = [] | ||||||||||||||||||
|
||||||||||||||||||
_callable_object_id: str = PrivateAttr() | ||||||||||||||||||
_table_type: str = ( | ||||||||||||||||||
PrivateAttr() | ||||||||||||||||||
) # Ideally we would be able to use the populated content of this field in the `set_actions` validator. | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Definitely the current code feels a bit tangled here, but I appreciate it's hard to get these things with private properties and validators working exactly as you'd like. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. I spent a while getting this to work, but also didn't hunt for a better solution once it was working (except removing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good. Indeed there's no point spending a long time perfecting this if we don't need it at all in the end. |
||||||||||||||||||
|
||||||||||||||||||
# Component properties for actions and interactions | ||||||||||||||||||
_output_property: str = PrivateAttr("children") | ||||||||||||||||||
|
||||||||||||||||||
# validator | ||||||||||||||||||
set_actions = _action_validator_factory("active_cell") | ||||||||||||||||||
_validate_callable = validator("figure", allow_reuse=True, always=True)(_process_callable_data_frame) | ||||||||||||||||||
|
||||||||||||||||||
@validator("actions") | ||||||||||||||||||
def set_actions(cls, v, values): | ||||||||||||||||||
_, table_type = _get_table_type(values["figure"]) | ||||||||||||||||||
if table_type == "DataTable": | ||||||||||||||||||
Comment on lines
+64
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we do this by something other than string comparison? e.g. change |
||||||||||||||||||
return _set_actions(v, values, "active_cell") | ||||||||||||||||||
elif table_type == "AgGrid": | ||||||||||||||||||
return _set_actions(v, values, "cellClicked") | ||||||||||||||||||
else: | ||||||||||||||||||
raise ValueError(f"Table type {table_type} not supported.") | ||||||||||||||||||
|
||||||||||||||||||
# Convenience wrapper/syntactic sugar. | ||||||||||||||||||
def __call__(self, **kwargs): | ||||||||||||||||||
kwargs.setdefault("data_frame", data_manager._get_component_data(self.id)) | ||||||||||||||||||
|
@@ -61,13 +85,7 @@ def __getitem__(self, arg_name: str): | |||||||||||||||||
@_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) | ||||||||||||||||||
underlying_table_object, table_type = _get_table_type(self.figure) | ||||||||||||||||||
|
||||||||||||||||||
if not hasattr(underlying_table_object, "id"): | ||||||||||||||||||
raise ValueError( | ||||||||||||||||||
|
@@ -76,6 +94,10 @@ def pre_build(self): | |||||||||||||||||
) | ||||||||||||||||||
|
||||||||||||||||||
self._callable_object_id = underlying_table_object.id | ||||||||||||||||||
self._table_type = table_type | ||||||||||||||||||
# Idea: fetch it from the functions attributes? Or just hard-code it here? | ||||||||||||||||||
# Can check difference between AGGrid and dashtable because we call it already | ||||||||||||||||||
# Once we recognize, two ways to go: 1) slightly change model properties 2) inject dash dependencies, | ||||||||||||||||||
|
||||||||||||||||||
def build(self): | ||||||||||||||||||
return dcc.Loading( | ||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like implementing a solution for this would require a lot of development effort. However, this is something we should enable in case of increasing the flexibility.
Enabling filter interaction function to be customisable looks like it should be considered similarly as predefined action (e.g. taken into account in the filtering or parameterisation process).
Does this also mean we need to enable some kind of custom_filter/custom_parameter (e.g. users want to implement custom filer component with custom filer action e.g. range_data_picker_filtering)?
I keep coming across this question these days and it seems like something we should look into very soon. As it looks like a big feature, I suggest considering it separately from this PR. Maybe we should discuss it before this PR merges (just in case to avoid breaking changes in the future).