diff --git a/lib/filter.js b/lib/filter.js new file mode 100644 index 00000000000..e7e4ecf078e --- /dev/null +++ b/lib/filter.js @@ -0,0 +1,9 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +module.exports = require('../src/transforms/filter'); diff --git a/lib/index.js b/lib/index.js index 4f37449c9c3..374dacc1681 100644 --- a/lib/index.js +++ b/lib/index.js @@ -29,4 +29,9 @@ Plotly.register([ require('./scattermapbox') ]); +// add transforms +Plotly.register([ + require('./filter') +]); + module.exports = Plotly; diff --git a/test/jasmine/assets/transforms/filter.js b/src/transforms/filter.js similarity index 65% rename from test/jasmine/assets/transforms/filter.js rename to src/transforms/filter.js index 95215fc50cf..d0ea26f5de8 100644 --- a/test/jasmine/assets/transforms/filter.js +++ b/src/transforms/filter.js @@ -1,11 +1,20 @@ +/** + * Copyright 2012-2016, Plotly, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + 'use strict'; +var isNumeric = require('fast-isnumeric'); + // var Lib = require('@src/lib'); -var Lib = require('../../../../src/lib'); +var Lib = require('../lib'); /*eslint no-unused-vars: 0*/ - // so that Plotly.register knows what to do with it exports.moduleType = 'transform'; @@ -16,11 +25,11 @@ exports.name = 'filter'; exports.attributes = { operation: { valType: 'enumerated', - values: ['=', '<', '>'], + values: ['=', '<', '>', 'within', 'notwithin', 'in', 'notin'], dflt: '=' }, value: { - valType: 'number', + valType: 'any', dflt: 0 }, filtersrc: { @@ -54,6 +63,16 @@ exports.supplyDefaults = function(transformIn, fullData, layout) { coerce('value'); coerce('filtersrc'); + // numeric values as character should be converted to numeric + if(Array.isArray(transformOut.value)) { + transformOut.value = transformOut.value.map(function(v) { + if(isNumeric(v)) v = +v; + return v; + }); + } else { + if(isNumeric(transformOut.value)) transformOut.value = +transformOut.value; + } + // or some more complex logic using fullData and layout return transformOut; @@ -121,6 +140,16 @@ function transformOne(trace, state) { function getFilterFunc(opts) { var value = opts.value; + // if value is not array then coerce to + // an array of [value,value] so the + // filter function will work + // but perhaps should just error out + var valueArr = []; + if(!Array.isArray(value)) { + valueArr = [value, value]; + } else { + valueArr = value; + } switch(opts.operation) { case '=': @@ -129,6 +158,30 @@ function getFilterFunc(opts) { return function(v) { return v < value; }; case '>': return function(v) { return v > value; }; + case 'within': + return function(v) { + // if character then ignore with no side effect + function notDateNumber(d) { + return !(isNumeric(d) || Lib.isDateTime(d)); + } + if(valueArr.some(notDateNumber)) { + return true; + } + + // keep the = ? + return v >= Math.min.apply(null, valueArr) && + v <= Math.max.apply(null, valueArr); + }; + case 'notwithin': + return function(v) { + // keep the = ? + return !(v >= Math.min.apply(null, valueArr) && + v <= Math.max.apply(null, valueArr)); + }; + case 'in': + return function(v) { return valueArr.indexOf(v) >= 0; }; + case 'notin': + return function(v) { return valueArr.indexOf(v) === -1; }; } } diff --git a/test/jasmine/assets/transforms/groupby.js b/src/transforms/groupby.js similarity index 92% rename from test/jasmine/assets/transforms/groupby.js rename to src/transforms/groupby.js index 41748a75c66..a1d70782df5 100644 --- a/test/jasmine/assets/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -1,7 +1,15 @@ +/** + * Copyright 2012-2016, Plotly, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + 'use strict'; // var Lib = require('@src/lib'); -var Lib = require('../../../../src/lib'); +var Lib = require('../lib'); /*eslint no-unused-vars: 0*/ diff --git a/test/jasmine/tests/transforms_test.js b/test/jasmine/tests/transforms_test.js index 1344518c8d7..0925cd39914 100644 --- a/test/jasmine/tests/transforms_test.js +++ b/test/jasmine/tests/transforms_test.js @@ -7,8 +7,9 @@ var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); Plotly.register([ - require('../assets/transforms/filter'), - require('../assets/transforms/groupby') + //register filter in build + //require('@src/transforms/filter'), + require('@src/transforms/groupby') ]); @@ -34,15 +35,17 @@ describe('one-to-one transforms:', function() { }] }]; + var traceIn, traceOut; + afterEach(destroyGraphDiv); it('supplyTraceDefaults should supply the transform defaults', function() { - var traceIn = { + traceIn = { y: [2, 1, 2], transforms: [{ type: 'filter' }] }; - var traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); + traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); expect(traceOut.transforms).toEqual([{ type: 'filter', @@ -53,18 +56,18 @@ describe('one-to-one transforms:', function() { }); it('supplyTraceDefaults should not bail if transform module is not found', function() { - var traceIn = { + traceIn = { y: [2, 1, 2], transforms: [{ type: 'invalid' }] }; - var traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); + traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); expect(traceOut.y).toBe(traceIn.y); }); it('supplyTraceDefaults should honored global transforms', function() { - var traceIn = { + traceIn = { y: [2, 1, 2], transforms: [{ type: 'filter', @@ -80,7 +83,7 @@ describe('one-to-one transforms:', function() { }] }; - var traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); + traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); expect(traceOut.transforms[0]).toEqual({ type: 'filter', @@ -139,7 +142,7 @@ describe('one-to-one transforms:', function() { it('supplyDataDefaults should apply the transform while', function() { var dataIn = [{ x: [-2, -2, 1, 2, 3], - y: [1, 2, 2, 3, 1], + y: [1, 2, 2, 3, 1] }, { x: [-2, -1, -2, 0, 1, 2, 3], y: [1, 2, 3, 1, 2, 3, 1], @@ -329,6 +332,443 @@ describe('one-to-one transforms:', function() { }); }); + + it('supplyTraceDefaults should supply the transform defaults', function() { + traceIn = { + y: [2, 1, 2], + transforms: [{ type: 'filter' }] + }; + + traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); + + expect(traceOut.transforms).toEqual([{ + type: 'filter', + operation: '=', + value: 0, + filtersrc: 'x' + }]); + }); + + it('supplyTraceDefaults should accept numeric as character', function() { + traceIn = { + x: '1', + transforms: [{ + type: 'filter', + value: '0' + }] + }; + + traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); + + expect(traceOut.transforms).toEqual([{ + type: 'filter', + operation: '=', + value: 0, + filtersrc: 'x' + }]); + + // should also convert if array + traceIn = { + x: '1', + transforms: [{ + type: 'filter', + value: ['0'] + }] + }; + + traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); + + expect(traceOut.transforms).toEqual([{ + type: 'filter', + operation: '=', + value: [0], + filtersrc: 'x' + }]); + }); + + it('supplyTraceDefaults should accept numeric as character', function() { + traceIn = { + x: '1', + transforms: [{ + type: 'filter', + value: '0' + }] + }; + + traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); + + expect(traceOut.transforms).toEqual([{ + type: 'filter', + operation: '=', + value: 0, + filtersrc: 'x' + }]); + + // should also convert if array + traceIn = { + x: '1', + transforms: [{ + type: 'filter', + value: ['0'] + }] + }; + + traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); + + expect(traceOut.transforms).toEqual([{ + type: 'filter', + operation: '=', + value: [0], + filtersrc: 'x' + }]); + }); + + it('supplyDataDefaults should apply the transform', function() { + var dataIn = [{ + x: [-2, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + transforms: [{ + type: 'filter', + operation: '>', + value: 0, + filtersrc: 'x' + }] + }]; + + var dataOut = []; + Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + + // does not mutate user data + expect(dataIn[0].x).toEqual([-2, -1, -2, 0, 1, 2, 3]); + expect(dataIn[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + expect(dataIn[0].transforms).toEqual([{ + type: 'filter', + operation: '>', + value: 0, + filtersrc: 'x' + }]); + + // applies transform + expect(dataOut[0].x).toEqual([1, 2, 3]); + expect(dataOut[0].y).toEqual([2, 3, 1]); + + // TODO what is the expected behavior ??? +// expect(dataOut[0].transforms).toEqual([]); + + // keep ref to user data + expect(dataOut[0]._input.x).toEqual([-2, -1, -2, 0, 1, 2, 3]); + expect(dataOut[0]._input.y).toEqual([1, 2, 3, 1, 2, 3, 1]); + expect(dataOut[0]._input.transforms).toEqual([{ + type: 'filter', + operation: '>', + value: 0, + filtersrc: 'x' + }]); + + // keep ref to full transforms array + expect(dataOut[0]._fullInput.transforms).toEqual([{ + type: 'filter', + operation: '>', + value: 0, + filtersrc: 'x' + }]); + + // set index w.r.t. fullData + expect(dataOut[0].index).toEqual(0); + + // TODO do we really need this ??? + // set _index w.r.t. user data + expect(dataOut[0].index).toEqual(0); + }); + + it('filters should chain as AND', function() { + var dataIn = [{ + x: [-2, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + transforms: [ + { + type: 'filter', + operation: '>', + value: 0, + filtersrc: 'x' + }, + { + type: 'filter', + operation: '<', + value: 3, + filtersrc: 'x' + } + ] + }]; + + var dataOut = []; + Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + + // applies transform + expect(dataOut[0].x).toEqual([1, 2]); + expect(dataOut[0].y).toEqual([2, 3]); + }); + + it('filters should handle range numeric values within and notwithin', function() { + var dataIn = [{ + x: [-2, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + transforms: [{ + type: 'filter', + operation: 'within', + value: [-1, 1], + filtersrc: 'x' + }] + }]; + + var dataOut = []; + Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + + // leave this section guarding against mutation + // for now but can probably eliminate later + // does not mutate user data + expect(dataIn[0].x).toEqual([-2, -1, -2, 0, 1, 2, 3]); + expect(dataIn[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + expect(dataIn[0].transforms).toEqual([{ + type: 'filter', + operation: 'within', + value: [-1, 1], + filtersrc: 'x' + }]); + + // applies transform + expect(dataOut[0].x).toEqual([-1, 0, 1]); + expect(dataOut[0].y).toEqual([2, 1, 2]); + }); + + it('filters should ignore character values within and notwithin', function() { + var dataIn = [{ + x: ['a', 'b', 'c', 'd'], + y: [1, 2, 3, 4], + transforms: [{ + type: 'filter', + operation: 'within', + value: ['a', 'c'], + filtersrc: 'x' + }] + }]; + + var dataOut = []; + Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + + // leave this section guarding against mutation + // for now but can probably eliminate later + // does not mutate user data + expect(dataIn[0].x).toEqual(['a', 'b', 'c', 'd']); + expect(dataIn[0].y).toEqual([1, 2, 3, 4]); + expect(dataIn[0].transforms).toEqual([{ + type: 'filter', + operation: 'within', + value: ['a', 'c'], + filtersrc: 'x' + }]); + + // applies transform + expect(dataOut[0].x).toEqual(['a', 'b', 'c', 'd']); + expect(dataOut[0].y).toEqual([1, 2, 3, 4]); + }); + + it('filters should handle numeric values in', function() { + var dataIn = [{ + x: [-2, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + transforms: [{ + type: 'filter', + operation: 'in', + value: [-2, 0], + filtersrc: 'x' + }] + }]; + + var dataOut = []; + Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + + // applies transform + expect(dataOut[0].x).toEqual([-2, -2, 0]); + expect(dataOut[0].y).toEqual([1, 3, 1]); + }); + + it('filters should handle numeric values in', function() { + var dataIn = [{ + x: [-2, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + transforms: [{ + type: 'filter', + operation: 'notin', + value: [-2, 0], + filtersrc: 'x' + }] + }]; + + var dataOut = []; + Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + + // applies transform + expect(dataOut[0].x).toEqual([-1, 1, 2, 3]); + expect(dataOut[0].y).toEqual([2, 2, 3, 1]); + }); + + + it('filters should handle strings with in', function() { + var dataIn = [{ + x: ['y', 't', 'b', 'm', 'p', 'l', 'o'], + y: [1, 2, 3, 1, 5, 10, 20], + transforms: [{ + type: 'filter', + operation: 'in', + value: ['p', 'l', 'o'], + filtersrc: 'x' + }] + }]; + + + var dataOut = []; + Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + + // applies transform + expect(dataOut[0].x).toEqual(['p', 'l', 'o']); + expect(dataOut[0].y).toEqual([5, 10, 20]); + }); + + it('filters should handle strings with in', function() { + var dataIn = [{ + x: ['y', 't', 'b', 'm', 'p', 'l', 'o'], + y: [1, 2, 3, 1, 5, 10, 20], + transforms: [{ + type: 'filter', + operation: 'notin', + value: ['p', 'l', 'o'], + filtersrc: 'x' + }] + }]; + + + var dataOut = []; + Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + + // applies transform + expect(dataOut[0].x).toEqual(['y', 't', 'b', 'm']); + expect(dataOut[0].y).toEqual([1, 2, 3, 1]); + }); + + + it('Plotly.plot should plot the transform trace', function(done) { + var data = Lib.extendDeep([], mockData0); + + Plotly.plot(createGraphDiv(), data).then(function(gd) { + assertDims([3]); + + var uid = data[0].uid; + expect(gd._fullData[0].uid).toEqual(uid + '0'); + + done(); + }); + }); + + it('Plotly.restyle should work', function(done) { + var data = Lib.extendDeep([], mockData0); + data[0].marker = { color: 'red' }; + + var gd = createGraphDiv(); + var dims = [3]; + + Plotly.plot(gd, data).then(function() { + expect(gd._fullData[0].marker.color).toEqual('red'); + assertStyle(dims, ['rgb(255, 0, 0)'], [1]); + + return Plotly.restyle(gd, 'marker.color', 'blue'); + }).then(function() { + expect(gd._fullData[0].marker.color).toEqual('blue'); + assertStyle(dims, ['rgb(0, 0, 255)'], [1]); + + return Plotly.restyle(gd, 'marker.color', 'red'); + }).then(function() { + expect(gd._fullData[0].marker.color).toEqual('red'); + assertStyle(dims, ['rgb(255, 0, 0)'], [1]); + + return Plotly.restyle(gd, 'transforms[0].value', 2.5); + }).then(function() { + assertStyle([1], ['rgb(255, 0, 0)'], [1]); + + done(); + }); + }); + + it('Plotly.extendTraces should work', function(done) { + var data = Lib.extendDeep([], mockData0); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + expect(gd.data[0].x.length).toEqual(7); + expect(gd._fullData[0].x.length).toEqual(3); + + assertDims([3]); + + return Plotly.extendTraces(gd, { + x: [ [-3, 4, 5] ], + y: [ [1, -2, 3] ] + }, [0]); + }).then(function() { + expect(gd.data[0].x.length).toEqual(10); + expect(gd._fullData[0].x.length).toEqual(5); + + assertDims([5]); + + done(); + }); + }); + + it('Plotly.deleteTraces should work', function(done) { + var data = Lib.extendDeep([], mockData1); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + assertDims([3, 4]); + + return Plotly.deleteTraces(gd, [1]); + }).then(function() { + assertDims([3]); + + return Plotly.deleteTraces(gd, [0]); + }).then(function() { + assertDims([]); + + done(); + }); + + }); + + it('toggling trace visibility should work', function(done) { + var data = Lib.extendDeep([], mockData1); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + assertDims([3, 4]); + + return Plotly.restyle(gd, 'visible', 'legendonly', [1]); + }).then(function() { + assertDims([3]); + + return Plotly.restyle(gd, 'visible', false, [0]); + }).then(function() { + assertDims([]); + + return Plotly.restyle(gd, 'visible', [true, true], [0, 1]); + }).then(function() { + assertDims([3, 4]); + + done(); + }); + }); + }); describe('one-to-many transforms:', function() { @@ -358,34 +798,6 @@ describe('one-to-many transforms:', function() { afterEach(destroyGraphDiv); - it('supplyDataDefaults should apply the transform while', function() { - var dummyTrace0 = { - x: [-2, -2, 1, 2, 3], - y: [1, 2, 2, 3, 1], - }; - - var dummyTrace1 = { - x: [-1, 2, 3], - y: [2, 3, 1], - }; - - var dataIn = [ - dummyTrace0, - Lib.extendDeep({}, mockData0[0]), - dummyTrace1, - Lib.extendDeep({}, mockData1[0]) - ]; - - var dataOut = []; - Plots.supplyDataDefaults(dataIn, dataOut, {}, []); - - expect(dataOut.map(function(trace) { return trace.index; })) - .toEqual([0, 1, 1, 2, 3, 3], 'setting index w.r.t user data'); - - expect(dataOut.map(function(trace) { return trace._expandedIndex; })) - .toEqual([0, 1, 2, 3, 4, 5], 'setting index w.r.t full data'); - }); - it('Plotly.plot should plot the transform traces', function(done) { var data = Lib.extendDeep([], mockData0); @@ -563,34 +975,6 @@ describe('multiple transforms:', function() { afterEach(destroyGraphDiv); - it('supplyDataDefaults should apply the transform while', function() { - var dummyTrace0 = { - x: [-2, -2, 1, 2, 3], - y: [1, 2, 2, 3, 1], - }; - - var dummyTrace1 = { - x: [-1, 2, 3], - y: [2, 3, 1], - }; - - var dataIn = [ - dummyTrace0, - Lib.extendDeep({}, mockData0[0]), - Lib.extendDeep({}, mockData1[0]), - dummyTrace1 - ]; - - var dataOut = []; - Plots.supplyDataDefaults(dataIn, dataOut, {}, []); - - expect(dataOut.map(function(trace) { return trace.index; })) - .toEqual([0, 1, 1, 2, 2, 3], 'setting index w.r.t user data'); - - expect(dataOut.map(function(trace) { return trace._expandedIndex; })) - .toEqual([0, 1, 2, 3, 4, 5], 'setting index w.r.t full data'); - }); - it('Plotly.plot should plot the transform traces', function(done) { var data = Lib.extendDeep([], mockData0); @@ -788,34 +1172,6 @@ describe('multiple traces with transforms:', function() { afterEach(destroyGraphDiv); - it('supplyDataDefaults should apply the transform while', function() { - var dummyTrace0 = { - x: [-2, -2, 1, 2, 3], - y: [1, 2, 2, 3, 1], - }; - - var dummyTrace1 = { - x: [-1, 2, 3], - y: [2, 3, 1], - }; - - var dataIn = [ - dummyTrace0, - Lib.extendDeep({}, mockData0[0]), - Lib.extendDeep({}, mockData0[1]), - dummyTrace1 - ]; - - var dataOut = []; - Plots.supplyDataDefaults(dataIn, dataOut, {}, []); - - expect(dataOut.map(function(trace) { return trace.index; })) - .toEqual([0, 1, 2, 2, 3], 'setting index w.r.t user data'); - - expect(dataOut.map(function(trace) { return trace._expandedIndex; })) - .toEqual([0, 1, 2, 3, 4], 'setting index w.r.t full data'); - }); - it('Plotly.plot should plot the transform traces', function(done) { var data = Lib.extendDeep([], mockData0); diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js index 8a55323a6bd..40220481f7e 100644 --- a/test/jasmine/tests/validate_test.js +++ b/test/jasmine/tests/validate_test.js @@ -1,12 +1,6 @@ var Plotly = require('@lib/index'); var Lib = require('@src/lib'); -Plotly.register([ - // until they become official - require('../assets/transforms/filter') -]); - - describe('Plotly.validate', function() { function assertErrorContent(obj, code, cont, trace, path, astr, msg) {