From 913c7cc1b7e86b3c0b528fe56c48f3412e9a691f Mon Sep 17 00:00:00 2001 From: Jay Zhou <190699450+jayzhou2309@users.noreply.github.com> Date: Tue, 29 Sep 2026 02:50:58 +0800 Subject: [PATCH 1/2] Add failing test for overall trendline on marginal subplots Co-Authored-By: Claude Opus 5.5 --- tests/test_optional/test_px/test_trendline.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_optional/test_px/test_trendline.py b/tests/test_optional/test_px/test_trendline.py index 48ff6ee3ba1..6a47a59424d 100644 --- a/tests/test_optional/test_px/test_trendline.py +++ b/tests/test_optional/test_px/test_trendline.py @@ -264,3 +264,34 @@ def test_overall_trendline(backend): params3 = results3["px_fit_results"].iloc[0].params assert np.all(np.array_equal(params1, params3)) + + +@pytest.mark.parametrize( + "kwargs", + [ + dict(marginal_x="box", marginal_y="box"), + dict(marginal_x="histogram", facet_col="smoker"), + dict(marginal_y="violin", facet_row="smoker"), + ], +) +def test_overall_trendline_skips_marginals(backend, kwargs): + df = px.data.tips(return_type=backend) + fig = px.scatter( + df, + x="total_bill", + y="tip", + color="sex", + trendline="ols", + trendline_scope="overall", + **kwargs, + ) + trendline_axes = [ + (t.xaxis, t.yaxis) for t in fig.data if t.name == "Overall Trendline" + ] + scatter_axes = { + (t.xaxis, t.yaxis) + for t in fig.data + if t.type == "scatter" and t.name != "Overall Trendline" + } + assert len(trendline_axes) == len(scatter_axes) + assert set(trendline_axes) == scatter_axes From 32c6fb164f54aaeac6af94c1008cfcc83c02a5c1 Mon Sep 17 00:00:00 2001 From: Jay Zhou <190699450+jayzhou2309@users.noreply.github.com> Date: Tue, 29 Sep 2026 02:52:01 +0800 Subject: [PATCH 2/2] Keep overall trendline off marginal subplots make_figure added the overall trendline with row="all", col="all", so it landed on every non-empty subplot, including the marginal_x row and marginal_y column. Add it only to the main (facet) cells. Closes #4669 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + plotly/express/_core.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0583fec45c8..8e2fd1b8c80 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")` [[#3441](https://github.com/plotly/plotly.py/issues/3441)], with thanks to @hb1915 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 [[#5765](https://github.com/plotly/plotly.py/issues/5765)], with thanks to @Irahan2 for the contribution! +- Fix `trendline_scope="overall"` drawing the overall trendline on the `marginal_x`/`marginal_y` subplots as well as the main plot; it is now drawn only on the main subplots [[#4669](https://github.com/plotly/plotly.py/issues/4669)] ## [7.1.0] - 2026-09-15 diff --git a/plotly/express/_core.py b/plotly/express/_core.py index e99bf40b874..dafb4d5d5b2 100644 --- a/plotly/express/_core.py +++ b/plotly/express/_core.py @@ -2833,9 +2833,13 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None): args, trendline_spec, args["data_frame"], {}, sizeref ) trendline_trace.update(patch) - fig.add_trace( - trendline_trace, row="all", col="all", exclude_empty_subplots=True - ) + main_rows = nrows - 1 if args.get("marginal_x") is not None else nrows + main_cols = ncols - 1 if args.get("marginal_y") is not None else ncols + for row in range(1, main_rows + 1): + for col in range(1, main_cols + 1): + fig.add_trace( + trendline_trace, row=row, col=col, exclude_empty_subplots=True + ) fig.update_traces(selector=-1, showlegend=True) if fit_results is not None: trendline_rows.append(dict(px_fit_results=fit_results))