Skip to content

Commit

Permalink
[Feat] Enable provision of theme-specific logos (#745)
Browse files Browse the repository at this point in the history
Co-authored-by: Jo Stichbury <[email protected]>
  • Loading branch information
huong-li-nguyen and stichbury authored Sep 27, 2024
1 parent 0e920f9 commit a5a53f9
Show file tree
Hide file tree
Showing 12 changed files with 196 additions and 36 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

- Theme switcher can now switch between light and dark logos. ([#745](https://github.com/mckinsey/vizro/pull/745))

<!--
### 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
- 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))
-->
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 21 additions & 3 deletions vizro-core/docs/pages/user-guides/assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,29 @@ For more information, see the [Dash documentation](https://dash.plotly.com/exter

## Add a logo image

Vizro automatically incorporates the dashboard logo in the top-left corner of each page if an image named
`logo.<extension>` is present within the assets folder.
### Single logo

Image types of `apng`, `avif`, `gif`, `jpeg`, `jpg`, `png`, `svg`, and `webp` are supported.
If an image named `logo.<extension>` is present in the assets folder, Vizro automatically incorporates the logo in the top-left corner of the dashboard.

**Supported image extensions:** `apng`, `avif`, `gif`, `jpeg`, `jpg`, `png`, `svg`, and `webp`

### Theme-specific logos

You can also supply two images named `logo_dark.<extension>` and `logo_light.<extension>` to switch logos
based on the theme (dark or light).

Note that both `logo_dark.<extension>` and `logo_light.<extension>` must be provided together, and they cannot be
provided if a single `logo.<extension>` is also provided for both light/dark themes.

The valid configurations are as follows:

* Single logo: Provide only `logo.<extension>`, which is used for dark and light themes.
* Theme logos: Provide both `logo_dark.<extension>` and `logo_light.<extension>` for dark/light themes.
Do not provide `logo.<extension>`.
* No logo: No logo images provided.

![Logo dark](../../assets/user_guides/assets/logo-dark.png)
![Logo light](../../assets/user_guides/assets/logo-light.png)

## Change the `assets` folder path
If you do not want to place your `assets` folder in the root directory of your app, you can
Expand Down
27 changes: 8 additions & 19 deletions vizro-core/examples/scratch_dev/app.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,23 @@
"""Dev app to try things out."""

import plotly.io as pio
import vizro.models as vm
import vizro.plotly.express as px
from vizro import Vizro

pio.templates["vizro_dark"]["layout"]["paper_bgcolor"] = "rgba(0, 0, 0, 0)"
pio.templates["vizro_light"]["layout"]["paper_bgcolor"] = "rgba(0, 0, 0, 0)"
pio.templates["vizro_dark"]["layout"]["plot_bgcolor"] = "rgba(0, 0, 0, 0)"
pio.templates["vizro_light"]["layout"]["plot_bgcolor"] = "rgba(0, 0, 0, 0)"


iris = px.data.iris()
df = px.data.iris()

page = vm.Page(
title="Page with subsections",
layout=vm.Layout(grid=[[0, 1]]),
title="My first dashboard",
components=[
vm.Container(
title="Container I",
components=[vm.Graph(figure=px.scatter(iris, x="sepal_width", y="sepal_length", color="species"))],
),
vm.Container(
title="Container II",
components=[vm.Graph(figure=px.box(iris, x="species", y="sepal_length", color="species"))],
),
vm.Graph(id="scatter_chart", figure=px.scatter(df, x="sepal_length", y="petal_width", color="species")),
vm.Graph(id="hist_chart", figure=px.histogram(df, x="sepal_width", color="species")),
],
controls=[
vm.Filter(column="species", selector=vm.Dropdown(value=["ALL"])),
],
)

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

if __name__ == "__main__":
Vizro().build(dashboard).run(debug=False)
Vizro().build(dashboard).run()
5 changes: 0 additions & 5 deletions vizro-core/examples/scratch_dev/assets/css/custom.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
#page-header {
padding-left: 8px;
}

.page-component-container {
background: #1b1e2a;
padding: 12px;
}
3 changes: 0 additions & 3 deletions vizro-core/examples/scratch_dev/assets/css/logo.svg

This file was deleted.

8 changes: 8 additions & 0 deletions vizro-core/examples/scratch_dev/assets/logo_dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions vizro-core/examples/scratch_dev/assets/logo_light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 43 additions & 4 deletions vizro-core/src/vizro/models/_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ def _all_hidden(components: List[Component]):
"nav-bar": dbc.Navbar,
"nav-panel": dbc.Nav,
"logo": html.Div,
"logo-dark": html.Div,
"logo-light": html.Div,
"control-panel": html.Div,
"page-components": html.Div,
},
Expand Down Expand Up @@ -109,19 +111,20 @@ def set_navigation_pages(cls, navigation, values):

@_log_call
def pre_build(self):
meta_image = self._infer_image("app") or self._infer_image("logo")
self._validate_logos()

# Setting order here ensures that the pages in dash.page_registry preserves the order of the List[Page].
# For now the homepage (path /) corresponds to self.pages[0].
# Note redirect_from=["/"] doesn't work and so the / route must be defined separately.
self.pages[0].path = "/"
meta_img = self._infer_image("app") or self._infer_image("logo") or self._infer_image("logo_dark")

for order, page in enumerate(self.pages):
dash.register_page(
module=page.id,
name=page.title,
description=page.description,
image=meta_image,
image=meta_img,
title=f"{self.title}: {page.title}" if self.title else page.title,
path=page.path,
order=order,
Expand Down Expand Up @@ -169,6 +172,22 @@ def build(self):
className=self.theme,
)

def _validate_logos(self):
logo_img = self._infer_image(filename="logo")
logo_dark_img = self._infer_image(filename="logo_dark")
logo_light_img = self._infer_image(filename="logo_light")

if logo_dark_img and logo_light_img:
if logo_img:
raise ValueError(
"Cannot provide `logo` together with both `logo_dark` and `logo_light`. "
"Please provide either `logo`, or both `logo_dark` and `logo_light`."
)
elif logo_dark_img or logo_light_img:
raise ValueError(
"Both `logo_dark` and `logo_light` must be provided together. Please provide either both or neither."
)

def _get_page_divs(self, page: Page) -> _PageDivsType:
# Identical across pages
dashboard_title = (
Expand All @@ -186,9 +205,18 @@ def _get_page_divs(self, page: Page) -> _PageDivsType:
),
id="settings",
)

logo_img = self._infer_image(filename="logo")
logo_dark_img = self._infer_image(filename="logo_dark")
logo_light_img = self._infer_image(filename="logo_light")

path_to_logo = get_asset_url(logo_img) if logo_img else None
path_to_logo_dark = get_asset_url(logo_dark_img) if logo_dark_img else None
path_to_logo_light = get_asset_url(logo_light_img) if logo_light_img else None

logo = html.Img(id="logo", src=path_to_logo, hidden=not path_to_logo)
logo_dark = html.Img(id="logo-dark", src=path_to_logo_dark, hidden=not path_to_logo_dark)
logo_light = html.Img(id="logo-light", src=path_to_logo_light, hidden=not path_to_logo_light)

# Shared across pages but slightly differ in content. These could possibly be done by a clientside
# callback instead.
Expand All @@ -203,11 +231,22 @@ def _get_page_divs(self, page: Page) -> _PageDivsType:
page_components = page_content["page-components"]

return html.Div(
[dashboard_title, settings, page_title, nav_bar, nav_panel, logo, control_panel, page_components]
[
dashboard_title,
settings,
page_title,
nav_bar,
nav_panel,
logo,
logo_dark,
logo_light,
control_panel,
page_components,
]
)

def _arrange_page_divs(self, page_divs: _PageDivsType):
logo_title = [page_divs["logo"], page_divs["dashboard-title"]]
logo_title = [page_divs["logo"], page_divs["logo-dark"], page_divs["logo-light"], page_divs["dashboard-title"]]
page_header_divs = [html.Div(id="logo-and-title", children=logo_title, hidden=_all_hidden(logo_title))]
left_sidebar_divs = [page_divs["nav-bar"]]
left_main_divs = [page_divs["nav-panel"], page_divs["control-panel"]]
Expand Down
19 changes: 17 additions & 2 deletions vizro-core/src/vizro/static/css/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,23 @@
margin: 0;
}

#logo {
height: 48px;
#logo,
#logo-light,
#logo-dark {
height: 100%;
}

#logo-dark,
#logo-light {
display: none;
}

[data-bs-theme="dark"] #logo-dark {
display: inline;
}

[data-bs-theme="light"] #logo-light {
display: inline;
}

#logo-and-title {
Expand Down
44 changes: 44 additions & 0 deletions vizro-core/tests/unit/vizro/models/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,50 @@ def test_page_registry_with_images(self, page_1, mocker, tmp_path):
layout=mocker.ANY, # partial call is tricky to mock out so we ignore it.
)

@pytest.mark.parametrize(
"logo_files, error_msg",
[
(
["logo.svg", "logo_dark.svg", "logo_light.svg"],
"Cannot provide `logo` together with both `logo_dark` and `logo_light`. Please provide either `logo`, "
"or both `logo_dark` and `logo_light`.",
),
(
["logo.svg", "logo_light.svg"],
"Both `logo_dark` and `logo_light` must be provided together. Please provide either both or neither.",
),
(
["logo.svg", "logo_dark.svg"],
"Both `logo_dark` and `logo_light` must be provided together. Please provide either both or neither.",
),
(
["logo_light.svg"],
"Both `logo_dark` and `logo_light` must be provided together. Please provide either both or neither.",
),
(
["logo_dark.svg"],
"Both `logo_dark` and `logo_light` must be provided together. Please provide either both or neither.",
),
],
)
def test_invalid_logo_combinations(self, page_1, tmp_path, logo_files, error_msg):
for file_path in logo_files:
Path(tmp_path / file_path).touch()
Vizro(assets_folder=tmp_path)

with pytest.raises(ValueError, match=error_msg):
vm.Dashboard(pages=[page_1]).pre_build()

@pytest.mark.parametrize(
"logo_files",
[["logo_dark.svg", "logo_light.svg"], ["logo.svg"], []],
)
def test_valid_logo_combinations(self, page_1, tmp_path, logo_files):
for file_path in logo_files:
Path(tmp_path / file_path).touch()
Vizro(assets_folder=tmp_path)
vm.Dashboard(pages=[page_1]).pre_build()

def test_make_page_404_layout(self, vizro_app):
# vizro_app fixture is needed to avoid mocking out get_relative_path.
expected = html.Div(
Expand Down

0 comments on commit a5a53f9

Please sign in to comment.