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

AGGrid implementation #260

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions vizro-core/examples/default/app_dev.py
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()
67 changes: 67 additions & 0 deletions vizro-core/examples/default/scratch_3.py
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)


3 changes: 2 additions & 1 deletion vizro-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ dependencies = [
"tornado>=6.3.2", # not directly required, pinned by Snyk to avoid a vulnerability: https://security.snyk.io/vuln/SNYK-PYTHON-TORNADO-5537286
"setuptools>=65.5.1", # not directly required, pinned by Snyk to avoid a vulnerability: https://security.snyk.io/vuln/SNYK-PYTHON-SETUPTOOLS-3180412
"werkzeug>=3.0.1", # not directly required, pinned by Snyk to avoid a vulnerability: https://security.snyk.io/vuln/SNYK-PYTHON-WERKZEUG-6035177
"MarkupSafe" # required to sanitize user input
"MarkupSafe", # required to sanitize user input,
"dash-ag-grid"
]
description = "Vizro is a package to facilitate visual analytics."
dynamic = ["version"]
Expand Down
33 changes: 31 additions & 2 deletions vizro-core/src/vizro/actions/_actions_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def _get_parent_vizro_model(_underlying_callable_object_id: str) -> VizroBaseMod
)


def _apply_table_filter_interaction(
def _apply_dashtable_filter_interaction(
data_frame: pd.DataFrame, target: str, ctd_filter_interaction: Dict[str, CallbackTriggerDict]
) -> pd.DataFrame:
ctd_active_cell = ctd_filter_interaction["active_cell"]
Expand All @@ -133,12 +133,34 @@ def _apply_table_filter_interaction(
return data_frame


def _apply_aggrid_filter_interaction(
data_frame: pd.DataFrame, target: str, ctd_filter_interaction: Dict[str, CallbackTriggerDict]
) -> pd.DataFrame:
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


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:
# This would also have to be abstracted outside the function, but is a little more complicated
# essentially we have to go: ctd_filter_interaction[<wildcard>]["id"] -> get parent Vizro Table -> get function -> get attributes
Comment on lines +162 to +163
Copy link
Contributor

@petar-qb petar-qb Jan 17, 2024

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).

if "clickData" in ctd_filter_interaction:
data_frame = _apply_graph_filter_interaction(
data_frame=data_frame,
Expand All @@ -147,7 +169,14 @@ def _apply_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 = _apply_dashtable_filter_interaction(
data_frame=data_frame,
target=target,
ctd_filter_interaction=ctd_filter_interaction,
)

if "cellClicked" in ctd_filter_interaction:
data_frame = _apply_aggrid_filter_interaction(
data_frame=data_frame,
target=target,
ctd_filter_interaction=ctd_filter_interaction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from vizro.actions import _on_page_load, _parameter, export_data, filter_interaction
from vizro.managers import data_manager, model_manager
from vizro.managers._model_manager import ModelID
from vizro.models import Action, Page, Table, VizroBaseModel
from vizro.models import Action, Page, VizroBaseModel
from vizro.models._action._actions_chain import ActionsChain
from vizro.models._controls import Filter, Parameter
from vizro.models.types import ControlType
Expand Down Expand Up @@ -108,18 +108,13 @@ def _get_inputs_of_figure_interactions(
for action in figure_interactions_on_page:
# TODO: Consider do we want to move the following logic into Model implementation
triggered_model = _get_triggered_model(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",
),
}
)
# This also needs to be done for the graph callables, but that should not be hard
# I think this would have made more sense if we had one model for tables and graphs
# But really this seems to rather demand an object oriented approach

if hasattr(triggered_model, "_table_type"): # not check this, put this configuration inside the models
inputs.append(triggered_model.figure.action_info["filter_interaction_input"](triggered_model))

else:
inputs.append(
{
Expand Down
Loading
Loading