Skip to content

Commit 61f3385

Browse files
committed
hierarchical property split (squashed)
1 parent 87de31f commit 61f3385

File tree

2 files changed

+124
-16
lines changed

2 files changed

+124
-16
lines changed

src/transforms/groupby.js

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,20 @@ exports.transform = function(data, state) {
9191
var newData = [];
9292

9393
data.forEach(function(trace) {
94-
newData = newData.concat(transformOne(trace, state));
94+
95+
var splittingAttributes = [];
96+
97+
var attributes = trace._module.attributes;
98+
crawl(attributes, splittingAttributes);
99+
100+
newData = newData.concat(transformOne(trace, state, splittingAttributes));
95101
});
96102

97103
return newData;
98104
};
99105

100-
function transformOne(trace, state) {
106+
function transformOne(trace, state, splittingAttributes) {
107+
101108
var opts = state.transform;
102109
var groups = opts.groups;
103110

@@ -106,30 +113,100 @@ function transformOne(trace, state) {
106113
});
107114

108115
var newData = new Array(groupNames.length);
109-
var len = Math.min(trace.x.length, trace.y.length, groups.length);
116+
var len = groups.length;
110117

111118
var style = opts.style || {};
112119

120+
var topLevelAttributes = splittingAttributes
121+
.filter(function(array) {return Array.isArray(getDeepProp(trace, array));});
122+
123+
var initializeArray = function(newTrace, a) {
124+
setDeepProp(newTrace, a, []);
125+
};
126+
127+
var pasteArray = function(newTrace, trace, j, a) {
128+
getDeepProp(newTrace, a).push(getDeepProp(trace, a)[j]);
129+
};
130+
131+
// fixme the O(n**3) complexity
113132
for(var i = 0; i < groupNames.length; i++) {
114133
var groupName = groupNames[i];
115134

116135
// TODO is this the best pattern ???
117136
// maybe we could abstract this out
118137
var newTrace = newData[i] = Lib.extendDeep({}, trace);
119138

120-
newTrace.x = [];
121-
newTrace.y = [];
139+
topLevelAttributes.forEach(initializeArray.bind(null, newTrace));
122140

123141
for(var j = 0; j < len; j++) {
124142
if(groups[j] !== groupName) continue;
125143

126-
newTrace.x.push(trace.x[j]);
127-
newTrace.y.push(trace.y[j]);
144+
topLevelAttributes.forEach(pasteArray.bind(0, newTrace, trace, j));
128145
}
129146

130147
newTrace.name = groupName;
148+
149+
// there's no need to coerce style[groupName] here
150+
// as another round of supplyDefaults is done on the transformed traces
131151
newTrace = Lib.extendDeep(newTrace, style[groupName] || {});
132152
}
133153

134154
return newData;
135155
}
156+
157+
function getDeepProp(thing, propArray) {
158+
var result = thing;
159+
var i;
160+
for(i = 0; i < propArray.length; i++) {
161+
result = result[propArray[i]];
162+
if(result === void(0)) {
163+
return result;
164+
}
165+
}
166+
return result;
167+
}
168+
169+
function setDeepProp(thing, propArray, value) {
170+
var current = thing;
171+
var i;
172+
for(i = 0; i < propArray.length - 1; i++) {
173+
if(current[propArray[i]] === void(0)) {
174+
current[propArray[i]] = {};
175+
}
176+
current = current[propArray[i]];
177+
}
178+
current[propArray[propArray.length - 1]] = value;
179+
}
180+
181+
// fixme check if similar functions in plot_schema.js can be reused
182+
function crawl(attrs, list, path) {
183+
path = path || [];
184+
185+
Object.keys(attrs).forEach(function(attrName) {
186+
var attr = attrs[attrName];
187+
var _path = path.slice();
188+
_path.push(attrName);
189+
190+
if(attrName.charAt(0) === '_') return;
191+
192+
callback(attr, list, _path);
193+
194+
if(isValObject(attr)) return;
195+
if(isPlainObject(attr)) crawl(attr, list, _path);
196+
});
197+
}
198+
199+
function isValObject(obj) {
200+
return obj && obj.valType !== undefined;
201+
}
202+
203+
function callback(attr, list, path) {
204+
// see schema.defs for complete list of 'val types'
205+
if(attr.valType === 'data_array' || attr.arrayOk === true) {
206+
list.push(path);
207+
}
208+
}
209+
210+
function isPlainObject(obj) {
211+
return Object.prototype.toString.call(obj) === '[object Object]';
212+
}

test/jasmine/tests/transform_groupby_test.js

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -366,14 +366,22 @@ describe('groupby', function() {
366366
Plotly.plot(gd, data).then(function() {
367367

368368
expect(gd.data.length).toEqual(1);
369+
expect(gd.data[0].ids).toEqual(['q', 'w', 'r', 't', 'y', 'u', 'i']);
369370
expect(gd.data[0].x).toEqual([1, -1, -2, 0, 1, 2, 3]);
370371
expect(gd.data[0].y).toEqual([0, 1, 2, 3, 5, 4, 6]);
372+
expect(gd.data[0].marker.line.width).toEqual([4, 2, 4, 2, 2, 3, 3]);
371373

372374
expect(gd._fullData.length).toEqual(2);
375+
376+
expect(gd._fullData[0].ids).toEqual(['q', 'w', 't', 'i']);
373377
expect(gd._fullData[0].x).toEqual([1, -1, 0, 3]);
374378
expect(gd._fullData[0].y).toEqual([0, 1, 3, 6]);
379+
expect(gd._fullData[0].marker.line.width).toEqual([4, 2, 2, 3]);
380+
381+
expect(gd._fullData[1].ids).toEqual(['r', 'y', 'u']);
375382
expect(gd._fullData[1].x).toEqual([-2, 1, 2]);
376383
expect(gd._fullData[1].y).toEqual([2, 5, 4]);
384+
expect(gd._fullData[1].marker.line.width).toEqual([4, 2, 3]);
377385

378386
assertDims([4, 3]);
379387

@@ -385,8 +393,10 @@ describe('groupby', function() {
385393
// basic test
386394
var mockData1 = [{
387395
mode: 'markers',
396+
ids: ['q', 'w', 'r', 't', 'y', 'u', 'i'],
388397
x: [1, -1, -2, 0, 1, 2, 3],
389398
y: [0, 1, 2, 3, 5, 4, 6],
399+
marker: {line: {width: [4, 2, 4, 2, 2, 3, 3]}},
390400
transforms: [{
391401
type: 'groupby',
392402
groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'],
@@ -397,8 +407,10 @@ describe('groupby', function() {
397407
// heterogenously present attributes
398408
var mockData2 = [{
399409
mode: 'markers',
410+
ids: ['q', 'w', 'r', 't', 'y', 'u', 'i'],
400411
x: [1, -1, -2, 0, 1, 2, 3],
401412
y: [0, 1, 2, 3, 5, 4, 6],
413+
marker: {line: {width: [4, 2, 4, 2, 2, 3, 3]}},
402414
transforms: [{
403415
type: 'groupby',
404416
groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'],
@@ -408,25 +420,22 @@ describe('groupby', function() {
408420
color: 'orange',
409421
size: 20,
410422
line: {
411-
color: 'red',
412-
width: 1
423+
color: 'red'
413424
}
414425
}
415426
},
416427
b: {
417-
mode: 'markers+lines', // heterogeonos attributes are OK: group "a" doesn't need to define this
428+
mode: 'markers+lines', // heterogeonos attributes are OK: group 'a' doesn't need to define this
418429
marker: {
419430
color: 'cyan',
420431
size: 15,
421432
line: {
422-
color: 'purple',
423-
width: 4
433+
color: 'purple'
424434
},
425435
opacity: 0.5,
426436
symbol: 'triangle-up'
427437
},
428438
line: {
429-
width: 1,
430439
color: 'purple'
431440
}
432441
}
@@ -437,12 +446,13 @@ describe('groupby', function() {
437446
// attributes set at top level and partially overridden in the group item level
438447
var mockData3 = [{
439448
mode: 'markers+lines',
449+
ids: ['q', 'w', 'r', 't', 'y', 'u', 'i'],
440450
x: [1, -1, -2, 0, 1, 2, 3],
441451
y: [0, 1, 2, 3, 5, 4, 6],
442452
marker: {
443-
color: 'darkred', // general "default" color
453+
color: 'darkred', // general 'default' color
444454
line: {
445-
width: 8,
455+
width: [4, 2, 4, 2, 2, 3, 3],
446456
// a general, not overridden array will be interpreted per group
447457
color: ['orange', 'red', 'green', 'cyan']
448458
}
@@ -461,8 +471,10 @@ describe('groupby', function() {
461471

462472
var mockData4 = [{
463473
mode: 'markers+lines',
474+
ids: ['q', 'w', 'r', 't', 'y', 'u', 'i'],
464475
x: [1, -1, -2, 0, 1, 2, 3],
465476
y: [0, 1, 2, 3, 5, 4, 6],
477+
marker: {line: {width: [4, 2, 4, 2, 2, 3, 3]}},
466478
transforms: [{
467479
type: 'groupby',
468480
groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'],
@@ -472,20 +484,39 @@ describe('groupby', function() {
472484

473485
var mockData5 = [{
474486
mode: 'markers+lines',
487+
ids: ['q', 'w', 'r', 't', 'y', 'u', 'i'],
475488
x: [1, -1, -2, 0, 1, 2, 3],
476489
y: [0, 1, 2, 3, 5, 4, 6],
490+
marker: {
491+
line: {width: [4, 2, 4, 2, 2, 3, 3]},
492+
size: 10,
493+
color: ['red', '#eee', 'lightgreen', 'blue', 'red', '#eee', 'lightgreen']
494+
},
477495
transforms: [{
478496
type: 'groupby',
479497
groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a']
480498
}]
481499
}];
482500

483-
// this passes OK as expected
501+
var mockData6 = [{
502+
mode: 'markers+lines',
503+
ids: ['q', 'w', 'r', 't', 'y', 'u', 'i'],
504+
x: [1, -1, -2, 0, 1, 2, 3],
505+
y: [0, 1, 2, 3, 5, 4, 6],
506+
marker: {line: {width: [4, 2, 4, 2, 2, 3, 3]}},
507+
transforms: [{
508+
type: 'groupby',
509+
groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'],
510+
style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} }
511+
}]
512+
}];
513+
484514
it('`data` preserves user supplied input but `gd._fullData` reflects the grouping', test(mockData1));
485515
it('passes with lots of attributes and heterogenous attrib presence', test(mockData2));
486516
it('passes with group styles partially overriding top level aesthetics', test(mockData3));
487517
it('passes with no explicit styling for the individual group', test(mockData4));
488518
it('passes with no explicit styling in the group transform at all', test(mockData5));
519+
it('passes with no explicit styling in the group transform at all', test(mockData6));
489520

490521
});
491522

0 commit comments

Comments
 (0)