Skip to content

Commit e369300

Browse files
committed
Merge pull request #564 from plotly/zoomscroll-fix
Zoomscroll fix
2 parents b5331e3 + cf62849 commit e369300

File tree

7 files changed

+194
-32
lines changed

7 files changed

+194
-32
lines changed

src/lib/index.js

+39-3
Original file line numberDiff line numberDiff line change
@@ -449,12 +449,12 @@ lib.addStyleRule = function(selector, styleString) {
449449

450450
lib.getTranslate = function(element) {
451451

452-
var re = /(\btranslate\()(\d*\.?\d*)([^\d]*)(\d*\.?\d*)([^\d]*)(.*)/,
452+
var re = /.*\btranslate\((\d*\.?\d*)[^\d]*(\d*\.?\d*)[^\d].*/,
453453
getter = element.attr ? 'attr' : 'getAttribute',
454454
transform = element[getter]('transform') || '';
455455

456-
var translate = transform.replace(re, function(match, p1, p2, p3, p4) {
457-
return [p2, p4].join(' ');
456+
var translate = transform.replace(re, function(match, p1, p2) {
457+
return [p1, p2].join(' ');
458458
})
459459
.split(' ');
460460

@@ -483,6 +483,42 @@ lib.setTranslate = function(element, x, y) {
483483
return transform;
484484
};
485485

486+
lib.getScale = function(element) {
487+
488+
var re = /.*\bscale\((\d*\.?\d*)[^\d]*(\d*\.?\d*)[^\d].*/,
489+
getter = element.attr ? 'attr' : 'getAttribute',
490+
transform = element[getter]('transform') || '';
491+
492+
var translate = transform.replace(re, function(match, p1, p2) {
493+
return [p1, p2].join(' ');
494+
})
495+
.split(' ');
496+
497+
return {
498+
x: +translate[0] || 1,
499+
y: +translate[1] || 1
500+
};
501+
};
502+
503+
lib.setScale = function(element, x, y) {
504+
505+
var re = /(\bscale\(.*?\);?)/,
506+
getter = element.attr ? 'attr' : 'getAttribute',
507+
setter = element.attr ? 'attr' : 'setAttribute',
508+
transform = element[getter]('transform') || '';
509+
510+
x = x || 1;
511+
y = y || 1;
512+
513+
transform = transform.replace(re, '').trim();
514+
transform += ' scale(' + x + ', ' + y + ')';
515+
transform = transform.trim();
516+
517+
element[setter]('transform', transform);
518+
519+
return transform;
520+
};
521+
486522
lib.isIE = function() {
487523
return typeof window.navigator.msSaveBlob !== 'undefined';
488524
};

src/plot_api/plot_api.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -2913,16 +2913,16 @@ function lsInner(gd) {
29132913

29142914

29152915
// Clip so that data only shows up on the plot area.
2916-
var clips = fullLayout._defs.selectAll('g.clips'),
2917-
clipId = 'clip' + fullLayout._uid + subplot + 'plot';
2916+
plotinfo.clipId = 'clip' + fullLayout._uid + subplot + 'plot';
29182917

2919-
var plotClip = clips.selectAll('#' + clipId)
2918+
var plotClip = fullLayout._defs.selectAll('g.clips')
2919+
.selectAll('#' + plotinfo.clipId)
29202920
.data([0]);
29212921

29222922
plotClip.enter().append('clipPath')
29232923
.attr({
29242924
'class': 'plotclip',
2925-
'id': clipId
2925+
'id': plotinfo.clipId
29262926
})
29272927
.append('rect');
29282928

@@ -2934,7 +2934,7 @@ function lsInner(gd) {
29342934

29352935

29362936
plotinfo.plot.call(Lib.setTranslate, xa._offset, ya._offset);
2937-
plotinfo.plot.call(Drawing.setClipUrl, clipId);
2937+
plotinfo.plot.call(Drawing.setClipUrl, plotinfo.clipId);
29382938

29392939
var xlw = Drawing.crispRound(gd, xa.linewidth, 1),
29402940
ylw = Drawing.crispRound(gd, ya.linewidth, 1),

src/plots/cartesian/constants.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,8 @@ module.exports = {
6969
HOVERMINTIME: 50,
7070

7171
// max pixels off straight before a lasso select line counts as bent
72-
BENDPX: 1.5
72+
BENDPX: 1.5,
73+
74+
// delay before a redraw (relayout) after smooth panning and zooming
75+
REDRAWDELAY: 50
7376
};

src/plots/cartesian/dragbox.js

+21-14
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) {
347347
var scrollViewBox = [0, 0, pw, ph],
348348
// wait a little after scrolling before redrawing
349349
redrawTimer = null,
350-
REDRAWDELAY = 300,
350+
REDRAWDELAY = constants.REDRAWDELAY,
351351
mainplot = plotinfo.mainplot ?
352352
fullLayout._plots[plotinfo.mainplot] : plotinfo;
353353

@@ -601,27 +601,34 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) {
601601
subplots = Object.keys(plotinfos);
602602

603603
for(var i = 0; i < subplots.length; i++) {
604+
604605
var subplot = plotinfos[subplots[i]],
605606
xa2 = subplot.x(),
606607
ya2 = subplot.y(),
607608
editX = ew && xa.indexOf(xa2) !== -1 && !xa2.fixedrange,
608609
editY = ns && ya.indexOf(ya2) !== -1 && !ya2.fixedrange;
609610

610-
if(editX || editY) {
611-
// plot requires offset position and
612-
// clip moves with opposite sign
613-
var clipDx = editX ? viewBox[0] : 0,
614-
clipDy = editY ? viewBox[1] : 0,
615-
plotDx = xa2._offset - clipDx,
616-
plotDy = ya2._offset - clipDy;
617611

618-
var clipId = 'clip' + fullLayout._uid + subplots[i] + 'plot';
612+
var xScaleFactor = editX ? xa2._length / viewBox[2] : 1,
613+
yScaleFactor = editY ? ya2._length / viewBox[3] : 1;
619614

620-
fullLayout._defs.selectAll('#' + clipId)
621-
.attr('transform', 'translate(' + clipDx + ', ' + clipDy + ')');
622-
subplot.plot
623-
.attr('transform', 'translate(' + plotDx + ', ' + plotDy + ')');
624-
}
615+
var clipDx = editX ? viewBox[0] : 0,
616+
clipDy = editY ? viewBox[1] : 0;
617+
618+
var fracDx = editX ? (viewBox[0] / viewBox[2] * xa2._length) : 0,
619+
fracDy = editY ? (viewBox[1] / viewBox[3] * ya2._length) : 0;
620+
621+
var plotDx = xa2._offset - fracDx,
622+
plotDy = ya2._offset - fracDy;
623+
624+
625+
fullLayout._defs.selectAll('#' + subplot.clipId)
626+
.call(Lib.setTranslate, clipDx, clipDy)
627+
.call(Lib.setScale, 1 / xScaleFactor, 1 / yScaleFactor);
628+
629+
subplot.plot
630+
.call(Lib.setTranslate, plotDx, plotDy)
631+
.call(Lib.setScale, xScaleFactor, yScaleFactor);
625632
}
626633
}
627634

test/jasmine/assets/mouse_event.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ module.exports = function(type, x, y, opts) {
1010
fullOpts.buttons = opts.buttons;
1111
}
1212

13-
var el = document.elementFromPoint(x, y);
14-
var ev = new window.MouseEvent(type, fullOpts);
13+
var el = document.elementFromPoint(x, y),
14+
ev;
15+
16+
if(type === 'scroll') {
17+
ev = new window.WheelEvent('wheel', opts);
18+
} else {
19+
ev = new window.MouseEvent(type, fullOpts);
20+
}
21+
1522
el.dispatchEvent(ev);
1623
};

test/jasmine/tests/click_test.js

+30-1
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,35 @@ describe('Test click interactions:', function() {
711711
});
712712
});
713713

714+
describe('scroll zoom interactions', function() {
715+
716+
beforeEach(function(done) {
717+
Plotly.plot(gd, mockCopy.data, mockCopy.layout, { scrollZoom: true }).then(done);
718+
});
719+
720+
it('zooms in on scroll up', function() {
721+
722+
var plot = gd._fullLayout._plots.xy.plot;
723+
724+
mouseEvent('mousemove', 400, 250);
725+
mouseEvent('scroll', 400, 250, { deltaX: 0, deltaY: -1000 });
726+
727+
var transform = plot.attr('transform');
728+
729+
var mockEl = {
730+
attr: function() {
731+
return transform;
732+
}
733+
};
734+
735+
var translate = Lib.getTranslate(mockEl),
736+
scale = Lib.getScale(mockEl);
737+
738+
expect([translate.x, translate.y]).toBeCloseToArray([62.841, 99.483]);
739+
expect([scale.x, scale.y]).toBeCloseToArray([1.221, 1.221]);
740+
});
741+
});
742+
714743
describe('pan interactions', function() {
715744
beforeEach(function(done) {
716745
mockCopy.layout.dragmode = 'pan';
@@ -745,7 +774,7 @@ describe('Test click interactions:', function() {
745774
mouseEvent('mousedown', start, start);
746775
mouseEvent('mousemove', end, end);
747776

748-
expect(plot.attr('transform')).toBe('translate(250, 280)');
777+
expect(plot.attr('transform')).toBe('translate(250, 280) scale(1, 1)');
749778

750779
mouseEvent('mouseup', end, end);
751780
});

test/jasmine/tests/lib_test.js

+86-6
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,9 @@ describe('Test lib.js:', function() {
823823
el.setAttribute('transform', 'translate(1 2); rotate(20deg)');
824824
expect(Lib.getTranslate(el)).toEqual({ x: 1, y: 2 });
825825

826+
el.setAttribute('transform', 'rotate(20deg) translate(1 2);');
827+
expect(Lib.getTranslate(el)).toEqual({ x: 1, y: 2 });
828+
826829
el.setAttribute('transform', 'rotate(20deg)');
827830
expect(Lib.getTranslate(el)).toEqual({ x: 0, y: 0 });
828831
});
@@ -858,9 +861,6 @@ describe('Test lib.js:', function() {
858861
Lib.setTranslate(el, 10, 20);
859862
expect(el.getAttribute('transform')).toBe('translate(10, 20)');
860863

861-
Lib.setTranslate(el, 30, 40);
862-
expect(el.getAttribute('transform')).toBe('translate(30, 40)');
863-
864864
Lib.setTranslate(el);
865865
expect(el.getAttribute('transform')).toBe('translate(0, 0)');
866866

@@ -875,9 +875,6 @@ describe('Test lib.js:', function() {
875875
Lib.setTranslate(el, 5);
876876
expect(el.attr('transform')).toBe('translate(5, 0)');
877877

878-
Lib.setTranslate(el, 10, 20);
879-
expect(el.attr('transform')).toBe('translate(10, 20)');
880-
881878
Lib.setTranslate(el, 30, 40);
882879
expect(el.attr('transform')).toBe('translate(30, 40)');
883880

@@ -890,6 +887,89 @@ describe('Test lib.js:', function() {
890887
});
891888
});
892889

890+
describe('getScale', function() {
891+
892+
it('should work with regular DOM elements', function() {
893+
var el = document.createElement('div');
894+
895+
expect(Lib.getScale(el)).toEqual({ x: 1, y: 1 });
896+
897+
el.setAttribute('transform', 'scale(1.23, 45)');
898+
expect(Lib.getScale(el)).toEqual({ x: 1.23, y: 45 });
899+
900+
el.setAttribute('transform', 'scale(123.45)');
901+
expect(Lib.getScale(el)).toEqual({ x: 123.45, y: 1 });
902+
903+
el.setAttribute('transform', 'scale(0.1 2)');
904+
expect(Lib.getScale(el)).toEqual({ x: 0.1, y: 2 });
905+
906+
el.setAttribute('transform', 'scale(0.1 2); rotate(20deg)');
907+
expect(Lib.getScale(el)).toEqual({ x: 0.1, y: 2 });
908+
909+
el.setAttribute('transform', 'rotate(20deg) scale(0.1 2);');
910+
expect(Lib.getScale(el)).toEqual({ x: 0.1, y: 2 });
911+
912+
el.setAttribute('transform', 'rotate(20deg)');
913+
expect(Lib.getScale(el)).toEqual({ x: 1, y: 1 });
914+
});
915+
916+
it('should work with d3 elements', function() {
917+
var el = d3.select(document.createElement('div'));
918+
919+
el.attr('transform', 'scale(1.23, 45)');
920+
expect(Lib.getScale(el)).toEqual({ x: 1.23, y: 45 });
921+
922+
el.attr('transform', 'scale(123.45)');
923+
expect(Lib.getScale(el)).toEqual({ x: 123.45, y: 1 });
924+
925+
el.attr('transform', 'scale(0.1 2)');
926+
expect(Lib.getScale(el)).toEqual({ x: 0.1, y: 2 });
927+
928+
el.attr('transform', 'scale(0.1 2); rotate(20)');
929+
expect(Lib.getScale(el)).toEqual({ x: 0.1, y: 2 });
930+
931+
el.attr('transform', 'rotate(20)');
932+
expect(Lib.getScale(el)).toEqual({ x: 1, y: 1 });
933+
});
934+
});
935+
936+
describe('setScale', function() {
937+
938+
it('should work with regular DOM elements', function() {
939+
var el = document.createElement('div');
940+
941+
Lib.setScale(el, 5);
942+
expect(el.getAttribute('transform')).toBe('scale(5, 1)');
943+
944+
Lib.setScale(el, 30, 40);
945+
expect(el.getAttribute('transform')).toBe('scale(30, 40)');
946+
947+
Lib.setScale(el);
948+
expect(el.getAttribute('transform')).toBe('scale(1, 1)');
949+
950+
el.setAttribute('transform', 'scale(1, 1); rotate(30)');
951+
Lib.setScale(el, 30, 40);
952+
expect(el.getAttribute('transform')).toBe('rotate(30) scale(30, 40)');
953+
});
954+
955+
it('should work with d3 elements', function() {
956+
var el = d3.select(document.createElement('div'));
957+
958+
Lib.setScale(el, 5);
959+
expect(el.attr('transform')).toBe('scale(5, 1)');
960+
961+
Lib.setScale(el, 30, 40);
962+
expect(el.attr('transform')).toBe('scale(30, 40)');
963+
964+
Lib.setScale(el);
965+
expect(el.attr('transform')).toBe('scale(1, 1)');
966+
967+
el.attr('transform', 'scale(0, 0); rotate(30)');
968+
Lib.setScale(el, 30, 40);
969+
expect(el.attr('transform')).toBe('rotate(30) scale(30, 40)');
970+
});
971+
});
972+
893973
describe('pushUnique', function() {
894974

895975
beforeEach(function() {

0 commit comments

Comments
 (0)