Skip to content

Commit

Permalink
[Bug] Make single dynamic dropdown clearable (#915)
Browse files Browse the repository at this point in the history
  • Loading branch information
petar-qb authored Dec 4, 2024
1 parent 4e0ddb6 commit c18a6eb
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 58 deletions.
47 changes: 47 additions & 0 deletions vizro-core/changelog.d/20241203_133819_petar_pejovic.md
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
- A bullet item for the Changed 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))
-->
<!--
### 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

- Ensure the single-select dropdown value can be cleared when used as a dynamic filter. ([#915](https://github.com/mckinsey/vizro/pull/915))

<!--
### 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: 222 additions & 43 deletions vizro-core/examples/scratch_dev/app.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,248 @@
from typing import List, Literal
"""Dev app to try things out."""

from dash import html
import time
import yaml

import dash
import pandas as pd
from flask_caching import Cache

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

print("INITIALIZING")

df_gapminder = px.data.gapminder()
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)


class ControlGroup(vm.VizroBaseModel):
"""Container to group controls."""
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")

type: Literal["control_group"] = "control_group"
title: str
controls: List[ControlType] = []
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 {})

def build(self):
return html.Div(
[html.H4(self.title), html.Hr()] + [control.build() for control in self.controls],
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,
)
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"
),
),
],
)


vm.Page.add_type("controls", ControlGroup)
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"
),
),
],
)

page1 = vm.Page(
title="Relationship Analysis",
page_5 = vm.Page(
title="Parametrised dynamic selectors",
components=[
vm.Graph(id="scatter", figure=px.scatter(df_gapminder, x="gdpPercap", y="lifeExp", size="pop")),
vm.Graph(
id="p5-G-1",
figure=px.bar(data_frame="load_from_file_species", **BAR_CHART_CONF),
),
],
controls=[
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"
),
),
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.",
],
selector=vm.Dropdown(
options=["setosa", "versicolor", "virginica"], multi=True, title="Parametrized species"
),
),
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"
),
)
vm.Parameter(
targets=[
"p5-G-1.x",
# TODO: Uncomment the following target and see the magic :D
# "p5-F-1.",
],
selector=vm.RadioItems(
options=["species", "sepal_width"], value="species", title="Simple X-axis parameter"
),
),
],
)

dashboard = vm.Dashboard(pages=[page1])

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

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

print("RUNNING\n")

app.run(dev_tools_hot_reload=False)
3 changes: 2 additions & 1 deletion vizro-core/src/vizro/models/_components/form/checklist.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ def __call__(self, options):

def _build_dynamic_placeholder(self):
if self.value is None:
self.value = [get_options_and_default(self.options, multi=True)[1]]
_, default_value = get_options_and_default(self.options, multi=True)
self.value = [default_value]

return self.__call__(self.options)

Expand Down
14 changes: 1 addition & 13 deletions vizro-core/src/vizro/models/_components/form/dropdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from pydantic import Field, PrivateAttr, StrictBool, root_validator, validator

import dash_bootstrap_components as dbc
import dash_mantine_components as dmc

from vizro.models import Action, VizroBaseModel
from vizro.models._action._actions_chain import _action_validator_factory
Expand Down Expand Up @@ -115,18 +114,7 @@ def _build_dynamic_placeholder(self):
_, default_value = get_options_and_default(self.options, self.multi)
self.value = default_value

# TODO-NEXT: Replace this with the "universal Vizro placeholder" component.
return html.Div(
children=[
html.Legend(children=self.title, className="form-label") if self.title else None,
dmc.DateRangePicker(
id=self.id,
value=self.value,
persistence=True,
persistence_type="session",
),
]
)
return self.__call__(self.options)

@_log_call
def build(self):
Expand Down
3 changes: 2 additions & 1 deletion vizro-core/src/vizro/models/_components/form/radio_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ def __call__(self, options):

def _build_dynamic_placeholder(self):
if self.value is None:
self.value = get_options_and_default(self.options, multi=False)[1]
_, default_value = get_options_and_default(self.options, multi=False)
self.value = default_value

return self.__call__(self.options)

Expand Down

0 comments on commit c18a6eb

Please sign in to comment.