diff --git a/CHANGELOG.md b/CHANGELOG.md index 1632f92203..b7ecb4bc2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Add support for converting matplotlib reference lines (`axhline`, `axvline`, and horizontal/vertical `axline`) as well as axes-coordinate lines into Plotly layout shapes in `mpl_to_plotly`, including support for date axes [[#5719](https://github.com/plotly/plotly.py/pull/5719)], with thanks to @robertoffmoura for the contribution! ### Fixed +- Preserve distinct Plotly Express legend entries and groups when grouping columns have the same display label ([#2562](https://github.com/plotly/plotly.py/issues/2562)). - Fix `mpl_to_plotly` tick marker mirroring, axis positioning, and trace visibility for twinned and overlaid subplots (such as `twinx` and `twiny`) [[#5310](https://github.com/plotly/plotly.py/pull/5310)], with thanks to @robertoffmoura for the contribution! - Fix `mpl_to_plotly` losing matplotlib axis line colors by exporting the color of each axis's displayed spine to the plotly x and y axis `linecolor` [[#5311](https://github.com/plotly/plotly.py/pull/5311)], with thanks to @robertoffmoura for the contribution! - 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! diff --git a/plotly/express/_core.py b/plotly/express/_core.py index 9994720b09..6feaef2f84 100644 --- a/plotly/express/_core.py +++ b/plotly/express/_core.py @@ -2594,7 +2594,7 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None): if not isinstance(m.val_map, IdentityMap): mapping_labels[key] = str(val) if m.show_in_trace_name: - trace_name_labels[key] = str(val) + trace_name_labels[col] = str(val) if m.variable == "animation_frame": frame_name = val trace_name = ", ".join(trace_name_labels.values()) @@ -2768,7 +2768,9 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None): layout_patch[v] = args[v] layout_patch["legend"] = dict(tracegroupgap=0) if trace_name_labels: - layout_patch["legend"]["title_text"] = ", ".join(trace_name_labels) + layout_patch["legend"]["title_text"] = ", ".join( + get_label(args, column) for column in trace_name_labels + ) if args["title"]: layout_patch["title_text"] = args["title"] elif args["template"].layout.margin.t is None: diff --git a/tests/test_optional/test_px/test_px.py b/tests/test_optional/test_px/test_px.py index d4773de66f..3db792a123 100644 --- a/tests/test_optional/test_px/test_px.py +++ b/tests/test_optional/test_px/test_px.py @@ -92,6 +92,41 @@ def test_labels(backend): assert fig.layout.annotations[4].text.startswith("TIME") +@pytest.mark.parametrize( + "labels", [{"color": "Group", "symbol": "Group"}, {"color": "symbol"}] +) +def test_legend_with_duplicate_labels(constructor, labels): + df = constructor( + { + "x": [1, 2, 3, 4], + "y": [4, 3, 2, 1], + "color": ["A", "A", "B", "B"], + "symbol": ["one", "two", "one", "two"], + } + ) + fig = px.scatter(df, x="x", y="y", color="color", symbol="symbol", labels=labels) + + expected_names = ["A, one", "A, two", "B, one", "B, two"] + assert [trace.name for trace in fig.data] == expected_names + assert [trace.legendgroup for trace in fig.data] == expected_names + assert all(trace.showlegend for trace in fig.data) + assert fig.layout.legend.title.text == ", ".join( + labels.get(column, column) for column in ["color", "symbol"] + ) + + +def test_legend_with_same_column_for_color_and_symbol(constructor): + df = constructor({"x": [1, 2], "y": [3, 4], "group": ["A", "B"]}) + fig = px.scatter( + df, x="x", y="y", color="group", symbol="group", labels={"group": "Group"} + ) + + assert [trace.name for trace in fig.data] == ["A", "B"] + assert [trace.legendgroup for trace in fig.data] == ["A", "B"] + assert all(trace.showlegend for trace in fig.data) + assert fig.layout.legend.title.text == "Group" + + @pytest.mark.parametrize( ["extra_kwargs", "expected_mode"], [