diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index 77fa41ebdb3..167f96cebdf 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -28,7 +28,7 @@ module.exports = { // ms between first mousedown and 2nd mouseup to constitute dblclick... // we don't seem to have access to the system setting - DBLCLICKDELAY: 600, + DBLCLICKDELAY: 300, // pixels to move mouse before you stop clamping to starting point MINDRAG: 8, diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 1a13c920c5d..19c428761fa 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -1576,7 +1576,6 @@ function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { function zoomDone(dragged, numClicks) { if(Math.min(box.h, box.w) < MINDRAG * 2) { if(numClicks === 2) doubleClick(); - else pauseForDrag(gd); return removeZoombox(gd); } @@ -1629,7 +1628,6 @@ function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { } }); } - else pauseForDrag(gd); } // scroll zoom, on all draggers except corners @@ -1932,21 +1930,6 @@ function getEndText(ax, end) { } } -function pauseForDrag(gd) { - // prevent more redraws until we know if a doubleclick - // has occurred - gd._dragging = true; - var deferredReplot = gd._replotPending; - gd._replotPending = false; - - setTimeout(function() { - gd._replotPending = deferredReplot; - finishDrag(gd); - }, - constants.DBLCLICKDELAY - ); -} - function finishDrag(gd) { gd._dragging = false; if(gd._replotPending) Plotly.plot(gd); @@ -2032,11 +2015,6 @@ fx.dragElement = function(options) { if(!gd._mouseDownTime) gd._mouseDownTime = 0; function onStart(e) { - // because we cancel event bubbling, - // explicitly trigger input blur event. - var inputBox = document.querySelector('.plugin-editable'); - if(inputBox) d3.select(inputBox).on('blur').call(inputBox); - // make dragging and dragged into properties of gd // so that others can look at and modify them gd._dragged = false; diff --git a/test/jasmine/assets/get_rect_center.js b/test/jasmine/assets/get_rect_center.js new file mode 100644 index 00000000000..51b5df0128d --- /dev/null +++ b/test/jasmine/assets/get_rect_center.js @@ -0,0 +1,48 @@ +'use strict'; + + +/** + * Get the screen coordinates of the center of + * an SVG rectangle node. + * + * @param {rect} rect svg node + */ +module.exports = function getRectCenter(rect) { + var corners = getRectScreenCoords(rect); + + return [ + corners.nw.x + (corners.ne.x - corners.nw.x) / 2, + corners.ne.y + (corners.se.y - corners.ne.y) / 2 + ]; +}; + +// Taken from: http://stackoverflow.com/a/5835212/4068492 +function getRectScreenCoords(rect) { + var svg = findParentSVG(rect); + var pt = svg.createSVGPoint(); + var corners = {}; + var matrix = rect.getScreenCTM(); + + pt.x = rect.x.animVal.value; + pt.y = rect.y.animVal.value; + corners.nw = pt.matrixTransform(matrix); + pt.x += rect.width.animVal.value; + corners.ne = pt.matrixTransform(matrix); + pt.y += rect.height.animVal.value; + corners.se = pt.matrixTransform(matrix); + pt.x -= rect.width.animVal.value; + corners.sw = pt.matrixTransform(matrix); + + return corners; +} + +function findParentSVG(node) { + var parentNode = node.parentNode; + + if(parentNode.tagName === 'svg') { + return parentNode; + } + else { + return findParentSVG(parentNode); + } +} diff --git a/test/jasmine/tests/click_test.js b/test/jasmine/tests/click_test.js index 7b123dc6266..fe1a1e39d18 100644 --- a/test/jasmine/tests/click_test.js +++ b/test/jasmine/tests/click_test.js @@ -5,16 +5,30 @@ var DBLCLICKDELAY = require('@src/plots/cartesian/constants').DBLCLICKDELAY; var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var mouseEvent = require('../assets/mouse_event'); +var getRectCenter = require('../assets/get_rect_center'); var customMatchers = require('../assets/custom_matchers'); describe('Test click interactions:', function() { - var mock = require('@mocks/14.json'), - gd; + var mock = require('@mocks/14.json'); + + var mockCopy, gd; var pointPos = [351, 223], blankPos = [70, 363]; + var autoRangeX = [-3.011967491973726, 2.1561305597186564], + autoRangeY = [-0.9910086301469277, 1.389382716298284]; + + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + beforeEach(function() { + gd = createGraphDiv(); + mockCopy = Lib.extendDeep({}, mock); + }); + afterEach(destroyGraphDiv); // cartesian click events events use the hover data @@ -37,15 +51,24 @@ describe('Test click interactions:', function() { }); } + function drag(fromX, fromY, toX, toY, delay) { + return new Promise(function(resolve) { + mouseEvent('mousemove', fromX, fromY); + mouseEvent('mousedown', fromX, fromY); + mouseEvent('mousemove', toX, toY); + + setTimeout(function() { + mouseEvent('mouseup', toX, toY); + resolve(); + }, delay || DBLCLICKDELAY / 4); + }); + } + describe('click events', function() { var futureData; beforeEach(function(done) { - gd = createGraphDiv(); - - var mockCopy = Lib.extendDeep({}, mock); - Plotly.plot(gd, mockCopy.data, mockCopy.layout) - .then(done); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); gd.on('plotly_click', function(data) { futureData = data; @@ -77,11 +100,7 @@ describe('Test click interactions:', function() { var futureData; beforeEach(function(done) { - gd = createGraphDiv(); - - var mockCopy = Lib.extendDeep({}, mock); - Plotly.plot(gd, mockCopy.data, mockCopy.layout) - .then(done); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); gd.on('plotly_doubleclick', function(data) { futureData = data; @@ -97,12 +116,254 @@ describe('Test click interactions:', function() { }); }); - describe('double click interactions', function() { - var mockCopy; + describe('drag interactions', function() { + beforeEach(function(done) { + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); + + it('on nw dragbox should update the axis ranges', function(done) { + var node = document.querySelector('rect.nwdrag'); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe('drag'); + expect(node.classList[1]).toBe('nwdrag'); + expect(node.classList[2]).toBe('cursor-nw-resize'); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-3.08579746, 2.156130559]); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.99100863, 1.86546098]); + + return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.99100863, 1.10938115]); + + done(); + }); + }); + + it('on ne dragbox should update the axis ranges', function(done) { + var node = document.querySelector('rect.nedrag'); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe('drag'); + expect(node.classList[1]).toBe('nedrag'); + expect(node.classList[2]).toBe('cursor-ne-resize'); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-3.01196749, 1.72466470]); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.99100863, 1.86546098]); + + return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-3.01196749, 2.08350047]); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.99100863, 1.10938115]); + + done(); + }); + }); + + it('on sw dragbox should update the axis ranges', function(done) { + var node = document.querySelector('rect.swdrag'); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe('drag'); + expect(node.classList[1]).toBe('swdrag'); + expect(node.classList[2]).toBe('cursor-sw-resize'); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-3.08579746, 2.15613055]); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.36094210, 1.38938271]); + + return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-3.00958227, 2.15613055]); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.71100706, 1.38938271]); + + done(); + }); + }); + + it('on se dragbox should update the axis ranges', function(done) { + var node = document.querySelector('rect.sedrag'); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe('drag'); + expect(node.classList[1]).toBe('sedrag'); + expect(node.classList[2]).toBe('cursor-se-resize'); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-3.01196749, 1.72466470]); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.36094210, 1.38938271]); + + return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-3.01196749, 2.08350047]); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.71100706, 1.38938271]); + + done(); + }); + }); + + it('on ew dragbox should update the xaxis range', function(done) { + var node = document.querySelector('rect.ewdrag'); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe('drag'); + expect(node.classList[1]).toBe('ewdrag'); + expect(node.classList[2]).toBe('cursor-ew-resize'); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-3.375918058, 1.792179992]); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-3.01196749, 2.15613055]); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + done(); + }); + }); + + it('on w dragbox should update the xaxis range', function(done) { + var node = document.querySelector('rect.wdrag'); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe('drag'); + expect(node.classList[1]).toBe('wdrag'); + expect(node.classList[2]).toBe('cursor-w-resize'); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-3.40349007, 2.15613055]); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-2.93933740, 2.15613055]); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + done(); + }); + }); + + it('on e dragbox should update the xaxis range', function(done) { + var node = document.querySelector('rect.edrag'); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe('drag'); + expect(node.classList[1]).toBe('edrag'); + expect(node.classList[2]).toBe('cursor-e-resize'); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 50, pos[1] + 50).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-3.01196749, 1.7246647]); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + return drag(pos[0], pos[1], pos[0] - 50, pos[1] - 50); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-3.01196749, 2.0835004]); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + done(); + }); + }); - var autoRangeX = [-3.011967491973726, 2.1561305597186564], - autoRangeY = [-0.9910086301469277, 1.389382716298284]; + it('on ns dragbox should update the yaxis range', function(done) { + var node = document.querySelector('rect.nsdrag'); + var pos = getRectCenter(node); + expect(node.classList[0]).toBe('drag'); + expect(node.classList[1]).toBe('nsdrag'); + expect(node.classList[2]).toBe('cursor-ns-resize'); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.59427673, 1.78611460]); + + return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + done(); + }); + }); + + it('on s dragbox should update the yaxis range', function(done) { + var node = document.querySelector('rect.sdrag'); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe('drag'); + expect(node.classList[1]).toBe('sdrag'); + expect(node.classList[2]).toBe('cursor-s-resize'); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.3609421011, 1.3893827]); + + return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.7110070646, 1.3893827]); + + done(); + }); + }); + + it('on n dragbox should update the yaxis range', function(done) { + var node = document.querySelector('rect.ndrag'); + var pos = getRectCenter(node); + + expect(node.classList[0]).toBe('drag'); + expect(node.classList[1]).toBe('ndrag'); + expect(node.classList[2]).toBe('cursor-n-resize'); + + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(pos[0], pos[1], pos[0] + 10, pos[1] + 50).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.991008630, 1.86546098]); + + return drag(pos[0], pos[1], pos[0] - 10, pos[1] - 50); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.991008630, 1.10938115]); + + done(); + }); + }); + + }); + + describe('double click interactions', function() { var setRangeX = [-3, 1], setRangeY = [-0.5, 1]; @@ -116,15 +377,6 @@ describe('Test click interactions:', function() { 'yaxis.range[1]': zoomRangeY[1] }; - beforeAll(function() { - jasmine.addMatchers(customMatchers); - }); - - beforeEach(function() { - gd = createGraphDiv(); - mockCopy = Lib.extendDeep({}, mock); - }); - function setRanges(mockCopy) { mockCopy.layout.xaxis.autorange = false; mockCopy.layout.xaxis.range = setRangeX.slice(); @@ -140,17 +392,17 @@ describe('Test click interactions:', function() { expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - Plotly.relayout(gd, update).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); + return Plotly.relayout(gd, update); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(zoomRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(zoomRangeY); - return doubleClick(blankPos[0], blankPos[1]); - }).then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); - expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + return doubleClick(blankPos[0], blankPos[1]); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); - done(); - }); + done(); }); }); @@ -196,6 +448,27 @@ describe('Test click interactions:', function() { }); }); + it('when set to \'reset+autorange\' (the default) should autosize on 1st double click and zoom when immediately dragged', function(done) { + mockCopy = setRanges(mockCopy); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(setRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(setRangeY); + + return doubleClick(blankPos[0], blankPos[1]); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + return drag(100, 100, 200, 200, DBLCLICKDELAY / 2); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-2.70624901567643, -1.9783478816352495]); + expect(gd.layout.yaxis.range).toBeCloseToArray([0.5007032802920716, 1.2941670624404753]); + + done(); + }); + }); + it('when set to \'reset+autorange\' (the default) should follow updated auto ranges', function(done) { var updateData = { x: [[1e-4, 0, 1e3]], @@ -346,4 +619,52 @@ describe('Test click interactions:', function() { }); }); + + describe('zoom interactions', function() { + beforeEach(function(done) { + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); + + it('on main dragbox should update the axis ranges', function(done) { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(100, 100, 400, 300).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-2.70624901, -0.52254561]); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.29276050, 1.294167062]); + + return drag(100, 100, 400, 300); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-2.57707219, -1.65438061]); + expect(gd.layout.yaxis.range).toBeCloseToArray([0.172738250, 1.230689959]); + + done(); + }); + }); + }); + + describe('pan interactions', function() { + beforeEach(function(done) { + mockCopy.layout.dragmode = 'pan'; + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); + + it('on main dragbox should update the axis ranges', function(done) { + expect(gd.layout.xaxis.range).toBeCloseToArray(autoRangeX); + expect(gd.layout.yaxis.range).toBeCloseToArray(autoRangeY); + + drag(100, 100, 400, 300).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-5.19567089, -0.02757284]); + expect(gd.layout.yaxis.range).toBeCloseToArray([0.595918934, 2.976310280]); + + return drag(100, 100, 400, 300); + }).then(function() { + expect(gd.layout.xaxis.range).toBeCloseToArray([-7.37937429, -2.21127624]); + expect(gd.layout.yaxis.range).toBeCloseToArray([2.182846498, 4.563237844]); + + done(); + }); + }); + }); });