diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index 6590c428c26..92e00d17fb3 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -116,34 +116,29 @@ exports.supplyDefaults = function(transformIn) { * array of transformed traces */ exports.transform = function(data, state) { + var newTraces, i, j; var newData = []; - for(var i = 0; i < data.length; i++) { - newData = newData.concat(transformOne(data[i], state)); + for(i = 0; i < data.length; i++) { + newTraces = transformOne(data[i], state); + + for(j = 0; j < newTraces.length; j++) { + newData.push(newTraces[j]); + } } return newData; }; -function initializeArray(newTrace, a) { - Lib.nestedProperty(newTrace, a).set([]); -} - -function pasteArray(newTrace, trace, j, a) { - Lib.nestedProperty(newTrace, a).set( - Lib.nestedProperty(newTrace, a).get().concat([ - Lib.nestedProperty(trace, a).get()[j] - ]) - ); -} function transformOne(trace, state) { - var i; + var i, j, k, attr, srcArray, groupName, newTrace, transforms, arrayLookup; + var opts = state.transform; var groups = trace.transforms[state.transformIndex].groups; if(!(Array.isArray(groups)) || groups.length === 0) { - return trace; + return [trace]; } var groupNames = Lib.filterUnique(groups), @@ -158,20 +153,59 @@ function transformOne(trace, state) { styleLookup[styles[i].target] = styles[i].value; } + // An index to map group name --> expanded trace index + var indexLookup = {}; + for(i = 0; i < groupNames.length; i++) { - var groupName = groupNames[i]; + groupName = groupNames[i]; + indexLookup[groupName] = i; + + // Start with a deep extend that just copies array references. + newTrace = newData[i] = Lib.extendDeepNoArrays({}, trace); + newTrace.name = groupName; - var newTrace = newData[i] = Lib.extendDeepNoArrays({}, trace); + // In order for groups to apply correctly to other transform data (e.g. + // a filter transform), we have to break the connection and clone the + // transforms so that each group writes grouped values into a different + // destination. This function does not break the array reference + // connection between the split transforms it creates. That's handled in + // initialize, which creates a new empty array for each arrayAttr. + transforms = newTrace.transforms; + newTrace.transforms = []; + for(j = 0; j < transforms.length; j++) { + newTrace.transforms[j] = Lib.extendDeepNoArrays({}, transforms[j]); + } - arrayAttrs.forEach(initializeArray.bind(null, newTrace)); + // Initialize empty arrays for the arrayAttrs, to be split in the next step + for(j = 0; j < arrayAttrs.length; j++) { + Lib.nestedProperty(newTrace, arrayAttrs[j]).set([]); + } + } - for(var j = 0; j < len; j++) { - if(groups[j] !== groupName) continue; + // For each array attribute including those nested inside this and other + // transforms (small note that we technically only need to do this for + // transforms that have not yet been applied): + for(k = 0; k < arrayAttrs.length; k++) { + attr = arrayAttrs[k]; - arrayAttrs.forEach(pasteArray.bind(0, newTrace, trace, j)); + // Cache all the arrays to which we'll push: + for(j = 0, arrayLookup = []; j < groupNames.length; j++) { + arrayLookup[j] = Lib.nestedProperty(newData[j], attr).get(); } - newTrace.name = groupName; + // Get the input data: + srcArray = Lib.nestedProperty(trace, attr).get(); + + // Send each data point to the appropriate expanded trace: + for(j = 0; j < len; j++) { + // Map group data --> trace index --> array and push data onto it + arrayLookup[indexLookup[groups[j]]].push(srcArray[j]); + } + } + + for(i = 0; i < groupNames.length; i++) { + groupName = groupNames[i]; + newTrace = newData[i]; Plots.clearExpandedTraceDefaultColors(newTrace); diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index 681c39f3737..0592b177b8c 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -727,3 +727,166 @@ describe('restyle applied on transforms:', function() { }); }); + +describe('supplyDefaults with groupby + filter', function() { + function calcDatatoTrace(calcTrace) { + return calcTrace[0].trace; + } + + function _transform(data, layout) { + var gd = { + data: data, + layout: layout || {} + }; + + Plots.supplyDefaults(gd); + Plots.doCalcdata(gd); + + return gd.calcdata.map(calcDatatoTrace); + } + + it('filter + groupby with blank target', function() { + var out = _transform([{ + x: [1, 2, 3, 4, 5, 6, 7], + y: [4, 6, 5, 7, 6, 8, 9], + transforms: [{ + type: 'filter', + operation: '<', + value: 6.5 + }, { + type: 'groupby', + groups: [1, 1, 1, 2, 2, 2, 2] + }] + }]); + + expect(out[0].x).toEqual([1, 2, 3]); + expect(out[0].y).toEqual([4, 6, 5]); + + expect(out[1].x).toEqual([4, 5, 6]); + expect(out[1].y).toEqual([7, 6, 8]); + }); + + it('fiter + groupby', function() { + var out = _transform([{ + x: [5, 4, 3], + y: [6, 5, 4], + }, { + x: [1, 2, 3, 4, 5, 6, 7], + y: [4, 6, 5, 7, 8, 9, 10], + transforms: [{ + type: 'filter', + target: [1, 2, 3, 4, 5, 6, 7], + operation: '<', + value: 6.5 + }, { + type: 'groupby', + groups: [1, 1, 1, 2, 2, 2, 2] + }] + }]); + + expect(out[0].x).toEqual([5, 4, 3]); + expect(out[0].y).toEqual([6, 5, 4]); + + expect(out[1].x).toEqual([1, 2, 3]); + expect(out[1].y).toEqual([4, 6, 5]); + + expect(out[2].x).toEqual([4, 5, 6]); + expect(out[2].y).toEqual([7, 8, 9]); + }); + + it('groupby + filter', function() { + var out = _transform([{ + x: [1, 2, 3, 4, 5, 6, 7], + y: [4, 6, 5, 7, 6, 8, 9], + transforms: [{ + type: 'groupby', + groups: [1, 1, 1, 2, 2, 2, 2] + }, { + type: 'filter', + target: [1, 2, 3, 4, 5, 6, 7], + operation: '<', + value: 6.5 + }] + }]); + + expect(out[0].x).toEqual([1, 2, 3]); + expect(out[0].y).toEqual([4, 6, 5]); + + expect(out[1].x).toEqual([4, 5, 6]); + expect(out[1].y).toEqual([7, 6, 8]); + }); + + it('groupby + groupby', function() { + var out = _transform([{ + x: [1, 2, 3, 4, 5, 6, 7, 8], + y: [4, 6, 5, 7, 6, 8, 9, 10], + transforms: [{ + type: 'groupby', + groups: [1, 1, 1, 1, 2, 2, 2, 2] + }, { + type: 'groupby', + groups: [3, 4, 3, 4, 3, 4, 3, 5], + }] + }]); + // | | | | | | | | + // v v v v v v v v + // Trace number: 0 1 0 1 2 3 2 4 + + expect(out.length).toEqual(5); + expect(out[0].x).toEqual([1, 3]); + expect(out[1].x).toEqual([2, 4]); + expect(out[2].x).toEqual([5, 7]); + expect(out[3].x).toEqual([6]); + expect(out[4].x).toEqual([8]); + }); + + it('groupby + groupby + filter', function() { + var out = _transform([{ + x: [1, 2, 3, 4, 5, 6, 7, 8], + y: [4, 6, 5, 7, 6, 8, 9, 10], + transforms: [{ + type: 'groupby', + groups: [1, 1, 1, 1, 2, 2, 2, 2] + }, { + type: 'groupby', + groups: [3, 4, 3, 4, 3, 4, 3, 5], + }, { + type: 'filter', + target: [1, 2, 3, 4, 5, 6, 7, 8], + operation: '<', + value: 4.5 + }] + }]); + // | | | | | | | | + // v v v v v v v v + // Trace number: 0 1 0 1 2 3 2 4 + + expect(out.length).toEqual(5); + expect(out[0].x).toEqual([1, 3]); + expect(out[1].x).toEqual([2, 4]); + expect(out[2].x).toEqual([]); + expect(out[3].x).toEqual([]); + expect(out[4].x).toEqual([]); + }); + + it('fiter + filter', function() { + var out = _transform([{ + x: [1, 2, 3, 4, 5, 6, 7], + y: [4, 6, 5, 7, 8, 9, 10], + transforms: [{ + type: 'filter', + target: [1, 2, 3, 4, 5, 6, 7], + operation: '<', + value: 6.5 + }, { + type: 'filter', + target: [1, 2, 3, 4, 5, 6, 7], + operation: '>', + value: 1.5 + }] + }]); + + expect(out[0].x).toEqual([2, 3, 4, 5, 6]); + expect(out[0].y).toEqual([6, 5, 7, 8, 9]); + }); +});