From 9361e7d5c722718da5bf5f662c41544810a66621 Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Mon, 13 Nov 2023 11:06:06 +0000 Subject: [PATCH 01/14] Remove dead code --- vizro-core/src/vizro/actions/__init__.py | 9 --------- vizro-core/src/vizro/models/_dashboard.py | 9 ++------- vizro-core/tests/unit/vizro/models/test_dashboard.py | 7 ------- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/vizro-core/src/vizro/actions/__init__.py b/vizro-core/src/vizro/actions/__init__.py index f3411d31f..acc394f56 100644 --- a/vizro-core/src/vizro/actions/__init__.py +++ b/vizro-core/src/vizro/actions/__init__.py @@ -9,12 +9,3 @@ # Please keep alphabetically ordered __all__ = ["export_data", "filter_interaction"] - -# Actions lookup dictionary to facilitate function comparison -action_functions: Dict[Callable[[Any], Dict[str, Any]], str] = { - export_data.__wrapped__: "export_data", - filter_interaction.__wrapped__: "filter_interaction", - _filter.__wrapped__: "filter", - _parameter.__wrapped__: "parameter", - _on_page_load.__wrapped__: "on_page_load", -} diff --git a/vizro-core/src/vizro/models/_dashboard.py b/vizro-core/src/vizro/models/_dashboard.py index 11d482706..c4f55250c 100644 --- a/vizro-core/src/vizro/models/_dashboard.py +++ b/vizro-core/src/vizro/models/_dashboard.py @@ -23,10 +23,6 @@ logger = logging.getLogger(__name__) -def update_theme(on: bool): - return "vizro_dark" if on else "vizro_light" - - class Dashboard(VizroBaseModel): """Vizro Dashboard to be used within [`Vizro`][vizro._vizro.Vizro.build]. @@ -65,6 +61,7 @@ def __init__(self, *args, **kwargs): # pio is the backend global state and shouldn't be changed while # the app is running. This limitation leads to the case that Graphs blink # on page load if user previously has changed theme_selector. + # AM: Did I fix this? No need to set this any more? pio.templates.default = self.theme @_log_call @@ -103,9 +100,7 @@ def _make_page_layout(self, page: Page): if self.title else html.Div(hidden=True, id="dashboard_title_outer") ) - theme_switch = daq.BooleanSwitch( - id="theme_selector", on=True if self.theme == "vizro_dark" else False, persistence=True - ) + theme_switch = daq.BooleanSwitch(id="theme_selector", on=self.theme == "vizro_dark", persistence=True) # Shared across pages but slightly differ in content page_title = html.H2(children=page.title, id="page_title") diff --git a/vizro-core/tests/unit/vizro/models/test_dashboard.py b/vizro-core/tests/unit/vizro/models/test_dashboard.py index 43353b85d..8f2937153 100644 --- a/vizro-core/tests/unit/vizro/models/test_dashboard.py +++ b/vizro-core/tests/unit/vizro/models/test_dashboard.py @@ -12,7 +12,6 @@ import vizro import vizro.models as vm from vizro.actions._action_loop._action_loop import ActionLoop -from vizro.models._dashboard import update_theme @pytest.fixture() @@ -163,9 +162,3 @@ def test_dashboard_build(self, dashboard_container, dashboard): result = json.loads(json.dumps(dashboard.build(), cls=plotly.utils.PlotlyJSONEncoder)) expected = json.loads(json.dumps(dashboard_container, cls=plotly.utils.PlotlyJSONEncoder)) assert result == expected - - -@pytest.mark.parametrize("on, expected", [(True, "vizro_dark"), (False, "vizro_light")]) -def test_update_theme(on, expected): - result = update_theme(on) - assert result == expected From 3e9d7eab27c6422500d1d954bded262aa358739c Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Mon, 13 Nov 2023 11:31:33 +0000 Subject: [PATCH 02/14] Move update_layout code --- vizro-core/src/vizro/actions/_actions_utils.py | 3 --- .../src/vizro/models/_components/graph.py | 8 ++++++++ vizro-core/src/vizro/models/_dashboard.py | 15 ++++++--------- vizro-core/src/vizro/models/_page.py | 17 ++++++++++++----- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/vizro-core/src/vizro/actions/_actions_utils.py b/vizro-core/src/vizro/actions/_actions_utils.py index e82756e03..759418c10 100644 --- a/vizro-core/src/vizro/actions/_actions_utils.py +++ b/vizro-core/src/vizro/actions/_actions_utils.py @@ -267,7 +267,4 @@ def _get_modified_page_figures( outputs[target] = model_manager[target]( # type: ignore[operator] data_frame=filtered_data[target], **parameterized_config[target] ) - if hasattr(outputs[target], "update_layout"): - outputs[target].update_layout(template="vizro_dark" if ctd_theme["value"] else "vizro_light") - return outputs diff --git a/vizro-core/src/vizro/models/_components/graph.py b/vizro-core/src/vizro/models/_components/graph.py index cb792615e..3dd3889df 100644 --- a/vizro-core/src/vizro/models/_components/graph.py +++ b/vizro-core/src/vizro/models/_components/graph.py @@ -56,6 +56,14 @@ def __call__(self, **kwargs): # Remove top margin if title is provided if fig.layout.title.text is None: fig.update_layout(margin_t=24) + + try: + fig.update_layout(template="vizro_dark" if ctx.states["theme_selector.on"] else "vizro_light") + except MissingCallbackContextException: + # Possibly we should enforce that __call__ can only be used within the context of a callback, but it's easy + # to just swallow up the error here as it doesn't cause any problems. + logger.info("fig.update_layout called outside of callback context.") + return fig # Convenience wrapper/syntactic sugar. diff --git a/vizro-core/src/vizro/models/_dashboard.py b/vizro-core/src/vizro/models/_dashboard.py index c4f55250c..d966cb3c7 100644 --- a/vizro-core/src/vizro/models/_dashboard.py +++ b/vizro-core/src/vizro/models/_dashboard.py @@ -80,7 +80,12 @@ def pre_build(self): def build(self): for page in self.pages: page.build() # TODO: ideally remove, but necessary to register slider callbacks - self._update_theme() + + clientside_callback( + ClientsideFunction(namespace="clientside", function_name="update_dashboard_theme"), + Output("dashboard_container_outer", "className"), + Input("theme_selector", "on"), + ) return dbc.Container( id="dashboard_container_outer", @@ -122,14 +127,6 @@ def _make_page_layout(self, page: Page): right_side = html.Div(children=[header, component_container], className="right_side", id="right_side_outer") return html.Div([left_side, right_side], className="page_container", id="page_container_outer") - @staticmethod - def _update_theme(): - clientside_callback( - ClientsideFunction(namespace="clientside", function_name="update_dashboard_theme"), - Output("dashboard_container_outer", "className"), - Input("theme_selector", "on"), - ) - @staticmethod def _make_page_404_layout(): return html.Div( diff --git a/vizro-core/src/vizro/models/_page.py b/vizro-core/src/vizro/models/_page.py index e8e0a8245..701ccb4d0 100644 --- a/vizro-core/src/vizro/models/_page.py +++ b/vizro-core/src/vizro/models/_page.py @@ -136,6 +136,17 @@ def build(self): 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 update_layout call in Graph.__call__, but this results in an extra callback and the graph + # flickering. + # TODO: consider making this clientside callback and then possibly we can remove the update_layout in + # Graph.__call__ without any flickering. + # TODO: consider putting this as dashboard level like with _update_theme (though possibly pointless because you + # don't see graphs on other pages and then when you change page it always redraws graphs anyway). + # TODO: consider putting the Graph-specific logic here in the Graph model itself (whether clientside or + # serverside) to keep the code here abstract. outputs = [ Output(component.id, "figure", allow_duplicate=True) for component in self.components @@ -143,11 +154,7 @@ def _update_graph_theme(self): ] if outputs: - @callback( - outputs, - Input("theme_selector", "on"), - prevent_initial_call="initial_duplicate", - ) + @callback(outputs, Input("theme_selector", "on"), prevent_initial_call="initial_duplicate") def update_graph_theme(theme_selector_on: bool): patched_figure = Patch() patched_figure["layout"]["template"] = themes.dark if theme_selector_on else themes.light From 9f9dd45163958efeeb226572ab7f5264f35e4a41 Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Mon, 13 Nov 2023 11:53:44 +0000 Subject: [PATCH 03/14] Fix flickering graph behaviour --- .../src/vizro/models/_components/graph.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/vizro-core/src/vizro/models/_components/graph.py b/vizro-core/src/vizro/models/_components/graph.py index 3dd3889df..3822a90e2 100644 --- a/vizro-core/src/vizro/models/_components/graph.py +++ b/vizro-core/src/vizro/models/_components/graph.py @@ -1,7 +1,8 @@ import logging from typing import List, Literal -from dash import dcc +from dash import dcc, ctx +from dash.exceptions import MissingCallbackContextException from plotly import graph_objects as go from pydantic import Field, PrivateAttr, validator @@ -16,18 +17,6 @@ logger = logging.getLogger(__name__) -def create_empty_fig(message: str) -> go.Figure: - """Creates empty go.Figure object with a display message.""" - fig = go.Figure() - fig.add_trace(go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo="none")) - fig.update_layout( - xaxis={"visible": False}, - yaxis={"visible": False}, - annotations=[{"text": message, "showarrow": False, "font": {"size": 16}}], - ) - return fig - - class Graph(VizroBaseModel): """Wrapper for `dcc.Graph` to visualize charts in dashboard. @@ -58,6 +47,8 @@ def __call__(self, **kwargs): fig.update_layout(margin_t=24) try: + # if "theme_selector" in ctx.states # Probably want this here given code should work outside Dashboard + # for now fig.update_layout(template="vizro_dark" if ctx.states["theme_selector.on"] else "vizro_light") except MissingCallbackContextException: # Possibly we should enforce that __call__ can only be used within the context of a callback, but it's easy @@ -76,14 +67,21 @@ def __getitem__(self, arg_name: str): @_log_call def build(self): + # 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 + # graph callback is executed to make the dcc.Loading icon appear. return dcc.Loading( dcc.Graph( id=self.id, - # We don't do self.__call__() until the Graph is actually built. This ensures that lazy data is not - # loaded until the graph is first shown on the screen. At the moment, we eagerly run page.build() for - # all pages in Dashboard.build in order to register all the callbacks in advance. In future this should - # no longer be the case so that we achieve true lazy loading. - figure=create_empty_fig(""), + figure=go.Figure( + layout={ + "paper_bgcolor": "rgba(0,0,0,0)", + "plot_bgcolor": "rgba(0,0,0,0)", + "xaxis": {"visible": False}, + "yaxis": {"visible": False}, + } + ), config={ "autosizable": True, "frameMargins": 0, From cf4ead19896e89706e60a01447bb784aab2554d1 Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Mon, 13 Nov 2023 12:25:10 +0000 Subject: [PATCH 04/14] Remove pio.default setting --- vizro-core/src/vizro/models/_dashboard.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/vizro-core/src/vizro/models/_dashboard.py b/vizro-core/src/vizro/models/_dashboard.py index d966cb3c7..1cf23d638 100644 --- a/vizro-core/src/vizro/models/_dashboard.py +++ b/vizro-core/src/vizro/models/_dashboard.py @@ -56,14 +56,6 @@ def validate_navigation(cls, navigation, values): return Navigation(pages=[page.id for page in values["pages"]]) return navigation - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # pio is the backend global state and shouldn't be changed while - # the app is running. This limitation leads to the case that Graphs blink - # on page load if user previously has changed theme_selector. - # AM: Did I fix this? No need to set this any more? - pio.templates.default = self.theme - @_log_call def pre_build(self): # Setting order here ensures that the pages in dash.page_registry preserves the order of the List[Page]. From 6cdf2d78998ca3a13959ff4cd2ce9381b1f9a768 Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Mon, 13 Nov 2023 12:36:29 +0000 Subject: [PATCH 05/14] Add changelog --- ...231113_122927_antony.milne_update_theme.md | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 vizro-core/changelog.d/20231113_122927_antony.milne_update_theme.md diff --git a/vizro-core/changelog.d/20231113_122927_antony.milne_update_theme.md b/vizro-core/changelog.d/20231113_122927_antony.milne_update_theme.md new file mode 100644 index 000000000..6aeea568a --- /dev/null +++ b/vizro-core/changelog.d/20231113_122927_antony.milne_update_theme.md @@ -0,0 +1,48 @@ + + + + + + + + + From 772fefb9c49b844f8e86c594310a11aa1b1e0ad4 Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Tue, 14 Nov 2023 15:54:25 +0000 Subject: [PATCH 06/14] Add changelog --- .../changelog.d/20231113_122927_antony.milne_update_theme.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/vizro-core/changelog.d/20231113_122927_antony.milne_update_theme.md b/vizro-core/changelog.d/20231113_122927_antony.milne_update_theme.md index 6aeea568a..6e15f2555 100644 --- a/vizro-core/changelog.d/20231113_122927_antony.milne_update_theme.md +++ b/vizro-core/changelog.d/20231113_122927_antony.milne_update_theme.md @@ -34,12 +34,10 @@ Uncomment the section that is right (remove the HTML comment wrapper). - 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 - Remove graph flickering on page load with Vizro light theme ([#166](https://github.com/mckinsey/vizro/pull/166)) diff --git a/vizro-core/src/vizro/actions/__init__.py b/vizro-core/src/vizro/actions/__init__.py index acc394f56..39224154f 100644 --- a/vizro-core/src/vizro/actions/__init__.py +++ b/vizro-core/src/vizro/actions/__init__.py @@ -1,6 +1,3 @@ -# Redundant aliases here to prevent ruff from removing unused imports. -from typing import Any, Callable, Dict - from vizro.actions._filter_action import _filter from vizro.actions._on_page_load_action import _on_page_load from vizro.actions._parameter_action import _parameter diff --git a/vizro-core/src/vizro/models/_dashboard.py b/vizro-core/src/vizro/models/_dashboard.py index 1cf23d638..f28b08f22 100644 --- a/vizro-core/src/vizro/models/_dashboard.py +++ b/vizro-core/src/vizro/models/_dashboard.py @@ -7,7 +7,6 @@ import dash import dash_bootstrap_components as dbc import dash_daq as daq -import plotly.io as pio from dash import ClientsideFunction, Input, Output, clientside_callback, get_relative_path, html from pydantic import Field, validator diff --git a/vizro-core/tests/unit/vizro/models/_components/test_graph.py b/vizro-core/tests/unit/vizro/models/_components/test_graph.py index e45609321..9694febe9 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_graph.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_graph.py @@ -11,7 +11,6 @@ import vizro.plotly.express as px from vizro.managers import data_manager from vizro.models._action._action import Action -from vizro.models._components.graph import create_empty_fig @pytest.fixture @@ -44,7 +43,14 @@ def expected_graph(): return dcc.Loading( dcc.Graph( id="text_graph", - figure=create_empty_fig(""), + figure=go.Figure( + layout={ + "paper_bgcolor": "rgba(0,0,0,0)", + "plot_bgcolor": "rgba(0,0,0,0)", + "xaxis": {"visible": False}, + "yaxis": {"visible": False}, + } + ), config={ "autosizable": True, "frameMargins": 0, @@ -136,15 +142,8 @@ def test_process_figure_data_frame_df(self, standard_px_chart, gapminder): class TestBuild: - def test_create_empty_fig(self, expected_empty_chart): - result = create_empty_fig("NO DATA") - assert result == expected_empty_chart - def test_graph_build(self, standard_px_chart, expected_graph): - graph = vm.Graph( - id="text_graph", - figure=standard_px_chart, - ) + graph = vm.Graph(id="text_graph", figure=standard_px_chart) result = json.loads(json.dumps(graph.build(), cls=plotly.utils.PlotlyJSONEncoder)) expected = json.loads(json.dumps(expected_graph, cls=plotly.utils.PlotlyJSONEncoder)) From 7f9560385a6ae85dc8684dda6630f2021a4cefe3 Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Tue, 14 Nov 2023 16:14:08 +0000 Subject: [PATCH 09/14] Move update_layout to Graph.__call__ --- vizro-core/src/vizro/actions/_actions_utils.py | 2 -- vizro-core/src/vizro/models/_components/graph.py | 12 +++++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/vizro-core/src/vizro/actions/_actions_utils.py b/vizro-core/src/vizro/actions/_actions_utils.py index e82756e03..476cc992f 100644 --- a/vizro-core/src/vizro/actions/_actions_utils.py +++ b/vizro-core/src/vizro/actions/_actions_utils.py @@ -267,7 +267,5 @@ def _get_modified_page_figures( outputs[target] = model_manager[target]( # type: ignore[operator] data_frame=filtered_data[target], **parameterized_config[target] ) - if hasattr(outputs[target], "update_layout"): - outputs[target].update_layout(template="vizro_dark" if ctd_theme["value"] else "vizro_light") return outputs diff --git a/vizro-core/src/vizro/models/_components/graph.py b/vizro-core/src/vizro/models/_components/graph.py index 220e835bf..702ecbf72 100644 --- a/vizro-core/src/vizro/models/_components/graph.py +++ b/vizro-core/src/vizro/models/_components/graph.py @@ -1,7 +1,8 @@ import logging from typing import List, Literal -from dash import dcc +from dash import dcc, ctx +from dash.exceptions import MissingCallbackContextException from plotly import graph_objects as go from pydantic import Field, PrivateAttr, validator @@ -44,6 +45,15 @@ def __call__(self, **kwargs): # Remove top margin if title is provided if fig.layout.title.text is None: fig.update_layout(margin_t=24) + + try: + # if "theme_selector" in ctx.states # Probably want this here given code should work outside Dashboard + # for now + fig.update_layout(template="vizro_dark" if ctx.states["theme_selector.on"] else "vizro_light") + except MissingCallbackContextException: + # Possibly we should enforce that __call__ can only be used within the context of a callback, but it's easy + # to just swallow up the error here as it doesn't cause any problems. + logger.info("fig.update_layout called outside of callback context.") return fig # Convenience wrapper/syntactic sugar. From 5991fd99ee6018febf8e63608868e03f013a7faa Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Tue, 14 Nov 2023 16:38:51 +0000 Subject: [PATCH 10/14] Remove theme_selector from being passed round actions --- vizro-core/src/vizro/actions/_actions_utils.py | 1 - .../actions/_callback_mapping/_callback_mapping_utils.py | 3 +-- vizro-core/src/vizro/actions/_filter_action.py | 3 +-- vizro-core/src/vizro/actions/_on_page_load_action.py | 3 +-- vizro-core/src/vizro/actions/_parameter_action.py | 3 +-- vizro-core/src/vizro/actions/export_data_action.py | 2 +- vizro-core/src/vizro/actions/filter_interaction_action.py | 3 +-- vizro-core/src/vizro/models/_components/graph.py | 2 +- vizro-core/src/vizro/models/_page.py | 4 ++++ 9 files changed, 11 insertions(+), 13 deletions(-) diff --git a/vizro-core/src/vizro/actions/_actions_utils.py b/vizro-core/src/vizro/actions/_actions_utils.py index 476cc992f..4f4363203 100644 --- a/vizro-core/src/vizro/actions/_actions_utils.py +++ b/vizro-core/src/vizro/actions/_actions_utils.py @@ -246,7 +246,6 @@ def _get_modified_page_figures( ctds_filter: List[CallbackTriggerDict], ctds_filter_interaction: List[Dict[str, CallbackTriggerDict]], ctds_parameters: List[CallbackTriggerDict], - ctd_theme: CallbackTriggerDict, targets: Optional[List[ModelID]] = None, ) -> Dict[ModelID, Any]: if not targets: diff --git a/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py b/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py index da6620214..d3f1513cd 100644 --- a/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py +++ b/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py @@ -137,7 +137,7 @@ def _get_action_callback_inputs(action_id: ModelID) -> Dict[str, Any]: if action_function == export_data.__wrapped__: include_inputs = ["filters", "filter_interaction"] else: - include_inputs = ["filters", "parameters", "filter_interaction", "theme_selector"] + include_inputs = ["filters", "parameters", "filter_interaction"] action_input_mapping = { "filters": ( @@ -154,7 +154,6 @@ def _get_action_callback_inputs(action_id: ModelID) -> Dict[str, Any]: if "filter_interaction" in include_inputs else [] ), - "theme_selector": (State("theme_selector", "on") if "theme_selector" in include_inputs else []), } return action_input_mapping diff --git a/vizro-core/src/vizro/actions/_filter_action.py b/vizro-core/src/vizro/actions/_filter_action.py index f35daa7b4..63c867dc8 100644 --- a/vizro-core/src/vizro/actions/_filter_action.py +++ b/vizro-core/src/vizro/actions/_filter_action.py @@ -26,7 +26,7 @@ def _filter( targets: List of target component ids to apply filters on filter_function: Filter function to apply inputs: Dict mapping action function names with their inputs e.g. - inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': [], 'theme_selector': True} + inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': []} Returns: Dict mapping target component ids to modified charts/components e.g. {'my_scatter': Figure({})} @@ -36,5 +36,4 @@ def _filter( ctds_filter=ctx.args_grouping["filters"], ctds_filter_interaction=ctx.args_grouping["filter_interaction"], ctds_parameters=ctx.args_grouping["parameters"], - ctd_theme=ctx.args_grouping["theme_selector"], ) diff --git a/vizro-core/src/vizro/actions/_on_page_load_action.py b/vizro-core/src/vizro/actions/_on_page_load_action.py index 16c6279f5..4fcb28a60 100644 --- a/vizro-core/src/vizro/actions/_on_page_load_action.py +++ b/vizro-core/src/vizro/actions/_on_page_load_action.py @@ -19,7 +19,7 @@ def _on_page_load(page_id: ModelID, **inputs: Dict[str, Any]) -> Dict[ModelID, A Args: page_id: Page ID of relevant page inputs: Dict mapping action function names with their inputs e.g. - inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': [], 'theme_selector': True} + inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': []} Returns: Dict mapping target chart ids to modified figures e.g. {'my_scatter': Figure({})} @@ -31,5 +31,4 @@ def _on_page_load(page_id: ModelID, **inputs: Dict[str, Any]) -> Dict[ModelID, A ctds_filter=ctx.args_grouping["filters"], ctds_filter_interaction=ctx.args_grouping["filter_interaction"], ctds_parameters=ctx.args_grouping["parameters"], - ctd_theme=ctx.args_grouping["theme_selector"], ) diff --git a/vizro-core/src/vizro/actions/_parameter_action.py b/vizro-core/src/vizro/actions/_parameter_action.py index ba77a7762..82baf75d1 100644 --- a/vizro-core/src/vizro/actions/_parameter_action.py +++ b/vizro-core/src/vizro/actions/_parameter_action.py @@ -18,7 +18,7 @@ def _parameter(targets: List[str], **inputs: Dict[str, Any]) -> Dict[ModelID, An Args: targets: List of target component ids to change parameters of. inputs: Dict mapping action function names with their inputs e.g. - inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': [], 'theme_selector': True} + inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': []} Returns: Dict mapping target component ids to modified charts/components e.g. {'my_scatter': Figure({})} @@ -30,5 +30,4 @@ def _parameter(targets: List[str], **inputs: Dict[str, Any]) -> Dict[ModelID, An ctds_filter=ctx.args_grouping["filters"], ctds_filter_interaction=ctx.args_grouping["filter_interaction"], ctds_parameters=ctx.args_grouping["parameters"], - ctd_theme=ctx.args_grouping["theme_selector"], ) diff --git a/vizro-core/src/vizro/actions/export_data_action.py b/vizro-core/src/vizro/actions/export_data_action.py index afaded8cf..bfcd0d1ee 100644 --- a/vizro-core/src/vizro/actions/export_data_action.py +++ b/vizro-core/src/vizro/actions/export_data_action.py @@ -25,7 +25,7 @@ def export_data( targets: List of target component ids to download data from. Defaults to None. file_format: Format of downloaded files. Defaults to `csv`. inputs: Dict mapping action function names with their inputs e.g. - inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': [], 'theme_selector': True} + inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': []} Raises: ValueError: If unknown file extension is provided. diff --git a/vizro-core/src/vizro/actions/filter_interaction_action.py b/vizro-core/src/vizro/actions/filter_interaction_action.py index d3a04f3a8..40bc432d9 100644 --- a/vizro-core/src/vizro/actions/filter_interaction_action.py +++ b/vizro-core/src/vizro/actions/filter_interaction_action.py @@ -27,7 +27,7 @@ def filter_interaction( targets: List of target component ids to filter by chart interaction. If missing, will target all valid components on page. Defaults to None. inputs: Dict mapping action function names with their inputs e.g. - inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': [], 'theme_selector': True} + inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': []} Returns: Dict mapping target component ids to modified charts/components e.g. {'my_scatter': Figure({})} @@ -37,5 +37,4 @@ def filter_interaction( ctds_filter=ctx.args_grouping["filters"], ctds_filter_interaction=ctx.args_grouping["filter_interaction"], ctds_parameters=ctx.args_grouping["parameters"], - ctd_theme=ctx.args_grouping["theme_selector"], ) diff --git a/vizro-core/src/vizro/models/_components/graph.py b/vizro-core/src/vizro/models/_components/graph.py index 702ecbf72..5098363c7 100644 --- a/vizro-core/src/vizro/models/_components/graph.py +++ b/vizro-core/src/vizro/models/_components/graph.py @@ -49,7 +49,7 @@ def __call__(self, **kwargs): try: # if "theme_selector" in ctx.states # Probably want this here given code should work outside Dashboard # for now - fig.update_layout(template="vizro_dark" if ctx.states["theme_selector.on"] else "vizro_light") + fig.update_layout(template="vizro_dark" if ctx.args_grouping["theme_selector"]["value"] else "vizro_light") except MissingCallbackContextException: # Possibly we should enforce that __call__ can only be used within the context of a callback, but it's easy # to just swallow up the error here as it doesn't cause any problems. diff --git a/vizro-core/src/vizro/models/_page.py b/vizro-core/src/vizro/models/_page.py index 17ae4a073..ec47de2b6 100644 --- a/vizro-core/src/vizro/models/_page.py +++ b/vizro-core/src/vizro/models/_page.py @@ -145,6 +145,10 @@ def _update_graph_theme(self): # Graph.__call__ without any flickering. # TODO: consider putting the Graph-specific logic here in the Graph model itself (whether clientside or # serverside) to keep the code here abstract. + # AM: do this. Reuse same logic as in Graph.__call__? + # On the last TODO, my thinking was that the graph-specific logic like patched_figure["layout"]["template"] = themes.dark if theme_selector_on else themes.light should ideally live in the Graph model, and we might somehow try to change the ifinstance(component, Graph) to e.g. hasattr(component, "update_theme"). The callback itself would still live here like it does now. + # + # It would just be a bit of refactoring, not changing the number of callbacks or anything like that. I'll try it out in a separate PR and see what it looks like 🙂 outputs = [ Output(component.id, "figure", allow_duplicate=True) for component in self.components From 37147386221437ea13815d91914d743103cf8b8a Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Tue, 14 Nov 2023 20:10:37 +0000 Subject: [PATCH 11/14] Make sure theme_selector state still there where needed --- .../actions/_callback_mapping/_callback_mapping_utils.py | 3 ++- vizro-core/src/vizro/actions/_filter_action.py | 2 +- vizro-core/src/vizro/actions/_on_page_load_action.py | 2 +- vizro-core/src/vizro/actions/_parameter_action.py | 2 +- vizro-core/src/vizro/actions/export_data_action.py | 2 +- vizro-core/src/vizro/actions/filter_interaction_action.py | 2 +- vizro-core/src/vizro/models/_components/graph.py | 8 +++++--- 7 files changed, 12 insertions(+), 9 deletions(-) diff --git a/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py b/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py index d3f1513cd..2e55f12da 100644 --- a/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py +++ b/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py @@ -137,7 +137,7 @@ def _get_action_callback_inputs(action_id: ModelID) -> Dict[str, Any]: if action_function == export_data.__wrapped__: include_inputs = ["filters", "filter_interaction"] else: - include_inputs = ["filters", "parameters", "filter_interaction"] + include_inputs = ["filters", "parameters", "filter_interaction", "theme_selector"] action_input_mapping = { "filters": ( @@ -154,6 +154,7 @@ def _get_action_callback_inputs(action_id: ModelID) -> Dict[str, Any]: if "filter_interaction" in include_inputs else [] ), + "theme_selector": State("theme_selector", "on") if "theme_selector" in include_inputs else [], } return action_input_mapping diff --git a/vizro-core/src/vizro/actions/_filter_action.py b/vizro-core/src/vizro/actions/_filter_action.py index 63c867dc8..bd3a2b596 100644 --- a/vizro-core/src/vizro/actions/_filter_action.py +++ b/vizro-core/src/vizro/actions/_filter_action.py @@ -26,7 +26,7 @@ def _filter( targets: List of target component ids to apply filters on filter_function: Filter function to apply inputs: Dict mapping action function names with their inputs e.g. - inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': []} + inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': [], 'theme_selector': True} Returns: Dict mapping target component ids to modified charts/components e.g. {'my_scatter': Figure({})} diff --git a/vizro-core/src/vizro/actions/_on_page_load_action.py b/vizro-core/src/vizro/actions/_on_page_load_action.py index 4fcb28a60..ca492907a 100644 --- a/vizro-core/src/vizro/actions/_on_page_load_action.py +++ b/vizro-core/src/vizro/actions/_on_page_load_action.py @@ -19,7 +19,7 @@ def _on_page_load(page_id: ModelID, **inputs: Dict[str, Any]) -> Dict[ModelID, A Args: page_id: Page ID of relevant page inputs: Dict mapping action function names with their inputs e.g. - inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': []} + inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': [], 'theme_selector': True} Returns: Dict mapping target chart ids to modified figures e.g. {'my_scatter': Figure({})} diff --git a/vizro-core/src/vizro/actions/_parameter_action.py b/vizro-core/src/vizro/actions/_parameter_action.py index 82baf75d1..defc5d5aa 100644 --- a/vizro-core/src/vizro/actions/_parameter_action.py +++ b/vizro-core/src/vizro/actions/_parameter_action.py @@ -18,7 +18,7 @@ def _parameter(targets: List[str], **inputs: Dict[str, Any]) -> Dict[ModelID, An Args: targets: List of target component ids to change parameters of. inputs: Dict mapping action function names with their inputs e.g. - inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': []} + inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': [], 'theme_selector': True} Returns: Dict mapping target component ids to modified charts/components e.g. {'my_scatter': Figure({})} diff --git a/vizro-core/src/vizro/actions/export_data_action.py b/vizro-core/src/vizro/actions/export_data_action.py index bfcd0d1ee..afaded8cf 100644 --- a/vizro-core/src/vizro/actions/export_data_action.py +++ b/vizro-core/src/vizro/actions/export_data_action.py @@ -25,7 +25,7 @@ def export_data( targets: List of target component ids to download data from. Defaults to None. file_format: Format of downloaded files. Defaults to `csv`. inputs: Dict mapping action function names with their inputs e.g. - inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': []} + inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': [], 'theme_selector': True} Raises: ValueError: If unknown file extension is provided. diff --git a/vizro-core/src/vizro/actions/filter_interaction_action.py b/vizro-core/src/vizro/actions/filter_interaction_action.py index 40bc432d9..fd2392617 100644 --- a/vizro-core/src/vizro/actions/filter_interaction_action.py +++ b/vizro-core/src/vizro/actions/filter_interaction_action.py @@ -27,7 +27,7 @@ def filter_interaction( targets: List of target component ids to filter by chart interaction. If missing, will target all valid components on page. Defaults to None. inputs: Dict mapping action function names with their inputs e.g. - inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': []} + inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': [], 'theme_selector': True} Returns: Dict mapping target component ids to modified charts/components e.g. {'my_scatter': Figure({})} diff --git a/vizro-core/src/vizro/models/_components/graph.py b/vizro-core/src/vizro/models/_components/graph.py index 5098363c7..773376cad 100644 --- a/vizro-core/src/vizro/models/_components/graph.py +++ b/vizro-core/src/vizro/models/_components/graph.py @@ -47,9 +47,11 @@ def __call__(self, **kwargs): fig.update_layout(margin_t=24) try: - # if "theme_selector" in ctx.states # Probably want this here given code should work outside Dashboard - # for now - fig.update_layout(template="vizro_dark" if ctx.args_grouping["theme_selector"]["value"] else "vizro_light") + # AM:comment that this case doesn't occur + if "theme_selector" in ctx.args_grouping: + fig.update_layout( + template="vizro_dark" if ctx.args_grouping["theme_selector"]["value"] else "vizro_light" + ) except MissingCallbackContextException: # Possibly we should enforce that __call__ can only be used within the context of a callback, but it's easy # to just swallow up the error here as it doesn't cause any problems. From 1991d6e6fa49d00c1988fec77b54a4611e991ce5 Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Wed, 15 Nov 2023 10:29:56 +0000 Subject: [PATCH 12/14] Finialise code and update tests --- ..._102949_antony.milne_update_graph_theme.md | 48 ++++++++++++ .../src/vizro/models/_components/graph.py | 21 ++++-- vizro-core/src/vizro/models/_page.py | 38 +++++----- .../vizro/actions/test_on_page_load_action.py | 2 +- .../vizro/models/_components/test_graph.py | 73 ++++++++++--------- 5 files changed, 118 insertions(+), 64 deletions(-) create mode 100644 vizro-core/changelog.d/20231115_102949_antony.milne_update_graph_theme.md diff --git a/vizro-core/changelog.d/20231115_102949_antony.milne_update_graph_theme.md b/vizro-core/changelog.d/20231115_102949_antony.milne_update_graph_theme.md new file mode 100644 index 000000000..f1f65e73c --- /dev/null +++ b/vizro-core/changelog.d/20231115_102949_antony.milne_update_graph_theme.md @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/vizro-core/src/vizro/models/_components/graph.py b/vizro-core/src/vizro/models/_components/graph.py index 773376cad..33b5dbe09 100644 --- a/vizro-core/src/vizro/models/_components/graph.py +++ b/vizro-core/src/vizro/models/_components/graph.py @@ -1,12 +1,13 @@ import logging from typing import List, Literal -from dash import dcc, ctx +from dash import ctx, dcc from dash.exceptions import MissingCallbackContextException from plotly import graph_objects as go from pydantic import Field, PrivateAttr, validator import vizro.plotly.express as px +from vizro import _themes as themes from vizro.managers import data_manager from vizro.models import Action, VizroBaseModel from vizro.models._action._actions_chain import _action_validator_factory @@ -46,15 +47,14 @@ def __call__(self, **kwargs): if fig.layout.title.text is None: fig.update_layout(margin_t=24) + # Possibly we should enforce that __call__ can only be used within the context of a callback, but it's easy + # to just swallow up the error here as it doesn't cause any problems. try: - # AM:comment that this case doesn't occur + # At the moment theme_selector is always present so this if statement is redundant, but possibly in + # future we'll have callbacks that do Graph.__call__() without theme_selector set. if "theme_selector" in ctx.args_grouping: - fig.update_layout( - template="vizro_dark" if ctx.args_grouping["theme_selector"]["value"] else "vizro_light" - ) + fig = self._update_theme(fig, ctx.args_grouping["theme_selector"]["value"]) except MissingCallbackContextException: - # Possibly we should enforce that __call__ can only be used within the context of a callback, but it's easy - # to just swallow up the error here as it doesn't cause any problems. logger.info("fig.update_layout called outside of callback context.") return fig @@ -93,3 +93,10 @@ def build(self): color="grey", parent_className="loading-container", ) + + @staticmethod + def _update_theme(fig: go.Figure, theme_selector: bool): + # Basically the same as doing fig.update_layout(template="vizro_light/dark") but works for both the call in + # self.__call__ and in the update_graph_theme callback. + fig["layout"]["template"] = themes.dark if theme_selector else themes.light + return fig diff --git a/vizro-core/src/vizro/models/_page.py b/vizro-core/src/vizro/models/_page.py index ec47de2b6..2a8f09a15 100644 --- a/vizro-core/src/vizro/models/_page.py +++ b/vizro-core/src/vizro/models/_page.py @@ -5,11 +5,10 @@ from dash import Input, Output, Patch, callback, dcc, html from pydantic import Field, root_validator, validator -import vizro._themes as themes from vizro._constants import ON_PAGE_LOAD_ACTION_PREFIX from vizro.actions import _on_page_load from vizro.managers._model_manager import DuplicateIDError -from vizro.models import Action, Graph, Layout, VizroBaseModel +from vizro.models import Action, Layout, VizroBaseModel from vizro.models._action._actions_chain import ActionsChain, Trigger from vizro.models._models_utils import _log_call, get_unique_grid_component_ids @@ -139,28 +138,25 @@ 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 update_layout call in Graph.__call__, but this results in an extra callback and the graph + # need the call to _update_theme inside Graph.__call__ also, but this results in an extra callback and the graph # flickering. - # TODO: consider making this clientside callback and then possibly we can remove the update_layout in + # 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: consider putting the Graph-specific logic here in the Graph model itself (whether clientside or - # serverside) to keep the code here abstract. - # AM: do this. Reuse same logic as in Graph.__call__? - # On the last TODO, my thinking was that the graph-specific logic like patched_figure["layout"]["template"] = themes.dark if theme_selector_on else themes.light should ideally live in the Graph model, and we might somehow try to change the ifinstance(component, Graph) to e.g. hasattr(component, "update_theme"). The callback itself would still live here like it does now. - # - # It would just be a bit of refactoring, not changing the number of callbacks or anything like that. I'll try it out in a separate PR and see what it looks like 🙂 - outputs = [ - Output(component.id, "figure", allow_duplicate=True) - for component in self.components - if isinstance(component, Graph) - ] - if outputs: + # 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 = [component for component in self.components if hasattr(component, "_update_theme")] + if themed_components: - @callback(outputs, Input("theme_selector", "on"), prevent_initial_call="initial_duplicate") - def update_graph_theme(theme_selector_on: bool): - patched_figure = Patch() - patched_figure["layout"]["template"] = themes.dark if theme_selector_on else themes.light - return [patched_figure] * len(outputs) + @callback( + [Output(component.id, "figure", allow_duplicate=True) for component in themed_components], + Input("theme_selector", "on"), + prevent_initial_call="initial_duplicate", + ) + def update_graph_theme(theme_selector: bool): + return [component._update_theme(Patch(), theme_selector) for component in themed_components] def _create_component_container(self, components_content): component_container = html.Div( diff --git a/vizro-core/tests/unit/vizro/actions/test_on_page_load_action.py b/vizro-core/tests/unit/vizro/actions/test_on_page_load_action.py index 6e402a095..f9e3b70d1 100644 --- a/vizro-core/tests/unit/vizro/actions/test_on_page_load_action.py +++ b/vizro-core/tests/unit/vizro/actions/test_on_page_load_action.py @@ -80,7 +80,7 @@ def callback_context_on_page_load(request): "theme_selector": CallbackTriggerDict( id="theme_selector", property="on", - value=True if template == "vizro_dark" else False, + value=template == "vizro_dark", str_id="theme_selector", triggered=False, ), diff --git a/vizro-core/tests/unit/vizro/models/_components/test_graph.py b/vizro-core/tests/unit/vizro/models/_components/test_graph.py index 9694febe9..ed1b262c5 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_graph.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_graph.py @@ -1,14 +1,18 @@ """Unit tests for vizro.models.Graph.""" import json +import logging import plotly import plotly.graph_objects as go import pytest from dash import dcc +from dash._callback_context import context_value +from dash._utils import AttributeDict from pydantic import ValidationError import vizro.models as vm import vizro.plotly.express as px +from vizro.actions._actions_utils import CallbackTriggerDict from vizro.managers import data_manager from vizro.models._action._action import Action @@ -26,18 +30,6 @@ def standard_px_chart_with_str_dataframe(): ) -@pytest.fixture -def expected_empty_chart(): - figure = go.Figure() - figure.add_trace(go.Scatter(x=[None], y=[None], showlegend=False, hoverinfo="none")) - figure.update_layout( - xaxis={"visible": False}, - yaxis={"visible": False}, - annotations=[{"text": "NO DATA", "showarrow": False, "font": {"size": 16}}], - ) - return figure - - @pytest.fixture def expected_graph(): return dcc.Loading( @@ -74,11 +66,7 @@ def test_create_graph_mandatory_only(self, standard_px_chart): @pytest.mark.parametrize("id", ["id_1", "id_2"]) def test_create_graph_mandatory_and_optional(self, standard_px_chart, id): - graph = vm.Graph( - figure=standard_px_chart, - id=id, - actions=[], - ) + graph = vm.Graph(figure=standard_px_chart, id=id, actions=[]) assert graph.id == id assert graph.type == "graph" @@ -90,9 +78,7 @@ def test_mandatory_figure_missing(self): def test_failed_graph_with_wrong_figure(self, standard_go_chart): with pytest.raises(ValidationError, match="must provide a valid CapturedCallable object"): - vm.Graph( - figure=standard_go_chart, - ) + vm.Graph(figure=standard_go_chart) def test_getitem_known_args(self, standard_px_chart): graph = vm.Graph(figure=standard_px_chart) @@ -107,13 +93,36 @@ def test_getitem_unknown_args(self, standard_px_chart): @pytest.mark.parametrize("title, expected", [(None, 24), ("Test", None)]) def test_title_margin_adjustment(self, gapminder, title, expected): - figure = vm.Graph(figure=px.bar(data_frame=gapminder, x="year", y="pop", title=title)).__call__() - - assert figure.layout.margin.t == expected - assert figure.layout.template.layout.margin.t == 64 - assert figure.layout.template.layout.margin.l == 80 - assert figure.layout.template.layout.margin.b == 64 - assert figure.layout.template.layout.margin.r == 12 + graph = vm.Graph(figure=px.bar(data_frame=gapminder, x="year", y="pop", title=title)).__call__() + + assert graph.layout.margin.t == expected + assert graph.layout.template.layout.margin.t == 64 + assert graph.layout.template.layout.margin.l == 80 + assert graph.layout.template.layout.margin.b == 64 + assert graph.layout.template.layout.margin.r == 12 + + def test_update_theme_outside_callback(self, standard_px_chart, caplog): + caplog.set_level(logging.INFO) + graph = vm.Graph(figure=standard_px_chart).__call__() + assert "fig.update_layout called outside of callback context." in caplog.messages + assert graph == standard_px_chart.update_layout(margin_t=24, template="vizro_dark") + + @pytest.mark.parametrize("template", ["vizro_dark", "vizro_light"]) + def test_update_theme_inside_callback(self, standard_px_chart, template): + mock_callback_context = { + "args_grouping": { + "theme_selector": CallbackTriggerDict( + id="theme_selector", + property="on", + value=template == "vizro_dark", + str_id="theme_selector", + triggered=False, + ) + } + } + context_value.set(AttributeDict(**mock_callback_context)) + graph = vm.Graph(figure=standard_px_chart).__call__() + assert graph == standard_px_chart.update_layout(margin_t=24, template=template) def test_set_action_via_validator(self, standard_px_chart, test_action_function): graph = vm.Graph(figure=standard_px_chart, actions=[Action(function=test_action_function)]) @@ -124,18 +133,12 @@ def test_set_action_via_validator(self, standard_px_chart, test_action_function) class TestProcessFigureDataFrame: def test_process_figure_data_frame_str_df(self, standard_px_chart_with_str_dataframe, gapminder): data_manager["gapminder"] = gapminder - graph_with_str_df = vm.Graph( - id="text_graph", - figure=standard_px_chart_with_str_dataframe, - ) + graph_with_str_df = vm.Graph(id="text_graph", figure=standard_px_chart_with_str_dataframe) assert data_manager._get_component_data("text_graph").equals(gapminder) assert graph_with_str_df["data_frame"] == "gapminder" def test_process_figure_data_frame_df(self, standard_px_chart, gapminder): - graph_with_df = vm.Graph( - id="text_graph", - figure=standard_px_chart, - ) + graph_with_df = vm.Graph(id="text_graph", figure=standard_px_chart) assert data_manager._get_component_data("text_graph").equals(gapminder) with pytest.raises(KeyError, match="'data_frame'"): graph_with_df.figure["data_frame"] From e3915db34276d70f65d5fefb552a757c634534ad Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Wed, 15 Nov 2023 10:42:34 +0000 Subject: [PATCH 13/14] Remove caplog --- vizro-core/tests/unit/vizro/models/_components/test_graph.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vizro-core/tests/unit/vizro/models/_components/test_graph.py b/vizro-core/tests/unit/vizro/models/_components/test_graph.py index ed1b262c5..bf6a73b03 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_graph.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_graph.py @@ -101,10 +101,8 @@ def test_title_margin_adjustment(self, gapminder, title, expected): assert graph.layout.template.layout.margin.b == 64 assert graph.layout.template.layout.margin.r == 12 - def test_update_theme_outside_callback(self, standard_px_chart, caplog): - caplog.set_level(logging.INFO) + def test_update_theme_outside_callback(self, standard_px_chart): graph = vm.Graph(figure=standard_px_chart).__call__() - assert "fig.update_layout called outside of callback context." in caplog.messages assert graph == standard_px_chart.update_layout(margin_t=24, template="vizro_dark") @pytest.mark.parametrize("template", ["vizro_dark", "vizro_light"]) From 131b513ec85244f8d699d7046057a32eedb73e09 Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Wed, 15 Nov 2023 10:54:54 +0000 Subject: [PATCH 14/14] Lint --- vizro-core/tests/unit/vizro/models/_components/test_graph.py | 1 - 1 file changed, 1 deletion(-) diff --git a/vizro-core/tests/unit/vizro/models/_components/test_graph.py b/vizro-core/tests/unit/vizro/models/_components/test_graph.py index bf6a73b03..6f3e190f9 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_graph.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_graph.py @@ -1,6 +1,5 @@ """Unit tests for vizro.models.Graph.""" import json -import logging import plotly import plotly.graph_objects as go