Hacktoberfest 2026:維護者為十月標記出來的 issue,仍然開放、適合新手。 瀏覽 Hacktoberfest issue

[BUG]: findEmpties throws "iterated with no new neighbors" for large NaN regions in contour (double-precision underflow)

未關閉 適合新手
#8,108 0 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

維護者通常 1 天內回覆

還沒有人認領這個 Issue。

評估

難度
2/5
預估耗時
1-3 小時
新手友好度
86/100
Issue 類型
缺陷
描述清晰度
描述清楚
活躍度
活躍
技術堆疊
javascript

研究方向

從 src/traces/heatmap/find_empties.js 開始,重點關注 flood-fill 權重計算以及 issue 中描述的無鄰居保護。使用提供的 5 x 251 條帶重現此故障,然後在 test/jasmine/tests/heatmap_test.js 中現有案例旁邊新增覆蓋測試。完成的標準是:連接的大型空區域能夠完成處理且不會拋出例外,同時現有的插值行為仍然受到覆蓋。

由索引模型根據 Issue 內容生成。

描述

bug
Description

findEmpties() (used by contour, heatmap with connectgaps, surface with connectgaps, and
contourcarpet) throws

findEmpties iterated with no new neighbors

for large empty (NaN) regions, even though every empty cell is connected to real data. The cause
is a double-precision underflow of the internal "fractional neighbour count" weight, which the code
then misreads as "this cell has no neighbours".

Inside the flood-fill loop the weight is recomputed once per layer as

neighborCount = ((neighborHash[[i - 1, j]] || blank)[2] +
    (neighborHash[[i + 1, j]] || blank)[2] +
    (neighborHash[[i, j - 1]] || blank)[2] +
    (neighborHash[[i, j + 1]] || blank)[2]) / 20;   // src/traces/heatmap/find_empties.js, L70-L73

if(neighborCount) {                                  // L75: 0 is treated as "no neighbour"
    newNeighborHash[thisPt] = [i, j, neighborCount];
    noNeighborList.splice(p, 1);
    foundNewNeighbors = true;
}

so the weight decays geometrically. With n already-filled neighbours per layer, after k layers
w ≈ w0 * (n / 20)^k. Once w falls below Number.MIN_VALUE (≈ 4.94e-324, the smallest positive
double) it becomes exactly 0, the if(neighborCount) guard fails, no cell is marked as filled in
that round, foundNewNeighbors stays false, and the function throws.

The number of layers until underflow is 324 / log10(20 / n):

already-filled neighbours per layer layers until the weight becomes 0 typical geometry
1 249 a 1-cell-wide propagation channel
2 324 diagonal / corner of a 2-D front
4 463 interior of a 2-D island

Numeric evidence: 20 ** -248 === 1.9763e-323 (still representable) but 20 ** -249 === 0
(exactly zero, verified in Node/Chrome).

So the real limit is the depth of contiguous empty cells along the propagation direction
(roughly 250-330)
, not the amount of missing data and not connectivity. Note also that a lot of
missing data is fine as long as it is shallow:

  • a 5 x 251 grid whose column 0 is data and columns 1-250 are NaN (only 1250 empty cells) throws;
  • a 200 x 200 grid of data with a 100 x 100 NaN hole (10000 empty cells) renders fine.
Screenshots/Video

Console output for the reproducer below (plotly.js 3.7.0, unminified):

Uncaught findEmpties iterated with no new neighbors

Note that the exception is thrown synchronously out of Plotly.newPlot(), so it is not
delivered as a promise rejection and cannot be handled with .catch().

Steps to reproduce

Minimal, no browser required (the function body is copied verbatim from
src/traces/heatmap/find_empties.js; run with node repro.js):

// NOTE: plotly passes z through clean2dArray, whose cleanZvalue() maps any non-numeric
// value to undefined (`if(!isNumeric(v)) return undefined;`), so NaN reaches findEmpties
// as undefined - that is what "empty" means here.
var maxRowLength = function(z) { var m = 0; for (var k = 0; k < z.length; k++) if (z[k].length > m) m = z[k].length; return m; };

function findEmpties(z) { /* exact copy of src/traces/heatmap/find_empties.js, see file above */ }

// 5 rows, column 0 is data, columns 1..250 are empty  -> THROWS
function strip(rows, nanCount) {
    var z = [], r, i, j;
    for(i = 0; i < rows; i++) { r = [1]; for(j = 0; j < nanCount; j++) r.push(undefined); z.push(r); }
    return z;
}

findEmpties(strip(5, 250));  // Uncaught findEmpties iterated with no new neighbors
findEmpties(strip(5, 249));  // fine - one column less

End-to-end in the browser / CodePen (also verified through Plotly.newPlot with jsdom against the
real 3.7.0 bundle - the same exception is thrown synchronously):

<script src="https://cdn.plot.ly/plotly-3.7.0.js"></script>
<div id="plot" style="width:900px;height:400px"></div>
<script>
var z = [], i, j, row;
for(i = 0; i < 5; i++) { row = [1]; for(j = 0; j < 250; j++) row.push(NaN); z.push(row); }

// throws synchronously; try/catch is required, .catch() on the promise is not enough
try {
    Plotly.newPlot('plot', [{ z: z, type: 'contour' }], {});
} catch(e) {
    console.log(e);   // findEmpties iterated with no new neighbors
}
</script>

CodePen: https://codepen.io/.../pen/... (see the attached example; button "Case 1" fails in < 1 s,
"Case 2" - a 325 x 325 grid whose only data are the centre 2 x 2 cells - fails after ~1 minute).

Suggested fix

The weight is only an ordering heuristic (see the file's own comment: "this is to give us an order
of points to evaluate for interpolation"
), so it must not affect control flow. Testing the sum
instead of the quotient, and clamping the underflowed quotient to Number.MIN_VALUE, is enough:

--- a/src/traces/heatmap/find_empties.js
+++ b/src/traces/heatmap/find_empties.js
@@
-    var neighborCount;
+    var neighborCount;
+    var neighborSum;
@@
-            neighborCount = ((neighborHash[[i - 1, j]] || blank)[2] +
+            neighborSum = (neighborHash[[i - 1, j]] || blank)[2] +
                 (neighborHash[[i + 1, j]] || blank)[2] +
                 (neighborHash[[i, j - 1]] || blank)[2] +
-                (neighborHash[[i, j + 1]] || blank)[2]) / 20;
+                (neighborHash[[i, j + 1]] || blank)[2];
+
+            neighborCount = neighborSum / 20;
 
-            if(neighborCount) {
+            if(neighborSum) {
+                // neighborSum / 20 can underflow to 0 (once neighborSum <= ~1e-323),
+                // but this cell does have neighbours: keep a strictly positive weight
+                if(!neighborCount) neighborCount = Number.MIN_VALUE;
                 newNeighborHash[thisPt] = [i, j, neighborCount];
                 noNeighborList.splice(p, 1);
                 foundNewNeighbors = true;

Measured with this patch applied to the shipping implementation:

input unpatched patched
5 x 251 strip (1250 empty) throws fills all 1250, ~0.6 s
5 x 250 strip (1245 empty) fills 1245 fills 1245 (unchanged)
324 x 324 centre island fills 104972 fills 104972 (unchanged)
325 x 325 centre island throws after ~60 s fills all 105621
512 x 512 centre island throws after ~134 s fills all 262140
200 x 200 with a 60 x 60 hole fills 3600 fills 3600 (unchanged)

The returned list is still sorted by descending weight (verified monotonic non-increasing), and in
the 325 x 325 case only a single cell ends up clamped to Number.MIN_VALUE, so the interpolation
order is not distorted. A regression test could be added next to the existing heatmap tests in
test/jasmine/tests/heatmap_test.js.

Two further suggestions, happy to open PRs for either:

  1. Replace the weighted flood fill with a plain BFS over layer indices (w = 1 / (1 + layer)).
    This removes the underflow at the source and fixes the complexity: the current loop rescans
    every remaining empty cell on every round and uses string-keyed hash lookups plus
    Array#splice in the middle of a large array, so an input that fails can take minutes of
    wasted work first (measured ~60 s for a 325 x 325 grid and ~134 s for 512 x 512 on a desktop CPU).
  2. throw 'findEmpties iterated with no new neighbors' throws a bare string, so it cannot be
    identified with instanceof Error. If a hard failure is really the intent (e.g. all-NaN z),
    please throw an Error with a message that points at the missing data instead.
Notes
  • Affected traces: contour (findEmpties is called unconditionally), heatmap with
    connectgaps: true, surface (3D) with connectgaps: true, and contourcarpet.
  • There is no configuration that avoids this; zsmooth, connectgaps: false etc. only change
    whether the code path is reached, not whether it can throw.
  • Every empty cell in all reproducers above is 4-connected to real data, so the error message
    ("iterated with no new neighbors") is misleading - it suggests disconnected data where there is none.
  • findEmpties only sees undefined cells because clean2dArray converts NaN to undefined
    first; this is why a repro that calls the function directly with a Float64Array filled with
    NaN silently returns [].
Examples

CodePen Link:

https://codepen.io/Czhaowu/pen/myWBYEW
Image
主要語言
JavaScript
星號
18.4k
分支
2k
平均合併
2 天 7 小時
30 天內合併 PR
18

環境準備

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

plotly/plotly.js 的其他 Issue

查看 plotly/plotly.js 的全部 Issue

相似的 Issue

更多 JavaScript Issue

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。