diff --git a/draftlogs/8162_fix.md b/draftlogs/8162_fix.md new file mode 100644 index 00000000000..533a28c2830 --- /dev/null +++ b/draftlogs/8162_fix.md @@ -0,0 +1 @@ +- Prevent double-precision underflow in `findEmpties` for deep empty (NaN) regions [[#8162](https://github.com/plotly/plotly.js/pull/8162)] diff --git a/src/traces/heatmap/find_empties.js b/src/traces/heatmap/find_empties.js index 88c75894fcd..406759f3fc2 100644 --- a/src/traces/heatmap/find_empties.js +++ b/src/traces/heatmap/find_empties.js @@ -23,6 +23,7 @@ module.exports = function findEmpties(z) { var thisPt; var p; var neighborCount; + var neighborSum; var newNeighborHash; var foundNewNeighbors; @@ -67,12 +68,17 @@ module.exports = function findEmpties(z) { i = thisPt[0]; j = thisPt[1]; - 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]; + + if(neighborSum) { + neighborCount = neighborSum / 20; + // neighborSum / 20 can underflow to 0 in float64 when deep empty regions decay + // past ~249 layers, but this cell does have neighbours: keep a strictly positive weight + if(!neighborCount) neighborCount = Number.MIN_VALUE; - if(neighborCount) { newNeighborHash[thisPt] = [i, j, neighborCount]; noNeighborList.splice(p, 1); foundNewNeighbors = true; diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index ceb527f4132..557549629ff 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -4,6 +4,7 @@ var Lib = require('../../../src/lib'); var setConvert = require('../../../src/plots/cartesian/set_convert'); var convertColumnXYZ = require('../../../src/traces/heatmap/convert_column_xyz'); +var findEmpties = require('../../../src/traces/heatmap/find_empties'); var Heatmap = require('../../../src/traces/heatmap'); var d3Select = require('../../strict-d3').select; @@ -1115,3 +1116,29 @@ describe('heatmap hover', function() { }); }); }); + +describe('findEmpties', function() { + 'use strict'; + + it('should fill deep empty regions without floating-point underflow (#8108)', function() { + // 5 rows, column 0 is defined, columns 1..250 are undefined (empty). + // Depth of 250 causes geometric weight decay (1/20)^249 to underflow double precision. + var z = []; + var r; + var i; + var j; + var nanCount = 250; + for(i = 0; i < 5; i++) { + r = [1]; + for(j = 0; j < nanCount; j++) r.push(undefined); + z.push(r); + } + + var empties; + expect(function() { + empties = findEmpties(z); + }).not.toThrow(); + + expect(empties.length).toBe(5 * nanCount); + }); +});