@@ -13,92 +13,63 @@ var Plotly = require('../plotly');
13
13
var Lib = require ( '../lib' ) ;
14
14
15
15
/*
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.
19
19
*
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.
23
30
*/
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 ;
26
34
27
- var refBinding ;
35
+ if ( container && container . _commandObserver ) {
36
+ ret = container . _commandObserver ;
37
+ }
28
38
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
+ }
34
42
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 = { } ;
40
45
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 ) ;
45
47
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 ;
50
56
}
51
57
} 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
+
76
63
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 ;
84
64
}
85
65
}
86
66
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
-
96
67
// 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 ) ;
100
68
101
- check = function check ( ) {
69
+ if ( binding ) {
70
+ bindingValueHasChanged ( gd , binding , ret . cache ) ;
71
+
72
+ ret . check = function check ( ) {
102
73
if ( ! enabled ) return ;
103
74
104
75
var container , value , obj ;
@@ -117,7 +88,7 @@ exports.createCommandObserver = function(gd, commandList, onchange) {
117
88
118
89
value = Lib . nestedProperty ( container , binding . prop ) . get ( ) ;
119
90
120
- obj = cache [ binding . type ] = cache [ binding . type ] || { } ;
91
+ obj = ret . cache [ binding . type ] = ret . cache [ binding . type ] || { } ;
121
92
122
93
if ( obj . hasOwnProperty ( binding . prop ) ) {
123
94
if ( obj [ binding . prop ] !== value ) {
@@ -130,15 +101,15 @@ exports.createCommandObserver = function(gd, commandList, onchange) {
130
101
if ( changed && onchange ) {
131
102
// Disable checks for the duration of this command in order to avoid
132
103
// infinite loops:
133
- if ( lookupTable [ value ] !== undefined ) {
134
- disable ( ) ;
104
+ if ( ret . lookupTable [ value ] !== undefined ) {
105
+ ret . disable ( ) ;
135
106
Promise . resolve ( onchange ( {
136
107
value : value ,
137
108
type : binding . type ,
138
109
prop : binding . prop ,
139
110
traces : binding . traces ,
140
- index : lookupTable [ value ]
141
- } ) ) . then ( enable , enable ) ;
111
+ index : ret . lookupTable [ value ]
112
+ } ) ) . then ( ret . enable , ret . enable ) ;
142
113
}
143
114
}
144
115
@@ -155,32 +126,111 @@ exports.createCommandObserver = function(gd, commandList, onchange) {
155
126
] ;
156
127
157
128
for ( var i = 0 ; i < checkEvents . length ; i ++ ) {
158
- gd . _internalOn ( checkEvents [ i ] , check ) ;
129
+ gd . _internalOn ( checkEvents [ i ] , ret . check ) ;
159
130
}
160
131
161
- remove = function ( ) {
132
+ ret . remove = function ( ) {
162
133
for ( var i = 0 ; i < checkEvents . length ; i ++ ) {
163
- gd . _removeInternalListener ( checkEvents [ i ] , check ) ;
134
+ gd . _removeInternalListener ( checkEvents [ i ] , ret . check ) ;
164
135
}
165
136
} ;
166
137
} 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 ( ) { } ;
169
144
}
170
145
171
- function disable ( ) {
146
+ ret . disable = function disable ( ) {
172
147
enabled = false ;
173
- }
148
+ } ;
174
149
175
- function enable ( ) {
150
+ ret . enable = function enable ( ) {
176
151
enabled = true ;
152
+ } ;
153
+
154
+ if ( container ) {
155
+ container . _commandObserver = ret ;
177
156
}
178
157
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 ;
184
234
} ;
185
235
186
236
function bindingValueHasChanged ( gd , binding , cache ) {
@@ -213,6 +263,17 @@ function bindingValueHasChanged(gd, binding, cache) {
213
263
return changed ;
214
264
}
215
265
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
+ */
216
277
exports . executeAPICommand = function ( gd , method , args ) {
217
278
var apiMethod = Plotly [ method ] ;
218
279
@@ -223,6 +284,7 @@ exports.executeAPICommand = function(gd, method, args) {
223
284
224
285
return apiMethod . apply ( null , allArgs ) . catch ( function ( err ) {
225
286
Lib . warn ( 'API call to Plotly.' + method + ' rejected.' , err ) ;
287
+ return Promise . reject ( err ) ;
226
288
} ) ;
227
289
} ;
228
290
@@ -243,9 +305,10 @@ exports.computeAPICommandBindings = function(gd, method, args) {
243
305
bindings = computeAnimateBindings ( gd , args ) ;
244
306
break ;
245
307
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 = [ ] ;
249
312
}
250
313
return bindings ;
251
314
} ;
0 commit comments