-
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
Closed
Closed
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
96b6259
Minimal viable AGGrid implementation
maxschulz-COL a74a28f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 4878865
Improve MVP
maxschulz-COL e27c329
Merge branch 'main' into feature/enable_AG_grid
maxschulz-COL f9e14cb
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 7d370c2
Remove super().__init__ and property
maxschulz-COL 2c0e0a7
MVP abstraction of implementation details
maxschulz-COL 6a5e382
Thoughts on callable classes
maxschulz-COL File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# REQUIREMENTS & THOUGHTS | ||
# User must be able to create a Python function (=`python_function`) and at most decorate it -> for a plotly chart, absolutely, for tables and grid as well | ||
# Only fiddle with callable classes and attributes, if you want custom behaviour/new component in the callable | ||
|
||
# If that is the case, then the ONE decorator must tranform the `python_function` (i.e. the plain function that returns e.g. a go.Figure) into a CallableClass with appropriate attributes | ||
# if the user uses a standard callable (e.g. OUR dash_datatable, or technically any px.function), that callable could already be the CallableClass | ||
|
||
# Few ideas | ||
# Would it make sense to have a new mode? | ||
# New arguments in the capture decorator | ||
import functools | ||
from vizro.models.types import CapturedCallable | ||
import inspect | ||
|
||
# This is our current capture decorator in pure form (except the attempted change), something like this must do the transformation of python_function into a CallableClass given the requirements above | ||
class capture: | ||
def __call__(self, func, /): | ||
@functools.wraps(func) | ||
def wrapped(*args, **kwargs): | ||
# Something like this must be done, but currently 2nd line fails - see below | ||
callable_class_instance_of_python_function = CallableClass(func) | ||
return CapturedCallable(callable_class_instance_of_python_function, *args, **kwargs) | ||
|
||
return wrapped | ||
|
||
# This is my first attempt at a CallableClass, but it fails because of the *args in the __call__ method | ||
# This is where the action info, it the information about specific implementations of actions for that callable would be stored, if we could make the instantiation work in the decorator | ||
class CallableClass: | ||
def __init__(self, func): | ||
self.func = func | ||
def __call__(self,*args, **kwargs): | ||
return self.func(*args, **kwargs) | ||
# Started fiddling with the signature bit | ||
# def __signature__(self): | ||
# return inspect.signature(self.func) | ||
|
||
|
||
# This is the python_function that we want to decorate/transform | ||
def python_function(a, b=2): | ||
print("This is returning say a dash.datatable!",a,b) | ||
|
||
callable_class_instance_of_python_function = CallableClass(python_function) # creating the equi | ||
callable_class_instance_of_python_function(4,5) # this behaves like a normal `python_function` | ||
|
||
@capture() | ||
def another_python_function_but_decorated(a=1, b=2): | ||
print("Whoo!",a,b) | ||
|
||
try: | ||
another_python_function_but_decorated(2, b=3) # this does not work, for the reason shown below (it complains about *args) | ||
except Exception as e: | ||
print("Exception",e) | ||
|
||
# One can comment out the other line to see why the CapturedCallable does not work | ||
parameters = inspect.signature(callable_class_instance_of_python_function.func).parameters # this works | ||
# parameters = inspect.signature(function).parameters # this does not as it complains about *args | ||
|
||
invalid_params = { | ||
param.name | ||
for param in parameters.values() | ||
if param.kind in [inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.VAR_POSITIONAL] | ||
} | ||
|
||
print("Params",parameters) | ||
print("Invalid",invalid_params) | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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).