Skip to content

Commit 8265df5

Browse files
authored
Merge pull request #1236 from plotly/fix-number-named-frames
Fix number named frames
2 parents 2dbdf07 + 0b3bdca commit 8265df5

File tree

6 files changed

+89
-13
lines changed

6 files changed

+89
-13
lines changed

src/plot_api/plot_api.js

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2327,7 +2327,11 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
23272327
var newFrame = trans._currentFrame = trans._frameQueue.shift();
23282328

23292329
if(newFrame) {
2330-
gd._fullLayout._currentFrame = newFrame.name;
2330+
// Since it's sometimes necessary to do deep digging into frame data,
2331+
// we'll consider it not 100% impossible for nulls or numbers to sneak through,
2332+
// so check when casting the name, just to be absolutely certain:
2333+
var stringName = newFrame.name ? newFrame.name.toString() : null;
2334+
gd._fullLayout._currentFrame = stringName;
23312335

23322336
trans._lastFrameAt = Date.now();
23332337
trans._timeToNext = newFrame.frameOpts.duration;
@@ -2349,7 +2353,7 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
23492353
});
23502354

23512355
gd.emit('plotly_animatingframe', {
2352-
name: newFrame.name,
2356+
name: stringName,
23532357
frame: newFrame.frame,
23542358
animation: {
23552359
frame: newFrame.frameOpts,
@@ -2416,18 +2420,18 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
24162420
type: 'object',
24172421
data: setTransitionConfig(Lib.extendFlat({}, frameOrGroupNameOrFrameList))
24182422
});
2419-
} else if(allFrames || typeof frameOrGroupNameOrFrameList === 'string') {
2423+
} else if(allFrames || ['string', 'number'].indexOf(typeof frameOrGroupNameOrFrameList) !== -1) {
24202424
// In this case, null or undefined has been passed so that we want to
24212425
// animate *all* currently defined frames
24222426
for(i = 0; i < trans._frames.length; i++) {
24232427
frame = trans._frames[i];
24242428

24252429
if(!frame) continue;
24262430

2427-
if(allFrames || frame.group === frameOrGroupNameOrFrameList) {
2431+
if(allFrames || String(frame.group) === String(frameOrGroupNameOrFrameList)) {
24282432
frameList.push({
24292433
type: 'byname',
2430-
name: frame.name,
2434+
name: String(frame.name),
24312435
data: setTransitionConfig({name: frame.name})
24322436
});
24332437
}
@@ -2528,6 +2532,8 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
25282532
Plotly.addFrames = function(gd, frameList, indices) {
25292533
gd = helpers.getGraphDiv(gd);
25302534

2535+
var numericNameWarningCount = 0;
2536+
25312537
if(frameList === null || frameList === undefined) {
25322538
return Promise.resolve();
25332539
}
@@ -2558,6 +2564,25 @@ Plotly.addFrames = function(gd, frameList, indices) {
25582564

25592565
var insertions = [];
25602566
for(i = frameList.length - 1; i >= 0; i--) {
2567+
var name = (_hash[frameList[i].name] || {}).name;
2568+
var newName = frameList[i].name;
2569+
2570+
if(name && newName && typeof newName === 'number' && _hash[name]) {
2571+
numericNameWarningCount++;
2572+
2573+
Lib.warn('addFrames: overwriting frame "' + _hash[name].name +
2574+
'" with a frame whose name of type "number" also equates to "' +
2575+
name + '". This is valid but may potentially lead to unexpected ' +
2576+
'behavior since all plotly.js frame names are stored internally ' +
2577+
'as strings.');
2578+
2579+
if(numericNameWarningCount > 5) {
2580+
Lib.warn('addFrames: This API call has yielded too many warnings. ' +
2581+
'For the rest of this call, further warnings about numeric frame ' +
2582+
'names will be suppressed.');
2583+
}
2584+
}
2585+
25612586
insertions.push({
25622587
frame: Plots.supplyFrameDefaults(frameList[i]),
25632588
index: (indices && indices[i] !== undefined && indices[i] !== null) ? indices[i] : bigIndex + i
@@ -2578,6 +2603,12 @@ Plotly.addFrames = function(gd, frameList, indices) {
25782603
for(i = insertions.length - 1; i >= 0; i--) {
25792604
frame = insertions[i].frame;
25802605

2606+
if(typeof frame.name === 'number') {
2607+
Lib.warn('Warning: addFrames accepts frames with numeric names, but the numbers are' +
2608+
'implicitly cast to strings');
2609+
2610+
}
2611+
25812612
if(!frame.name) {
25822613
// Repeatedly assign a default name, incrementing the counter each time until
25832614
// we get a name that's not in the hashed lookup table:

src/plots/command.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,8 @@ exports.computeAPICommandBindings = function(gd, method, args) {
301301
function computeAnimateBindings(gd, args) {
302302
// We'll assume that the only relevant modification an animation
303303
// makes that's meaningfully tracked is the frame:
304-
if(Array.isArray(args[0]) && args[0].length === 1 && typeof args[0][0] === 'string') {
305-
return [{type: 'layout', prop: '_currentFrame', value: args[0][0]}];
304+
if(Array.isArray(args[0]) && args[0].length === 1 && ['string', 'number'].indexOf(typeof args[0][0]) !== -1) {
305+
return [{type: 'layout', prop: '_currentFrame', value: args[0][0].toString()}];
306306
} else {
307307
return [];
308308
}

src/plots/plots.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,7 +1478,17 @@ plots.computeFrame = function(gd, frameName) {
14781478
var frameLookup = gd._transitionData._frameHash;
14791479
var i, traceIndices, traceIndex, destIndex;
14801480

1481-
var framePtr = frameLookup[frameName];
1481+
// Null or undefined will fail on .toString(). We'll allow numbers since we
1482+
// make it clear frames must be given string names, but we'll allow numbers
1483+
// here since they're otherwise fine for looking up frames as long as they're
1484+
// properly cast to strings. We really just want to ensure here that this
1485+
// 1) doesn't fail, and
1486+
// 2) doens't give an incorrect answer (which String(frameName) would)
1487+
if(!frameName) {
1488+
throw new Error('computeFrame must be given a string frame name');
1489+
}
1490+
1491+
var framePtr = frameLookup[frameName.toString()];
14821492

14831493
// Return false if the name is invalid:
14841494
if(!framePtr) {
@@ -1489,7 +1499,7 @@ plots.computeFrame = function(gd, frameName) {
14891499
var frameNameStack = [framePtr.name];
14901500

14911501
// Follow frame pointers:
1492-
while((framePtr = frameLookup[framePtr.baseframe])) {
1502+
while(framePtr.baseframe && (framePtr = frameLookup[framePtr.baseframe.toString()])) {
14931503
// Avoid infinite loops:
14941504
if(frameNameStack.indexOf(framePtr.name) !== -1) break;
14951505

test/jasmine/tests/animate_test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ describe('Test animate API', function() {
549549
transition: {duration: 1},
550550
frame: {duration: 1}
551551
}).then(function() {
552-
expect(frames).toEqual(['frame0', 'frame1', undefined, undefined]);
552+
expect(frames).toEqual(['frame0', 'frame1', null, null]);
553553
}).catch(fail).then(done);
554554

555555
});

test/jasmine/tests/command_test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,12 @@ describe('Plots.computeAPICommandBindings', function() {
444444
expect(result).toEqual([{type: 'layout', prop: '_currentFrame', value: 'framename'}]);
445445
});
446446

447+
it('treats numeric frame names as strings', function() {
448+
var result = Plots.computeAPICommandBindings(gd, 'animate', [[8]]);
449+
450+
expect(result).toEqual([{type: 'layout', prop: '_currentFrame', value: '8'}]);
451+
});
452+
447453
it('binds to nothing for a multi-frame animate command', function() {
448454
var result = Plots.computeAPICommandBindings(gd, 'animate', [['frame1', 'frame2']]);
449455

test/jasmine/tests/frame_api_test.js

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
var Plotly = require('@lib/index');
2+
var Lib = require('@src/lib');
23

34
var createGraphDiv = require('../assets/create_graph_div');
45
var destroyGraphDiv = require('../assets/destroy_graph_div');
@@ -73,13 +74,41 @@ describe('Test frame api', function() {
7374
});
7475

7576
it('creates multiple unnamed frames in series', function(done) {
76-
Plotly.addFrames(gd, [{}]).then(
77-
Plotly.addFrames(gd, [{}])
78-
).then(function() {
77+
Plotly.addFrames(gd, [{}]).then(function() {
78+
return Plotly.addFrames(gd, [{}]);
79+
}).then(function() {
7980
expect(f).toEqual([{name: 'frame 0'}, {name: 'frame 1'}]);
8081
}).catch(fail).then(done);
8182
});
8283

84+
it('casts number names to strings on insertion', function(done) {
85+
Plotly.addFrames(gd, [{name: 2}]).then(function() {
86+
expect(f).toEqual([{name: '2'}]);
87+
}).catch(fail).then(done);
88+
});
89+
90+
it('updates frames referenced by number', function(done) {
91+
Plotly.addFrames(gd, [{name: 2}]).then(function() {
92+
return Plotly.addFrames(gd, [{name: 2, layout: {foo: 'bar'}}]);
93+
}).then(function() {
94+
expect(f).toEqual([{name: '2', layout: {foo: 'bar'}}]);
95+
}).catch(fail).then(done);
96+
});
97+
98+
it('issues a warning if a number-named frame would overwrite a frame', function(done) {
99+
var warnings = [];
100+
spyOn(Lib, 'warn').and.callFake(function(msg) {
101+
warnings.push(msg);
102+
});
103+
104+
Plotly.addFrames(gd, [{name: 2}]).then(function() {
105+
return Plotly.addFrames(gd, [{name: 2, layout: {foo: 'bar'}}]);
106+
}).then(function() {
107+
expect(warnings.length).toEqual(1);
108+
expect(warnings[0]).toMatch(/overwriting/);
109+
}).catch(fail).then(done);
110+
});
111+
83112
it('avoids name collisions', function(done) {
84113
Plotly.addFrames(gd, [{name: 'frame 0'}, {name: 'frame 2'}]).then(function() {
85114
expect(f).toEqual([{name: 'frame 0'}, {name: 'frame 2'}]);

0 commit comments

Comments
 (0)