Skip to content

Commit 93f2a53

Browse files
authored
Merge pull request #3888 from plotly/event-drag
`plotly_relayouting` event: live updates during panning/zooming
2 parents c23b69a + a76a52a commit 93f2a53

File tree

12 files changed

+427
-20
lines changed

12 files changed

+427
-20
lines changed

Diff for: src/plots/cartesian/dragbox.js

+20-10
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
8585
// graph-wide optimization flags
8686
var hasScatterGl, hasSplom, hasSVG;
8787
// collected changes to be made to the plot by relayout at the end
88-
var updates;
88+
var updates = {};
8989

9090
function recomputeAxisLists() {
9191
xa0 = plotinfo.xaxis;
@@ -409,18 +409,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
409409
gd._dragged = zoomDragged;
410410

411411
updateZoombox(zb, corners, box, path0, dimmed, lum);
412+
computeZoomUpdates();
413+
gd.emit('plotly_relayouting', updates);
412414
dimmed = true;
413415
}
414416

415-
function zoomDone() {
416-
updates = {};
417-
418-
// more strict than dragged, which allows you to come back to where you started
419-
// and still count as dragged
420-
if(Math.min(box.h, box.w) < MINDRAG * 2) {
421-
return removeZoombox(gd);
422-
}
423-
417+
function computeZoomUpdates() {
424418
// TODO: edit linked axes in zoomAxRanges and in dragTail
425419
if(zoomMode === 'xy' || zoomMode === 'x') {
426420
zoomAxRanges(xaxes, box.l / pw, box.r / pw, updates, links.xaxes);
@@ -430,6 +424,18 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
430424
zoomAxRanges(yaxes, (ph - box.b) / ph, (ph - box.t) / ph, updates, links.yaxes);
431425
updateMatchedAxRange('y', updates);
432426
}
427+
}
428+
429+
function zoomDone() {
430+
updates = {};
431+
432+
// more strict than dragged, which allows you to come back to where you started
433+
// and still count as dragged
434+
if(Math.min(box.h, box.w) < MINDRAG * 2) {
435+
return removeZoombox(gd);
436+
}
437+
438+
computeZoomUpdates();
433439

434440
removeZoombox(gd);
435441
dragTail();
@@ -515,6 +521,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
515521
updateSubplots(scrollViewBox);
516522
ticksAndAnnotations();
517523

524+
gd.emit('plotly_relayouting', updates);
525+
518526
// then replot after a delay to make sure
519527
// no more scrolling is coming
520528
redrawTimer = setTimeout(function() {
@@ -552,6 +560,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
552560
}
553561
updateSubplots([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]);
554562
ticksAndAnnotations();
563+
gd.emit('plotly_relayouting', updates);
555564
return;
556565
}
557566

@@ -626,6 +635,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
626635
updateMatchedAxRange('y');
627636
updateSubplots([xStart, yStart, pw - dx, ph - dy]);
628637
ticksAndAnnotations();
638+
gd.emit('plotly_relayouting', updates);
629639
}
630640

631641
function updateMatchedAxRange(axLetter, out) {

Diff for: src/plots/geo/zoom.js

+24
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ function zoomScoped(geo, projection) {
8686
.scale(d3.event.scale)
8787
.translate(d3.event.translate);
8888
geo.render();
89+
90+
var center = projection.invert(geo.midPt);
91+
geo.graphDiv.emit('plotly_relayouting', {
92+
'geo.projection.scale': projection.scale() / geo.fitScale,
93+
'geo.center.lon': center[0],
94+
'geo.center.lat': center[1]
95+
});
8996
}
9097

9198
function syncCb(set) {
@@ -164,6 +171,16 @@ function zoomNonClipped(geo, projection) {
164171

165172
didZoom = true;
166173
geo.render();
174+
175+
var rotate = projection.rotate();
176+
var center = projection.invert(geo.midPt);
177+
geo.graphDiv.emit('plotly_relayouting', {
178+
'geo.projection.scale': projection.scale() / geo.fitScale,
179+
'geo.center.lon': center[0],
180+
'geo.center.lat': center[1],
181+
'geo.projection.rotation.lon': -rotate[0]
182+
183+
});
167184
}
168185

169186
function handleZoomend() {
@@ -261,6 +278,13 @@ function zoomClipped(geo, projection) {
261278
})
262279
.on('zoom.redraw', function() {
263280
geo.render();
281+
282+
var _rotate = projection.rotate();
283+
geo.graphDiv.emit('plotly_relayouting', {
284+
'geo.projection.scale': projection.scale() / geo.fitScale,
285+
'geo.projection.rotation.lon': -_rotate[0],
286+
'geo.projection.rotation.lat': -_rotate[1]
287+
});
264288
});
265289

266290
function zoomstarted(dispatch) {

Diff for: src/plots/gl3d/scene.js

+9
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,15 @@ function initializeGLPlot(scene, pixelRatio, canvas, gl) {
272272
}
273273
}, passiveSupported ? {passive: false} : false);
274274

275+
scene.glplot.canvas.addEventListener('mousemove', function() {
276+
if(scene.fullSceneLayout.dragmode === false) return;
277+
if(scene.camera.mouseListener.buttons === 0) return;
278+
279+
var update = {};
280+
update[scene.id + '.camera'] = getLayoutCamera(scene.camera);
281+
scene.graphDiv.emit('plotly_relayouting', update);
282+
});
283+
275284
if(!scene.staticMode) {
276285
scene.glplot.canvas.addEventListener('webglcontextlost', function(event) {
277286
if(gd && gd.emit) {

Diff for: src/plots/mapbox/mapbox.js

+8
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,14 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) {
182182
map.on('dragstart', unhover);
183183
map.on('zoomstart', unhover);
184184

185+
function emitUpdate() {
186+
var viewNow = self.getView();
187+
gd.emit('plotly_relayouting', self.getViewEdits(viewNow));
188+
}
189+
190+
map.on('drag', emitUpdate);
191+
map.on('zoom', emitUpdate);
192+
185193
map.on('dblclick', function() {
186194
var optsNow = gd._fullLayout[self.id];
187195
Registry.call('_storeDirectGUIEdit', gd.layout, gd._fullLayout._preGUI, self.getViewEdits(optsNow));

Diff for: src/plots/polar/polar.js

+34-5
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,10 @@ proto.updateMainDrag = function(fullLayout) {
836836
corners.attr('d', cpath);
837837
dragBox.transitionZoombox(zb, corners, dimmed, lum);
838838
dimmed = true;
839+
840+
var updateObj = {};
841+
computeZoomUpdates(updateObj);
842+
gd.emit('plotly_relayouting', updateObj);
839843
}
840844

841845
function zoomMove(dx, dy) {
@@ -889,16 +893,22 @@ proto.updateMainDrag = function(fullLayout) {
889893
dragBox.removeZoombox(gd);
890894

891895
if(r0 === null || r1 === null) return;
896+
var updateObj = {};
897+
computeZoomUpdates(updateObj);
892898

893899
dragBox.showDoubleClickNotifier(gd);
894900

901+
Registry.call('_guiRelayout', gd, updateObj);
902+
}
903+
904+
function computeZoomUpdates(update) {
895905
var rl = radialAxis._rl;
896906
var m = (rl[1] - rl[0]) / (1 - innerRadius / radius) / radius;
897907
var newRng = [
898908
rl[0] + (r0 - innerRadius) * m,
899909
rl[0] + (r1 - innerRadius) * m
900910
];
901-
Registry.call('_guiRelayout', gd, _this.id + '.radialaxis.range', newRng);
911+
update[_this.id + '.radialaxis.range'] = newRng;
902912
}
903913

904914
function zoomClick(numClicks, evt) {
@@ -1037,6 +1047,18 @@ proto.updateRadialDrag = function(fullLayout, polarLayout, rngIndex) {
10371047
moveFn2 = comp < 0.5 ? rotateMove : rerangeMove;
10381048
}
10391049
}
1050+
1051+
var update = {};
1052+
computeRadialAxisUpdates(update);
1053+
gd.emit('plotly_relayouting', update);
1054+
}
1055+
1056+
function computeRadialAxisUpdates(update) {
1057+
if(angle1 !== null) {
1058+
update[_this.id + '.radialaxis.angle'] = angle1;
1059+
} else if(rprime !== null) {
1060+
update[_this.id + '.radialaxis.range[' + rngIndex + ']'] = rprime;
1061+
}
10401062
}
10411063

10421064
function doneFn() {
@@ -1236,18 +1258,25 @@ proto.updateAngularDrag = function(fullLayout) {
12361258
clearGlCanvases(gd);
12371259
redrawReglTraces(gd);
12381260
}
1239-
}
12401261

1241-
function doneFn() {
1242-
scatterTextPoints.select('text').attr('transform', null);
1262+
var update = {};
1263+
computeRotationUpdates(update);
1264+
gd.emit('plotly_relayouting', update);
1265+
}
12431266

1244-
var updateObj = {};
1267+
function computeRotationUpdates(updateObj) {
12451268
updateObj[_this.id + '.angularaxis.rotation'] = rot1;
12461269

12471270
if(_this.vangles) {
12481271
updateObj[_this.id + '.radialaxis.angle'] = rrot1;
12491272
}
1273+
}
12501274

1275+
function doneFn() {
1276+
scatterTextPoints.select('text').attr('transform', null);
1277+
1278+
var updateObj = {};
1279+
computeRotationUpdates(updateObj);
12511280
Registry.call('_guiRelayout', gd, updateObj);
12521281
}
12531282

Diff for: src/plots/ternary/ternary.js

+4
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,8 @@ proto.initInteractions = function() {
644644
.duration(200);
645645
dimmed = true;
646646
}
647+
648+
gd.emit('plotly_relayouting', makeUpdate(mins));
647649
}
648650

649651
function zoomDone() {
@@ -720,6 +722,8 @@ proto.initInteractions = function() {
720722
.select('.scatterlayer').selectAll('.trace')
721723
.call(Drawing.hideOutsideRangePoints, _this);
722724
}
725+
726+
gd.emit('plotly_relayouting', makeUpdate(mins));
723727
}
724728

725729
function dragDone() {

Diff for: test/jasmine/tests/cartesian_interact_test.js

+84-5
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,43 @@ describe('main plot pan', function() {
188188
.then(done);
189189
});
190190

191+
it('should emit plotly_relayouting events during pan interactions', function(done) {
192+
var mock = Lib.extendDeep({}, require('@mocks/10.json'));
193+
mock.layout.dragmode = 'pan';
194+
195+
function _drag(x0, y0, x1, y1, n) {
196+
mouseEvent('mousedown', x0, y0);
197+
var dx = (x1 - x0) / n;
198+
var dy = (y1 - y0) / n;
199+
for(var i = 0; i <= n; i++) {
200+
mouseEvent('mousemove', x0 + dx * i, y0 + dy * i);
201+
}
202+
mouseEvent('mouseup', x1, y1);
203+
}
204+
205+
var nsteps = 10; var events = []; var relayoutCallback;
206+
Plotly.plot(gd, mock.data, mock.layout)
207+
.then(function() {
208+
relayoutCallback = jasmine.createSpy('relayoutCallback');
209+
gd.on('plotly_relayout', relayoutCallback);
210+
gd.on('plotly_relayouting', function(e) {
211+
events.push(e);
212+
});
213+
_drag(100, 150, 220, 250, nsteps);
214+
})
215+
.then(function() {
216+
expect(events.length).toEqual(nsteps);
217+
var first = events.splice(0, 1)[0];
218+
var last = events.splice(-1, 1)[0];
219+
expect(first['xaxis.range[1]'] - first['xaxis.range[0]']).toBeCloseTo(6, 0);
220+
expect(last['xaxis.range[1]'] - last['xaxis.range[0]']).toBeCloseTo(6, 0);
221+
222+
expect(first['xaxis.range[1]'] - last['xaxis.range[1]']).toBeCloseTo(1, 0);
223+
})
224+
.catch(failTest)
225+
.then(done);
226+
});
227+
191228
it('should show/hide `cliponaxis: false` pts according to range', function(done) {
192229
function _assert(markerDisplay, textDisplay, barTextDisplay) {
193230
var gd3 = d3.select(gd);
@@ -289,10 +326,10 @@ describe('axis zoom/pan and main plot zoom', function() {
289326
return document.querySelector('.' + directions + 'drag[data-subplot="' + subplot + '"]');
290327
}
291328

292-
function doDrag(subplot, directions, dx, dy) {
329+
function doDrag(subplot, directions, dx, dy, nsteps) {
293330
return function() {
294331
var dragger = getDragger(subplot, directions);
295-
return drag(dragger, dx, dy);
332+
return drag(dragger, dx, dy, undefined, undefined, undefined, nsteps);
296333
};
297334
}
298335

@@ -311,7 +348,11 @@ describe('axis zoom/pan and main plot zoom', function() {
311348
var dy = opts.dy || 0;
312349
var dragger = getDragger(subplot, directions);
313350
var coords = getNodeCoords(dragger, edge);
314-
mouseEvent('scroll', coords.x + dx, coords.y + dy, {deltaY: deltaY, element: dragger});
351+
var nsteps = opts.nsteps || 1;
352+
353+
for(var i = 1; i <= nsteps; i++) {
354+
mouseEvent('scroll', coords.x + dx, coords.y + dy, {deltaY: deltaY / nsteps * i, element: dragger});
355+
}
315356
return delay(constants.REDRAWDELAY + 10)();
316357
};
317358
}
@@ -629,6 +670,44 @@ describe('axis zoom/pan and main plot zoom', function() {
629670
.then(done);
630671
});
631672

673+
it('should emit plotly_relayouting events when drawing zoom selection', function(done) {
674+
var nsteps = 10; var events = []; var relayoutCallback;
675+
Plotly.plot(gd, [{ y: [1, 2, 1] }])
676+
.then(function() {
677+
relayoutCallback = jasmine.createSpy('relayoutCallback');
678+
gd.on('plotly_relayout', relayoutCallback);
679+
gd.on('plotly_relayouting', function(e) {
680+
events.push(e);
681+
});
682+
})
683+
.then(doDrag('xy', 'nsew', 100, 100, nsteps))
684+
.then(function() {
685+
expect(events.length).toEqual(nsteps);
686+
expect(relayoutCallback).toHaveBeenCalledTimes(1);
687+
})
688+
.catch(failTest)
689+
.then(done);
690+
});
691+
692+
it('should emit plotly_relayouting events when zooming via mouse wheel', function(done) {
693+
var nsteps = 10; var events = []; var relayoutCallback;
694+
Plotly.plot(gd, [{ y: [1, 2, 1] }], {}, {scrollZoom: true})
695+
.then(function() {
696+
relayoutCallback = jasmine.createSpy('relayoutCallback');
697+
gd.on('plotly_relayout', relayoutCallback);
698+
gd.on('plotly_relayouting', function(e) {
699+
events.push(e);
700+
});
701+
})
702+
.then(doScroll('xy', 'nsew', 100, {edge: 'se', nsteps: nsteps}))
703+
.then(function() {
704+
expect(events.length).toEqual(nsteps);
705+
expect(relayoutCallback).toHaveBeenCalledTimes(1);
706+
})
707+
.catch(failTest)
708+
.then(done);
709+
});
710+
632711
it('handles xy, x-only and y-only zoombox updates', function(done) {
633712
function _assert(msg, xrng, yrng) {
634713
expect(gd.layout.xaxis.range).toBeCloseToArray(xrng, 2, 'xrng - ' + msg);
@@ -1466,15 +1545,15 @@ describe('axis zoom/pan and main plot zoom', function() {
14661545
return drag.start()
14671546
.then(_assert('just after start of zoombox', {
14681547
nodeCnt: 4,
1469-
xrng: [-0.1927, 3.1927],
1548+
xrng: [1.5, 1.6880],
14701549
hasDragData: true,
14711550
zoombox: 'M269.5,114.5h-3v41h3ZM300.5,114.5h3v41h-3Z',
14721551
clipTranslate: [0, 0]
14731552
}))
14741553
.then(delay(step))
14751554
.then(_assert('during zoombox drag', {
14761555
nodeCnt: 5,
1477-
xrng: [-0.257, 4.257],
1556+
xrng: [2, 2.2507],
14781557
hasDragData: true,
14791558
zoombox: 'M269.5,114.5h-3v41h3ZM300.5,114.5h3v41h-3Z',
14801559
clipTranslate: [0, 0]

0 commit comments

Comments
 (0)