diff --git a/vizro-core/examples/scratch_dev/app.py b/vizro-core/examples/scratch_dev/app.py index 5e4ea3559..60ca65932 100644 --- a/vizro-core/examples/scratch_dev/app.py +++ b/vizro-core/examples/scratch_dev/app.py @@ -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"), @@ -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"), @@ -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( @@ -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", ) diff --git a/vizro-core/src/vizro/models/_components/_form.py b/vizro-core/src/vizro/models/_components/_form.py index 97dc93a61..0bf71239c 100644 --- a/vizro-core/src/vizro/models/_components/_form.py +++ b/vizro-core/src/vizro/models/_components/_form.py @@ -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 @@ -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): @@ -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 diff --git a/vizro-core/src/vizro/models/_components/container.py b/vizro-core/src/vizro/models/_components/container.py index 45e44b5da..1c7e6d9ed 100644 --- a/vizro-core/src/vizro/models/_components/container.py +++ b/vizro-core/src/vizro/models/_components/container.py @@ -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 @@ -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`." @@ -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( @@ -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 diff --git a/vizro-core/src/vizro/models/_layout.py b/vizro-core/src/vizro/models/_layout.py index 326ee4dd4..2cb5e1355 100644 --- a/vizro-core/src/vizro/models/_layout.py +++ b/vizro-core/src/vizro/models/_layout.py @@ -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): diff --git a/vizro-core/src/vizro/models/_page.py b/vizro-core/src/vizro/models/_page.py index 5751ecc0c..cec2c2875 100644 --- a/vizro-core/src/vizro/models/_page.py +++ b/vizro-core/src/vizro/models/_page.py @@ -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) @@ -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