From 29bc81e9199dc0174a6b5cab59d9f3e52b69c2c9 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 23 May 2018 16:37:33 -0400 Subject: [PATCH] remove cached `trace._interpz` from heatmaps/contour maps with gaps it turned out to be more trouble to properly invalidate this cache than it was worth - especially for edge cases like removing a previously filled-in value. --- src/traces/contourcarpet/calc.js | 2 +- src/traces/heatmap/calc.js | 2 +- src/traces/heatmap/interp2d.js | 45 ++++++++++++++++-------------- test/jasmine/tests/contour_test.js | 42 ++++++++++++++++++++++++++++ test/jasmine/tests/heatmap_test.js | 29 +++++++++++++++++++ 5 files changed, 97 insertions(+), 23 deletions(-) diff --git a/src/traces/contourcarpet/calc.js b/src/traces/contourcarpet/calc.js index ebaccfa0f73..0e02e252ff9 100644 --- a/src/traces/contourcarpet/calc.js +++ b/src/traces/contourcarpet/calc.js @@ -84,7 +84,7 @@ function heatmappishCalc(gd, trace) { z = trace._z = clean2dArray(trace._z || trace.z, trace.transpose); trace._emptypoints = findEmpties(z); - trace._interpz = interp2d(z, trace._emptypoints, trace._interpz); + interp2d(z, trace._emptypoints); // create arrays of brick boundaries, to be used by autorange and heatmap.plot var xlen = maxRowLength(z), diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index 0ff85f1a24d..a1f0e7bb6c1 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -77,7 +77,7 @@ module.exports = function calc(gd, trace) { if(isContour || trace.connectgaps) { trace._emptypoints = findEmpties(z); - trace._interpz = interp2d(z, trace._emptypoints, trace._interpz); + interp2d(z, trace._emptypoints); } } diff --git a/src/traces/heatmap/interp2d.js b/src/traces/heatmap/interp2d.js index 586cb762faa..785f3baad4e 100644 --- a/src/traces/heatmap/interp2d.js +++ b/src/traces/heatmap/interp2d.js @@ -10,8 +10,8 @@ var Lib = require('../../lib'); -var INTERPTHRESHOLD = 1e-2, - NEIGHBORSHIFTS = [[-1, 0], [1, 0], [0, -1], [0, 1]]; +var INTERPTHRESHOLD = 1e-2; +var NEIGHBORSHIFTS = [[-1, 0], [1, 0], [0, -1], [0, 1]]; function correctionOvershoot(maxFractionalChange) { // start with less overshoot, until we know it's converging, @@ -19,25 +19,28 @@ function correctionOvershoot(maxFractionalChange) { return 0.5 - 0.25 * Math.min(1, maxFractionalChange * 0.5); } -module.exports = function interp2d(z, emptyPoints, savedInterpZ) { - // fill in any missing data in 2D array z using an iterative - // poisson equation solver with zero-derivative BC at edges - // amazingly, this just amounts to repeatedly averaging all the existing - // nearest neighbors (at least if we don't take x/y scaling into account) - var maxFractionalChange = 1, - i, - thisPt; - - if(Array.isArray(savedInterpZ)) { - for(i = 0; i < emptyPoints.length; i++) { - thisPt = emptyPoints[i]; - z[thisPt[0]][thisPt[1]] = savedInterpZ[thisPt[0]][thisPt[1]]; - } - } - else { - // one pass to fill in a starting value for all the empties - iterateInterp2d(z, emptyPoints); - } +/* + * interp2d: Fill in missing data from a 2D array using an iterative + * poisson equation solver with zero-derivative BC at edges. + * Amazingly, this just amounts to repeatedly averaging all the existing + * nearest neighbors, at least if we don't take x/y scaling into account, + * which is the right approach here where x and y may not even have the + * same units. + * + * @param {array of arrays} z + * The 2D array to fill in. Will be mutated here. Assumed to already be + * cleaned, so all entries are numbers except gaps, which are `undefined`. + * @param {array of arrays} emptyPoints + * Each entry [i, j, neighborCount] for empty points z[i][j] and the number + * of neighbors that are *not* missing. Assumed to be sorted from most to + * least neighbors, as produced by heatmap/find_empties. + */ +module.exports = function interp2d(z, emptyPoints) { + var maxFractionalChange = 1; + var i; + + // one pass to fill in a starting value for all the empties + iterateInterp2d(z, emptyPoints); // we're don't need to iterate lone empties - remove them for(i = 0; i < emptyPoints.length; i++) { diff --git a/test/jasmine/tests/contour_test.js b/test/jasmine/tests/contour_test.js index 98b50bd77be..1ae5858c3d6 100644 --- a/test/jasmine/tests/contour_test.js +++ b/test/jasmine/tests/contour_test.js @@ -458,4 +458,46 @@ describe('contour plotting and editing', function() { .catch(fail) .then(done); }); + + it('can change z values with gaps', function(done) { + Plotly.newPlot(gd, [{ + type: 'contour', + z: [[1, 2], [null, 4], [1, 2]] + }]) + .then(function() { + expect(gd.calcdata[0][0].z).toEqual([[1, 2], [2, 4], [1, 2]]); + expect(gd.calcdata[0][0].zmask).toEqual([[1, 1], [0, 1], [1, 1]]); + + return Plotly.react(gd, [{ + type: 'contour', + z: [[6, 5], [8, 7], [null, 10]] + }]); + }) + .then(function() { + expect(gd.calcdata[0][0].z).toEqual([[6, 5], [8, 7], [9, 10]]); + expect(gd.calcdata[0][0].zmask).toEqual([[1, 1], [1, 1], [0, 1]]); + + return Plotly.react(gd, [{ + type: 'contour', + z: [[1, 2], [null, 4], [1, 2]] + }]); + }) + .then(function() { + expect(gd.calcdata[0][0].z).toEqual([[1, 2], [2, 4], [1, 2]]); + expect(gd.calcdata[0][0].zmask).toEqual([[1, 1], [0, 1], [1, 1]]); + + return Plotly.react(gd, [{ + type: 'contour', + // notice that this one is the same as the previous, except that + // a previously present value was removed... + z: [[1, 2], [null, 4], [1, null]] + }]); + }) + .then(function() { + expect(gd.calcdata[0][0].z).toEqual([[1, 2], [2, 4], [1, 2.5]]); + expect(gd.calcdata[0][0].zmask).toEqual([[1, 1], [0, 1], [1, 0]]); + }) + .catch(fail) + .then(done); + }); }); diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index 98e4a801d2e..19fd83e48e3 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -587,6 +587,35 @@ describe('heatmap plot', function() { .catch(failTest) .then(done); }); + + it('can change z values with connected gaps', function(done) { + var gd = createGraphDiv(); + Plotly.newPlot(gd, [{ + type: 'heatmap', connectgaps: true, + z: [[1, 2], [null, 4], [1, 2]] + }]) + .then(function() { + expect(gd.calcdata[0][0].z).toEqual([[1, 2], [2, 4], [1, 2]]); + + return Plotly.react(gd, [{ + type: 'heatmap', connectgaps: true, + z: [[6, 5], [8, 7], [null, 10]] + }]); + }) + .then(function() { + expect(gd.calcdata[0][0].z).toEqual([[6, 5], [8, 7], [9, 10]]); + + return Plotly.react(gd, [{ + type: 'heatmap', connectgaps: true, + z: [[1, 2], [null, 4], [1, 2]] + }]); + }) + .then(function() { + expect(gd.calcdata[0][0].z).toEqual([[1, 2], [2, 4], [1, 2]]); + }) + .catch(fail) + .then(done); + }); }); describe('heatmap hover', function() {