From e1ca4ac772b9fc072f260a40d1ecb7fc62483b28 Mon Sep 17 00:00:00 2001 From: Lay-quq <187484661+Lay-quq@users.noreply.github.com> Date: Sun, 25 Oct 2026 17:41:35 +0800 Subject: [PATCH] fix: make the plotly backends work with Plotly 7 (#1243) Plotly 7 rejects string marker sizes, so latent, inducing and error-bar plots raised ValueError under 'plotly_offline'. Use numbers instead. The 'plotly' / 'plotly_online' library still called plotly.plotly (Chart Studio), which is no longer part of plotly. Its show_canvas now raises a RuntimeError that points at 'plotly_offline' or 'matplotlib'. --- CHANGELOG.md | 1 + GPy/plotting/plotly_dep/defaults.py | 6 +-- GPy/plotting/plotly_dep/plot_definitions.py | 27 +++++------- GPy/testing/test_plotting.py | 46 +++++++++++++++++++++ 4 files changed, 61 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5992ff73d..0363ab50d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +* plotting: make the plotly backends work with Plotly 7: marker sizes are numbers instead of strings (`plotly_offline` latent, inducing and error-bar plots raised `ValueError`), and showing a figure with the `'plotly'` / `'plotly_online'` library now raises a clear error pointing at `'plotly_offline'` or `'matplotlib'` instead of failing on the removed `plotly.plotly` Chart Studio API (#1243) * packaging: require Python >=3.11 (matches CI; 3.9/3.10 are EOL) so the lockfile can drop unpatched 3.9-only pins (Discussion #1207) ## v1.15.0 (2026-10-24) diff --git a/GPy/plotting/plotly_dep/defaults.py b/GPy/plotting/plotly_dep/defaults.py index e0d5b6b12..dbe032d9d 100644 --- a/GPy/plotting/plotly_dep/defaults.py +++ b/GPy/plotting/plotly_dep/defaults.py @@ -46,13 +46,13 @@ data_2d = dict(marker="o", cmap="Hot", marker_kwargs=dict(opacity=1.0, size=5, line=dict(width=0.5, color="black"))) inducing_1d = dict(color=Tango.colorsHex["darkRed"]) inducing_2d = dict( - marker_kwargs=dict(size="5", opacity=0.7, line=dict(width=0.5, color="black")), + marker_kwargs=dict(size=5, opacity=0.7, line=dict(width=0.5, color="black")), opacity=0.7, color="white", marker="star-triangle-up", ) inducing_3d = dict( - marker_kwargs=dict(symbol="diamond", size="5", opacity=0.7, line=dict(width=0.1, color="black")), color="#F5F5F5" + marker_kwargs=dict(symbol="diamond", size=5, opacity=0.7, line=dict(width=0.1, color="black")), color="#F5F5F5" ) xerrorbar = dict(color="black", error_kwargs=dict(thickness=0.5), opacity=0.5) yerrorbar = dict(color=Tango.colorsHex["darkRed"], error_kwargs=dict(thickness=0.5), opacity=0.5) @@ -82,5 +82,5 @@ latent = dict(colorscale="Greys", reversescale=True, zsmooth="best") gradient = dict(colorscale="RdBu", opacity=0.7) magnification = dict(colorscale="Greys", zsmooth="best", reversescale=True) -latent_scatter = dict(marker_kwargs=dict(size="5", opacity=0.7)) +latent_scatter = dict(marker_kwargs=dict(size=5, opacity=0.7)) # annotation = dict(fontdict=dict(family='sans-serif', weight='light', fontsize=9), zorder=.3, alpha=.7) diff --git a/GPy/plotting/plotly_dep/plot_definitions.py b/GPy/plotting/plotly_dep/plot_definitions.py index 025d90cdc..8a28e7d5a 100644 --- a/GPy/plotting/plotly_dep/plot_definitions.py +++ b/GPy/plotting/plotly_dep/plot_definitions.py @@ -283,7 +283,7 @@ def xerrorbar( z=Z, mode="markers", error_x=ErrorX(color=color, **error_kwargs or {}), - marker=dict(size="0"), + marker=dict(size=0), name=label, showlegend=label is not None, **kwargs, @@ -293,7 +293,7 @@ def xerrorbar( y=Y, mode="markers", error_x=ErrorX(color=color, **error_kwargs or {}), - marker=dict(size="0"), + marker=dict(size=0), name=label, showlegend=label is not None, **kwargs, @@ -316,7 +316,7 @@ def yerrorbar( z=Z, mode="markers", error_y=ErrorY(color=color, **error_kwargs or {}), - marker=dict(size="0"), + marker=dict(size=0), name=label, showlegend=label is not None, **kwargs, @@ -326,7 +326,7 @@ def yerrorbar( y=Y, mode="markers", error_y=ErrorY(color=color, **error_kwargs or {}), - marker=dict(size="0"), + marker=dict(size=0), name=label, showlegend=label is not None, **kwargs, @@ -499,18 +499,13 @@ def __init__(self): super(PlotlyPlotsOnline, self).__init__() def show_canvas(self, canvas, filename=None, **kwargs): - figure, _, _ = canvas - if len(figure.data) == 0: - # add mock data - figure.add_trace(Scatter(x=[], y=[], name="", showlegend=False), row=1, col=1) - from ..gpy_plot.plot_util import in_ipynb - - if in_ipynb(): - return plotly.plotly.iplot(figure, filename=filename, **kwargs) - else: - return plotly.plotly.plot( - figure, filename=filename, **kwargs - ) # self.current_states[hex(id(figure))]['filename']) + # plotly.plotly (Chart Studio upload) is no longer part of plotly (#1243). + raise RuntimeError( + "The 'plotly' (online) plotting library relied on plotly.plotly (Chart Studio), " + "which is not part of current plotly releases. Use " + "GPy.plotting.change_plotting_library('plotly_offline') or " + "GPy.plotting.change_plotting_library('matplotlib') instead." + ) class PlotlyPlotsOffline(PlotlyPlotsBase): diff --git a/GPy/testing/test_plotting.py b/GPy/testing/test_plotting.py index dc70d29a5..7e22dfc14 100644 --- a/GPy/testing/test_plotting.py +++ b/GPy/testing/test_plotting.py @@ -107,6 +107,52 @@ def test_plotly_offline_loads_without_deprecation(self): self.teardown_method() +class TestPlotly7: + """#1243: the plotly backends must work with current plotly (no Chart Studio, numeric marker sizes).""" + + def setup_method(self): + pytest.importorskip("plotly") + np.random.seed(1243) + + def teardown_method(self): + if matplotlib is not None: + change_plotting_library("matplotlib") + else: + change_plotting_library("none") + + def test_online_show_names_offline_alternatives(self): + change_plotting_library("plotly") + lib = plotting_library() + canvas, _ = lib.new_canvas() + with pytest.raises(RuntimeError, match="plotly_offline") as err: + lib.show_canvas(canvas) + assert "matplotlib" in str(err.value) + + @pytest.mark.parametrize("projection", ["2d", "3d"]) + def test_offline_inducing_marker_size(self, projection): + change_plotting_library("plotly_offline") + m = GPy.models.SparseGPRegression(np.random.rand(20, 2), np.random.rand(20, 1), num_inducing=5) + figure, _, _ = m.plot_inducing(projection=projection) + assert figure.data[0].marker.size == 5 + + def test_offline_latent_scatter_marker_size(self): + change_plotting_library("plotly_offline") + m = GPy.models.GPLVM(np.random.rand(15, 4), 2) + figure, _, _ = m.plot_latent() + assert [t.marker.size for t in figure.data if t.type == "scatter"] == [5] + + @pytest.mark.parametrize("which", ["xerrorbar", "yerrorbar"]) + @pytest.mark.parametrize("projection", ["2d", "3d"]) + def test_offline_errorbar_marker_size(self, which, projection): + change_plotting_library("plotly_offline") + lib = plotting_library() + canvas, _ = lib.new_canvas(projection=projection) + X, Y, error = np.random.rand(5, 1), np.random.rand(5, 1), np.random.rand(5) + Z = np.random.rand(5, 1) if projection == "3d" else None + trace = getattr(lib, which)(canvas, X, Y, error, Z=Z) + assert trace.marker.size == 0 + + @pytest.mark.skipif(matplotlib is None, reason="Matplotlib not installed") class TestShowAcceptsPlotDict: """#920: model.plot() returns a plots dict; show() must accept it (and Axes)."""