Repository navigation
[BUG]: Bar traces draw in O(n²) time since v4.0.0 (getZeroBarDir runs once per bar) #8157
Description
Description
Since v4.0.0, drawing a bar trace takes O(n²) time whenever all bar values have the same sign, which is the common case for counts, totals, and revenue. 60k bars take ~3.7 s in v4.x versus ~0.3 s in v3.7.0, and 100k bars freeze the page for close to a minute (measured in an app that embeds plotly.js 4.1.2).
The cause is in src/traces/bar/plot.js. Since #7872, appendBarText() calls getZeroBarDir(cd, isHorizontal, xa, ya) for every bar. getZeroBarDir loops over all of cd and only returns early once it has seen both a positive and a negative value:
function appendBarText(gd, plotinfo, bar, cd, i, x0, x1, y0, y1, r, overhead, opts, makeOnCompleteCallback) {
...
var zeroBarDir = getZeroBarDir(cd, isHorizontal, xa, ya); // O(n), runs once per bar
var text = getText(fullLayout, cd, i, xa, ya);
textPosition = getTextPosition(trace, i);
...
if (!text || textPosition === 'none' || ...) {
bar.select('text').remove();
return;
}function getZeroBarDir(cd, isHorizontal, xa, ya) {
var hasPositive = false;
var hasNegative = false;
for (var i = 0; i < cd.length; i++) {
var s = cd[i].s;
if (s > 0) {
hasPositive = true;
} else if (s < 0) {
hasNegative = true;
}
if (hasPositive && hasNegative) {
return 0;
}
}
...
}The call runs before the !text || textPosition === 'none' early return. So traces without any text, and traces with textposition: 'none', pay the quadratic cost too. Mixed-sign data exits the loop almost immediately, which is why it stays fast.
A Chrome CPU profile of a 30k-bar newPlot attributes 2.6 s of the 3.4 s long task to this function.
Screenshots/Video
N/A (performance). Plotly.newPlot timings for a single bar trace in headless Chromium 156 on an Apple M4 (macOS). Each cell is one run in a fresh page:
| plotly.js | bars | all positive (ms) | mixed sign (ms) | all positive, textposition: 'none' (ms) |
|---|---|---|---|---|
| 3.7.0 | 10,000 | 93 | 86 | 76 |
| 3.7.0 | 30,000 | 175 | 179 | 172 |
| 3.7.0 | 60,000 | 299 | 304 | 301 |
| 4.0.0 | 10,000 | 184 | 83 | 184 |
| 4.0.0 | 30,000 | 1,052 | 191 | 1,134 |
| 4.0.0 | 60,000 | 3,716 | 350 | 3,698 |
| 4.2.0 | 10,000 | 189 | 105 | 186 |
| 4.2.0 | 30,000 | 1,043 | 195 | 1,048 |
| 4.2.0 | 60,000 | 3,736 | 386 | 3,701 |
Doubling the bar count roughly quadruples the time on 4.x, while 3.7.0 and mixed-sign data scale linearly.
Steps to reproduce
Open this page in Chrome and look at the console. Switching the version in the script URL to 3.7.0, or making some y values negative, makes it fast again.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/plotly.min.js"></script>
</head>
<body>
<div id="plot" style="width:900px;height:450px"></div>
<script>
const n = 60000;
const x = Array.from({ length: n }, (_, i) => i);
const y = Array.from({ length: n }, (_, i) => 1 + ((i * 7919) % 100)); // all positive
const t0 = performance.now();
Plotly.newPlot('plot', [{ type: 'bar', x, y }]).then(() =>
console.log(`newPlot: ${Math.round(performance.now() - t0)} ms`)
);
</script>
</body>
</html>The same happens from Python with px.bar(x=np.arange(60_000), y=np.random.rand(60_000)).show(), since plotly.py 7.1.0 bundles plotly.js 4.1.1.
Notes
- Introduced by fix(bar): prevent outside text labels from overlapping tilted axis ticks #7872 ("fix(bar): prevent outside text labels from overlapping tilted axis ticks"), commits 6b1363e / f291d70 / 79c20f8. It ships in v4.0.0 and is still present in v4.2.0 and
main. - Suggested fix: compute the zero-bar direction once per trace and pass it into
appendBarText. For example, compute it inplotBarsbefore iterating the bars and pass it throughopts, or cache it oncd[0]. Moving the call below the!text/textposition === 'none'early return would also help traces without text, but traces with text would remain quadratic. - Workaround: none from the public API, since
textposition: 'none'does not avoid the call. - Found while benchmarking chart rendering in Streamlit, which bundles plotly.js 4.1.2 for
st.plotly_chart.
Activity