diff --git a/devtools/test_dashboard/devtools.js b/devtools/test_dashboard/devtools.js index ea5b7b71636..23a2c6a401d 100644 --- a/devtools/test_dashboard/devtools.js +++ b/devtools/test_dashboard/devtools.js @@ -5,6 +5,7 @@ var Fuse = require('fuse.js'); var mocks = require('../../build/test_dashboard_mocks.json'); var credentials = require('../../build/credentials.json'); +var Lib = require('@src/lib'); // put d3 in window scope var d3 = window.d3 = Plotly.d3; @@ -149,6 +150,7 @@ var Tabs = { // Bind things to the window window.Tabs = Tabs; +window.Lib = Lib; setInterval(function() { window.gd = Tabs.getGraph() || Tabs.fresh(); window.fullLayout = window.gd._fullLayout; diff --git a/devtools/test_dashboard/server.js b/devtools/test_dashboard/server.js index 9dfba98dad7..b1d8eba25f0 100644 --- a/devtools/test_dashboard/server.js +++ b/devtools/test_dashboard/server.js @@ -8,6 +8,7 @@ var watchify = require('watchify'); var constants = require('../../tasks/util/constants'); var compress = require('../../tasks/util/compress_attributes'); +var shortcutPaths = require('../../tasks/util/shortcut_paths'); var PORT = process.argv[2] || 3000; @@ -32,7 +33,9 @@ b.on('update', bundlePlotly); // Bundle devtools code var devtoolsPath = path.join(constants.pathToRoot, 'devtools/test_dashboard'); -var devtools = browserify(path.join(devtoolsPath, 'devtools.js'), {}); +var devtools = browserify(path.join(devtoolsPath, 'devtools.js'), { + transform: [shortcutPaths] +}); var firstBundle = true; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 3cf9366e167..9b2f72f0420 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -846,9 +846,8 @@ Plotly.newPlot = function(gd, data, layout, config) { function doCalcdata(gd) { var axList = Plotly.Axes.list(gd), fullData = gd._fullData, - fullLayout = gd._fullLayout; - - var i, trace, module, cd; + fullLayout = gd._fullLayout, + i; var calcdata = gd.calcdata = new Array(fullData.length); @@ -874,12 +873,12 @@ function doCalcdata(gd) { } for(i = 0; i < fullData.length; i++) { - trace = fullData[i]; - module = trace._module; - cd = []; + var trace = fullData[i], + _module = trace._module, + cd = []; - if(module && trace.visible === true) { - if(module.calc) cd = module.calc(gd, trace); + if(_module && trace.visible === true) { + if(_module.calc) cd = _module.calc(gd, trace); } // make sure there is a first point @@ -1551,7 +1550,7 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { if(isNumeric(traces)) traces = [traces]; else if(!Array.isArray(traces) || !traces.length) { - traces = gd._fullData.map(function(v, i) { return i; }); + traces = gd.data.map(function(v, i) { return i; }); } // recalcAttrs attributes need a full regeneration of calcdata @@ -1726,6 +1725,9 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { continue; } + // take no chances on transforms + if(ai.substr(0, 10) === 'transforms') docalc = true; + // set attribute in gd.data undoit[ai] = a0(); for(i = 0; i < traces.length; i++) { diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index 36d83861831..43fe2293dd6 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -29,6 +29,7 @@ var UNDERSCORE_ATTRS = [IS_SUBPLOT_OBJ, IS_LINKED_TO_ARRAY, DEPRECATED]; var plotSchema = { traces: {}, layout: {}, + transforms: {}, defs: {} }; @@ -45,7 +46,11 @@ PlotSchema.get = function() { .forEach(getTraceAttributes); getLayoutAttributes(); + + Object.keys(Plots.transformsRegistry).forEach(getTransformAttributes); + getDefs(); + return plotSchema; }; @@ -136,6 +141,18 @@ function getLayoutAttributes() { plotSchema.layout = { layoutAttributes: layoutAttributes }; } +function getTransformAttributes(name) { + var _module = Plots.transformsRegistry[name], + _schema = {}; + + _schema = coupleAttrs(_schema, _module.attributes || {}, 'attributes', '*'); + _schema = removeUnderscoreAttrs(_schema); + mergeValTypeAndRole(_schema); + handleLinkedToArray(_schema); + + plotSchema.transforms[name] = { attributes: _schema }; +} + function getDefs() { plotSchema.defs = { valObjects: Lib.valObjects, diff --git a/src/plot_api/validate.js b/src/plot_api/validate.js index 58dc5829934..fc9584fe353 100644 --- a/src/plot_api/validate.js +++ b/src/plot_api/validate.js @@ -102,6 +102,39 @@ module.exports = function valiate(data, layout) { } crawl(traceIn, traceOut, traceSchema, errorList, base); + + var transformsIn = traceIn.transforms, + transformsOut = traceOut.transforms; + + if(transformsIn) { + if(!isArray(transformsIn)) { + errorList.push(format('array', base, ['transforms'])); + } + + base.push('transforms'); + + for(var j = 0; j < transformsIn.length; j++) { + var path = ['transforms', j], + transformType = transformsIn[j].type; + + if(!isPlainObject(transformsIn[j])) { + errorList.push(format('object', base, path)); + continue; + } + + var transformSchema = schema.transforms[transformType] ? + schema.transforms[transformType].attributes : + {}; + + // add 'type' to transform schema to validate the transform type + transformSchema.type = { + valType: 'enumerated', + values: Object.keys(schema.transforms) + }; + + crawl(transformsIn[j], transformsOut[j], transformSchema, errorList, base, path); + } + } } var layoutOut = gd._fullLayout, @@ -121,6 +154,9 @@ function crawl(objIn, objOut, schema, list, base, path) { for(var i = 0; i < keys.length; i++) { var k = keys[i]; + // transforms are handled separately + if(k === 'transforms') continue; + var p = path.slice(); p.push(k); @@ -184,7 +220,7 @@ var code2msgFunc = { var prefix; if(base === 'layout' && astr === '') prefix = 'The layout argument'; - else if(base[0] === 'data') { + else if(base[0] === 'data' && astr === '') { prefix = 'Trace ' + base[1] + ' in the data argument'; } else prefix = inBase(base) + 'key ' + astr; diff --git a/src/plotly.js b/src/plotly.js index 5ceb2019839..32f552a45a4 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -22,7 +22,7 @@ require('es6-promise').polyfill(); // lib functions -exports.Lib = require('./lib'); +var Lib = exports.Lib = require('./lib'); exports.util = require('./lib/svg_text_utils'); exports.Queue = require('./lib/queue'); @@ -55,21 +55,51 @@ exports.ModeBar = require('./components/modebar'); exports.register = function register(_modules) { if(!_modules) { throw new Error('No argument passed to Plotly.register.'); - } else if(_modules && !Array.isArray(_modules)) { + } + else if(_modules && !Array.isArray(_modules)) { _modules = [_modules]; } for(var i = 0; i < _modules.length; i++) { var newModule = _modules[i]; - if(newModule && newModule.moduleType !== 'trace') { + if(!newModule) { throw new Error('Invalid module was attempted to be registered!'); - } else { - Plots.register(newModule, newModule.name, newModule.categories, newModule.meta); + } + + switch(newModule.moduleType) { + case 'trace': + Plots.register(newModule, newModule.name, newModule.categories, newModule.meta); + + if(!Plots.subplotsRegistry[newModule.basePlotModule.name]) { + Plots.registerSubplot(newModule.basePlotModule); + } + + break; + + case 'transform': + if(typeof newModule.name !== 'string') { + throw new Error('Transform module *name* must be a string.'); + } + + var prefix = 'Transform module ' + newModule.name; + + if(typeof newModule.transform !== 'function') { + throw new Error(prefix + ' is missing a *transform* function.'); + } + 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.'); + } + + Plots.transformsRegistry[newModule.name] = newModule; + + break; - if(!Plots.subplotsRegistry[newModule.basePlotModule.name]) { - Plots.registerSubplot(newModule.basePlotModule); - } + default: + throw new Error('Invalid module was attempted to be registered!'); } } }; diff --git a/src/plots/plots.js b/src/plots/plots.js index 4b814bfd85c..2e0a4d28e85 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -21,7 +21,8 @@ var plots = module.exports = {}; var modules = plots.modules = {}, allTypes = plots.allTypes = [], allCategories = plots.allCategories = {}, - subplotsRegistry = plots.subplotsRegistry = {}; + subplotsRegistry = plots.subplotsRegistry = {}, + transformsRegistry = plots.transformsRegistry = {}; plots.attributes = require('./attributes'); plots.attributes.type.values = allTypes; @@ -461,10 +462,7 @@ plots.supplyDefaults = function(gd) { newFullData = gd._fullData = [], newData = gd.data || []; - var modules = newFullLayout._modules = [], - basePlotModules = newFullLayout._basePlotModules = []; - - var i, _module; + var i; // first fill in what we can of layout without looking at data // because fullData needs a few things from layout @@ -474,27 +472,15 @@ plots.supplyDefaults = function(gd) { newFullLayout._dataLength = newData.length; // then do the data - for(i = 0; i < newData.length; i++) { - var fullTrace = plots.supplyDataDefaults(newData[i], i, newFullLayout); - newFullData.push(fullTrace); - - // detect polar - if('r' in newData[i]) continue; - - _module = fullTrace._module; - if(!_module) continue; - - // fill in module lists - Lib.pushUnique(modules, _module); - Lib.pushUnique(basePlotModules, fullTrace._module.basePlotModule); - } + plots.supplyDataDefaults(newData, newFullData, newFullLayout); // attach helper method to check whether a plot type is present on graph newFullLayout._has = plots._hasPlotType.bind(newFullLayout); // special cases that introduce interactions between traces - for(i = 0; i < modules.length; i++) { - _module = modules[i]; + var _modules = newFullLayout._modules; + for(i = 0; i < _modules.length; i++) { + var _module = _modules[i]; if(_module.cleanData) _module.cleanData(newFullData); } @@ -599,50 +585,105 @@ plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayou }; /** - * Relink private _keys and keys with a function value from one layout - * (usually cached) to the new fullLayout. - * relink means copying if object is pass-by-value and adding a reference - * if object is pass-by-ref. This prevents deepCopying massive structures like - * a webgl context. + * Relink private _keys and keys with a function value from one container + * to the new container. + * Relink means copying if object is pass-by-value and adding a reference + * if object is pass-by-ref. + * This prevents deepCopying massive structures like a webgl context. */ -function relinkPrivateKeys(toLayout, fromLayout) { - var keys = Object.keys(fromLayout); - var j; +function relinkPrivateKeys(toContainer, fromContainer) { + var isPlainObject = Lib.isPlainObject, + isArray = Array.isArray; + + var keys = Object.keys(fromContainer); + + for(var i = 0; i < keys.length; i++) { + var k = keys[i], + fromVal = fromContainer[k], + toVal = toContainer[k]; + + if(k.charAt(0) === '_' || typeof fromVal === 'function') { - for(var i = 0; i < keys.length; ++i) { - var k = keys[i]; - if(k.charAt(0) === '_' || typeof fromLayout[k] === 'function') { // if it already exists at this point, it's something // that we recreate each time around, so ignore it - if(k in toLayout) continue; + if(k in toContainer) continue; - toLayout[k] = fromLayout[k]; + toContainer[k] = fromVal; } - else if(Array.isArray(fromLayout[k]) && - Array.isArray(toLayout[k]) && - fromLayout[k].length && - Lib.isPlainObject(fromLayout[k][0])) { - if(fromLayout[k].length !== toLayout[k].length) { - // this should be handled elsewhere, it causes - // ambiguity if we try to deal with it here. - throw new Error('relinkPrivateKeys needs equal ' + - 'length arrays'); - } + else if(isArray(fromVal) && isArray(toVal)) { - for(j = 0; j < fromLayout[k].length; j++) { - relinkPrivateKeys(toLayout[k][j], fromLayout[k][j]); + // recurse into arrays + for(var j = 0; j < fromVal.length; j++) { + if(isPlainObject(fromVal[j]) && isPlainObject(toVal[j])) { + relinkPrivateKeys(toVal[j], fromVal[j]); + } } } - else if(Lib.isPlainObject(fromLayout[k]) && - Lib.isPlainObject(toLayout[k])) { + else if(isPlainObject(fromVal) && isPlainObject(toVal)) { + // recurse into objects, but only if they still exist - relinkPrivateKeys(toLayout[k], fromLayout[k]); - if(!Object.keys(toLayout[k]).length) delete toLayout[k]; + relinkPrivateKeys(toVal, fromVal); + + if(!Object.keys(toVal).length) delete toContainer[k]; } } } -plots.supplyDataDefaults = function(traceIn, traceIndex, layout) { +plots.supplyDataDefaults = function(dataIn, dataOut, layout) { + var modules = layout._modules = [], + basePlotModules = layout._basePlotModules = [], + cnt = 0; + + function pushModule(fullTrace) { + dataOut.push(fullTrace); + + var _module = fullTrace._module; + if(!_module) return; + + Lib.pushUnique(modules, _module); + Lib.pushUnique(basePlotModules, fullTrace._module.basePlotModule); + + cnt++; + } + + for(var i = 0; i < dataIn.length; i++) { + var trace = dataIn[i], + fullTrace = plots.supplyTraceDefaults(trace, cnt, layout); + + if(fullTrace.transforms && fullTrace.transforms.length) { + var expandedTraces = applyTransforms(fullTrace, dataOut, layout); + + for(var j = 0; j < expandedTraces.length; j++) { + var expandedTrace = expandedTraces[j], + fullExpandedTrace = plots.supplyTraceDefaults(expandedTrace, cnt, layout); + + // mutate uid here using parent uid and expanded index + // to promote consistency between update calls + expandedTrace.uid = fullExpandedTrace.uid = fullTrace.uid + j; + + // add info about parent data trace + fullExpandedTrace.index = i; + fullExpandedTrace._input = trace; + fullExpandedTrace._fullInput = fullTrace; + + // add info about the expanded data + fullExpandedTrace._expandedIndex = cnt; + fullExpandedTrace._expandedInput = expandedTrace; + + pushModule(fullExpandedTrace); + } + } + else { + fullTrace.index = i; + fullTrace._input = trace; + fullTrace._expandedIndex = cnt; + + pushModule(fullTrace); + } + } +}; + +plots.supplyTraceDefaults = function(traceIn, traceIndex, layout) { var traceOut = {}, defaultColor = Color.defaults[traceIndex % Color.defaults.length]; @@ -657,8 +698,6 @@ plots.supplyDataDefaults = function(traceIn, traceIndex, layout) { plots.subplotsRegistry[subplotType].attributes, subplotAttr); } - // module-independent attributes - traceOut.index = traceIndex; var visible = coerce('visible'); coerce('type'); @@ -703,18 +742,61 @@ plots.supplyDataDefaults = function(traceIn, traceIndex, layout) { coerce('showlegend'); coerce('legendgroup'); } - } - - // NOTE: I didn't include fit info at all... for now I think it can stay - // just in gd.data, as this info isn't involved in creating plots at all, - // only in pulling back up the fit popover - // reference back to the input object for convenience - traceOut._input = traceIn; + supplyTransformDefaults(traceIn, traceOut, layout); + } return traceOut; }; +function supplyTransformDefaults(traceIn, traceOut, layout) { + if(!Array.isArray(traceIn.transforms)) return; + + var containerIn = traceIn.transforms, + containerOut = traceOut.transforms = []; + + for(var i = 0; i < containerIn.length; i++) { + var transformIn = containerIn[i], + type = transformIn.type, + _module = transformsRegistry[type], + transformOut; + + if(!_module) Lib.warn('Unrecognized transform type ' + type + '.'); + + if(_module && _module.supplyDefaults) { + transformOut = _module.supplyDefaults(transformIn, traceOut, layout); + transformOut.type = type; + } + else { + transformOut = Lib.extendFlat({}, transformIn); + } + + containerOut.push(transformOut); + } +} + +function applyTransforms(fullTrace, fullData, layout) { + var container = fullTrace.transforms, + dataOut = [fullTrace]; + + for(var i = 0; i < container.length; i++) { + var transform = container[i], + type = transform.type, + _module = transformsRegistry[type]; + + if(_module) { + dataOut = _module.transform(dataOut, { + transform: transform, + fullTrace: fullTrace, + fullData: fullData, + layout: layout + }); + } + } + + return dataOut; +} + plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) { function coerce(attr, dflt) { return Lib.coerce(layoutIn, layoutOut, plots.layoutAttributes, attr, dflt); diff --git a/test/jasmine/assets/destroy_graph_div.js b/test/jasmine/assets/destroy_graph_div.js index 84f3d72eaa7..a1bd18b9741 100644 --- a/test/jasmine/assets/destroy_graph_div.js +++ b/test/jasmine/assets/destroy_graph_div.js @@ -2,5 +2,6 @@ module.exports = function destroyGraphDiv() { var gd = document.getElementById('graph'); - document.body.removeChild(gd); + + if(gd) document.body.removeChild(gd); }; diff --git a/test/jasmine/assets/transforms/filter.js b/test/jasmine/assets/transforms/filter.js new file mode 100644 index 00000000000..95215fc50cf --- /dev/null +++ b/test/jasmine/assets/transforms/filter.js @@ -0,0 +1,161 @@ +'use strict'; + +// var Lib = require('@src/lib'); +var Lib = require('../../../../src/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 = 'filter'; + +// ... as trace attributes +exports.attributes = { + operation: { + valType: 'enumerated', + values: ['=', '<', '>'], + dflt: '=' + }, + value: { + valType: 'number', + dflt: 0 + }, + filtersrc: { + valType: 'enumerated', + values: ['x', 'y'], + dflt: 'x' + } +}; + +/** + * 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) { + var transformOut = {}; + + function coerce(attr, dflt) { + return Lib.coerce(transformIn, transformOut, exports.attributes, attr, dflt); + } + + coerce('operation'); + coerce('value'); + coerce('filtersrc'); + + // or some more complex logic using fullData and layout + + 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; +}; + +function transformOne(trace, state) { + var newTrace = Lib.extendDeep({}, trace); + + var opts = state.transform; + var src = opts.filtersrc; + var filterFunc = getFilterFunc(opts); + var len = trace[src].length; + var arrayAttrs = findArrayAttributes(trace); + + arrayAttrs.forEach(function(attr) { + Lib.nestedProperty(newTrace, attr).set([]); + }); + + function fill(attr, i) { + var arr = Lib.nestedProperty(trace, attr).get(); + var newArr = Lib.nestedProperty(newTrace, attr).get(); + + newArr.push(arr[i]); + } + + for(var i = 0; i < len; i++) { + var v = trace[src][i]; + + if(!filterFunc(v)) continue; + + for(var j = 0; j < arrayAttrs.length; j++) { + fill(arrayAttrs[j], i); + } + } + + return newTrace; +} + +function getFilterFunc(opts) { + var value = opts.value; + + switch(opts.operation) { + case '=': + return function(v) { return v === value; }; + case '<': + return function(v) { return v < value; }; + case '>': + return function(v) { return v > value; }; + } +} + +function findArrayAttributes(obj, root) { + root = root || ''; + + var list = []; + + Object.keys(obj).forEach(function(k) { + var val = obj[k]; + + if(k.charAt(0) === '_') return; + + 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); + } + }); + + return list; +} diff --git a/test/jasmine/assets/transforms/groupby.js b/test/jasmine/assets/transforms/groupby.js new file mode 100644 index 00000000000..41748a75c66 --- /dev/null +++ b/test/jasmine/assets/transforms/groupby.js @@ -0,0 +1,125 @@ +'use strict'; + +// var Lib = require('@src/lib'); +var Lib = require('../../../../src/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: { + valType: 'boolean', + dflt: true + }, + groups: { + valType: 'data_array', + dflt: [] + }, + groupColors: { + valType: 'any', + dflt: {} + } +}; + +/** + * 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) { + var transformOut = {}; + + function coerce(attr, dflt) { + return Lib.coerce(transformIn, transformOut, exports.attributes, attr, dflt); + } + + var active = coerce('active'); + + if(!active) return transformOut; + + coerce('groups'); + coerce('groupColors'); + + // or some more complex logic using fullData and layout + + 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-many case + + var newData = []; + + data.forEach(function(trace) { + newData = newData.concat(transformOne(trace, state)); + }); + + return newData; +}; + +function transformOne(trace, state) { + var opts = state.transform; + var groups = opts.groups; + + var groupNames = groups.filter(function(g, i, self) { + return self.indexOf(g) === i; + }); + + var newData = new Array(groupNames.length); + var len = Math.min(trace.x.length, trace.y.length, groups.length); + + 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); + + newTrace.x = []; + newTrace.y = []; + + for(var j = 0; j < len; j++) { + if(groups[j] !== groupName) continue; + + newTrace.x.push(trace.x[j]); + newTrace.y.push(trace.y[j]); + } + + newTrace.name = groupName; + newTrace.marker.color = opts.groupColors[groupName]; + } + + return newData; +} diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index 0555d590ae4..c9a72894da9 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -51,13 +51,13 @@ describe('heatmap supplyDefaults', function() { type: 'heatmap', z: [[1, 2], []] }; - traceOut = Plots.supplyDataDefaults(traceIn, 0, layout); + traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); traceIn = { type: 'heatmap', z: [[], [1, 2], [1, 2, 3]] }; - traceOut = Plots.supplyDataDefaults(traceIn, 0, layout); + traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); expect(traceOut.visible).toBe(true); expect(traceOut.visible).toBe(true); }); diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index 3d46f8bd298..aa9f52d228f 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -7,6 +7,63 @@ var destroyGraphDiv = require('../assets/destroy_graph_div'); describe('Test Plots', function() { 'use strict'; + describe('Plots.supplyDefaults', function() { + + var gd; + + it('should relink private keys', function() { + var oldFullData = [{ + type: 'scatter3d', + z: [1, 2, 3] + }, { + type: 'contour', + _empties: [1, 2, 3] + }]; + + var oldFullLayout = { + _plots: { xy: {} }, + xaxis: { c2p: function() {} }, + yaxis: { _m: 20 }, + scene: { _scene: {} }, + annotations: [{ _min: 10, }, { _max: 20 }], + someFunc: function() {} + }; + + var newData = [{ + type: 'scatter3d', + z: [1, 2, 3, 4] + }, { + type: 'contour', + z: [[1, 2, 3], [2, 3, 4]] + }]; + + var newLayout = { + annotations: [{}, {}, {}] + }; + + gd = { + _fullData: oldFullData, + _fullLayout: oldFullLayout, + data: newData, + layout: newLayout + }; + + Plots.supplyDefaults(gd); + + expect(gd._fullData[1]._empties).toBe(oldFullData[1]._empties); + expect(gd._fullLayout.scene._scene).toBe(oldFullLayout.scene._scene); + expect(gd._fullLayout._plots).toBe(oldFullLayout._plots); + expect(gd._fullLayout.annotations[0]._min).toBe(oldFullLayout.annotations[0]._min); + expect(gd._fullLayout.annotations[1]._max).toBe(oldFullLayout.annotations[1]._max); + expect(gd._fullLayout.someFunc).toBe(oldFullLayout.someFunc); + + expect(gd._fullLayout.xaxis.c2p) + .not.toBe(oldFullLayout.xaxis.c2p, '(set during ax.setScale'); + expect(gd._fullLayout.yaxis._m) + .not.toBe(oldFullLayout.yaxis._m, '(set during ax.setScale'); + }); + }); + describe('Plots.supplyLayoutGlobalDefaults should', function() { var layoutIn, layoutOut, @@ -66,8 +123,8 @@ describe('Test Plots', function() { }); - describe('Plots.supplyDataDefaults', function() { - var supplyDataDefaults = Plots.supplyDataDefaults, + describe('Plots.supplyTraceDefaults', function() { + var supplyTraceDefaults = Plots.supplyTraceDefaults, layout = {}; var traceIn, traceOut; @@ -77,11 +134,11 @@ describe('Test Plots', function() { layout._dataLength = 1; traceIn = {}; - traceOut = supplyDataDefaults(traceIn, 0, layout); + traceOut = supplyTraceDefaults(traceIn, 0, layout); expect(traceOut.hoverinfo).toEqual('x+y+z+text'); traceIn = { hoverinfo: 'name' }; - traceOut = supplyDataDefaults(traceIn, 0, layout); + traceOut = supplyTraceDefaults(traceIn, 0, layout); expect(traceOut.hoverinfo).toEqual('name'); }); @@ -89,11 +146,11 @@ describe('Test Plots', function() { layout._dataLength = 2; traceIn = {}; - traceOut = supplyDataDefaults(traceIn, 0, layout); + traceOut = supplyTraceDefaults(traceIn, 0, layout); expect(traceOut.hoverinfo).toEqual('all'); traceIn = { hoverinfo: 'name' }; - traceOut = supplyDataDefaults(traceIn, 0, layout); + traceOut = supplyTraceDefaults(traceIn, 0, layout); expect(traceOut.hoverinfo).toEqual('name'); }); }); diff --git a/test/jasmine/tests/plotschema_test.js b/test/jasmine/tests/plotschema_test.js index bbbc9df65e5..5f2195bfe28 100644 --- a/test/jasmine/tests/plotschema_test.js +++ b/test/jasmine/tests/plotschema_test.js @@ -1,10 +1,13 @@ var Plotly = require('@lib/index'); var Lib = require('@src/lib'); -// until it is part of the main plotly.js bundle -Plotly.register( - require('@lib/scattermapbox') -); +Plotly.register([ + // until it is part of the main plotly.js bundle + require('@lib/scattermapbox'), + + // until they become official + require('../assets/transforms/filter') +]); describe('plot schema', function() { 'use strict'; @@ -180,4 +183,10 @@ describe('plot schema', function() { ); }); + it('should work with registered transforms', function() { + var valObjects = plotSchema.transforms.filter.attributes, + attrNames = Object.keys(valObjects); + + expect(attrNames).toEqual(['operation', 'value', 'filtersrc']); + }); }); diff --git a/test/jasmine/tests/register_test.js b/test/jasmine/tests/register_test.js index 0e5a436ad71..dd1356dc50a 100644 --- a/test/jasmine/tests/register_test.js +++ b/test/jasmine/tests/register_test.js @@ -9,6 +9,7 @@ describe('the register function', function() { this.modulesKeys = Object.keys(Plots.modules); this.allTypesKeys = Object.keys(Plots.allTypes); this.allCategoriesKeys = Object.keys(Plots.allCategories); + this.allTransformsKeys = Object.keys(Plots.transformsRegistry); }); afterEach(function() { @@ -21,6 +22,7 @@ describe('the register function', function() { revertObj(Plots.modules, this.modulesKeys); revertObj(Plots.allTypes, this.allTypesKeys); revertObj(Plots.allCategories, this.allCategoriesKeys); + revertObj(Plots.transformsRegistry, this.allTransformsKeys); }); it('should throw an error when no argument is given', function() { @@ -68,4 +70,35 @@ describe('the register function', function() { Plotly.register([invalidTrace]); }).toThrowError(Error, 'Invalid module was attempted to be registered!'); }); + + it('should throw when if transform module is invalid', function() { + var missingTransformName = { + moduleType: 'transform' + }; + + expect(function() { + Plotly.register(missingTransformName); + }).toThrowError(Error, 'Transform module *name* must be a string.'); + + var missingTransformFunc = { + moduleType: 'transform', + name: 'mah-transform' + }; + + expect(function() { + Plotly.register(missingTransformFunc); + }).toThrowError(Error, 'Transform module mah-transform is missing a *transform* function.'); + + var transformModule = { + moduleType: 'transform', + name: 'mah-transform', + transform: function() {} + }; + + expect(function() { + Plotly.register(transformModule); + }).not.toThrow(); + + expect(Plots.transformsRegistry['mah-transform']).toBeDefined(); + }); }); diff --git a/test/jasmine/tests/toimage_test.js b/test/jasmine/tests/toimage_test.js index e9c28694f62..7bfab046784 100644 --- a/test/jasmine/tests/toimage_test.js +++ b/test/jasmine/tests/toimage_test.js @@ -2,20 +2,27 @@ // once established and confirmed? var Plotly = require('@lib/index'); + +var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); var subplotMock = require('@mocks/multiple_subplots.json'); describe('Plotly.toImage', function() { 'use strict'; + var gd; beforeEach(function() { gd = createGraphDiv(); }); - afterEach(destroyGraphDiv); + afterEach(function() { + + // make sure ALL graph divs are deleted, + // even the ones generated by Plotly.toImage + d3.selectAll('.js-plotly-plot').remove(); + }); it('should be attached to Plotly', function() { expect(Plotly.toImage).toBeDefined(); diff --git a/test/jasmine/tests/transforms_test.js b/test/jasmine/tests/transforms_test.js new file mode 100644 index 00000000000..d95eb1319bd --- /dev/null +++ b/test/jasmine/tests/transforms_test.js @@ -0,0 +1,941 @@ +var Plotly = require('@lib/index'); +var Plots = require('@src/plots/plots'); +var Lib = require('@src/lib'); + +var d3 = require('d3'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); + +Plotly.register([ + require('../assets/transforms/filter'), + require('../assets/transforms/groupby') +]); + + +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 + }] + }]; + + afterEach(destroyGraphDiv); + + it('supplyTraceDefaults should supply the transform defaults', function() { + var traceIn = { + y: [2, 1, 2], + transforms: [{ type: 'filter' }] + }; + + var traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); + + expect(traceOut.transforms).toEqual([{ + type: 'filter', + operation: '=', + value: 0, + filtersrc: 'x' + }]); + }); + + it('supplyTraceDefaults should not bail if transform module is not found', function() { + var traceIn = { + y: [2, 1, 2], + transforms: [{ type: 'invalid' }] + }; + + var traceOut = Plots.supplyTraceDefaults(traceIn, 0, {}); + + expect(traceOut.y).toBe(traceIn.y); + }); + + 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 = 'applies transform'; + expect(dataOut[1].x).toEqual([1, 2, 3], msg); + expect(dataOut[1].y).toEqual([2, 3, 1], msg); + + msg = 'supplying the transform defaults'; + expect(dataOut[1].transforms[0]).toEqual({ + type: 'filter', + 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', + 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); + }); + + 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]; + + 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(); + }); + }); + + 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() { + 'use strict'; + + var mockData0 = [{ + mode: 'markers', + x: [1, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + transforms: [{ + type: 'groupby', + groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], + groupColors: { a: 'red', b: 'blue' } + }] + }]; + + var mockData1 = [Lib.extendDeep({}, mockData0[0]), { + mode: 'markers', + x: [20, 11, 12, 0, 1, 2, 3], + y: [1, 2, 3, 2, 5, 2, 0], + transforms: [{ + type: 'groupby', + groups: ['b', 'a', 'b', 'b', 'b', 'a', 'a'], + groupColors: { a: 'green', b: 'black' } + }] + }]; + + 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); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(1); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + + expect(gd._fullData.length).toEqual(2); + expect(gd._fullData[0].x).toEqual([1, -1, 0, 3]); + expect(gd._fullData[0].y).toEqual([1, 2, 1, 1]); + expect(gd._fullData[1].x).toEqual([-2, 1, 2]); + expect(gd._fullData[1].y).toEqual([3, 2, 3]); + + assertDims([4, 3]); + + done(); + }); + }); + + it('Plotly.restyle should work', function(done) { + var data = Lib.extendDeep([], mockData0); + data[0].marker = { size: 20 }; + + var gd = createGraphDiv(); + var dims = [4, 3]; + + Plotly.plot(gd, data).then(function() { + assertStyle(dims, + ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + [1, 1] + ); + + return Plotly.restyle(gd, 'marker.opacity', 0.4); + }).then(function() { + assertStyle(dims, + ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + [0.4, 0.4] + ); + + expect(gd._fullData[0].marker.opacity).toEqual(0.4); + expect(gd._fullData[1].marker.opacity).toEqual(0.4); + + return Plotly.restyle(gd, 'marker.opacity', 1); + }).then(function() { + assertStyle(dims, + ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + [1, 1] + ); + + expect(gd._fullData[0].marker.opacity).toEqual(1); + expect(gd._fullData[1].marker.opacity).toEqual(1); + + return Plotly.restyle(gd, { + 'transforms[0].groupColors': { a: 'green', b: 'red' }, + 'marker.opacity': 0.4 + }); + }).then(function() { + assertStyle(dims, + ['rgb(0, 128, 0)', 'rgb(255, 0, 0)'], + [0.4, 0.4] + ); + + 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(4); + expect(gd._fullData[1].x.length).toEqual(3); + + assertDims([4, 3]); + + return Plotly.extendTraces(gd, { + x: [ [-3, 4, 5] ], + y: [ [1, -2, 3] ], + 'transforms[0].groups': [ ['b', 'a', 'b'] ] + }, [0]); + }).then(function() { + expect(gd.data[0].x.length).toEqual(10); + expect(gd._fullData[0].x.length).toEqual(5); + expect(gd._fullData[1].x.length).toEqual(5); + + assertDims([5, 5]); + + done(); + }); + }); + + it('Plotly.deleteTraces should work', function(done) { + var data = Lib.extendDeep([], mockData1); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + assertDims([4, 3, 4, 3]); + + return Plotly.deleteTraces(gd, [1]); + }).then(function() { + assertDims([4, 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([4, 3, 4, 3]); + + return Plotly.restyle(gd, 'visible', 'legendonly', [1]); + }).then(function() { + assertDims([4, 3]); + + return Plotly.restyle(gd, 'visible', false, [0]); + }).then(function() { + assertDims([]); + + return Plotly.restyle(gd, 'visible', [true, true], [0, 1]); + }).then(function() { + assertDims([4, 3, 4, 3]); + + done(); + }); + }); + +}); + +describe('multiple transforms:', function() { + 'use strict'; + + var mockData0 = [{ + mode: 'markers', + x: [1, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + transforms: [{ + type: 'groupby', + groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], + groupColors: { a: 'red', b: 'blue' } + }, { + type: 'filter', + operation: '>' + }] + }]; + + var mockData1 = [Lib.extendDeep({}, mockData0[0]), { + mode: 'markers', + x: [20, 11, 12, 0, 1, 2, 3], + y: [1, 2, 3, 2, 5, 2, 0], + transforms: [{ + type: 'groupby', + groups: ['b', 'a', 'b', 'b', 'b', 'a', 'a'], + groupColors: { a: 'green', b: 'black' } + }, { + type: 'filter', + operation: '<', + value: 10 + }] + }]; + + 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); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(1); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + + expect(gd._fullData.length).toEqual(2); + expect(gd._fullData[0].x).toEqual([1, 3]); + expect(gd._fullData[0].y).toEqual([1, 1]); + expect(gd._fullData[1].x).toEqual([1, 2]); + expect(gd._fullData[1].y).toEqual([2, 3]); + + assertDims([2, 2]); + + done(); + }); + }); + + it('Plotly.plot should plot the transform traces (reverse case)', function(done) { + var data = Lib.extendDeep([], mockData0); + + data[0].transforms.reverse(); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(1); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + + expect(gd._fullData.length).toEqual(2); + expect(gd._fullData[0].x).toEqual([1, 1, 3]); + expect(gd._fullData[0].y).toEqual([1, 2, 1]); + expect(gd._fullData[1].x).toEqual([2]); + expect(gd._fullData[1].y).toEqual([3]); + + assertDims([3, 1]); + + done(); + }); + }); + + it('Plotly.restyle should work', function(done) { + var data = Lib.extendDeep([], mockData0); + data[0].marker = { size: 20 }; + + var gd = createGraphDiv(); + var dims = [2, 2]; + + Plotly.plot(gd, data).then(function() { + assertStyle(dims, + ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + [1, 1] + ); + + return Plotly.restyle(gd, 'marker.opacity', 0.4); + }).then(function() { + assertStyle(dims, + ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + [0.4, 0.4] + ); + + expect(gd._fullData[0].marker.opacity).toEqual(0.4); + expect(gd._fullData[1].marker.opacity).toEqual(0.4); + + return Plotly.restyle(gd, 'marker.opacity', 1); + }).then(function() { + assertStyle(dims, + ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + [1, 1] + ); + + expect(gd._fullData[0].marker.opacity).toEqual(1); + expect(gd._fullData[1].marker.opacity).toEqual(1); + + return Plotly.restyle(gd, { + 'transforms[0].groupColors': { a: 'green', b: 'red' }, + 'marker.opacity': 0.4 + }); + }).then(function() { + assertStyle(dims, + ['rgb(0, 128, 0)', 'rgb(255, 0, 0)'], + [0.4, 0.4] + ); + + 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(2); + expect(gd._fullData[1].x.length).toEqual(2); + + assertDims([2, 2]); + + return Plotly.extendTraces(gd, { + x: [ [-3, 4, 5] ], + y: [ [1, -2, 3] ], + 'transforms[0].groups': [ ['b', 'a', 'b'] ] + }, [0]); + }).then(function() { + expect(gd.data[0].x.length).toEqual(10); + expect(gd._fullData[0].x.length).toEqual(3); + expect(gd._fullData[1].x.length).toEqual(3); + + assertDims([3, 3]); + + done(); + }); + }); + + it('Plotly.deleteTraces should work', function(done) { + var data = Lib.extendDeep([], mockData1); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + assertDims([2, 2, 2, 2]); + + return Plotly.deleteTraces(gd, [1]); + }).then(function() { + assertDims([2, 2]); + + 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([2, 2, 2, 2]); + + return Plotly.restyle(gd, 'visible', 'legendonly', [1]); + }).then(function() { + assertDims([2, 2]); + + return Plotly.restyle(gd, 'visible', false, [0]); + }).then(function() { + assertDims([]); + + return Plotly.restyle(gd, 'visible', [true, true]); + }).then(function() { + assertDims([2, 2, 2, 2]); + + done(); + }); + }); + +}); + +describe('multiple traces with transforms:', function() { + 'use strict'; + + var mockData0 = [{ + mode: 'markers', + x: [1, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + marker: { color: 'green' }, + name: 'filtered', + transforms: [{ + type: 'filter', + operation: '>', + value: 1 + }] + }, { + mode: 'markers', + x: [20, 11, 12, 0, 1, 2, 3], + y: [1, 2, 3, 2, 5, 2, 0], + transforms: [{ + type: 'groupby', + groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], + groupColors: { a: 'red', b: 'blue' } + }, { + type: 'filter', + operation: '>' + }] + }]; + + 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); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + expect(gd.data.length).toEqual(2); + expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]); + expect(gd.data[0].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + expect(gd.data[1].x).toEqual([20, 11, 12, 0, 1, 2, 3]); + expect(gd.data[1].y).toEqual([1, 2, 3, 2, 5, 2, 0]); + + expect(gd._fullData.length).toEqual(3); + expect(gd._fullData[0].x).toEqual([2, 3]); + expect(gd._fullData[0].y).toEqual([3, 1]); + expect(gd._fullData[1].x).toEqual([20, 11, 3]); + expect(gd._fullData[1].y).toEqual([1, 2, 0]); + expect(gd._fullData[2].x).toEqual([12, 1, 2]); + expect(gd._fullData[2].y).toEqual([3, 5, 2]); + + assertDims([2, 3, 3]); + + done(); + }); + }); + + it('Plotly.restyle should work', function(done) { + var data = Lib.extendDeep([], mockData0); + data[0].marker.size = 20; + + var gd = createGraphDiv(); + var dims = [2, 3, 3]; + + Plotly.plot(gd, data).then(function() { + assertStyle(dims, + ['rgb(0, 128, 0)', 'rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + [1, 1, 1] + ); + + return Plotly.restyle(gd, 'marker.opacity', 0.4); + }).then(function() { + assertStyle(dims, + ['rgb(0, 128, 0)', 'rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + [0.4, 0.4, 0.4] + ); + + gd._fullData.forEach(function(trace) { + expect(trace.marker.opacity).toEqual(0.4); + }); + + return Plotly.restyle(gd, 'marker.opacity', 1); + }).then(function() { + assertStyle(dims, + ['rgb(0, 128, 0)', 'rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + [1, 1, 1] + ); + + gd._fullData.forEach(function(trace) { + expect(trace.marker.opacity).toEqual(1); + }); + + return Plotly.restyle(gd, { + 'transforms[0].groupColors': { a: 'green', b: 'red' }, + 'marker.opacity': [0.4, 0.6] + }); + }).then(function() { + assertStyle(dims, + ['rgb(0, 128, 0)', 'rgb(0, 128, 0)', 'rgb(255, 0, 0)'], + [0.4, 0.6, 0.6] + ); + + done(); + }); + }); + + it('Plotly.extendTraces should work', function(done) { + var data = Lib.extendDeep([], mockData0); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + assertDims([2, 3, 3]); + + return Plotly.extendTraces(gd, { + x: [ [-3, 4, 5] ], + y: [ [1, -2, 3] ], + 'transforms[0].groups': [ ['b', 'a', 'b'] ] + }, [1]); + }).then(function() { + assertDims([2, 4, 4]); + + return Plotly.extendTraces(gd, { + x: [ [5, 7, 10] ], + y: [ [1, -2, 3] ] + }, [0]); + }).then(function() { + assertDims([5, 4, 4]); + + done(); + }); + }); + + it('Plotly.deleteTraces should work', function(done) { + var data = Lib.extendDeep([], mockData0); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + assertDims([2, 3, 3]); + + return Plotly.deleteTraces(gd, [1]); + }).then(function() { + assertDims([2]); + + return Plotly.deleteTraces(gd, [0]); + }).then(function() { + assertDims([]); + + done(); + }); + }); + + it('toggling trace visibility should work', function(done) { + var data = Lib.extendDeep([], mockData0); + + var gd = createGraphDiv(); + + Plotly.plot(gd, data).then(function() { + assertDims([2, 3, 3]); + + return Plotly.restyle(gd, 'visible', 'legendonly', [1]); + }).then(function() { + assertDims([2]); + + return Plotly.restyle(gd, 'visible', false, [0]); + }).then(function() { + assertDims([]); + + return Plotly.restyle(gd, 'visible', [true, true]); + }).then(function() { + assertDims([2, 3, 3]); + + return Plotly.restyle(gd, 'visible', 'legendonly', [0]); + }).then(function() { + assertDims([3, 3]); + + done(); + }); + }); + +}); + +function assertDims(dims) { + var traces = d3.selectAll('.trace'); + + expect(traces.size()) + .toEqual(dims.length, 'to have correct number of traces'); + + traces.each(function(_, i) { + var trace = d3.select(this); + var points = trace.selectAll('.point'); + + expect(points.size()) + .toEqual(dims[i], 'to have correct number of pts in trace ' + i); + }); +} + +function assertStyle(dims, color, opacity) { + var N = dims.reduce(function(a, b) { + return a + b; + }); + + var traces = d3.selectAll('.trace'); + expect(traces.size()) + .toEqual(dims.length, 'to have correct number of traces'); + + expect(d3.selectAll('.point').size()) + .toEqual(N, 'to have correct total number of points'); + + traces.each(function(_, i) { + var trace = d3.select(this); + var points = trace.selectAll('.point'); + + expect(points.size()) + .toEqual(dims[i], 'to have correct number of pts in trace ' + i); + + points.each(function() { + var point = d3.select(this); + + expect(point.style('fill')) + .toEqual(color[i], 'to have correct pt color'); + expect(+point.style('opacity')) + .toEqual(opacity[i], 'to have correct pt opacity'); + }); + }); +} diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js index 4273b84a355..5ce33eb738f 100644 --- a/test/jasmine/tests/validate_test.js +++ b/test/jasmine/tests/validate_test.js @@ -1,4 +1,10 @@ var Plotly = require('@lib/index'); +var Lib = require('@src/lib'); + +Plotly.register([ + // until they become official + require('../assets/transforms/filter') +]); describe('Plotly.validate', function() { @@ -38,7 +44,7 @@ describe('Plotly.validate', function() { it('should report when a data trace is not an object', function() { var out = Plotly.validate([{ - type: 'scatter', + type: 'bar', x: [1, 2, 3] }, [1, 2, 3]]); @@ -61,7 +67,7 @@ describe('Plotly.validate', function() { it('should report when trace is defaulted to not be visible', function() { var out = Plotly.validate([{ - type: 'scatter' + type: 'scattergeo' // missing 'x' and 'y }], {}); @@ -248,4 +254,68 @@ describe('Plotly.validate', function() { 'In layout, key yaxis5 must be linked to an object container' ); }); + + it('should work with attributes in registered transforms', function() { + var base = { + x: [-2, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + }; + + var out = Plotly.validate([ + Lib.extendFlat({}, base, { + transforms: [{ + type: 'filter', + operation: '=' + }, { + type: 'filter', + operation: '=', + wrongKey: 'sup?' + }, { + type: 'filter', + operation: 'wrongVal' + }, + 'wont-work' + ] + }), + Lib.extendFlat({}, base, { + transforms: { + type: 'filter' + } + }), + Lib.extendFlat({}, base, { + transforms: [{ + type: 'no gonna work' + }] + }), + ], { + title: 'my transformed graph' + }); + + expect(out.length).toEqual(5); + assertErrorContent( + out[0], 'schema', 'data', 0, + ['transforms', 1, 'wrongKey'], 'transforms[1].wrongKey', + 'In data trace 0, key transforms[1].wrongKey is not part of the schema' + ); + assertErrorContent( + out[1], 'value', 'data', 0, + ['transforms', 2, 'operation'], 'transforms[2].operation', + 'In data trace 0, key transforms[2].operation is set to an invalid value (wrongVal)' + ); + assertErrorContent( + out[2], 'object', 'data', 0, + ['transforms', 3], 'transforms[3]', + 'In data trace 0, key transforms[3] must be linked to an object container' + ); + assertErrorContent( + out[3], 'array', 'data', 1, + ['transforms'], 'transforms', + 'In data trace 1, key transforms must be linked to an array container' + ); + assertErrorContent( + out[4], 'value', 'data', 2, + ['transforms', 0, 'type'], 'transforms[0].type', + 'In data trace 2, key transforms[0].type is set to an invalid value (no gonna work)' + ); + }); });