From bbe3533874afafd9f5092700e4bb65406e1f61bc Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 18 Apr 2018 11:39:17 -0400 Subject: [PATCH 01/26] remove no longer needed hack for finance in findArrayAttributes thanks @VeraZab! --- src/plot_api/plot_schema.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index f5681c44a3a..77d764192ca 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -221,17 +221,6 @@ exports.findArrayAttributes = function(trace) { } } - // 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; }; From ed2476526f6031ce9b85234ce4e279d728c2e693 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 18 Apr 2018 14:59:24 -0400 Subject: [PATCH 02/26] _commonLength -> _length so aggregations and other common routines only have to look at _length --- src/plots/cartesian/type_defaults.js | 2 +- src/traces/parcoords/calc.js | 2 +- src/traces/parcoords/defaults.js | 6 +++--- src/traces/parcoords/parcoords.js | 2 +- src/traces/scattergl/convert.js | 2 +- src/traces/splom/defaults.js | 2 +- src/traces/splom/index.js | 4 ++-- test/jasmine/tests/parcoords_test.js | 2 +- test/jasmine/tests/splom_test.js | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) 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/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/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/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/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']); From fcc459d985f0a3a80826da8df51b2daf150a4634 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 18 Apr 2018 16:49:52 -0400 Subject: [PATCH 03/26] fix #2508, fix #2470 - problems with Plotly.react and aggregate transforms also makes transforms work with parcoords (and by extension splom) by having findArrayAttributes dig into container arrays --- src/plot_api/plot_api.js | 8 +- src/plot_api/plot_schema.js | 45 +++++-- src/plots/plots.js | 30 +++-- src/transforms/aggregate.js | 2 + test/jasmine/tests/plot_api_test.js | 184 ++++++++++++++++++++++++++++ 5 files changed, 243 insertions(+), 26 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 9ea68b9f5e4..ce7e8cb6364 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, 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 diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index 77d764192ca..1191f8d4d72 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,33 +193,51 @@ 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); } } } diff --git a/src/plots/plots.js b/src/plots/plots.js index 05aa915e073..51a750e9ff4 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -274,7 +274,7 @@ var extraFormatKeys = [ // gd._fullLayout._transformModules // is a list of all the transform modules invoked. // -plots.supplyDefaults = function(gd) { +plots.supplyDefaults = function(gd, skipCalcUpdate) { var oldFullLayout = gd._fullLayout || {}; if(oldFullLayout._skipDefaults) { @@ -458,24 +458,28 @@ 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(!skipCalcUpdate && 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) { + if(cd0.trace._hasCalcTransform) { + remapTransformedArrays(cd0, newTrace); + } else { + cd0.trace = newTrace; + } + } + } +}; + /** * Make a container for collecting subplots we need to display. * diff --git a/src/transforms/aggregate.js b/src/transforms/aggregate.js index f346d44cd6b..8fee0b47971 100644 --- a/src/transforms/aggregate.js +++ b/src/transforms/aggregate.js @@ -254,6 +254,8 @@ exports.calcTransform = function(gd, trace, opts) { enabled: true }); } + + trace._length = groupings.length; }; function aggregateOneArray(gd, trace, groupings, aggregation) { diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 3302fe9a137..176175de080 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -2955,6 +2955,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 reactWith(fig) { + return function() { return Plotly.react(gd, fig); }; + } + + it('can change pie aggregations', function(done) { + Plotly.newPlot(gd, aggregatedPie(1)) + .then(checkCalcData(aggPie1CD)) + + .then(reactWith(aggregatedPie(2))) + .then(checkCalcData(aggPie2CD)) + + .then(reactWith(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(reactWith(aggregatedScatter(2))) + .then(checkCalcData(aggScatter2CD)) + + .then(reactWith(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(reactWith(aggregatedParcoords(1))) + .then(checkValues(aggParcoords1Vals)) + + .then(reactWith(aggregatedParcoords(2))) + .then(checkValues(aggParcoords2Vals)) + + .then(reactWith(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(reactWith(aggregatedPie(1))) + .then(checkCalcData(aggPie1CD)) + + .then(reactWith(aggregatedParcoords(1))) + .then(checkValues(aggParcoords1Vals)) + + .then(reactWith(aggregatedScatter(1))) + .then(checkCalcData(aggScatter1CD)) + + .then(reactWith(aggregatedParcoords(2))) + .then(checkValues(aggParcoords2Vals)) + + .then(reactWith(aggregatedPie(2))) + .then(checkCalcData(aggPie2CD)) + + .then(reactWith(aggregatedScatter(2))) + .then(checkCalcData(aggScatter2CD)) + + .then(reactWith(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 = {}; From 7044a13f2b19ab7ca5557d6c301d62597008a220 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 19 Apr 2018 10:22:28 -0400 Subject: [PATCH 04/26] standardize transforms handling of _length --- src/transforms/aggregate.js | 5 ++- src/transforms/filter.js | 7 ++- src/transforms/sort.js | 11 +++-- .../jasmine/tests/transform_aggregate_test.js | 40 +++++++++++++++++ test/jasmine/tests/transform_filter_test.js | 44 ++++++++++++++++++- test/jasmine/tests/transform_sort_test.js | 26 ++++++----- 6 files changed, 115 insertions(+), 18 deletions(-) diff --git a/src/transforms/aggregate.js b/src/transforms/aggregate.js index 8fee0b47971..cd772999839 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) { 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/sort.js b/src/transforms/sort.js index 9ff49f9419a..941db61ea6b 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,14 +112,14 @@ 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 indices = new Array(len); var sortedArray = targetArray - .slice() + .slice(0, len) .sort(getSortFunc(opts, d2c)); for(var i = 0; i < len; i++) { 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_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); }); }); From 88b7b43a5b5b11d7d5c38d02a1206f8b38959118 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 19 Apr 2018 10:26:15 -0400 Subject: [PATCH 05/26] :racehorse: refactor sort transform from O(n^2) to O(n) plus whatever Array.sort is of course... O(n log(n)) or whatever --- src/transforms/sort.js | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/transforms/sort.js b/src/transforms/sort.js index 941db61ea6b..146101509bd 100644 --- a/src/transforms/sort.js +++ b/src/transforms/sort.js @@ -116,27 +116,18 @@ exports.calcTransform = function(gd, trace, opts) { }; function getIndices(opts, targetArray, d2c, len) { + var sortedArray = new Array(len); var indices = new Array(len); + var i; - var sortedArray = targetArray - .slice(0, len) - .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; @@ -145,8 +136,8 @@ function getIndices(opts, targetArray, d2c, len) { 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); }; } } From cf4c9c36f3c471aea0038e9ed519425960528757 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 20 Apr 2018 13:06:36 -0400 Subject: [PATCH 06/26] heatmap&carpet/has_columns -> Lib.is1D heatmap and carpet used opposite meanings here... --- src/lib/index.js | 1 + src/lib/is_array.js | 18 ++++++++++++++---- src/traces/carpet/ab_defaults.js | 2 -- src/traces/carpet/calc.js | 6 +++--- src/traces/carpet/has_columns.js | 15 --------------- src/traces/contour/defaults.js | 3 +-- src/traces/contourcarpet/calc.js | 5 +++-- src/traces/contourcarpet/defaults.js | 2 +- src/traces/heatmap/calc.js | 3 +-- src/traces/heatmap/defaults.js | 7 +++---- src/traces/heatmap/has_columns.js | 15 --------------- src/traces/heatmap/xyz_defaults.js | 7 +++---- 12 files changed, 30 insertions(+), 54 deletions(-) delete mode 100644 src/traces/carpet/has_columns.js delete mode 100644 src/traces/heatmap/has_columns.js diff --git a/src/lib/index.js b/src/lib/index.js index d21cc0398a6..8ed364ea39d 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.is1D = isArrayModule.is1D; var coerceModule = require('./coerce'); lib.valObjectMeta = coerceModule.valObjectMeta; diff --git a/src/lib/is_array.js b/src/lib/is_array.js index 28b58d0d1a0..8c78244d9ff 100644 --- a/src/lib/is_array.js +++ b/src/lib/is_array.js @@ -18,10 +18,20 @@ 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); +} + +function is1D(a) { + return !isArrayOrTypedArray(a[0]); +} -exports.isArrayOrTypedArray = function(a) { - return Array.isArray(a) || exports.isTypedArray(a); +module.exports = { + isTypedArray: isTypedArray, + isArrayOrTypedArray: isArrayOrTypedArray, + is1D: is1D }; 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..e24c7a5b1ac 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 is1D = require('../../lib').is1D; 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 && is1D(x)) cols.push('x'); + if(y && is1D(y)) cols.push('y'); if(cols.length) { convertColumnData(trace, aax, bax, 'a', 'b', cols); 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/contour/defaults.js b/src/traces/contour/defaults.js index c8be70030a9..72d87d14d61 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.is1D(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/contourcarpet/calc.js b/src/traces/contourcarpet/calc.js index f5f68ea5f4b..fdd9490a834 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 is1D = require('../../lib').is1D; + 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(is1D(trace.z)) convertColumnData(trace, aax, bax, 'a', 'b', ['z']); a = trace._a = trace._a || trace.a; b = trace._b = trace._b || trace.b; diff --git a/src/traces/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js index 235d448d244..0b831463454 100644 --- a/src/traces/contourcarpet/defaults.js +++ b/src/traces/contourcarpet/defaults.js @@ -56,7 +56,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var isConstraint = (coerce('contours.type') === 'constraint'); // Unimplemented: - // coerce('connectgaps', hasColumns(traceOut)); + // coerce('connectgaps', Lib.is1D(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/heatmap/calc.js b/src/traces/heatmap/calc.js index 7364a8bc88d..ea3c7da891a 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.is1D(zIn)) { convertColumnData(trace, xa, ya, 'x', 'y', ['z']); x = trace._x; y = trace._y; diff --git a/src/traces/heatmap/defaults.js b/src/traces/heatmap/defaults.js index 2c65a0be3ed..48579e892a4 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.is1D(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..9deeafee374 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,7 +22,7 @@ module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout, x if(z === undefined || !z.length) return 0; - if(hasColumns(traceIn)) { + if(Lib.is1D(traceIn.z)) { x = coerce(xName); y = coerce(yName); @@ -76,7 +75,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; } From e84d4b9e09b680c382e1dbd79b89b8e4323ed5c4 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 20 Apr 2018 17:44:48 -0400 Subject: [PATCH 07/26] close #1410 - yes, stop pruning in nestedProperty --- src/lib/nested_property.js | 101 +++++++---------------------- test/jasmine/tests/legend_test.js | 4 +- test/jasmine/tests/lib_test.js | 24 ++++--- test/jasmine/tests/sliders_test.js | 9 ++- 4 files changed, 45 insertions(+), 93 deletions(-) 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/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..1196e7d5c6f 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]); @@ -1950,7 +1956,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 +1977,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 +2012,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 +2035,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/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: [] }]); }); From b436d521ec87e06bb71a58fe40052cac98c6d340 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 21 Apr 2018 23:24:32 -0400 Subject: [PATCH 08/26] ensure every trace defines _length in supplyDefaults, and abort transforms if it's falsy --- src/plots/plots.js | 5 +++ src/traces/box/calc.js | 3 +- src/traces/box/defaults.js | 13 +++++-- src/traces/carpet/defaults.js | 9 +++-- src/traces/carpet/xy_defaults.js | 16 ++++++++- src/traces/choropleth/calc.js | 2 +- src/traces/choropleth/defaults.js | 15 ++------ src/traces/contourcarpet/defaults.js | 1 + src/traces/heatmap/convert_column_xyz.js | 22 ++++-------- src/traces/heatmap/xyz_defaults.js | 8 +++-- src/traces/histogram/defaults.js | 23 +++++++----- src/traces/histogram2d/calc.js | 2 +- src/traces/histogram2d/sample_defaults.js | 6 ++-- src/traces/mesh3d/defaults.js | 3 ++ src/traces/pie/defaults.js | 11 +++++- src/traces/pointcloud/defaults.js | 3 ++ src/traces/sankey/defaults.js | 4 +++ src/traces/scatter3d/defaults.js | 2 +- src/traces/surface/defaults.js | 4 +++ src/traces/table/defaults.js | 3 ++ test/jasmine/tests/choropleth_test.js | 15 +++++++- test/jasmine/tests/heatmap_test.js | 4 ++- test/jasmine/tests/histogram_test.js | 15 ++++---- test/jasmine/tests/plot_api_test.js | 16 +++++++++ test/jasmine/tests/plots_test.js | 4 +-- test/jasmine/tests/transform_multi_test.js | 41 ++++++++++++++++------ 26 files changed, 175 insertions(+), 75 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 51a750e9ff4..d23fee8c266 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1178,6 +1178,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..c898a86b2cd 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'); + if(hasX) { + len = Math.min(x.length, y.length); + } + else { + coerce('x0'); + len = y.length; + } } else if(x && x.length) { 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/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/xy_defaults.js b/src/traces/carpet/xy_defaults.js index 4e046f8eb53..0853a402b65 100644 --- a/src/traces/carpet/xy_defaults.js +++ b/src/traces/carpet/xy_defaults.js @@ -9,11 +9,25 @@ 'use strict'; +var is1D = require('../../lib').is1D; + 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 || is1D(x)) && (!hasY || is1D(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/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js index 0b831463454..384655becc3 100644 --- a/src/traces/contourcarpet/defaults.js +++ b/src/traces/contourcarpet/defaults.js @@ -69,5 +69,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } } else { traceOut._defaultColor = defaultColor; + traceOut._length = null; } }; diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js index 909fff986a8..7ac4a5acbb0 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.is1D(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/xyz_defaults.js b/src/traces/heatmap/xyz_defaults.js index 9deeafee374..9dfa4e68681 100644 --- a/src/traces/heatmap/xyz_defaults.js +++ b/src/traces/heatmap/xyz_defaults.js @@ -27,7 +27,9 @@ module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout, x 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); @@ -37,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) { diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js index 79b3dcb9985..210c991c9c8 100644 --- a/src/traces/histogram/defaults.js +++ b/src/traces/histogram/defaults.js @@ -22,8 +22,10 @@ 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 hasX = x && x.length; + var hasY = y && y.length; var cumulative = coerce('cumulative.enabled'); if(cumulative) { @@ -33,22 +35,27 @@ 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', (hasY && !hasX) ? 'h' : 'v'); + var sampleLetter = orientation === 'v' ? 'x' : 'y'; + var aggLetter = orientation === 'v' ? 'y' : 'x'; + var sample = traceOut[sampleLetter]; - if(!(sample && sample.length)) { + var len = (hasX && hasY) ? Math.min(x.length && y.length) : (sample || []).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..ee37fcf88e5 100644 --- a/src/traces/mesh3d/defaults.js +++ b/src/traces/mesh3d/defaults.js @@ -86,4 +86,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } coerce('text'); + + // disable 1D transforms + traceOut._length = null; }; diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js index bfd94b9f169..dc653c83399 100644 --- a/src/traces/pie/defaults.js +++ b/src/traces/pie/defaults.js @@ -18,19 +18,28 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } var coerceFont = Lib.coerceFont; + var len; var vals = coerce('values'); + var hasVals = Lib.isArrayOrTypedArray(vals) && vals.length; var labels = coerce('labels'); + if(Array.isArray(labels) && labels.length) { + 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'); } + 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/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/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/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..480f1864e92 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'); @@ -36,37 +38,34 @@ describe('Test histogram', function() { 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 type is histogram2d(contour) and x or y are empty', function() { traceIn = { type: 'histogram2d', x: [], y: [1, 2, 2] }; - supplyDefaults(traceIn, traceOut, '', {}); + supplyDefaults2D(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); traceIn = { - type: 'histogram2d', x: [1, 2, 2], y: [] }; - supplyDefaults(traceIn, traceOut, '', {}); + supplyDefaults2D(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); traceIn = { - type: 'histogram2d', x: [], y: [] }; - supplyDefaults(traceIn, traceOut, '', {}); + supplyDefaults2D(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); traceIn = { - type: 'histogram2dcontour', x: [], y: [1, 2, 2] }; - supplyDefaults(traceIn, traceOut, '', {}); + supplyDefaults2DC(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); }); diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 176175de080..2194c476897 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -3262,10 +3262,26 @@ describe('Test plot api', function() { 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() { 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/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); }); }); From 2a41f9ed9184014a543004ae6a5042334ccf8fec Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 24 Apr 2018 10:56:21 -0400 Subject: [PATCH 09/26] react+transforms PR review edits - is1D -> isArray1D - improved comments - opts object instead of confusing boolean to Plots.supplyDefaults - merge & simplify remapTransformedArrays - hasX --- src/lib/index.js | 2 +- src/lib/is_array.js | 12 +++- src/plot_api/plot_api.js | 2 +- src/plots/plots.js | 88 ++++++++++++------------ src/traces/box/defaults.js | 2 +- src/traces/carpet/calc.js | 6 +- src/traces/carpet/xy_defaults.js | 4 +- src/traces/contour/defaults.js | 2 +- src/traces/contourcarpet/calc.js | 4 +- src/traces/contourcarpet/defaults.js | 2 +- src/traces/heatmap/calc.js | 2 +- src/traces/heatmap/convert_column_xyz.js | 2 +- src/traces/heatmap/defaults.js | 2 +- src/traces/heatmap/xyz_defaults.js | 2 +- src/traces/mesh3d/defaults.js | 2 + 15 files changed, 71 insertions(+), 63 deletions(-) diff --git a/src/lib/index.js b/src/lib/index.js index 8ed364ea39d..75354fd4972 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -30,7 +30,7 @@ lib.ensureArray = require('./ensure_array'); var isArrayModule = require('./is_array'); lib.isTypedArray = isArrayModule.isTypedArray; lib.isArrayOrTypedArray = isArrayModule.isArrayOrTypedArray; -lib.is1D = isArrayModule.is1D; +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 8c78244d9ff..b8c5e1ae47c 100644 --- a/src/lib/is_array.js +++ b/src/lib/is_array.js @@ -26,12 +26,20 @@ function isArrayOrTypedArray(a) { return Array.isArray(a) || isTypedArray(a); } -function is1D(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]); } module.exports = { isTypedArray: isTypedArray, isArrayOrTypedArray: isArrayOrTypedArray, - is1D: is1D + isArray1D: isArray1D }; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index ce7e8cb6364..03bad4cf8e4 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2270,7 +2270,7 @@ exports.react = function(gd, data, layout, config) { // "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, true); + Plots.supplyDefaults(gd, {skipUpdateCalc: true}); var newFullData = gd._fullData; var newFullLayout = gd._fullLayout; diff --git a/src/plots/plots.js b/src/plots/plots.js index d23fee8c266..0e8aecf2452 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, skipCalcUpdate) { +/* + * 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, + * 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, opts) { + var skipUpdateCalc = opts && opts.skipUpdateCalc; var oldFullLayout = gd._fullLayout || {}; if(oldFullLayout._skipDefaults) { @@ -458,7 +468,7 @@ plots.supplyDefaults = function(gd, skipCalcUpdate) { } // update object references in calcdata - if(!skipCalcUpdate && oldCalcdata.length === newFullData.length) { + if(!skipUpdateCalc && oldCalcdata.length === newFullData.length) { plots.supplyDefaultsUpdateCalc(oldCalcdata, newFullData); } @@ -471,11 +481,18 @@ plots.supplyDefaultsUpdateCalc = function(oldCalcdata, newFullData) { var newTrace = newFullData[i]; var cd0 = oldCalcdata[i][0]; if(cd0 && cd0.trace) { - if(cd0.trace._hasCalcTransform) { - remapTransformedArrays(cd0, newTrace); - } else { - cd0.trace = newTrace; + 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; } } }; @@ -522,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 diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js index c898a86b2cd..dc75d7f1266 100644 --- a/src/traces/box/defaults.js +++ b/src/traces/box/defaults.js @@ -51,7 +51,7 @@ function handleSampleDefaults(traceIn, traceOut, coerce, layout) { coerce('x0'); len = y.length; } - } else if(x && x.length) { + } else if(hasX) { defaultOrientation = 'h'; coerce('y0'); len = x.length; diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index e24c7a5b1ac..f9b0e65b648 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -9,7 +9,7 @@ 'use strict'; var Axes = require('../../plots/cartesian/axes'); -var is1D = require('../../lib').is1D; +var isArray1D = require('../../lib').isArray1D; var cheaterBasis = require('./cheater_basis'); var arrayMinmax = require('./array_minmax'); var calcGridlines = require('./calc_gridlines'); @@ -29,8 +29,8 @@ module.exports = function calc(gd, trace) { var x = trace.x; var y = trace.y; var cols = []; - if(x && is1D(x)) cols.push('x'); - if(y && is1D(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/xy_defaults.js b/src/traces/carpet/xy_defaults.js index 0853a402b65..dbf9252c175 100644 --- a/src/traces/carpet/xy_defaults.js +++ b/src/traces/carpet/xy_defaults.js @@ -9,7 +9,7 @@ 'use strict'; -var is1D = require('../../lib').is1D; +var isArray1D = require('../../lib').isArray1D; module.exports = function handleXYDefaults(traceIn, traceOut, coerce) { var x = coerce('x'); @@ -20,7 +20,7 @@ module.exports = function handleXYDefaults(traceIn, traceOut, coerce) { traceOut._cheater = !x; - if((!hasX || is1D(x)) && (!hasY || is1D(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); diff --git a/src/traces/contour/defaults.js b/src/traces/contour/defaults.js index 72d87d14d61..f995de309f5 100644 --- a/src/traces/contour/defaults.js +++ b/src/traces/contour/defaults.js @@ -35,7 +35,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('text'); var isConstraint = (coerce('contours.type') === 'constraint'); - coerce('connectgaps', Lib.is1D(traceOut.z)); + 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/contourcarpet/calc.js b/src/traces/contourcarpet/calc.js index fdd9490a834..162a5bb2d9b 100644 --- a/src/traces/contourcarpet/calc.js +++ b/src/traces/contourcarpet/calc.js @@ -9,7 +9,7 @@ 'use strict'; var colorscaleCalc = require('../../components/colorscale/calc'); -var is1D = require('../../lib').is1D; +var isArray1D = require('../../lib').isArray1D; var convertColumnData = require('../heatmap/convert_column_xyz'); var clean2dArray = require('../heatmap/clean_2d_array'); @@ -70,7 +70,7 @@ function heatmappishCalc(gd, trace) { aax._minDtick = 0; bax._minDtick = 0; - if(is1D(trace.z)) 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; diff --git a/src/traces/contourcarpet/defaults.js b/src/traces/contourcarpet/defaults.js index 384655becc3..a833016beca 100644 --- a/src/traces/contourcarpet/defaults.js +++ b/src/traces/contourcarpet/defaults.js @@ -56,7 +56,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var isConstraint = (coerce('contours.type') === 'constraint'); // Unimplemented: - // coerce('connectgaps', Lib.is1D(traceOut.z)); + // 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/heatmap/calc.js b/src/traces/heatmap/calc.js index ea3c7da891a..0ff85f1a24d 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -58,7 +58,7 @@ module.exports = function calc(gd, trace) { } else { var zIn = trace.z; - if(Lib.is1D(zIn)) { + 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 7ac4a5acbb0..57638ace45e 100644 --- a/src/traces/heatmap/convert_column_xyz.js +++ b/src/traces/heatmap/convert_column_xyz.js @@ -17,7 +17,7 @@ module.exports = function convertColumnData(trace, ax1, ax2, var1Name, var2Name, var col1 = trace[var1Name].slice(0, colLen); var col2 = trace[var2Name].slice(0, colLen); var textCol = trace.text; - var hasColumnText = (textCol !== undefined && Lib.is1D(textCol)); + var hasColumnText = (textCol !== undefined && Lib.isArray1D(textCol)); var col1Calendar = trace[var1Name + 'calendar']; var col2Calendar = trace[var2Name + 'calendar']; diff --git a/src/traces/heatmap/defaults.js b/src/traces/heatmap/defaults.js index 48579e892a4..442d5beed07 100644 --- a/src/traces/heatmap/defaults.js +++ b/src/traces/heatmap/defaults.js @@ -32,7 +32,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout handleStyleDefaults(traceIn, traceOut, coerce, layout); - coerce('connectgaps', Lib.is1D(traceOut.z) && (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/xyz_defaults.js b/src/traces/heatmap/xyz_defaults.js index 9dfa4e68681..c457abfbfb4 100644 --- a/src/traces/heatmap/xyz_defaults.js +++ b/src/traces/heatmap/xyz_defaults.js @@ -22,7 +22,7 @@ module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout, x if(z === undefined || !z.length) return 0; - if(Lib.is1D(traceIn.z)) { + if(Lib.isArray1D(traceIn.z)) { x = coerce(xName); y = coerce(yName); diff --git a/src/traces/mesh3d/defaults.js b/src/traces/mesh3d/defaults.js index ee37fcf88e5..56fc6ae1821 100644 --- a/src/traces/mesh3d/defaults.js +++ b/src/traces/mesh3d/defaults.js @@ -88,5 +88,7 @@ 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; }; From 965bcfb0d20717ba2aa2e2b1f4cda2d049ff4f08 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 24 Apr 2018 16:41:51 -0400 Subject: [PATCH 10/26] fixes and test for checklist in #2508 --- src/plot_api/plot_api.js | 7 --- src/plot_api/plot_schema.js | 3 + src/plots/attributes.js | 8 +++ test/jasmine/tests/transform_groupby_test.js | 64 ++++++++++++++++++++ 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 03bad4cf8e4..5e3139c19a8 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2467,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; diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index 1191f8d4d72..b7487eeca0b 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -266,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/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); From 6fae22917ea84b2faf2f87dc0602d1f1baa9e685 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 24 Apr 2018 18:48:46 -0400 Subject: [PATCH 11/26] some fixes and tests for empty data arrays --- src/traces/histogram/defaults.js | 7 +--- src/traces/pie/defaults.js | 9 ++++- test/jasmine/tests/bar_test.js | 17 +++++++++ test/jasmine/tests/finance_test.js | 13 ++++++- test/jasmine/tests/histogram_test.js | 25 +++++++++++- test/jasmine/tests/pie_test.js | 46 +++++++++++++++++++++++ test/jasmine/tests/scatter_test.js | 16 ++++++++ test/jasmine/tests/scatterternary_test.js | 15 ++++++++ 8 files changed, 139 insertions(+), 9 deletions(-) diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js index 210c991c9c8..b56b451311a 100644 --- a/src/traces/histogram/defaults.js +++ b/src/traces/histogram/defaults.js @@ -24,8 +24,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var x = coerce('x'); var y = coerce('y'); - var hasX = x && x.length; - var hasY = y && y.length; var cumulative = coerce('cumulative.enabled'); if(cumulative) { @@ -35,12 +33,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('text'); - var orientation = coerce('orientation', (hasY && !hasX) ? 'h' : 'v'); + var orientation = coerce('orientation', (y && !x) ? 'h' : 'v'); var sampleLetter = orientation === 'v' ? 'x' : 'y'; var aggLetter = orientation === 'v' ? 'y' : 'x'; - var sample = traceOut[sampleLetter]; - var len = (hasX && hasY) ? Math.min(x.length && y.length) : (sample || []).length; + var len = (x && y) ? Math.min(x.length && y.length) : (traceOut[sampleLetter] || []).length; if(!len) { traceOut.visible = false; diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js index dc653c83399..46068e92717 100644 --- a/src/traces/pie/defaults.js +++ b/src/traces/pie/defaults.js @@ -21,9 +21,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var len; var vals = coerce('values'); - var hasVals = Lib.isArrayOrTypedArray(vals) && vals.length; + var hasVals = Lib.isArrayOrTypedArray(vals); var labels = coerce('labels'); - if(Array.isArray(labels) && labels.length) { + if(Array.isArray(labels)) { len = labels.length; if(hasVals) len = Math.min(len, vals.length); } @@ -39,6 +39,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('label0'); coerce('dlabel'); } + + if(!len) { + traceOut.visible = false; + return; + } traceOut._length = len; var lineWidth = coerce('marker.line.width'); 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/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/histogram_test.js b/test/jasmine/tests/histogram_test.js index 480f1864e92..be23de532d4 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -34,13 +34,30 @@ describe('Test histogram', function() { traceIn = { y: [] }; + traceOut = {}; + supplyDefaults(traceIn, traceOut, '', {}); + expect(traceOut.visible).toBe(false); + }); + + it('should set visible to false when x or y is empty AND the other is present', function() { + traceIn = { + x: [], + y: [1, 2, 2] + }; + supplyDefaults(traceIn, traceOut, '', {}); + expect(traceOut.visible).toBe(false); + + traceIn = { + 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] }; @@ -51,6 +68,7 @@ describe('Test histogram', function() { x: [1, 2, 2], y: [] }; + traceOut = {}; supplyDefaults2D(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); @@ -58,6 +76,7 @@ describe('Test histogram', function() { x: [], y: [] }; + traceOut = {}; supplyDefaults2D(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); @@ -65,6 +84,7 @@ describe('Test histogram', function() { x: [], y: [1, 2, 2] }; + traceOut = {}; supplyDefaults2DC(traceIn, traceOut, '', {}); expect(traceOut.visible).toBe(false); }); @@ -80,6 +100,7 @@ describe('Test histogram', function() { x: [1, 2, 2], y: [1, 2, 2] }; + traceOut = {}; supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.orientation).toBe('v'); }); @@ -111,6 +132,7 @@ describe('Test histogram', function() { traceIn = { x: [1, 2, 2] }; + traceOut = {}; supplyDefaults(traceIn, traceOut, '', {}); expect(traceOut.autobinx).toBeUndefined(); }); @@ -130,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/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/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', From 79295f15547bd9a542345e6ead9cf38a054f4079 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 25 Apr 2018 09:47:35 -0400 Subject: [PATCH 12/26] rename violins mock that doesn't contain violin traces --- .../baselines/{violins.png => fake_violins.png} | Bin .../image/mocks/{violins.json => fake_violins.json} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename test/image/baselines/{violins.png => fake_violins.png} (100%) rename test/image/mocks/{violins.json => fake_violins.json} (100%) 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/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 From 5e9aa65fbdbc3dfa5b4739675831c615ac10b2d0 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Apr 2018 13:30:25 -0400 Subject: [PATCH 13/26] transforms mock --- test/image/baselines/transforms.png | Bin 0 -> 32084 bytes test/image/mocks/transforms.json | 71 ++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 test/image/baselines/transforms.png create mode 100644 test/image/mocks/transforms.json diff --git a/test/image/baselines/transforms.png b/test/image/baselines/transforms.png new file mode 100644 index 0000000000000000000000000000000000000000..b07f1ca235ab98eca2c9e4dc870af71631264441 GIT binary patch literal 32084 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", + "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" + } +} \ No newline at end of file From 690eb9561ca06a72ef6aa5a7200c72e2afbee860 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Apr 2018 14:01:06 -0400 Subject: [PATCH 14/26] clean up keyedContainer for possibly missing array --- src/lib/keyed_container.js | 26 +++++++++++++++----- test/jasmine/tests/lib_test.js | 45 ++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) 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/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 1196e7d5c6f..62db6868a9f 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1742,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; From a244cecf361afb128c1e81503377c33461f92492 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Apr 2018 14:02:33 -0400 Subject: [PATCH 15/26] violin should not explicitly set whiskerwidth it's not an attribute, and box/plot doesn't mind it being undefined --- src/traces/violin/plot.js | 3 --- 1 file changed, 3 deletions(-) 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, From 92bd5d22faa6cf66442c90776ef49b2feb4b1023 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Apr 2018 14:04:06 -0400 Subject: [PATCH 16/26] add _length and stop slicing in scattercarpet --- src/traces/scattercarpet/calc.js | 2 +- src/traces/scattercarpet/defaults.js | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) 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'); From dc6de2f1876a8917839f25bb79b19b7387d817be Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Apr 2018 14:07:52 -0400 Subject: [PATCH 17/26] clean up handling of colorscale defaults especially pushing auto values back to input when there's a groupby transform --- src/components/colorscale/calc.js | 73 ++++++++++++++++++--------- src/traces/contour/set_contours.js | 16 ++++-- src/traces/contourcarpet/calc.js | 2 +- src/transforms/groupby.js | 21 ++++++-- test/jasmine/tests/colorscale_test.js | 21 ++++++-- 5 files changed, 96 insertions(+), 37 deletions(-) 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/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/calc.js b/src/traces/contourcarpet/calc.js index 162a5bb2d9b..ebaccfa0f73 100644 --- a/src/traces/contourcarpet/calc.js +++ b/src/traces/contourcarpet/calc.js @@ -99,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/transforms/groupby.js b/src/transforms/groupby.js index 36bcfc05dec..25670c41f95 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -161,7 +161,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 +197,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 +258,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 +276,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/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'); From 03956e1125117f89039189154034927398842133 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Apr 2018 14:09:44 -0400 Subject: [PATCH 18/26] include count aggregates in _arrayAttrs - so they remap correctly --- src/transforms/aggregate.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/transforms/aggregate.js b/src/transforms/aggregate.js index cd772999839..00d5ac29b37 100644 --- a/src/transforms/aggregate.js +++ b/src/transforms/aggregate.js @@ -275,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) { From f439e41eb57f1af0a79ae677223700bdd85562ff Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Apr 2018 14:11:01 -0400 Subject: [PATCH 19/26] in diffData I had _fullInput in the new trace, but not the old! --- src/plot_api/plot_api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 5e3139c19a8..0c5fea55084 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2403,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) { From e47e6a96e75fb5c8091124a95badefd56cccb5da Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Apr 2018 14:12:24 -0400 Subject: [PATCH 20/26] _compareAsJSON for groupby styles --- src/plot_api/plot_api.js | 2 ++ src/transforms/groupby.js | 13 ++++++++++--- test/jasmine/tests/plotschema_test.js | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 0c5fea55084..34a078c3101 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2493,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/transforms/groupby.js b/src/transforms/groupby.js index 25670c41f95..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; } } 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); From aa30ad62a9db5b4e44e0f79be429eac246a97e96 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Apr 2018 14:16:39 -0400 Subject: [PATCH 21/26] update plot_api_test to :lock: recent changes in react/transforms etc --- test/jasmine/tests/plot_api_test.js | 203 +++++++++++++++++----------- 1 file changed, 125 insertions(+), 78 deletions(-) diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 2194c476897..a5455f95e9a 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); @@ -3183,6 +3186,7 @@ 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_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')], @@ -3196,12 +3200,14 @@ 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')], ['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: [{ @@ -3211,86 +3217,127 @@ 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]; + // 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; } + + // Only include scattermapbox locally, see below + delete typesTested.scattermapbox; + + // Not 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); } - // 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); - } - }); + typesTested[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); + 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); + } + + mockList.forEach(function(mockSpec) { + it('can redraw "' + mockSpec[0] + '" with no changes as a noop', function(done) { + _runReactMock(mockSpec, done); + }); + }); + + it('@noCI can redraw scattermapbox with no changes as a noop', function(done) { + typesTested.scattermapbox = 0; + + Plotly.setPlotConfig({ + mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN + }); + + _runReactMock(['scattermapbox', require('@mocks/mapbox_bubbles-text.json')], done); + }); + + it('tested every trace & transform type at least once', function() { + for(var itemType in typesTested) { + expect(typesTested[itemType]).toBeGreaterThan(0, itemType + ' was not tested'); + } }); }); From 4c70826b69cd7a3db4de663859f927f93b76d306 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Apr 2018 14:31:01 -0400 Subject: [PATCH 22/26] lint --- test/image/mocks/transforms.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/image/mocks/transforms.json b/test/image/mocks/transforms.json index 79d8ef3b641..115b3259c4c 100644 --- a/test/image/mocks/transforms.json +++ b/test/image/mocks/transforms.json @@ -68,4 +68,4 @@ "height": 400, "title": "Transforms" } -} \ No newline at end of file +} From 7279b5596b98f28faac4b636f332f5fbe459ab6b Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Apr 2018 14:51:30 -0400 Subject: [PATCH 23/26] +shapes & annotations3d in react-noop test --- test/jasmine/tests/plot_api_test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index a5455f95e9a..f5f3b35b9b4 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -3190,8 +3190,9 @@ describe('Test plot api', function() { ['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_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')], ['layout_image', require('@mocks/layout_image.json')], ['layout-colorway', require('@mocks/layout-colorway.json')], @@ -3201,6 +3202,7 @@ describe('Test plot api', function() { ['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')], @@ -3226,7 +3228,7 @@ describe('Test plot api', function() { // Only include scattermapbox locally, see below delete typesTested.scattermapbox; - // Not being supported? This isn't part of the main bundle, and it's pretty broken, + // 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; From 3e250df2459b58bac33e3165b7282fd01ce8ad9a Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Apr 2018 16:11:05 -0400 Subject: [PATCH 24/26] tweak docs & remove commented-out code --- src/plots/plots.js | 6 +++--- src/traces/contourcarpet/attributes.js | 3 --- src/traces/contourcarpet/defaults.js | 3 --- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 0e8aecf2452..77dc572f4e6 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -265,9 +265,9 @@ var extraFormatKeys = [ * trace into the new one. Use skipUpdateCalc to defer this (needed by Plotly.react) * * gd.data, gd.layout: - * are precisely what the user specified, - * these fields shouldn't be modified nor used directly - * after the supply defaults step. + * 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, 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/defaults.js b/src/traces/contourcarpet/defaults.js index a833016beca..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', 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; From cd08479f925745225692dd3440a24cd14ed52fbf Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Apr 2018 16:34:54 -0400 Subject: [PATCH 25/26] reactWith -> reactTo --- test/jasmine/tests/plot_api_test.js | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index f5f3b35b9b4..9771bad2520 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -3067,7 +3067,7 @@ describe('Test plot api', function() { }; } - function reactWith(fig) { + function reactTo(fig) { return function() { return Plotly.react(gd, fig); }; } @@ -3075,10 +3075,10 @@ describe('Test plot api', function() { Plotly.newPlot(gd, aggregatedPie(1)) .then(checkCalcData(aggPie1CD)) - .then(reactWith(aggregatedPie(2))) + .then(reactTo(aggregatedPie(2))) .then(checkCalcData(aggPie2CD)) - .then(reactWith(aggregatedPie(1))) + .then(reactTo(aggregatedPie(1))) .then(checkCalcData(aggPie1CD)) .catch(failTest) .then(done); @@ -3088,10 +3088,10 @@ describe('Test plot api', function() { Plotly.newPlot(gd, aggregatedScatter(1)) .then(checkCalcData(aggScatter1CD)) - .then(reactWith(aggregatedScatter(2))) + .then(reactTo(aggregatedScatter(2))) .then(checkCalcData(aggScatter2CD)) - .then(reactWith(aggregatedScatter(1))) + .then(reactTo(aggregatedScatter(1))) .then(checkCalcData(aggScatter1CD)) .catch(failTest) .then(done); @@ -3101,13 +3101,13 @@ describe('Test plot api', function() { Plotly.newPlot(gd, aggregatedParcoords(0)) .then(checkValues(aggParcoords0Vals)) - .then(reactWith(aggregatedParcoords(1))) + .then(reactTo(aggregatedParcoords(1))) .then(checkValues(aggParcoords1Vals)) - .then(reactWith(aggregatedParcoords(2))) + .then(reactTo(aggregatedParcoords(2))) .then(checkValues(aggParcoords2Vals)) - .then(reactWith(aggregatedParcoords(0))) + .then(reactTo(aggregatedParcoords(0))) .then(checkValues(aggParcoords0Vals)) .catch(failTest) @@ -3118,25 +3118,25 @@ describe('Test plot api', function() { Plotly.newPlot(gd, aggregatedScatter(1)) .then(checkCalcData(aggScatter1CD)) - .then(reactWith(aggregatedPie(1))) + .then(reactTo(aggregatedPie(1))) .then(checkCalcData(aggPie1CD)) - .then(reactWith(aggregatedParcoords(1))) + .then(reactTo(aggregatedParcoords(1))) .then(checkValues(aggParcoords1Vals)) - .then(reactWith(aggregatedScatter(1))) + .then(reactTo(aggregatedScatter(1))) .then(checkCalcData(aggScatter1CD)) - .then(reactWith(aggregatedParcoords(2))) + .then(reactTo(aggregatedParcoords(2))) .then(checkValues(aggParcoords2Vals)) - .then(reactWith(aggregatedPie(2))) + .then(reactTo(aggregatedPie(2))) .then(checkCalcData(aggPie2CD)) - .then(reactWith(aggregatedScatter(2))) + .then(reactTo(aggregatedScatter(2))) .then(checkCalcData(aggScatter2CD)) - .then(reactWith(aggregatedParcoords(0))) + .then(reactTo(aggregatedParcoords(0))) .then(checkValues(aggParcoords0Vals)) .catch(failTest) .then(done); From 04141478be801fd5314f409dd6905826fcf0dab2 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Apr 2018 16:50:56 -0400 Subject: [PATCH 26/26] separate svg and gl traces in react-noop tests --- test/jasmine/tests/plot_api_test.js | 41 +++++++++++++++++------------ 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 9771bad2520..81325bb6a85 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -3167,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')], @@ -3186,14 +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_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')], ['layout_image', require('@mocks/layout_image.json')], ['layout-colorway', require('@mocks/layout-colorway.json')], ['polar_categories', require('@mocks/polar_categories.json')], @@ -3219,15 +3211,23 @@ describe('Test plot api', function() { }] ]; + 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; } - // Only include scattermapbox locally, see below - delete typesTested.scattermapbox; - // 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; @@ -3320,15 +3320,19 @@ describe('Test plot api', function() { .then(done); } - mockList.forEach(function(mockSpec) { - it('can redraw "' + mockSpec[0] + '" with no changes as a noop', function(done) { + svgMockList.forEach(function(mockSpec) { + it('can redraw "' + mockSpec[0] + '" with no changes as a noop (svg mocks)', function(done) { _runReactMock(mockSpec, done); }); }); - it('@noCI can redraw scattermapbox with no changes as a noop', function(done) { - typesTested.scattermapbox = 0; + 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 }); @@ -3336,7 +3340,10 @@ describe('Test plot api', function() { _runReactMock(['scattermapbox', require('@mocks/mapbox_bubbles-text.json')], done); }); - it('tested every trace & transform type at least once', function() { + // 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'); }