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] Recognise controls outside page.controls #903

Merged
merged 18 commits into from
Dec 2, 2024
Merged
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
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))
petar-qb marked this conversation as resolved.
Show resolved Hide resolved

<!--
### 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))],
antonymilne marked this conversation as resolved.
Show resolved Hide resolved
id="app_action_models_components_div",
hidden=True,
)
Loading
Loading