Skip to content

Commit 970db80

Browse files
committed
stop mutating user data to add uids
- Importantly, Plotly.react users probably were trashing these every time anyway resulting in bigger redraws than we needed. Now we're pulling them from the previous fullData if possible. - Also cleanData is now SOLELY for backward compatibility
1 parent 43d81a8 commit 970db80

13 files changed

+115
-95
lines changed

src/lib/index.js

+20-13
Original file line numberDiff line numberDiff line change
@@ -217,21 +217,24 @@ lib.simpleMap = function(array, func, x1, x2) {
217217
return out;
218218
};
219219

220-
// random string generator
221-
lib.randstr = function randstr(existing, bits, base) {
222-
/*
223-
* Include number of bits, the base of the string you want
224-
* and an optional array of existing strings to avoid.
225-
*/
220+
/**
221+
* Random string generator
222+
*
223+
* @param {object} existing
224+
* pass in strings to avoid as keys with truthy values
225+
* @param {int} bits
226+
* bits of information in the output string, default 24
227+
* @param {int} base
228+
* base of string representation, default 16. Should be a power of 2.
229+
*/
230+
lib.randstr = function randstr(existing, bits, base, _recursion) {
226231
if(!base) base = 16;
227232
if(bits === undefined) bits = 24;
228233
if(bits <= 0) return '0';
229234

230-
var digits = Math.log(Math.pow(2, bits)) / Math.log(base),
231-
res = '',
232-
i,
233-
b,
234-
x;
235+
var digits = Math.log(Math.pow(2, bits)) / Math.log(base);
236+
var res = '';
237+
var i, b, x;
235238

236239
for(i = 2; digits === Infinity; i *= 2) {
237240
digits = Math.log(Math.pow(2, bits / i)) / Math.log(base) * i;
@@ -251,9 +254,13 @@ lib.randstr = function randstr(existing, bits, base) {
251254
}
252255

253256
var parsed = parseInt(res, base);
254-
if((existing && (existing.indexOf(res) > -1)) ||
257+
if((existing && existing[res]) ||
255258
(parsed !== Infinity && parsed >= Math.pow(2, bits))) {
256-
return randstr(existing, bits, base);
259+
if(_recursion > 10) {
260+
lib.warn('randstr failed uniqueness');
261+
return res;
262+
}
263+
return randstr(existing, bits, base, (_recursion || 0) + 1);
257264
}
258265
else return res;
259266
};

src/lib/svg_text_utils.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ function cleanEscapesForTex(s) {
164164
}
165165

166166
function texToSVG(_texString, _config, _callback) {
167-
var randomID = 'math-output-' + Lib.randstr([], 64);
167+
var randomID = 'math-output-' + Lib.randstr({}, 64);
168168
var tmpDiv = d3.select('body').append('div')
169169
.attr({id: randomID})
170170
.style({visibility: 'hidden', position: 'absolute'})

src/plot_api/helpers.js

+3-28
Original file line numberDiff line numberDiff line change
@@ -197,42 +197,17 @@ function cleanAxRef(container, attr) {
197197
}
198198

199199
/*
200-
* cleanData: Make a few changes to the data right away
201-
* before it gets used for anything
202-
* Mostly for backward compatibility, modifies the data traces users provide.
200+
* cleanData: Make a few changes to the data for backward compatibility
201+
* before it gets used for anything. Modifies the data traces users provide.
203202
*
204203
* Important: if you're going to add something here that modifies a data array,
205204
* update it in place so the new array === the old one.
206205
*/
207-
exports.cleanData = function(data, existingData) {
208-
// Enforce unique IDs
209-
var suids = [], // seen uids --- so we can weed out incoming repeats
210-
uids = data.concat(Array.isArray(existingData) ? existingData : [])
211-
.filter(function(trace) { return 'uid' in trace; })
212-
.map(function(trace) { return trace.uid; });
213-
206+
exports.cleanData = function(data) {
214207
for(var tracei = 0; tracei < data.length; tracei++) {
215208
var trace = data[tracei];
216209
var i;
217210

218-
// assign uids to each trace and detect collisions.
219-
if(!('uid' in trace) || suids.indexOf(trace.uid) !== -1) {
220-
var newUid;
221-
222-
for(i = 0; i < 100; i++) {
223-
newUid = Lib.randstr(uids);
224-
if(suids.indexOf(newUid) === -1) break;
225-
}
226-
trace.uid = Lib.randstr(uids);
227-
uids.push(trace.uid);
228-
}
229-
// keep track of already seen uids, so that if there are
230-
// doubles we force the trace with a repeat uid to
231-
// acquire a new one
232-
suids.push(trace.uid);
233-
234-
// BACKWARD COMPATIBILITY FIXES
235-
236211
// use xbins to bin data in x, and ybins to bin data in y
237212
if(trace.type === 'histogramy' && 'xbins' in trace && !('ybins' in trace)) {
238213
trace.ybins = trace.xbins;

src/plot_api/plot_api.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ exports.plot = function(gd, data, layout, config) {
123123
// if there is already data on the graph, append the new data
124124
// if you only want to redraw, pass a non-array for data
125125
if(Array.isArray(data)) {
126-
helpers.cleanData(data, gd.data);
126+
helpers.cleanData(data);
127127

128128
if(graphWasEmpty) gd.data = data;
129129
else gd.data.push.apply(gd.data, data);
@@ -559,7 +559,7 @@ exports.redraw = function(gd) {
559559
throw new Error('This element is not a Plotly plot: ' + gd);
560560
}
561561

562-
helpers.cleanData(gd.data, gd.data);
562+
helpers.cleanData(gd.data);
563563
helpers.cleanLayout(gd.layout);
564564

565565
gd.calcdata = undefined;
@@ -1060,7 +1060,7 @@ exports.addTraces = function addTraces(gd, traces, newIndices) {
10601060
return Lib.extendFlat({}, trace);
10611061
});
10621062

1063-
helpers.cleanData(traces, gd.data);
1063+
helpers.cleanData(traces);
10641064

10651065
// add the traces to gd.data (no redrawing yet!)
10661066
for(i = 0; i < traces.length; i++) {
@@ -2259,7 +2259,7 @@ exports.react = function(gd, data, layout, config) {
22592259
}
22602260

22612261
gd.data = data || [];
2262-
helpers.cleanData(gd.data, []);
2262+
helpers.cleanData(gd.data);
22632263
gd.layout = layout || {};
22642264
helpers.cleanLayout(gd.layout);
22652265

@@ -3257,9 +3257,9 @@ function makePlotFramework(gd) {
32573257
.classed('main-svg', true);
32583258

32593259
if(!fullLayout._uid) {
3260-
var otherUids = [];
3260+
var otherUids = {};
32613261
d3.selectAll('defs').each(function() {
3262-
if(this.id) otherUids.push(this.id.split('-')[1]);
3262+
if(this.id) otherUids[this.id.split('-')[1]] = 1;
32633263
});
32643264
fullLayout._uid = Lib.randstr(otherUids);
32653265
}

src/plots/attributes.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,7 @@ module.exports = {
7373
uid: {
7474
valType: 'string',
7575
role: 'info',
76-
dflt: '',
77-
editType: 'calc'
76+
editType: 'plot'
7877
},
7978
ids: {
8079
valType: 'data_array',

src/plots/plots.js

+47-3
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,9 @@ plots.supplyDefaults = function(gd, opts) {
389389
// eg set `_requestRangeslider.x2 = true` for xaxis2
390390
newFullLayout._requestRangeslider = {};
391391

392+
// pull uids from old data to use as new defaults
393+
newFullLayout._traceUids = getTraceUids(oldFullData, newData);
394+
392395
// then do the data
393396
newFullLayout._globalTransforms = (gd._context || {}).globalTransforms;
394397
plots.supplyDataDefaults(newData, newFullData, newLayout, newFullLayout);
@@ -499,6 +502,46 @@ plots.supplyDefaultsUpdateCalc = function(oldCalcdata, newFullData) {
499502
}
500503
};
501504

505+
/**
506+
* Create a list of uid strings satisfying (in this order of importance):
507+
* 1. all unique, all strings
508+
* 2. matches input uids if provided
509+
* 3. matches previous data uids
510+
*/
511+
function getTraceUids(oldFullData, newData) {
512+
var len = newData.length;
513+
var oldFullInput = [];
514+
var i, prevFullInput;
515+
for(i = 0; i < oldFullData.length; i++) {
516+
var thisFullInput = oldFullData[i]._fullInput;
517+
if(thisFullInput !== prevFullInput) oldFullInput.push(thisFullInput);
518+
prevFullInput = thisFullInput;
519+
}
520+
var oldLen = oldFullInput.length;
521+
var out = new Array(len);
522+
var seenUids = {};
523+
524+
function setUid(uid, i) {
525+
out[i] = uid;
526+
seenUids[uid] = 1;
527+
}
528+
529+
function tryUid(uid, i) {
530+
if(uid && typeof uid === 'string' && !seenUids[uid]) {
531+
setUid(uid, i);
532+
return true;
533+
}
534+
}
535+
536+
for(i = 0; i < len; i++) {
537+
if(tryUid(newData[i].uid, i)) continue;
538+
if(i < oldLen && tryUid(oldFullInput[i].uid, i)) continue;
539+
setUid(Lib.randstr(seenUids), i);
540+
}
541+
542+
return out;
543+
}
544+
502545
/**
503546
* Make a container for collecting subplots we need to display.
504547
*
@@ -888,6 +931,8 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) {
888931
trace = dataIn[i];
889932
fullTrace = plots.supplyTraceDefaults(trace, colorCnt, fullLayout, i);
890933

934+
fullTrace.uid = fullLayout._traceUids[i];
935+
891936
fullTrace.index = i;
892937
fullTrace._input = trace;
893938
fullTrace._expandedIndex = cnt;
@@ -903,9 +948,9 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) {
903948
// that transform supply-default methods can set _ keys for future use.
904949
relinkPrivateKeys(fullExpandedTrace, expandedTrace);
905950

906-
// mutate uid here using parent uid and expanded index
951+
// set uid using parent uid and expanded index
907952
// to promote consistency between update calls
908-
expandedTrace.uid = fullExpandedTrace.uid = fullTrace.uid + j;
953+
fullExpandedTrace.uid = fullTrace.uid + j;
909954

910955
// add info about parent data trace
911956
fullExpandedTrace.index = i;
@@ -1045,7 +1090,6 @@ plots.supplyTraceDefaults = function(traceIn, colorIndex, layout, traceInIndex)
10451090
var visible = coerce('visible');
10461091

10471092
coerce('type');
1048-
coerce('uid');
10491093
coerce('name', layout._traceWord + ' ' + traceInIndex);
10501094

10511095
// we want even invisible traces to make their would-be subplots visible

test/jasmine/tests/gl2d_click_test.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ describe('@gl @flaky Test hover and click interactions', function() {
124124
'data', 'fullData', 'xaxis', 'yaxis'
125125
]), 'event data keys');
126126

127-
expect(typeof pt.data.uid).toBe('string', msg + ' - uid');
128127
expect(pt.xaxis.domain.length).toBe(2, msg + ' - xaxis');
129128
expect(pt.yaxis.domain.length).toBe(2, msg + ' - yaxis');
130129

@@ -149,7 +148,7 @@ describe('@gl @flaky Test hover and click interactions', function() {
149148
return delay(100)()
150149
.then(_hover)
151150
.then(function(eventData) {
152-
assertEventData(eventData, expected);
151+
assertEventData(eventData, expected, opts.msg);
153152

154153
var g = d3.select('g.hovertext');
155154
if(g.node() === null) {

test/jasmine/tests/gl2d_pointcloud_test.js

+10-13
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,6 @@ var plotData = {
138138
}
139139
};
140140

141-
function makePlot(gd, mock, done) {
142-
return Plotly.plot(gd, mock.data, mock.layout)
143-
.then(null, failTest)
144-
.then(done);
145-
}
146-
147141
describe('@gl pointcloud traces', function() {
148142

149143
var gd;
@@ -157,8 +151,10 @@ describe('@gl pointcloud traces', function() {
157151
destroyGraphDiv();
158152
});
159153

160-
it('render without raising an error', function(done) {
161-
makePlot(gd, plotData, done);
154+
it('renders without raising an error', function(done) {
155+
Plotly.plot(gd, plotData)
156+
.catch(failTest)
157+
.then(done);
162158
});
163159

164160
it('should update properly', function(done) {
@@ -171,10 +167,11 @@ describe('@gl pointcloud traces', function() {
171167
var yBaselineMins = [{val: 0, pad: 50, extrapad: false}];
172168
var yBaselineMaxes = [{val: 9, pad: 50, extrapad: false}];
173169

174-
Plotly.plot(gd, mock.data, mock.layout).then(function() {
170+
Plotly.plot(gd, mock)
171+
.then(function() {
175172
scene2d = gd._fullLayout._plots.xy._scene2d;
176173

177-
expect(scene2d.traces[mock.data[0].uid].type).toEqual('pointcloud');
174+
expect(scene2d.traces[gd._fullData[0].uid].type).toBe('pointcloud');
178175

179176
expect(scene2d.xaxis._min).toEqual(xBaselineMins);
180177
expect(scene2d.xaxis._max).toEqual(xBaselineMaxes);
@@ -204,8 +201,8 @@ describe('@gl pointcloud traces', function() {
204201
}).then(function() {
205202
expect(scene2d.yaxis._min).toEqual(yBaselineMins);
206203
expect(scene2d.yaxis._max).toEqual(yBaselineMaxes);
207-
208-
done();
209-
});
204+
})
205+
.catch(failTest)
206+
.then(done);
210207
});
211208
});

test/jasmine/tests/plot_api_test.js

-7
Original file line numberDiff line numberDiff line change
@@ -1497,37 +1497,30 @@ describe('Test plot api', function() {
14971497
it('should work when newIndices is undefined', function() {
14981498
Plotly.addTraces(gd, [{'name': 'c'}, {'name': 'd'}]);
14991499
expect(gd.data[2].name).toBeDefined();
1500-
expect(gd.data[2].uid).toBeDefined();
15011500
expect(gd.data[3].name).toBeDefined();
1502-
expect(gd.data[3].uid).toBeDefined();
15031501
expect(plotApi.redraw).toHaveBeenCalled();
15041502
expect(plotApi.moveTraces).not.toHaveBeenCalled();
15051503
});
15061504

15071505
it('should work when newIndices is defined', function() {
15081506
Plotly.addTraces(gd, [{'name': 'c'}, {'name': 'd'}], [1, 3]);
15091507
expect(gd.data[2].name).toBeDefined();
1510-
expect(gd.data[2].uid).toBeDefined();
15111508
expect(gd.data[3].name).toBeDefined();
1512-
expect(gd.data[3].uid).toBeDefined();
15131509
expect(plotApi.redraw).not.toHaveBeenCalled();
15141510
expect(plotApi.moveTraces).toHaveBeenCalledWith(gd, [-2, -1], [1, 3]);
15151511
});
15161512

15171513
it('should work when newIndices has negative indices', function() {
15181514
Plotly.addTraces(gd, [{'name': 'c'}, {'name': 'd'}], [-3, -1]);
15191515
expect(gd.data[2].name).toBeDefined();
1520-
expect(gd.data[2].uid).toBeDefined();
15211516
expect(gd.data[3].name).toBeDefined();
1522-
expect(gd.data[3].uid).toBeDefined();
15231517
expect(plotApi.redraw).not.toHaveBeenCalled();
15241518
expect(plotApi.moveTraces).toHaveBeenCalledWith(gd, [-2, -1], [-3, -1]);
15251519
});
15261520

15271521
it('should work when newIndices is an integer', function() {
15281522
Plotly.addTraces(gd, {'name': 'c'}, 0);
15291523
expect(gd.data[2].name).toBeDefined();
1530-
expect(gd.data[2].uid).toBeDefined();
15311524
expect(plotApi.redraw).not.toHaveBeenCalled();
15321525
expect(plotApi.moveTraces).toHaveBeenCalledWith(gd, [-1], [0]);
15331526
});

test/jasmine/tests/plots_test.js

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ describe('Test Plots', function() {
3737
type: 'contour',
3838
_empties: [1, 2, 3]
3939
}];
40+
oldFullData.forEach(function(trace) { trace._fullInput = trace; });
4041

4142
var oldFullLayout = {
4243
_plots: { xy: { plot: {} } },

0 commit comments

Comments
 (0)