Skip to content

Commit f50fcaa

Browse files
committed
Switch return format of bindings to structured output
1 parent edb6773 commit f50fcaa

File tree

5 files changed

+197
-70
lines changed

5 files changed

+197
-70
lines changed

src/components/updatemenus/draw.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,28 @@ var anchorUtils = require('../legend/anchor_utils');
2121

2222
var constants = require('./constants');
2323

24+
function computeBindings(gd, menuOpts) {
25+
var bindings = [], newBindings;
26+
var buttons = menuOpts.buttons;
27+
for(var i = 0; i < buttons.length; i++) {
28+
newBindings = Plots.computeAPICommandBindings(gd, buttons[i].method, buttons[i].args);
29+
30+
if(i > 0 && !Plots.bindingsAreConsistent(bindings, newBindings)) {
31+
bindings = null;
32+
break;
33+
}
34+
35+
for(var j = 0; j < newBindings.length; j++) {
36+
var b = newBindings[j];
37+
38+
if(bindings.indexOf(b) === -1) {
39+
bindings.push(b);
40+
}
41+
}
42+
}
43+
44+
return bindings;
45+
}
2446

2547
module.exports = function draw(gd) {
2648
var fullLayout = gd._fullLayout,
@@ -115,6 +137,8 @@ module.exports = function draw(gd) {
115137
headerGroups.each(function(menuOpts) {
116138
var gHeader = d3.select(this);
117139

140+
computeBindings(gd, menuOpts);
141+
118142
if(menuOpts.type === 'dropdown') {
119143
drawHeader(gd, gHeader, gButton, menuOpts);
120144

src/plots/command.js

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,60 @@
1212
var Plotly = require('../plotly');
1313
var Lib = require('../lib');
1414

15+
var attrPrefixRegex = /^(data|layout)(\[(-?[0-9]*)\])?\.(.*)$/;
16+
17+
/*
18+
* This function checks to see if a set of bindings is compatible
19+
* with automatic two-way binding. The criteria right now are that
20+
*
21+
* 1. multiple traces may be affected
22+
* 2. only one property may be affected
23+
*/
24+
exports.bindingsAreConsistent = function(currentBindings, newBindings) {
25+
// If they're not both arrays of equal length, return false:
26+
if(!newBindings || !currentBindings || currentBindings.length !== newBindings.length) {
27+
return false;
28+
}
29+
30+
var n = currentBindings.length;
31+
32+
for(var i = 0; i < n; i++) {
33+
// This is not the most efficient check, but the pathological case where there
34+
// are an excessive number of bindings should be rare, and at any rate we really
35+
// try to bail out early at every opportunity.
36+
if(currentBindings.indexOf(newBindings[i]) === -1) {
37+
return false;
38+
}
39+
}
40+
41+
return true;
42+
};
43+
44+
exports.evaluateAPICommandBinding = function(gd, attrName) {
45+
var match = attrName.match(attrPrefixRegex);
46+
47+
if(!match) {
48+
return null;
49+
}
50+
51+
var group = match[1];
52+
var propStr = match[4];
53+
var container;
54+
55+
switch(group) {
56+
case 'data':
57+
container = gd._fullData[parseInt(match[3])];
58+
break;
59+
case 'layout':
60+
container = gd._fullLayout;
61+
break;
62+
default:
63+
return null;
64+
}
65+
66+
return Lib.nestedProperty(container, propStr).get();
67+
};
68+
1569
exports.executeAPICommand = function(gd, method, args) {
1670
var apiMethod = Plotly[method];
1771

@@ -44,7 +98,7 @@ exports.computeAPICommandBindings = function(gd, method, args) {
4498
// This case could be analyzed more in-depth, but for a start,
4599
// we'll assume that the only relevant modification an animation
46100
// makes that's meaningfully tracked is the frame:
47-
bindings = ['layout._currentFrame'];
101+
bindings = [{type: 'layout', prop: '_currentFrame'}];
48102
break;
49103
default:
50104
// We'll elect to fail-non-fatal since this is a correct
@@ -68,14 +122,14 @@ function computeLayoutBindings(gd, args) {
68122
}
69123

70124
crawl(aobj, function(path) {
71-
bindings.push('layout' + path);
72-
});
125+
bindings.push({type: 'layout', prop: path});
126+
}, '', 0);
73127

74128
return bindings;
75129
}
76130

77131
function computeDataBindings(gd, args) {
78-
var i, traces, astr, val, aobj;
132+
var traces, astr, val, aobj;
79133
var bindings = [];
80134

81135
// Logic copied from Plotly.restyle:
@@ -97,41 +151,45 @@ function computeDataBindings(gd, args) {
97151
}
98152

99153
if(traces === undefined) {
100-
traces = [];
101-
for(i = 0; i < gd.data.length; i++) {
102-
traces.push(i);
103-
}
154+
// Explicitly assign this to null instead of undefined:
155+
traces = null;
104156
}
105157

106158
crawl(aobj, function(path, attrName, attr) {
107-
var nAttr;
159+
var thisTraces;
108160
if(Array.isArray(attr)) {
109-
nAttr = Math.min(attr.length, traces.length);
161+
var nAttr = Math.min(attr.length, gd.data.length);
162+
if(traces) {
163+
nAttr = Math.min(nAttr, traces.length);
164+
}
165+
thisTraces = [];
166+
for(var j = 0; j < nAttr; j++) {
167+
thisTraces[j] = traces ? traces[j] : j;
168+
}
110169
} else {
111-
nAttr = traces.length;
112-
}
113-
for(var j = 0; j < nAttr; j++) {
114-
bindings.push('data[' + traces[j] + ']' + path);
170+
thisTraces = traces ? traces.slice(0) : null;
115171
}
116-
});
172+
173+
bindings.push({
174+
type: 'data',
175+
prop: path,
176+
traces: thisTraces
177+
});
178+
}, '', 0);
117179

118180
return bindings;
119181
}
120182

121-
function crawl(attrs, callback, path) {
122-
if(path === undefined) {
123-
path = '';
124-
}
125-
183+
function crawl(attrs, callback, path, depth) {
126184
Object.keys(attrs).forEach(function(attrName) {
127185
var attr = attrs[attrName];
128186

129187
if(attrName[0] === '_') return;
130188

131-
var thisPath = path + '.' + attrName;
189+
var thisPath = path + (depth > 0 ? '.' : '') + attrName;
132190

133191
if(Lib.isPlainObject(attr)) {
134-
crawl(attr, callback, thisPath);
192+
crawl(attr, callback, thisPath, depth + 1);
135193
} else {
136194
// Only execute the callback on leaf nodes:
137195
callback(thisPath, attrName, attr);

src/plots/plots.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ var ErrorBars = require('../components/errorbars');
3939
var commandModule = require('./command');
4040
plots.executeAPICommand = commandModule.executeAPICommand;
4141
plots.computeAPICommandBindings = commandModule.computeAPICommandBindings;
42+
plots.evaluateAPICommandBinding = commandModule.evaluateAPICommandBinding;
43+
plots.bindingsAreConsistent = commandModule.bindingsAreConsistent;
4244

4345
/**
4446
* Find subplot ids in data.

test/jasmine/tests/binding_test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
var Lib = require('@src/lib/index');
2+
var Plotly = require('@lib/index');
3+
var createGraphDiv = require('../assets/create_graph_div');
4+
var destroyGraphDiv = require('../assets/destroy_graph_div');
5+
var fail = require('../assets/fail_test');
6+
7+
describe('updateBindings', function() {
8+
'use strict';
9+
10+
var gd;
11+
var mock = require('@mocks/updatemenus.json');
12+
13+
beforeEach(function(done) {
14+
gd = createGraphDiv();
15+
16+
var mockCopy = Lib.extendDeep({}, mock);
17+
Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done);
18+
});
19+
20+
afterEach(function() {
21+
destroyGraphDiv(gd);
22+
});
23+
24+
it('updates bindings on plot', function(done) {
25+
Plotly.restyle(gd, 'marker.size', 10).catch(fail).then(done);
26+
});
27+
});

0 commit comments

Comments
 (0)