diff --git a/src/lib/index.js b/src/lib/index.js index 2d742a12e94..13a053004c4 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -449,12 +449,12 @@ lib.addStyleRule = function(selector, styleString) { lib.getTranslate = function(element) { - var re = /(\btranslate\()(\d*\.?\d*)([^\d]*)(\d*\.?\d*)([^\d]*)(.*)/, + var re = /.*\btranslate\((\d*\.?\d*)[^\d]*(\d*\.?\d*)[^\d].*/, getter = element.attr ? 'attr' : 'getAttribute', transform = element[getter]('transform') || ''; - var translate = transform.replace(re, function(match, p1, p2, p3, p4) { - return [p2, p4].join(' '); + var translate = transform.replace(re, function(match, p1, p2) { + return [p1, p2].join(' '); }) .split(' '); @@ -483,6 +483,42 @@ lib.setTranslate = function(element, x, y) { return transform; }; +lib.getScale = function(element) { + + var re = /.*\bscale\((\d*\.?\d*)[^\d]*(\d*\.?\d*)[^\d].*/, + getter = element.attr ? 'attr' : 'getAttribute', + transform = element[getter]('transform') || ''; + + var translate = transform.replace(re, function(match, p1, p2) { + return [p1, p2].join(' '); + }) + .split(' '); + + return { + x: +translate[0] || 1, + y: +translate[1] || 1 + }; +}; + +lib.setScale = function(element, x, y) { + + var re = /(\bscale\(.*?\);?)/, + getter = element.attr ? 'attr' : 'getAttribute', + setter = element.attr ? 'attr' : 'setAttribute', + transform = element[getter]('transform') || ''; + + x = x || 1; + y = y || 1; + + transform = transform.replace(re, '').trim(); + transform += ' scale(' + x + ', ' + y + ')'; + transform = transform.trim(); + + element[setter]('transform', transform); + + return transform; +}; + lib.isIE = function() { return typeof window.navigator.msSaveBlob !== 'undefined'; }; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index d6ed0a60d43..f567d260680 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2913,16 +2913,16 @@ function lsInner(gd) { // Clip so that data only shows up on the plot area. - var clips = fullLayout._defs.selectAll('g.clips'), - clipId = 'clip' + fullLayout._uid + subplot + 'plot'; + plotinfo.clipId = 'clip' + fullLayout._uid + subplot + 'plot'; - var plotClip = clips.selectAll('#' + clipId) + var plotClip = fullLayout._defs.selectAll('g.clips') + .selectAll('#' + plotinfo.clipId) .data([0]); plotClip.enter().append('clipPath') .attr({ 'class': 'plotclip', - 'id': clipId + 'id': plotinfo.clipId }) .append('rect'); @@ -2934,7 +2934,7 @@ function lsInner(gd) { plotinfo.plot.call(Lib.setTranslate, xa._offset, ya._offset); - plotinfo.plot.call(Drawing.setClipUrl, clipId); + plotinfo.plot.call(Drawing.setClipUrl, plotinfo.clipId); var xlw = Drawing.crispRound(gd, xa.linewidth, 1), ylw = Drawing.crispRound(gd, ya.linewidth, 1), diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index 995bc8fc665..81467cee968 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -69,5 +69,8 @@ module.exports = { HOVERMINTIME: 50, // max pixels off straight before a lasso select line counts as bent - BENDPX: 1.5 + BENDPX: 1.5, + + // delay before a redraw (relayout) after smooth panning and zooming + REDRAWDELAY: 50 }; diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index cccf64d56ba..b9f94001823 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -347,7 +347,7 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { var scrollViewBox = [0, 0, pw, ph], // wait a little after scrolling before redrawing redrawTimer = null, - REDRAWDELAY = 300, + REDRAWDELAY = constants.REDRAWDELAY, mainplot = plotinfo.mainplot ? fullLayout._plots[plotinfo.mainplot] : plotinfo; @@ -601,27 +601,34 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { subplots = Object.keys(plotinfos); for(var i = 0; i < subplots.length; i++) { + var subplot = plotinfos[subplots[i]], xa2 = subplot.x(), ya2 = subplot.y(), editX = ew && xa.indexOf(xa2) !== -1 && !xa2.fixedrange, editY = ns && ya.indexOf(ya2) !== -1 && !ya2.fixedrange; - if(editX || editY) { - // plot requires offset position and - // clip moves with opposite sign - var clipDx = editX ? viewBox[0] : 0, - clipDy = editY ? viewBox[1] : 0, - plotDx = xa2._offset - clipDx, - plotDy = ya2._offset - clipDy; - var clipId = 'clip' + fullLayout._uid + subplots[i] + 'plot'; + var xScaleFactor = editX ? xa2._length / viewBox[2] : 1, + yScaleFactor = editY ? ya2._length / viewBox[3] : 1; - fullLayout._defs.selectAll('#' + clipId) - .attr('transform', 'translate(' + clipDx + ', ' + clipDy + ')'); - subplot.plot - .attr('transform', 'translate(' + plotDx + ', ' + plotDy + ')'); - } + var clipDx = editX ? viewBox[0] : 0, + clipDy = editY ? viewBox[1] : 0; + + var fracDx = editX ? (viewBox[0] / viewBox[2] * xa2._length) : 0, + fracDy = editY ? (viewBox[1] / viewBox[3] * ya2._length) : 0; + + var plotDx = xa2._offset - fracDx, + plotDy = ya2._offset - fracDy; + + + fullLayout._defs.selectAll('#' + subplot.clipId) + .call(Lib.setTranslate, clipDx, clipDy) + .call(Lib.setScale, 1 / xScaleFactor, 1 / yScaleFactor); + + subplot.plot + .call(Lib.setTranslate, plotDx, plotDy) + .call(Lib.setScale, xScaleFactor, yScaleFactor); } } diff --git a/test/jasmine/assets/mouse_event.js b/test/jasmine/assets/mouse_event.js index 9800215bbfa..39101d76d35 100644 --- a/test/jasmine/assets/mouse_event.js +++ b/test/jasmine/assets/mouse_event.js @@ -10,7 +10,14 @@ module.exports = function(type, x, y, opts) { fullOpts.buttons = opts.buttons; } - var el = document.elementFromPoint(x, y); - var ev = new window.MouseEvent(type, fullOpts); + var el = document.elementFromPoint(x, y), + ev; + + if(type === 'scroll') { + ev = new window.WheelEvent('wheel', opts); + } else { + ev = new window.MouseEvent(type, fullOpts); + } + el.dispatchEvent(ev); }; diff --git a/test/jasmine/tests/click_test.js b/test/jasmine/tests/click_test.js index 4e0d5795d8f..c9f70b4dcd9 100644 --- a/test/jasmine/tests/click_test.js +++ b/test/jasmine/tests/click_test.js @@ -711,6 +711,35 @@ describe('Test click interactions:', function() { }); }); + describe('scroll zoom interactions', function() { + + beforeEach(function(done) { + Plotly.plot(gd, mockCopy.data, mockCopy.layout, { scrollZoom: true }).then(done); + }); + + it('zooms in on scroll up', function() { + + var plot = gd._fullLayout._plots.xy.plot; + + mouseEvent('mousemove', 400, 250); + mouseEvent('scroll', 400, 250, { deltaX: 0, deltaY: -1000 }); + + var transform = plot.attr('transform'); + + var mockEl = { + attr: function() { + return transform; + } + }; + + var translate = Lib.getTranslate(mockEl), + scale = Lib.getScale(mockEl); + + expect([translate.x, translate.y]).toBeCloseToArray([62.841, 99.483]); + expect([scale.x, scale.y]).toBeCloseToArray([1.221, 1.221]); + }); + }); + describe('pan interactions', function() { beforeEach(function(done) { mockCopy.layout.dragmode = 'pan'; @@ -745,7 +774,7 @@ describe('Test click interactions:', function() { mouseEvent('mousedown', start, start); mouseEvent('mousemove', end, end); - expect(plot.attr('transform')).toBe('translate(250, 280)'); + expect(plot.attr('transform')).toBe('translate(250, 280) scale(1, 1)'); mouseEvent('mouseup', end, end); }); diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 91a632d9e47..b736d31a73e 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -823,6 +823,9 @@ describe('Test lib.js:', function() { el.setAttribute('transform', 'translate(1 2); rotate(20deg)'); expect(Lib.getTranslate(el)).toEqual({ x: 1, y: 2 }); + el.setAttribute('transform', 'rotate(20deg) translate(1 2);'); + expect(Lib.getTranslate(el)).toEqual({ x: 1, y: 2 }); + el.setAttribute('transform', 'rotate(20deg)'); expect(Lib.getTranslate(el)).toEqual({ x: 0, y: 0 }); }); @@ -858,9 +861,6 @@ describe('Test lib.js:', function() { Lib.setTranslate(el, 10, 20); expect(el.getAttribute('transform')).toBe('translate(10, 20)'); - Lib.setTranslate(el, 30, 40); - expect(el.getAttribute('transform')).toBe('translate(30, 40)'); - Lib.setTranslate(el); expect(el.getAttribute('transform')).toBe('translate(0, 0)'); @@ -875,9 +875,6 @@ describe('Test lib.js:', function() { Lib.setTranslate(el, 5); expect(el.attr('transform')).toBe('translate(5, 0)'); - Lib.setTranslate(el, 10, 20); - expect(el.attr('transform')).toBe('translate(10, 20)'); - Lib.setTranslate(el, 30, 40); expect(el.attr('transform')).toBe('translate(30, 40)'); @@ -890,6 +887,89 @@ describe('Test lib.js:', function() { }); }); + describe('getScale', function() { + + it('should work with regular DOM elements', function() { + var el = document.createElement('div'); + + expect(Lib.getScale(el)).toEqual({ x: 1, y: 1 }); + + el.setAttribute('transform', 'scale(1.23, 45)'); + expect(Lib.getScale(el)).toEqual({ x: 1.23, y: 45 }); + + el.setAttribute('transform', 'scale(123.45)'); + expect(Lib.getScale(el)).toEqual({ x: 123.45, y: 1 }); + + el.setAttribute('transform', 'scale(0.1 2)'); + expect(Lib.getScale(el)).toEqual({ x: 0.1, y: 2 }); + + el.setAttribute('transform', 'scale(0.1 2); rotate(20deg)'); + expect(Lib.getScale(el)).toEqual({ x: 0.1, y: 2 }); + + el.setAttribute('transform', 'rotate(20deg) scale(0.1 2);'); + expect(Lib.getScale(el)).toEqual({ x: 0.1, y: 2 }); + + el.setAttribute('transform', 'rotate(20deg)'); + expect(Lib.getScale(el)).toEqual({ x: 1, y: 1 }); + }); + + it('should work with d3 elements', function() { + var el = d3.select(document.createElement('div')); + + el.attr('transform', 'scale(1.23, 45)'); + expect(Lib.getScale(el)).toEqual({ x: 1.23, y: 45 }); + + el.attr('transform', 'scale(123.45)'); + expect(Lib.getScale(el)).toEqual({ x: 123.45, y: 1 }); + + el.attr('transform', 'scale(0.1 2)'); + expect(Lib.getScale(el)).toEqual({ x: 0.1, y: 2 }); + + el.attr('transform', 'scale(0.1 2); rotate(20)'); + expect(Lib.getScale(el)).toEqual({ x: 0.1, y: 2 }); + + el.attr('transform', 'rotate(20)'); + expect(Lib.getScale(el)).toEqual({ x: 1, y: 1 }); + }); + }); + + describe('setScale', function() { + + it('should work with regular DOM elements', function() { + var el = document.createElement('div'); + + Lib.setScale(el, 5); + expect(el.getAttribute('transform')).toBe('scale(5, 1)'); + + Lib.setScale(el, 30, 40); + expect(el.getAttribute('transform')).toBe('scale(30, 40)'); + + Lib.setScale(el); + expect(el.getAttribute('transform')).toBe('scale(1, 1)'); + + el.setAttribute('transform', 'scale(1, 1); rotate(30)'); + Lib.setScale(el, 30, 40); + expect(el.getAttribute('transform')).toBe('rotate(30) scale(30, 40)'); + }); + + it('should work with d3 elements', function() { + var el = d3.select(document.createElement('div')); + + Lib.setScale(el, 5); + expect(el.attr('transform')).toBe('scale(5, 1)'); + + Lib.setScale(el, 30, 40); + expect(el.attr('transform')).toBe('scale(30, 40)'); + + Lib.setScale(el); + expect(el.attr('transform')).toBe('scale(1, 1)'); + + el.attr('transform', 'scale(0, 0); rotate(30)'); + Lib.setScale(el, 30, 40); + expect(el.attr('transform')).toBe('rotate(30) scale(30, 40)'); + }); + }); + describe('pushUnique', function() { beforeEach(function() {