From 34488dd6ba07c4741a9262dece3de388de654148 Mon Sep 17 00:00:00 2001 From: Roberto Moura Date: Tue, 11 Aug 2026 10:07:30 +0000 Subject: [PATCH] Fix boxplot conversion by mapping 'none' colors to transparent rgba --- plotly/matplotlylib/renderer.py | 38 +++++++++++++++++----- plotly/matplotlylib/tests/test_renderer.py | 9 +++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/plotly/matplotlylib/renderer.py b/plotly/matplotlylib/renderer.py index 7c2340180cc..c14a8e78d0f 100644 --- a/plotly/matplotlylib/renderer.py +++ b/plotly/matplotlylib/renderer.py @@ -14,6 +14,24 @@ from plotly.matplotlylib import mpltools +def _export_color(color): + """Export a matplotlib color for use as a plotly color. + + matplotlib uses "none" for fully transparent colors, which plotly does not + accept, so transparent colors are exported as transparent black. + Colors already exported by the mplexporter (hex or rgba strings) are + passed through unchanged. + """ + if isinstance(color, str): + return "rgba(0,0,0,0)" if color == "none" else color + if isinstance(color, (list, tuple)) and all( + isinstance(c, str) for c in color + ): + return [_export_color(c) for c in color] + bgcolor = export_color(color) + return "rgba(0,0,0,0)" if bgcolor == "none" else bgcolor + + class PlotlyRenderer(Renderer): """A renderer class inheriting from base for rendering mpl plots in plotly. @@ -317,7 +335,7 @@ def draw_bar(self, coll): yaxis="y{0}".format(self.axis_ct), opacity=trace[0]["alpha"], # TODO: get all alphas if array? marker=go.bar.Marker( - color=trace[0]["facecolor"], # TODO: get all + color=_export_color(trace[0]["facecolor"]), # TODO: get all line=dict(width=trace[0]["edgewidth"]), ), ) # TODO ditto @@ -379,9 +397,13 @@ def draw_marked_line(self, **props): self.msg += "... with just markers\n" mode = "markers" if props["linestyle"]: - color = mpltools.merge_color_and_opacity( - props["linestyle"]["color"], props["linestyle"]["alpha"] - ) + if props["linestyle"]["color"] == "none": + # a fully transparent line; plotly rejects "none" as a color + color = "rgba(0,0,0,0)" + else: + color = mpltools.merge_color_and_opacity( + props["linestyle"]["color"], props["linestyle"]["alpha"] + ) if props["coordinates"] == "data": line = go.scatter.Line( @@ -401,22 +423,22 @@ def draw_marked_line(self, **props): if props["coordinates"] == "data": marker = go.scatter.Marker( opacity=props["markerstyle"]["alpha"], - color=props["markerstyle"]["facecolor"], + color=_export_color(props["markerstyle"]["facecolor"]), symbol=mpltools.convert_symbol(props["markerstyle"]["marker"]), size=props["markerstyle"]["markersize"], line=dict( - color=props["markerstyle"]["edgecolor"], + color=_export_color(props["markerstyle"]["edgecolor"]), width=props["markerstyle"]["edgewidth"], ), ) else: shape = dict( opacity=props["markerstyle"]["alpha"], - fillcolor=props["markerstyle"]["facecolor"], + fillcolor=_export_color(props["markerstyle"]["facecolor"]), symbol=mpltools.convert_symbol(props["markerstyle"]["marker"]), size=props["markerstyle"]["markersize"], line=dict( - color=props["markerstyle"]["edgecolor"], + color=_export_color(props["markerstyle"]["edgecolor"]), width=props["markerstyle"]["edgewidth"], ), ) diff --git a/plotly/matplotlylib/tests/test_renderer.py b/plotly/matplotlylib/tests/test_renderer.py index 0d63e4815b9..18d9a8f627d 100644 --- a/plotly/matplotlylib/tests/test_renderer.py +++ b/plotly/matplotlylib/tests/test_renderer.py @@ -1,3 +1,4 @@ +import numpy as np import matplotlib.pyplot as plt import plotly.tools as tls @@ -84,3 +85,11 @@ def test_multiple_traces_native_legend(): assert plotly_fig.data[0].mode == "lines" assert plotly_fig.data[1].mode == "markers" assert plotly_fig.data[2].mode == "lines+markers" + + +def test_boxplot_converts_with_none_marker_facecolor(): + """Boxplot outlier markers use facecolor 'none', which plotly rejects.""" + fig, ax = plt.subplots() + ax.boxplot(np.random.randn(100, 4)) + plotly_fig = tls.mpl_to_plotly(fig) # used to raise ValueError + assert len(plotly_fig.data) > 0