Skip to content

Commit

Permalink
[Feat] Recognise controls outside page.controls (#903)
Browse files Browse the repository at this point in the history
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Li Nguyen <[email protected]>
Co-authored-by: Maximilian Schulz <[email protected]>
  • Loading branch information
4 people authored Dec 2, 2024
1 parent 57faa1e commit 14b1aad
Show file tree
Hide file tree
Showing 24 changed files with 319 additions and 455 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
-->

<!--
### Highlights ✨
- A bullet item for the Highlights ✨ category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Removed
- A bullet item for the Removed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Added
- A bullet item for the Added category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
-->

### Changed

- Custom controls can be nested arbitrarily deep inside `Page.controls`. ([#903](https://github.com/mckinsey/vizro/pull/903))

<!--
### Deprecated
- A bullet item for the Deprecated category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Fixed
- A bullet item for the Fixed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Security
- A bullet item for the Security category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
265 changes: 43 additions & 222 deletions vizro-core/examples/scratch_dev/app.py
Original file line number Diff line number Diff line change
@@ -1,248 +1,69 @@
"""Dev app to try things out."""
from typing import List, Literal

import time
import yaml

import dash
import pandas as pd
from flask_caching import Cache
from dash import html

import vizro.models as vm
import vizro.plotly.express as px
from vizro import Vizro
from vizro.managers import data_manager
from functools import partial

print("INITIALIZING")
from vizro.models.types import ControlType

SPECIES_COLORS = {"setosa": "#00b4ff", "versicolor": "#ff9222", "virginica": "#3949ab"}
BAR_CHART_CONF = dict(x="species", color="species", color_discrete_map=SPECIES_COLORS)
SCATTER_CHART_CONF = dict(x="sepal_length", y="petal_length", color="species", color_discrete_map=SPECIES_COLORS)
df_gapminder = px.data.gapminder()


def load_from_file(filter_column=None, parametrized_species=None):
# Load the full iris dataset
df = px.data.iris()
df["date_column"] = pd.date_range(start=pd.to_datetime("2024-01-01"), periods=len(df), freq="D")
class ControlGroup(vm.VizroBaseModel):
"""Container to group controls."""

with open("data.yaml", "r") as file:
data = {
"setosa": 0,
"versicolor": 0,
"virginica": 0,
"min": 0,
"max": 10,
"date_min": "2024-01-01",
"date_max": "2024-05-29",
}
data.update(yaml.safe_load(file) or {})
type: Literal["control_group"] = "control_group"
title: str
controls: List[ControlType] = []

if filter_column == "species":
df = pd.concat(
objs=[
df[df[filter_column] == "setosa"].head(data["setosa"]),
df[df[filter_column] == "versicolor"].head(data["versicolor"]),
df[df[filter_column] == "virginica"].head(data["virginica"]),
],
ignore_index=True,
def build(self):
return html.Div(
[html.H4(self.title), html.Hr()] + [control.build() for control in self.controls],
)
elif filter_column == "sepal_length":
df = df[df[filter_column].between(data["min"], data["max"], inclusive="both")]
elif filter_column == "date_column":
date_min = pd.to_datetime(data["date_min"])
date_max = pd.to_datetime(data["date_max"])
df = df[df[filter_column].between(date_min, date_max, inclusive="both")]
else:
raise ValueError("Invalid filter_column")

if parametrized_species:
df = df[df["species"].isin(parametrized_species)]

return df


data_manager["load_from_file_species"] = partial(load_from_file, filter_column="species")
data_manager["load_from_file_sepal_length"] = partial(load_from_file, filter_column="sepal_length")
data_manager["load_from_file_date_column"] = partial(load_from_file, filter_column="date_column")


# TODO-DEV: Turn on/off caching to see how it affects the app.
# data_manager.cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 10})


homepage = vm.Page(
title="Homepage",
components=[
vm.Card(text="This is the homepage."),
],
)

page_1 = vm.Page(
title="Dynamic vs Static filter",
components=[
vm.Graph(
id="p1-G-1",
figure=px.bar(data_frame="load_from_file_species", **BAR_CHART_CONF),
),
vm.Graph(
id="p1-G-2",
figure=px.scatter(data_frame=px.data.iris(), **SCATTER_CHART_CONF),
),
],
controls=[
vm.Filter(id="p1-F-1", column="species", targets=["p1-G-1"], selector=vm.Dropdown(title="Dynamic filter")),
vm.Filter(id="p1-F-2", column="species", targets=["p1-G-2"], selector=vm.Dropdown(title="Static filter")),
vm.Parameter(
targets=["p1-G-1.x", "p1-G-2.x"],
selector=vm.RadioItems(options=["species", "sepal_width"], title="Simple X-axis parameter"),
),
],
)


page_2 = vm.Page(
title="Categorical dynamic selectors",
components=[
vm.Graph(
id="p2-G-1",
figure=px.bar(data_frame="load_from_file_species", **BAR_CHART_CONF),
),
],
controls=[
vm.Filter(id="p2-F-1", column="species", selector=vm.Dropdown()),
vm.Filter(id="p2-F-2", column="species", selector=vm.Dropdown(multi=False)),
vm.Filter(id="p2-F-3", column="species", selector=vm.Checklist()),
vm.Filter(id="p2-F-4", column="species", selector=vm.RadioItems()),
vm.Parameter(
targets=["p2-G-1.x"],
selector=vm.RadioItems(
options=["species", "sepal_width"], value="species", title="Simple X-axis parameter"
),
),
],
)


page_3 = vm.Page(
title="Numerical dynamic selectors",
components=[
vm.Graph(
id="p3-G-1",
figure=px.bar(data_frame="load_from_file_sepal_length", **BAR_CHART_CONF),
),
],
controls=[
vm.Filter(id="p3-F-1", column="sepal_length", selector=vm.Slider()),
vm.Filter(id="p3-F-2", column="sepal_length", selector=vm.RangeSlider()),
vm.Parameter(
targets=["p3-G-1.x"],
selector=vm.RadioItems(
options=["species", "sepal_width"], value="species", title="Simple X-axis parameter"
),
),
],
)

page_4 = vm.Page(
title="[TO BE DONE IN THE FOLLOW UP PR] Temporal dynamic selectors",
components=[
vm.Graph(
id="p4-G-1",
figure=px.bar(data_frame="load_from_file_date_column", **BAR_CHART_CONF),
),
],
controls=[
vm.Filter(id="p4-F-1", column="date_column", selector=vm.DatePicker(range=False)),
vm.Filter(id="p4-F-2", column="date_column", selector=vm.DatePicker()),
vm.Parameter(
targets=["p4-G-1.x"],
selector=vm.RadioItems(
options=["species", "sepal_width"], value="species", title="Simple X-axis parameter"
),
),
],
)
vm.Page.add_type("controls", ControlGroup)

page_5 = vm.Page(
title="Parametrised dynamic selectors",
page1 = vm.Page(
title="Relationship Analysis",
components=[
vm.Graph(
id="p5-G-1",
figure=px.bar(data_frame="load_from_file_species", **BAR_CHART_CONF),
),
vm.Graph(id="scatter", figure=px.scatter(df_gapminder, x="gdpPercap", y="lifeExp", size="pop")),
],
controls=[
vm.Filter(id="p5-F-1", column="species", targets=["p5-G-1"], selector=vm.Checklist()),
vm.Parameter(
targets=[
"p5-G-1.data_frame.parametrized_species",
# TODO: Uncomment the following target and see the magic :D
# Is this the indicator that parameter.targets prop has to support 'target' definition without the '.'?
# "p5-F-1.",
ControlGroup(
title="Group A",
controls=[
vm.Parameter(
id="this",
targets=["scatter.x"],
selector=vm.Dropdown(
options=["lifeExp", "gdpPercap", "pop"], multi=False, value="gdpPercap", title="Choose x-axis"
),
),
vm.Parameter(
targets=["scatter.y"],
selector=vm.Dropdown(
options=["lifeExp", "gdpPercap", "pop"], multi=False, value="lifeExp", title="Choose y-axis"
),
),
],
selector=vm.Dropdown(
options=["setosa", "versicolor", "virginica"], multi=True, title="Parametrized species"
),
),
vm.Parameter(
targets=[
"p5-G-1.x",
# TODO: Uncomment the following target and see the magic :D
# "p5-F-1.",
ControlGroup(
title="Group B",
controls=[
vm.Parameter(
targets=["scatter.size"],
selector=vm.Dropdown(
options=["lifeExp", "gdpPercap", "pop"], multi=False, value="pop", title="Choose bubble size"
),
)
],
selector=vm.RadioItems(
options=["species", "sepal_width"], value="species", title="Simple X-axis parameter"
),
),
],
)


page_6 = vm.Page(
title="Page to test things out",
components=[
vm.Graph(id="graph_dynamic", figure=px.bar(data_frame="load_from_file_species", **BAR_CHART_CONF)),
vm.Graph(
id="graph_static",
figure=px.scatter(data_frame=px.data.iris(), **SCATTER_CHART_CONF),
),
],
controls=[
vm.Filter(
id="filter_container_id",
column="species",
targets=["graph_dynamic"],
# targets=["graph_static"],
# selector=vm.Dropdown(id="filter_id"),
# selector=vm.Dropdown(id="filter_id", value=["setosa"]),
# selector=vm.Checklist(id="filter_id"),
# selector=vm.Checklist(id="filter_id", value=["setosa"]),
# TODO-BUG: vm.Dropdown(multi=False) Doesn't work if value is cleared. The persistence storage become
# "null" and our placeholder component dmc.DateRangePicker can't process null value. It expects a value or
# a list of values.
# SOLUTION -> Create the "Universal Vizro placeholder component".
# TEMPORARY SOLUTION -> set clearable=False for the dynamic Dropdown(multi=False)
# selector=vm.Dropdown(id="filter_id", multi=False),
# selector=vm.Dropdown(id="filter_id", multi=False, value="setosa"),
# selector=vm.RadioItems(id="filter_id"),
# selector=vm.RadioItems(id="filter_id", value="setosa"),
# selector=vm.Slider(id="filter_id"),
# selector=vm.Slider(id="filter_id", value=5),
# selector=vm.RangeSlider(id="filter_id"),
# selector=vm.RangeSlider(id="filter_id", value=[5, 7]),
),
vm.Parameter(
targets=["graph_dynamic.x"],
selector=vm.RadioItems(options=["species", "sepal_width"], title="Simple X-axis parameter"),
),
],
)

dashboard = vm.Dashboard(pages=[homepage, page_1, page_2, page_3, page_4, page_5, page_6])
dashboard = vm.Dashboard(pages=[page1])

if __name__ == "__main__":
app = Vizro().build(dashboard)

print("RUNNING\n")

app.run(dev_tools_hot_reload=False)
Vizro().build(dashboard).run()
7 changes: 3 additions & 4 deletions vizro-core/src/vizro/_vizro.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from collections.abc import Iterable
from contextlib import suppress
from pathlib import Path, PurePosixPath
from typing import TYPE_CHECKING, TypedDict
from typing import TYPE_CHECKING, TypedDict, cast

import dash
import plotly.io as pio
Expand Down Expand Up @@ -145,13 +145,12 @@ def _pre_build():
# Any models that are created during the pre-build process *will not* themselves have pre_build run on them.
# In future may add a second pre_build loop after the first one.

# model_manager results is wrapped into a list to avoid RuntimeError: dictionary changed size during iteration
for _, filter_obj in list(model_manager._items_with_type(Filter)):
for filter in cast(Iterable[Filter], model_manager._get_models(Filter)):
# Run pre_build on all filters first, then on all other models. This handles dependency between Filter
# and Page pre_build and ensures that filters are pre-built before the Page objects that use them.
# This is important because the Page pre_build method checks whether filters are dynamic or not, which is
# defined in the filter's pre_build method.
filter_obj.pre_build()
filter.pre_build()
for model_id in set(model_manager):
model = model_manager[model_id]
if hasattr(model, "pre_build") and not isinstance(model, Filter):
Expand Down
13 changes: 10 additions & 3 deletions vizro-core/src/vizro/actions/_action_loop/_action_loop.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
"""The action loop creates all the required action callbacks and its components."""

from collections.abc import Iterable
from typing import cast

from dash import html

from vizro.actions._action_loop._action_loop_utils import _get_actions_on_registered_pages
from vizro.actions._action_loop._build_action_loop_callbacks import _build_action_loop_callbacks
from vizro.actions._action_loop._get_action_loop_components import _get_action_loop_components
from vizro.managers import model_manager
from vizro.models import Action


class ActionLoop:
Expand Down Expand Up @@ -37,5 +41,8 @@ def _build_actions_models():
List of required components for each `Action` in the `Dashboard` e.g. list[dcc.Download]
"""
actions = _get_actions_on_registered_pages()
return html.Div([action.build() for action in actions], id="app_action_models_components_div", hidden=True)
return html.Div(
[action.build() for action in cast(Iterable[Action], model_manager._get_models(Action))],
id="app_action_models_components_div",
hidden=True,
)
Loading

0 comments on commit 14b1aad

Please sign in to comment.