diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 7c4e21b69d0..d8948fcb50e 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -507,6 +507,25 @@ function setPlotContext(gd, config) { // Check if gd has a specified widht/height to begin with context._hasZeroHeight = context._hasZeroHeight || gd.clientHeight === 0; context._hasZeroWidth = context._hasZeroWidth || gd.clientWidth === 0; + + // fill context._scrollZoom helper to help manage scrollZoom flaglist + var szIn = context.scrollZoom; + var szOut = context._scrollZoom = {}; + if(szIn === true) { + szOut.cartesian = 1; + szOut.gl3d = 1; + szOut.geo = 1; + szOut.mapbox = 1; + } else if(typeof szIn === 'string') { + var parts = szIn.split('+'); + for(i = 0; i < parts.length; i++) { + szOut[parts[i]] = 1; + } + } else if(szIn !== false) { + szOut.gl3d = 1; + szOut.geo = 1; + szOut.mapbox = 1; + } } function plotLegacyPolar(gd, data, layout) { diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index 13f88a4ed48..bac84f84ecc 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -145,11 +145,16 @@ var configAttributes = { }, scrollZoom: { - valType: 'boolean', - dflt: false, + valType: 'flaglist', + flags: ['cartesian', 'gl3d', 'geo', 'mapbox'], + extras: [true, false], + dflt: 'gl3d+geo+mapbox', description: [ - 'Determines whether mouse wheel or two-finger scroll zooms is', - 'enable. Has an effect only on cartesian subplots.' + 'Determines whether mouse wheel or two-finger scroll zooms is enable.', + 'Turned on by default for gl3d, geo and mapbox subplots', + '(as these subplot types do not have zoombox via pan),', + 'but turned off by default for cartesian subplots.', + 'Set `scrollZoom` to *false* to disable scrolling for all subplots.' ].join(' ') }, doubleClick: { diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index e8110756b0c..10d2cb97e60 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -417,7 +417,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // deactivate mousewheel scrolling on embedded graphs // devs can override this with layout._enablescrollzoom, // but _ ensures this setting won't leave their page - if(!gd._context.scrollZoom && !gd._fullLayout._enablescrollzoom) { + if(!gd._context._scrollZoom.cartesian && !gd._fullLayout._enablescrollzoom) { return; } diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index ce4fb293994..9a0961f1661 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -420,6 +420,9 @@ proto.updateFx = function(fullLayout, geoLayout) { bgRect.node().onmousedown = null; bgRect.call(createGeoZoom(_this, geoLayout)); bgRect.on('dblclick.zoom', zoomReset); + if(!gd._context._scrollZoom.geo) { + bgRect.on('wheel.zoom', null); + } } else if(dragMode === 'select' || dragMode === 'lasso') { bgRect.on('.zoom', null); diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 0b8ea837ea8..789efdfe096 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -38,7 +38,7 @@ function Scene2D(options, fullLayout) { this.pixelRatio = options.plotGlPixelRatio || window.devicePixelRatio; this.id = options.id; this.staticPlot = !!options.staticPlot; - this.scrollZoom = this.graphDiv._context.scrollZoom; + this.scrollZoom = this.graphDiv._context._scrollZoom.cartesian; this.fullData = null; this.updateRefs(fullLayout); diff --git a/src/plots/gl3d/camera.js b/src/plots/gl3d/camera.js index 0331cd3f0b3..32e796a8760 100644 --- a/src/plots/gl3d/camera.js +++ b/src/plots/gl3d/camera.js @@ -48,6 +48,7 @@ function createCamera(element, options) { var camera = { keyBindingMode: 'rotate', + enableWheel: true, view: view, element: element, delay: options.delay || 16, @@ -257,7 +258,9 @@ function createCamera(element, options) { } camera.wheelListener = mouseWheel(element, function(dx, dy) { + // TODO remove now that we can disable scroll via scrollZoom? if(camera.keyBindingMode === false) return; + if(!camera.enableWheel) return; var flipX = camera.flipX ? 1 : -1; var flipY = camera.flipY ? 1 : -1; diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index bb3dea88c8c..a6bc7c91a56 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -227,8 +227,15 @@ function initializeGLPlot(scene, canvas, gl) { scene.graphDiv.emit('plotly_relayout', update); }; - scene.glplot.canvas.addEventListener('mouseup', relayoutCallback.bind(null, scene)); - scene.glplot.canvas.addEventListener('wheel', relayoutCallback.bind(null, scene), passiveSupported ? {passive: false} : false); + scene.glplot.canvas.addEventListener('mouseup', function() { + relayoutCallback(scene); + }); + + scene.glplot.canvas.addEventListener('wheel', function() { + if(gd._context._scrollZoom.gl3d) { + relayoutCallback(scene); + } + }, passiveSupported ? {passive: false} : false); if(!scene.staticMode) { scene.glplot.canvas.addEventListener('webglcontextlost', function(event) { @@ -385,7 +392,6 @@ function computeTraceBounds(scene, trace, bounds) { } proto.plot = function(sceneData, fullLayout, layout) { - // Save parameters this.plotArgs = [sceneData, fullLayout, layout]; @@ -412,6 +418,7 @@ proto.plot = function(sceneData, fullLayout, layout) { // Update camera and camera mode this.setCamera(fullSceneLayout.camera); this.updateFx(fullSceneLayout.dragmode, fullSceneLayout.hovermode); + this.camera.enableWheel = this.graphDiv._context._scrollZoom.gl3d; // Update scene this.glplot.update({}); @@ -776,7 +783,6 @@ proto.updateFx = function(dragmode, hovermode) { fullCamera.up = zUp; Lib.nestedProperty(layout, attr).set(zUp); } else { - // none rotation modes [pan or zoom] camera.keyBindingMode = dragmode; } diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js index 78509140a2c..51e8ea9372d 100644 --- a/src/plots/mapbox/mapbox.js +++ b/src/plots/mapbox/mapbox.js @@ -258,6 +258,12 @@ proto.updateMap = function(calcData, fullLayout, resolve, reject) { self.updateLayout(fullLayout); self.resolveOnRender(resolve); } + + if(this.gd._context._scrollZoom.mapbox) { + map.scrollZoom.enable(); + } else { + map.scrollZoom.disable(); + } }; proto.updateData = function(calcData) { diff --git a/test/jasmine/tests/config_test.js b/test/jasmine/tests/config_test.js index 2c8b10d8c39..0d6715531a3 100644 --- a/test/jasmine/tests/config_test.js +++ b/test/jasmine/tests/config_test.js @@ -760,4 +760,63 @@ describe('config argument', function() { .then(done); }); }); + + describe('scrollZoom:', function() { + var gd; + + beforeEach(function() { gd = createGraphDiv(); }); + + afterEach(destroyGraphDiv); + + function plot(config) { + return Plotly.plot(gd, [], {}, config); + } + + it('should fill in scrollZoom default', function(done) { + plot(undefined).then(function() { + expect(gd._context.scrollZoom).toBe('gl3d+geo+mapbox'); + expect(gd._context._scrollZoom).toEqual({gl3d: 1, geo: 1, mapbox: 1}); + expect(gd._context._scrollZoom.cartesian).toBe(undefined, 'no cartesian!'); + }) + .catch(failTest) + .then(done); + }); + + it('should fill in blank scrollZoom value', function(done) { + plot({scrollZoom: null}).then(function() { + expect(gd._context.scrollZoom).toBe(null); + expect(gd._context._scrollZoom).toEqual({gl3d: 1, geo: 1, mapbox: 1}); + expect(gd._context._scrollZoom.cartesian).toBe(undefined, 'no cartesian!'); + }) + .catch(failTest) + .then(done); + }); + + it('should honor scrollZoom:true', function(done) { + plot({scrollZoom: true}).then(function() { + expect(gd._context.scrollZoom).toBe(true); + expect(gd._context._scrollZoom).toEqual({gl3d: 1, geo: 1, cartesian: 1, mapbox: 1}); + }) + .catch(failTest) + .then(done); + }); + + it('should honor scrollZoom:false', function(done) { + plot({scrollZoom: false}).then(function() { + expect(gd._context.scrollZoom).toBe(false); + expect(gd._context._scrollZoom).toEqual({}); + }) + .catch(failTest) + .then(done); + }); + + it('should honor scrollZoom flaglist', function(done) { + plot({scrollZoom: 'mapbox+cartesian'}).then(function() { + expect(gd._context.scrollZoom).toBe('mapbox+cartesian'); + expect(gd._context._scrollZoom).toEqual({mapbox: 1, cartesian: 1}); + }) + .catch(failTest) + .then(done); + }); + }); }); diff --git a/test/jasmine/tests/geo_test.js b/test/jasmine/tests/geo_test.js index 0c57fc6e444..be42d2abc08 100644 --- a/test/jasmine/tests/geo_test.js +++ b/test/jasmine/tests/geo_test.js @@ -1961,4 +1961,52 @@ describe('Test geo zoom/pan/drag interactions:', function() { .catch(failTest) .then(done); }); + + it('should respect scrollZoom config option', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/geo_winkel-tripel')); + fig.layout.width = 700; + fig.layout.height = 500; + fig.layout.dragmode = 'pan'; + + function _assert(step, attr, proj, eventKeys) { + var msg = '[' + step + '] '; + + var geoLayout = gd._fullLayout.geo; + var scale = geoLayout.projection.scale; + expect(scale).toBeCloseTo(attr[0], 1, msg + 'zoom'); + + var geo = geoLayout._subplot; + var _scale = geo.projection.scale(); + expect(_scale).toBeCloseTo(proj[0], 0, msg + 'scale'); + + assertEventData(msg, eventKeys); + } + + plot(fig) + .then(function() { + _assert('base', [1], [101.9], undefined); + }) + .then(function() { return scroll([200, 250], [-200, -200]); }) + .then(function() { + _assert('with scroll enable (by default)', + [1.3], [134.4], + ['geo.projection.rotation.lon', 'geo.center.lon', 'geo.center.lat', 'geo.projection.scale'] + ); + }) + .then(function() { return Plotly.plot(gd, [], {}, {scrollZoom: false}); }) + .then(function() { return scroll([200, 250], [-200, -200]); }) + .then(function() { + _assert('with scrollZoom:false', [1.3], [134.4], undefined); + }) + .then(function() { return Plotly.plot(gd, [], {}, {scrollZoom: 'geo'}); }) + .then(function() { return scroll([200, 250], [-200, -200]); }) + .then(function() { + _assert('with scrollZoom:geo', + [1.74], [177.34], + ['geo.projection.rotation.lon', 'geo.center.lon', 'geo.center.lat', 'geo.projection.scale'] + ); + }) + .catch(failTest) + .then(done); + }); }); diff --git a/test/jasmine/tests/gl3d_plot_interact_test.js b/test/jasmine/tests/gl3d_plot_interact_test.js index 65de62cce8f..7578762f3a2 100644 --- a/test/jasmine/tests/gl3d_plot_interact_test.js +++ b/test/jasmine/tests/gl3d_plot_interact_test.js @@ -1097,6 +1097,11 @@ describe('Test gl3d drag and wheel interactions', function() { } }; + function _assertAndReset(cnt) { + expect(relayoutCallback).toHaveBeenCalledTimes(cnt); + relayoutCallback.calls.reset(); + } + Plotly.plot(gd, mock) .then(function() { relayoutCallback = jasmine.createSpy('relayoutCallback'); @@ -1115,48 +1120,32 @@ describe('Test gl3d drag and wheel interactions', function() { return scroll(sceneTarget); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - + _assertAndReset(1); return scroll(sceneTarget2); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - + _assertAndReset(1); return drag(sceneTarget2, [0, 0], [100, 100]); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - + _assertAndReset(1); return drag(sceneTarget, [0, 0], [100, 100]); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - - return Plotly.relayout(gd, { - 'scene.dragmode': false, - 'scene2.dragmode': false - }); + _assertAndReset(1); + return Plotly.relayout(gd, {'scene.dragmode': false, 'scene2.dragmode': false}); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - + _assertAndReset(1); return drag(sceneTarget, [0, 0], [100, 100]); }) .then(function() { return drag(sceneTarget2, [0, 0], [100, 100]); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(0); + _assertAndReset(0); - return Plotly.relayout(gd, { - 'scene.dragmode': 'orbit', - 'scene2.dragmode': 'turntable' - }); + return Plotly.relayout(gd, {'scene.dragmode': 'orbit', 'scene2.dragmode': 'turntable'}); }) .then(function() { expect(relayoutCallback).toHaveBeenCalledTimes(1); @@ -1168,7 +1157,27 @@ describe('Test gl3d drag and wheel interactions', function() { return drag(sceneTarget2, [0, 0], [100, 100]); }) .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(2); + _assertAndReset(2); + return Plotly.plot(gd, [], {}, {scrollZoom: false}); + }) + .then(function() { + return scroll(sceneTarget); + }) + .then(function() { + return scroll(sceneTarget2); + }) + .then(function() { + _assertAndReset(0); + return Plotly.plot(gd, [], {}, {scrollZoom: 'gl3d'}); + }) + .then(function() { + return scroll(sceneTarget); + }) + .then(function() { + return scroll(sceneTarget2); + }) + .then(function() { + _assertAndReset(2); }) .catch(failTest) .then(done); diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js index 9d6f6f8e53a..a0881b6c17b 100644 --- a/test/jasmine/tests/mapbox_test.js +++ b/test/jasmine/tests/mapbox_test.js @@ -976,6 +976,51 @@ describe('@noCI, mapbox plots', function() { .then(done); }, LONG_TIMEOUT_INTERVAL); + it('should respect scrollZoom config option', function(done) { + var relayoutCnt = 0; + gd.on('plotly_relayout', function() { relayoutCnt++; }); + + function _scroll() { + relayoutCnt = 0; + return new Promise(function(resolve) { + mouseEvent('mousemove', pointPos[0], pointPos[1]); + mouseEvent('scroll', pointPos[0], pointPos[1], {deltaY: -400}); + setTimeout(resolve, 500); + }); + } + + var zoom = getMapInfo(gd).zoom; + expect(zoom).toBeCloseTo(1.234); + + _scroll().then(function() { + expect(relayoutCnt).toBe(1, 'scroll relayout cnt'); + + var zoomNew = getMapInfo(gd).zoom; + expect(zoomNew).toBeGreaterThan(zoom); + zoom = zoomNew; + }) + .then(function() { return Plotly.plot(gd, [], {}, {scrollZoom: false}); }) + .then(_scroll) + .then(function() { + expect(relayoutCnt).toBe(0, 'no additional relayout call'); + + var zoomNew = getMapInfo(gd).zoom; + expect(zoomNew).toBe(zoom); + zoom = zoomNew; + }) + .then(function() { return Plotly.plot(gd, [], {}, {scrollZoom: true}); }) + .then(_scroll) + .then(function() { + expect(relayoutCnt).toBe(1, 'scroll relayout cnt'); + + var zoomNew = getMapInfo(gd).zoom; + expect(zoomNew).toBeGreaterThan(zoom); + zoom = zoomNew; + }) + .catch(failTest) + .then(done); + }, LONG_TIMEOUT_INTERVAL); + function getMapInfo(gd) { var subplot = gd._fullLayout.mapbox._subplot; var map = subplot.map;