diff --git a/src/components/selections/select.js b/src/components/selections/select.js index e44734665ad..1ad28507160 100644 --- a/src/components/selections/select.js +++ b/src/components/selections/select.js @@ -790,6 +790,8 @@ function determineSearchTraces(gd, xAxes, yAxes, subplot) { for(i = 0; i < gd.calcdata.length; i++) { cd = gd.calcdata[i]; + // guard against a transient empty/undefined calcdata entry (see epmtySplomSelectionBatch) + if(!cd || !cd[0]) continue; trace = cd[0].trace; if(trace.visible !== true || !trace._module || !trace._module.selectPoints) continue; @@ -1286,6 +1288,10 @@ function epmtySplomSelectionBatch(gd) { if(!cd) return; for(var i = 0; i < cd.length; i++) { + // calcdata can transiently hold an empty/undefined entry (e.g. during + // react/relayout diffing); reselect runs on every redraw, so skip + // instead of throwing. See supplyDefaultsUpdateCalc for the same guard. + if(!cd[i] || !cd[i][0]) continue; var cd0 = cd[i][0]; var trace = cd0.trace; var splomScenes = gd._fullLayout._splomScenes; diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index 2261a72b24b..7e380a095a3 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -3,6 +3,7 @@ var d3SelectAll = require('../../strict-d3').selectAll; var Plotly = require('../../../lib/index'); var Lib = require('../../../src/lib'); +var Registry = require('../../../src/registry'); var click = require('../assets/click'); var doubleClick = require('../assets/double_click'); var DBLCLICKDELAY = require('../../../src/plot_api/plot_config').dfltConfig.doubleClickDelay; @@ -3600,6 +3601,35 @@ describe('Test that selection styles propagate to range-slider plot:', function( }); }); +describe('Test reselect with malformed calcdata:', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + // reselect runs unconditionally at the end of every redraw sequence + // (newPlot / react / relayout / resize) and dereferences gd.calcdata[i][0]. + // calcdata can transiently hold an empty or undefined entry, which used to + // throw "Cannot read properties of undefined (reading '0')" and kill the + // chart even when no selection exists. + it('should not throw on an empty or undefined calcdata entry', function(done) { + var reselect = Registry.getComponentMethod('selections', 'reselect'); + + Plotly.newPlot(gd, [{y: [1, 2, 3]}, {y: [2, 3, 4]}]) + .then(function() { + gd.calcdata[1] = undefined; + expect(function() { reselect(gd); }).not.toThrow(); + + gd.calcdata[0] = []; + expect(function() { reselect(gd); }).not.toThrow(); + }) + .then(done, done.fail); + }); +}); + // to make sure none of the above tests fail with extraneous invisible traces, // add a bunch of them here function addInvisible(fig, canHaveLegend) {