Skip to content

Commit f8c0094

Browse files
committed
Improve robustness of command bindings
1 parent 99d7c8e commit f8c0094

File tree

4 files changed

+249
-106
lines changed

4 files changed

+249
-106
lines changed

src/components/sliders/draw.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,12 @@ module.exports = function draw(gd) {
7171

7272
computeLabelSteps(sliderOpts);
7373

74-
if(!sliderOpts._commandObserver) {
75-
sliderOpts._commandObserver = Plots.createCommandObserver(gd, sliderOpts.steps, function(data) {
76-
if(sliderOpts.active === data.index) return;
77-
if(sliderOpts._dragging) return;
74+
Plots.createCommandObserver(gd, sliderOpts, sliderOpts.steps, function(data) {
75+
if(sliderOpts.active === data.index) return;
76+
if(sliderOpts._dragging) return;
7877

79-
setActive(gd, gSlider, sliderOpts, data.index, false, true);
80-
});
81-
}
78+
setActive(gd, gSlider, sliderOpts, data.index, false, true);
79+
});
8280

8381
drawSlider(gd, d3.select(this), sliderOpts);
8482

src/components/updatemenus/draw.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,10 @@ module.exports = function draw(gd) {
113113
headerGroups.each(function(menuOpts) {
114114
var gHeader = d3.select(this);
115115

116-
if(!menuOpts._commandObserver) {
117-
var _gButton = menuOpts.type === 'dropdown' ? gButton : null;
118-
menuOpts._commandObserver = Plots.createCommandObserver(gd, menuOpts.buttons, function(data) {
119-
setActive(gd, menuOpts, menuOpts.buttons[data.index], gHeader, _gButton, data.index, true);
120-
});
121-
}
116+
var _gButton = menuOpts.type === 'dropdown' ? gButton : null;
117+
Plots.createCommandObserver(gd, menuOpts, menuOpts.buttons, function(data) {
118+
setActive(gd, menuOpts, menuOpts.buttons[data.index], gHeader, _gButton, data.index, true);
119+
});
122120

123121
if(menuOpts.type === 'dropdown') {
124122
drawHeader(gd, gHeader, gButton, menuOpts);

src/plots/command.js

Lines changed: 155 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -13,92 +13,63 @@ var Plotly = require('../plotly');
1313
var Lib = require('../lib');
1414

1515
/*
16-
* This function checks to see if an array of objects containing
17-
* method and args properties is compatible with automatic two-way
18-
* binding. The criteria right now are that
16+
* Create or update an observer. This function is designed to be
17+
* idempotent so that it can be called over and over as the component
18+
* updates, and will attach and detach listeners as needed.
1919
*
20-
* 1. multiple traces may be affected
21-
* 2. only one property may be affected
22-
* 3. the same property must be affected by all commands
20+
* @param {optional object} container
21+
* An object on which the observer is stored. This is the mechanism
22+
* by which it is idempotent. If it already exists, another won't be
23+
* added. Each time it's called, the value lookup table is updated.
24+
* @param {array} commandList
25+
* An array of commands, following either `buttons` of `updatemenus`
26+
* or `steps` of `sliders`.
27+
* @param {function} onchange
28+
* A listener called when the value is changed. Receives data object
29+
* with information about the new state.
2330
*/
24-
exports.hasSimpleAPICommandBindings = function(gd, commandList, bindingsByValue) {
25-
var n = commandList.length;
31+
exports.createCommandObserver = function(gd, container, commandList, onchange) {
32+
var ret = {};
33+
var enabled = true;
2634

27-
var refBinding;
35+
if(container && container._commandObserver) {
36+
ret = container._commandObserver;
37+
}
2838

29-
for(var i = 0; i < n; i++) {
30-
var binding;
31-
var command = commandList[i];
32-
var method = command.method;
33-
var args = command.args;
39+
if(!ret.cache) {
40+
ret.cache = {};
41+
}
3442

35-
// If any command has no method, refuse to bind:
36-
if(!method) {
37-
return false;
38-
}
39-
var bindings = exports.computeAPICommandBindings(gd, method, args);
43+
// Either create or just recompute this:
44+
ret.lookupTable = {};
4045

41-
// Right now, handle one and *only* one property being set:
42-
if(bindings.length !== 1) {
43-
return false;
44-
}
46+
var binding = exports.hasSimpleAPICommandBindings(gd, commandList, ret.lookupTable);
4547

46-
if(!refBinding) {
47-
refBinding = bindings[0];
48-
if(Array.isArray(refBinding.traces)) {
49-
refBinding.traces.sort();
48+
if(container && container._commandObserver) {
49+
if(!binding) {
50+
// If container exists and there are no longer any bindings,
51+
// remove existing:
52+
if(container._commandObserver.remove) {
53+
container._commandObserver.remove();
54+
container._commandObserver = null;
55+
return ret;
5056
}
5157
} else {
52-
binding = bindings[0];
53-
if(binding.type !== refBinding.type) {
54-
return false;
55-
}
56-
if(binding.prop !== refBinding.prop) {
57-
return false;
58-
}
59-
if(Array.isArray(refBinding.traces)) {
60-
if(Array.isArray(binding.traces)) {
61-
binding.traces.sort();
62-
for(var j = 0; j < refBinding.traces.length; j++) {
63-
if(refBinding.traces[j] !== binding.traces[j]) {
64-
return false;
65-
}
66-
}
67-
} else {
68-
return false;
69-
}
70-
} else {
71-
if(binding.prop !== refBinding.prop) {
72-
return false;
73-
}
74-
}
75-
}
58+
// If container exists and there *are* bindings, then the lookup
59+
// table should have been updated and check is already attached,
60+
// so there's nothing to be done:
61+
return ret;
62+
7663

77-
binding = bindings[0];
78-
var value = binding.value;
79-
if(Array.isArray(value)) {
80-
value = value[0];
81-
}
82-
if(bindingsByValue) {
83-
bindingsByValue[value] = i;
8464
}
8565
}
8666

87-
return refBinding;
88-
};
89-
90-
exports.createCommandObserver = function(gd, commandList, onchange) {
91-
var cache = {};
92-
var lookupTable = {};
93-
var check, remove;
94-
var enabled = true;
95-
9667
// Determine whether there's anything to do for this binding:
97-
var binding;
98-
if((binding = exports.hasSimpleAPICommandBindings(gd, commandList, lookupTable))) {
99-
bindingValueHasChanged(gd, binding, cache);
10068

101-
check = function check() {
69+
if(binding) {
70+
bindingValueHasChanged(gd, binding, ret.cache);
71+
72+
ret.check = function check() {
10273
if(!enabled) return;
10374

10475
var container, value, obj;
@@ -117,7 +88,7 @@ exports.createCommandObserver = function(gd, commandList, onchange) {
11788

11889
value = Lib.nestedProperty(container, binding.prop).get();
11990

120-
obj = cache[binding.type] = cache[binding.type] || {};
91+
obj = ret.cache[binding.type] = ret.cache[binding.type] || {};
12192

12293
if(obj.hasOwnProperty(binding.prop)) {
12394
if(obj[binding.prop] !== value) {
@@ -130,15 +101,15 @@ exports.createCommandObserver = function(gd, commandList, onchange) {
130101
if(changed && onchange) {
131102
// Disable checks for the duration of this command in order to avoid
132103
// infinite loops:
133-
if(lookupTable[value] !== undefined) {
134-
disable();
104+
if(ret.lookupTable[value] !== undefined) {
105+
ret.disable();
135106
Promise.resolve(onchange({
136107
value: value,
137108
type: binding.type,
138109
prop: binding.prop,
139110
traces: binding.traces,
140-
index: lookupTable[value]
141-
})).then(enable, enable);
111+
index: ret.lookupTable[value]
112+
})).then(ret.enable, ret.enable);
142113
}
143114
}
144115

@@ -155,32 +126,111 @@ exports.createCommandObserver = function(gd, commandList, onchange) {
155126
];
156127

157128
for(var i = 0; i < checkEvents.length; i++) {
158-
gd._internalOn(checkEvents[i], check);
129+
gd._internalOn(checkEvents[i], ret.check);
159130
}
160131

161-
remove = function() {
132+
ret.remove = function() {
162133
for(var i = 0; i < checkEvents.length; i++) {
163-
gd._removeInternalListener(checkEvents[i], check);
134+
gd._removeInternalListener(checkEvents[i], ret.check);
164135
}
165136
};
166137
} else {
167-
lookupTable = {};
168-
remove = function() {};
138+
// TODO: It'd be really neat to actually give a *reason* for this, but at least a warning
139+
// is a start
140+
Lib.warn('Unable to automatically bind plot updates to API command');
141+
142+
ret.lookupTable = {};
143+
ret.remove = function() {};
169144
}
170145

171-
function disable() {
146+
ret.disable = function disable() {
172147
enabled = false;
173-
}
148+
};
174149

175-
function enable() {
150+
ret.enable = function enable() {
176151
enabled = true;
152+
};
153+
154+
if(container) {
155+
container._commandObserver = ret;
177156
}
178157

179-
return {
180-
disable: disable,
181-
enable: enable,
182-
remove: remove
183-
};
158+
return ret;
159+
};
160+
161+
/*
162+
* This function checks to see if an array of objects containing
163+
* method and args properties is compatible with automatic two-way
164+
* binding. The criteria right now are that
165+
*
166+
* 1. multiple traces may be affected
167+
* 2. only one property may be affected
168+
* 3. the same property must be affected by all commands
169+
*/
170+
exports.hasSimpleAPICommandBindings = function(gd, commandList, bindingsByValue) {
171+
var n = commandList.length;
172+
173+
var refBinding;
174+
175+
for(var i = 0; i < n; i++) {
176+
var binding;
177+
var command = commandList[i];
178+
var method = command.method;
179+
var args = command.args;
180+
181+
// If any command has no method, refuse to bind:
182+
if(!method) {
183+
return false;
184+
}
185+
var bindings = exports.computeAPICommandBindings(gd, method, args);
186+
187+
// Right now, handle one and *only* one property being set:
188+
if(bindings.length !== 1) {
189+
return false;
190+
}
191+
192+
if(!refBinding) {
193+
refBinding = bindings[0];
194+
if(Array.isArray(refBinding.traces)) {
195+
refBinding.traces.sort();
196+
}
197+
} else {
198+
binding = bindings[0];
199+
if(binding.type !== refBinding.type) {
200+
return false;
201+
}
202+
if(binding.prop !== refBinding.prop) {
203+
return false;
204+
}
205+
if(Array.isArray(refBinding.traces)) {
206+
if(Array.isArray(binding.traces)) {
207+
binding.traces.sort();
208+
for(var j = 0; j < refBinding.traces.length; j++) {
209+
if(refBinding.traces[j] !== binding.traces[j]) {
210+
return false;
211+
}
212+
}
213+
} else {
214+
return false;
215+
}
216+
} else {
217+
if(binding.prop !== refBinding.prop) {
218+
return false;
219+
}
220+
}
221+
}
222+
223+
binding = bindings[0];
224+
var value = binding.value;
225+
if(Array.isArray(value)) {
226+
value = value[0];
227+
}
228+
if(bindingsByValue) {
229+
bindingsByValue[value] = i;
230+
}
231+
}
232+
233+
return refBinding;
184234
};
185235

186236
function bindingValueHasChanged(gd, binding, cache) {
@@ -213,6 +263,17 @@ function bindingValueHasChanged(gd, binding, cache) {
213263
return changed;
214264
}
215265

266+
/*
267+
* Execute an API command. There's really not much to this; it just provides
268+
* a common hook so that implementations don't need to be synchronized across
269+
* multiple components with the ability to invoke API commands.
270+
*
271+
* @param {string} method
272+
* The name of the plotly command to execute. Must be one of 'animate',
273+
* 'restyle', 'relayout', 'update'.
274+
* @param {array} args
275+
* A list of arguments passed to the API command
276+
*/
216277
exports.executeAPICommand = function(gd, method, args) {
217278
var apiMethod = Plotly[method];
218279

@@ -223,6 +284,7 @@ exports.executeAPICommand = function(gd, method, args) {
223284

224285
return apiMethod.apply(null, allArgs).catch(function(err) {
225286
Lib.warn('API call to Plotly.' + method + ' rejected.', err);
287+
return Promise.reject(err);
226288
});
227289
};
228290

@@ -243,9 +305,10 @@ exports.computeAPICommandBindings = function(gd, method, args) {
243305
bindings = computeAnimateBindings(gd, args);
244306
break;
245307
default:
246-
// This is the case where someone forgot to whitelist and implement
247-
// a new API method, so focus on failing visibly.
248-
throw new Error('Command bindings for Plotly.' + method + ' not implemented');
308+
// This is the case where intelligent logic about what affects
309+
// this command is not implemented. It causes no ill effects.
310+
// For example, addFrames simply won't bind to a control component.
311+
bindings = [];
249312
}
250313
return bindings;
251314
};

0 commit comments

Comments
 (0)