Skip to content

Commit aef38e5

Browse files
committed
Add ability to rename grouped traces
1 parent 658a096 commit aef38e5

File tree

7 files changed

+310
-8
lines changed

7 files changed

+310
-8
lines changed

src/components/legend/draw.js

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var anchorUtils = require('./anchor_utils');
3131

3232
var SHOWISOLATETIP = true;
3333
var DBLCLICKDELAY = interactConstants.DBLCLICKDELAY;
34+
var BLANK_STRING_REGEX = /^[\s\r]*$/;
3435

3536
module.exports = function draw(gd) {
3637
var fullLayout = gd._fullLayout;
@@ -392,24 +393,57 @@ function drawTexts(g, gd) {
392393
this.text(text)
393394
.call(textLayout);
394395

396+
var origText = text;
397+
395398
if(!this.text()) text = ' \u0020\u0020 ';
396399

397-
var fullInput = legendItem.trace._fullInput || {},
398-
astr;
400+
var i, transforms, direction;
401+
var fullInput = legendItem.trace._fullInput || {};
402+
var needsRedraw = false;
403+
var update = {};
399404

400405
// N.B. this block isn't super clean,
401406
// is unfortunately untested at the moment,
402407
// and only works for for 'ohlc' and 'candlestick',
403408
// but should be generalized for other one-to-many transforms
404409
if(['ohlc', 'candlestick'].indexOf(fullInput.type) !== -1) {
405-
var transforms = legendItem.trace.transforms,
406-
direction = transforms[transforms.length - 1].direction;
410+
transforms = legendItem.trace.transforms;
411+
direction = transforms[transforms.length - 1].direction;
412+
413+
update[direction + '.name'] = [text];
414+
} else {
415+
if(fullInput.transforms) {
416+
for(i = fullInput.transforms.length - 1; i >= 0; i--) {
417+
if(fullInput.transforms[i].type === 'groupby') {
418+
break;
419+
}
420+
}
421+
422+
var carr = Lib.keyedContainer(fullInput, 'transforms[' + i + '].groupnames');
423+
424+
if(BLANK_STRING_REGEX.test(origText)) {
425+
carr.remove(legendItem.trace._group);
426+
needsRedraw = true;
427+
} else {
428+
carr.set(legendItem.trace._group, [text]);
429+
}
430+
431+
update = carr.constructUpdate();
432+
}
433+
}
434+
435+
var p = Plotly.restyle(gd, update, traceIndex);
407436

408-
astr = direction + '.name';
437+
// If a groupby label is deleted, it seems like we need another redraw in order
438+
// to restore the label. Otherwise it simply sets this property and the blank
439+
// string is retained.
440+
if(needsRedraw) {
441+
p = p.then(function() {
442+
return Plotly.redraw(gd);
443+
});
409444
}
410-
else astr = 'name';
411445

412-
Plotly.restyle(gd, astr, text, traceIndex);
446+
return p;
413447
});
414448
}
415449
else text.call(textLayout);

src/lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ var BADNUM = numConstants.BADNUM;
1919
var lib = module.exports = {};
2020

2121
lib.nestedProperty = require('./nested_property');
22+
lib.keyedContainer = require('./keyed_container');
2223
lib.isPlainObject = require('./is_plain_object');
2324
lib.isArray = require('./is_array');
2425
lib.mod = require('./mod');

src/lib/keyed_container.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Copyright 2012-2017, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var nestedProperty = require('./nested_property');
12+
13+
// bitmask for deciding what's updated:
14+
var NONE = 0;
15+
var NAME = 1;
16+
var VALUE = 2;
17+
var BOTH = 3;
18+
19+
module.exports = function keyedContainer(baseObj, path) {
20+
var i, arr;
21+
var changeTypes = {};
22+
23+
if(path && path.length) { arr = nestedProperty(baseObj, path).get();
24+
} else {
25+
arr = baseObj;
26+
}
27+
28+
path = path || '';
29+
arr = arr || [];
30+
31+
// Construct an index:
32+
var indexLookup = {};
33+
for(i = 0; i < arr.length; i++) {
34+
indexLookup[arr[i].name] = i;
35+
}
36+
37+
var obj = {
38+
// NB: this does not actually modify the baseObj
39+
set: function(name, value) {
40+
var changeType = NONE;
41+
var idx = indexLookup[name];
42+
if(idx === undefined) {
43+
changeType = BOTH;
44+
idx = arr.length;
45+
indexLookup[name] = idx;
46+
} else if(value !== arr[idx].value) {
47+
changeType = VALUE;
48+
}
49+
arr[idx] = {name: name, value: value};
50+
51+
changeTypes[idx] = changeTypes[idx] | changeType;
52+
53+
return obj;
54+
},
55+
get: function(name) {
56+
var idx = indexLookup[name];
57+
return idx === undefined ? undefined : arr[idx].value;
58+
},
59+
rename: function(name, newName) {
60+
var idx = indexLookup[name];
61+
62+
if(idx === undefined) return obj;
63+
changeTypes[idx] = changeTypes[idx] | NAME;
64+
65+
indexLookup[newName] = idx;
66+
delete indexLookup[name];
67+
68+
arr[idx].name = newName;
69+
70+
return obj;
71+
},
72+
remove: function(name) {
73+
var idx = indexLookup[name];
74+
if(idx === undefined) return obj;
75+
for(i = idx; i < arr.length; i++) {
76+
changeTypes[i] = changeTypes[i] | BOTH;
77+
}
78+
for(i = idx; i < arr.length; i++) {
79+
indexLookup[arr[i].name]--;
80+
}
81+
arr.splice(idx, 1);
82+
delete(indexLookup[name]);
83+
84+
return obj;
85+
},
86+
constructUpdate: function() {
87+
var astr, idx;
88+
var update = {};
89+
var changed = Object.keys(changeTypes);
90+
for(var i = 0; i < changed.length; i++) {
91+
idx = changed[i];
92+
astr = path + '[' + idx + ']';
93+
if(arr[idx]) {
94+
if(changeTypes[idx] & NAME) {
95+
update[astr + '.name'] = arr[idx].name;
96+
}
97+
if(changeTypes[idx] & VALUE) {
98+
update[astr + '.value'] = arr[idx].value;
99+
}
100+
} else {
101+
update[astr] = null;
102+
}
103+
}
104+
105+
return update;
106+
}
107+
};
108+
109+
return obj;
110+
};

src/plots/plots.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,11 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) {
811811
var expandedTrace = expandedTraces[j];
812812
var fullExpandedTrace = plots.supplyTraceDefaults(expandedTrace, cnt, fullLayout, i);
813813

814+
// The group key gets cleared. If set, pass it forward
815+
if(expandedTrace._group) {
816+
fullExpandedTrace._group = expandedTrace._group;
817+
}
818+
814819
// mutate uid here using parent uid and expanded index
815820
// to promote consistency between update calls
816821
expandedTrace.uid = fullExpandedTrace.uid = fullTrace.uid + j;

src/transforms/groupby.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,24 @@ exports.attributes = {
3535
'with `x` [1, 3] and one trace with `x` [2, 4].'
3636
].join(' ')
3737
},
38+
namepattern: {
39+
valType: 'string',
40+
dflt: '%g (%t)',
41+
description: [
42+
'Pattern by which grouped traces are named. Available special escape',
43+
'sequences are `%g`, which inserts the group name, and `%t`, which',
44+
'inserts the trace name. If grouping GDP data by country, for example',
45+
'The default "%g (%t)" would return "Monaco (GDP per capita)".'
46+
].join(' ')
47+
},
48+
groupnames: {
49+
valType: 'any',
50+
description: [
51+
'An array of trace names based on group name. Each entry must be an',
52+
'object `{name: "group", value: "trace name"}` which is then applied',
53+
'to the particular group, overriding the name derived from `namepattern`.'
54+
].join(' ')
55+
},
3856
styles: {
3957
_isLinkedToArray: 'style',
4058
target: {
@@ -83,6 +101,8 @@ exports.supplyDefaults = function(transformIn) {
83101
if(!enabled) return transformOut;
84102

85103
coerce('groups');
104+
coerce('groupnames');
105+
coerce('namepattern');
86106

87107
var styleIn = transformIn.styles;
88108
var styleOut = transformOut.styles = [];
@@ -130,9 +150,15 @@ exports.transform = function(data, state) {
130150
return newData;
131151
};
132152

153+
function computeName(pattern, traceName, groupName) {
154+
return pattern.replace(/%g/g, groupName)
155+
.replace(/%t/g, traceName);
156+
}
157+
133158

134159
function transformOne(trace, state) {
135160
var i, j, k, attr, srcArray, groupName, newTrace, transforms, arrayLookup;
161+
var groupNameObj;
136162

137163
var opts = state.transform;
138164
var groups = trace.transforms[state.transformIndex].groups;
@@ -153,6 +179,10 @@ function transformOne(trace, state) {
153179
styleLookup[styles[i].target] = styles[i].value;
154180
}
155181

182+
if(opts.groupnames) {
183+
groupNameObj = Lib.keyedContainer(opts, 'groupnames');
184+
}
185+
156186
// An index to map group name --> expanded trace index
157187
var indexLookup = {};
158188

@@ -162,7 +192,18 @@ function transformOne(trace, state) {
162192

163193
// Start with a deep extend that just copies array references.
164194
newTrace = newData[i] = Lib.extendDeepNoArrays({}, trace);
165-
newTrace.name = groupName;
195+
newTrace._group = groupName;
196+
197+
var suppliedName = null;
198+
if(groupNameObj) {
199+
suppliedName = groupNameObj.get(groupName);
200+
}
201+
202+
if(suppliedName) {
203+
newTrace.name = suppliedName;
204+
} else {
205+
newTrace.name = computeName(opts.namepattern, trace.name, groupName);
206+
}
166207

167208
// In order for groups to apply correctly to other transform data (e.g.
168209
// a filter transform), we have to break the connection and clone the

test/image/mocks/groupby.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"data": [
3+
{
4+
"x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
5+
"y": [1, 3, 2, 4, 3, 5, 4, 6, 5, 7, 6, 8, 7, 9],
6+
"transforms": [{
7+
"type": "groupby",
8+
"groups": [1, 2, 1, 2, 3, 4, 3, 4, 5, 6, 5, 6, 7, 8]
9+
}]
10+
}
11+
],
12+
"config": {
13+
"editable": true
14+
}
15+
}

0 commit comments

Comments
 (0)