Skip to content

fix: Fix flaky matplotlib-to-plotly conversion of touching bars (negative bargap) - #5696

Open
robertoffmoura wants to merge 1 commit into
plotly:mainfrom
robertoffmoura:rm/fix-negative-bargap
Open

robertoffmoura wants to merge 1 commit into
plotly:mainfrom
robertoffmoura:rm/fix-negative-bargap

Conversation

@robertoffmoura

Copy link
Copy Markdown
Contributor

mpl_to_plotly intermittently crashes when converting figures with touching bars (e.g. plt.hist). The bar gap is computed as bar_start[i+1] - bar_end[i], and for adjacent bars floating-point noise can make this a tiny negative number (e.g. -8.88e-16). Plotly's bargap property only accepts values in [0, 1], so the conversion fails with:

ValueError: Invalid value of type 'numpy.float64' received for the 'bargap' property of layout
    Received value: np.float64(-8.881784197001252e-16)

Reproduces with:

import warnings
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import plotly.tools as tls

np.random.seed(0)
plt.hist(np.random.randn(10000), 30)

with warnings.catch_warnings():
    warnings.simplefilter("ignore")
    tls.mpl_to_plotly(plt.gcf())

Fix: get_bar_gap now clamps the gap to [0, 1], so float noise around zero maps to bargap = 0 (touching bars) and genuine gaps are unchanged.

@camdecoster camdecoster left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please make a couple of changes and add a CHANGELOG entry?

Comment on lines +272 to +274
# 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to normalize gap0 to calculate the correct bargap.

Suggested change
# 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)

Comment on lines +242 to +248
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this test deterministic:

Suggested change
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
# 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

@camdecoster camdecoster changed the title Fix flaky matplotlib-to-plotly conversion of touching bars (negative bargap) fix: Fix flaky matplotlib-to-plotly conversion of touching bars (negative bargap) Sep 24, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants