diff --git a/src/components/colorscale/calc.js b/src/components/colorscale/calc.js index 62629b8649c..b609b38d5da 100644 --- a/src/components/colorscale/calc.js +++ b/src/components/colorscale/calc.js @@ -16,24 +16,47 @@ var flipScale = require('./flip_scale'); module.exports = function calc(trace, vals, containerStr, cLetter) { - var container, inputContainer; + var container = trace; + var inputContainer = trace._input; + var fullInputContainer = trace._fullInput; + + // set by traces with groupby transforms + var updateStyle = trace.updateStyle; + + function doUpdate(attr, inputVal, fullVal) { + if(fullVal === undefined) fullVal = inputVal; + + if(updateStyle) { + updateStyle(trace._input, containerStr ? (containerStr + '.' + attr) : attr, inputVal); + } + else { + inputContainer[attr] = inputVal; + } + + container[attr] = fullVal; + if(fullInputContainer && (trace !== trace._fullInput)) { + if(updateStyle) { + updateStyle(trace._fullInput, containerStr ? (containerStr + '.' + attr) : attr, fullVal); + } + else { + fullInputContainer[attr] = fullVal; + } + } + } if(containerStr) { - container = Lib.nestedProperty(trace, containerStr).get(); - inputContainer = Lib.nestedProperty(trace._input, containerStr).get(); - } - else { - container = trace; - inputContainer = trace._input; + container = Lib.nestedProperty(container, containerStr).get(); + inputContainer = Lib.nestedProperty(inputContainer, containerStr).get(); + fullInputContainer = Lib.nestedProperty(fullInputContainer, containerStr).get() || {}; } - var autoAttr = cLetter + 'auto', - minAttr = cLetter + 'min', - maxAttr = cLetter + 'max', - auto = container[autoAttr], - min = container[minAttr], - max = container[maxAttr], - scl = container.colorscale; + var autoAttr = cLetter + 'auto'; + var minAttr = cLetter + 'min'; + var maxAttr = cLetter + 'max'; + var auto = container[autoAttr]; + var min = container[minAttr]; + var max = container[maxAttr]; + var scl = container.colorscale; if(auto !== false || min === undefined) { min = Lib.aggNums(Math.min, null, vals); @@ -48,11 +71,8 @@ module.exports = function calc(trace, vals, containerStr, cLetter) { max += 0.5; } - container[minAttr] = min; - container[maxAttr] = max; - - inputContainer[minAttr] = min; - inputContainer[maxAttr] = max; + doUpdate(minAttr, min); + doUpdate(maxAttr, max); /* * If auto was explicitly false but min or max was missing, @@ -61,8 +81,7 @@ module.exports = function calc(trace, vals, containerStr, cLetter) { * Otherwise make sure the trace still looks auto as far as later * changes are concerned. */ - inputContainer[autoAttr] = (auto !== false || - (min === undefined && max === undefined)); + doUpdate(autoAttr, (auto !== false || (min === undefined && max === undefined))); if(container.autocolorscale) { if(min * max < 0) scl = scales.RdBu; @@ -70,8 +89,14 @@ module.exports = function calc(trace, vals, containerStr, cLetter) { else scl = scales.Blues; // reversescale is handled at the containerOut level - inputContainer.colorscale = scl; - if(container.reversescale) scl = flipScale(scl); - container.colorscale = scl; + doUpdate('colorscale', scl, container.reversescale ? flipScale(scl) : scl); + + // We pushed a colorscale back to input, which will change the default autocolorscale next time + // to avoid spurious redraws from Plotly.react, update resulting autocolorscale now + // This is a conscious decision so that changing the data later does not unexpectedly + // give you a new colorscale + if(!inputContainer.autocolorscale) { + doUpdate('autocolorscale', false); + } } }; diff --git a/src/lib/index.js b/src/lib/index.js index d21cc0398a6..75354fd4972 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -30,6 +30,7 @@ lib.ensureArray = require('./ensure_array'); var isArrayModule = require('./is_array'); lib.isTypedArray = isArrayModule.isTypedArray; lib.isArrayOrTypedArray = isArrayModule.isArrayOrTypedArray; +lib.isArray1D = isArrayModule.isArray1D; var coerceModule = require('./coerce'); lib.valObjectMeta = coerceModule.valObjectMeta; diff --git a/src/lib/is_array.js b/src/lib/is_array.js index 28b58d0d1a0..b8c5e1ae47c 100644 --- a/src/lib/is_array.js +++ b/src/lib/is_array.js @@ -18,10 +18,28 @@ var dv = (typeof DataView === 'undefined') ? function() {} : DataView; -exports.isTypedArray = function(a) { +function isTypedArray(a) { return ab.isView(a) && !(a instanceof dv); -}; +} + +function isArrayOrTypedArray(a) { + return Array.isArray(a) || isTypedArray(a); +} + +/* + * Test whether an input object is 1D. + * + * Assumes we already know the object is an array. + * + * Looks only at the first element, if the dimensionality is + * not consistent we won't figure that out here. + */ +function isArray1D(a) { + return !isArrayOrTypedArray(a[0]); +} -exports.isArrayOrTypedArray = function(a) { - return Array.isArray(a) || exports.isTypedArray(a); +module.exports = { + isTypedArray: isTypedArray, + isArrayOrTypedArray: isArrayOrTypedArray, + isArray1D: isArray1D }; diff --git a/src/lib/keyed_container.js b/src/lib/keyed_container.js index 72ddd5ed9eb..b3288b45cd3 100644 --- a/src/lib/keyed_container.js +++ b/src/lib/keyed_container.js @@ -33,32 +33,44 @@ var UNSET = 4; module.exports = function keyedContainer(baseObj, path, keyName, valueName) { keyName = keyName || 'name'; valueName = valueName || 'value'; - var i, arr; + var i, arr, baseProp; var changeTypes = {}; - if(path && path.length) { arr = nestedProperty(baseObj, path).get(); + if(path && path.length) { + baseProp = nestedProperty(baseObj, path); + arr = baseProp.get(); } else { arr = baseObj; } path = path || ''; - arr = arr || []; // Construct an index: var indexLookup = {}; - for(i = 0; i < arr.length; i++) { - indexLookup[arr[i][keyName]] = i; + if(arr) { + for(i = 0; i < arr.length; i++) { + indexLookup[arr[i][keyName]] = i; + } } var isSimpleValueProp = SIMPLE_PROPERTY_REGEX.test(valueName); var obj = { - // NB: this does not actually modify the baseObj set: function(name, value) { var changeType = value === null ? UNSET : NONE; + // create the base array if necessary + if(!arr) { + if(!baseProp || changeType === UNSET) return; + + arr = []; + baseProp.set(arr); + } + var idx = indexLookup[name]; if(idx === undefined) { + if(changeType === UNSET) return; + changeType = changeType | BOTH; idx = arr.length; indexLookup[name] = idx; @@ -86,6 +98,8 @@ module.exports = function keyedContainer(baseObj, path, keyName, valueName) { return obj; }, get: function(name) { + if(!arr) return; + var idx = indexLookup[name]; if(idx === undefined) { diff --git a/src/lib/nested_property.js b/src/lib/nested_property.js index 754609db789..a37bf5ca4ad 100644 --- a/src/lib/nested_property.js +++ b/src/lib/nested_property.js @@ -11,8 +11,6 @@ var isNumeric = require('fast-isnumeric'); var isArrayOrTypedArray = require('./is_array').isArrayOrTypedArray; -var isPlainObject = require('./is_plain_object'); -var containerArrayMatch = require('../plot_api/container_array_match'); /** * convert a string s (such as 'xaxis.range[0]') @@ -115,44 +113,21 @@ function npGet(cont, parts) { } /* - * Can this value be deleted? We can delete any empty object (null, undefined, [], {}) - * EXCEPT empty data arrays, {} inside an array, or anything INSIDE an *args* array. + * Can this value be deleted? We can delete `undefined`, and `null` except INSIDE an + * *args* array. * - * Info arrays can be safely deleted, but not deleting them has no ill effects other - * than leaving a trace or layout object with some cruft in it. + * Previously we also deleted some `{}` and `[]`, in order to try and make set/unset + * a net noop; but this causes far more complication than it's worth, and still had + * lots of exceptions. See https://github.com/plotly/plotly.js/issues/1410 * - * Deleting data arrays can change the meaning of the object, as `[]` means there is - * data for this attribute, it's just empty right now while `undefined` means the data - * should be filled in with defaults to match other data arrays. - * - * `{}` inside an array means "the default object" which is clearly different from - * popping it off the end of the array, or setting it `undefined` inside the array. - * - * *args* arrays get passed directly to API methods and we should respect precisely - * what the user has put there - although if the whole *args* array is empty it's fine - * to delete that. - * - * So we do some simple tests here to find known non-data arrays but don't worry too - * much about not deleting some arrays that would actually be safe to delete. + * *args* arrays get passed directly to API methods and we should respect null if + * the user put it there, but otherwise null is deleted as we use it as code + * in restyle/relayout/update for "delete this value" whereas undefined means + * "ignore this edit" */ -var INFO_PATTERNS = /(^|\.)((domain|range)(\.[xy])?|args|parallels)$/; var ARGS_PATTERN = /(^|\.)args\[/; function isDeletable(val, propStr) { - if(!emptyObj(val) || - (isPlainObject(val) && propStr.charAt(propStr.length - 1) === ']') || - (propStr.match(ARGS_PATTERN) && val !== undefined) - ) { - return false; - } - if(!isArrayOrTypedArray(val)) return true; - - if(propStr.match(INFO_PATTERNS)) return true; - - var match = containerArrayMatch(propStr); - // if propStr matches the container array itself, index is an empty string - // otherwise we've matched something inside the container array, which may - // still be a data array. - return match && (match.index === ''); + return (val === undefined) || (val === null && !propStr.match(ARGS_PATTERN)); } function npSet(cont, parts, propStr) { @@ -194,8 +169,18 @@ function npSet(cont, parts, propStr) { } if(toDelete) { - if(i === parts.length - 1) delete curCont[parts[i]]; - pruneContainers(containerLevels); + if(i === parts.length - 1) { + delete curCont[parts[i]]; + + // The one bit of pruning we still do: drop `undefined` from the end of arrays. + // In case someone has already unset previous items, continue until we hit a + // non-undefined value. + if(Array.isArray(curCont) && +parts[i] === curCont.length - 1) { + while(curCont.length && curCont[curCont.length - 1] === undefined) { + curCont.pop(); + } + } + } } else curCont[parts[i]] = val; }; @@ -249,48 +234,6 @@ function checkNewContainer(container, part, nextPart, toDelete) { return true; } -function pruneContainers(containerLevels) { - var i, - j, - curCont, - propPart, - keys, - remainingKeys; - for(i = containerLevels.length - 1; i >= 0; i--) { - curCont = containerLevels[i][0]; - propPart = containerLevels[i][1]; - - remainingKeys = false; - if(isArrayOrTypedArray(curCont)) { - for(j = curCont.length - 1; j >= 0; j--) { - if(isDeletable(curCont[j], joinPropStr(propPart, j))) { - if(remainingKeys) curCont[j] = undefined; - else curCont.pop(); - } - else remainingKeys = true; - } - } - else if(typeof curCont === 'object' && curCont !== null) { - keys = Object.keys(curCont); - remainingKeys = false; - for(j = keys.length - 1; j >= 0; j--) { - if(isDeletable(curCont[keys[j]], joinPropStr(propPart, keys[j]))) { - delete curCont[keys[j]]; - } - else remainingKeys = true; - } - } - if(remainingKeys) return; - } -} - -function emptyObj(obj) { - if(obj === undefined || obj === null) return true; - if(typeof obj !== 'object') return false; // any plain value - if(isArrayOrTypedArray(obj)) return !obj.length; // [] - return !Object.keys(obj).length; // {} -} - function badContainer(container, propStr, propParts) { return { set: function() { throw 'bad container'; }, diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 9ea68b9f5e4..34a078c3101 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2267,7 +2267,10 @@ exports.react = function(gd, data, layout, config) { gd.layout = layout || {}; helpers.cleanLayout(gd.layout); - Plots.supplyDefaults(gd); + // "true" skips updating calcdata and remapping arrays from calcTransforms, + // which supplyDefaults usually does at the end, but we may need to NOT do + // if the diff (which we haven't determined yet) says we'll recalc + Plots.supplyDefaults(gd, {skipUpdateCalc: true}); var newFullData = gd._fullData; var newFullLayout = gd._fullLayout; @@ -2289,6 +2292,9 @@ exports.react = function(gd, data, layout, config) { // clear calcdata if required if(restyleFlags.calc || relayoutFlags.calc) gd.calcdata = undefined; + // otherwise do the calcdata updates and calcTransform array remaps that we skipped earlier + else Plots.supplyDefaultsUpdateCalc(gd.calcdata, newFullData); + if(relayoutFlags.margins) helpers.clearAxisAutomargins(gd); // Note: what restyle/relayout use impliedEdits and clearAxisTypes for @@ -2397,7 +2403,7 @@ function diffData(gd, oldFullData, newFullData, immutable) { Axes.getFromId(gd, trace.xaxis).autorange || Axes.getFromId(gd, trace.yaxis).autorange ) : false; - getDiffFlags(oldFullData[i], trace, [], diffOpts); + getDiffFlags(oldFullData[i]._fullInput, trace, [], diffOpts); } if(flags.calc || flags.plot || flags.calcIfAutorange) { @@ -2461,13 +2467,6 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) { return valObject.valType === 'data_array' || valObject.arrayOk; } - // for transforms: look at _fullInput rather than the transform result, which often - // contains generated arrays. - var newFullInput = newContainer._fullInput; - var oldFullInput = oldContainer._fullInput; - if(newFullInput && newFullInput !== newContainer) newContainer = newFullInput; - if(oldFullInput && oldFullInput !== oldContainer) oldContainer = oldFullInput; - for(key in oldContainer) { // short-circuit based on previous calls or previous keys that already maximized the pathway if(flags.calc) return; @@ -2494,6 +2493,8 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) { // in case type changed, we may not even *have* a valObject. if(!valObject) continue; + if(valObject._compareAsJSON && JSON.stringify(oldVal) === JSON.stringify(newVal)) continue; + var valType = valObject.valType; var i; diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index f5681c44a3a..b7487eeca0b 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -171,9 +171,12 @@ exports.isValObject = function(obj) { exports.findArrayAttributes = function(trace) { var arrayAttributes = []; var stack = []; + var isArrayStack = []; + var baseContainer, baseAttrName; function callback(attr, attrName, attrs, level) { stack = stack.slice(0, level).concat([attrName]); + isArrayStack = isArrayStack.slice(0, level).concat([attr && attr._isLinkedToArray]); var splittableAttr = ( attr && @@ -190,48 +193,55 @@ exports.findArrayAttributes = function(trace) { if(!splittableAttr) return; - var astr = toAttrString(stack); - var val = Lib.nestedProperty(trace, astr).get(); - if(!Lib.isArrayOrTypedArray(val)) return; - - arrayAttributes.push(astr); + crawlIntoTrace(baseContainer, 0, ''); } - function toAttrString(stack) { - return stack.join('.'); + function crawlIntoTrace(container, i, astrPartial) { + var item = container[stack[i]]; + var newAstrPartial = astrPartial + stack[i]; + if(i === stack.length - 1) { + if(Lib.isArrayOrTypedArray(item)) { + arrayAttributes.push(baseAttrName + newAstrPartial); + } + } + else { + if(isArrayStack[i]) { + if(Array.isArray(item)) { + for(var j = 0; j < item.length; j++) { + if(Lib.isPlainObject(item[j])) { + crawlIntoTrace(item[j], i + 1, newAstrPartial + '[' + j + '].'); + } + } + } + } + else if(Lib.isPlainObject(item)) { + crawlIntoTrace(item, i + 1, newAstrPartial + '.'); + } + } } + baseContainer = trace; + baseAttrName = ''; exports.crawl(baseAttributes, callback); if(trace._module && trace._module.attributes) { exports.crawl(trace._module.attributes, callback); } - if(trace.transforms) { - var transforms = trace.transforms; - + var transforms = trace.transforms; + if(transforms) { for(var i = 0; i < transforms.length; i++) { var transform = transforms[i]; var module = transform._module; if(module) { - stack = ['transforms[' + i + ']']; + baseAttrName = 'transforms[' + i + '].'; + baseContainer = transform; - exports.crawl(module.attributes, callback, 1); + exports.crawl(module.attributes, callback); } } } - // Look into the fullInput module attributes for array attributes - // to make sure that 'custom' array attributes are detected. - // - // At the moment, we need this block to make sure that - // ohlc and candlestick 'open', 'high', 'low', 'close' can be - // used with filter and groupby transforms. - if(trace._fullInput && trace._fullInput._module && trace._fullInput._module.attributes) { - exports.crawl(trace._fullInput._module.attributes, callback); - arrayAttributes = Lib.filterUnique(arrayAttributes); - } - return arrayAttributes; }; @@ -256,6 +266,9 @@ exports.getTraceValObject = function(trace, parts) { var moduleAttrs, valObject; if(head === 'transforms') { + if(parts.length === 1) { + return baseAttributes.transforms; + } var transforms = trace.transforms; if(!Array.isArray(transforms) || !transforms.length) return false; var tNum = parts[1]; diff --git a/src/plots/attributes.js b/src/plots/attributes.js index 1c05f27e26a..7c1dfa445b8 100644 --- a/src/plots/attributes.js +++ b/src/plots/attributes.js @@ -156,5 +156,13 @@ module.exports = { ].join(' ') }, editType: 'calc' + }, + transforms: { + _isLinkedToArray: 'transform', + editType: 'calc', + description: [ + 'An array of operations that manipulate the trace data,', + 'for example filtering or sorting the data arrays.' + ].join(' ') } }; diff --git a/src/plots/cartesian/type_defaults.js b/src/plots/cartesian/type_defaults.js index 3d4f7c4bf35..7e5e7cc0ac6 100644 --- a/src/plots/cartesian/type_defaults.js +++ b/src/plots/cartesian/type_defaults.js @@ -107,7 +107,7 @@ function getFirstNonEmptyTrace(data, id, axLetter) { var trace = data[i]; if(trace.type === 'splom' && - trace._commonLength > 0 && + trace._length > 0 && trace['_' + axLetter + 'axes'][id] ) { return trace; diff --git a/src/plots/plots.js b/src/plots/plots.js index 05aa915e073..77dc572f4e6 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -254,27 +254,37 @@ var extraFormatKeys = [ 'year', 'month', 'dayMonth', 'dayMonthYear' ]; -// Fill in default values: -// -// gd.data, gd.layout: -// are precisely what the user specified, -// these fields shouldn't be modified nor used directly -// after the supply defaults step. -// -// gd._fullData, gd._fullLayout: -// are complete descriptions of how to draw the plot, -// use these fields in all required computations. -// -// gd._fullLayout._modules -// is a list of all the trace modules required to draw the plot. -// -// gd._fullLayout._basePlotModules -// is a list of all the plot modules required to draw the plot. -// -// gd._fullLayout._transformModules -// is a list of all the transform modules invoked. -// -plots.supplyDefaults = function(gd) { +/* + * Fill in default values + * @param {DOM element} gd + * @param {object} opts + * @param {boolean} opts.skipUpdateCalc: normally if the existing gd.calcdata looks + * compatible with the new gd._fullData we finish by linking the new _fullData traces + * to the old gd.calcdata, so it's correctly set if we're not going to recalc. But also, + * if there are calcTransforms on the trace, we first remap data arrays from the old full + * trace into the new one. Use skipUpdateCalc to defer this (needed by Plotly.react) + * + * gd.data, gd.layout: + * are precisely what the user specified (except as modified by cleanData/cleanLayout), + * these fields shouldn't be modified (except for filling in some auto values) + * nor used directly after the supply defaults step. + * + * gd._fullData, gd._fullLayout: + * are complete descriptions of how to draw the plot, + * use these fields in all required computations. + * + * gd._fullLayout._modules + * is a list of all the trace modules required to draw the plot. + * + * gd._fullLayout._basePlotModules + * is a list of all the plot modules required to draw the plot. + * + * gd._fullLayout._transformModules + * is a list of all the transform modules invoked. + * + */ +plots.supplyDefaults = function(gd, opts) { + var skipUpdateCalc = opts && opts.skipUpdateCalc; var oldFullLayout = gd._fullLayout || {}; if(oldFullLayout._skipDefaults) { @@ -458,24 +468,35 @@ plots.supplyDefaults = function(gd) { } // update object references in calcdata - if(oldCalcdata.length === newFullData.length) { - for(i = 0; i < newFullData.length; i++) { - var newTrace = newFullData[i]; - var cd0 = oldCalcdata[i][0]; - if(cd0 && cd0.trace) { - if(cd0.trace._hasCalcTransform) { - remapTransformedArrays(cd0, newTrace); - } else { - cd0.trace = newTrace; - } - } - } + if(!skipUpdateCalc && oldCalcdata.length === newFullData.length) { + plots.supplyDefaultsUpdateCalc(oldCalcdata, newFullData); } // sort base plot modules for consistent ordering newFullLayout._basePlotModules.sort(sortBasePlotModules); }; +plots.supplyDefaultsUpdateCalc = function(oldCalcdata, newFullData) { + for(var i = 0; i < newFullData.length; i++) { + var newTrace = newFullData[i]; + var cd0 = oldCalcdata[i][0]; + if(cd0 && cd0.trace) { + var oldTrace = cd0.trace; + if(oldTrace._hasCalcTransform) { + var arrayAttrs = oldTrace._arrayAttrs; + var j, astr, oldArrayVal; + + for(j = 0; j < arrayAttrs.length; j++) { + astr = arrayAttrs[j]; + oldArrayVal = Lib.nestedProperty(oldTrace, astr).get().slice(); + Lib.nestedProperty(newTrace, astr).set(oldArrayVal); + } + } + cd0.trace = newTrace; + } + } +}; + /** * Make a container for collecting subplots we need to display. * @@ -518,25 +539,6 @@ function emptySubplotLists() { return out; } -function remapTransformedArrays(cd0, newTrace) { - var oldTrace = cd0.trace; - var arrayAttrs = oldTrace._arrayAttrs; - var transformedArrayHash = {}; - var i, astr; - - for(i = 0; i < arrayAttrs.length; i++) { - astr = arrayAttrs[i]; - transformedArrayHash[astr] = Lib.nestedProperty(oldTrace, astr).get().slice(); - } - - cd0.trace = newTrace; - - for(i = 0; i < arrayAttrs.length; i++) { - astr = arrayAttrs[i]; - Lib.nestedProperty(cd0.trace, astr).set(transformedArrayHash[astr]); - } -} - /** * getFormatObj: use _context to get the format object from locale. * Used to get d3.locale argument object and extraFormat argument object @@ -1174,6 +1176,11 @@ plots.supplyTraceDefaults = function(traceIn, colorIndex, layout, traceInIndex) }; plots.supplyTransformDefaults = function(traceIn, traceOut, layout) { + // For now we only allow transforms on 1D traces, ie those that specify a _length. + // If we were to implement 2D transforms, we'd need to have each transform + // describe its own applicability and disable itself when it doesn't apply. + if(!traceOut._length) return; + var globalTransforms = layout._globalTransforms || []; var transformModules = layout._transformModules || []; diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index 0c4c75dc58a..d9f6aeb7e5b 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -48,12 +48,11 @@ module.exports = function calc(gd, trace) { var dPos = dv.minDiff / 2; var posBins = makeBins(posDistinct, dPos); - var vLen = val.length; var pLen = posDistinct.length; var ptsPerBin = initNestedArray(pLen); // bin pts info per position bins - for(i = 0; i < vLen; i++) { + for(i = 0; i < trace._length; i++) { var v = val[i]; if(!isNumeric(v)) continue; diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js index 0ab1d18c377..dc75d7f1266 100644 --- a/src/traces/box/defaults.js +++ b/src/traces/box/defaults.js @@ -38,19 +38,28 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function handleSampleDefaults(traceIn, traceOut, coerce, layout) { var y = coerce('y'); var x = coerce('x'); + var hasX = x && x.length; - var defaultOrientation; + var defaultOrientation, len; if(y && y.length) { defaultOrientation = 'v'; - if(!x) coerce('x0'); - } else if(x && x.length) { + if(hasX) { + len = Math.min(x.length, y.length); + } + else { + coerce('x0'); + len = y.length; + } + } else if(hasX) { defaultOrientation = 'h'; coerce('y0'); + len = x.length; } else { traceOut.visible = false; return; } + traceOut._length = len; var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout); diff --git a/src/traces/carpet/ab_defaults.js b/src/traces/carpet/ab_defaults.js index 02ed037f88e..0c94e38d956 100644 --- a/src/traces/carpet/ab_defaults.js +++ b/src/traces/carpet/ab_defaults.js @@ -26,8 +26,6 @@ module.exports = function handleABDefaults(traceIn, traceOut, fullLayout, coerce } mimickAxisDefaults(traceIn, traceOut, fullLayout, dfltColor); - - return; }; function mimickAxisDefaults(traceIn, traceOut, fullLayout, dfltColor) { diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index 50ebafff330..f9b0e65b648 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -9,6 +9,7 @@ 'use strict'; var Axes = require('../../plots/cartesian/axes'); +var isArray1D = require('../../lib').isArray1D; var cheaterBasis = require('./cheater_basis'); var arrayMinmax = require('./array_minmax'); var calcGridlines = require('./calc_gridlines'); @@ -16,7 +17,6 @@ var calcLabels = require('./calc_labels'); var calcClipPath = require('./calc_clippath'); var clean2dArray = require('../heatmap/clean_2d_array'); var smoothFill2dArray = require('./smooth_fill_2d_array'); -var hasColumns = require('./has_columns'); var convertColumnData = require('../heatmap/convert_column_xyz'); var setConvert = require('./set_convert'); @@ -29,8 +29,8 @@ module.exports = function calc(gd, trace) { var x = trace.x; var y = trace.y; var cols = []; - if(x && !hasColumns(x)) cols.push('x'); - if(y && !hasColumns(y)) cols.push('y'); + if(x && isArray1D(x)) cols.push('x'); + if(y && isArray1D(y)) cols.push('y'); if(cols.length) { convertColumnData(trace, aax, bax, 'a', 'b', cols); diff --git a/src/traces/carpet/defaults.js b/src/traces/carpet/defaults.js index da6373e69ea..6e2cf30f511 100644 --- a/src/traces/carpet/defaults.js +++ b/src/traces/carpet/defaults.js @@ -46,13 +46,12 @@ module.exports = function supplyDefaults(traceIn, traceOut, dfltColor, fullLayou // corresponds to b and the second to a. This sounds backwards but ends up making sense // the important part to know is that when you write y[j][i], j goes from 0 to b.length - 1 // and i goes from 0 to a.length - 1. - var len = handleXYDefaults(traceIn, traceOut, coerce); + var validData = handleXYDefaults(traceIn, traceOut, coerce); + if(!validData) { + traceOut.visible = false; + } if(traceOut._cheater) { coerce('cheaterslope'); } - - if(!len) { - traceOut.visible = false; - } }; diff --git a/src/traces/carpet/has_columns.js b/src/traces/carpet/has_columns.js deleted file mode 100644 index fad4812f044..00000000000 --- a/src/traces/carpet/has_columns.js +++ /dev/null @@ -1,15 +0,0 @@ -/** -* Copyright 2012-2018, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - -'use strict'; - -var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; - -module.exports = function(data) { - return isArrayOrTypedArray(data[0]); -}; diff --git a/src/traces/carpet/xy_defaults.js b/src/traces/carpet/xy_defaults.js index 4e046f8eb53..dbf9252c175 100644 --- a/src/traces/carpet/xy_defaults.js +++ b/src/traces/carpet/xy_defaults.js @@ -9,11 +9,25 @@ 'use strict'; +var isArray1D = require('../../lib').isArray1D; + module.exports = function handleXYDefaults(traceIn, traceOut, coerce) { var x = coerce('x'); + var hasX = x && x.length; var y = coerce('y'); + var hasY = y && y.length; + if(!hasX && !hasY) return false; traceOut._cheater = !x; - return !!x || !!y; + if((!hasX || isArray1D(x)) && (!hasY || isArray1D(y))) { + var len = hasX ? x.length : Infinity; + if(hasY) len = Math.min(len, y.length); + if(traceOut.a && traceOut.a.length) len = Math.min(len, traceOut.a.length); + if(traceOut.b && traceOut.b.length) len = Math.min(len, traceOut.b.length); + traceOut._length = len; + } + else traceOut._length = null; + + return true; }; diff --git a/src/traces/choropleth/calc.js b/src/traces/choropleth/calc.js index f818ea57d10..b9a88430701 100644 --- a/src/traces/choropleth/calc.js +++ b/src/traces/choropleth/calc.js @@ -17,7 +17,7 @@ var arraysToCalcdata = require('../scatter/arrays_to_calcdata'); var calcSelection = require('../scatter/calc_selection'); module.exports = function calc(gd, trace) { - var len = trace.locations.length; + var len = trace._length; var calcTrace = new Array(len); for(var i = 0; i < len; i++) { diff --git a/src/traces/choropleth/defaults.js b/src/traces/choropleth/defaults.js index 650571cc4d6..890ddf26692 100644 --- a/src/traces/choropleth/defaults.js +++ b/src/traces/choropleth/defaults.js @@ -19,22 +19,13 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } var locations = coerce('locations'); - - var len; - if(locations) len = locations.length; - - if(!locations || !len) { - traceOut.visible = false; - return; - } - var z = coerce('z'); - if(!Lib.isArrayOrTypedArray(z)) { + + if(!(locations && locations.length && Lib.isArrayOrTypedArray(z) && z.length)) { traceOut.visible = false; return; } - - if(z.length > len) traceOut.z = z.slice(0, len); + traceOut._length = Math.min(locations.length, z.length); coerce('locationmode'); diff --git a/src/traces/contour/defaults.js b/src/traces/contour/defaults.js index c8be70030a9..f995de309f5 100644 --- a/src/traces/contour/defaults.js +++ b/src/traces/contour/defaults.js @@ -11,7 +11,6 @@ var Lib = require('../../lib'); -var hasColumns = require('../heatmap/has_columns'); var handleXYZDefaults = require('../heatmap/xyz_defaults'); var handleConstraintDefaults = require('./constraint_defaults'); var handleContoursDefaults = require('./contours_defaults'); @@ -36,7 +35,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('text'); var isConstraint = (coerce('contours.type') === 'constraint'); - coerce('connectgaps', hasColumns(traceOut)); + coerce('connectgaps', Lib.isArray1D(traceOut.z)); // trace-level showlegend has already been set, but is only allowed if this is a constraint if(!isConstraint) delete traceOut.showlegend; diff --git a/src/traces/contour/set_contours.js b/src/traces/contour/set_contours.js index 46ec7436211..91eaf715456 100644 --- a/src/traces/contour/set_contours.js +++ b/src/traces/contour/set_contours.js @@ -10,7 +10,7 @@ 'use strict'; var Axes = require('../../plots/cartesian/axes'); -var extendFlat = require('../../lib').extendFlat; +var Lib = require('../../lib'); module.exports = function setContours(trace) { @@ -18,7 +18,13 @@ module.exports = function setContours(trace) { // check if we need to auto-choose contour levels if(trace.autocontour) { - var dummyAx = autoContours(trace.zmin, trace.zmax, trace.ncontours); + var zmin = trace.zmin; + var zmax = trace.zmax; + if(zmin === undefined || zmax === undefined) { + zmin = Lib.aggNums(Math.min, null, trace._z); + zmax = Lib.aggNums(Math.max, null, trace._z); + } + var dummyAx = autoContours(zmin, zmax, trace.ncontours); contours.size = dummyAx.dtick; @@ -26,8 +32,8 @@ module.exports = function setContours(trace) { dummyAx.range.reverse(); contours.end = Axes.tickFirst(dummyAx); - if(contours.start === trace.zmin) contours.start += contours.size; - if(contours.end === trace.zmax) contours.end -= contours.size; + if(contours.start === zmin) contours.start += contours.size; + if(contours.end === zmax) contours.end -= contours.size; // if you set a small ncontours, *and* the ends are exactly on zmin/zmax // there's an edge case where start > end now. Make sure there's at least @@ -40,7 +46,7 @@ module.exports = function setContours(trace) { // previously we copied the whole contours object back, but that had // other info (coloring, showlines) that should be left to supplyDefaults if(!trace._input.contours) trace._input.contours = {}; - extendFlat(trace._input.contours, { + Lib.extendFlat(trace._input.contours, { start: contours.start, end: contours.end, size: contours.size diff --git a/src/traces/contourcarpet/attributes.js b/src/traces/contourcarpet/attributes.js index 0ab2605299c..534b0cba77a 100644 --- a/src/traces/contourcarpet/attributes.js +++ b/src/traces/contourcarpet/attributes.js @@ -40,9 +40,6 @@ module.exports = extendFlat({}, { atype: heatmapAttrs.xtype, btype: heatmapAttrs.ytype, - // unimplemented - looks like connectgaps is implied true - // connectgaps: heatmapAttrs.connectgaps, - fillcolor: contourAttrs.fillcolor, autocontour: contourAttrs.autocontour, diff --git a/src/traces/contourcarpet/calc.js b/src/traces/contourcarpet/calc.js index f5f68ea5f4b..ebaccfa0f73 100644 --- a/src/traces/contourcarpet/calc.js +++ b/src/traces/contourcarpet/calc.js @@ -9,7 +9,8 @@ 'use strict'; var colorscaleCalc = require('../../components/colorscale/calc'); -var hasColumns = require('../heatmap/has_columns'); +var isArray1D = require('../../lib').isArray1D; + var convertColumnData = require('../heatmap/convert_column_xyz'); var clean2dArray = require('../heatmap/clean_2d_array'); var maxRowLength = require('../heatmap/max_row_length'); @@ -69,7 +70,7 @@ function heatmappishCalc(gd, trace) { aax._minDtick = 0; bax._minDtick = 0; - if(hasColumns(trace)) convertColumnData(trace, aax, bax, 'a', 'b', ['z']); + if(isArray1D(trace.z)) convertColumnData(trace, aax, bax, 'a', 'b', ['z']); a = trace._a = trace._a || trace.a; b = trace._b = trace._b || trace.b; @@ -98,7 +99,7 @@ function heatmappishCalc(gd, trace) { z: z, }; - if(trace.contours.type === 'levels') { + if(trace.contours.type === 'levels' && trace.contours.coloring !== 'none') { // auto-z and autocolorscale if applicable colorscaleCalc(trace, z, '', 'z'); } diff --git a/src/traces/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js index 235d448d244..b0cc3f19817 100644 --- a/src/traces/contourcarpet/defaults.js +++ b/src/traces/contourcarpet/defaults.js @@ -55,9 +55,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('text'); var isConstraint = (coerce('contours.type') === 'constraint'); - // Unimplemented: - // coerce('connectgaps', hasColumns(traceOut)); - // trace-level showlegend has already been set, but is only allowed if this is a constraint if(!isConstraint) delete traceOut.showlegend; @@ -69,5 +66,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } } else { traceOut._defaultColor = defaultColor; + traceOut._length = null; } }; diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index 7364a8bc88d..0ff85f1a24d 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -15,7 +15,6 @@ var Axes = require('../../plots/cartesian/axes'); var histogram2dCalc = require('../histogram2d/calc'); var colorscaleCalc = require('../../components/colorscale/calc'); -var hasColumns = require('./has_columns'); var convertColumnData = require('./convert_column_xyz'); var maxRowLength = require('./max_row_length'); var clean2dArray = require('./clean_2d_array'); @@ -59,7 +58,7 @@ module.exports = function calc(gd, trace) { } else { var zIn = trace.z; - if(hasColumns(trace)) { + if(Lib.isArray1D(zIn)) { convertColumnData(trace, xa, ya, 'x', 'y', ['z']); x = trace._x; y = trace._y; diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js index 909fff986a8..57638ace45e 100644 --- a/src/traces/heatmap/convert_column_xyz.js +++ b/src/traces/heatmap/convert_column_xyz.js @@ -13,24 +13,16 @@ var Lib = require('../../lib'); var BADNUM = require('../../constants/numerical').BADNUM; module.exports = function convertColumnData(trace, ax1, ax2, var1Name, var2Name, arrayVarNames) { - var col1 = trace[var1Name].slice(), - col2 = trace[var2Name].slice(), - textCol = trace.text, - colLen = Math.min(col1.length, col2.length), - hasColumnText = (textCol !== undefined && !Array.isArray(textCol[0])), - col1Calendar = trace[var1Name + 'calendar'], - col2Calendar = trace[var2Name + 'calendar']; + var colLen = trace._length; + var col1 = trace[var1Name].slice(0, colLen); + var col2 = trace[var2Name].slice(0, colLen); + var textCol = trace.text; + var hasColumnText = (textCol !== undefined && Lib.isArray1D(textCol)); + var col1Calendar = trace[var1Name + 'calendar']; + var col2Calendar = trace[var2Name + 'calendar']; var i, j, arrayVar, newArray, arrayVarName; - for(i = 0; i < arrayVarNames.length; i++) { - arrayVar = trace[arrayVarNames[i]]; - if(arrayVar) colLen = Math.min(colLen, arrayVar.length); - } - - if(colLen < col1.length) col1 = col1.slice(0, colLen); - if(colLen < col2.length) col2 = col2.slice(0, colLen); - for(i = 0; i < colLen; i++) { col1[i] = ax1.d2c(col1[i], 0, col1Calendar); col2[i] = ax2.d2c(col2[i], 0, col2Calendar); diff --git a/src/traces/heatmap/defaults.js b/src/traces/heatmap/defaults.js index 2c65a0be3ed..442d5beed07 100644 --- a/src/traces/heatmap/defaults.js +++ b/src/traces/heatmap/defaults.js @@ -11,7 +11,6 @@ var Lib = require('../../lib'); -var hasColumns = require('./has_columns'); var handleXYZDefaults = require('./xyz_defaults'); var handleStyleDefaults = require('./style_defaults'); var colorscaleDefaults = require('../../components/colorscale/defaults'); @@ -23,8 +22,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var len = handleXYZDefaults(traceIn, traceOut, coerce, layout); - if(!len) { + var validData = handleXYZDefaults(traceIn, traceOut, coerce, layout); + if(!validData) { traceOut.visible = false; return; } @@ -33,7 +32,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout handleStyleDefaults(traceIn, traceOut, coerce, layout); - coerce('connectgaps', hasColumns(traceOut) && (traceOut.zsmooth !== false)); + coerce('connectgaps', Lib.isArray1D(traceOut.z) && (traceOut.zsmooth !== false)); colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'}); }; diff --git a/src/traces/heatmap/has_columns.js b/src/traces/heatmap/has_columns.js deleted file mode 100644 index cea3c5733ec..00000000000 --- a/src/traces/heatmap/has_columns.js +++ /dev/null @@ -1,15 +0,0 @@ -/** -* Copyright 2012-2018, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - -'use strict'; - -var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; - -module.exports = function(trace) { - return !isArrayOrTypedArray(trace.z[0]); -}; diff --git a/src/traces/heatmap/xyz_defaults.js b/src/traces/heatmap/xyz_defaults.js index 1857768e4e5..c457abfbfb4 100644 --- a/src/traces/heatmap/xyz_defaults.js +++ b/src/traces/heatmap/xyz_defaults.js @@ -10,10 +10,9 @@ 'use strict'; var isNumeric = require('fast-isnumeric'); -var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; +var Lib = require('../../lib'); var Registry = require('../../registry'); -var hasColumns = require('./has_columns'); module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout, xName, yName) { var z = coerce('z'); @@ -23,12 +22,14 @@ module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout, x if(z === undefined || !z.length) return 0; - if(hasColumns(traceIn)) { + if(Lib.isArray1D(traceIn.z)) { x = coerce(xName); y = coerce(yName); // column z must be accompanied by xName and yName arrays - if(!x || !y) return 0; + if(!(x && x.length && y && y.length)) return 0; + + traceOut._length = Math.min(x.length, y.length, z.length); } else { x = coordDefaults(xName, coerce); @@ -38,12 +39,14 @@ module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout, x if(!isValidZ(z)) return 0; coerce('transpose'); + + traceOut._length = null; } var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); handleCalendarDefaults(traceIn, traceOut, [xName, yName], layout); - return traceOut.z.length; + return true; }; function coordDefaults(coordStr, coerce) { @@ -76,7 +79,7 @@ function isValidZ(z) { for(var i = 0; i < z.length; i++) { zi = z[i]; - if(!isArrayOrTypedArray(zi)) { + if(!Lib.isArrayOrTypedArray(zi)) { allRowsAreArrays = false; break; } diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js index 79b3dcb9985..b56b451311a 100644 --- a/src/traces/histogram/defaults.js +++ b/src/traces/histogram/defaults.js @@ -22,8 +22,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var x = coerce('x'), - y = coerce('y'); + var x = coerce('x'); + var y = coerce('y'); var cumulative = coerce('cumulative.enabled'); if(cumulative) { @@ -33,22 +33,26 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('text'); - var orientation = coerce('orientation', (y && !x) ? 'h' : 'v'), - sample = traceOut[orientation === 'v' ? 'x' : 'y']; + var orientation = coerce('orientation', (y && !x) ? 'h' : 'v'); + var sampleLetter = orientation === 'v' ? 'x' : 'y'; + var aggLetter = orientation === 'v' ? 'y' : 'x'; - if(!(sample && sample.length)) { + var len = (x && y) ? Math.min(x.length && y.length) : (traceOut[sampleLetter] || []).length; + + if(!len) { traceOut.visible = false; return; } + traceOut._length = len; + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout); - var hasAggregationData = traceOut[orientation === 'h' ? 'x' : 'y']; + var hasAggregationData = traceOut[aggLetter]; if(hasAggregationData) coerce('histfunc'); - var binDirections = (orientation === 'h') ? ['y'] : ['x']; - handleBinDefaults(traceIn, traceOut, coerce, binDirections); + handleBinDefaults(traceIn, traceOut, coerce, [sampleLetter]); handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); diff --git a/src/traces/histogram2d/calc.js b/src/traces/histogram2d/calc.js index 7ac47f23eed..bc3c91294fe 100644 --- a/src/traces/histogram2d/calc.js +++ b/src/traces/histogram2d/calc.js @@ -33,7 +33,7 @@ module.exports = function calc(gd, trace) { var i, j, n, m; - var serieslen = Math.min(x.length, y.length); + var serieslen = trace._length; if(x.length > serieslen) x.splice(serieslen, x.length - serieslen); if(y.length > serieslen) y.splice(serieslen, y.length - serieslen); diff --git a/src/traces/histogram2d/sample_defaults.js b/src/traces/histogram2d/sample_defaults.js index 3217cc5e2ce..80575051d26 100644 --- a/src/traces/histogram2d/sample_defaults.js +++ b/src/traces/histogram2d/sample_defaults.js @@ -14,8 +14,8 @@ var handleBinDefaults = require('../histogram/bin_defaults'); module.exports = function handleSampleDefaults(traceIn, traceOut, coerce, layout) { - var x = coerce('x'), - y = coerce('y'); + var x = coerce('x'); + var y = coerce('y'); // we could try to accept x0 and dx, etc... // but that's a pretty weird use case. @@ -25,6 +25,8 @@ module.exports = function handleSampleDefaults(traceIn, traceOut, coerce, layout return; } + traceOut._length = Math.min(x.length, y.length); + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout); diff --git a/src/traces/mesh3d/defaults.js b/src/traces/mesh3d/defaults.js index eff7a082dfa..56fc6ae1821 100644 --- a/src/traces/mesh3d/defaults.js +++ b/src/traces/mesh3d/defaults.js @@ -86,4 +86,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } coerce('text'); + + // disable 1D transforms + // x/y/z should match lengths, and i/j/k should match as well, but + // the two sets have different lengths so transforms wouldn't work. + traceOut._length = null; }; diff --git a/src/traces/parcoords/calc.js b/src/traces/parcoords/calc.js index e7bc16ac6bd..d584e66225d 100644 --- a/src/traces/parcoords/calc.js +++ b/src/traces/parcoords/calc.js @@ -15,7 +15,7 @@ var wrap = require('../../lib/gup').wrap; module.exports = function calc(gd, trace) { var cs = !!trace.line.colorscale && Lib.isArrayOrTypedArray(trace.line.color); - var color = cs ? trace.line.color : constHalf(trace._commonLength); + var color = cs ? trace.line.color : constHalf(trace._length); var cscale = cs ? trace.line.colorscale : [[0, trace.line.color], [1, trace.line.color]]; if(hasColorscale(trace, 'line')) { diff --git a/src/traces/parcoords/defaults.js b/src/traces/parcoords/defaults.js index 1ef7817cbd4..07d1e97b0a8 100644 --- a/src/traces/parcoords/defaults.js +++ b/src/traces/parcoords/defaults.js @@ -26,7 +26,7 @@ function handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce) { // TODO: I think it would be better to keep showing lines beyond the last line color // but I'm not sure what color to give these lines - probably black or white // depending on the background color? - traceOut._commonLength = Math.min(traceOut._commonLength, lineColor.length); + traceOut._length = Math.min(traceOut._length, lineColor.length); } else { traceOut.line.color = defaultColor; @@ -84,7 +84,7 @@ function dimensionsDefaults(traceIn, traceOut) { dimensionsOut.push(dimensionOut); } - traceOut._commonLength = commonLength; + traceOut._length = commonLength; return dimensionsOut; } @@ -108,7 +108,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout // but we can't do this in dimensionsDefaults (yet?) because line.color can also // truncate for(var i = 0; i < dimensions.length; i++) { - if(dimensions[i].visible) dimensions[i]._length = traceOut._commonLength; + if(dimensions[i].visible) dimensions[i]._length = traceOut._length; } // make default font size 10px (default is 12), diff --git a/src/traces/parcoords/parcoords.js b/src/traces/parcoords/parcoords.js index af3f568a4c5..a8e04fbc03e 100644 --- a/src/traces/parcoords/parcoords.js +++ b/src/traces/parcoords/parcoords.js @@ -142,7 +142,7 @@ function model(layout, d, i) { color: lineColor.map(d3.scale.linear().domain(dimensionExtent({ values: lineColor, range: [line.cmin, line.cmax], - _length: trace._commonLength + _length: trace._length }))), blockLineCount: c.blockLineCount, canvasOverdrag: c.overdrag * c.canvasPixelRatio diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js index bfd94b9f169..46068e92717 100644 --- a/src/traces/pie/defaults.js +++ b/src/traces/pie/defaults.js @@ -18,20 +18,34 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } var coerceFont = Lib.coerceFont; + var len; var vals = coerce('values'); + var hasVals = Lib.isArrayOrTypedArray(vals); var labels = coerce('labels'); + if(Array.isArray(labels)) { + len = labels.length; + if(hasVals) len = Math.min(len, vals.length); + } if(!Array.isArray(labels)) { - if(!Lib.isArrayOrTypedArray(vals) || !vals.length) { + if(!hasVals) { // must have at least one of vals or labels traceOut.visible = false; return; } + len = vals.length; + coerce('label0'); coerce('dlabel'); } + if(!len) { + traceOut.visible = false; + return; + } + traceOut._length = len; + var lineWidth = coerce('marker.line.width'); if(lineWidth) coerce('marker.line.color'); diff --git a/src/traces/pointcloud/defaults.js b/src/traces/pointcloud/defaults.js index 49567e22eed..3014be6f6dd 100644 --- a/src/traces/pointcloud/defaults.js +++ b/src/traces/pointcloud/defaults.js @@ -40,4 +40,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor) { coerce('marker.sizemax'); coerce('marker.border.color', defaultColor); coerce('marker.border.arearatio'); + + // disable 1D transforms - that would defeat the purpose of this trace type, performance! + traceOut._length = null; }; diff --git a/src/traces/sankey/defaults.js b/src/traces/sankey/defaults.js index 173c88dd100..b65899a88e4 100644 --- a/src/traces/sankey/defaults.js +++ b/src/traces/sankey/defaults.js @@ -63,4 +63,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout if(traceOut.node.label.some(missing)) { Lib.warn('Some of the nodes are neither sources nor targets, they will not be displayed.'); } + + // disable 1D transforms - arrays here are 1D but their lengths/meanings + // don't match, between nodes and links + traceOut._length = null; }; diff --git a/src/traces/scatter3d/defaults.js b/src/traces/scatter3d/defaults.js index afbad02ecea..fe1bea443da 100644 --- a/src/traces/scatter3d/defaults.js +++ b/src/traces/scatter3d/defaults.js @@ -79,7 +79,7 @@ function handleXYZDefaults(traceIn, traceOut, coerce, layout) { if(x && y && z) { // TODO: what happens if one is missing? len = Math.min(x.length, y.length, z.length); - traceOut._xlength = traceOut._ylength = traceOut._zlength = len; + traceOut._length = traceOut._xlength = traceOut._ylength = traceOut._zlength = len; } return len; diff --git a/src/traces/scattercarpet/calc.js b/src/traces/scattercarpet/calc.js index 3574a8e8189..d3ea17ff846 100644 --- a/src/traces/scattercarpet/calc.js +++ b/src/traces/scattercarpet/calc.js @@ -28,7 +28,7 @@ module.exports = function calc(gd, trace) { trace.yaxis = carpet.yaxis; // make the calcdata array - var serieslen = trace.a.length; + var serieslen = trace._length; var cd = new Array(serieslen); var a, b; var needsCull = false; diff --git a/src/traces/scattercarpet/defaults.js b/src/traces/scattercarpet/defaults.js index bf500251b99..3bbc261d777 100644 --- a/src/traces/scattercarpet/defaults.js +++ b/src/traces/scattercarpet/defaults.js @@ -32,20 +32,16 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout traceOut.xaxis = 'x'; traceOut.yaxis = 'y'; - var a = coerce('a'), - b = coerce('b'), - len; - - len = Math.min(a.length, b.length); + var a = coerce('a'); + var b = coerce('b'); + var len = Math.min(a.length, b.length); if(!len) { traceOut.visible = false; return; } - // cut all data arrays down to same length - if(a && len < a.length) traceOut.a = a.slice(0, len); - if(b && len < b.length) traceOut.b = b.slice(0, len); + traceOut._length = len; coerce('text'); diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js index 8c4d4199438..b1dbce3df12 100644 --- a/src/traces/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -87,7 +87,7 @@ function convertStyle(gd, trace) { } function convertMarkerStyle(trace) { - var count = trace._length || trace._commonLength; + var count = trace._length; var optsIn = trace.marker; var optsOut = {}; var i; diff --git a/src/traces/splom/defaults.js b/src/traces/splom/defaults.js index aceab9290dd..dafe263d645 100644 --- a/src/traces/splom/defaults.js +++ b/src/traces/splom/defaults.js @@ -86,7 +86,7 @@ function handleDimensionsDefaults(traceIn, traceOut) { if(dimOut.visible) dimOut._length = commonLength; } - traceOut._commonLength = commonLength; + traceOut._length = commonLength; return dimensionsOut.length; } diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js index 3d9feff2f4b..c7f7f80b6a9 100644 --- a/src/traces/splom/index.js +++ b/src/traces/splom/index.js @@ -29,7 +29,7 @@ var TOO_MANY_POINTS = require('../scattergl/constants').TOO_MANY_POINTS; function calc(gd, trace) { var dimensions = trace.dimensions; - var commonLength = trace._commonLength; + var commonLength = trace._length; var stash = {}; var opts = {}; // 'c' for calculated, 'l' for linear, @@ -228,7 +228,7 @@ function plotOne(gd, cd0) { scene.unselectBatch = null; if(selectMode) { - var commonLength = trace._commonLength; + var commonLength = trace._length; if(!scene.selectBatch) { scene.selectBatch = []; diff --git a/src/traces/surface/defaults.js b/src/traces/surface/defaults.js index df78f075cc8..b7493f91ccb 100644 --- a/src/traces/surface/defaults.js +++ b/src/traces/surface/defaults.js @@ -96,6 +96,10 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout colorscaleDefaults( traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'c'} ); + + // disable 1D transforms - currently surface does NOT support column data like heatmap does + // you can use mesh3d for this use case, but not surface + traceOut._length = null; }; function mapLegacy(traceIn, oldAttr, newAttr) { diff --git a/src/traces/table/defaults.js b/src/traces/table/defaults.js index 989b435766d..7422adbbc2d 100644 --- a/src/traces/table/defaults.js +++ b/src/traces/table/defaults.js @@ -57,4 +57,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('cells.line.color'); coerce('cells.fill.color'); Lib.coerceFont(coerce, 'cells.font', Lib.extendFlat({}, layout.font)); + + // disable 1D transforms + traceOut._length = null; }; diff --git a/src/traces/violin/plot.js b/src/traces/violin/plot.js index 4b89227cad6..6c6203f47f6 100644 --- a/src/traces/violin/plot.js +++ b/src/traces/violin/plot.js @@ -159,9 +159,6 @@ module.exports = function plot(gd, plotinfo, cd) { bPosPxOffset = boxLineWidth; } - // do not draw whiskers on inner boxes - trace.whiskerwidth = 0; - boxPlot.plotBoxAndWhiskers(sel, {pos: posAxis, val: valAxis}, trace, { bPos: bPos, bdPos: bdPosScaled, diff --git a/src/transforms/aggregate.js b/src/transforms/aggregate.js index f346d44cd6b..00d5ac29b37 100644 --- a/src/transforms/aggregate.js +++ b/src/transforms/aggregate.js @@ -224,7 +224,10 @@ exports.calcTransform = function(gd, trace, opts) { var originalPointsAccessor = pointsAccessorFunction(trace.transforms, opts); - for(i = 0; i < groupArray.length; i++) { + var len = groupArray.length; + if(trace._length) len = Math.min(len, trace._length); + + for(i = 0; i < len; i++) { vi = groupArray[i]; groupIndex = groupIndices[vi]; if(groupIndex === undefined) { @@ -254,6 +257,8 @@ exports.calcTransform = function(gd, trace, opts) { enabled: true }); } + + trace._length = groupings.length; }; function aggregateOneArray(gd, trace, groupings, aggregation) { @@ -270,6 +275,12 @@ function aggregateOneArray(gd, trace, groupings, aggregation) { arrayOut[i] = func(arrayIn, groupings[i]); } targetNP.set(arrayOut); + + if(aggregation.func === 'count') { + // count does not depend on an input array, so it's likely not part of _arrayAttrs yet + // but after this transform it most definitely *is* an array attribute. + Lib.pushUnique(trace._arrayAttrs, attr); + } } function getAggregateFunction(opts, conversions) { diff --git a/src/transforms/filter.js b/src/transforms/filter.js index 3b5533b4e82..708bf5c7483 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -158,9 +158,13 @@ exports.calcTransform = function(gd, trace, opts) { if(!targetArray) return; var target = opts.target; + var len = targetArray.length; + if(trace._length) len = Math.min(len, trace._length); + var targetCalendar = opts.targetcalendar; var arrayAttrs = trace._arrayAttrs; + var preservegaps = opts.preservegaps; // even if you provide targetcalendar, if target is a string and there // is a calendar attribute matching target it will get used instead. @@ -184,7 +188,7 @@ exports.calcTransform = function(gd, trace, opts) { var initFn; var fillFn; - if(opts.preservegaps) { + if(preservegaps) { initFn = function(np) { originalArrays[np.astr] = Lib.extendDeep([], np.get()); np.set(new Array(len)); @@ -216,6 +220,7 @@ exports.calcTransform = function(gd, trace, opts) { forAllAttrs(fillFn, i); indexToPoints[index++] = originalPointsAccessor(i); } + else if(preservegaps) index++; } opts._indexToPoints = indexToPoints; diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index 36bcfc05dec..53d253d4d2e 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -73,7 +73,8 @@ exports.attributes = { 'For example, with `groups` set to *[\'a\', \'b\', \'a\', \'b\']*', 'and `styles` set to *[{target: \'a\', value: { marker: { color: \'red\' } }}]', 'marker points in group *\'a\'* will be drawn in red.' - ].join(' ') + ].join(' '), + _compareAsJSON: true }, editType: 'calc' }, @@ -115,9 +116,15 @@ exports.supplyDefaults = function(transformIn, traceOut, layout) { if(styleIn) { for(i = 0; i < styleIn.length; i++) { - styleOut[i] = {}; + var thisStyle = styleOut[i] = {}; Lib.coerce(styleIn[i], styleOut[i], exports.attributes.styles, 'target'); - Lib.coerce(styleIn[i], styleOut[i], exports.attributes.styles, 'value'); + var value = Lib.coerce(styleIn[i], styleOut[i], exports.attributes.styles, 'value'); + + // so that you can edit value in place and have Plotly.react notice it, or + // rebuild it every time and have Plotly.react NOT think it changed: + // use _compareAsJSON to say we should diff the _JSON_value + if(Lib.isPlainObject(value)) thisStyle.value = Lib.extendDeep({}, value); + else if(value) delete thisStyle.value; } } @@ -161,7 +168,8 @@ function transformOne(trace, state) { var groupNameObj; var opts = state.transform; - var groups = trace.transforms[state.transformIndex].groups; + var transformIndex = state.transformIndex; + var groups = trace.transforms[transformIndex].groups; var originalPointsAccessor = pointsAccessorFunction(trace.transforms, opts); if(!(Array.isArray(groups)) || groups.length === 0) { @@ -196,7 +204,10 @@ function transformOne(trace, state) { // Start with a deep extend that just copies array references. newTrace = newData[i] = Lib.extendDeepNoArrays({}, trace); newTrace._group = groupName; - newTrace.transforms[state.transformIndex]._indexToPoints = {}; + // helper function for when we need to push updates back to the input, + // outside of the normal restyle/relayout pathway, like filling in auto values + newTrace.updateStyle = styleUpdater(groupName, transformIndex); + newTrace.transforms[transformIndex]._indexToPoints = {}; var suppliedName = null; if(groupNameObj) { @@ -254,7 +265,7 @@ function transformOne(trace, state) { for(j = 0; j < len; j++) { newTrace = newData[indexLookup[groups[j]]]; - var indexToPoints = newTrace.transforms[state.transformIndex]._indexToPoints; + var indexToPoints = newTrace.transforms[transformIndex]._indexToPoints; indexToPoints[indexCnts[groups[j]]] = originalPointsAccessor(j); indexCnts[groups[j]]++; } @@ -272,3 +283,14 @@ function transformOne(trace, state) { return newData; } + +function styleUpdater(groupName, transformIndex) { + return function(trace, attr, value) { + Lib.keyedContainer( + trace, + 'transforms[' + transformIndex + '].styles', + 'target', + 'value.' + attr + ).set(String(groupName), value); + }; +} diff --git a/src/transforms/sort.js b/src/transforms/sort.js index 9ff49f9419a..146101509bd 100644 --- a/src/transforms/sort.js +++ b/src/transforms/sort.js @@ -84,10 +84,13 @@ exports.calcTransform = function(gd, trace, opts) { if(!targetArray) return; var target = opts.target; + var len = targetArray.length; + if(trace._length) len = Math.min(len, trace._length); + var arrayAttrs = trace._arrayAttrs; var d2c = Axes.getDataToCoordFunc(gd, trace, target, targetArray); - var indices = getIndices(opts, targetArray, d2c); + var indices = getIndices(opts, targetArray, d2c, len); var originalPointsAccessor = pointsAccessorFunction(trace.transforms, opts); var indexToPoints = {}; var i, j; @@ -109,31 +112,22 @@ exports.calcTransform = function(gd, trace, opts) { } opts._indexToPoints = indexToPoints; + trace._length = len; }; -function getIndices(opts, targetArray, d2c) { - var len = targetArray.length; +function getIndices(opts, targetArray, d2c, len) { + var sortedArray = new Array(len); var indices = new Array(len); + var i; - var sortedArray = targetArray - .slice() - .sort(getSortFunc(opts, d2c)); - - for(var i = 0; i < len; i++) { - var vTarget = targetArray[i]; - - for(var j = 0; j < len; j++) { - var vSorted = sortedArray[j]; + for(i = 0; i < len; i++) { + sortedArray[i] = {v: targetArray[i], i: i}; + } - if(vTarget === vSorted) { - indices[j] = i; + sortedArray.sort(getSortFunc(opts, d2c)); - // clear sortedArray item to get correct - // index of duplicate items (if any) - sortedArray[j] = null; - break; - } - } + for(i = 0; i < len; i++) { + indices[i] = sortedArray[i].i; } return indices; @@ -142,8 +136,8 @@ function getIndices(opts, targetArray, d2c) { function getSortFunc(opts, d2c) { switch(opts.order) { case 'ascending': - return function(a, b) { return d2c(a) - d2c(b); }; + return function(a, b) { return d2c(a.v) - d2c(b.v); }; case 'descending': - return function(a, b) { return d2c(b) - d2c(a); }; + return function(a, b) { return d2c(b.v) - d2c(a.v); }; } } diff --git a/test/image/baselines/violins.png b/test/image/baselines/fake_violins.png similarity index 100% rename from test/image/baselines/violins.png rename to test/image/baselines/fake_violins.png diff --git a/test/image/baselines/transforms.png b/test/image/baselines/transforms.png new file mode 100644 index 00000000000..b07f1ca235a Binary files /dev/null and b/test/image/baselines/transforms.png differ diff --git a/test/image/mocks/violins.json b/test/image/mocks/fake_violins.json similarity index 100% rename from test/image/mocks/violins.json rename to test/image/mocks/fake_violins.json diff --git a/test/image/mocks/transforms.json b/test/image/mocks/transforms.json new file mode 100644 index 00000000000..115b3259c4c --- /dev/null +++ b/test/image/mocks/transforms.json @@ -0,0 +1,71 @@ +{ + "data": [{ + "mode": "lines+markers", + "x": [1, -1, -2, 0, 1, 3, 3], + "y": [2, 1, 0, 1, 3, 4, 3], + "transforms": [{ + "type": "groupby", + "groups": ["a", "a", "b", "a", "b", "b", "a"], + "styles": [ + {"target": "a", "value": {"marker": {"color": "orange"}}}, + {"target": "b", "value": {"marker": {"color": "blue"}}} + ] + }, { + "type": "filter", + "target": "x", + "operation": ">=", + "value": 0, + "preservegaps": true + }], + "name": "Groupby+filter" + }, + { + "x": [1, 2, 3, 4, -3], + "y": [1.1, 2.2, 3.3, 4.4, 5.5], + "marker": { + "size": [0.3, 0.2, 0.1, 0.4, 0.5], + "sizeref": 0.01, + "color": [2, 4, 6, 10, 8], + "opacity": [0.9, 0.6, 0.2, 0.8, 1.0], + "line": { + "color": [2.2, 3.3, 4.4, 5.5, 1.1] + } + }, + "transforms": [{ + "type": "aggregate", + "groups": ["a", "b", "a", "a", "a"], + "aggregations": [ + {"target": "x", "func": "sum"}, + {"target": "y", "func": "avg"}, + {"target": "marker.size", "func": "min"}, + {"target": "marker.color", "func": "max"}, + {"target": "marker.line.color", "func": "last"}, + {"target": "marker.line.width", "func": "count"} + ] + }], + "name": "Aggregate" + }, + { + "x": [1, 2, 3, 4, 5, 6], + "y": [1, 4, 2, 6, 5, 3], + "transforms": [{ + "type": "sort", + "target": [1, 6, 2, 5, 3, 4] + }], + "name": "Sort" + }, + { + "x":[4, 5, 6, 4, 5, 6], + "y": [1, 1, 1, 2, 2, 2], + "marker": {"color": [1, 2, 3, -1, -2, -3], "size": 20}, + "mode": "lines+markers", + "transforms": [ + {"type": "groupby", "groups": [1, 1, 1, 2, 2, 2]} + ] + }], + "layout": { + "width": 600, + "height": 400, + "title": "Transforms" + } +} diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index 29cfd175833..5c34963bb8d 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -74,6 +74,23 @@ describe('Bar.supplyDefaults', function() { expect(traceOut.visible).toBe(false); }); + [{letter: 'y', counter: 'x'}, {letter: 'x', counter: 'y'}].forEach(function(spec) { + var l = spec.letter; + var c = spec.counter; + var c0 = c + '0'; + var dc = 'd' + c; + it('should be visible using ' + c0 + '/' + dc + ' if ' + c + ' is missing completely but ' + l + ' is present', function() { + traceIn = {}; + traceIn[l] = [1, 2]; + supplyDefaults(traceIn, traceOut, defaultColor, {}); + expect(traceOut.visible).toBe(undefined, l); // visible: true gets set above the module level + expect(traceOut._length).toBe(2, l); + expect(traceOut[c0]).toBe(0, c0); + expect(traceOut[dc]).toBe(1, dc); + expect(traceOut.orientation).toBe(l === 'x' ? 'h' : 'v', l); + }); + }); + it('should not set base, offset or width', function() { traceIn = { y: [1, 2, 3] diff --git a/test/jasmine/tests/choropleth_test.js b/test/jasmine/tests/choropleth_test.js index 9dd6cf79b02..15a807543fa 100644 --- a/test/jasmine/tests/choropleth_test.js +++ b/test/jasmine/tests/choropleth_test.js @@ -30,14 +30,26 @@ describe('Test choropleth', function() { traceOut = {}; }); - it('should slice z if it is longer than locations', function() { + it('should set _length based on locations and z but not slice', function() { traceIn = { locations: ['CAN', 'USA'], z: [1, 2, 3] }; + Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.z).toEqual([1, 2, 3]); + expect(traceOut.locations).toEqual(['CAN', 'USA']); + expect(traceOut._length).toBe(2); + + traceIn = { + locations: ['CAN', 'USA', 'ALB'], + z: [1, 2] + }; + Choropleth.supplyDefaults(traceIn, traceOut, defaultColor, layout); expect(traceOut.z).toEqual([1, 2]); + expect(traceOut.locations).toEqual(['CAN', 'USA', 'ALB']); + expect(traceOut._length).toBe(2); }); it('should make trace invisible if locations is not defined', function() { @@ -51,6 +63,7 @@ describe('Test choropleth', function() { it('should make trace invisible if z is not an array', function() { traceIn = { + locations: ['CAN', 'USA'], z: 'no gonna work' }; diff --git a/test/jasmine/tests/colorscale_test.js b/test/jasmine/tests/colorscale_test.js index 22771341d9e..2bfa115a75a 100644 --- a/test/jasmine/tests/colorscale_test.js +++ b/test/jasmine/tests/colorscale_test.js @@ -364,7 +364,7 @@ describe('Test colorscale:', function() { type: 'heatmap', z: [[0, -1.5], [-2, -10]], autocolorscale: true, - _input: {} + _input: {autocolorscale: true} }; z = [[0, -1.5], [-2, -10]]; calcColorscale(trace, z, '', 'z'); @@ -372,12 +372,25 @@ describe('Test colorscale:', function() { expect(trace.colorscale[5]).toEqual([1, 'rgb(220,220,220)']); }); + it('should set autocolorscale to false if it wasn\'t explicitly set true in input', function() { + trace = { + type: 'heatmap', + z: [[0, -1.5], [-2, -10]], + autocolorscale: true, + _input: {} + }; + z = [[0, -1.5], [-2, -10]]; + calcColorscale(trace, z, '', 'z'); + expect(trace.autocolorscale).toBe(false); + expect(trace.colorscale[5]).toEqual([1, 'rgb(220,220,220)']); + }); + it('should be Blues when the only numerical z <= -0.5', function() { trace = { type: 'heatmap', z: [['a', 'b'], [-0.5, 'd']], autocolorscale: true, - _input: {} + _input: {autocolorscale: true} }; z = [[undefined, undefined], [-0.5, undefined]]; calcColorscale(trace, z, '', 'z'); @@ -390,7 +403,7 @@ describe('Test colorscale:', function() { type: 'heatmap', z: [['a', 'b'], [0.5, 'd']], autocolorscale: true, - _input: {} + _input: {autocolorscale: true} }; z = [[undefined, undefined], [0.5, undefined]]; calcColorscale(trace, z, '', 'z'); @@ -404,7 +417,7 @@ describe('Test colorscale:', function() { z: [['a', 'b'], [0.5, 'd']], autocolorscale: true, reversescale: true, - _input: {} + _input: {autocolorscale: true} }; z = [[undefined, undefined], [0.5, undefined]]; calcColorscale(trace, z, '', 'z'); diff --git a/test/jasmine/tests/finance_test.js b/test/jasmine/tests/finance_test.js index 7d0f4c85a76..1814f6214c6 100644 --- a/test/jasmine/tests/finance_test.js +++ b/test/jasmine/tests/finance_test.js @@ -141,7 +141,7 @@ describe('finance charts defaults:', function() { expect(out._fullData[1]._fullInput.x).toBeDefined(); }); - it('should set visible to *false* when minimum supplied length is 0', function() { + it('should set visible to *false* when a component (other than x) is missing', function() { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc' }); trace0.close = undefined; @@ -160,6 +160,17 @@ describe('finance charts defaults:', function() { expect(visibilities).toEqual([false, false]); }); + it('should return visible: false if any data component is empty', function() { + ['ohlc', 'candlestick'].forEach(function(type) { + ['open', 'high', 'low', 'close', 'x'].forEach(function(attr) { + var trace = Lib.extendDeep({}, mock1, {type: type}); + trace[attr] = []; + var out = _supply([trace]); + expect(out._fullData[0].visible).toBe(false, type + ' - ' + attr); + }); + }); + }); + it('direction *showlegend* should be inherited from trace-wide *showlegend*', function() { var trace0 = Lib.extendDeep({}, mock0, { type: 'ohlc', diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index 8dbfd37920d..98e4a801d2e 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -175,6 +175,7 @@ describe('heatmap convertColumnXYZ', function() { var ya = makeMockAxis(); function checkConverted(trace, x, y, z) { + trace._length = Math.min(trace.x.length, trace.y.length, trace.z.length); convertColumnXYZ(trace, xa, ya, 'x', 'y', ['z']); expect(trace._x).toEqual(x); expect(trace._y).toEqual(y); @@ -216,7 +217,8 @@ describe('heatmap convertColumnXYZ', function() { x: [1, 1, 1, 2, 2, 2], y: [1, 2, 3, 1, 2, 3], z: [1, 2, 3, 4, 5, 6], - text: ['a', 'b', 'c', 'd', 'e', 'f', 'g'] + text: ['a', 'b', 'c', 'd', 'e', 'f', 'g'], + _length: 6 }; convertColumnXYZ(trace, xa, ya, 'x', 'y', ['z']); diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index 8c8167dc238..be23de532d4 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -3,6 +3,8 @@ var Lib = require('@src/lib'); var setConvert = require('@src/plots/cartesian/set_convert'); var supplyDefaults = require('@src/traces/histogram/defaults'); +var supplyDefaults2D = require('@src/traces/histogram2d/defaults'); +var supplyDefaults2DC = require('@src/traces/histogram2dcontour/defaults'); var calc = require('@src/traces/histogram/calc'); var getBinSpanLabelRound = require('@src/traces/histogram/bin_label_vals'); @@ -32,13 +34,13 @@ describe('Test histogram', function() { traceIn = { y: [] }; + traceOut = {}; supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); }); - it('should set visible to false when type is histogram2d and x or y are empty', function() { + it('should set visible to false when x or y is empty AND the other is present', function() { traceIn = { - type: 'histogram2d', x: [], y: [1, 2, 2] }; @@ -46,27 +48,44 @@ describe('Test histogram', function() { expect(traceOut.visible).toBe(false); traceIn = { - type: 'histogram2d', x: [1, 2, 2], y: [] }; + traceOut = {}; supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); + }); + it('should set visible to false when type is histogram2d(contour) and x or y are empty', function() { traceIn = { - type: 'histogram2d', x: [], + y: [1, 2, 2] + }; + supplyDefaults2D(traceIn, traceOut, '', {}); + expect(traceOut.visible).toBe(false); + + traceIn = { + x: [1, 2, 2], y: [] }; - supplyDefaults(traceIn, traceOut, '', {}); + traceOut = {}; + supplyDefaults2D(traceIn, traceOut, '', {}); + expect(traceOut.visible).toBe(false); + + traceIn = { + x: [], + y: [] + }; + traceOut = {}; + supplyDefaults2D(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); traceIn = { - type: 'histogram2dcontour', x: [], y: [1, 2, 2] }; - supplyDefaults(traceIn, traceOut, '', {}); + traceOut = {}; + supplyDefaults2DC(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); }); @@ -81,6 +100,7 @@ describe('Test histogram', function() { x: [1, 2, 2], y: [1, 2, 2] }; + traceOut = {}; supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.orientation).toBe('v'); }); @@ -112,6 +132,7 @@ describe('Test histogram', function() { traceIn = { x: [1, 2, 2] }; + traceOut = {}; supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.autobinx).toBeUndefined(); }); @@ -131,6 +152,7 @@ describe('Test histogram', function() { traceIn = { y: [1, 2, 2] }; + traceOut = {}; supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.autobiny).toBeUndefined(); }); diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index a216412c658..eecef5461e9 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -973,8 +973,8 @@ describe('legend interaction', function() { }).then(function() { // Verify the group names have been cleaned up: expect(gd.data[1].transforms[0].styles).toEqual([ - {target: 3}, - {target: 4} + {target: 3, value: {}}, + {target: 4, value: {}} ]); }).catch(fail).then(done); }); diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index e80da9be22d..62db6868a9f 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -328,7 +328,7 @@ describe('Test lib.js:', function() { expect(obj).toEqual({a: false, b: '', c: 0, d: NaN}); }); - it('should not remove data arrays or empty objects inside container arrays', function() { + it('should not remove arrays or empty objects inside container arrays', function() { var obj = { annotations: [{a: [1, 2, 3]}], c: [1, 2, 3], @@ -351,11 +351,17 @@ describe('Test lib.js:', function() { propS.set(null); // 'a' and 'c' are both potentially data arrays so we need to keep them - expect(obj).toEqual({annotations: [{a: []}], c: []}); + expect(obj).toEqual({ + annotations: [{a: []}], + c: [], + domain: [], + range: [], + shapes: [] + }); }); - it('should allow empty object sub-containers only in arrays', function() { + it('should allow empty object sub-containers', function() { var obj = {}, prop = np(obj, 'a[1].b.c'), // we never set a value into a[0] so it doesn't even get {} @@ -370,7 +376,7 @@ describe('Test lib.js:', function() { prop.set(null); expect(prop.get()).toBe(undefined); - expect(obj).toEqual({a: [undefined, {}]}); + expect(obj).toEqual({a: [undefined, {b: {}}]}); }); it('does not prune inside `args` arrays', function() { @@ -378,7 +384,7 @@ describe('Test lib.js:', function() { args = np(obj, 'args'); args.set([]); - expect(obj.args).toBeUndefined(); + expect(obj.args).toEqual([]); args.set([null]); expect(obj.args).toEqual([null]); @@ -1736,6 +1742,51 @@ describe('Test lib.js:', function() { }); describe('keyedContainer', function() { + describe('with no existing container', function() { + it('creates a named container only when setting a value', function() { + var container = {}; + var kCont = Lib.keyedContainer(container, 'styles'); + + expect(kCont.get('name1')).toBeUndefined(); + expect(container).toEqual({}); + + kCont.set('name1', null); + expect(container).toEqual({}); + + kCont.set('name1', 'value1'); + expect(container).toEqual({ + styles: [{name: 'name1', value: 'value1'}] + }); + expect(kCont.get('name1')).toBe('value1'); + expect(kCont.get('name2')).toBeUndefined(); + }); + }); + + describe('with no path', function() { + it('adds elements just like when there is a path', function() { + var arr = []; + var kCont = Lib.keyedContainer(arr); + + expect(kCont.get('name1')).toBeUndefined(); + expect(arr).toEqual([]); + + kCont.set('name1', null); + expect(arr).toEqual([]); + + kCont.set('name1', 'value1'); + expect(arr).toEqual([{name: 'name1', value: 'value1'}]); + expect(kCont.get('name1')).toBe('value1'); + expect(kCont.get('name2')).toBeUndefined(); + }); + + it('does not barf if the array is missing', function() { + var kCont = Lib.keyedContainer(); + kCont.set('name1', null); + kCont.set('name1', 'value1'); + expect(kCont.get('name1')).toBeUndefined(); + }); + }); + describe('with a filled container', function() { var container, carr; @@ -1950,7 +2001,7 @@ describe('Test lib.js:', function() { expect(container).toEqual({styles: [ {foo: 'name4', bar: {value: 'value1'}}, - {foo: 'name2'}, + {foo: 'name2', bar: {}}, {foo: 'name3', bar: {value: 'value3'}} ]}); @@ -1971,7 +2022,7 @@ describe('Test lib.js:', function() { carr.remove('name'); - expect(container.styles).toEqual([{foo: 'name', extra: 'data'}]); + expect(container.styles).toEqual([{foo: 'name', bar: {}, extra: 'data'}]); expect(carr.constructUpdate()).toEqual({ 'styles[0].bar.value': null, @@ -2006,7 +2057,7 @@ describe('Test lib.js:', function() { expect(container.styles).toEqual([ {foo: 'name1', bar: {extra: 'data'}}, - {foo: 'name2'}, + {foo: 'name2', bar: {}}, {foo: 'name3', bar: {value: 'value3', extra: 'data'}}, ]); @@ -2029,7 +2080,7 @@ describe('Test lib.js:', function() { carr.remove('name1'); expect(container.styles).toEqual([ - {foo: 'name1'}, + {foo: 'name1', bar: {}}, {foo: 'name2', bar: {value: 'value2', extra: 'data2'}}, ]); diff --git a/test/jasmine/tests/parcoords_test.js b/test/jasmine/tests/parcoords_test.js index 6baadcfbc6a..06a65999815 100644 --- a/test/jasmine/tests/parcoords_test.js +++ b/test/jasmine/tests/parcoords_test.js @@ -182,7 +182,7 @@ describe('parcoords initialization tests', function() { {values: [], visible: true}, {values: [1, 2], visible: false} // shouldn't be truncated to as visible: false ]}); - expect(fullTrace._commonLength).toBe(3); + expect(fullTrace._length).toBe(3); }); it('cleans up constraintrange', function() { diff --git a/test/jasmine/tests/pie_test.js b/test/jasmine/tests/pie_test.js index 91aeab8422d..d56dc6a2ee9 100644 --- a/test/jasmine/tests/pie_test.js +++ b/test/jasmine/tests/pie_test.js @@ -8,11 +8,57 @@ var failTest = require('../assets/fail_test'); var click = require('../assets/click'); var getClientPosition = require('../assets/get_client_position'); var mouseEvent = require('../assets/mouse_event'); +var supplyAllDefaults = require('../assets/supply_defaults'); var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelStyle = customAssertions.assertHoverLabelStyle; var assertHoverLabelContent = customAssertions.assertHoverLabelContent; + +describe('Pie defaults', function() { + function _supply(trace) { + var gd = { + data: [trace], + layout: {} + }; + + supplyAllDefaults(gd); + + return gd._fullData[0]; + } + + it('finds the minimum length of labels & values', function() { + var out = _supply({type: 'pie', labels: ['A', 'B'], values: [1, 2, 3]}); + expect(out._length).toBe(2); + + out = _supply({type: 'pie', labels: ['A', 'B', 'C'], values: [1, 2]}); + expect(out._length).toBe(2); + }); + + it('allows labels or values to be missing but not both', function() { + var out = _supply({type: 'pie', values: [1, 2]}); + expect(out.visible).toBe(true); + expect(out._length).toBe(2); + expect(out.label0).toBe(0); + expect(out.dlabel).toBe(1); + + out = _supply({type: 'pie', labels: ['A', 'B']}); + expect(out.visible).toBe(true); + expect(out._length).toBe(2); + + out = _supply({type: 'pie'}); + expect(out.visible).toBe(false); + }); + + it('is marked invisible if either labels or values is empty', function() { + var out = _supply({type: 'pie', labels: [], values: [1, 2]}); + expect(out.visible).toBe(false); + + out = _supply({type: 'pie', labels: ['A', 'B'], values: []}); + expect(out.visible).toBe(false); + }); +}); + describe('Pie traces:', function() { 'use strict'; diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 3302fe9a137..81325bb6a85 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -12,6 +12,7 @@ var helpers = require('@src/plot_api/helpers'); var editTypes = require('@src/plot_api/edit_types'); var annotations = require('@src/components/annotations'); var images = require('@src/components/images'); +var Registry = require('@src/registry'); var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); @@ -1053,14 +1054,14 @@ describe('Test plot api', function() { var mlcmax1 = 6; var mlcscl1 = 'greens'; - function check(auto, msg) { + function check(auto, autocolorscale, msg) { expect(gd.data[0].marker.cauto).toBe(auto, msg); expect(gd.data[0].marker.cmin).negateIf(auto).toBe(mcmin0); - expect(gd._fullData[0].marker.autocolorscale).toBe(auto, msg); + expect(gd._fullData[0].marker.autocolorscale).toBe(autocolorscale, msg); expect(gd.data[0].marker.colorscale).toEqual(auto ? autocscale : mcscl0); expect(gd.data[1].marker.line.cauto).toBe(auto, msg); expect(gd.data[1].marker.line.cmax).negateIf(auto).toBe(mlcmax1); - expect(gd._fullData[1].marker.line.autocolorscale).toBe(auto, msg); + expect(gd._fullData[1].marker.line.autocolorscale).toBe(autocolorscale, msg); expect(gd.data[1].marker.line.colorscale).toEqual(auto ? autocscale : mlcscl1); } @@ -1069,28 +1070,30 @@ describe('Test plot api', function() { {y: [2, 1], mode: 'markers', marker: {line: {width: 2, color: [3, 4]}}} ]) .then(function() { - check(true, 'initial'); + // autocolorscale is actually true after supplyDefaults, but during calc it's set + // to false when we push the resulting colorscale back to the input container + check(true, false, 'initial'); return Plotly.restyle(gd, {'marker.cmin': mcmin0, 'marker.colorscale': mcscl0}, null, [0]); }) .then(function() { return Plotly.restyle(gd, {'marker.line.cmax': mlcmax1, 'marker.line.colorscale': mlcscl1}, null, [1]); }) .then(function() { - check(false, 'set min/max/scale'); + check(false, false, 'set min/max/scale'); return Plotly.restyle(gd, {'marker.cauto': true, 'marker.autocolorscale': true}, null, [0]); }) .then(function() { return Plotly.restyle(gd, {'marker.line.cauto': true, 'marker.line.autocolorscale': true}, null, [1]); }) .then(function() { - check(true, 'reset'); + check(true, true, 'reset'); return Queue.undo(gd); }) .then(function() { return Queue.undo(gd); }) .then(function() { - check(false, 'undo'); + check(false, false, 'undo'); }) .catch(failTest) .then(done); @@ -2955,6 +2958,190 @@ describe('Test plot api', function() { .then(done); }); + function aggregatedPie(i) { + var labels = i <= 1 ? + ['A', 'B', 'A', 'C', 'A', 'B', 'C', 'A', 'B', 'C', 'A'] : + ['X', 'Y', 'Z', 'Z', 'Y', 'Z', 'X', 'Z', 'Y', 'Z', 'X']; + var trace = { + type: 'pie', + values: [4, 1, 4, 4, 1, 4, 4, 2, 1, 1, 15], + labels: labels, + transforms: [{ + type: 'aggregate', + groups: labels, + aggregations: [{target: 'values', func: 'sum'}] + }] + }; + return { + data: [trace], + layout: { + datarevision: i, + colorway: ['red', 'orange', 'yellow', 'green', 'blue', 'violet'] + } + }; + } + + var aggPie1CD = [[ + {v: 26, label: 'A', color: 'red', i: 0}, + {v: 9, label: 'C', color: 'orange', i: 2}, + {v: 6, label: 'B', color: 'yellow', i: 1} + ]]; + + var aggPie2CD = [[ + {v: 23, label: 'X', color: 'red', i: 0}, + {v: 15, label: 'Z', color: 'orange', i: 2}, + {v: 3, label: 'Y', color: 'yellow', i: 1} + ]]; + + function aggregatedScatter(i) { + return { + data: [{ + x: [1, 2, 3, 4, 6, 5], + y: [2, 1, 3, 5, 6, 4], + transforms: [{ + type: 'aggregate', + groups: [1, -1, 1, -1, 1, -1], + aggregations: i > 1 ? [{func: 'last', target: 'x'}] : [] + }] + }], + layout: {daterevision: i + 10} + }; + } + + var aggScatter1CD = [[ + {x: 1, y: 2, i: 0}, + {x: 2, y: 1, i: 1} + ]]; + + var aggScatter2CD = [[ + {x: 6, y: 2, i: 0}, + {x: 5, y: 1, i: 1} + ]]; + + function aggregatedParcoords(i) { + return { + data: [{ + type: 'parcoords', + dimensions: [ + {label: 'A', values: [1, 2, 3, 4]}, + {label: 'B', values: [4, 3, 2, 1]} + ], + transforms: i ? [{ + type: 'aggregate', + groups: [1, 2, 1, 2], + aggregations: [ + {target: 'dimensions[0].values', func: i > 1 ? 'avg' : 'first'}, + {target: 'dimensions[1].values', func: i > 1 ? 'first' : 'avg'} + ] + }] : + [] + }] + }; + } + + var aggParcoords0Vals = [[1, 2, 3, 4], [4, 3, 2, 1]]; + var aggParcoords1Vals = [[1, 2], [3, 2]]; + var aggParcoords2Vals = [[2, 3], [4, 3]]; + + function checkCalcData(expectedCD) { + return function() { + expect(gd.calcdata.length).toBe(expectedCD.length); + expectedCD.forEach(function(expectedCDi, i) { + var cdi = gd.calcdata[i]; + expect(cdi.length).toBe(expectedCDi.length, i); + expectedCDi.forEach(function(expectedij, j) { + expect(cdi[j]).toEqual(jasmine.objectContaining(expectedij)); + }); + }); + }; + } + + function checkValues(expectedVals) { + return function() { + expect(gd._fullData.length).toBe(1); + var dims = gd._fullData[0].dimensions; + expect(dims.length).toBe(expectedVals.length); + expectedVals.forEach(function(expected, i) { + expect(dims[i].values).toEqual(expected); + }); + }; + } + + function reactTo(fig) { + return function() { return Plotly.react(gd, fig); }; + } + + it('can change pie aggregations', function(done) { + Plotly.newPlot(gd, aggregatedPie(1)) + .then(checkCalcData(aggPie1CD)) + + .then(reactTo(aggregatedPie(2))) + .then(checkCalcData(aggPie2CD)) + + .then(reactTo(aggregatedPie(1))) + .then(checkCalcData(aggPie1CD)) + .catch(failTest) + .then(done); + }); + + it('can change scatter aggregations', function(done) { + Plotly.newPlot(gd, aggregatedScatter(1)) + .then(checkCalcData(aggScatter1CD)) + + .then(reactTo(aggregatedScatter(2))) + .then(checkCalcData(aggScatter2CD)) + + .then(reactTo(aggregatedScatter(1))) + .then(checkCalcData(aggScatter1CD)) + .catch(failTest) + .then(done); + }); + + it('can change parcoords aggregations', function(done) { + Plotly.newPlot(gd, aggregatedParcoords(0)) + .then(checkValues(aggParcoords0Vals)) + + .then(reactTo(aggregatedParcoords(1))) + .then(checkValues(aggParcoords1Vals)) + + .then(reactTo(aggregatedParcoords(2))) + .then(checkValues(aggParcoords2Vals)) + + .then(reactTo(aggregatedParcoords(0))) + .then(checkValues(aggParcoords0Vals)) + + .catch(failTest) + .then(done); + }); + + it('can change type with aggregations', function(done) { + Plotly.newPlot(gd, aggregatedScatter(1)) + .then(checkCalcData(aggScatter1CD)) + + .then(reactTo(aggregatedPie(1))) + .then(checkCalcData(aggPie1CD)) + + .then(reactTo(aggregatedParcoords(1))) + .then(checkValues(aggParcoords1Vals)) + + .then(reactTo(aggregatedScatter(1))) + .then(checkCalcData(aggScatter1CD)) + + .then(reactTo(aggregatedParcoords(2))) + .then(checkValues(aggParcoords2Vals)) + + .then(reactTo(aggregatedPie(2))) + .then(checkCalcData(aggPie2CD)) + + .then(reactTo(aggregatedScatter(2))) + .then(checkCalcData(aggScatter2CD)) + + .then(reactTo(aggregatedParcoords(0))) + .then(checkValues(aggParcoords0Vals)) + .catch(failTest) + .then(done); + }); + it('can change frames without redrawing', function(done) { var data = [{y: [1, 2, 3]}]; var layout = {}; @@ -2980,7 +3167,7 @@ describe('Test plot api', function() { .then(done); }); - var mockList = [ + var svgMockList = [ ['1', require('@mocks/1.json')], ['4', require('@mocks/4.json')], ['5', require('@mocks/5.json')], @@ -2999,12 +3186,6 @@ describe('Test plot api', function() { ['cheater_smooth', require('@mocks/cheater_smooth.json')], ['finance_style', require('@mocks/finance_style.json')], ['geo_first', require('@mocks/geo_first.json')], - ['gl2d_line_dash', require('@mocks/gl2d_line_dash.json')], - ['gl2d_parcoords_2', require('@mocks/gl2d_parcoords_2.json')], - ['gl2d_pointcloud-basic', require('@mocks/gl2d_pointcloud-basic.json')], - ['gl3d_world-cals', require('@mocks/gl3d_world-cals.json')], - ['gl3d_set-ranges', require('@mocks/gl3d_set-ranges.json')], - ['glpolar_style', require('@mocks/glpolar_style.json')], ['layout_image', require('@mocks/layout_image.json')], ['layout-colorway', require('@mocks/layout-colorway.json')], ['polar_categories', require('@mocks/polar_categories.json')], @@ -3012,12 +3193,15 @@ describe('Test plot api', function() { ['range_selector_style', require('@mocks/range_selector_style.json')], ['range_slider_multiple', require('@mocks/range_slider_multiple.json')], ['sankey_energy', require('@mocks/sankey_energy.json')], + ['scattercarpet', require('@mocks/scattercarpet.json')], + ['shapes', require('@mocks/shapes.json')], ['splom_iris', require('@mocks/splom_iris.json')], ['table_wrapped_birds', require('@mocks/table_wrapped_birds.json')], ['ternary_fill', require('@mocks/ternary_fill.json')], ['text_chart_arrays', require('@mocks/text_chart_arrays.json')], + ['transforms', require('@mocks/transforms.json')], ['updatemenus', require('@mocks/updatemenus.json')], - ['violins', require('@mocks/violins.json')], + ['violin_side-by-side', require('@mocks/violin_side-by-side.json')], ['world-cals', require('@mocks/world-cals.json')], ['typed arrays', { data: [{ @@ -3027,70 +3211,142 @@ describe('Test plot api', function() { }] ]; - mockList.forEach(function(mockSpec) { - it('can redraw "' + mockSpec[0] + '" with no changes as a noop', function(done) { - var mock = mockSpec[1]; - var initialJson; - - function fullJson() { - var out = JSON.parse(Plotly.Plots.graphJson({ - data: gd._fullData, - layout: gd._fullLayout - })); - - // TODO: does it matter that ax.tick0/dtick/range and zmin/zmax - // are often not regenerated without a calc step? - // in as far as editor and others rely on _full, I think the - // answer must be yes, but I'm not sure about within plotly.js - [ - 'xaxis', 'xaxis2', 'xaxis3', 'xaxis4', 'xaxis5', - 'yaxis', 'yaxis2', 'yaxis3', 'yaxis4', - 'zaxis' - ].forEach(function(axName) { - var ax = out.layout[axName]; + var glMockList = [ + ['gl2d_heatmapgl', require('@mocks/gl2d_heatmapgl.json')], + ['gl2d_line_dash', require('@mocks/gl2d_line_dash.json')], + ['gl2d_parcoords_2', require('@mocks/gl2d_parcoords_2.json')], + ['gl2d_pointcloud-basic', require('@mocks/gl2d_pointcloud-basic.json')], + ['gl3d_annotations', require('@mocks/gl3d_annotations.json')], + ['gl3d_set-ranges', require('@mocks/gl3d_set-ranges.json')], + ['gl3d_world-cals', require('@mocks/gl3d_world-cals.json')], + ['glpolar_style', require('@mocks/glpolar_style.json')], + ]; + + // make sure we've included every trace type in this suite + var typesTested = {}; + var itemType; + for(itemType in Registry.modules) { typesTested[itemType] = 0; } + for(itemType in Registry.transformsRegistry) { typesTested[itemType] = 0; } + + // Not really being supported... This isn't part of the main bundle, and it's pretty broken, + // but it gets registered and used by a couple of the gl2d tests. + delete typesTested.contourgl; + + function _runReactMock(mockSpec, done) { + var mock = mockSpec[1]; + var initialJson; + + function fullJson() { + var out = JSON.parse(Plotly.Plots.graphJson({ + data: gd._fullData.map(function(trace) { return trace._fullInput; }), + layout: gd._fullLayout + })); + + // TODO: does it matter that ax.tick0/dtick/range and zmin/zmax + // are often not regenerated without a calc step? + // in as far as editor and others rely on _full, I think the + // answer must be yes, but I'm not sure about within plotly.js + [ + 'xaxis', 'xaxis2', 'xaxis3', 'xaxis4', 'xaxis5', + 'yaxis', 'yaxis2', 'yaxis3', 'yaxis4', + 'zaxis' + ].forEach(function(axName) { + var ax = out.layout[axName]; + if(ax) { + delete ax.dtick; + delete ax.tick0; + + // TODO this one I don't understand and can't reproduce + // in the dashboard but it's needed here? + delete ax.range; + } + if(out.layout.scene) { + ax = out.layout.scene[axName]; if(ax) { delete ax.dtick; delete ax.tick0; - - // TODO this one I don't understand and can't reproduce - // in the dashboard but it's needed here? + // TODO: this is the only one now that uses '_input_' + key + // as a hack to tell Plotly.react to ignore changes. + // Can we kill this? delete ax.range; } - if(out.layout.scene) { - ax = out.layout.scene[axName]; - if(ax) { - delete ax.dtick; - delete ax.tick0; - // TODO: this is the only one now that uses '_input_' + key - // as a hack to tell Plotly.react to ignore changes. - // Can we kill this? - delete ax.range; - } - } - }); - out.data.forEach(function(trace) { - if(trace.type === 'contourcarpet') { - delete trace.zmin; - delete trace.zmax; - } - }); + } + }); + out.data.forEach(function(trace) { + if(trace.type === 'contourcarpet') { + delete trace.zmin; + delete trace.zmax; + } + }); + + return out; + } - return out; + // Make sure we define `_length` in every trace *in supplyDefaults*. + // This is only relevant for traces that *have* a 1D concept of length, + // and in addition to simplifying calc/plot logic later on, ths serves + // as a signal to transforms about how they should operate. For traces + // that do NOT have a 1D length, `_length` should be `null`. + var mockGD = Lib.extendDeep({}, mock); + supplyAllDefaults(mockGD); + expect(mockGD._fullData.length).not.toBeLessThan((mock.data || []).length, mockSpec[0]); + mockGD._fullData.forEach(function(trace, i) { + var len = trace._length; + if(trace.visible !== false && len !== null) { + expect(typeof len).toBe('number', mockSpec[0] + ' trace ' + i + ': type=' + trace.type); } - Plotly.newPlot(gd, mock) - .then(countPlots) - .then(function() { - initialJson = fullJson(); - return Plotly.react(gd, mock); - }) - .then(function() { - expect(fullJson()).toEqual(initialJson); - countCalls({}); - }) - .catch(failTest) - .then(done); + typesTested[trace.type]++; + + if(trace.transforms) { + trace.transforms.forEach(function(transform) { + typesTested[transform.type]++; + }); + } }); + + Plotly.newPlot(gd, mock) + .then(countPlots) + .then(function() { + initialJson = fullJson(); + + return Plotly.react(gd, mock); + }) + .then(function() { + expect(fullJson()).toEqual(initialJson); + countCalls({}); + }) + .catch(failTest) + .then(done); + } + + svgMockList.forEach(function(mockSpec) { + it('can redraw "' + mockSpec[0] + '" with no changes as a noop (svg mocks)', function(done) { + _runReactMock(mockSpec, done); + }); + }); + + glMockList.forEach(function(mockSpec) { + it('can redraw "' + mockSpec[0] + '" with no changes as a noop (gl mocks)', function(done) { + _runReactMock(mockSpec, done); + }); + }); + + it('@noCI can redraw scattermapbox with no changes as a noop', function(done) { + Plotly.setPlotConfig({ + mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN + }); + + _runReactMock(['scattermapbox', require('@mocks/mapbox_bubbles-text.json')], done); + }); + + // since CI breaks up gl/svg types, and drops scattermapbox, this test won't work there + // but I should hope that if someone is doing something as major as adding a new type, + // they'll run the full test suite locally! + it('@noCI tested every trace & transform type at least once', function() { + for(var itemType in typesTested) { + expect(typesTested[itemType]).toBeGreaterThan(0, itemType + ' was not tested'); + } }); }); diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index 61b3ae29497..27f1d08e848 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -276,12 +276,12 @@ describe('Test Plots', function() { describe('Plots.supplyTransformDefaults', function() { it('should accept an empty layout when transforms present', function() { - var traceOut = {}; + var traceOut = {y: [1], _length: 1}; Plots.supplyTransformDefaults({}, traceOut, { _globalTransforms: [{ type: 'filter'}] }); - // This isn't particularly interseting. More relevant is that + // This isn't particularly interesting. More relevant is that // the above supplyTransformDefaults call didn't fail due to // missing transformModules data. expect(traceOut.transforms.length).toEqual(1); diff --git a/test/jasmine/tests/plotschema_test.js b/test/jasmine/tests/plotschema_test.js index 452ede89a77..5e1c440e088 100644 --- a/test/jasmine/tests/plotschema_test.js +++ b/test/jasmine/tests/plotschema_test.js @@ -115,7 +115,7 @@ describe('plot schema', function() { var valObject = valObjects[attr.valType], opts = valObject.requiredOpts .concat(valObject.otherOpts) - .concat(['valType', 'description', 'role', 'editType', 'impliedEdits']); + .concat(['valType', 'description', 'role', 'editType', 'impliedEdits', '_compareAsJSON']); Object.keys(attr).forEach(function(key) { expect(opts.indexOf(key) !== -1).toBe(true, key, attr); diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js index ec295c7d9d9..e6fe705d710 100644 --- a/test/jasmine/tests/scatter_test.js +++ b/test/jasmine/tests/scatter_test.js @@ -79,6 +79,22 @@ describe('Test scatter', function() { expect(traceOut.visible).toBe(false); }); + [{letter: 'y', counter: 'x'}, {letter: 'x', counter: 'y'}].forEach(function(spec) { + var l = spec.letter; + var c = spec.counter; + var c0 = c + '0'; + var dc = 'd' + c; + it('should be visible using ' + c0 + '/' + dc + ' if ' + c + ' is missing completely but ' + l + ' is present', function() { + traceIn = {}; + traceIn[spec.letter] = [1, 2]; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(undefined, l); // visible: true gets set above the module level + expect(traceOut._length).toBe(2, l); + expect(traceOut[c0]).toBe(0, c0); + expect(traceOut[dc]).toBe(1, dc); + }); + }); + it('should correctly assign \'hoveron\' default', function() { traceIn = { x: [1, 2, 3], diff --git a/test/jasmine/tests/scatterternary_test.js b/test/jasmine/tests/scatterternary_test.js index be24bbb5814..932f03c9485 100644 --- a/test/jasmine/tests/scatterternary_test.js +++ b/test/jasmine/tests/scatterternary_test.js @@ -146,6 +146,21 @@ describe('scatterternary defaults', function() { expect(traceOut._length).toBe(1); }); + it('is set visible: false if a, b, or c is empty', function() { + var trace0 = { + a: [1, 2], + b: [2, 1], + c: [2, 2] + }; + + ['a', 'b', 'c'].forEach(function(letter) { + traceIn = Lib.extendDeep({}, trace0); + traceIn[letter] = []; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false, letter); + }); + }); + it('should include \'name\' in \'hoverinfo\' default if multi trace graph', function() { traceIn = { type: 'scatterternary', diff --git a/test/jasmine/tests/sliders_test.js b/test/jasmine/tests/sliders_test.js index d959667ecee..5d317fcf8a9 100644 --- a/test/jasmine/tests/sliders_test.js +++ b/test/jasmine/tests/sliders_test.js @@ -100,17 +100,20 @@ describe('sliders defaults', function() { method: 'relayout', label: 'Label #1', value: 'label-1', - execute: true + execute: true, + args: [] }, { method: 'update', label: 'Label #2', value: 'Label #2', - execute: true + execute: true, + args: [] }, { method: 'animate', label: 'step-2', value: 'lacks-label', - execute: true + execute: true, + args: [] }]); }); diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 357f5b943e2..6151eed8668 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -82,7 +82,7 @@ describe('Test splom trace defaults:', function() { }); var fullTrace = gd._fullData[0]; - expect(fullTrace._commonLength).toBe(3, 'common length'); + expect(fullTrace._length).toBe(3, 'common length'); expect(fullTrace.dimensions[0]._length).toBe(3, 'dim 0 length'); expect(fullTrace.dimensions[1]._length).toBe(3, 'dim 1 length'); expect(fullTrace.xaxes).toEqual(['x', 'x2']); diff --git a/test/jasmine/tests/transform_aggregate_test.js b/test/jasmine/tests/transform_aggregate_test.js index 0f7932b2c48..9eb5e227afc 100644 --- a/test/jasmine/tests/transform_aggregate_test.js +++ b/test/jasmine/tests/transform_aggregate_test.js @@ -257,6 +257,46 @@ describe('aggregate', function() { expect(traceOut.marker.color).toBeCloseToArray([Math.sqrt(1 / 3), 0], 5); }); + it('handles ragged data - extra groups are ignored', function() { + Plotly.newPlot(gd, [{ + x: [1, 1, 2, 2, 1, 3, 4], + y: [1, 2, 3, 4, 5], // shortest array controls all + transforms: [{ + type: 'aggregate', + groups: [1, 2, 1, 1, 1, 3, 3, 4, 4, 5], + aggregations: [ + {target: 'x', func: 'mode'}, + {target: 'y', func: 'median'}, + ] + }] + }]); + + var traceOut = gd._fullData[0]; + + expect(traceOut.x).toEqual([2, 1]); + expect(traceOut.y).toBeCloseToArray([3.5, 2], 5); + }); + + it('handles ragged data - groups is the shortest, others are ignored', function() { + Plotly.newPlot(gd, [{ + x: [1, 1, 2, 2, 1, 3, 4], + y: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + transforms: [{ + type: 'aggregate', + groups: [1, 2, 1, 1, 1], // shortest array controls all + aggregations: [ + {target: 'x', func: 'mode'}, + {target: 'y', func: 'median'}, + ] + }] + }]); + + var traceOut = gd._fullData[0]; + + expect(traceOut.x).toEqual([2, 1]); + expect(traceOut.y).toBeCloseToArray([3.5, 2], 5); + }); + it('links fullData aggregations to userData via _index', function() { Plotly.newPlot(gd, [{ x: [1, 2, 3, 4, 5], diff --git a/test/jasmine/tests/transform_filter_test.js b/test/jasmine/tests/transform_filter_test.js index 7dc565a1fad..4a9eb4e27c5 100644 --- a/test/jasmine/tests/transform_filter_test.js +++ b/test/jasmine/tests/transform_filter_test.js @@ -365,6 +365,7 @@ describe('filter transforms calc:', function() { expect(out[0].x).toEqual([undefined, undefined, undefined, undefined, 1, 2, 3]); expect(out[0].y).toEqual([undefined, undefined, undefined, undefined, 2, 3, 1]); expect(out[0].marker.color).toEqual([undefined, undefined, undefined, undefined, 0.2, 0.3, 0.4]); + expect(out[0].transforms[0]._indexToPoints).toEqual({4: [4], 5: [5], 6: [6]}); }); it('two filter transforms with `preservegaps: true` should commute', function() { @@ -391,6 +392,11 @@ describe('filter transforms calc:', function() { var out1 = _transform([Lib.extendDeep({}, base, { transforms: [transform1, transform0] })]); + // _indexToPoints differs in the first transform but matches in the second + expect(out0[0].transforms[0]._indexToPoints).toEqual({3: [3], 4: [4], 5: [5], 6: [6]}); + expect(out1[0].transforms[0]._indexToPoints).toEqual({0: [0], 1: [1], 2: [2], 3: [3], 4: [4]}); + expect(out0[0].transforms[1]._indexToPoints).toEqual({3: [3], 4: [4]}); + expect(out1[0].transforms[1]._indexToPoints).toEqual({3: [3], 4: [4]}); ['x', 'y', 'ids', 'marker.color', 'marker.size'].forEach(function(k) { var v0 = Lib.nestedProperty(out0[0], k).get(); @@ -424,6 +430,12 @@ describe('filter transforms calc:', function() { transforms: [transform1, transform0] })]); + // _indexToPoints differs in the first transform but matches in the second + expect(out0[0].transforms[0]._indexToPoints).toEqual({0: [3], 1: [4], 2: [5], 3: [6]}); + expect(out1[0].transforms[0]._indexToPoints).toEqual({0: [0], 1: [1], 2: [2], 3: [3], 4: [4]}); + expect(out0[0].transforms[1]._indexToPoints).toEqual({0: [3], 1: [4]}); + expect(out1[0].transforms[1]._indexToPoints).toEqual({0: [3], 1: [4]}); + ['x', 'y', 'ids', 'marker.color', 'marker.size'].forEach(function(k) { var v0 = Lib.nestedProperty(out0[0], k).get(); var v1 = Lib.nestedProperty(out1[0], k).get(); @@ -431,7 +443,7 @@ describe('filter transforms calc:', function() { }); }); - it('two filter transforms with different `preservegaps` values should not necessary commute', function() { + it('two filter transforms with different `preservegaps` values should not necessarily commute', function() { var transform0 = { type: 'filter', preservegaps: true, @@ -872,6 +884,36 @@ describe('filter transforms calc:', function() { expect(out[0].transforms[0].target).toEqual([0, 0, 0]); }); + it('with ragged items - longer target', function() { + var out = _transform([Lib.extendDeep({}, _base, { + transforms: [{ + target: [1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1], + operation: '{}', + value: 0 + }] + })]); + + _assert(out, [-2, 0, 2], [3, 1, 3], [0.3, 0.1, 0.3]); + expect(out[0].transforms[0].target).toEqual([0, 0, 0]); + }); + + it('with ragged items - longer data', function() { + var out = _transform([Lib.extendDeep({}, _base, { + x: _base.x.concat(_base.x), + y: _base.y.concat(_base.y), + ids: _base.ids.concat(['a1', 'a2', 'a3', 'a4']), + marker: {color: _base.marker.color.concat(_base.marker.color)}, + transforms: [{ + target: [1, 1, 0, 0, 1, 0, 1], + operation: '{}', + value: 0 + }] + })]); + + _assert(out, [-2, 0, 2], [3, 1, 3], [0.3, 0.1, 0.3]); + expect(out[0].transforms[0].target).toEqual([0, 0, 0]); + }); + it('with categorical items and *{}*', function() { var out = _transform([Lib.extendDeep({}, _base, { transforms: [{ diff --git a/test/jasmine/tests/transform_groupby_test.js b/test/jasmine/tests/transform_groupby_test.js index 5f6c564be5d..b9170ca6c01 100644 --- a/test/jasmine/tests/transform_groupby_test.js +++ b/test/jasmine/tests/transform_groupby_test.js @@ -182,6 +182,70 @@ describe('groupby', function() { .then(done); }); + it('Plotly.react 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] + ); + + gd.data[0].marker.opacity = 0.4; + // contrived test of relinkPrivateKeys + // we'll have to do better if we refactor it to opt-in instead of catchall + gd._fullData[0].marker._boo = 'here I am'; + return Plotly.react(gd, gd.data, gd.layout); + }).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); + expect(gd._fullData[0].marker._boo).toBe('here I am'); + + gd.data[0].marker.opacity = 1; + return Plotly.react(gd, gd.data, gd.layout); + }).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); + + // edit just affects the first group + gd.data[0].transforms[0].styles[0].value.marker.color = 'green'; + return Plotly.react(gd, gd.data, gd.layout); + }).then(function() { + assertStyle(dims, + ['rgb(0, 128, 0)', 'rgb(0, 0, 255)'], + [1, 1] + ); + + expect(gd._fullData[0].marker.opacity).toEqual(1); + expect(gd._fullData[1].marker.opacity).toEqual(1); + + // edit just affects the second group + gd.data[0].transforms[0].styles[1].value.marker.color = 'red'; + return Plotly.react(gd, gd.data, gd.layout); + }).then(function() { + assertStyle(dims, + ['rgb(0, 128, 0)', 'rgb(255, 0, 0)'], + [1, 1] + ); + }) + .catch(failTest) + .then(done); + }); + it('Plotly.extendTraces should work', function(done) { var data = Lib.extendDeep([], mockData0); diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index 9ad0c331c87..71b1d16a001 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -45,6 +45,17 @@ describe('general transforms:', function() { expect(traceOut.transforms).toEqual([{}]); }); + it('does not transform traces with no length', function() { + traceIn = { + y: [], + transforms: [{}] + }; + + traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + + expect(traceOut.transforms).toBeUndefined(); + }); + it('supplyTraceDefaults should supply the transform defaults', function() { traceIn = { y: [2, 1, 2], @@ -198,11 +209,12 @@ describe('user-defined transforms:', function() { var transformIn = { type: 'fake' }; var transformOut = {}; - var calledSupplyDefaults = false; - var calledTransform = false; - var calledSupplyLayoutDefaults = false; + var calledSupplyDefaults = 0; + var calledTransform = 0; + var calledSupplyLayoutDefaults = 0; var dataIn = [{ + y: [1, 2, 3], transforms: [transformIn] }]; @@ -212,10 +224,19 @@ describe('user-defined transforms:', function() { var transitionData = {}; function assertSupplyDefaultsArgs(_transformIn, traceOut, _layout) { - expect(_transformIn).toBe(transformIn); + if(!calledSupplyDefaults) { + expect(_transformIn).toBe(transformIn); + } + else { + // second supplyDefaults call has _module attached + expect(_transformIn).toEqual(jasmine.objectContaining({ + type: 'fake', + _module: jasmine.objectContaining({name: 'fake'}) + })); + } expect(_layout).toBe(fullLayout); - calledSupplyDefaults = true; + calledSupplyDefaults++; return transformOut; } @@ -227,7 +248,7 @@ describe('user-defined transforms:', function() { expect(opts.layout).toBe(layout); expect(opts.fullLayout).toBe(fullLayout); - calledTransform = true; + calledTransform++; return dataOut; } @@ -238,7 +259,7 @@ describe('user-defined transforms:', function() { expect(_fullData).toBe(fullData); expect(_transitionData).toBe(transitionData); - calledSupplyLayoutDefaults = true; + calledSupplyLayoutDefaults++; } var fakeTransformModule = { @@ -254,9 +275,9 @@ describe('user-defined transforms:', function() { Plots.supplyDataDefaults(dataIn, fullData, layout, fullLayout); Plots.supplyLayoutModuleDefaults(layout, fullLayout, fullData, transitionData); delete Plots.transformsRegistry.fake; - expect(calledSupplyDefaults).toBe(true); - expect(calledTransform).toBe(true); - expect(calledSupplyLayoutDefaults).toBe(true); + expect(calledSupplyDefaults).toBe(2); + expect(calledTransform).toBe(1); + expect(calledSupplyLayoutDefaults).toBe(1); }); }); diff --git a/test/jasmine/tests/transform_sort_test.js b/test/jasmine/tests/transform_sort_test.js index 8f2e37d080c..2f6bfdfcc62 100644 --- a/test/jasmine/tests/transform_sort_test.js +++ b/test/jasmine/tests/transform_sort_test.js @@ -214,12 +214,14 @@ describe('Test sort transform calc:', function() { expect(out[0].ids).toEqual(['n1', 'n0']); expect(out[0].marker.color).toEqual([0.2, 0.1]); expect(out[0].marker.size).toEqual([20, 10]); + expect(out[0]._length).toBe(2); expect(out[1].x).toEqual([-2, -1]); expect(out[1].y).toEqual([1, 2]); expect(out[1].ids).toEqual(['n0', 'n1']); expect(out[1].marker.color).toEqual([0.1, 0.2]); expect(out[1].marker.size).toEqual([10, 20]); + expect(out[1]._length).toBe(2); }); it('should truncate transformed arrays to target array length (long target case)', function() { @@ -235,17 +237,19 @@ describe('Test sort transform calc:', function() { transforms: [{ target: 'text' }] })]); - expect(out[0].x).toEqual([1, undefined, -2, 3, undefined, -1, 1, undefined, -2, 0, undefined]); - expect(out[0].y).toEqual([1, undefined, 3, 3, undefined, 2, 2, undefined, 1, 1, undefined]); - expect(out[0].ids).toEqual(['p3', undefined, 'n2', 'p2', undefined, 'n1', 'p1', undefined, 'n0', 'z', undefined]); - expect(out[0].marker.color).toEqual([0.4, undefined, 0.3, 0.3, undefined, 0.2, 0.2, undefined, 0.1, 0.1, undefined]); - expect(out[0].marker.size).toEqual([10, undefined, 5, 0, undefined, 20, 6, undefined, 10, 1, undefined]); - - expect(out[1].x).toEqual([-2, -1, -2, 0, 1, 3, 1, undefined, undefined]); - expect(out[1].y).toEqual([1, 2, 3, 1, 2, 3, 1, undefined, undefined]); - expect(out[1].ids).toEqual(['n0', 'n1', 'n2', 'z', 'p1', 'p2', 'p3', undefined, undefined]); - expect(out[1].marker.color).toEqual([0.1, 0.2, 0.3, 0.1, 0.2, 0.3, 0.4, undefined, undefined]); - expect(out[1].marker.size).toEqual([10, 20, 5, 1, 6, 0, 10, undefined, undefined]); + expect(out[0].x).toEqual([1, -2, 3, -1, 1, -2, 0]); + expect(out[0].y).toEqual([1, 3, 3, 2, 2, 1, 1]); + expect(out[0].ids).toEqual(['p3', 'n2', 'p2', 'n1', 'p1', 'n0', 'z']); + expect(out[0].marker.color).toEqual([0.4, 0.3, 0.3, 0.2, 0.2, 0.1, 0.1]); + expect(out[0].marker.size).toEqual([10, 5, 0, 20, 6, 10, 1]); + expect(out[0]._length).toBe(7); + + expect(out[1].x).toEqual([-2, -1, -2, 0, 1, 3, 1]); + expect(out[1].y).toEqual([1, 2, 3, 1, 2, 3, 1]); + expect(out[1].ids).toEqual(['n0', 'n1', 'n2', 'z', 'p1', 'p2', 'p3']); + expect(out[1].marker.color).toEqual([0.1, 0.2, 0.3, 0.1, 0.2, 0.3, 0.4]); + expect(out[1].marker.size).toEqual([10, 20, 5, 1, 6, 0, 10]); + expect(out[1]._length).toBe(7); }); });