@@ -13,92 +13,63 @@ var Plotly = require('../plotly');
1313var 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
186236function 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+ */
216277exports . 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