Skip to content
78 changes: 56 additions & 22 deletions src/transforms/groupby.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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);

Expand Down
163 changes: 163 additions & 0 deletions test/jasmine/tests/transform_multi_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});
});