Skip to content

Commit

Permalink
[Tidy] Update Graph theme using clientside_callback (#523)
Browse files Browse the repository at this point in the history
  • Loading branch information
petar-qb authored Jun 14, 2024
1 parent 3ad4e1d commit 7fcb6fa
Show file tree
Hide file tree
Showing 13 changed files with 129 additions and 55 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
- 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

- Move changing theme callback to the client-side. ([#523](https://github.com/mckinsey/vizro/pull/523))

<!--
### 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))
-->
40 changes: 30 additions & 10 deletions vizro-core/examples/_dev/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,46 @@
from vizro import Vizro
from vizro.tables import dash_ag_grid, dash_data_table

df = px.data.gapminder()
NUMBER_OF_COMPONENTS = 64


def squared_layout(N):
"""Util function."""
import math

size = math.ceil(math.sqrt(N))
layout = [[(i * size + j) if (i * size + j) < N else -1 for j in range(size)] for i in range(size)]
return layout


page_one = vm.Page(
title="Dash AG Grid",
layout=vm.Layout(grid=[[0, 1]], col_gap="0px"),
title="Page 1",
layout=vm.Layout(grid=squared_layout(NUMBER_OF_COMPONENTS), col_gap="0px"),
components=[
vm.AgGrid(title="Equal Title One", figure=dash_ag_grid(data_frame=df)),
vm.Graph(figure=px.box(df, x="continent", y="lifeExp", title="Equal Title One")),
vm.Graph(id=f"{i}_graph", figure=px.box(px.data.gapminder(), x="continent", y="lifeExp", title=f"Graph {i}"))
for i in range(NUMBER_OF_COMPONENTS)
],
controls=[vm.Filter(column="continent")],
)

page_two = vm.Page(
title="Dash Data Table",
layout=vm.Layout(grid=[[0, 1]]),
title="Page 2",
layout=vm.Layout(grid=[[0, 1], [2, 2]]),
components=[
vm.Table(title="Equal Title One", figure=dash_data_table(data_frame=df)),
vm.Graph(figure=px.box(df, x="continent", y="lifeExp", title="Equal Title One")),
vm.Table(
id="P2_table", title="Data Table", figure=dash_data_table(id="P2_UL_table", data_frame=px.data.gapminder())
),
vm.AgGrid(
id="P2_aggrid", title="AG Grid", figure=dash_ag_grid(id="P2_UL_aggrid", data_frame=px.data.gapminder())
),
vm.Graph(id="P2_graph", figure=px.box(px.data.gapminder(), x="continent", y="lifeExp", title="Graph")),
],
controls=[vm.Filter(column="continent")],
)
dashboard = vm.Dashboard(
pages=[page_one, page_two],
# theme="vizro_light"
)
dashboard = vm.Dashboard(pages=[page_one, page_two])


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion vizro-core/hatch.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ scripts = {lint = "SKIP=gitleaks pre-commit run {args:--all-files}"}
[envs.lower-bounds]
extra-dependencies = [
"pydantic==1.10.13",
"dash==2.17.0"
"dash==2.17.1"
]

[publish.index]
Expand Down
2 changes: 1 addition & 1 deletion vizro-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ classifiers = [
"Programming Language :: Python :: 3.12"
]
dependencies = [
"dash>=2.17.0", # 2.17.0 needed for new features on loading spinner
"dash>=2.17.1", # 2.17.1 needed for no_output fix in clientside_callback
"dash_bootstrap_components",
"dash-ag-grid>=31.0.0",
"pandas",
Expand Down
2 changes: 1 addition & 1 deletion vizro-core/snyk/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
dash>=2.17.0
dash>=2.17.1
dash_bootstrap_components
dash-ag-grid>=31.0.0
pandas
Expand Down
10 changes: 9 additions & 1 deletion vizro-core/src/vizro/models/_components/graph.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
from typing import Dict, List, Literal

from dash import State, ctx, dcc
from dash import ClientsideFunction, Input, Output, State, clientside_callback, ctx, dcc
from dash.exceptions import MissingCallbackContextException
from plotly import graph_objects as go

Expand Down Expand Up @@ -116,6 +116,14 @@ def _filter_interaction(

@_log_call
def build(self):
clientside_callback(
ClientsideFunction(namespace="clientside", function_name="update_graph_theme"),
# Output here to ensure that the callback is only triggered if the graph exists on the currently open page.
output=[Output(self.id, "figure")],
inputs=[Input("theme_selector", "checked"), State("vizro_themes", "data"), State(self.id, "id")],
prevent_initial_call=True,
)

# The empty figure here is just a placeholder designed to be replaced by the actual figure when the filters
# etc. are applied. It only appears on the screen for a brief instant, but we need to make sure it's
# transparent and has no axes so it doesn't draw anything on the screen which would flicker away when the
Expand Down
16 changes: 14 additions & 2 deletions vizro-core/src/vizro/models/_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@
import dash
import dash_bootstrap_components as dbc
import dash_mantine_components as dmc
from dash import ClientsideFunction, Input, Output, State, clientside_callback, get_asset_url, get_relative_path, html
from dash import (
ClientsideFunction,
Input,
Output,
State,
clientside_callback,
dcc,
get_asset_url,
get_relative_path,
html,
)

try:
from pydantic.v1 import Field, validator
Expand All @@ -18,6 +28,7 @@
from dash.development.base_component import Component

import vizro
from vizro import _themes as themes
from vizro._constants import MODULE_PAGE_404, STATIC_URL_PREFIX
from vizro.actions._action_loop._action_loop import ActionLoop
from vizro.models import Navigation, VizroBaseModel
Expand Down Expand Up @@ -142,7 +153,8 @@ def build(self):
return html.Div(
id="dashboard-container",
children=[
html.Div(vizro.__version__, id="vizro_version", hidden=True),
html.Div(id="vizro_version", children=vizro.__version__, hidden=True),
dcc.Store(id="vizro_themes", data={"dark": themes.dark, "light": themes.light}),
ActionLoop._create_app_callbacks(),
dash.page_container,
],
Expand Down
32 changes: 1 addition & 31 deletions vizro-core/src/vizro/models/_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import List, TypedDict

from dash import Input, Output, Patch, callback, dcc, html
from dash import dcc, html

try:
from pydantic.v1 import Field, root_validator, validator
Expand Down Expand Up @@ -108,7 +108,6 @@ def pre_build(self):

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

Expand All @@ -120,32 +119,3 @@ def build(self) -> _PageBuildType:
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 _update_graph_theme(self):
# The obvious way to do this would be to alter pio.templates.default, but this changes global state and so is
# not good.
# Putting graphs as inputs here would be a nice way to trigger the theme change automatically so that we don't

# need the call to _update_theme inside Graph.__call__ also, but this results in an extra callback and the graph
# flickering.
# The code is written to be generic and extensible so that it runs _update_theme on any component with such a
# method defined. But at the moment this just means Graphs.
# TODO: consider making this clientside callback and then possibly we can remove the call to _update_theme in
# Graph.__call__ without any flickering.
# TODO: if we do this then we should *consider* defining the callback in Graph itself rather than at Page
# level. This would mean multiple callbacks on one page but if it's clientside that probably doesn't matter.

themed_components = [
model_manager[model_id]
for model_id in model_manager._get_model_children(model_id=ModelID(str(self.id)))
if hasattr(model_manager[model_id], "_update_theme")
]
if themed_components:

@callback(
[Output(component.id, "figure", allow_duplicate=True) for component in themed_components],
Input("theme_selector", "checked"),
prevent_initial_call="initial_duplicate",
)
def update_graph_theme(theme_selector: bool):
return [component._update_theme(Patch(), theme_selector) for component in themed_components]
8 changes: 5 additions & 3 deletions vizro-core/src/vizro/static/js/clientside_functions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {
_update_dashboard_theme,
_collapse_nav_panel,
_update_ag_grid_theme,
_update_graph_theme,
_collapse_nav_panel,
} from "./models/dashboard.js";
import { _update_range_slider_values } from "./models/range_slider.js";
import { _update_slider_values } from "./models/slider.js";
Expand All @@ -18,13 +19,14 @@ import {
window.dash_clientside = Object.assign({}, window.dash_clientside, {
clientside: {
update_dashboard_theme: _update_dashboard_theme,
update_ag_grid_theme: _update_ag_grid_theme,
update_graph_theme: _update_graph_theme,
collapse_nav_panel: _collapse_nav_panel,
update_range_slider_values: _update_range_slider_values,
update_slider_values: _update_slider_values,
trigger_to_global_store: _trigger_to_global_store,
gateway: _gateway,
after_action_cycle_breaker: _after_action_cycle_breaker,
collapse_nav_panel: _collapse_nav_panel,
update_ag_grid_theme: _update_ag_grid_theme,
update_date_picker_values: _update_date_picker_values,
update_date_picker_position: _update_date_picker_position,
},
Expand Down
15 changes: 15 additions & 0 deletions vizro-core/src/vizro/static/js/models/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ export function _update_ag_grid_theme(checked) {
: "ag-theme-quartz-dark ag-theme-vizro";
}

export function _update_graph_theme(checked, vizro_themes, graph_id) {
// Determine the theme to be applied based on the checked value
const theme_to_apply = checked ? vizro_themes["light"] : vizro_themes["dark"];

// Find the Plotly graph element in the HTML document
const plotly_graph = document
.getElementById(graph_id)
.querySelector(".js-plotly-plot");

// Adjust `layout` property for the Plotly graph element
Plotly.relayout(plotly_graph, { template: theme_to_apply });

return dash_clientside.no_update;
}

export function _collapse_nav_panel(n_clicks, is_open) {
if (!n_clicks) {
/* Automatically collapses left-side if xs and s-devices are detected*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,8 @@ def test_failed_ag_grid_with_no_captured_callable(self, standard_go_chart):
with pytest.raises(ValidationError, match="must provide a valid CapturedCallable object"):
vm.AgGrid(figure=standard_go_chart)

@pytest.mark.xfail(reason="This test is failing as we are not yet detecting different types of captured callables")
def test_failed_ag_grid_with_wrong_captured_callable(self, standard_px_chart):
with pytest.raises(ValidationError, match="must provide a valid ag_grid function vm.AgGrid"):
with pytest.raises(ValidationError, match="CapturedCallable mode mismatch. Expected ag_grid but got graph."):
vm.AgGrid(figure=standard_px_chart)

def test_set_action_via_validator(self, standard_ag_grid, identity_action_function):
Expand Down
3 changes: 1 addition & 2 deletions vizro-core/tests/unit/vizro/models/_components/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,8 @@ def test_failed_table_with_no_captured_callable(self, standard_go_chart):
with pytest.raises(ValidationError, match="must provide a valid CapturedCallable object"):
vm.Table(figure=standard_go_chart)

@pytest.mark.xfail(reason="This test is failing as we are not yet detecting different types of captured callables")
def test_failed_table_with_wrong_captured_callable(self, standard_px_chart):
with pytest.raises(ValidationError, match="must provide a valid table function vm.Table"):
with pytest.raises(ValidationError, match="CapturedCallable mode mismatch. Expected table but got graph."):
vm.Table(figure=standard_px_chart)

def test_set_action_via_validator(self, standard_dash_table, identity_action_function):
Expand Down
4 changes: 3 additions & 1 deletion vizro-core/tests/unit/vizro/models/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import dash_bootstrap_components as dbc
import pytest
from asserts import assert_component_equal
from dash import html
from dash import dcc, html

try:
from pydantic.v1 import ValidationError
Expand All @@ -14,6 +14,7 @@
import vizro
import vizro.models as vm
from vizro import Vizro
from vizro import _themes as themes
from vizro.actions._action_loop._action_loop import ActionLoop
from vizro.models._dashboard import _all_hidden

Expand Down Expand Up @@ -230,6 +231,7 @@ def test_dashboard_build(self, vizro_app, page_1, page_2):
id="dashboard-container",
children=[
html.Div(id="vizro_version", children=vizro.__version__, hidden=True),
dcc.Store(id="vizro_themes", data={"dark": themes.dark, "light": themes.light}),
ActionLoop._create_app_callbacks(),
dash.page_container,
],
Expand Down

0 comments on commit 7fcb6fa

Please sign in to comment.