diff --git a/CHANGELOG.md b/CHANGELOG.md index 0583fec45c..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 diff --git a/plotly/matplotlylib/mpltools.py b/plotly/matplotlylib/mpltools.py index ad01b37520..e1198c9792 100644 --- a/plotly/matplotlylib/mpltools.py +++ b/plotly/matplotlylib/mpltools.py @@ -269,7 +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: - return gap0 + # 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): diff --git a/plotly/matplotlylib/tests/test_renderer.py b/plotly/matplotlylib/tests/test_renderer.py index 18ce2d02b3..02dd6cf34a 100644 --- a/plotly/matplotlylib/tests/test_renderer.py +++ b/plotly/matplotlylib/tests/test_renderer.py @@ -235,6 +235,21 @@ 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.""" + # Seed 0 makes the first gap slightly negative (-4.4e-16) + rng = np.random.RandomState(0) + fig, ax = plt.subplots() + ax.hist(rng.randn(10000), 30) + + plotly_fig = tls.mpl_to_plotly(fig) + + assert len(plotly_fig.data) == 1 + assert plotly_fig.layout.bargap == 0 + + 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."""