diff --git a/src/lib/coerce.js b/src/lib/coerce.js index 11f9153cac1..989daa6a331 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -458,6 +458,17 @@ exports.findArrayAttributes = function(trace) { exports.crawl(trace._module.attributes, callback); + if(trace.transforms) { + var transforms = trace.transforms; + + for(var i = 0; i < transforms.length; i++) { + var transform = transforms[i]; + + stack = ['transforms[' + i + ']']; + exports.crawl(transform._module.attributes, callback, 1); + } + } + // Look into the fullInput module attributes for array attributes // to make sure that 'custom' array attributes are detected. // diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index 35d9a0495f1..d7b5ea03af8 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -340,6 +340,24 @@ exports.cleanData = function(data, existingData) { } } + // transforms backward compatibility fixes + if(Array.isArray(trace.transforms)) { + var transforms = trace.transforms; + + for(i = 0; i < transforms.length; i++) { + var transform = transforms[i]; + + if(!Lib.isPlainObject(transform)) continue; + + if(transform.type === 'filter') { + if(transform.filtersrc) { + transform.target = transform.filtersrc; + delete transform.filtersrc; + } + } + } + } + // prune empty containers made before the new nestedProperty if(emptyContainer(trace, 'line')) delete trace.line; if('marker' in trace) { diff --git a/src/plots/plots.js b/src/plots/plots.js index f8c52b56624..6ca52d59d4e 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -807,6 +807,7 @@ function supplyTransformDefaults(traceIn, traceOut, layout) { if(_module && _module.supplyDefaults) { transformOut = _module.supplyDefaults(transformIn, traceOut, layout, traceIn); transformOut.type = type; + transformOut._module = _module; } else { transformOut = Lib.extendFlat({}, transformIn); diff --git a/src/transforms/filter.js b/src/transforms/filter.js index 042798ac4ef..8c9a23b7071 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -10,6 +10,8 @@ var Lib = require('../lib'); 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 INTERVAL_OPS = ['[]', '()', '[)', '(]', '][', ')(', '](', ')[']; @@ -27,18 +29,22 @@ exports.attributes = { 'Determines whether this filter transform is enabled or disabled.' ].join(' ') }, - filtersrc: { + target: { valType: 'string', strict: true, noBlank: true, + arrayOk: true, dflt: 'x', description: [ - 'Sets the variable in the parent trace object', - 'by which the filter will be applied.', + 'Sets the filter target by which the filter is applied.', + 'If a string, *target* is assumed to be a reference to a data array', + 'in the parent trace object.', 'To filter about nested variables, use *.* to access them.', - 'For example, set `filtersrc` to *marker.color* to filter', - 'about the marker color array.' + 'For example, set `target` to *marker.color* to filter', + 'about the marker color array.', + + 'If an array, *target* is then the data array by which the filter is applied.' ].join(' ') }, operation: { @@ -77,7 +83,7 @@ exports.attributes = { 'Sets the value or values by which to filter by.', 'Values are expected to be in the same type as the data linked', - 'to *filtersrc*.', + 'to *target*.', 'When `operation` is set to one of the inequality values', '(' + INEQUALITY_OPS + ')', @@ -108,25 +114,24 @@ exports.supplyDefaults = function(transformIn) { if(enabled) { coerce('operation'); coerce('value'); - coerce('filtersrc'); + coerce('target'); } return transformOut; }; exports.calcTransform = function(gd, trace, opts) { - var filtersrc = opts.filtersrc, - filtersrcOk = filtersrc && Array.isArray(Lib.nestedProperty(trace, filtersrc).get()); - - if(!opts.enabled || !filtersrcOk) return; + if(!opts.enabled) return; - var dataToCoord = getDataToCoordFunc(gd, trace, filtersrc), - filterFunc = getFilterFunc(opts, dataToCoord); + var target = opts.target, + filterArray = getFilterArray(trace, target), + len = filterArray.length; - var filterArr = Lib.nestedProperty(trace, filtersrc).get(), - len = filterArr.length; + if(!len) return; - var arrayAttrs = Lib.findArrayAttributes(trace), + var dataToCoord = getDataToCoordFunc(gd, trace, target), + filterFunc = getFilterFunc(opts, dataToCoord), + arrayAttrs = Lib.findArrayAttributes(trace), originalArrays = {}; // copy all original array attribute values, @@ -147,7 +152,7 @@ exports.calcTransform = function(gd, trace, opts) { } for(var i = 0; i < len; i++) { - var v = filterArr[i]; + var v = filterArray[i]; if(!filterFunc(v)) continue; @@ -157,18 +162,43 @@ exports.calcTransform = function(gd, trace, opts) { } }; -function getDataToCoordFunc(gd, trace, filtersrc) { - var ax = axisIds.getFromTrace(gd, trace, filtersrc); +function getFilterArray(trace, target) { + if(typeof target === 'string' && target) { + var array = Lib.nestedProperty(trace, target).get(); + + return Array.isArray(array) ? array : []; + } + else if(Array.isArray(target)) return target.slice(); + + return false; +} + +function getDataToCoordFunc(gd, trace, target) { + var ax; + + // In the case of an array target, make a mock data array + // and call supplyDefaults to the data type and + // setup the data-to-calc method. + if(Array.isArray(target)) { + ax = { + type: autoType(target), + _categories: [] + }; + setConvert(ax); + } + else { + ax = axisIds.getFromTrace(gd, trace, target); + } - // if 'filtersrc' has corresponding axis + // if 'target' has corresponding axis // -> use setConvert method if(ax) return ax.d2c; // special case for 'ids' // -> cast to String - if(filtersrc === 'ids') return function(v) { return String(v); }; + if(target === 'ids') return function(v) { return String(v); }; - // otherwise + // otherwise (e.g. numeric-array of 'marker.color' or 'marker.size') // -> cast to Number return function(v) { return +v; }; } diff --git a/test/jasmine/tests/finance_test.js b/test/jasmine/tests/finance_test.js index 3cf908235f9..6470dbb8fdb 100644 --- a/test/jasmine/tests/finance_test.js +++ b/test/jasmine/tests/finance_test.js @@ -423,7 +423,7 @@ describe('finance charts calc transforms:', function() { transforms: [{ type: 'filter', operation: '>', - filtersrc: 'open', + target: 'open', value: 33 }] }); @@ -433,7 +433,7 @@ describe('finance charts calc transforms:', function() { transforms: [{ type: 'filter', operation: '{}', - filtersrc: 'x', + target: 'x', value: ['2016-09-01', '2016-09-10'] }] }); diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index c42ebf273e9..723a8f5f581 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -995,6 +995,35 @@ describe('Test plot api', function() { expect(gd.data[1].contours).toBeUndefined(); }); + + it('should rename *filtersrc* to *target* in filter transforms', function() { + var data = [{ + transforms: [{ + type: 'filter', + filtersrc: 'y' + }, { + type: 'filter', + operation: '<' + }] + }, { + transforms: [{ + type: 'filter', + target: 'y' + }] + }]; + + Plotly.plot(gd, data); + + var trace0 = gd.data[0], + trace1 = gd.data[1]; + + expect(trace0.transforms.length).toEqual(2); + expect(trace0.transforms[0].filtersrc).toBeUndefined(); + expect(trace0.transforms[0].target).toEqual('y'); + + expect(trace1.transforms.length).toEqual(1); + expect(trace1.transforms[0].target).toEqual('y'); + }); }); describe('Plotly.update should', function() { diff --git a/test/jasmine/tests/plotschema_test.js b/test/jasmine/tests/plotschema_test.js index e16aa5e8b82..fa822c23fdf 100644 --- a/test/jasmine/tests/plotschema_test.js +++ b/test/jasmine/tests/plotschema_test.js @@ -176,7 +176,7 @@ describe('plot schema', function() { var valObjects = plotSchema.transforms.filter.attributes, attrNames = Object.keys(valObjects); - ['operation', 'value', 'filtersrc'].forEach(function(k) { + ['operation', 'value', 'target'].forEach(function(k) { expect(attrNames).toContain(k); }); }); diff --git a/test/jasmine/tests/transform_filter_test.js b/test/jasmine/tests/transform_filter_test.js index fdba96d7609..722b810c7a2 100644 --- a/test/jasmine/tests/transform_filter_test.js +++ b/test/jasmine/tests/transform_filter_test.js @@ -1,4 +1,6 @@ var Plotly = require('@lib/index'); +var Filter = require('@lib/filter'); + var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); @@ -7,7 +9,6 @@ var destroyGraphDiv = require('../assets/destroy_graph_div'); var assertDims = require('../assets/assert_dims'); var assertStyle = require('../assets/assert_style'); - describe('filter transforms defaults:', function() { var traceIn, traceOut; @@ -28,7 +29,8 @@ describe('filter transforms defaults:', function() { enabled: true, operation: '=', value: 0, - filtersrc: 'x' + target: 'x', + _module: Filter }]); }); @@ -47,32 +49,33 @@ describe('filter transforms defaults:', function() { expect(traceOut.transforms).toEqual([{ type: 'filter', enabled: false, + _module: Filter }]); }); - it('supplyTraceDefaults should coerce *filtersrc* as a strict / noBlank string', function() { + it('supplyTraceDefaults should coerce *target* as a strict / noBlank string', function() { traceIn = { x: [1, 2, 3], transforms: [{ type: 'filter', }, { type: 'filter', - filtersrc: 0 + target: 0 }, { type: 'filter', - filtersrc: '' + target: '' }, { type: 'filter', - filtersrc: 'marker.color' + target: 'marker.color' }] }; traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); - expect(traceOut.transforms[0].filtersrc).toEqual('x'); - expect(traceOut.transforms[1].filtersrc).toEqual('x'); - expect(traceOut.transforms[2].filtersrc).toEqual('x'); - expect(traceOut.transforms[3].filtersrc).toEqual('marker.color'); + expect(traceOut.transforms[0].target).toEqual('x'); + expect(traceOut.transforms[1].target).toEqual('x'); + expect(traceOut.transforms[2].target).toEqual('x'); + expect(traceOut.transforms[3].target).toEqual('marker.color'); }); }); @@ -106,13 +109,13 @@ describe('filter transforms calc:', function() { transforms: [{ type: 'filter' }] }; - it('filters should skip if *filtersrc* isn\'t present in trace', function() { + it('filters should skip if *target* isn\'t present in trace', function() { var out = _transform([Lib.extendDeep({}, base, { transforms: [{ type: 'filter', operation: '>', value: 0, - filtersrc: 'z' + target: 'z' }] })]); @@ -128,7 +131,7 @@ describe('filter transforms calc:', function() { type: 'filter', operation: '>', value: '2016-10-01', - filtersrc: 'z' + target: 'z' }] })]); @@ -146,7 +149,7 @@ describe('filter transforms calc:', function() { type: 'filter', operation: '>', value: 0, - filtersrc: 'lon' + target: 'lon' }] }; @@ -158,7 +161,7 @@ describe('filter transforms calc:', function() { type: 'filter', operation: '<', value: 0, - filtersrc: 'lat' + target: 'lat' }] }; @@ -177,7 +180,7 @@ describe('filter transforms calc:', function() { type: 'filter', operation: '>', value: 0.2, - filtersrc: 'marker.color' + target: 'marker.color' }] })]); @@ -193,7 +196,7 @@ describe('filter transforms calc:', function() { enabled: false, operation: '>', value: 0, - filtersrc: 'x' + target: 'x' }] })]); @@ -207,12 +210,12 @@ describe('filter transforms calc:', function() { type: 'filter', operation: '>', value: 0, - filtersrc: 'x' + target: 'x' }, { type: 'filter', operation: '<', value: 3, - filtersrc: 'x' + target: 'x' }] })]); @@ -226,18 +229,18 @@ describe('filter transforms calc:', function() { type: 'filter', operation: '>', value: 0, - filtersrc: 'x' + target: 'x' }, { type: 'filter', enabled: false, operation: '>', value: 2, - filtersrc: 'y' + target: 'y' }, { type: 'filter', operation: '<', value: 2, - filtersrc: 'y' + target: 'y' }] })]); @@ -260,7 +263,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '[]', value: [-1, 1], - filtersrc: 'x' + target: 'x' }] })]); @@ -276,7 +279,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '[)', value: [-1, 1], - filtersrc: 'x' + target: 'x' }] })]); @@ -288,7 +291,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '(]', value: [-1, 1], - filtersrc: 'x' + target: 'x' }] })]); @@ -300,7 +303,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '()', value: [-1, 1], - filtersrc: 'x' + target: 'x' }] })]); @@ -312,7 +315,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: ')(', value: [-1, 1], - filtersrc: 'x' + target: 'x' }] })]); @@ -328,7 +331,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: ')[', value: [-1, 1], - filtersrc: 'x' + target: 'x' }] })]); @@ -344,7 +347,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '](', value: [-1, 1], - filtersrc: 'x' + target: 'x' }] })]); @@ -360,7 +363,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '][', value: [-1, 1], - filtersrc: 'x' + target: 'x' }] })]); @@ -376,7 +379,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '{}', value: [-2, 0], - filtersrc: 'x' + target: 'x' }] })]); @@ -392,7 +395,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '}{', value: [-2, 0], - filtersrc: 'x' + target: 'x' }] })]); @@ -409,7 +412,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '>', value: -1, - filtersrc: 'x' + target: 'x' }] })], { xaxis: { type: 'category' } @@ -443,7 +446,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '()', value: ['a', 'c'], - filtersrc: 'x' + target: 'x' }] })]); @@ -455,7 +458,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: ')(', value: ['a', 'c'], - filtersrc: 'x' + target: 'x' }] })]); @@ -467,7 +470,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '{}', value: ['b', 'd'], - filtersrc: 'x' + target: 'x' }] })]); @@ -479,7 +482,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '}{', value: ['b', 'd'], - filtersrc: 'x' + target: 'x' }] })]); @@ -513,7 +516,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '=', value: ['2015-07-20'], - filtersrc: 'x' + target: 'x' }] })]); @@ -525,7 +528,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '<', value: '2016-01-01', - filtersrc: 'x' + target: 'x' }] })]); @@ -537,7 +540,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '>=', value: '2016-08-01', - filtersrc: 'x' + target: 'x' }] })]); @@ -553,7 +556,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '[]', value: ['2016-08-01', '2016-10-01'], - filtersrc: 'x' + target: 'x' }] })]); @@ -565,7 +568,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: ')(', value: ['2016-08-01', '2016-10-01'], - filtersrc: 'x' + target: 'x' }] })]); @@ -577,7 +580,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '{}', value: '2015-07-20', - filtersrc: 'x' + target: 'x' }] })]); @@ -589,7 +592,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '}{', value: ['2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], - filtersrc: 'x' + target: 'x' }] })]); @@ -603,7 +606,7 @@ describe('filter transforms calc:', function() { transforms: [{ operation: '{}', value: ['p1', 'p2', 'n1'], - filtersrc: 'ids' + target: 'ids' }] })]); @@ -611,6 +614,74 @@ describe('filter transforms calc:', function() { expect(out[0].y).toEqual([2, 2, 3]); expect(out[0].ids).toEqual(['n1', 'p1', 'p2']); }); + + describe('filters should handle array *target* values', function() { + var _base = Lib.extendDeep({}, base); + + function _assert(out, x, y, markerColor) { + expect(out[0].x).toEqual(x, '- x coords'); + expect(out[0].y).toEqual(y, '- y coords'); + expect(out[0].marker.color).toEqual(markerColor, '- marker.color arrayOk'); + expect(out[0].marker.size).toEqual(20, '- marker.size style'); + } + + it('with numeric items', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + target: [1, 1, 0, 0, 1, 0, 1], + operation: '{}', + value: 0 + }] + })]); + + _assert(out, [-2, 0, 2], [3, 1, 3], [0.3, 0.1, 0.3]); + expect(out[0].transforms[0].target).toEqual([0, 0, 0]); + }); + + it('with categorical items', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + target: ['a', 'a', 'b', 'b', 'a', 'b', 'a'], + operation: '{}', + value: 'b' + }] + })]); + + _assert(out, [-2, 0, 2], [3, 1, 3], [0.3, 0.1, 0.3]); + expect(out[0].transforms[0].target).toEqual(['b', 'b', 'b']); + }); + + it('with dates items', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + target: ['2015-07-20', '2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], + operation: '<', + value: '2016-01-01' + }] + })]); + + _assert(out, [-2], [1], [0.1]); + expect(out[0].transforms[0].target).toEqual(['2015-07-20']); + }); + + it('with multiple transforms (dates) ', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + target: ['2015-07-20', '2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], + operation: '>', + value: '2016-01-01' + }, { + type: 'filter', + target: ['2015-07-20', '2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], + operation: '<', + value: '2016-09-01' + }] + })]); + + _assert(out, [-1], [2], [0.2]); + expect(out[0].transforms[0].target).toEqual(['2016-08-01']); + }); + }); }); describe('filter transforms interactions', function() { diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index 43499b73079..73c264f50e9 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -1,4 +1,6 @@ var Plotly = require('@lib/index'); +var Filter = require('@lib/filter'); + var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); @@ -26,7 +28,8 @@ describe('general transforms:', function() { enabled: true, operation: '=', value: 0, - filtersrc: 'x' + target: 'x', + _module: Filter }]); }); @@ -48,7 +51,7 @@ describe('general transforms:', function() { type: 'filter', operation: '>', value: 0, - filtersrc: 'x' + target: 'x' }] }; @@ -65,7 +68,8 @@ describe('general transforms:', function() { enabled: true, operation: '=', value: 0, - filtersrc: 'x' + target: 'x', + _module: Filter }, '- global first'); expect(traceOut.transforms[1]).toEqual({ @@ -73,7 +77,8 @@ describe('general transforms:', function() { enabled: true, operation: '>', value: 0, - filtersrc: 'x' + target: 'x', + _module: Filter }, '- trace second'); }); @@ -88,7 +93,7 @@ describe('general transforms:', function() { type: 'filter', operation: '>', value: 0, - filtersrc: 'x' + target: 'x' }] }]; @@ -104,7 +109,7 @@ describe('general transforms:', function() { type: 'filter', operation: '>', value: 0, - filtersrc: 'x' + target: 'x' }], msg); msg = 'supplying the transform defaults'; @@ -113,7 +118,8 @@ describe('general transforms:', function() { enabled: true, operation: '>', value: 0, - filtersrc: 'x' + target: 'x', + _module: Filter }, msg); msg = 'keeping refs to user data'; @@ -123,7 +129,7 @@ describe('general transforms:', function() { type: 'filter', operation: '>', value: 0, - filtersrc: 'x' + target: 'x', }], msg); msg = 'keeping refs to full transforms array'; @@ -132,7 +138,8 @@ describe('general transforms:', function() { enabled: true, operation: '>', value: 0, - filtersrc: 'x' + target: 'x', + _module: Filter }], msg); msg = 'setting index w.r.t user data'; diff --git a/test/jasmine/tests/transition_test.js b/test/jasmine/tests/transition_test.js index ff54580b747..86348f394e2 100644 --- a/test/jasmine/tests/transition_test.js +++ b/test/jasmine/tests/transition_test.js @@ -71,17 +71,17 @@ function runTests(transitionDuration) { enabled: true, type: 'filter', operation: '<', - filtersrc: 'x', + target: 'x', value: 10 } }, [0]).then(function() { - expect(gd._fullData[0].transforms).toEqual([{ + expect(gd._fullData[0].transforms).toEqual([jasmine.objectContaining({ enabled: true, type: 'filter', operation: '<', - filtersrc: 'x', + target: 'x', value: 10 - }]); + })]); return Plots.transition(gd, [{ 'transforms[0].operation': '>' @@ -90,13 +90,13 @@ function runTests(transitionDuration) { {duration: transitionDuration, easing: 'cubic-in-out'} ); }).then(function() { - expect(gd._fullData[0].transforms).toEqual([{ + expect(gd._fullData[0].transforms).toEqual([jasmine.objectContaining({ enabled: true, type: 'filter', operation: '>', - filtersrc: 'x', + target: 'x', value: 10 - }]); + })]); }).catch(fail).then(done); });