Skip to content

Fix number named frames #1236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2327,7 +2327,11 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
var newFrame = trans._currentFrame = trans._frameQueue.shift();

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha. 'string'.toString() is a thing. How useful!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flushing all falsy names to null is a nice addition maintenance-wise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fun

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could maybe have

var nameIn = (newFrame.name === 'number') ? newFrame.name.toPrecision(1) : newFrame.name;
var stringName = nameIn ? nameIn.toString() : null;

to make sure that we only cast integer to string.


This is probably an overkill though. @rreusser I'll let decide what's best.

Copy link
Contributor Author

@rreusser rreusser Dec 7, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have strong feelings. I feel like you kinda get what you deserve if you're going to be dealing in floating point lookups, so I think maybe it's best to let the chips fall where they may and a different fixed point cuttoff is somewhat arbitrary unless we get fancy with a sprintf library or something.

And yeah, one subtlety: String(name) is definitely not the same as name.toString():

screen shot 2016-12-07 at 16 45 07

Which is why the extra gymnastics to ensure nothing is falsey.

gd._fullLayout._currentFrame = stringName;

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

gd.emit('plotly_animatingframe', {
name: newFrame.name,
name: stringName,
frame: newFrame.frame,
animation: {
frame: newFrame.frameOpts,
Expand Down Expand Up @@ -2416,18 +2420,18 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
type: 'object',
data: setTransitionConfig(Lib.extendFlat({}, frameOrGroupNameOrFrameList))
});
} else if(allFrames || typeof frameOrGroupNameOrFrameList === 'string') {
} else if(allFrames || ['string', 'number'].indexOf(typeof frameOrGroupNameOrFrameList) !== -1) {
// In this case, null or undefined has been passed so that we want to
// animate *all* currently defined frames
for(i = 0; i < trans._frames.length; i++) {
frame = trans._frames[i];

if(!frame) continue;

if(allFrames || frame.group === frameOrGroupNameOrFrameList) {
if(allFrames || String(frame.group) === String(frameOrGroupNameOrFrameList)) {
frameList.push({
type: 'byname',
name: frame.name,
name: String(frame.name),
data: setTransitionConfig({name: frame.name})
});
}
Expand Down Expand Up @@ -2528,6 +2532,8 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
Plotly.addFrames = function(gd, frameList, indices) {
gd = helpers.getGraphDiv(gd);

var numericNameWarningCount = 0;

if(frameList === null || frameList === undefined) {
return Promise.resolve();
}
Expand Down Expand Up @@ -2558,6 +2564,25 @@ Plotly.addFrames = function(gd, frameList, indices) {

var insertions = [];
for(i = frameList.length - 1; i >= 0; i--) {
var name = (_hash[frameList[i].name] || {}).name;
var newName = frameList[i].name;

if(name && newName && typeof newName === 'number' && _hash[name]) {
numericNameWarningCount++;

Lib.warn('addFrames: overwriting frame "' + _hash[name].name +
'" with a frame whose name of type "number" also equates to "' +
name + '". This is valid but may potentially lead to unexpected ' +
'behavior since all plotly.js frame names are stored internally ' +
'as strings.');

if(numericNameWarningCount > 5) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice touch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I published a module once where I said "meh, probably won't be too many warnings." Haha, WRONG.

Lib.warn('addFrames: This API call has yielded too many warnings. ' +
'For the rest of this call, further warnings about numeric frame ' +
'names will be suppressed.');
}
}

insertions.push({
frame: Plots.supplyFrameDefaults(frameList[i]),
index: (indices && indices[i] !== undefined && indices[i] !== null) ? indices[i] : bigIndex + i
Expand All @@ -2578,6 +2603,12 @@ Plotly.addFrames = function(gd, frameList, indices) {
for(i = insertions.length - 1; i >= 0; i--) {
frame = insertions[i].frame;

if(typeof frame.name === 'number') {
Lib.warn('Warning: addFrames accepts frames with numeric names, but the numbers are' +
'implicitly cast to strings');

}

if(!frame.name) {
// Repeatedly assign a default name, incrementing the counter each time until
// we get a name that's not in the hashed lookup table:
Expand Down
4 changes: 2 additions & 2 deletions src/plots/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,8 @@ exports.computeAPICommandBindings = function(gd, method, args) {
function computeAnimateBindings(gd, args) {
// We'll assume that the only relevant modification an animation
// makes that's meaningfully tracked is the frame:
if(Array.isArray(args[0]) && args[0].length === 1 && typeof args[0][0] === 'string') {
return [{type: 'layout', prop: '_currentFrame', value: args[0][0]}];
if(Array.isArray(args[0]) && args[0].length === 1 && ['string', 'number'].indexOf(typeof args[0][0]) !== -1) {
return [{type: 'layout', prop: '_currentFrame', value: args[0][0].toString()}];
} else {
return [];
}
Expand Down
14 changes: 12 additions & 2 deletions src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -1478,7 +1478,17 @@ plots.computeFrame = function(gd, frameName) {
var frameLookup = gd._transitionData._frameHash;
var i, traceIndices, traceIndex, destIndex;

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

var framePtr = frameLookup[frameName.toString()];

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

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

Expand Down
2 changes: 1 addition & 1 deletion test/jasmine/tests/animate_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ describe('Test animate API', function() {
transition: {duration: 1},
frame: {duration: 1}
}).then(function() {
expect(frames).toEqual(['frame0', 'frame1', undefined, undefined]);
expect(frames).toEqual(['frame0', 'frame1', null, null]);
}).catch(fail).then(done);

});
Expand Down
6 changes: 6 additions & 0 deletions test/jasmine/tests/command_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,12 @@ describe('Plots.computeAPICommandBindings', function() {
expect(result).toEqual([{type: 'layout', prop: '_currentFrame', value: 'framename'}]);
});

it('treats numeric frame names as strings', function() {
var result = Plots.computeAPICommandBindings(gd, 'animate', [[8]]);

expect(result).toEqual([{type: 'layout', prop: '_currentFrame', value: '8'}]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nicely done

});

it('binds to nothing for a multi-frame animate command', function() {
var result = Plots.computeAPICommandBindings(gd, 'animate', [['frame1', 'frame2']]);

Expand Down
35 changes: 32 additions & 3 deletions test/jasmine/tests/frame_api_test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var Plotly = require('@lib/index');
var Lib = require('@src/lib');

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

it('creates multiple unnamed frames in series', function(done) {
Plotly.addFrames(gd, [{}]).then(
Plotly.addFrames(gd, [{}])
).then(function() {
Plotly.addFrames(gd, [{}]).then(function() {
return Plotly.addFrames(gd, [{}]);
}).then(function() {
expect(f).toEqual([{name: 'frame 0'}, {name: 'frame 1'}]);
}).catch(fail).then(done);
});

it('casts number names to strings on insertion', function(done) {
Plotly.addFrames(gd, [{name: 2}]).then(function() {
expect(f).toEqual([{name: '2'}]);
}).catch(fail).then(done);
});

it('updates frames referenced by number', function(done) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sweet 🎉

Plotly.addFrames(gd, [{name: 2}]).then(function() {
return Plotly.addFrames(gd, [{name: 2, layout: {foo: 'bar'}}]);
}).then(function() {
expect(f).toEqual([{name: '2', layout: {foo: 'bar'}}]);
}).catch(fail).then(done);
});

it('issues a warning if a number-named frame would overwrite a frame', function(done) {
var warnings = [];
spyOn(Lib, 'warn').and.callFake(function(msg) {
warnings.push(msg);
});

Plotly.addFrames(gd, [{name: 2}]).then(function() {
return Plotly.addFrames(gd, [{name: 2, layout: {foo: 'bar'}}]);
}).then(function() {
expect(warnings.length).toEqual(1);
expect(warnings[0]).toMatch(/overwriting/);
}).catch(fail).then(done);
});

it('avoids name collisions', function(done) {
Plotly.addFrames(gd, [{name: 'frame 0'}, {name: 'frame 2'}]).then(function() {
expect(f).toEqual([{name: 'frame 0'}, {name: 'frame 2'}]);
Expand Down