Skip to content

Commit

Permalink
Enable Flex inside Container and Form
Browse files Browse the repository at this point in the history
  • Loading branch information
huong-li-nguyen committed Mar 5, 2025
1 parent 780d8fe commit 7ec6252
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 32 deletions.
81 changes: 78 additions & 3 deletions vizro-core/examples/scratch_dev/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
page_default = vm.Page(
title="No Layout",
components=[
vm.Card(text="""# Good morning!"""),
vm.Graph(
title="Where do we get more tips?",
figure=px.bar(tips, y="tip", x="day"),
Expand All @@ -31,8 +32,9 @@

page_grid = vm.Page(
title="Grid",
layout=vm.Layout(grid=[[0, 1], [2, 2]]),
layout=vm.Layout(grid=[[0, -1], [1, 2], [3, 3]]),
components=[
vm.Card(text="""# Good morning!"""),
vm.Graph(
title="Where do we get more tips?",
figure=px.bar(tips, y="tip", x="day"),
Expand All @@ -49,8 +51,9 @@
controls=[vm.Filter(column="day")],
)


page_flex = vm.Page(
title="Flex",
title="Flex without card",
layout=vm.Flex(),
components=[
vm.Graph(
Expand All @@ -69,8 +72,80 @@
controls=[vm.Filter(column="day")],
)

page_flex_card = vm.Page(
title="Flex with card - why?",
layout=vm.Flex(),
components=[
vm.Card(text="""# Good morning!"""),
vm.Graph(
title="Where do we get more tips?",
figure=px.bar(tips, y="tip", x="day"),
),
vm.Graph(
title="Is the average driven by a few outliers?",
figure=px.violin(tips, y="tip", x="day", color="day", box=True),
),
vm.Graph(
title="Which group size is more profitable?",
figure=px.density_heatmap(tips, x="day", y="size", z="tip", histfunc="avg", text_auto="$.2f"),
),
],
controls=[vm.Filter(column="day")],
)

container_flex = vm.Page(
title="Container with flex",
components=[
vm.Container(
title="Container inside grid with Flex",
layout=vm.Flex(),
components=[
vm.Graph(
title="Where do we get more tips?",
figure=px.bar(tips, y="tip", x="day"),
),
vm.Graph(
title="Is the average driven by a few outliers?",
figure=px.violin(tips, y="tip", x="day", color="day", box=True),
),
vm.Graph(
title="Which group size is more profitable?",
figure=px.density_heatmap(tips, x="day", y="size", z="tip", histfunc="avg", text_auto="$.2f"),
),
],
)
],
controls=[vm.Filter(column="day")],
)

container_flex_card = vm.Page(
title="Container with flex and card",
components=[
vm.Container(
title="Container inside grid with Flex with card",
layout=vm.Flex(),
components=[
vm.Card(text="""# Good morning!"""),
vm.Graph(
title="Where do we get more tips?",
figure=px.bar(tips, y="tip", x="day"),
),
vm.Graph(
title="Is the average driven by a few outliers?",
figure=px.violin(tips, y="tip", x="day", color="day", box=True),
),
vm.Graph(
title="Which group size is more profitable?",
figure=px.density_heatmap(tips, x="day", y="size", z="tip", histfunc="avg", text_auto="$.2f"),
),
],
)
],
controls=[vm.Filter(column="day")],
)

dashboard = vm.Dashboard(
pages=[page_default, page_grid, page_flex],
pages=[page_default, page_grid, page_flex, page_flex_card, container_flex, container_flex_card],
title="Tips Analysis Dashboard",
)

Expand Down
25 changes: 16 additions & 9 deletions vizro-core/src/vizro/models/_components/_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from vizro.models._components.form import Checklist, Dropdown, RadioItems, RangeSlider, Slider
from vizro.models._layout import set_layout
from vizro.models._models_utils import _log_call, check_captured_callable_model
from vizro.models.types import _FormComponentType
from vizro.models.types import LayoutType, _FormComponentType

if TYPE_CHECKING:
from vizro.models import Layout
Expand All @@ -28,7 +28,7 @@ class Form(VizroBaseModel):
type: Literal["form"] = "form"
# TODO[mypy], see: https://github.com/pydantic/pydantic/issues/156 for components field
components: conlist(Annotated[_FormComponentType, BeforeValidator(check_captured_callable_model)], min_length=1) # type: ignore[valid-type]
layout: Annotated[Optional[Layout], AfterValidator(set_layout), Field(default=None, validate_default=True)]
layout: Annotated[Optional[LayoutType], AfterValidator(set_layout), Field(default=None, validate_default=True)]

@_log_call
def pre_build(self):
Expand All @@ -42,11 +42,18 @@ def pre_build(self):

@_log_call
def build(self):
self.layout = cast(
Layout, # cannot actually be None if you check components and layout field together
self.layout,
)
return html.Div(id=self.id, children=self._build_inner_layout())

def _build_inner_layout(self):
"""Builds inner layout and adds components to grid or flex."""
# Below added to remove mypy error - cannot actually be None if you check components and layout field together
self.layout = cast(Layout, self.layout)

components_container = self.layout.build()
for component_idx, component in enumerate(self.components):
components_container[f"{self.layout.id}_{component_idx}"].children = component.build()
return html.Div(id=self.id, children=components_container)
if isinstance(self.layout, Layout):
for component_idx, component in enumerate(self.components):
components_container[f"{self.layout.id}_{component_idx}"].children = component.build()
else:
components_container.children = [component.build() for component in self.components]

return components_container
31 changes: 16 additions & 15 deletions vizro-core/src/vizro/models/_components/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from vizro.models import VizroBaseModel
from vizro.models._layout import set_layout
from vizro.models._models_utils import _log_call, check_captured_callable_model
from vizro.models.types import ComponentType
from vizro.models.types import ComponentType, LayoutType

if TYPE_CHECKING:
from vizro.models import Layout
Expand All @@ -36,7 +36,7 @@ class Container(VizroBaseModel):
min_length=1,
)
title: str = Field(description="Title to be displayed.")
layout: Annotated[Optional[Layout], AfterValidator(set_layout), Field(default=None, validate_default=True)]
layout: Annotated[Optional[LayoutType], AfterValidator(set_layout), Field(default=None, validate_default=True)]
variant: Literal["plain", "filled", "outlined"] = Field(
default="plain",
description="Predefined styles to choose from. Options are `plain`, `filled` or `outlined`."
Expand All @@ -49,6 +49,14 @@ def build(self):
# It needs to be properly designed and tested out (margins have to be added etc.).
# Below corresponds to bootstrap utility classnames, while 'bg-container' is introduced by us.
# See: https://getbootstrap.com/docs/4.0/utilities
# Title is not displayed if Container is inside Tabs using CSS combinators (only applies to outer container)
# Other options we might want to consider in the future to hide the title:
# 1) Argument inside Container.build that flags if used inside Tabs, then sets hidden attribute for the heading
# or just doesn't supply the element at all
# 2) Logic inside Tabs.build that sets hidden=True for the heading or uses del to remove the heading via
# providing an ID to the heading and accessing it in the component tree
# 3) New field in Container like short_title to allow tab label to be set independently
# Below added to remove mypy error - cannot actually be None if you check components and layout field together
variants = {"plain": "", "filled": "bg-container p-3", "outlined": "border p-3"}

return dbc.Container(
Expand All @@ -63,21 +71,14 @@ def build(self):

def _build_inner_layout(self):
"""Builds inner layout and assigns components to grid position."""
# Title is not displayed if Container is inside Tabs using CSS combinators (only applies to outer container)
# Other options we might want to consider in the future to hide the title:
# 1) Argument inside Container.build that flags if used inside Tabs, then sets hidden attribute for the heading
# or just doesn't supply the element at all
# 2) Logic inside Tabs.build that sets hidden=True for the heading or uses del to remove the heading via
# providing an ID to the heading and accessing it in the component tree
# 3) New field in Container like short_title to allow tab label to be set independently
from vizro.models import Layout
self.layout = cast(Layout, self.layout)

self.layout = cast(
Layout, # cannot actually be None if you check components and layout field together
self.layout,
)
components_container = self.layout.build()
for component_idx, component in enumerate(self.components):
components_container[f"{self.layout.id}_{component_idx}"].children = component.build()
if isinstance(self.layout, Layout):
for component_idx, component in enumerate(self.components):
components_container[f"{self.layout.id}_{component_idx}"].children = component.build()
else:
components_container.children = [component.build() for component in self.components]

return components_container
2 changes: 1 addition & 1 deletion vizro-core/src/vizro/models/_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def _get_unique_grid_component_ids(grid: list[list[int]]):

# Validators for reuse
def set_layout(layout, info: ValidationInfo):
from vizro.models import Layout, Flex
from vizro.models import Flex, Layout

# No validation for Flex layout
if isinstance(layout, Flex):
Expand Down
14 changes: 10 additions & 4 deletions vizro-core/src/vizro/models/_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,18 @@ def pre_build(self):

@_log_call
def build(self) -> _PageBuildType:
# Build control panel
controls_content = [control.build() for control in self.controls]
control_panel = html.Div(id="control-panel", children=controls_content, hidden=not controls_content)

# Build layout with components
components_container = self._build_inner_layout()
components_container.children.append(dcc.Store(id=f"{ON_PAGE_LOAD_ACTION_PREFIX}_trigger_{self.id}"))
components_container.id = "page-components"
return html.Div([control_panel, components_container])

def _build_inner_layout(self):
"""Builds inner layout and adds components to grid or flex."""
# Below added to remove mypy error - cannot actually be None if you check components and layout field together
self.layout = cast(Layout, self.layout)

Expand All @@ -148,7 +157,4 @@ def build(self) -> _PageBuildType:
else:
components_container.children = [component.build() for component in self.components]

# Page specific CSS ID and Stores
components_container.children.append(dcc.Store(id=f"{ON_PAGE_LOAD_ACTION_PREFIX}_trigger_{self.id}"))
components_container.id = "page-components"
return html.Div([control_panel, components_container])
return components_container

0 comments on commit 7ec6252

Please sign in to comment.