From 76a5ce1998601a44763f5f5a140363a0c1a7f92b Mon Sep 17 00:00:00 2001 From: Roberto Moura Date: Mon, 10 Aug 2026 19:17:55 +0000 Subject: [PATCH 1/5] Clamp bargap to [0, 1] in get_bar_gap --- plotly/matplotlylib/mpltools.py | 4 +++- plotly/matplotlylib/tests/test_renderer.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/plotly/matplotlylib/mpltools.py b/plotly/matplotlylib/mpltools.py index ad01b37520..bf7316bd23 100644 --- a/plotly/matplotlylib/mpltools.py +++ b/plotly/matplotlylib/mpltools.py @@ -269,7 +269,9 @@ def get_bar_gap(bar_starts, bar_ends, tol=1e-10): gap0 = gaps[0] uniform = all([abs(gap0 - gap) < tol for gap in gaps]) if uniform: - return gap0 + # plotly's bargap must be in [0, 1]; clamp to guard against + # floating point noise (e.g. -8.9e-16 for touching bars) + return min(max(gap0, 0.0), 1.0) def convert_rgba_array(color_list): diff --git a/plotly/matplotlylib/tests/test_renderer.py b/plotly/matplotlylib/tests/test_renderer.py index 18ce2d02b3..e2eaa74f85 100644 --- a/plotly/matplotlylib/tests/test_renderer.py +++ b/plotly/matplotlylib/tests/test_renderer.py @@ -235,6 +235,19 @@ def test_semitransparent_axes_background_preserved(): assert plotly_fig.layout.plot_bgcolor == "rgba(26, 51, 76, 0.4)" +def test_histogram_converts(): + """Histograms must convert without error and keep bargap in plotly's + valid [0, 1] range; get_bar_gap can return a gap with floating point + noise for touching bars, which plotly rejects.""" + fig, ax = plt.subplots() + ax.hist(np.random.randn(1000), 30) + + plotly_fig = tls.mpl_to_plotly(fig) + + assert len(plotly_fig.data) == 1 + assert 0 <= plotly_fig.layout.bargap <= 1 + + def test_line_color_is_valid_plotly_color(): """Converted line colors are valid plotly color strings: plotly rejects a space between 'rgba' and the opening parenthesis.""" From 58f5c3dd38b91c606861706caa017772c5317314 Mon Sep 17 00:00:00 2001 From: Roberto Moura Date: Sun, 4 Oct 2026 03:27:07 +0100 Subject: [PATCH 2/5] Normalize gap0 to calculate the correct bargap Co-authored-by: Cameron DeCoster --- plotly/matplotlylib/mpltools.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plotly/matplotlylib/mpltools.py b/plotly/matplotlylib/mpltools.py index bf7316bd23..e1198c9792 100644 --- a/plotly/matplotlylib/mpltools.py +++ b/plotly/matplotlylib/mpltools.py @@ -269,9 +269,13 @@ def get_bar_gap(bar_starts, bar_ends, tol=1e-10): gap0 = gaps[0] uniform = all([abs(gap0 - gap) < tol for gap in gaps]) if uniform: - # plotly's bargap must be in [0, 1]; clamp to guard against - # floating point noise (e.g. -8.9e-16 for touching bars) - return min(max(gap0, 0.0), 1.0) + # Plotly's `bargap` is a fraction of the distance between bar positions, not a gap in data units + # so we need to normalize `gap0` with `bar_delta` + bar_delta = bar_starts[1] - bar_starts[0] + if bar_delta <= 0: + return None + # Clamp to guard against floating point noise, such as -8.9e-16 for touching bars + return min(max(gap0 / bar_delta, 0.0), 1.0) def convert_rgba_array(color_list): From d9b5b9a9dd24810ab754c7ce54e727084de664c4 Mon Sep 17 00:00:00 2001 From: Roberto Moura Date: Sun, 4 Oct 2026 03:28:12 +0100 Subject: [PATCH 3/5] Make bargap test deterministic Co-authored-by: Cameron DeCoster --- plotly/matplotlylib/tests/test_renderer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plotly/matplotlylib/tests/test_renderer.py b/plotly/matplotlylib/tests/test_renderer.py index e2eaa74f85..02dd6cf34a 100644 --- a/plotly/matplotlylib/tests/test_renderer.py +++ b/plotly/matplotlylib/tests/test_renderer.py @@ -239,13 +239,15 @@ def test_histogram_converts(): """Histograms must convert without error and keep bargap in plotly's valid [0, 1] range; get_bar_gap can return a gap with floating point noise for touching bars, which plotly rejects.""" + # Seed 0 makes the first gap slightly negative (-4.4e-16) + rng = np.random.RandomState(0) fig, ax = plt.subplots() - ax.hist(np.random.randn(1000), 30) + ax.hist(rng.randn(10000), 30) plotly_fig = tls.mpl_to_plotly(fig) assert len(plotly_fig.data) == 1 - assert 0 <= plotly_fig.layout.bargap <= 1 + assert plotly_fig.layout.bargap == 0 def test_line_color_is_valid_plotly_color(): From f35beb3c4a6f23f6896ef1ac3097f419626d015e Mon Sep 17 00:00:00 2001 From: Roberto Moura Date: Sun, 4 Oct 2026 02:31:29 +0000 Subject: [PATCH 4/5] Add changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0583fec45c..87ffd419af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +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 `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! ## [7.1.0] - 2026-09-15 From a100e3204410e29d250ea6ed91693edddf7912f2 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Mon, 5 Oct 2026 19:43:34 -0600 Subject: [PATCH 5/5] Fix order of CHANGELOG items (and some links) --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87ffd419af..ea837e3d0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased ### 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 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 `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