diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index e312690a176..73cac1a5821 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -370,10 +370,12 @@ function _hover(gd, evt, subplot, noHoverEvent) {
cd = searchData[curvenum];
// filter out invisible or broken data
- if(!cd || !cd[0] || !cd[0].trace || cd[0].trace.visible !== true) continue;
+ if(!cd || !cd[0] || !cd[0].trace) continue;
trace = cd[0].trace;
+ if(trace.visible !== true || trace._length === 0) continue;
+
// Explicitly bail out for these two. I don't know how to otherwise prevent
// the rest of this function from running and failing
if(['carpet', 'contourcarpet'].indexOf(trace._module.name) !== -1) continue;
diff --git a/src/lib/filter_visible.js b/src/lib/filter_visible.js
index 39088029478..7748237bd2a 100644
--- a/src/lib/filter_visible.js
+++ b/src/lib/filter_visible.js
@@ -32,7 +32,8 @@ function baseFilter(item) {
}
function calcDataFilter(item) {
- return item[0].trace.visible === true;
+ var trace = item[0].trace;
+ return trace.visible === true && trace._length !== 0;
}
function isCalcData(cont) {
diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js
index a310ae31bcc..ce221c8efbf 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -642,7 +642,7 @@ exports.redrawReglTraces = function(gd) {
for(i = 0; i < fullData.length; i++) {
var trace = fullData[i];
- if(trace.visible === true) {
+ if(trace.visible === true && trace._length !== 0) {
if(trace.type === 'splom') {
fullLayout._splomScenes[trace.uid].draw();
} else if(trace.type === 'scattergl') {
diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js
index d700c83be06..a407b912e78 100644
--- a/src/plots/cartesian/set_convert.js
+++ b/src/plots/cartesian/set_convert.js
@@ -403,7 +403,7 @@ module.exports = function setConvert(ax, fullLayout) {
return;
}
- if(ax.type === 'date') {
+ if(ax.type === 'date' && !ax.autorange) {
// check if milliseconds or js date objects are provided for range
// and convert to date strings
range[0] = Lib.cleanDate(range[0], BADNUM, ax.calendar);
diff --git a/src/plots/get_data.js b/src/plots/get_data.js
index b543c2f75bb..4523b5f4434 100644
--- a/src/plots/get_data.js
+++ b/src/plots/get_data.js
@@ -69,8 +69,10 @@ exports.getModuleCalcData = function(calcdata, arg1) {
for(var i = 0; i < calcdata.length; i++) {
var cd = calcdata[i];
var trace = cd[0].trace;
- // N.B. 'legendonly' traces do not make it past here
- if(trace.visible !== true) continue;
+ // N.B.
+ // - 'legendonly' traces do not make it past here
+ // - skip over 'visible' traces that got trimmed completely during calc transforms
+ if(trace.visible !== true || trace._length === 0) continue;
// group calcdata trace not by 'module' (as the name of this function
// would suggest), but by 'module plot method' so that if some traces
diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js
index b15ef824713..57a317e72c8 100644
--- a/src/plots/gl3d/scene.js
+++ b/src/plots/gl3d/scene.js
@@ -502,7 +502,7 @@ proto.plot = function(sceneData, fullLayout, layout) {
for(i = 0; i < sceneData.length; ++i) {
data = sceneData[i];
- if(data.visible !== true) continue;
+ if(data.visible !== true || data._length === 0) continue;
computeTraceBounds(this, data, dataBounds);
}
@@ -526,7 +526,7 @@ proto.plot = function(sceneData, fullLayout, layout) {
// Update traces
for(i = 0; i < sceneData.length; ++i) {
data = sceneData[i];
- if(data.visible !== true) {
+ if(data.visible !== true || data._length === 0) {
continue;
}
trace = this.traces[data.uid];
@@ -551,7 +551,8 @@ proto.plot = function(sceneData, fullLayout, layout) {
traceIdLoop:
for(i = 0; i < traceIds.length; ++i) {
for(j = 0; j < sceneData.length; ++j) {
- if(sceneData[j].uid === traceIds[i] && sceneData[j].visible === true) {
+ if(sceneData[j].uid === traceIds[i] &&
+ (sceneData[j].visible === true && sceneData[j]._length !== 0)) {
continue traceIdLoop;
}
}
diff --git a/src/plots/plots.js b/src/plots/plots.js
index 6e6494e0fb7..76db1aae239 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -2749,17 +2749,13 @@ plots.doCalcdata = function(gd, traces) {
);
}
- setupAxisCategories(axList, fullData);
-
var hasCalcTransform = false;
- // transform loop
- for(i = 0; i < fullData.length; i++) {
+ function transformCalci(i) {
trace = fullData[i];
+ _module = trace._module;
if(trace.visible === true && trace.transforms) {
- _module = trace._module;
-
// we need one round of trace module calc before
// the calc transform to 'fill in' the categories list
// used for example in the data-to-coordinate method
@@ -2786,9 +2782,6 @@ plots.doCalcdata = function(gd, traces) {
}
}
- // clear stuff that should recomputed in 'regular' loop
- if(hasCalcTransform) setupAxisCategories(axList, fullData);
-
function calci(i, isContainer) {
trace = fullData[i];
_module = trace._module;
@@ -2797,7 +2790,7 @@ plots.doCalcdata = function(gd, traces) {
var cd = [];
- if(trace.visible === true) {
+ if(trace.visible === true && trace._length !== 0) {
// clear existing ref in case it got relinked
delete trace._indexToPoints;
// keep ref of index-to-points map object of the *last* enabled transform,
@@ -2833,6 +2826,16 @@ plots.doCalcdata = function(gd, traces) {
calcdata[i] = cd;
}
+ setupAxisCategories(axList, fullData);
+
+ // 'transform' loop - must calc container traces first
+ // so that if their dependent traces can get transform properly
+ for(i = 0; i < fullData.length; i++) calci(i, true);
+ for(i = 0; i < fullData.length; i++) transformCalci(i);
+
+ // clear stuff that should recomputed in 'regular' loop
+ if(hasCalcTransform) setupAxisCategories(axList, fullData);
+
// 'regular' loop - make sure container traces (eg carpet) calc before
// contained traces (eg contourcarpet)
for(i = 0; i < fullData.length; i++) calci(i, true);
diff --git a/src/plots/polar/set_convert.js b/src/plots/polar/set_convert.js
index bbcc341d1da..d3fc465b5db 100644
--- a/src/plots/polar/set_convert.js
+++ b/src/plots/polar/set_convert.js
@@ -174,6 +174,9 @@ function setConvertAngular(ax, polarLayout) {
var catLen = ax._categories.length;
var _period = ax.period ? Math.max(ax.period, catLen) : catLen;
+ // fallback in case all categories have been filtered out
+ if(_period === 0) _period = 1;
+
c2rad = t2rad = function(v) { return v * 2 * Math.PI / _period; };
rad2c = rad2t = function(v) { return v * _period / Math.PI / 2; };
diff --git a/src/traces/scattermapbox/convert.js b/src/traces/scattermapbox/convert.js
index 8aba0245549..ace20bea379 100644
--- a/src/traces/scattermapbox/convert.js
+++ b/src/traces/scattermapbox/convert.js
@@ -23,7 +23,7 @@ var convertTextOpts = require('../../plots/mapbox/convert_text_opts');
module.exports = function convert(calcTrace) {
var trace = calcTrace[0].trace;
- var isVisible = (trace.visible === true);
+ var isVisible = (trace.visible === true && trace._length !== 0);
var hasFill = (trace.fill !== 'none');
var hasLines = subTypes.hasLines(trace);
var hasMarkers = subTypes.hasMarkers(trace);
diff --git a/src/transforms/filter.js b/src/transforms/filter.js
index 4e96fbd30e9..e23556ee1d1 100644
--- a/src/transforms/filter.js
+++ b/src/transforms/filter.js
@@ -138,10 +138,16 @@ exports.supplyDefaults = function(transformIn) {
var enabled = coerce('enabled');
if(enabled) {
+ var target = coerce('target');
+
+ if(Lib.isArrayOrTypedArray(target) && target.length === 0) {
+ transformOut.enabled = false;
+ return transformOut;
+ }
+
coerce('preservegaps');
coerce('operation');
coerce('value');
- coerce('target');
var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleDefaults');
handleCalendarDefaults(transformIn, transformOut, 'valuecalendar', null);
diff --git a/test/jasmine/tests/funnel_test.js b/test/jasmine/tests/funnel_test.js
index 7ed7898dc35..1368f46efb6 100644
--- a/test/jasmine/tests/funnel_test.js
+++ b/test/jasmine/tests/funnel_test.js
@@ -1018,7 +1018,7 @@ describe('A funnel plot', function() {
.then(done);
});
- it('should be able to deal with blank bars on transform', function(done) {
+ it('should be able to deal with transform that empty out the data coordinate arrays', function(done) {
Plotly.plot(gd, {
data: [{
type: 'funnel',
@@ -1038,14 +1038,11 @@ describe('A funnel plot', function() {
})
.then(function() {
var traceNodes = getAllTraceNodes(gd);
- var funnelNodes = getAllFunnelNodes(traceNodes[0]);
- var pathNode = funnelNodes[0].querySelector('path');
+ expect(traceNodes.length).toBe(0);
expect(gd.calcdata[0][0].x).toEqual(NaN);
expect(gd.calcdata[0][0].y).toEqual(NaN);
- expect(gd.calcdata[0][0].isBlank).toBe(true);
-
- expect(pathNode.outerHTML).toEqual('');
+ expect(gd.calcdata[0][0].isBlank).toBe(undefined);
})
.catch(failTest)
.then(done);
diff --git a/test/jasmine/tests/transform_filter_test.js b/test/jasmine/tests/transform_filter_test.js
index 9679e415841..9287a1a0a98 100644
--- a/test/jasmine/tests/transform_filter_test.js
+++ b/test/jasmine/tests/transform_filter_test.js
@@ -86,6 +86,33 @@ describe('filter transforms defaults:', function() {
expect(traceOut.transforms[2].target).toEqual('x');
expect(traceOut.transforms[3].target).toEqual('marker.color');
});
+
+ it('supplyTraceDefaults should set *enabled:false* and return early when *target* is an empty array', function() {
+ // see https://github.com/plotly/plotly.js/issues/2908
+ // this solves multiple problems downstream
+
+ traceIn = {
+ x: [1, 2, 3],
+ transforms: [{
+ type: 'filter',
+ target: []
+ }]
+ };
+ traceOut = Plots.supplyTraceDefaults(traceIn, {type: 'scatter'}, 0, fullLayout);
+ expect(traceOut.transforms[0].target).toEqual([]);
+ expect(traceOut.transforms[0].enabled).toBe(false, 'set to false!');
+
+ traceIn = {
+ x: new Float32Array([1, 2, 3]),
+ transforms: [{
+ type: 'filter',
+ target: new Float32Array()
+ }]
+ };
+ traceOut = Plots.supplyTraceDefaults(traceIn, {type: 'scatter'}, 0, fullLayout);
+ expect(traceOut.transforms[0].target).toEqual(new Float32Array());
+ expect(traceOut.transforms[0].enabled).toBe(false, 'set to false!');
+ });
});
describe('filter transforms calc:', function() {
@@ -1292,3 +1319,69 @@ describe('filter transforms interactions', function() {
.then(done);
});
});
+
+describe('filter resulting in empty coordinate arrays', function() {
+ var gd;
+
+ afterEach(function(done) {
+ Plotly.purge(gd);
+ setTimeout(function() {
+ destroyGraphDiv();
+ done();
+ }, 200);
+ });
+
+ function filter2empty(mock) {
+ var fig = Lib.extendDeep({}, mock);
+ var data = fig.data || [];
+
+ data.forEach(function(trace) {
+ trace.transforms = [{
+ type: 'filter',
+ target: [null]
+ }];
+ });
+
+ return fig;
+ }
+
+ describe('svg mocks', function() {
+ var mockList = require('../assets/mock_lists').svg;
+
+ mockList.forEach(function(d) {
+ it(d[0], function(done) {
+ gd = createGraphDiv();
+ var fig = filter2empty(d[1]);
+ Plotly.newPlot(gd, fig).catch(failTest).then(done);
+ });
+ });
+ });
+
+ describe('gl mocks', function() {
+ var mockList = require('../assets/mock_lists').gl;
+
+ mockList.forEach(function(d) {
+ it('@gl ' + d[0], function(done) {
+ gd = createGraphDiv();
+ var fig = filter2empty(d[1]);
+ Plotly.newPlot(gd, fig).catch(failTest).then(done);
+ });
+ });
+ });
+
+ describe('mapbox mocks', function() {
+ var mockList = require('../assets/mock_lists').mapbox;
+
+ Plotly.setPlotConfig({
+ mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN
+ });
+
+ mockList.forEach(function(d) {
+ it('@noCI ' + d[0], function(done) {
+ gd = createGraphDiv();
+ var fig = filter2empty(d[1]);
+ Plotly.newPlot(gd, fig).catch(failTest).then(done);
+ });
+ });
+ });
+});
diff --git a/test/jasmine/tests/waterfall_test.js b/test/jasmine/tests/waterfall_test.js
index 36116e9ffc8..c48451f2e4e 100644
--- a/test/jasmine/tests/waterfall_test.js
+++ b/test/jasmine/tests/waterfall_test.js
@@ -976,7 +976,7 @@ describe('A waterfall plot', function() {
.then(done);
});
- it('should be able to deal with blank bars on transform', function(done) {
+ it('should be able to deal with transform that empty out the data coordinate arrays', function(done) {
Plotly.plot(gd, {
data: [{
type: 'waterfall',
@@ -993,14 +993,11 @@ describe('A waterfall plot', function() {
})
.then(function() {
var traceNodes = getAllTraceNodes(gd);
- var waterfallNodes = getAllWaterfallNodes(traceNodes[0]);
- var pathNode = waterfallNodes[0].querySelector('path');
+ expect(traceNodes.length).toBe(0);
expect(gd.calcdata[0][0].x).toEqual(NaN);
expect(gd.calcdata[0][0].y).toEqual(NaN);
- expect(gd.calcdata[0][0].isBlank).toBe(true);
-
- expect(pathNode.outerHTML).toEqual('');
+ expect(gd.calcdata[0][0].isBlank).toBe(undefined);
})
.catch(failTest)
.then(done);