From 63677956be619e0496c2fa738e136bf2c751a1df Mon Sep 17 00:00:00 2001 From: Roberto Moura Date: Tue, 11 Aug 2026 10:25:15 +0000 Subject: [PATCH 1/3] Only show a legend when the matplotlib figure has one --- plotly/matplotlylib/renderer.py | 15 ++++++++++----- plotly/matplotlylib/tests/test_renderer.py | 11 +++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/plotly/matplotlylib/renderer.py b/plotly/matplotlylib/renderer.py index 65bbcfabb1..a804adcc4d 100644 --- a/plotly/matplotlylib/renderer.py +++ b/plotly/matplotlylib/renderer.py @@ -100,6 +100,10 @@ def open_figure(self, fig, props): height=int(props["figheight"] * props["dpi"]), autosize=False, hovermode="closest", + # plotly.js auto-names unnamed traces "trace N" and shows them + # in the legend; the legend is only enabled when the mpl figure + # actually has one (see open_legend) + showlegend=False, ) self.plotly_fig["layout"].paper_bgcolor = _export_color(props["figbg"]) self.mpl_x_bounds, self.mpl_y_bounds = mpltools.get_axes_bounds(fig) @@ -440,13 +444,14 @@ def draw_marked_line(self, **props): ), ) if props["coordinates"] == "data": + label = props["label"] + # matplotlib uses "_nolegend_" and auto-generated "_childN" + # labels for artists that must not appear in a legend + if isinstance(label, str) and label.startswith("_"): + label = None marked_line = go.Scatter( mode=mode, - name=( - str(props["label"]) - if isinstance(props["label"], str) - else props["label"] - ), + name=label, x=[xy_pair[0] for xy_pair in props["data"]], y=[xy_pair[1] for xy_pair in props["data"]], xaxis="x{0}".format(self.axis_ct), diff --git a/plotly/matplotlylib/tests/test_renderer.py b/plotly/matplotlylib/tests/test_renderer.py index 02dd6cf34a..0a1141ce05 100644 --- a/plotly/matplotlylib/tests/test_renderer.py +++ b/plotly/matplotlylib/tests/test_renderer.py @@ -312,6 +312,17 @@ def test_fixed_formatter_ticktext(): assert plotly_fig.layout.xaxis.ticktext == ("Baseline", "param = 1", "param = 2") +def test_no_legend_entries_for_internal_mpl_labels(): + """mpl internal labels (_nolegend_, _childN) must not become legend entries.""" + fig, ax = plt.subplots() + ax.plot([0, 1, 2, 3], [0, 1, 0, 1], "b", [0, 1, 2, 3], [1, 0, 1, 0], "r--") + + plotly_fig = tls.mpl_to_plotly(fig) + + assert plotly_fig.layout.showlegend == False + assert all(t.name is None for t in plotly_fig.data) + + def test_custom_date_xtickvals_are_converted(): """Custom tick values on a date axis must be converted to date strings, not left as raw matplotlib date numbers or datetime objects.""" From e5d95bf7e24d26e65cccbd8e861241760161c41d Mon Sep 17 00:00:00 2001 From: Roberto Moura Date: Sun, 4 Oct 2026 02:47:32 +0000 Subject: [PATCH 2/3] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea837e3d0b..b26cdec264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fix concurrent first access to lazily initialized graph object properties, which could raise `ValueError("Invalid value")` [[#5691](https://github.com/plotly/plotly.py/pull/5691)], with thanks to @hb1915 for the contribution! - Fix `mpl_to_plotly` crashing on touching bars (such as `plt.hist`) due to floating-point noise producing negative `bargap` values by clamping `bargap` to `[0, 1]` [[#5696](https://github.com/plotly/plotly.py/pull/5696)], with thanks to @robertoffmoura for the contribution! +- Fix `mpl_to_plotly` showing unwanted legends with placeholder or internal label entries (`_nolegend_`, `_childN`) by defaulting `layout.showlegend=False` unless the matplotlib figure explicitly defines a legend [[#5699](https://github.com/plotly/plotly.py/pull/5699)], with thanks to @robertoffmoura for the contribution! - Fix `px.sunburst`, `px.treemap` and `px.icicle` listing sectors in a different order on every run when `path` is used with a Polars DataFrame; sectors now follow their order of first appearance for all dataframe backends [[#5766](https://github.com/plotly/plotly.py/pull/5766)], with thanks to @Irahan2 for the contribution! ## [7.1.0] - 2026-09-15 From e3ad1e71cca184547e3474a7f6f3c514da44054a Mon Sep 17 00:00:00 2001 From: Roberto Moura Date: Tue, 6 Oct 2026 06:13:10 +0000 Subject: [PATCH 3/3] Hide unlabeled traces from legend when figure has a legend --- plotly/matplotlylib/renderer.py | 6 +++++- plotly/matplotlylib/tests/test_renderer.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/plotly/matplotlylib/renderer.py b/plotly/matplotlylib/renderer.py index a804adcc4d..4b1278f03e 100644 --- a/plotly/matplotlylib/renderer.py +++ b/plotly/matplotlylib/renderer.py @@ -447,11 +447,15 @@ def draw_marked_line(self, **props): label = props["label"] # matplotlib uses "_nolegend_" and auto-generated "_childN" # labels for artists that must not appear in a legend - if isinstance(label, str) and label.startswith("_"): + if not label or (isinstance(label, str) and label.startswith("_")): label = None + showlegend = False + else: + showlegend = None marked_line = go.Scatter( mode=mode, name=label, + showlegend=showlegend, x=[xy_pair[0] for xy_pair in props["data"]], y=[xy_pair[1] for xy_pair in props["data"]], xaxis="x{0}".format(self.axis_ct), diff --git a/plotly/matplotlylib/tests/test_renderer.py b/plotly/matplotlylib/tests/test_renderer.py index 0a1141ce05..376a1c7c4f 100644 --- a/plotly/matplotlylib/tests/test_renderer.py +++ b/plotly/matplotlylib/tests/test_renderer.py @@ -323,6 +323,23 @@ def test_no_legend_entries_for_internal_mpl_labels(): assert all(t.name is None for t in plotly_fig.data) +def test_unlabeled_traces_hidden_from_legend_when_figure_has_legend(): + """Traces without labels must have showlegend=False when a figure has a legend.""" + fig, ax = plt.subplots() + ax.plot([0, 1], [0, 1], label="Labeled line") + ax.plot([0, 1], [1, 0]) # Unlabeled line + ax.legend() + + plotly_fig = tls.mpl_to_plotly(fig) + + assert plotly_fig.layout.showlegend == True + assert len(plotly_fig.data) == 2 + assert plotly_fig.data[0].name == "Labeled line" + assert plotly_fig.data[0].showlegend is not False + assert plotly_fig.data[1].name is None + assert plotly_fig.data[1].showlegend is False + + def test_custom_date_xtickvals_are_converted(): """Custom tick values on a date axis must be converted to date strings, not left as raw matplotlib date numbers or datetime objects."""