@@ -23,12 +23,13 @@ var attrPrefixRegex = /^(data|layout)(\[(-?[0-9]*)\])?\.(.*)$/;
23
23
* 2. only one property may be affected
24
24
* 3. the same property must be affected by all commands
25
25
*/
26
- exports . hasSimpleBindings = function ( gd , commandList ) {
26
+ exports . hasSimpleBindings = function ( gd , commandList , bindingsByValue ) {
27
27
var n = commandList . length ;
28
28
29
29
var refBinding ;
30
30
31
31
for ( var i = 0 ; i < n ; i ++ ) {
32
+ var binding ;
32
33
var command = commandList [ i ] ;
33
34
var method = command . method ;
34
35
var args = command . args ;
@@ -50,7 +51,7 @@ exports.hasSimpleBindings = function(gd, commandList) {
50
51
refBinding . traces . sort ( ) ;
51
52
}
52
53
} else {
53
- var binding = bindings [ 0 ] ;
54
+ binding = bindings [ 0 ] ;
54
55
if ( binding . type !== refBinding . type ) {
55
56
return false ;
56
57
}
@@ -74,9 +75,129 @@ exports.hasSimpleBindings = function(gd, commandList) {
74
75
}
75
76
}
76
77
}
78
+
79
+ binding = bindings [ 0 ] ;
80
+ var value = binding . value [ 0 ] ;
81
+ if ( Array . isArray ( value ) ) {
82
+ value = value [ 0 ] ;
83
+ }
84
+ bindingsByValue [ value ] = i ;
85
+ }
86
+
87
+ return refBinding ;
88
+ } ;
89
+
90
+ exports . createBindingObserver = function ( gd , commandList , onchange ) {
91
+ var cache = { } ;
92
+ var lookupTable = { } ;
93
+ var check , remove ;
94
+ var enabled = true ;
95
+
96
+ // Determine whether there's anything to do for this binding:
97
+ var binding ;
98
+ if ( ( binding = exports . hasSimpleBindings ( gd , commandList , lookupTable ) ) ) {
99
+ exports . bindingValueHasChanged ( gd , binding , cache ) ;
100
+
101
+ check = function check ( ) {
102
+ if ( ! enabled ) return ;
103
+
104
+ var container , value , obj ;
105
+ var changed = false ;
106
+
107
+ if ( binding . type === 'data' ) {
108
+ // If it's data, we need to get a trace. Based on the limited scope
109
+ // of what we cover, we can just take the first trace from the list,
110
+ // or otherwise just the first trace:
111
+ container = gd . _fullData [ binding . traces !== null ? binding . traces [ 0 ] : 0 ] ;
112
+ } else if ( binding . type === 'layout' ) {
113
+ container = gd . _fullLayout ;
114
+ } else {
115
+ return false ;
116
+ }
117
+
118
+ value = Lib . nestedProperty ( container , binding . prop ) . get ( ) ;
119
+
120
+ obj = cache [ binding . type ] = cache [ binding . type ] || { } ;
121
+
122
+ if ( obj . hasOwnProperty ( binding . prop ) ) {
123
+ if ( obj [ binding . prop ] !== value ) {
124
+ changed = true ;
125
+ }
126
+ }
127
+
128
+ obj [ binding . prop ] = value ;
129
+
130
+ if ( changed && onchange ) {
131
+ // Disable checks for the duration of this command in order to avoid
132
+ // infinite loops:
133
+ if ( lookupTable [ value ] !== undefined ) {
134
+ disable ( ) ;
135
+ Promise . resolve ( onchange ( {
136
+ value : value ,
137
+ type : binding . type ,
138
+ prop : binding . prop ,
139
+ traces : binding . traces ,
140
+ index : lookupTable [ value ]
141
+ } ) ) . then ( enable , enable ) ;
142
+ }
143
+ }
144
+
145
+ return changed ;
146
+ } ;
147
+
148
+ gd . _internalOn ( 'plotly_plotmodified' , check ) ;
149
+
150
+ remove = function ( ) {
151
+ gd . _removeInternalListener ( 'plotly_plotmodified' , check ) ;
152
+ } ;
153
+ } else {
154
+ lookupTable = { } ;
155
+ remove = function ( ) { } ;
156
+ }
157
+
158
+ function disable ( ) {
159
+ enabled = false ;
77
160
}
78
161
79
- return true ;
162
+ function enable ( ) {
163
+ enabled = true ;
164
+ }
165
+
166
+ return {
167
+ disable : disable ,
168
+ enable : enable ,
169
+ remove : remove
170
+ } ;
171
+ } ;
172
+
173
+ exports . bindingValueHasChanged = function ( gd , binding , cache ) {
174
+ var container , value , obj ;
175
+ var changed = false ;
176
+
177
+ if ( binding . type === 'data' ) {
178
+ // If it's data, we need to get a trace. Based on the limited scope
179
+ // of what we cover, we can just take the first trace from the list,
180
+ // or otherwise just the first trace:
181
+ container = gd . _fullData [ binding . traces !== null ? binding . traces [ 0 ] : 0 ] ;
182
+ } else if ( binding . type === 'layout' ) {
183
+ container = gd . _fullLayout ;
184
+ } else {
185
+ return false ;
186
+ }
187
+
188
+ value = Lib . nestedProperty ( container , binding . prop ) . get ( ) ;
189
+
190
+ obj = cache [ binding . type ] = cache [ binding . type ] || { } ;
191
+
192
+ if ( obj . hasOwnProperty ( binding . prop ) ) {
193
+ if ( obj [ binding . prop ] !== value ) {
194
+ changed = true ;
195
+ }
196
+ }
197
+
198
+ obj [ binding . prop ] = value ;
199
+
200
+ return changed ;
80
201
} ;
81
202
82
203
exports . evaluateAPICommandBinding = function ( gd , attrName ) {
@@ -133,10 +254,7 @@ exports.computeAPICommandBindings = function(gd, method, args) {
133
254
. concat ( computeLayoutBindings ( gd , [ args [ 1 ] ] ) ) ;
134
255
break ;
135
256
case 'animate' :
136
- // This case could be analyzed more in-depth, but for a start,
137
- // we'll assume that the only relevant modification an animation
138
- // makes that's meaningfully tracked is the frame:
139
- bindings = [ { type : 'layout' , prop : '_currentFrame' } ] ;
257
+ bindings = computeAnimateBindings ( gd , args ) ;
140
258
break ;
141
259
default :
142
260
// We'll elect to fail-non-fatal since this is a correct
@@ -146,6 +264,16 @@ exports.computeAPICommandBindings = function(gd, method, args) {
146
264
return bindings ;
147
265
} ;
148
266
267
+ function computeAnimateBindings ( gd , args ) {
268
+ // We'll assume that the only relevant modification an animation
269
+ // makes that's meaningfully tracked is the frame:
270
+ if ( Array . isArray ( args [ 0 ] ) && args [ 0 ] . length === 1 && typeof args [ 0 ] [ 0 ] === 'string' ) {
271
+ return [ { type : 'layout' , prop : '_currentFrame' , value : args [ 0 ] [ 0 ] } ] ;
272
+ } else {
273
+ return [ ] ;
274
+ }
275
+ }
276
+
149
277
function computeLayoutBindings ( gd , args ) {
150
278
var bindings = [ ] ;
151
279
@@ -159,8 +287,8 @@ function computeLayoutBindings(gd, args) {
159
287
return bindings ;
160
288
}
161
289
162
- crawl ( aobj , function ( path ) {
163
- bindings . push ( { type : 'layout' , prop : path } ) ;
290
+ crawl ( aobj , function ( path , attrName , attr ) {
291
+ bindings . push ( { type : 'layout' , prop : path , value : attr } ) ;
164
292
} , '' , 0 ) ;
165
293
166
294
return bindings ;
@@ -208,10 +336,27 @@ function computeDataBindings(gd, args) {
208
336
thisTraces = traces ? traces . slice ( 0 ) : null ;
209
337
}
210
338
339
+ // Convert [7] to just 7 when traces is null:
340
+ if ( thisTraces === null ) {
341
+ if ( Array . isArray ( attr ) ) {
342
+ attr = attr [ 0 ] ;
343
+ }
344
+ } else if ( Array . isArray ( thisTraces ) ) {
345
+ if ( ! Array . isArray ( attr ) ) {
346
+ var tmp = attr ;
347
+ attr = [ ] ;
348
+ for ( var i = 0 ; i < thisTraces . length ; i ++ ) {
349
+ attr [ i ] = tmp ;
350
+ }
351
+ }
352
+ attr . length = Math . min ( thisTraces . length , attr . length ) ;
353
+ }
354
+
211
355
bindings . push ( {
212
356
type : 'data' ,
213
357
prop : path ,
214
- traces : thisTraces
358
+ traces : thisTraces ,
359
+ value : attr
215
360
} ) ;
216
361
} , '' , 0 ) ;
217
362
0 commit comments