Skip to content

Commit 4ae95dd

Browse files
authored
Merge pull request #1919 from plotly/rename-grouped-traces
Add ability to rename grouped traces
2 parents fd54bee + 4b04131 commit 4ae95dd

File tree

9 files changed

+764
-11
lines changed

9 files changed

+764
-11
lines changed

src/components/legend/draw.js

+24-7
Original file line numberDiff line numberDiff line change
@@ -392,24 +392,41 @@ function drawTexts(g, gd) {
392392
this.text(text)
393393
.call(textLayout);
394394

395+
var origText = text;
396+
395397
if(!this.text()) text = ' \u0020\u0020 ';
396398

397-
var fullInput = legendItem.trace._fullInput || {},
398-
astr;
399+
var transforms, direction;
400+
var fullInput = legendItem.trace._fullInput || {};
401+
var update = {};
399402

400403
// N.B. this block isn't super clean,
401404
// is unfortunately untested at the moment,
402405
// and only works for for 'ohlc' and 'candlestick',
403406
// but should be generalized for other one-to-many transforms
404407
if(['ohlc', 'candlestick'].indexOf(fullInput.type) !== -1) {
405-
var transforms = legendItem.trace.transforms,
406-
direction = transforms[transforms.length - 1].direction;
408+
transforms = legendItem.trace.transforms;
409+
direction = transforms[transforms.length - 1].direction;
410+
411+
update[direction + '.name'] = text;
412+
} else if(Registry.hasTransform(fullInput, 'groupby')) {
413+
var groupbyIndices = Registry.getTransformIndices(fullInput, 'groupby');
414+
var index = groupbyIndices[groupbyIndices.length - 1];
415+
416+
var carr = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'target', 'value.name');
407417

408-
astr = direction + '.name';
418+
if(origText === '') {
419+
carr.remove(legendItem.trace._group);
420+
} else {
421+
carr.set(legendItem.trace._group, text);
422+
}
423+
424+
update = carr.constructUpdate();
425+
} else {
426+
update.name = text;
409427
}
410-
else astr = 'name';
411428

412-
Plotly.restyle(gd, astr, text, traceIndex);
429+
return Plotly.restyle(gd, update, traceIndex);
413430
});
414431
}
415432
else text.call(textLayout);

src/lib/index.js

+31
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');
@@ -727,3 +728,33 @@ lib.numSeparate = function(value, separators, separatethousands) {
727728

728729
return x1 + x2;
729730
};
731+
732+
var TEMPLATE_STRING_REGEX = /%{([^\s%{}]*)}/g;
733+
var SIMPLE_PROPERTY_REGEX = /^\w*$/;
734+
735+
/*
736+
* Substitute values from an object into a string
737+
*
738+
* Examples:
739+
* Lib.templateString('name: %{trace}', {trace: 'asdf'}) --> 'name: asdf'
740+
* Lib.templateString('name: %{trace[0].name}', {trace: [{name: 'asdf'}]}) --> 'name: asdf'
741+
*
742+
* @param {string} input string containing %{...} template strings
743+
* @param {obj} data object containing substitution values
744+
*
745+
* @return {string} templated string
746+
*/
747+
748+
lib.templateString = function(string, obj) {
749+
// Not all that useful, but cache nestedProperty instantiation
750+
// just in case it speeds things up *slightly*:
751+
var getterCache = {};
752+
753+
return string.replace(TEMPLATE_STRING_REGEX, function(dummy, key) {
754+
if(SIMPLE_PROPERTY_REGEX.test(key)) {
755+
return obj[key] || '';
756+
}
757+
getterCache[key] = getterCache[key] || lib.nestedProperty(obj, key).get;
758+
return getterCache[key]() || '';
759+
});
760+
};

src/lib/keyed_container.js

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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+
var SIMPLE_PROPERTY_REGEX = /^\w*$/;
14+
15+
// bitmask for deciding what's updated. Sometimes the name needs to be updated,
16+
// sometimes the value needs to be updated, and sometimes both do. This is just
17+
// a simple way to track what's updated such that it's a simple OR operation to
18+
// assimilate new updates.
19+
//
20+
// The only exception is the UNSET bit that tracks when we need to explicitly
21+
// unset and remove the property. This concrn arises because of the special
22+
// way in which nestedProperty handles null/undefined. When you specify `null`,
23+
// it prunes any unused items in the tree. I ran into some issues with it getting
24+
// null vs undefined confused, so UNSET is just a bit that forces the property
25+
// update to send `null`, removing the property explicitly rather than setting
26+
// it to undefined.
27+
var NONE = 0;
28+
var NAME = 1;
29+
var VALUE = 2;
30+
var BOTH = 3;
31+
var UNSET = 4;
32+
33+
module.exports = function keyedContainer(baseObj, path, keyName, valueName) {
34+
keyName = keyName || 'name';
35+
valueName = valueName || 'value';
36+
var i, arr;
37+
var changeTypes = {};
38+
39+
if(path && path.length) { arr = nestedProperty(baseObj, path).get();
40+
} else {
41+
arr = baseObj;
42+
}
43+
44+
path = path || '';
45+
arr = arr || [];
46+
47+
// Construct an index:
48+
var indexLookup = {};
49+
for(i = 0; i < arr.length; i++) {
50+
indexLookup[arr[i][keyName]] = i;
51+
}
52+
53+
var isSimpleValueProp = SIMPLE_PROPERTY_REGEX.test(valueName);
54+
55+
var obj = {
56+
// NB: this does not actually modify the baseObj
57+
set: function(name, value) {
58+
var changeType = value === null ? UNSET : NONE;
59+
60+
var idx = indexLookup[name];
61+
if(idx === undefined) {
62+
changeType = changeType | BOTH;
63+
idx = arr.length;
64+
indexLookup[name] = idx;
65+
} else if(value !== (isSimpleValueProp ? arr[idx][valueName] : nestedProperty(arr[idx], valueName).get())) {
66+
changeType = changeType | VALUE;
67+
}
68+
69+
var newValue = arr[idx] = arr[idx] || {};
70+
newValue[keyName] = name;
71+
72+
if(isSimpleValueProp) {
73+
newValue[valueName] = value;
74+
} else {
75+
nestedProperty(newValue, valueName).set(value);
76+
}
77+
78+
// If it's not an unset, force that bit to be unset. This is all related to the fact
79+
// that undefined and null are a bit specially implemented in nestedProperties.
80+
if(value !== null) {
81+
changeType = changeType & ~UNSET;
82+
}
83+
84+
changeTypes[idx] = changeTypes[idx] | changeType;
85+
86+
return obj;
87+
},
88+
get: function(name) {
89+
var idx = indexLookup[name];
90+
91+
if(idx === undefined) {
92+
return undefined;
93+
} else if(isSimpleValueProp) {
94+
return arr[idx][valueName];
95+
} else {
96+
return nestedProperty(arr[idx], valueName).get();
97+
}
98+
},
99+
rename: function(name, newName) {
100+
var idx = indexLookup[name];
101+
102+
if(idx === undefined) return obj;
103+
changeTypes[idx] = changeTypes[idx] | NAME;
104+
105+
indexLookup[newName] = idx;
106+
delete indexLookup[name];
107+
108+
arr[idx][keyName] = newName;
109+
110+
return obj;
111+
},
112+
remove: function(name) {
113+
var idx = indexLookup[name];
114+
115+
if(idx === undefined) return obj;
116+
117+
var object = arr[idx];
118+
if(Object.keys(object).length > 2) {
119+
// This object contains more than just the key/value, so unset
120+
// the value without modifying the entry otherwise:
121+
changeTypes[idx] = changeTypes[idx] | VALUE;
122+
return obj.set(name, null);
123+
}
124+
125+
if(isSimpleValueProp) {
126+
for(i = idx; i < arr.length; i++) {
127+
changeTypes[i] = changeTypes[i] | BOTH;
128+
}
129+
for(i = idx; i < arr.length; i++) {
130+
indexLookup[arr[i][keyName]]--;
131+
}
132+
arr.splice(idx, 1);
133+
delete(indexLookup[name]);
134+
} else {
135+
// Perform this update *strictly* so we can check whether the result's
136+
// been pruned. If so, it's a removal. If not, it's a value unset only.
137+
nestedProperty(object, valueName).set(null);
138+
139+
// Now check if the top level nested property has any keys left. If so,
140+
// the object still has values so we only want to unset the key. If not,
141+
// the entire object can be removed since there's no other data.
142+
// var topLevelKeys = Object.keys(object[valueName.split('.')[0]] || []);
143+
144+
changeTypes[idx] = changeTypes[idx] | VALUE | UNSET;
145+
}
146+
147+
return obj;
148+
},
149+
constructUpdate: function() {
150+
var astr, idx;
151+
var update = {};
152+
var changed = Object.keys(changeTypes);
153+
for(var i = 0; i < changed.length; i++) {
154+
idx = changed[i];
155+
astr = path + '[' + idx + ']';
156+
if(arr[idx]) {
157+
if(changeTypes[idx] & NAME) {
158+
update[astr + '.' + keyName] = arr[idx][keyName];
159+
}
160+
if(changeTypes[idx] & VALUE) {
161+
if(isSimpleValueProp) {
162+
update[astr + '.' + valueName] = (changeTypes[idx] & UNSET) ? null : arr[idx][valueName];
163+
} else {
164+
update[astr + '.' + valueName] = (changeTypes[idx] & UNSET) ? null : nestedProperty(arr[idx], valueName).get();
165+
}
166+
}
167+
} else {
168+
update[astr] = null;
169+
}
170+
}
171+
172+
return update;
173+
}
174+
};
175+
176+
return obj;
177+
};

src/plots/plots.js

+4
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,10 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) {
811811
var expandedTrace = expandedTraces[j];
812812
var fullExpandedTrace = plots.supplyTraceDefaults(expandedTrace, cnt, fullLayout, i);
813813

814+
// relink private (i.e. underscore) keys expanded trace to full expanded trace so
815+
// that transform supply-default methods can set _ keys for future use.
816+
relinkPrivateKeys(fullExpandedTrace, expandedTrace);
817+
814818
// mutate uid here using parent uid and expanded index
815819
// to promote consistency between update calls
816820
expandedTrace.uid = fullExpandedTrace.uid = fullTrace.uid + j;

src/registry.js

+42
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,48 @@ exports.traceIs = function(traceType, category) {
166166
return !!_module.categories[category];
167167
};
168168

169+
/**
170+
* Determine if this trace has a transform of the given type and return
171+
* array of matching indices.
172+
*
173+
* @param {object} data
174+
* a trace object (member of data or fullData)
175+
* @param {string} type
176+
* type of trace to test
177+
* @return {array}
178+
* array of matching indices. If none found, returns []
179+
*/
180+
exports.getTransformIndices = function(data, type) {
181+
var indices = [];
182+
var transforms = data.transforms || [];
183+
for(var i = 0; i < transforms.length; i++) {
184+
if(transforms[i].type === type) {
185+
indices.push(i);
186+
}
187+
}
188+
return indices;
189+
};
190+
191+
/**
192+
* Determine if this trace has a transform of the given type
193+
*
194+
* @param {object} data
195+
* a trace object (member of data or fullData)
196+
* @param {string} type
197+
* type of trace to test
198+
* @return {boolean}
199+
*/
200+
exports.hasTransform = function(data, type) {
201+
var transforms = data.transforms || [];
202+
for(var i = 0; i < transforms.length; i++) {
203+
if(transforms[i].type === type) {
204+
return true;
205+
}
206+
}
207+
return false;
208+
209+
};
210+
169211
/**
170212
* Retrieve component module method. Falls back on noop if either the
171213
* module or the method is missing, so the result can always be safely called

0 commit comments

Comments
 (0)