diff --git a/lib/filter.js b/lib/filter.js index e7e4ecf078e..cb0f9ba8fad 100644 --- a/lib/filter.js +++ b/lib/filter.js @@ -6,4 +6,6 @@ * LICENSE file in the root directory of this source tree. */ +'use strict'; + module.exports = require('../src/transforms/filter'); diff --git a/lib/groupby.js b/lib/groupby.js new file mode 100644 index 00000000000..fc7bc528e09 --- /dev/null +++ b/lib/groupby.js @@ -0,0 +1,11 @@ +/** +* 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'; + +module.exports = require('../src/transforms/groupby'); diff --git a/lib/index.js b/lib/index.js index 7f767ebe8e1..359de92a48e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -10,6 +10,7 @@ var Plotly = require('./core'); +// traces Plotly.register([ require('./bar'), require('./box'), @@ -30,9 +31,19 @@ Plotly.register([ require('./scattermapbox') ]); -// add transforms +// transforms +// +// Please note that all *transform* methods are executed before +// all *calcTransform* methods - which could possibly lead to +// unexpected results when applying multiple transforms of different types +// to a given trace. +// +// For more info, see: +// https://github.com/plotly/plotly.js/pull/978#pullrequestreview-2403353 +// Plotly.register([ - require('./filter') + require('./filter'), + require('./groupby') ]); module.exports = Plotly; diff --git a/src/lib/coerce.js b/src/lib/coerce.js index b974f323016..d0f8565847f 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -409,3 +409,53 @@ exports.crawl = function(attrs, callback, specifiedLevel) { if(isPlainObject(attr)) exports.crawl(attr, callback, level + 1); }); }; + +/** + * Find all data array attributes in a given trace object - including + * `arrayOk` attributes. + * + * @param {object} trace + * full trace object that contains a reference to `_module.attributes` + * + * @return {array} arrayAttributes + * list of array attributes for the given trace + */ +exports.findArrayAttributes = function(trace) { + var arrayAttributes = [], + stack = []; + + /** + * A closure that gathers attribute paths into its enclosed arraySplitAttributes + * Attribute paths are collected iff their leaf node is a splittable attribute + * + * @callback callback + * @param {object} attr an attribute + * @param {String} attrName name string + * @param {object[]} attrs all the attributes + * @param {Number} level the recursion level, 0 at the root + * + * @closureVariable {String[][]} arrayAttributes the set of gathered attributes + * Example of filled closure variable (expected to be initialized to []): + * [["marker","size"],["marker","line","width"],["marker","line","color"]] + */ + function callback(attr, attrName, attrs, level) { + stack = stack.slice(0, level).concat([attrName]); + + var splittableAttr = attr.valType === 'data_array' || attr.arrayOk === true; + if(!splittableAttr) return; + + var astr = toAttrString(stack); + var val = nestedProperty(trace, astr).get(); + if(!Array.isArray(val)) return; + + arrayAttributes.push(astr); + } + + function toAttrString(stack) { + return stack.join('.'); + } + + exports.crawl(trace._module.attributes, callback); + + return arrayAttributes; +}; diff --git a/src/lib/index.js b/src/lib/index.js index 2cbe9af07c3..3518fca5d31 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -25,6 +25,7 @@ lib.coerceFont = coerceModule.coerceFont; lib.validate = coerceModule.validate; lib.isValObject = coerceModule.isValObject; lib.crawl = coerceModule.crawl; +lib.findArrayAttributes = coerceModule.findArrayAttributes; lib.IS_SUBPLOT_OBJ = coerceModule.IS_SUBPLOT_OBJ; lib.IS_LINKED_TO_ARRAY = coerceModule.IS_LINKED_TO_ARRAY; lib.DEPRECATED = coerceModule.DEPRECATED; diff --git a/src/plot_api/register.js b/src/plot_api/register.js index 4451bc17020..5192b95a99c 100644 --- a/src/plot_api/register.js +++ b/src/plot_api/register.js @@ -61,14 +61,28 @@ function registerTransformModule(newModule) { var prefix = 'Transform module ' + newModule.name; - if(typeof newModule.transform !== 'function') { - throw new Error(prefix + ' is missing a *transform* function.'); + var hasTransform = typeof newModule.transform === 'function', + hasCalcTransform = typeof newModule.calcTransform === 'function'; + + + if(!hasTransform && !hasCalcTransform) { + throw new Error(prefix + ' is missing a *transform* or *calcTransform* method.'); + } + + if(hasTransform && hasCalcTransform) { + Lib.log([ + prefix + ' has both a *transform* and *calcTransform* methods.', + 'Please note that all *transform* methods are executed', + 'before all *calcTransform* methods.' + ].join(' ')); } + if(!Lib.isPlainObject(newModule.attributes)) { Lib.log(prefix + ' registered without an *attributes* object.'); } + if(typeof newModule.supplyDefaults !== 'function') { - Lib.log(prefix + ' registered without a *supplyDefaults* function.'); + Lib.log(prefix + ' registered without a *supplyDefaults* method.'); } Registry.transformsRegistry[newModule.name] = newModule; diff --git a/src/plots/plots.js b/src/plots/plots.js index 195981f6185..7140900068a 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -16,7 +16,9 @@ var Plotly = require('../plotly'); var Registry = require('../registry'); var Lib = require('../lib'); var Color = require('../components/color'); + var plots = module.exports = {}; + var animationAttrs = require('./animation_attributes'); var frameAttrs = require('./frame_attributes'); @@ -786,52 +788,15 @@ function applyTransforms(fullTrace, fullData, layout) { var container = fullTrace.transforms, dataOut = [fullTrace]; - var attributeSets = dataOut.map(function(trace) { - - var arraySplitAttributes = []; - - var stack = []; - - /** - * A closure that gathers attribute paths into its enclosed arraySplitAttributes - * Attribute paths are collected iff their leaf node is a splittable attribute - * @callback callback - * @param {object} attr an attribute - * @param {String} attrName name string - * @param {object[]} attrs all the attributes - * @param {Number} level the recursion level, 0 at the root - * @closureVariable {String[][]} arraySplitAttributes the set of gathered attributes - * Example of filled closure variable (expected to be initialized to []): - * [["marker","size"],["marker","line","width"],["marker","line","color"]] - */ - function callback(attr, attrName, attrs, level) { - - stack = stack.slice(0, level).concat([attrName]); - - var splittableAttr = attr.valType === 'data_array' || attr.arrayOk === true; - if(splittableAttr) { - arraySplitAttributes.push(stack.slice()); - } - } - - Lib.crawl(trace._module.attributes, callback); - - return arraySplitAttributes.map(function(path) { - return path.join('.'); - }); - }); - for(var i = 0; i < container.length; i++) { var transform = container[i], - type = transform.type, - _module = transformsRegistry[type]; + _module = transformsRegistry[transform.type]; - if(_module) { + if(_module && _module.transform) { dataOut = _module.transform(dataOut, { transform: transform, fullTrace: fullTrace, fullData: fullData, - attributeSets: attributeSets, layout: layout, transformIndex: i }); @@ -1617,7 +1582,7 @@ plots.doCalcdata = function(gd, traces) { var axList = Plotly.Axes.list(gd), fullData = gd._fullData, fullLayout = gd._fullLayout, - i; + i, j; // XXX: Is this correct? Needs a closer look so that *some* traces can be recomputed without // *all* needing doCalcdata: @@ -1655,7 +1620,6 @@ plots.doCalcdata = function(gd, traces) { } var trace = fullData[i], - _module = trace._module, cd = []; // If traces were specified and this trace was not included, then transfer it over from @@ -1665,8 +1629,30 @@ plots.doCalcdata = function(gd, traces) { continue; } - if(_module && trace.visible === true) { - if(_module.calc) cd = _module.calc(gd, trace); + var _module; + if(trace.visible === true) { + + // call calcTransform method if any + if(trace.transforms) { + + // 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 + _module = trace._module; + if(_module && _module.calc) _module.calc(gd, trace); + + for(j = 0; j < trace.transforms.length; j++) { + var transform = trace.transforms[j]; + + _module = transformsRegistry[transform.type]; + if(_module && _module.calcTransform) { + _module.calcTransform(gd, trace, transform); + } + } + } + + _module = trace._module; + if(_module && _module.calc) cd = _module.calc(gd, trace); } // make sure there is a first point diff --git a/src/transforms/filter.js b/src/transforms/filter.js index 9bbe2700f57..042798ac4ef 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -8,129 +8,146 @@ 'use strict'; -var isNumeric = require('fast-isnumeric'); - -// var Lib = require('@src/lib'); var Lib = require('../lib'); +var axisIds = require('../plots/cartesian/axis_ids'); -/* eslint no-unused-vars: 0*/ +var INEQUALITY_OPS = ['=', '<', '>=', '>', '<=']; +var INTERVAL_OPS = ['[]', '()', '[)', '(]', '][', ')(', '](', ')[']; +var SET_OPS = ['{}', '}{']; -// so that Plotly.register knows what to do with it exports.moduleType = 'transform'; -// determines to link between transform type and transform module exports.name = 'filter'; -// ... as trace attributes exports.attributes = { + enabled: { + valType: 'boolean', + dflt: true, + description: [ + 'Determines whether this filter transform is enabled or disabled.' + ].join(' ') + }, + filtersrc: { + valType: 'string', + strict: true, + noBlank: true, + dflt: 'x', + description: [ + 'Sets the variable in the parent trace object', + 'by which the filter will be applied.', + + 'To filter about nested variables, use *.* to access them.', + 'For example, set `filtersrc` to *marker.color* to filter', + 'about the marker color array.' + ].join(' ') + }, operation: { valType: 'enumerated', - values: ['=', '<', '>', 'within', 'notwithin', 'in', 'notin'], - dflt: '=' + values: [].concat(INEQUALITY_OPS).concat(INTERVAL_OPS).concat(SET_OPS), + dflt: '=', + description: [ + 'Sets the filter operation.', + + '*=* keeps items equal to `value`', + + '*<* keeps items less than `value`', + '*<=* keeps items less than or equal to `value`', + + '*>* keeps items greater than `value`', + '*>=* keeps items greater than or equal to `value`', + + '*[]* keeps items inside `value[0]` to value[1]` including both bounds`', + '*()* keeps items inside `value[0]` to value[1]` excluding both bounds`', + '*[)* keeps items inside `value[0]` to value[1]` including `value[0]` but excluding `value[1]', + '*(]* keeps items inside `value[0]` to value[1]` excluding `value[0]` but including `value[1]', + + '*][* keeps items outside `value[0]` to value[1]` and equal to both bounds`', + '*)(* keeps items outside `value[0]` to value[1]`', + '*](* keeps items outside `value[0]` to value[1]` and equal to `value[0]`', + '*)[* keeps items outside `value[0]` to value[1]` and equal to `value[1]`', + + '*{}* keeps items present in a set of values', + '*}{* keeps items not present in a set of values' + ].join(' ') }, value: { valType: 'any', - dflt: 0 - }, - filtersrc: { - valType: 'enumerated', - values: ['x', 'y', 'ids'], - dflt: 'x', - ids: { - valType: 'data_array', - description: 'A list of keys for object constancy of data points during animation' - } + dflt: 0, + description: [ + '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*.', + + 'When `operation` is set to one of the inequality values', + '(' + INEQUALITY_OPS + ')', + '*value* is expected to be a number or a string.', + + 'When `operation` is set to one of the interval value', + '(' + 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', + '(' + SET_OPS + ')', + '*value* is expected to be an array with as many items as', + 'the desired set elements.' + ].join(' ') } }; -/** - * Supply transform attributes defaults - * - * @param {object} transformIn - * object linked to trace.transforms[i] with 'type' set to exports.name - * @param {object} fullData - * the plot's full data - * @param {object} layout - * the plot's (not-so-full) layout - * - * @return {object} transformOut - * copy of transformIn that contains attribute defaults - */ -exports.supplyDefaults = function(transformIn, fullData, layout) { +exports.supplyDefaults = function(transformIn) { var transformOut = {}; function coerce(attr, dflt) { return Lib.coerce(transformIn, transformOut, exports.attributes, attr, dflt); } - coerce('operation'); - 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; - } + var enabled = coerce('enabled'); - // or some more complex logic using fullData and layout + if(enabled) { + coerce('operation'); + coerce('value'); + coerce('filtersrc'); + } return transformOut; }; -/** - * Apply transform !!! - * - * @param {array} data - * array of transformed traces (is [fullTrace] upon first transform) - * - * @param {object} state - * state object which includes: - * - transform {object} full transform attributes - * - fullTrace {object} full trace object which is being transformed - * - fullData {array} full pre-transform(s) data array - * - layout {object} the plot's (not-so-full) layout - * - * @return {object} newData - * array of transformed traces - */ -exports.transform = function(data, state) { - - // one-to-one case - - var newData = data.map(function(trace) { - return transformOne(trace, state); - }); - - return newData; -}; +exports.calcTransform = function(gd, trace, opts) { + var filtersrc = opts.filtersrc, + filtersrcOk = filtersrc && Array.isArray(Lib.nestedProperty(trace, filtersrc).get()); -function transformOne(trace, state) { - var newTrace = Lib.extendDeep({}, trace); + if(!opts.enabled || !filtersrcOk) return; - var opts = state.transform; - var src = opts.filtersrc; - var filterFunc = getFilterFunc(opts); - var len = trace[src].length; - var arrayAttrs = findArrayAttributes(trace); + var dataToCoord = getDataToCoordFunc(gd, trace, filtersrc), + filterFunc = getFilterFunc(opts, dataToCoord); - arrayAttrs.forEach(function(attr) { - Lib.nestedProperty(newTrace, attr).set([]); - }); + var filterArr = Lib.nestedProperty(trace, filtersrc).get(), + len = filterArr.length; + + var arrayAttrs = Lib.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); + + originalArrays[attr] = Lib.extendDeep([], np.get()); + np.set([]); + } function fill(attr, i) { - var arr = Lib.nestedProperty(trace, attr).get(); - var newArr = Lib.nestedProperty(newTrace, attr).get(); + var oldArr = originalArrays[attr], + newArr = Lib.nestedProperty(trace, attr).get(); - newArr.push(arr[i]); + newArr.push(oldArr[i]); } for(var i = 0; i < len; i++) { - var v = trace[src][i]; + var v = filterArr[i]; if(!filterFunc(v)) continue; @@ -138,81 +155,120 @@ function transformOne(trace, state) { fill(arrayAttrs[j], i); } } +}; + +function getDataToCoordFunc(gd, trace, filtersrc) { + var ax = axisIds.getFromTrace(gd, trace, filtersrc); + + // if 'filtersrc' 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); }; - return newTrace; + // otherwise + // -> cast to Number + return function(v) { return +v; }; } -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; +function getFilterFunc(opts, d2c) { + var operation = opts.operation, + value = opts.value, + hasArrayValue = Array.isArray(value); + + function isOperationIn(array) { + return array.indexOf(operation) !== -1; + } + + var coercedValue; + + if(isOperationIn(INEQUALITY_OPS)) { + coercedValue = hasArrayValue ? d2c(value[0]) : d2c(value); } + else if(isOperationIn(INTERVAL_OPS)) { + coercedValue = hasArrayValue ? + [d2c(value[0]), d2c(value[1])] : + [d2c(value), d2c(value)]; + } + else if(isOperationIn(SET_OPS)) { + coercedValue = hasArrayValue ? value.map(d2c) : [d2c(value)]; + } + + switch(operation) { - switch(opts.operation) { case '=': - return function(v) { return v === value; }; + return function(v) { return d2c(v) === coercedValue; }; + case '<': - return function(v) { return v < value; }; + return function(v) { return d2c(v) < coercedValue; }; + + case '<=': + return function(v) { return d2c(v) <= coercedValue; }; + case '>': - return function(v) { return v > value; }; - case 'within': + return function(v) { return d2c(v) > coercedValue; }; + + case '>=': + return function(v) { return d2c(v) >= coercedValue; }; + + case '[]': 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); + var cv = d2c(v); + return cv >= coercedValue[0] && cv <= coercedValue[1]; }; - case 'notwithin': + + case '()': return function(v) { - // keep the = ? - return !(v >= Math.min.apply(null, valueArr) && - v <= Math.max.apply(null, valueArr)); + var cv = d2c(v); + return cv > coercedValue[0] && cv < coercedValue[1]; }; - case 'in': - return function(v) { return valueArr.indexOf(v) >= 0; }; - case 'notin': - return function(v) { return valueArr.indexOf(v) === -1; }; - } -} -function findArrayAttributes(obj, root) { - root = root || ''; + case '[)': + return function(v) { + var cv = d2c(v); + return cv >= coercedValue[0] && cv < coercedValue[1]; + }; - var list = []; + case '(]': + return function(v) { + var cv = d2c(v); + return cv > coercedValue[0] && cv <= coercedValue[1]; + }; - Object.keys(obj).forEach(function(k) { - var val = obj[k]; + case '][': + return function(v) { + var cv = d2c(v); + return cv <= coercedValue[0] || cv >= coercedValue[1]; + }; - if(k.charAt(0) === '_') return; + case ')(': + return function(v) { + var cv = d2c(v); + return cv < coercedValue[0] || cv > coercedValue[1]; + }; - if(k === 'transforms') { - val.forEach(function(item, i) { - list = list.concat( - findArrayAttributes(item, root + k + '[' + i + ']' + '.') - ); - }); - } - else if(Lib.isPlainObject(val)) { - list = list.concat(findArrayAttributes(val, root + k + '.')); - } - else if(Array.isArray(val)) { - list.push(root + k); - } - }); + case '](': + return function(v) { + var cv = d2c(v); + return cv <= coercedValue[0] || cv > coercedValue[1]; + }; + + case ')[': + return function(v) { + var cv = d2c(v); + return cv < coercedValue[0] || cv >= coercedValue[1]; + }; + + case '{}': + return function(v) { + return coercedValue.indexOf(d2c(v)) !== -1; + }; - return list; + case '}{': + return function(v) { + return coercedValue.indexOf(d2c(v)) === -1; + }; + } } diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index a2a9e97816b..f736c0e1a75 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -8,31 +8,40 @@ 'use strict'; -// 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'; -// determines to link between transform type and transform module exports.name = 'groupby'; -// ... as trace attributes exports.attributes = { - active: { + enabled: { valType: 'boolean', - dflt: true + dflt: true, + description: [ + 'Determines whether this group-by transform is enabled or disabled.' + ].join(' ') }, groups: { valType: 'data_array', - dflt: [] + dflt: [], + description: [ + 'Sets the groups in which the trace data will be split.', + 'For example, with `x` set to *[1, 2, 3, 4]* and', + '`groups` set to *[\'a\', \'b\', \'a\', \'b\']*,', + 'the groupby transform with split in one trace', + 'with `x` [1, 3] and one trace with `x` [2, 4].' + ].join(' ') }, style: { valType: 'any', - dflt: {} + dflt: {}, + description: [ + 'Sets each group style.', + 'For example, with `groups` set to *[\'a\', \'b\', \'a\', \'b\']*', + 'and `style` set to *{ a: { marker: { color: \'red\' } }}', + 'marker points in group *\'a\'* will be drawn in red.' + ].join(' ') } }; @@ -49,22 +58,20 @@ exports.attributes = { * @return {object} transformOut * copy of transformIn that contains attribute defaults */ -exports.supplyDefaults = function(transformIn, fullData, layout) { +exports.supplyDefaults = function(transformIn) { var transformOut = {}; function coerce(attr, dflt) { return Lib.coerce(transformIn, transformOut, exports.attributes, attr, dflt); } - var active = coerce('active'); + var enabled = coerce('enabled'); - if(!active) return transformOut; + if(!enabled) return transformOut; coerce('groups'); coerce('style'); - // or some more complex logic using fullData and layout - return transformOut; }; @@ -85,15 +92,11 @@ exports.supplyDefaults = function(transformIn, fullData, layout) { * array of transformed traces */ exports.transform = function(data, state) { - - // one-to-many case - var newData = []; - data.forEach(function(trace, i) { - - newData = newData.concat(transformOne(trace, state, state.attributeSets[i])); - }); + for(var i = 0; i < data.length; i++) { + newData = newData.concat(transformOne(data[i], state)); + } return newData; }; @@ -110,8 +113,7 @@ function pasteArray(newTrace, trace, j, a) { ); } -function transformOne(trace, state, attributeSet) { - +function transformOne(trace, state) { var opts = state.transform; var groups = trace.transforms[state.transformIndex].groups; @@ -126,31 +128,28 @@ function transformOne(trace, state, attributeSet) { var newData = new Array(groupNames.length); var len = groups.length; - var style = opts.style || {}; + var arrayAttrs = Lib.findArrayAttributes(trace); - var arrayAttributes = attributeSet - .filter(function(array) {return Array.isArray(Lib.nestedProperty(trace, array).get());}); + var style = opts.style || {}; for(var i = 0; i < groupNames.length; i++) { var groupName = groupNames[i]; - // TODO is this the best pattern ??? - // maybe we could abstract this out - var newTrace = newData[i] = Lib.extendDeep({}, trace); + var newTrace = newData[i] = Lib.extendDeepNoArrays({}, trace); - arrayAttributes.forEach(initializeArray.bind(null, newTrace)); + arrayAttrs.forEach(initializeArray.bind(null, newTrace)); for(var j = 0; j < len; j++) { if(groups[j] !== groupName) continue; - arrayAttributes.forEach(pasteArray.bind(0, newTrace, trace, j)); + arrayAttrs.forEach(pasteArray.bind(0, newTrace, trace, j)); } newTrace.name = groupName; - // there's no need to coerce style[groupName] here + // there's no need to coerce style[groupName] here // as another round of supplyDefaults is done on the transformed traces - newTrace = Lib.extendDeep(newTrace, style[groupName] || {}); + newTrace = Lib.extendDeepNoArrays(newTrace, style[groupName] || {}); } return newData; diff --git a/test/jasmine/tests/gl2d_pointcloud_test.js b/test/jasmine/tests/gl2d_pointcloud_test.js index 57773c86477..6cc0017ad21 100644 --- a/test/jasmine/tests/gl2d_pointcloud_test.js +++ b/test/jasmine/tests/gl2d_pointcloud_test.js @@ -2,11 +2,6 @@ var Plotly = require('@lib/index'); -// pointcloud is not part of the dist plotly.js bundle initially -Plotly.register([ - require('@lib/pointcloud') -]); - // Test utilities var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); diff --git a/test/jasmine/tests/plotschema_test.js b/test/jasmine/tests/plotschema_test.js index ffe72a93ccc..e16aa5e8b82 100644 --- a/test/jasmine/tests/plotschema_test.js +++ b/test/jasmine/tests/plotschema_test.js @@ -1,11 +1,6 @@ var Plotly = require('@lib/index'); var Lib = require('@src/lib'); -Plotly.register([ - require('@src/transforms/filter'), - require('@src/transforms/groupby') -]); - describe('plot schema', function() { 'use strict'; @@ -181,6 +176,8 @@ describe('plot schema', function() { var valObjects = plotSchema.transforms.filter.attributes, attrNames = Object.keys(valObjects); - expect(attrNames).toEqual(['operation', 'value', 'filtersrc']); + ['operation', 'value', 'filtersrc'].forEach(function(k) { + expect(attrNames).toContain(k); + }); }); }); diff --git a/test/jasmine/tests/register_test.js b/test/jasmine/tests/register_test.js index 9d9df33bdb0..6fa534a2e42 100644 --- a/test/jasmine/tests/register_test.js +++ b/test/jasmine/tests/register_test.js @@ -206,9 +206,11 @@ describe('the register function', function() { expect(function() { Plotly.register([invalidTrace]); }).toThrowError(Error, 'Invalid module was attempted to be registered!'); + + expect(Registry.transformsRegistry['mah-transform']).toBeUndefined(); }); - it('should throw when if transform module is invalid', function() { + it('should throw when if transform module is invalid (1)', function() { var missingTransformName = { moduleType: 'transform' }; @@ -217,6 +219,10 @@ describe('the register function', function() { Plotly.register(missingTransformName); }).toThrowError(Error, 'Transform module *name* must be a string.'); + expect(Registry.transformsRegistry['mah-transform']).toBeUndefined(); + }); + + it('should throw when if transform module is invalid (2)', function() { var missingTransformFunc = { moduleType: 'transform', name: 'mah-transform' @@ -224,8 +230,12 @@ describe('the register function', function() { expect(function() { Plotly.register(missingTransformFunc); - }).toThrowError(Error, 'Transform module mah-transform is missing a *transform* function.'); + }).toThrowError(Error, 'Transform module mah-transform is missing a *transform* or *calcTransform* method.'); + + expect(Registry.transformsRegistry['mah-transform']).toBeUndefined(); + }); + it('should not throw when transform module is valid (1)', function() { var transformModule = { moduleType: 'transform', name: 'mah-transform', @@ -238,4 +248,33 @@ describe('the register function', function() { expect(Registry.transformsRegistry['mah-transform']).toBeDefined(); }); + + it('should not throw when transform module is valid (2)', function() { + var transformModule = { + moduleType: 'transform', + name: 'mah-transform', + calcTransform: function() {} + }; + + expect(function() { + Plotly.register(transformModule); + }).not.toThrow(); + + expect(Registry.transformsRegistry['mah-transform']).toBeDefined(); + }); + + it('should not throw when transform module is valid (3)', function() { + var transformModule = { + moduleType: 'transform', + name: 'mah-transform', + transform: function() {}, + calcTransform: function() {} + }; + + expect(function() { + Plotly.register(transformModule); + }).not.toThrow(); + + expect(Registry.transformsRegistry['mah-transform']).toBeDefined(); + }); }); diff --git a/test/jasmine/tests/toimage_test.js b/test/jasmine/tests/toimage_test.js index 7bfab046784..26fc91f872b 100644 --- a/test/jasmine/tests/toimage_test.js +++ b/test/jasmine/tests/toimage_test.js @@ -22,6 +22,7 @@ describe('Plotly.toImage', function() { // make sure ALL graph divs are deleted, // even the ones generated by Plotly.toImage d3.selectAll('.js-plotly-plot').remove(); + d3.selectAll('#graph').remove(); }); it('should be attached to Plotly', function() { diff --git a/test/jasmine/tests/transform_filter_test.js b/test/jasmine/tests/transform_filter_test.js index 4e7bed11757..fdba96d7609 100644 --- a/test/jasmine/tests/transform_filter_test.js +++ b/test/jasmine/tests/transform_filter_test.js @@ -7,653 +7,635 @@ var destroyGraphDiv = require('../assets/destroy_graph_div'); var assertDims = require('../assets/assert_dims'); var assertStyle = require('../assets/assert_style'); -Plotly.register([ - require('@src/transforms/filter') -]); -describe('one-to-one transforms:', function() { - 'use strict'; - - var mockData0 = [{ - x: [-2, -1, -2, 0, 1, 2, 3], - y: [1, 2, 3, 1, 2, 3, 1], - transforms: [{ - type: 'filter', - operation: '>' - }] - }]; - - var mockData1 = [Lib.extendDeep({}, mockData0[0]), { - x: [20, 11, 12, 0, 1, 2, 3], - y: [1, 2, 3, 2, 5, 2, 0], - transforms: [{ - type: 'filter', - operation: '<', - value: 10 - }] - }]; +describe('filter transforms defaults:', function() { var traceIn, traceOut; - afterEach(destroyGraphDiv); - - it('supplyTraceDefaults should supply the transform defaults', function() { + it('supplyTraceDefaults should coerce all attributes', function() { traceIn = { - y: [2, 1, 2], - transforms: [{ type: 'filter' }] + x: [1, 2, 3], + transforms: [{ + type: 'filter', + value: 0 + }] }; traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); expect(traceOut.transforms).toEqual([{ type: 'filter', + enabled: true, operation: '=', value: 0, filtersrc: 'x' }]); }); - it('supplyTraceDefaults should not bail if transform module is not found', function() { + it('supplyTraceDefaults should not coerce attributes if enabled: false', function() { traceIn = { - y: [2, 1, 2], - transforms: [{ type: 'invalid' }] + x: [1, 2, 3], + transforms: [{ + enabled: false, + type: 'filter', + value: 0 + }] }; traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); - expect(traceOut.y).toBe(traceIn.y); + expect(traceOut.transforms).toEqual([{ + type: 'filter', + enabled: false, + }]); }); - it('supplyTraceDefaults should honored global transforms', function() { + it('supplyTraceDefaults should coerce *filtersrc* as a strict / noBlank string', function() { traceIn = { - y: [2, 1, 2], + x: [1, 2, 3], transforms: [{ type: 'filter', - operation: '>', - value: '0', - filtersrc: 'x' - }] - }; - - var layout = { - _globalTransforms: [{ - type: 'filter' + }, { + type: 'filter', + filtersrc: 0 + }, { + type: 'filter', + filtersrc: '' + }, { + type: 'filter', + filtersrc: 'marker.color' }] }; - traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); - - expect(traceOut.transforms[0]).toEqual({ - type: 'filter', - operation: '=', - value: 0, - filtersrc: 'x' - }, '- global first'); + traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); - expect(traceOut.transforms[1]).toEqual({ - type: 'filter', - operation: '>', - value: 0, - filtersrc: 'x' - }, '- trace second'); + 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'); }); +}); - it('should pass correctly arguments to transform methods', function() { - var transformIn = { type: 'fake' }; - var transformOut = {}; +describe('filter transforms calc:', function() { + 'use strict'; - var dataIn = [{ - transforms: [transformIn] - }]; + function calcDatatoTrace(calcTrace) { + return calcTrace[0].trace; + } - var layout = {}; + function _transform(data, layout) { + var gd = { + data: data, + layout: layout || {} + }; - function assertSupplyDefaultsArgs(_transformIn, traceOut, _layout) { - expect(_transformIn).toBe(transformIn); - expect(_layout).toBe(layout); + Plots.supplyDefaults(gd); + Plots.doCalcdata(gd); - return transformOut; - } + return gd.calcdata.map(calcDatatoTrace); + } - function assertTransformArgs(dataOut, opts) { - expect(dataOut[0]._input).toBe(dataIn[0]); - expect(opts.transform).toBe(transformOut); - expect(opts.fullTrace._input).toBe(dataIn[0]); - expect(opts.layout).toBe(layout); + var base = { + x: [-2, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + ids: ['n0', 'n1', 'n2', 'z', 'p1', 'p2', 'p3'], + marker: { + color: [0.1, 0.2, 0.3, 0.1, 0.2, 0.3, 0.4], + size: 20 + }, + transforms: [{ type: 'filter' }] + }; + + it('filters should skip if *filtersrc* isn\'t present in trace', function() { + var out = _transform([Lib.extendDeep({}, base, { + transforms: [{ + type: 'filter', + operation: '>', + value: 0, + filtersrc: 'z' + }] + })]); - return dataOut; - } + expect(out[0].x).toEqual(base.x); + expect(out[0].y).toEqual(base.y); + }); - var fakeTransformModule = { - moduleType: 'transform', - name: 'fake', - attributes: {}, - supplyDefaults: assertSupplyDefaultsArgs, - transform: assertTransformArgs - }; + it('filters should handle 3D *z* data', function() { + var out = _transform([Lib.extendDeep({}, base, { + type: 'scatter3d', + z: ['2015-07-20', '2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], + transforms: [{ + type: 'filter', + operation: '>', + value: '2016-10-01', + filtersrc: 'z' + }] + })]); - Plotly.register(fakeTransformModule); - Plots.supplyDataDefaults(dataIn, [], layout); - delete Plots.transformsRegistry.fake; + expect(out[0].x).toEqual([0, 1]); + expect(out[0].y).toEqual([1, 2]); + expect(out[0].z).toEqual(['2016-10-21', '2016-12-02']); }); - it('supplyDataDefaults should apply the transform while', function() { - var dataIn = [{ - x: [-2, -2, 1, 2, 3], - y: [1, 2, 2, 3, 1] - }, { - x: [-2, -1, -2, 0, 1, 2, 3], - y: [1, 2, 3, 1, 2, 3, 1], + it('filters should handle geographical *lon* data', function() { + var trace0 = { + type: 'scattergeo', + lon: [-90, -40, 100, 120, 130], + lat: [-50, -40, 10, 20, 30], transforms: [{ type: 'filter', operation: '>', - value: '0', - filtersrc: 'x' + value: 0, + filtersrc: 'lon' }] - }]; - - var dataOut = []; - Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + }; - var msg; + var trace1 = { + type: 'scattermapbox', + lon: [-90, -40, 100, 120, 130], + lat: [-50, -40, 10, 20, 30], + transforms: [{ + type: 'filter', + operation: '<', + value: 0, + filtersrc: 'lat' + }] + }; - msg = 'does not mutate user data'; - expect(dataIn[1].x).toEqual([-2, -1, -2, 0, 1, 2, 3], msg); - expect(dataIn[1].y).toEqual([1, 2, 3, 1, 2, 3, 1], msg); - expect(dataIn[1].transforms).toEqual([{ - type: 'filter', - operation: '>', - value: '0', - filtersrc: 'x' - }], msg); + var out = _transform([trace0, trace1]); - msg = 'applies transform'; - expect(dataOut[1].x).toEqual([1, 2, 3], msg); - expect(dataOut[1].y).toEqual([2, 3, 1], msg); + expect(out[0].lon).toEqual([100, 120, 130]); + expect(out[0].lat).toEqual([10, 20, 30]); - msg = 'supplying the transform defaults'; - expect(dataOut[1].transforms[0]).toEqual({ - type: 'filter', - operation: '>', - value: 0, - filtersrc: 'x' - }, msg); + expect(out[1].lon).toEqual([-90, -40]); + expect(out[1].lat).toEqual([-50, -40]); + }); - msg = 'keeping refs to user data'; - expect(dataOut[1]._input.x).toEqual([-2, -1, -2, 0, 1, 2, 3], msg); - expect(dataOut[1]._input.y).toEqual([1, 2, 3, 1, 2, 3, 1], msg); - expect(dataOut[1]._input.transforms).toEqual([{ - type: 'filter', - operation: '>', - value: '0', - filtersrc: 'x' - }], msg); + it('filters should handle nested attributes', function() { + var out = _transform([Lib.extendDeep({}, base, { + transforms: [{ + type: 'filter', + operation: '>', + value: 0.2, + filtersrc: 'marker.color' + }] + })]); - msg = 'keeping refs to full transforms array'; - expect(dataOut[1]._fullInput.transforms).toEqual([{ - type: 'filter', - operation: '>', - value: 0, - filtersrc: 'x' - }], msg); + expect(out[0].x).toEqual([-2, 2, 3]); + expect(out[0].y).toEqual([3, 3, 1]); + expect(out[0].marker.color).toEqual([0.3, 0.3, 0.4]); + }); - msg = 'setting index w.r.t user data'; - expect(dataOut[0].index).toEqual(0, msg); - expect(dataOut[1].index).toEqual(1, msg); + it('filters should skip if *enabled* is false', function() { + var out = _transform([Lib.extendDeep({}, base, { + transforms: [{ + type: 'filter', + enabled: false, + operation: '>', + value: 0, + filtersrc: 'x' + }] + })]); - msg = 'setting _expandedIndex w.r.t full data'; - expect(dataOut[0]._expandedIndex).toEqual(0, msg); - expect(dataOut[1]._expandedIndex).toEqual(1, msg); + expect(out[0].x).toEqual(base.x); + expect(out[0].y).toEqual(base.y); }); - it('Plotly.plot should plot the transform trace', function(done) { - var data = Lib.extendDeep([], mockData0); + it('filters should chain as AND (case 1)', function() { + var out = _transform([Lib.extendDeep({}, base, { + transforms: [{ + type: 'filter', + operation: '>', + value: 0, + filtersrc: 'x' + }, { + type: 'filter', + operation: '<', + value: 3, + filtersrc: 'x' + }] + })]); - Plotly.plot(createGraphDiv(), data).then(function(gd) { - assertDims([3]); + expect(out[0].x).toEqual([1, 2]); + expect(out[0].y).toEqual([2, 3]); + }); - var uid = data[0].uid; - expect(gd._fullData[0].uid).toEqual(uid + '0'); + it('filters should chain as AND (case 2)', function() { + var out = _transform([Lib.extendDeep({}, base, { + transforms: [{ + type: 'filter', + operation: '>', + value: 0, + filtersrc: 'x' + }, { + type: 'filter', + enabled: false, + operation: '>', + value: 2, + filtersrc: 'y' + }, { + type: 'filter', + operation: '<', + value: 2, + filtersrc: 'y' + }] + })]); - done(); - }); + expect(out[0].x).toEqual([3]); + expect(out[0].y).toEqual([1]); }); - it('Plotly.restyle should work', function(done) { - var data = Lib.extendDeep([], mockData0); - data[0].marker = { color: 'red' }; + describe('filters should handle numeric values', function() { + var _base = Lib.extendDeep({}, base); - var gd = createGraphDiv(); - var dims = [3]; - - var uid; - function assertUid(gd) { - expect(gd._fullData[0].uid) - .toEqual(uid + '0', 'should preserve uid on restyle'); + 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'); } - Plotly.plot(gd, data).then(function() { - uid = gd.data[0].uid; - - expect(gd._fullData[0].marker.color).toEqual('red'); - assertUid(gd); - assertStyle(dims, ['rgb(255, 0, 0)'], [1]); - - return Plotly.restyle(gd, 'marker.color', 'blue'); - }).then(function() { - expect(gd._fullData[0].marker.color).toEqual('blue'); - assertUid(gd); - assertStyle(dims, ['rgb(0, 0, 255)'], [1]); - - return Plotly.restyle(gd, 'marker.color', 'red'); - }).then(function() { - expect(gd._fullData[0].marker.color).toEqual('red'); - assertUid(gd); - assertStyle(dims, ['rgb(255, 0, 0)'], [1]); - - return Plotly.restyle(gd, 'transforms[0].value', 2.5); - }).then(function() { - assertUid(gd); - assertStyle([1], ['rgb(255, 0, 0)'], [1]); - - done(); + it('with operation *[]*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '[]', + value: [-1, 1], + filtersrc: 'x' + }] + })]); + + _assert(out, + [-1, 0, 1], + [2, 1, 2], + [0.2, 0.1, 0.2] + ); }); - }); - - 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]); + it('with operation *[)*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '[)', + value: [-1, 1], + filtersrc: 'x' + }] + })]); - done(); + _assert(out, [-1, 0], [2, 1], [0.2, 0.1]); }); - }); - - 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([]); + it('with operation *(]*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '(]', + value: [-1, 1], + filtersrc: 'x' + }] + })]); - done(); + _assert(out, [0, 1], [1, 2], [0.1, 0.2]); }); - }); - - it('toggling trace visibility should work', function(done) { - var data = Lib.extendDeep([], mockData1); + it('with operation *()*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '()', + value: [-1, 1], + filtersrc: 'x' + }] + })]); - var gd = createGraphDiv(); + _assert(out, [0], [1], [0.1]); + }); - Plotly.plot(gd, data).then(function() { - assertDims([3, 4]); + it('with operation *)(*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: ')(', + value: [-1, 1], + filtersrc: 'x' + }] + })]); + + _assert(out, + [-2, -2, 2, 3], + [1, 3, 3, 1], + [0.1, 0.3, 0.3, 0.4] + ); + }); - return Plotly.restyle(gd, 'visible', 'legendonly', [1]); - }).then(function() { - assertDims([3]); + it('with operation *)[*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: ')[', + value: [-1, 1], + filtersrc: 'x' + }] + })]); + + _assert(out, + [-2, -2, 1, 2, 3], + [1, 3, 2, 3, 1], + [0.1, 0.3, 0.2, 0.3, 0.4] + ); + }); - return Plotly.restyle(gd, 'visible', false, [0]); - }).then(function() { - assertDims([]); + it('with operation *](*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '](', + value: [-1, 1], + filtersrc: 'x' + }] + })]); + + _assert(out, + [-2, -1, -2, 2, 3], + [1, 2, 3, 3, 1], + [0.1, 0.2, 0.3, 0.3, 0.4] + ); + }); - return Plotly.restyle(gd, 'visible', [true, true], [0, 1]); - }).then(function() { - assertDims([3, 4]); + it('with operation *][*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '][', + value: [-1, 1], + filtersrc: 'x' + }] + })]); + + _assert(out, + [-2, -1, -2, 1, 2, 3], + [1, 2, 3, 2, 3, 1], + [0.1, 0.2, 0.3, 0.2, 0.3, 0.4] + ); + }); - done(); + it('with operation *{}*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '{}', + value: [-2, 0], + filtersrc: 'x' + }] + })]); + + _assert(out, + [-2, -2, 0], + [1, 3, 1], + [0.1, 0.3, 0.1] + ); }); - }); + it('with operation *}{*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '}{', + value: [-2, 0], + filtersrc: 'x' + }] + })]); + + _assert(out, + [-1, 1, 2, 3], + [2, 2, 3, 1], + [0.2, 0.2, 0.3, 0.4] + ); + }); - it('supplyTraceDefaults should supply the transform defaults', function() { - traceIn = { - y: [2, 1, 2], - transforms: [{ type: 'filter' }] - }; + it('should honored set axis type', function() { + var out = _transform([Lib.extendDeep({}, _base, { + x: [1, 2, 3, 0, -1, -2, -3], + transforms: [{ + operation: '>', + value: -1, + filtersrc: 'x' + }] + })], { + xaxis: { type: 'category' } + }); - traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); + _assert(out, [-2, -3], [3, 1], [0.3, 0.4]); + }); - 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' - }] + describe('filters should handle categories', function() { + var _base = { + x: ['a', 'b', 'c', 'd'], + y: [1, 2, 3, 4], + marker: { + color: 'red', + size: ['0', '1', '2', '0'] + }, + transforms: [{ type: 'filter' }] }; - traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); - - expect(traceOut.transforms).toEqual([{ - type: 'filter', - operation: '=', - value: 0, - filtersrc: 'x' - }]); + function _assert(out, x, y, markerSize) { + expect(out[0].x).toEqual(x, '- x coords'); + expect(out[0].y).toEqual(y, '- y coords'); + expect(out[0].marker.size).toEqual(markerSize, '- marker.size arrayOk'); + expect(out[0].marker.color).toEqual('red', '- marker.color style'); + } - // should also convert if array - traceIn = { - x: '1', - transforms: [{ - type: 'filter', - value: ['0'] - }] - }; + it('with operation *()*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '()', + value: ['a', 'c'], + filtersrc: 'x' + }] + })]); - traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); + _assert(out, ['b'], [2], ['1']); + }); - expect(traceOut.transforms).toEqual([{ - type: 'filter', - operation: '=', - value: [0], - filtersrc: 'x' - }]); - }); + it('with operation *)(*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: ')(', + value: ['a', 'c'], + filtersrc: 'x' + }] + })]); - it('supplyTraceDefaults should accept numeric as character', function() { - traceIn = { - x: '1', - transforms: [{ - type: 'filter', - value: '0' - }] - }; + _assert(out, ['d'], [4], ['0']); + }); - traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); + it('with operation *{}*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '{}', + value: ['b', 'd'], + filtersrc: 'x' + }] + })]); - expect(traceOut.transforms).toEqual([{ - type: 'filter', - operation: '=', - value: 0, - filtersrc: 'x' - }]); + _assert(out, ['b', 'd'], [2, 4], ['1', '0']); + }); - // should also convert if array - traceIn = { - x: '1', - transforms: [{ - type: 'filter', - value: ['0'] - }] - }; + it('with operation *}{*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '}{', + value: ['b', 'd'], + filtersrc: 'x' + }] + })]); - traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); + _assert(out, ['a', 'c'], [1, 3], ['0', '2']); + }); - 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' - }]); + describe('filters should handle dates', function() { + var _base = { + x: ['2015-07-20', '2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], + y: [1, 2, 3, 1, 5], + marker: { + line: { + color: [0.1, 0.2, 0.3, 0.1, 0.2], + width: 2.5 + } + }, + transforms: [{ type: 'filter' }] + }; - // keep ref to full transforms array - expect(dataOut[0]._fullInput.transforms).toEqual([{ - type: 'filter', - operation: '>', - value: 0, - filtersrc: 'x' - }]); + function _assert(out, x, y, markerLineColor) { + expect(out[0].x).toEqual(x, '- x coords'); + expect(out[0].y).toEqual(y, '- y coords'); + expect(out[0].marker.line.color).toEqual(markerLineColor, '- marker.line.color arrayOk'); + expect(out[0].marker.line.width).toEqual(2.5, '- marker.line.width style'); + } - // set index w.r.t. fullData - expect(dataOut[0].index).toEqual(0); + it('with operation *=*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '=', + value: ['2015-07-20'], + filtersrc: 'x' + }] + })]); - // TODO do we really need this ??? - // set _index w.r.t. user data - expect(dataOut[0].index).toEqual(0); - }); + _assert(out, ['2015-07-20'], [1], [0.1]); + }); - 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', + it('with operation *<*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ operation: '<', - value: 3, + value: '2016-01-01', 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]); - }); + _assert(out, ['2015-07-20'], [1], [0.1]); + }); - 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' - }] - }]; + it('with operation *>*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '>=', + value: '2016-08-01', + filtersrc: '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] + ); + }); - var dataOut = []; - Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + it('with operation *[]*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '[]', + value: ['2016-08-01', '2016-10-01'], + filtersrc: 'x' + }] + })]); - // 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' - }]); + _assert(out, ['2016-08-01', '2016-09-01'], [2, 3], [0.2, 0.3]); + }); - // applies transform - expect(dataOut[0].x).toEqual(['a', 'b', 'c', 'd']); - expect(dataOut[0].y).toEqual([1, 2, 3, 4]); - }); + it('with operation *)(*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: ')(', + value: ['2016-08-01', '2016-10-01'], + filtersrc: 'x' + }] + })]); - 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' - }] - }]; + _assert(out, ['2015-07-20', '2016-10-21', '2016-12-02'], [1, 1, 5], [0.1, 0.1, 0.2]); + }); - var dataOut = []; - Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + it('with operation *{}*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '{}', + value: '2015-07-20', + filtersrc: 'x' + }] + })]); - // applies transform - expect(dataOut[0].x).toEqual([-2, -2, 0]); - expect(dataOut[0].y).toEqual([1, 3, 1]); - }); + _assert(out, ['2015-07-20'], [1], [0.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' - }] - }]; + it('with operation *}{*', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + operation: '}{', + value: ['2016-08-01', '2016-09-01', '2016-10-21', '2016-12-02'], + filtersrc: 'x' + }] + })]); - var dataOut = []; - Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + _assert(out, ['2015-07-20'], [1], [0.1]); + }); - // 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], + it('filters should handle ids', function() { + var out = _transform([Lib.extendDeep({}, base, { transforms: [{ - type: 'filter', - operation: 'in', - value: ['p', 'l', 'o'], - filtersrc: 'x' + operation: '{}', + value: ['p1', 'p2', 'n1'], + filtersrc: 'ids' }] - }]; - - - var dataOut = []; - Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + })]); - // applies transform - expect(dataOut[0].x).toEqual(['p', 'l', 'o']); - expect(dataOut[0].y).toEqual([5, 10, 20]); + expect(out[0].x).toEqual([-1, 1, 2]); + expect(out[0].y).toEqual([2, 2, 3]); + expect(out[0].ids).toEqual(['n1', 'p1', 'p2']); }); +}); - 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' - }] - }]; - +describe('filter transforms interactions', function() { + 'use strict'; - var dataOut = []; - Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + var mockData0 = [{ + x: [-2, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + transforms: [{ + type: 'filter', + operation: '>' + }] + }]; - // applies transform - expect(dataOut[0].x).toEqual(['y', 't', 'b', 'm']); - expect(dataOut[0].y).toEqual([1, 2, 3, 1]); - }); + var mockData1 = [Lib.extendDeep({}, mockData0[0]), { + x: [20, 11, 12, 0, 1, 2, 3], + y: [1, 2, 3, 2, 5, 2, 0], + transforms: [{ + type: 'filter', + operation: '<', + value: 10 + }] + }]; + afterEach(destroyGraphDiv); it('Plotly.plot should plot the transform trace', function(done) { var data = Lib.extendDeep([], mockData0); @@ -675,22 +657,34 @@ describe('one-to-one transforms:', function() { var gd = createGraphDiv(); var dims = [3]; + var uid; + function assertUid(gd) { + expect(gd._fullData[0].uid) + .toEqual(uid + '0', 'should preserve uid on restyle'); + } + Plotly.plot(gd, data).then(function() { + uid = gd.data[0].uid; + expect(gd._fullData[0].marker.color).toEqual('red'); + assertUid(gd); assertStyle(dims, ['rgb(255, 0, 0)'], [1]); return Plotly.restyle(gd, 'marker.color', 'blue'); }).then(function() { expect(gd._fullData[0].marker.color).toEqual('blue'); + assertUid(gd); assertStyle(dims, ['rgb(0, 0, 255)'], [1]); return Plotly.restyle(gd, 'marker.color', 'red'); }).then(function() { expect(gd._fullData[0].marker.color).toEqual('red'); + assertUid(gd); assertStyle(dims, ['rgb(255, 0, 0)'], [1]); return Plotly.restyle(gd, 'transforms[0].value', 2.5); }).then(function() { + assertUid(gd); assertStyle([1], ['rgb(255, 0, 0)'], [1]); done(); @@ -766,5 +760,4 @@ describe('one-to-one transforms:', function() { done(); }); }); - }); diff --git a/test/jasmine/tests/transform_groupby_test.js b/test/jasmine/tests/transform_groupby_test.js index d37ef142d25..bb2ea0f607e 100644 --- a/test/jasmine/tests/transform_groupby_test.js +++ b/test/jasmine/tests/transform_groupby_test.js @@ -6,9 +6,6 @@ var destroyGraphDiv = require('../assets/destroy_graph_div'); var assertDims = require('../assets/assert_dims'); var assertStyle = require('../assets/assert_style'); -Plotly.register([ - require('@src/transforms/groupby') -]); describe('groupby', function() { diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index ae1b7abd94b..70ac4f4ed7b 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -1,4 +1,5 @@ var Plotly = require('@lib/index'); +var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); @@ -7,10 +8,187 @@ var assertDims = require('../assets/assert_dims'); var assertStyle = require('../assets/assert_style'); -Plotly.register([ - require('@src/transforms/filter'), - require('@src/transforms/groupby') -]); +describe('general transforms:', function() { + 'use strict'; + + var traceIn, traceOut; + + 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', + enabled: true, + operation: '=', + value: 0, + filtersrc: 'x' + }]); + }); + + it('supplyTraceDefaults should not bail if transform module is not found', function() { + traceIn = { + y: [2, 1, 2], + transforms: [{ type: 'invalid' }] + }; + + traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); + + expect(traceOut.y).toBe(traceIn.y); + }); + + it('supplyTraceDefaults should honored global transforms', function() { + traceIn = { + y: [2, 1, 2], + transforms: [{ + type: 'filter', + operation: '>', + value: 0, + filtersrc: 'x' + }] + }; + + var layout = { + _globalTransforms: [{ + type: 'filter' + }] + }; + + traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); + + expect(traceOut.transforms[0]).toEqual({ + type: 'filter', + enabled: true, + operation: '=', + value: 0, + filtersrc: 'x' + }, '- global first'); + + expect(traceOut.transforms[1]).toEqual({ + type: 'filter', + enabled: true, + operation: '>', + value: 0, + filtersrc: 'x' + }, '- trace second'); + }); + + it('supplyDataDefaults should apply the transform while', function() { + var dataIn = [{ + x: [-2, -2, 1, 2, 3], + y: [1, 2, 2, 3, 1] + }, { + 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, {}, []); + + var msg; + + msg = 'does not mutate user data'; + expect(dataIn[1].x).toEqual([-2, -1, -2, 0, 1, 2, 3], msg); + expect(dataIn[1].y).toEqual([1, 2, 3, 1, 2, 3, 1], msg); + expect(dataIn[1].transforms).toEqual([{ + type: 'filter', + operation: '>', + value: 0, + filtersrc: 'x' + }], msg); + + msg = 'supplying the transform defaults'; + expect(dataOut[1].transforms[0]).toEqual({ + type: 'filter', + enabled: true, + operation: '>', + value: 0, + filtersrc: 'x' + }, msg); + + msg = 'keeping refs to user data'; + expect(dataOut[1]._input.x).toEqual([-2, -1, -2, 0, 1, 2, 3], msg); + expect(dataOut[1]._input.y).toEqual([1, 2, 3, 1, 2, 3, 1], msg); + expect(dataOut[1]._input.transforms).toEqual([{ + type: 'filter', + operation: '>', + value: 0, + filtersrc: 'x' + }], msg); + + msg = 'keeping refs to full transforms array'; + expect(dataOut[1]._fullInput.transforms).toEqual([{ + type: 'filter', + enabled: true, + operation: '>', + value: 0, + filtersrc: 'x' + }], msg); + + msg = 'setting index w.r.t user data'; + expect(dataOut[0].index).toEqual(0, msg); + expect(dataOut[1].index).toEqual(1, msg); + + msg = 'setting _expandedIndex w.r.t full data'; + expect(dataOut[0]._expandedIndex).toEqual(0, msg); + expect(dataOut[1]._expandedIndex).toEqual(1, msg); + }); + +}); + +describe('user-defined transforms:', function() { + 'use strict'; + + it('should pass correctly arguments to transform methods', function() { + var transformIn = { type: 'fake' }; + var transformOut = {}; + + var dataIn = [{ + transforms: [transformIn] + }]; + + var layout = {}; + + function assertSupplyDefaultsArgs(_transformIn, traceOut, _layout) { + expect(_transformIn).toBe(transformIn); + expect(_layout).toBe(layout); + + return transformOut; + } + + function assertTransformArgs(dataOut, opts) { + expect(dataOut[0]._input).toBe(dataIn[0]); + expect(opts.transform).toBe(transformOut); + expect(opts.fullTrace._input).toBe(dataIn[0]); + expect(opts.layout).toBe(layout); + + return dataOut; + } + + var fakeTransformModule = { + moduleType: 'transform', + name: 'fake', + attributes: {}, + supplyDefaults: assertSupplyDefaultsArgs, + transform: assertTransformArgs + }; + + Plotly.register(fakeTransformModule); + Plots.supplyDataDefaults(dataIn, [], layout); + delete Plots.transformsRegistry.fake; + }); + +}); describe('multiple transforms:', function() { 'use strict'; @@ -392,5 +570,4 @@ describe('multiple traces with transforms:', function() { done(); }); }); - }); diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js index 4d6655c957a..10771bc154a 100644 --- a/test/jasmine/tests/validate_test.js +++ b/test/jasmine/tests/validate_test.js @@ -1,10 +1,6 @@ var Plotly = require('@lib/index'); var Lib = require('@src/lib'); -Plotly.register([ - require('@src/transforms/filter'), - require('@src/transforms/groupby') -]); describe('Plotly.validate', function() {