[BUG]: Bar traces draw in O(n²) time since v4.0.0 (getZeroBarDir runs once per bar)
維護者通常 2 天內回覆
@KoolADE85 已經在處理了。
開始於 2026年11月2日。
評估
- 難度
- 3/5
- 預估耗時
- 1-2 天
- 新手友好度
- 74/100
- Issue 類型
- 缺陷
- 描述清晰度
- 描述清楚
- 活躍度
- 活躍
- 技術堆疊
- javascript
研究方向
Read src/traces/bar/plot.js, focusing on appendBarText() and getZeroBarDir(). Run the provided 60,000-bar reproduction and compare all-positive data with mixed-sign data. Done means the all-positive case no longer scales quadratically, including traces without text or with textposition: 'none'.
由索引模型根據 Issue 內容生成。
描述
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 #7872 ("fix(bar): prevent outside text labels from overlapping tilted axis ticks"), commits 6b1363e5a / f291d70fb / 79c20f8ed. 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.
- 主要語言
- JavaScript
- 星號
- 18.4k
- 分支
- 2.1k
- 平均合併
- 1 天 5 小時
- 30 天內合併 PR
- 12
環境準備
- 沒有 Dockerfile 或 Docker Compose 檔案
- 有 Pull Request 範本
- 閱讀貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
plotly/plotly.js 的其他 Issue
-
[BUG]: `sankey`: `node.x` / `node.y` of `0` is ignored可能已有人在做 @adimalkar 於 3 天前認領。 未關閉bug P3 size: 1
難度 2/5 1-3 小時 新手友好度 75/100
plotly/plotly.js#8150 · 1 則留言 ·
維護者通常 2 天內回覆
-
[BUG]: `scatterpolargl` drops points between 10000 and 100000 points可能已有人在做 @Tanishdete333 於 3 天前認領。 未關閉bug P3 size: 1
難度 2/5 1-3 小時 新手友好度 82/100
plotly/plotly.js#8148 · 1 則留言 ·
維護者通常 2 天內回覆
-
[BUG]: findEmpties throws "iterated with no new neighbors" for large NaN regions in contour (double-precision underflow)可能已有人在做 @davrens101 於 2 天前認領。 未關閉bug P3 size: 3
難度 2/5 1-3 小時 新手友好度 86/100
plotly/plotly.js#8108 · 4 則留言 ·
維護者通常 2 天內回覆
-
chore P3 plotly-internal size: 3 task
難度 2/5 1-3 小時 新手友好度 77/100
plotly/plotly.js#8064 · 1 則留言 ·
維護者通常 2 天內回覆
-
chore P1 plotly-internal size: 1 task
難度 1/5 1 小時以內 新手友好度 82/100
維護者通常 2 天內回覆
相似的 Issue
-
難度 2/5 1-3 小時 新手友好度 66/100
riverqueue/river#1576 ·
維護者通常 1 天內回覆
-
documentation good first issue
難度 1/5 1 小時以內 新手友好度 72/100
維護者通常 1 天內回覆
-
good first issue help wanted
難度 1/5 1 小時以內 新手友好度 88/100
modrollstudio/Modroll-Studio-Website#80 · 1 則留言 ·
維護者通常 1 天內回覆
-
good first issue help wanted
難度 2/5 1-3 小時 新手友好度 85/100
mafzalkalwardev/mailforge#21 ·
-
good first issue help wanted
難度 1/5 1 小時以內 新手友好度 82/100