Skip to content

Commit 93b8253

Browse files
committed
First working version of bindings
1 parent 4bc8387 commit 93b8253

File tree

5 files changed

+224
-57
lines changed

5 files changed

+224
-57
lines changed

src/components/sliders/draw.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ module.exports = function draw(gd) {
5252
sliderGroups.exit().each(function(sliderOpts) {
5353
d3.select(this).remove();
5454

55+
sliderOpts._bindingObserver.remove();
56+
delete sliderOpts._bindingObserver;
57+
5558
Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index);
5659
});
5760

@@ -67,10 +70,15 @@ module.exports = function draw(gd) {
6770

6871
computeLabelSteps(sliderOpts);
6972

73+
if(!sliderOpts._bindingObserver) {
74+
sliderOpts._bindingObserver = Plots.createBindingObserver(gd, sliderOpts.steps, function(data) {
75+
setActive(gd, d3.select(this), sliderOpts, data.index, false, true);
76+
}.bind(this));
77+
}
78+
7079
drawSlider(gd, d3.select(this), sliderOpts);
7180

7281
// makeInputProxy(gd, d3.select(this), sliderOpts);
73-
7482
});
7583
};
7684

src/plot_api/plot_api.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,7 @@ Plotly.restyle = function restyle(gd, astr, val, traces) {
12041204

12051205
return plotDone.then(function() {
12061206
gd.emit('plotly_restyle', specs.eventData);
1207+
gd.emit('plotly_plotmodified');
12071208
return gd;
12081209
});
12091210
};
@@ -1710,6 +1711,7 @@ Plotly.relayout = function relayout(gd, astr, val) {
17101711

17111712
return plotDone.then(function() {
17121713
gd.emit('plotly_relayout', specs.eventData);
1714+
gd.emit('plotly_plotmodified');
17131715
return gd;
17141716
});
17151717
};
@@ -2124,6 +2126,7 @@ Plotly.update = function update(gd, traceUpdate, layoutUpdate, traces) {
21242126
data: restyleSpecs.eventData,
21252127
layout: relayoutSpecs.eventData
21262128
});
2129+
gd.emit('plotly_plotmodified');
21272130

21282131
return gd;
21292132
});
@@ -2324,6 +2327,8 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
23242327
newFrame.frameOpts,
23252328
newFrame.transitionOpts
23262329
);
2330+
2331+
gd.emit('plotly_plotmodified');
23272332
} else {
23282333
// If there are no more frames, then stop the RAF loop:
23292334
stopAnimationLoop();

src/plots/command.js

Lines changed: 155 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ var attrPrefixRegex = /^(data|layout)(\[(-?[0-9]*)\])?\.(.*)$/;
2323
* 2. only one property may be affected
2424
* 3. the same property must be affected by all commands
2525
*/
26-
exports.hasSimpleBindings = function(gd, commandList) {
26+
exports.hasSimpleBindings = function(gd, commandList, bindingsByValue) {
2727
var n = commandList.length;
2828

2929
var refBinding;
3030

3131
for(var i = 0; i < n; i++) {
32+
var binding;
3233
var command = commandList[i];
3334
var method = command.method;
3435
var args = command.args;
@@ -50,7 +51,7 @@ exports.hasSimpleBindings = function(gd, commandList) {
5051
refBinding.traces.sort();
5152
}
5253
} else {
53-
var binding = bindings[0];
54+
binding = bindings[0];
5455
if(binding.type !== refBinding.type) {
5556
return false;
5657
}
@@ -74,9 +75,129 @@ exports.hasSimpleBindings = function(gd, commandList) {
7475
}
7576
}
7677
}
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;
77160
}
78161

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;
80201
};
81202

82203
exports.evaluateAPICommandBinding = function(gd, attrName) {
@@ -133,10 +254,7 @@ exports.computeAPICommandBindings = function(gd, method, args) {
133254
.concat(computeLayoutBindings(gd, [args[1]]));
134255
break;
135256
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);
140258
break;
141259
default:
142260
// We'll elect to fail-non-fatal since this is a correct
@@ -146,6 +264,16 @@ exports.computeAPICommandBindings = function(gd, method, args) {
146264
return bindings;
147265
};
148266

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+
149277
function computeLayoutBindings(gd, args) {
150278
var bindings = [];
151279

@@ -159,8 +287,8 @@ function computeLayoutBindings(gd, args) {
159287
return bindings;
160288
}
161289

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});
164292
}, '', 0);
165293

166294
return bindings;
@@ -208,10 +336,27 @@ function computeDataBindings(gd, args) {
208336
thisTraces = traces ? traces.slice(0) : null;
209337
}
210338

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+
211355
bindings.push({
212356
type: 'data',
213357
prop: path,
214-
traces: thisTraces
358+
traces: thisTraces,
359+
value: attr
215360
});
216361
}, '', 0);
217362

src/plots/plots.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ plots.executeAPICommand = commandModule.executeAPICommand;
4343
plots.computeAPICommandBindings = commandModule.computeAPICommandBindings;
4444
plots.evaluateAPICommandBinding = commandModule.evaluateAPICommandBinding;
4545
plots.hasSimpleBindings = commandModule.hasSimpleBindings;
46+
plots.bindingValueHasChanged = commandModule.bindingValueHasChanged;
47+
plots.createBindingObserver = commandModule.createBindingObserver;
4648

4749
/**
4850
* Find subplot ids in data.

0 commit comments

Comments
 (0)