From 52e9e4d605fd067709c97b1c8e8a0b3f81692f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 12 Apr 2017 15:03:38 -0400 Subject: [PATCH 1/4] resolves #1177 - add '!=' filter operation --- src/transforms/filter.js | 6 +++++- test/jasmine/tests/transform_filter_test.js | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/transforms/filter.js b/src/transforms/filter.js index f77285b5927..1395611fa2b 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -15,7 +15,7 @@ var axisIds = require('../plots/cartesian/axis_ids'); var autoType = require('../plots/cartesian/axis_autotype'); var setConvert = require('../plots/cartesian/set_convert'); -var INEQUALITY_OPS = ['=', '<', '>=', '>', '<=']; +var INEQUALITY_OPS = ['=', '!=', '<', '>=', '>', '<=']; var INTERVAL_OPS = ['[]', '()', '[)', '(]', '][', ')(', '](', ')[']; var SET_OPS = ['{}', '}{']; @@ -57,6 +57,7 @@ exports.attributes = { 'Sets the filter operation.', '*=* keeps items equal to `value`', + '*!=* keeps items not equal to `value`', '*<* keeps items less than `value`', '*<=* keeps items less than or equal to `value`', @@ -262,6 +263,9 @@ function getFilterFunc(opts, d2c, targetCalendar) { case '=': return function(v) { return d2cTarget(v) === coercedValue; }; + case '!=': + return function(v) { return d2cTarget(v) !== coercedValue; }; + case '<': return function(v) { return d2cTarget(v) < coercedValue; }; diff --git a/test/jasmine/tests/transform_filter_test.js b/test/jasmine/tests/transform_filter_test.js index 9bbea9678c6..e3e79ad98a6 100644 --- a/test/jasmine/tests/transform_filter_test.js +++ b/test/jasmine/tests/transform_filter_test.js @@ -595,6 +595,23 @@ describe('filter transforms calc:', function() { _assert(out, ['2015-07-20'], [1], [0.1]); }); + it('with operation *!=*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '!=', + value: '2015-07-20', + target: 'x' + }] + })]); + + _assert( + out, + ['2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], + [2, 3, 1, 5], + [0.2, 0.3, 0.1, 0.2] + ); + }); + it('with operation *<*', function() { var out = _transform([Lib.extendDeep({}, _base, { transforms: [{ From 3f15c73613e0189f1742d6c57bee17c4ff29eb91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 12 Apr 2017 15:04:13 -0400 Subject: [PATCH 2/4] resolves #1178 - add preservegaps filter options --- src/transforms/filter.js | 72 ++++++++++++++------- test/jasmine/tests/transform_filter_test.js | 17 +++++ test/jasmine/tests/transform_multi_test.js | 14 ++-- 3 files changed, 72 insertions(+), 31 deletions(-) diff --git a/src/transforms/filter.js b/src/transforms/filter.js index 1395611fa2b..55a40d1901e 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -102,7 +102,17 @@ exports.attributes = { '*value* is expected to be an array with as many items as', 'the desired set elements.' ].join(' ') - } + }, + preservegaps: { + valType: 'boolean', + dflt: false, + description: [ + 'Determines whether or not gaps in data arrays produced by the filter operation', + 'are preserved or not.', + 'Setting this to *true* might be useful when plotting a line chart', + 'with `connectgaps` set to *true*.' + ].join(' ') + }, }; exports.supplyDefaults = function(transformIn) { @@ -115,6 +125,7 @@ exports.supplyDefaults = function(transformIn) { var enabled = coerce('enabled'); if(enabled) { + coerce('preservegaps'); coerce('operation'); coerce('value'); coerce('target'); @@ -150,36 +161,47 @@ exports.calcTransform = function(gd, trace, opts) { var d2cTarget = (target === 'x' || target === 'y' || target === 'z') ? target : filterArray; - var dataToCoord = getDataToCoordFunc(gd, trace, d2cTarget), - filterFunc = getFilterFunc(opts, dataToCoord, targetCalendar), - arrayAttrs = PlotSchema.findArrayAttributes(trace), - originalArrays = {}; - - // copy all original array attribute values, - // and clear arrays in trace - for(var k = 0; k < arrayAttrs.length; k++) { - var attr = arrayAttrs[k], - np = Lib.nestedProperty(trace, attr); + var dataToCoord = getDataToCoordFunc(gd, trace, d2cTarget); + var filterFunc = getFilterFunc(opts, dataToCoord, targetCalendar); + var arrayAttrs = PlotSchema.findArrayAttributes(trace); + var originalArrays = {}; - originalArrays[attr] = Lib.extendDeep([], np.get()); - np.set([]); + function forAllAttrs(fn, index) { + for(var j = 0; j < arrayAttrs.length; j++) { + var np = Lib.nestedProperty(trace, arrayAttrs[j]); + fn(np, index); + } } - function fill(attr, i) { - var oldArr = originalArrays[attr], - newArr = Lib.nestedProperty(trace, attr).get(); - - newArr.push(oldArr[i]); + var initFn; + var fillFn; + if(opts.preservegaps) { + initFn = function(np) { + originalArrays[np.astr] = Lib.extendDeep([], np.get()); + np.set(new Array(len)); + }; + fillFn = function(np, index) { + var val = originalArrays[np.astr][index]; + np.get()[index] = val; + }; + } else { + initFn = function(np) { + originalArrays[np.astr] = Lib.extendDeep([], np.get()); + np.set([]); + }; + fillFn = function(np, index) { + var val = originalArrays[np.astr][index]; + np.get().push(val); + }; } - for(var i = 0; i < len; i++) { - var v = filterArray[i]; + // copy all original array attribute values, and clear arrays in trace + forAllAttrs(initFn); - if(!filterFunc(v)) continue; - - for(var j = 0; j < arrayAttrs.length; j++) { - fill(arrayAttrs[j], i); - } + // loop through filter array, fill trace arrays if passed + for(var i = 0; i < len; i++) { + var passed = filterFunc(filterArray[i]); + if(passed) forAllAttrs(fillFn, i); } }; diff --git a/test/jasmine/tests/transform_filter_test.js b/test/jasmine/tests/transform_filter_test.js index e3e79ad98a6..186d57f8722 100644 --- a/test/jasmine/tests/transform_filter_test.js +++ b/test/jasmine/tests/transform_filter_test.js @@ -30,6 +30,7 @@ describe('filter transforms defaults:', function() { expect(traceOut.transforms).toEqual([{ type: 'filter', enabled: true, + preservegaps: false, operation: '=', value: 0, target: 'x', @@ -320,6 +321,22 @@ describe('filter transforms calc:', function() { expect(out[0].y).toEqual([1]); }); + it('should preserve gaps in data when `preservegaps` is turned on', function() { + var out = _transform([Lib.extendDeep({}, base, { + transforms: [{ + type: 'filter', + preservegaps: true, + operation: '>', + value: 0, + target: 'x' + }] + })]); + + expect(out[0].x).toEqual([undefined, undefined, undefined, undefined, 1, 2, 3]); + expect(out[0].y).toEqual([undefined, undefined, undefined, undefined, 2, 3, 1]); + expect(out[0].marker.color).toEqual([undefined, undefined, undefined, undefined, 0.2, 0.3, 0.4]); + }); + describe('filters should handle numeric values', function() { var _base = Lib.extendDeep({}, base); diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index 6930effbd8e..06576734649 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -31,6 +31,7 @@ describe('general transforms:', function() { operation: '=', value: 0, target: 'x', + preservegaps: false, _module: Filter }]); }); @@ -66,23 +67,23 @@ describe('general transforms:', function() { traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); - expect(traceOut.transforms[0]).toEqual({ + expect(traceOut.transforms[0]).toEqual(jasmine.objectContaining({ type: 'filter', enabled: true, operation: '=', value: 0, target: 'x', _module: Filter - }, '- global first'); + }), '- global first'); - expect(traceOut.transforms[1]).toEqual({ + expect(traceOut.transforms[1]).toEqual(jasmine.objectContaining({ type: 'filter', enabled: true, operation: '>', value: 0, target: 'x', _module: Filter - }, '- trace second'); + }), '- trace second'); expect(layout._transformModules).toEqual([Filter]); }); @@ -118,14 +119,14 @@ describe('general transforms:', function() { }], msg); msg = 'supplying the transform defaults'; - expect(dataOut[1].transforms[0]).toEqual({ + expect(dataOut[1].transforms[0]).toEqual(jasmine.objectContaining({ type: 'filter', enabled: true, operation: '>', value: 0, target: 'x', _module: Filter - }, msg); + }), msg); msg = 'keeping refs to user data'; expect(dataOut[1]._input.x).toEqual([-2, -1, -2, 0, 1, 2, 3], msg); @@ -144,6 +145,7 @@ describe('general transforms:', function() { operation: '>', value: 0, target: 'x', + preservegaps: false, _module: Filter }], msg); From 1e0fe2ff4af34b17cf7fcf01018a78ac7a7db625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 18 Apr 2017 10:48:04 -0400 Subject: [PATCH 3/4] AJ-proof filter preservegaps - add COMPARISON_OPS list - fixup `preservegaps` description - add `preservegaps` commute tests --- src/transforms/filter.js | 20 +++-- test/jasmine/tests/transform_filter_test.js | 98 +++++++++++++++++++++ 2 files changed, 111 insertions(+), 7 deletions(-) diff --git a/src/transforms/filter.js b/src/transforms/filter.js index 55a40d1901e..e76bb6762c2 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -15,7 +15,8 @@ var axisIds = require('../plots/cartesian/axis_ids'); var autoType = require('../plots/cartesian/axis_autotype'); var setConvert = require('../plots/cartesian/set_convert'); -var INEQUALITY_OPS = ['=', '!=', '<', '>=', '>', '<=']; +var COMPARISON_OPS = ['=', '!=']; +var INEQUALITY_OPS = ['<', '>=', '>', '<=']; var INTERVAL_OPS = ['[]', '()', '[)', '(]', '][', ')(', '](', ')[']; var SET_OPS = ['{}', '}{']; @@ -51,7 +52,11 @@ exports.attributes = { }, operation: { valType: 'enumerated', - values: [].concat(INEQUALITY_OPS).concat(INTERVAL_OPS).concat(SET_OPS), + values: [] + .concat(COMPARISON_OPS) + .concat(INEQUALITY_OPS) + .concat(INTERVAL_OPS) + .concat(SET_OPS), dflt: '=', description: [ 'Sets the filter operation.', @@ -88,8 +93,9 @@ exports.attributes = { 'Values are expected to be in the same type as the data linked', 'to *target*.', - 'When `operation` is set to one of the inequality values', - '(' + INEQUALITY_OPS + ')', + 'When `operation` is set to one of', + 'the comparison or (' + COMPARISON_OPS + ')', + 'inequality values (' + INEQUALITY_OPS + ')', '*value* is expected to be a number or a string.', 'When `operation` is set to one of the interval value', @@ -108,9 +114,9 @@ exports.attributes = { dflt: false, description: [ 'Determines whether or not gaps in data arrays produced by the filter operation', - 'are preserved or not.', + 'are preserved.', 'Setting this to *true* might be useful when plotting a line chart', - 'with `connectgaps` set to *true*.' + 'with `connectgaps` set to *false*.' ].join(' ') }, }; @@ -268,7 +274,7 @@ function getFilterFunc(opts, d2c, targetCalendar) { var coercedValue; - if(isOperationIn(INEQUALITY_OPS)) { + if(isOperationIn(COMPARISON_OPS) || isOperationIn(INEQUALITY_OPS)) { coercedValue = hasArrayValue ? d2cValue(value[0]) : d2cValue(value); } else if(isOperationIn(INTERVAL_OPS)) { diff --git a/test/jasmine/tests/transform_filter_test.js b/test/jasmine/tests/transform_filter_test.js index 186d57f8722..71c7748c5aa 100644 --- a/test/jasmine/tests/transform_filter_test.js +++ b/test/jasmine/tests/transform_filter_test.js @@ -337,6 +337,104 @@ describe('filter transforms calc:', function() { expect(out[0].marker.color).toEqual([undefined, undefined, undefined, undefined, 0.2, 0.3, 0.4]); }); + it('two filter transforms with `preservegaps: true` should commute', function() { + var transform0 = { + type: 'filter', + preservegaps: true, + operation: '>', + value: -1, + target: 'x' + }; + + var transform1 = { + type: 'filter', + preservegaps: true, + operation: '<', + value: 2, + target: 'x' + }; + + var out0 = _transform([Lib.extendDeep({}, base, { + transforms: [transform0, transform1] + })]); + + var out1 = _transform([Lib.extendDeep({}, base, { + transforms: [transform1, transform0] + })]); + + ['x', 'y', 'ids', 'marker.color', 'marker.size'].forEach(function(k) { + var v0 = Lib.nestedProperty(out0[0], k).get(); + var v1 = Lib.nestedProperty(out1[0], k).get(); + expect(v0).toEqual(v1); + }); + }); + + it('two filter transforms with `preservegaps: false` should commute', function() { + var transform0 = { + type: 'filter', + preservegaps: false, + operation: '>', + value: -1, + target: 'x' + }; + + var transform1 = { + type: 'filter', + preservegaps: false, + operation: '<', + value: 2, + target: 'x' + }; + + var out0 = _transform([Lib.extendDeep({}, base, { + transforms: [transform0, transform1] + })]); + + var out1 = _transform([Lib.extendDeep({}, base, { + transforms: [transform1, transform0] + })]); + + ['x', 'y', 'ids', 'marker.color', 'marker.size'].forEach(function(k) { + var v0 = Lib.nestedProperty(out0[0], k).get(); + var v1 = Lib.nestedProperty(out1[0], k).get(); + expect(v0).toEqual(v1); + }); + }); + + it('two filter transforms with different `preservegaps` values should not necessary commute', function() { + var transform0 = { + type: 'filter', + preservegaps: true, + operation: '>', + value: -1, + target: 'x' + }; + + var transform1 = { + type: 'filter', + preservegaps: false, + operation: '<', + value: 2, + target: 'x' + }; + + var out0 = _transform([Lib.extendDeep({}, base, { + transforms: [transform0, transform1] + })]); + + expect(out0[0].x).toEqual([0, 1]); + expect(out0[0].y).toEqual([1, 2]); + expect(out0[0].marker.color).toEqual([0.1, 0.2]); + + var out1 = _transform([Lib.extendDeep({}, base, { + transforms: [transform1, transform0] + })]); + + expect(out1[0].x).toEqual([undefined, undefined, undefined, 0, 1]); + expect(out1[0].y).toEqual([undefined, undefined, undefined, 1, 2]); + expect(out1[0].marker.color).toEqual([undefined, undefined, undefined, 0.1, 0.2]); + }); + describe('filters should handle numeric values', function() { var _base = Lib.extendDeep({}, base); From 2146bdd3cef8a8c52901a35513e926e13b86a6d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 18 Apr 2017 11:01:55 -0400 Subject: [PATCH 4/4] merge INEQUALITY_OPS with COMPARISON_OPS --- src/transforms/filter.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/transforms/filter.js b/src/transforms/filter.js index e76bb6762c2..1b5b0ab5faf 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -15,8 +15,7 @@ var axisIds = require('../plots/cartesian/axis_ids'); var autoType = require('../plots/cartesian/axis_autotype'); var setConvert = require('../plots/cartesian/set_convert'); -var COMPARISON_OPS = ['=', '!=']; -var INEQUALITY_OPS = ['<', '>=', '>', '<=']; +var COMPARISON_OPS = ['=', '!=', '<', '>=', '>', '<=']; var INTERVAL_OPS = ['[]', '()', '[)', '(]', '][', ')(', '](', ')[']; var SET_OPS = ['{}', '}{']; @@ -54,7 +53,6 @@ exports.attributes = { valType: 'enumerated', values: [] .concat(COMPARISON_OPS) - .concat(INEQUALITY_OPS) .concat(INTERVAL_OPS) .concat(SET_OPS), dflt: '=', @@ -94,16 +92,15 @@ exports.attributes = { 'to *target*.', 'When `operation` is set to one of', - 'the comparison or (' + COMPARISON_OPS + ')', - 'inequality values (' + INEQUALITY_OPS + ')', + 'the comparison values (' + COMPARISON_OPS + ')', '*value* is expected to be a number or a string.', - 'When `operation` is set to one of the interval value', + 'When `operation` is set to one of the interval values', '(' + INTERVAL_OPS + ')', '*value* is expected to be 2-item array where the first item', 'is the lower bound and the second item is the upper bound.', - 'When `operation`, is set to one of the set value', + 'When `operation`, is set to one of the set values', '(' + SET_OPS + ')', '*value* is expected to be an array with as many items as', 'the desired set elements.' @@ -274,7 +271,7 @@ function getFilterFunc(opts, d2c, targetCalendar) { var coercedValue; - if(isOperationIn(COMPARISON_OPS) || isOperationIn(INEQUALITY_OPS)) { + if(isOperationIn(COMPARISON_OPS)) { coercedValue = hasArrayValue ? d2cValue(value[0]) : d2cValue(value); } else if(isOperationIn(INTERVAL_OPS)) {