Skip to content

Commit 33196da

Browse files
authored
Merge pull request #1589 from plotly/filter-preservegaps
Filter transform '!=' operation and 'preservegaps' option
2 parents 5e7eaaf + 2146bdd commit 33196da

File tree

3 files changed

+201
-38
lines changed

3 files changed

+201
-38
lines changed

src/transforms/filter.js

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ var axisIds = require('../plots/cartesian/axis_ids');
1515
var autoType = require('../plots/cartesian/axis_autotype');
1616
var setConvert = require('../plots/cartesian/set_convert');
1717

18-
var INEQUALITY_OPS = ['=', '<', '>=', '>', '<='];
18+
var COMPARISON_OPS = ['=', '!=', '<', '>=', '>', '<='];
1919
var INTERVAL_OPS = ['[]', '()', '[)', '(]', '][', ')(', '](', ')['];
2020
var SET_OPS = ['{}', '}{'];
2121

@@ -51,12 +51,16 @@ exports.attributes = {
5151
},
5252
operation: {
5353
valType: 'enumerated',
54-
values: [].concat(INEQUALITY_OPS).concat(INTERVAL_OPS).concat(SET_OPS),
54+
values: []
55+
.concat(COMPARISON_OPS)
56+
.concat(INTERVAL_OPS)
57+
.concat(SET_OPS),
5558
dflt: '=',
5659
description: [
5760
'Sets the filter operation.',
5861

5962
'*=* keeps items equal to `value`',
63+
'*!=* keeps items not equal to `value`',
6064

6165
'*<* keeps items less than `value`',
6266
'*<=* keeps items less than or equal to `value`',
@@ -87,21 +91,31 @@ exports.attributes = {
8791
'Values are expected to be in the same type as the data linked',
8892
'to *target*.',
8993

90-
'When `operation` is set to one of the inequality values',
91-
'(' + INEQUALITY_OPS + ')',
94+
'When `operation` is set to one of',
95+
'the comparison values (' + COMPARISON_OPS + ')',
9296
'*value* is expected to be a number or a string.',
9397

94-
'When `operation` is set to one of the interval value',
98+
'When `operation` is set to one of the interval values',
9599
'(' + INTERVAL_OPS + ')',
96100
'*value* is expected to be 2-item array where the first item',
97101
'is the lower bound and the second item is the upper bound.',
98102

99-
'When `operation`, is set to one of the set value',
103+
'When `operation`, is set to one of the set values',
100104
'(' + SET_OPS + ')',
101105
'*value* is expected to be an array with as many items as',
102106
'the desired set elements.'
103107
].join(' ')
104-
}
108+
},
109+
preservegaps: {
110+
valType: 'boolean',
111+
dflt: false,
112+
description: [
113+
'Determines whether or not gaps in data arrays produced by the filter operation',
114+
'are preserved.',
115+
'Setting this to *true* might be useful when plotting a line chart',
116+
'with `connectgaps` set to *false*.'
117+
].join(' ')
118+
},
105119
};
106120

107121
exports.supplyDefaults = function(transformIn) {
@@ -114,6 +128,7 @@ exports.supplyDefaults = function(transformIn) {
114128
var enabled = coerce('enabled');
115129

116130
if(enabled) {
131+
coerce('preservegaps');
117132
coerce('operation');
118133
coerce('value');
119134
coerce('target');
@@ -149,36 +164,47 @@ exports.calcTransform = function(gd, trace, opts) {
149164
var d2cTarget = (target === 'x' || target === 'y' || target === 'z') ?
150165
target : filterArray;
151166

152-
var dataToCoord = getDataToCoordFunc(gd, trace, d2cTarget),
153-
filterFunc = getFilterFunc(opts, dataToCoord, targetCalendar),
154-
arrayAttrs = PlotSchema.findArrayAttributes(trace),
155-
originalArrays = {};
156-
157-
// copy all original array attribute values,
158-
// and clear arrays in trace
159-
for(var k = 0; k < arrayAttrs.length; k++) {
160-
var attr = arrayAttrs[k],
161-
np = Lib.nestedProperty(trace, attr);
167+
var dataToCoord = getDataToCoordFunc(gd, trace, d2cTarget);
168+
var filterFunc = getFilterFunc(opts, dataToCoord, targetCalendar);
169+
var arrayAttrs = PlotSchema.findArrayAttributes(trace);
170+
var originalArrays = {};
162171

163-
originalArrays[attr] = Lib.extendDeep([], np.get());
164-
np.set([]);
172+
function forAllAttrs(fn, index) {
173+
for(var j = 0; j < arrayAttrs.length; j++) {
174+
var np = Lib.nestedProperty(trace, arrayAttrs[j]);
175+
fn(np, index);
176+
}
165177
}
166178

167-
function fill(attr, i) {
168-
var oldArr = originalArrays[attr],
169-
newArr = Lib.nestedProperty(trace, attr).get();
170-
171-
newArr.push(oldArr[i]);
179+
var initFn;
180+
var fillFn;
181+
if(opts.preservegaps) {
182+
initFn = function(np) {
183+
originalArrays[np.astr] = Lib.extendDeep([], np.get());
184+
np.set(new Array(len));
185+
};
186+
fillFn = function(np, index) {
187+
var val = originalArrays[np.astr][index];
188+
np.get()[index] = val;
189+
};
190+
} else {
191+
initFn = function(np) {
192+
originalArrays[np.astr] = Lib.extendDeep([], np.get());
193+
np.set([]);
194+
};
195+
fillFn = function(np, index) {
196+
var val = originalArrays[np.astr][index];
197+
np.get().push(val);
198+
};
172199
}
173200

174-
for(var i = 0; i < len; i++) {
175-
var v = filterArray[i];
176-
177-
if(!filterFunc(v)) continue;
201+
// copy all original array attribute values, and clear arrays in trace
202+
forAllAttrs(initFn);
178203

179-
for(var j = 0; j < arrayAttrs.length; j++) {
180-
fill(arrayAttrs[j], i);
181-
}
204+
// loop through filter array, fill trace arrays if passed
205+
for(var i = 0; i < len; i++) {
206+
var passed = filterFunc(filterArray[i]);
207+
if(passed) forAllAttrs(fillFn, i);
182208
}
183209
};
184210

@@ -245,7 +271,7 @@ function getFilterFunc(opts, d2c, targetCalendar) {
245271

246272
var coercedValue;
247273

248-
if(isOperationIn(INEQUALITY_OPS)) {
274+
if(isOperationIn(COMPARISON_OPS)) {
249275
coercedValue = hasArrayValue ? d2cValue(value[0]) : d2cValue(value);
250276
}
251277
else if(isOperationIn(INTERVAL_OPS)) {
@@ -262,6 +288,9 @@ function getFilterFunc(opts, d2c, targetCalendar) {
262288
case '=':
263289
return function(v) { return d2cTarget(v) === coercedValue; };
264290

291+
case '!=':
292+
return function(v) { return d2cTarget(v) !== coercedValue; };
293+
265294
case '<':
266295
return function(v) { return d2cTarget(v) < coercedValue; };
267296

test/jasmine/tests/transform_filter_test.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('filter transforms defaults:', function() {
3030
expect(traceOut.transforms).toEqual([{
3131
type: 'filter',
3232
enabled: true,
33+
preservegaps: false,
3334
operation: '=',
3435
value: 0,
3536
target: 'x',
@@ -320,6 +321,120 @@ describe('filter transforms calc:', function() {
320321
expect(out[0].y).toEqual([1]);
321322
});
322323

324+
it('should preserve gaps in data when `preservegaps` is turned on', function() {
325+
var out = _transform([Lib.extendDeep({}, base, {
326+
transforms: [{
327+
type: 'filter',
328+
preservegaps: true,
329+
operation: '>',
330+
value: 0,
331+
target: 'x'
332+
}]
333+
})]);
334+
335+
expect(out[0].x).toEqual([undefined, undefined, undefined, undefined, 1, 2, 3]);
336+
expect(out[0].y).toEqual([undefined, undefined, undefined, undefined, 2, 3, 1]);
337+
expect(out[0].marker.color).toEqual([undefined, undefined, undefined, undefined, 0.2, 0.3, 0.4]);
338+
});
339+
340+
it('two filter transforms with `preservegaps: true` should commute', function() {
341+
var transform0 = {
342+
type: 'filter',
343+
preservegaps: true,
344+
operation: '>',
345+
value: -1,
346+
target: 'x'
347+
};
348+
349+
var transform1 = {
350+
type: 'filter',
351+
preservegaps: true,
352+
operation: '<',
353+
value: 2,
354+
target: 'x'
355+
};
356+
357+
var out0 = _transform([Lib.extendDeep({}, base, {
358+
transforms: [transform0, transform1]
359+
})]);
360+
361+
var out1 = _transform([Lib.extendDeep({}, base, {
362+
transforms: [transform1, transform0]
363+
})]);
364+
365+
['x', 'y', 'ids', 'marker.color', 'marker.size'].forEach(function(k) {
366+
var v0 = Lib.nestedProperty(out0[0], k).get();
367+
var v1 = Lib.nestedProperty(out1[0], k).get();
368+
expect(v0).toEqual(v1);
369+
});
370+
});
371+
372+
it('two filter transforms with `preservegaps: false` should commute', function() {
373+
var transform0 = {
374+
type: 'filter',
375+
preservegaps: false,
376+
operation: '>',
377+
value: -1,
378+
target: 'x'
379+
};
380+
381+
var transform1 = {
382+
type: 'filter',
383+
preservegaps: false,
384+
operation: '<',
385+
value: 2,
386+
target: 'x'
387+
};
388+
389+
var out0 = _transform([Lib.extendDeep({}, base, {
390+
transforms: [transform0, transform1]
391+
})]);
392+
393+
var out1 = _transform([Lib.extendDeep({}, base, {
394+
transforms: [transform1, transform0]
395+
})]);
396+
397+
['x', 'y', 'ids', 'marker.color', 'marker.size'].forEach(function(k) {
398+
var v0 = Lib.nestedProperty(out0[0], k).get();
399+
var v1 = Lib.nestedProperty(out1[0], k).get();
400+
expect(v0).toEqual(v1);
401+
});
402+
});
403+
404+
it('two filter transforms with different `preservegaps` values should not necessary commute', function() {
405+
var transform0 = {
406+
type: 'filter',
407+
preservegaps: true,
408+
operation: '>',
409+
value: -1,
410+
target: 'x'
411+
};
412+
413+
var transform1 = {
414+
type: 'filter',
415+
preservegaps: false,
416+
operation: '<',
417+
value: 2,
418+
target: 'x'
419+
};
420+
421+
var out0 = _transform([Lib.extendDeep({}, base, {
422+
transforms: [transform0, transform1]
423+
})]);
424+
425+
expect(out0[0].x).toEqual([0, 1]);
426+
expect(out0[0].y).toEqual([1, 2]);
427+
expect(out0[0].marker.color).toEqual([0.1, 0.2]);
428+
429+
var out1 = _transform([Lib.extendDeep({}, base, {
430+
transforms: [transform1, transform0]
431+
})]);
432+
433+
expect(out1[0].x).toEqual([undefined, undefined, undefined, 0, 1]);
434+
expect(out1[0].y).toEqual([undefined, undefined, undefined, 1, 2]);
435+
expect(out1[0].marker.color).toEqual([undefined, undefined, undefined, 0.1, 0.2]);
436+
});
437+
323438
describe('filters should handle numeric values', function() {
324439
var _base = Lib.extendDeep({}, base);
325440

@@ -595,6 +710,23 @@ describe('filter transforms calc:', function() {
595710
_assert(out, ['2015-07-20'], [1], [0.1]);
596711
});
597712

713+
it('with operation *!=*', function() {
714+
var out = _transform([Lib.extendDeep({}, _base, {
715+
transforms: [{
716+
operation: '!=',
717+
value: '2015-07-20',
718+
target: 'x'
719+
}]
720+
})]);
721+
722+
_assert(
723+
out,
724+
['2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'],
725+
[2, 3, 1, 5],
726+
[0.2, 0.3, 0.1, 0.2]
727+
);
728+
});
729+
598730
it('with operation *<*', function() {
599731
var out = _transform([Lib.extendDeep({}, _base, {
600732
transforms: [{

test/jasmine/tests/transform_multi_test.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ describe('general transforms:', function() {
3131
operation: '=',
3232
value: 0,
3333
target: 'x',
34+
preservegaps: false,
3435
_module: Filter
3536
}]);
3637
});
@@ -66,23 +67,23 @@ describe('general transforms:', function() {
6667

6768
traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout);
6869

69-
expect(traceOut.transforms[0]).toEqual({
70+
expect(traceOut.transforms[0]).toEqual(jasmine.objectContaining({
7071
type: 'filter',
7172
enabled: true,
7273
operation: '=',
7374
value: 0,
7475
target: 'x',
7576
_module: Filter
76-
}, '- global first');
77+
}), '- global first');
7778

78-
expect(traceOut.transforms[1]).toEqual({
79+
expect(traceOut.transforms[1]).toEqual(jasmine.objectContaining({
7980
type: 'filter',
8081
enabled: true,
8182
operation: '>',
8283
value: 0,
8384
target: 'x',
8485
_module: Filter
85-
}, '- trace second');
86+
}), '- trace second');
8687

8788
expect(layout._transformModules).toEqual([Filter]);
8889
});
@@ -118,14 +119,14 @@ describe('general transforms:', function() {
118119
}], msg);
119120

120121
msg = 'supplying the transform defaults';
121-
expect(dataOut[1].transforms[0]).toEqual({
122+
expect(dataOut[1].transforms[0]).toEqual(jasmine.objectContaining({
122123
type: 'filter',
123124
enabled: true,
124125
operation: '>',
125126
value: 0,
126127
target: 'x',
127128
_module: Filter
128-
}, msg);
129+
}), msg);
129130

130131
msg = 'keeping refs to user data';
131132
expect(dataOut[1]._input.x).toEqual([-2, -1, -2, 0, 1, 2, 3], msg);
@@ -144,6 +145,7 @@ describe('general transforms:', function() {
144145
operation: '>',
145146
value: 0,
146147
target: 'x',
148+
preservegaps: false,
147149
_module: Filter
148150
}], msg);
149151

0 commit comments

Comments
 (0)