Skip to content

Commit ae98cc7

Browse files
committed
Guard reselect against empty/undefined calcdata entries
reselect runs unconditionally at the end of every redraw sequence (newPlot / react / relayout / resize). Its helper epmtySplomSelectionBatch iterates gd.calcdata and dereferences cd[i][0] with no guard, so a transient empty or undefined calcdata entry throws "Cannot read properties of undefined (reading '0')" and kills the chart even when no selection exists. The sibling supplyDefaultsUpdateCalc already guards the same access with (oldCalcdata[i] || [])[0]; make epmtySplomSelectionBatch and determineSearchTraces equally defensive by skipping empty/undefined entries. Add a jasmine spec covering both cases.
1 parent c201dd1 commit ae98cc7

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

src/components/selections/select.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,8 @@ function determineSearchTraces(gd, xAxes, yAxes, subplot) {
790790

791791
for(i = 0; i < gd.calcdata.length; i++) {
792792
cd = gd.calcdata[i];
793+
// guard against a transient empty/undefined calcdata entry (see epmtySplomSelectionBatch)
794+
if(!cd || !cd[0]) continue;
793795
trace = cd[0].trace;
794796

795797
if(trace.visible !== true || !trace._module || !trace._module.selectPoints) continue;
@@ -1286,6 +1288,10 @@ function epmtySplomSelectionBatch(gd) {
12861288
if(!cd) return;
12871289

12881290
for(var i = 0; i < cd.length; i++) {
1291+
// calcdata can transiently hold an empty/undefined entry (e.g. during
1292+
// react/relayout diffing); reselect runs on every redraw, so skip
1293+
// instead of throwing. See supplyDefaultsUpdateCalc for the same guard.
1294+
if(!cd[i] || !cd[i][0]) continue;
12891295
var cd0 = cd[i][0];
12901296
var trace = cd0.trace;
12911297
var splomScenes = gd._fullLayout._splomScenes;

test/jasmine/tests/select_test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ var d3SelectAll = require('../../strict-d3').selectAll;
33

44
var Plotly = require('../../../lib/index');
55
var Lib = require('../../../src/lib');
6+
var Registry = require('../../../src/registry');
67
var click = require('../assets/click');
78
var doubleClick = require('../assets/double_click');
89
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(
36003601
});
36013602
});
36023603

3604+
describe('Test reselect with malformed calcdata:', function() {
3605+
var gd;
3606+
3607+
beforeEach(function() {
3608+
gd = createGraphDiv();
3609+
});
3610+
3611+
afterEach(destroyGraphDiv);
3612+
3613+
// reselect runs unconditionally at the end of every redraw sequence
3614+
// (newPlot / react / relayout / resize) and dereferences gd.calcdata[i][0].
3615+
// calcdata can transiently hold an empty or undefined entry, which used to
3616+
// throw "Cannot read properties of undefined (reading '0')" and kill the
3617+
// chart even when no selection exists.
3618+
it('should not throw on an empty or undefined calcdata entry', function(done) {
3619+
var reselect = Registry.getComponentMethod('selections', 'reselect');
3620+
3621+
Plotly.newPlot(gd, [{y: [1, 2, 3]}, {y: [2, 3, 4]}])
3622+
.then(function() {
3623+
gd.calcdata[1] = undefined;
3624+
expect(function() { reselect(gd); }).not.toThrow();
3625+
3626+
gd.calcdata[0] = [];
3627+
expect(function() { reselect(gd); }).not.toThrow();
3628+
})
3629+
.then(done, done.fail);
3630+
});
3631+
});
3632+
36033633
// to make sure none of the above tests fail with extraneous invisible traces,
36043634
// add a bunch of them here
36053635
function addInvisible(fig, canHaveLegend) {

0 commit comments

Comments
 (0)