diff --git a/cli/openbb_cli/outputs/figure.py b/cli/openbb_cli/outputs/figure.py index dacd406552e..125ccd9bfef 100644 --- a/cli/openbb_cli/outputs/figure.py +++ b/cli/openbb_cli/outputs/figure.py @@ -39,6 +39,31 @@ def is_plotly_figure(data: Any) -> bool: ) +def strip_theme_colors(layout: Any) -> Any: + """Remove the server-side theme from a figure layout. + + Parameters + ---------- + layout : Any + A Plotly layout, or any value nested inside one. + + Returns + ------- + Any + A copy without the embedded ``template`` or any ``*color`` setting, so + the displaying window's own light and dark templates style the chart. + """ + if isinstance(layout, dict): + return { + key: strip_theme_colors(value) + for key, value in layout.items() + if key != "template" and not key.lower().endswith("color") + } + if isinstance(layout, list): + return [strip_theme_colors(value) for value in layout] + return layout + + def _plotlyjs_tag() -> str: """Return the script tag that loads plotly.js, inlined when plotly is installed. @@ -94,7 +119,9 @@ def show_figure(figure: dict, title: str = "") -> None: from plotly.graph_objects import Figure backend.send_figure( - Figure(data=figure["data"], layout=figure["layout"]), + Figure( + data=figure["data"], layout=strip_theme_colors(figure["layout"]) + ), command_location=title, ) return diff --git a/cli/tests/test_outputs_figure.py b/cli/tests/test_outputs_figure.py index 89b7195ee56..43fa73bb62c 100644 --- a/cli/tests/test_outputs_figure.py +++ b/cli/tests/test_outputs_figure.py @@ -7,7 +7,12 @@ from unittest.mock import MagicMock, patch import pytest from openbb_cli.outputs import figure as figure_module -from openbb_cli.outputs.figure import is_plotly_figure, render_figure_html, show_figure +from openbb_cli.outputs.figure import ( + is_plotly_figure, + render_figure_html, + show_figure, + strip_theme_colors, +) FIGURE = { "data": [{"type": "scatter", "x": [1, 2], "y": [3, 4], "name": ""}], @@ -92,6 +97,25 @@ class TestShowFigure: == "/cboe/options/term_structure" ) + def test_backend_figure_drops_server_theme_colors(self, mock_session): + themed = { + "data": [{"type": "scatter", "y": [1, 2], "line": {"color": "red"}}], + "layout": { + "template": {"layout": {"font": {"color": "#2a3f5f"}}}, + "font": {"color": "white", "size": 12}, + "paper_bgcolor": "rgba(0,0,0,0)", + "xaxis": {"linecolor": "white", "title": {"text": "Date"}}, + }, + } + show_figure(themed, "t") + fig = mock_session.backend.send_figure.call_args[0][0] + assert fig.layout.font.color is None + assert fig.layout.font.size == 12 + assert fig.layout.paper_bgcolor is None + assert fig.layout.xaxis.linecolor is None + assert fig.layout.xaxis.title.text == "Date" + assert fig.data[0].line.color == "red" + def test_backend_failure_opens_browser(self, mock_session): mock_session.backend.send_figure.side_effect = RuntimeError("no window") with patch("openbb_cli.outputs.figure.webbrowser.open") as open_browser: @@ -112,3 +136,26 @@ class TestShowFigure: show_figure(FIGURE, "t") open_browser.assert_called_once_with((tmp_path / "chart.html").as_uri()) assert "Plotly.newPlot" in (tmp_path / "chart.html").read_text() + + +class TestStripThemeColors: + """Removing server-side theme styling from a layout.""" + + def test_removes_template_and_color_keys_recursively(self): + layout = { + "template": {"layout": {}}, + "paper_bgcolor": "black", + "colorway": ["#111", "#222"], + "annotations": [{"text": "a", "font": {"color": "white", "size": 9}}], + "legend": {"bgcolor": "black", "orientation": "h"}, + } + assert strip_theme_colors(layout) == { + "colorway": ["#111", "#222"], + "annotations": [{"text": "a", "font": {"size": 9}}], + "legend": {"orientation": "h"}, + } + + def test_leaves_the_input_unchanged(self): + layout = {"font": {"color": "white"}} + strip_theme_colors(layout) + assert layout == {"font": {"color": "white"}}