diff --git a/CHANGELOG.md b/CHANGELOG.md index 331ba9c3a0..a0f64acfe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - 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` drawing step plots with diagonal lines instead of vertical/horizontal step segments by mapping matplotlib step drawstyles (`steps-pre`, `steps-post`, `steps-mid`) to plotly line shapes [[#5697](https://github.com/plotly/plotly.py/pull/5697)], 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! - Export tick label colors to the plotly tickfont in `mpl_to_plotly` [[#5716](https://github.com/plotly/plotly.py/pull/5716)], 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! diff --git a/plotly/matplotlylib/renderer.py b/plotly/matplotlylib/renderer.py index af0b8baac1..fb7b565ebb 100644 --- a/plotly/matplotlylib/renderer.py +++ b/plotly/matplotlylib/renderer.py @@ -91,6 +91,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) @@ -485,13 +489,18 @@ 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 not label or (isinstance(label, str) and label.startswith("_")): + label = None + showlegend = False + else: + showlegend = None marked_line = go.Scatter( mode=mode, - name=( - str(props["label"]) - if isinstance(props["label"], str) - else props["label"] - ), + 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 848ba4e7c0..a64e2d3398 100644 --- a/plotly/matplotlylib/tests/test_renderer.py +++ b/plotly/matplotlylib/tests/test_renderer.py @@ -527,6 +527,34 @@ 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_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."""