From a7177f2acebea4219ca599732e5c68671e089161 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 22 Apr 2016 14:45:03 +0200 Subject: [PATCH 01/19] #30a emit an event upon manipulation --- src/plots/gl3d/scene.js | 71 ++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index bf054ecefb9..4678ab0b62c 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -50,7 +50,7 @@ function render(scene) { var lastPicked = null; var selection = scene.glplot.selection; for(var i = 0; i < keys.length; ++i) { - var trace = scene.traces[keys[i]]; + trace = scene.traces[keys[i]]; if(trace.handlePick(selection)) { lastPicked = trace; } @@ -172,6 +172,15 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) { showNoWebGlMsg(scene); } + var relayoutCallback = function(scene, domEvent) { + var update = {}; + update[scene.id] = getLayoutCamera(scene.camera); + scene.graphDiv.emit('plotly_relayout', update); + }; + + scene.glplot.canvas.addEventListener('mouseup', relayoutCallback.bind(null, scene)); + scene.glplot.canvas.addEventListener('wheel', relayoutCallback.bind(null, scene)); + if(!scene.staticMode) { scene.glplot.canvas.addEventListener('webglcontextlost', function(ev) { console.log('lost context'); @@ -255,7 +264,7 @@ function Scene(options, fullLayout) { this.contourLevels = [ [], [], [] ]; - if(!initializeGLPlot(this, fullLayout)) return; + if(!initializeGLPlot(this, fullLayout)) return; // todo check the necessity for this line } var proto = Scene.prototype; @@ -286,7 +295,7 @@ function coordinateBound(axis, coord, d, bounds) { for(var i=0; i dataBounds[1][j]) { dataScale[j] = 1.0; } @@ -379,7 +388,7 @@ proto.plot = function(sceneData, fullLayout, layout) { this.dataScale = dataScale; //Update traces - for(var i = 0; i < sceneData.length; ++i) { + for(i = 0; i < sceneData.length; ++i) { data = sceneData[i]; if(data.visible!==true) { continue; @@ -416,7 +425,7 @@ proto.plot = function(sceneData, fullLayout, layout) { axisTypeRatios = {}; for(i = 0; i < 3; ++i) { - var axis = fullSceneLayout[axisProperties[i]]; + axis = fullSceneLayout[axisProperties[i]]; var axisType = axis.type; if(axisType in axisTypeRatios) { @@ -471,9 +480,9 @@ proto.plot = function(sceneData, fullLayout, layout) { var axesScaleRatio = [1, 1, 1]; //Compute axis scale per category - for(var i=0; i<3; ++i) { - var axis = fullSceneLayout[axisProperties[i]]; - var axisType = axis.type; + for(i=0; i<3; ++i) { + axis = fullSceneLayout[axisProperties[i]]; + axisType = axis.type; var axisRatio = axisTypeRatios[axisType]; axesScaleRatio[i] = Math.pow(axisRatio.acc, 1.0/axisRatio.count) / dataScale[i]; } @@ -567,33 +576,35 @@ proto.setCameraToDefault = function setCameraToDefault() { }); }; -// get camera position in plotly coords from 'orbit-camera' coords -proto.getCamera = function getCamera() { - this.glplot.camera.view.recalcMatrix(this.camera.view.lastT()); - - var up = this.glplot.camera.up; - var center = this.glplot.camera.center; - var eye = this.glplot.camera.eye; +// getOrbitCamera :: plotly_coords -> orbit_camera_coords +// inverse of getLayoutCamera +function getOrbitCamera(camera) { + return [ + [camera.eye.x, camera.eye.y, camera.eye.z], + [camera.center.x, camera.center.y, camera.center.z], + [camera.up.x, camera.up.y, camera.up.z] + ]; +} +// getLayoutCamera :: orbit_camera_coords -> plotly_coords +// inverse of getOrbitCamera +function getLayoutCamera(camera) { return { - up: {x: up[0], y: up[1], z: up[2]}, - center: {x: center[0], y: center[1], z: center[2]}, - eye: {x: eye[0], y: eye[1], z: eye[2]} + up: {x: camera.up[0], y: camera.up[1], z: camera.up[2]}, + center: {x: camera.center[0], y: camera.center[1], z: camera.center[2]}, + eye: {x: camera.eye[0], y: camera.eye[1], z: camera.eye[2]} }; +} + +// get camera position in plotly coords from 'orbit-camera' coords +proto.getCamera = function getCamera() { + this.glplot.camera.view.recalcMatrix(this.camera.view.lastT()); + return getLayoutCamera(this.glplot.camera); }; // set camera position with a set of plotly coords proto.setCamera = function setCamera(cameraData) { - // getOrbitCamera :: plotly_coords -> orbit_camera_coords - function getOrbitCamera(camera) { - return [ - [camera.eye.x, camera.eye.y, camera.eye.z], - [camera.center.x, camera.center.y, camera.center.z], - [camera.up.x, camera.up.y, camera.up.z] - ]; - } - var update = {}; update[this.id] = cameraData; From 1b19b3ae734ba3b5012c559f8bcf3f88a44340a7 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 22 Apr 2016 18:21:21 +0200 Subject: [PATCH 02/19] #30a removing preexisting lint exemption directives and fixing former lint incompatibilities --- src/plots/gl3d/scene.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 4678ab0b62c..de472460004 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -7,9 +7,6 @@ */ -/*eslint block-scoped-var: 0*/ -/*eslint no-redeclare: 0*/ - 'use strict'; var createPlot = require('gl-plot3d'); @@ -34,6 +31,8 @@ var STATIC_CANVAS, STATIC_CONTEXT; function render(scene) { + var trace; + // update size of svg container var svgContainer = scene.svgContainer; var clientRect = scene.container.getBoundingClientRect(); @@ -69,9 +68,10 @@ function render(scene) { if(lastPicked !== null) { var pdata = project(scene.glplot.cameraParams, selection.dataCoordinate), - trace = lastPicked.data, hoverinfo = trace.hoverinfo; + trace = lastPicked.data; + var xVal = formatter('xaxis', selection.traceCoordinate[0]), yVal = formatter('yaxis', selection.traceCoordinate[1]), zVal = formatter('zaxis', selection.traceCoordinate[2]); @@ -172,7 +172,7 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) { showNoWebGlMsg(scene); } - var relayoutCallback = function(scene, domEvent) { + var relayoutCallback = function(scene) { var update = {}; update[scene.id] = getLayoutCamera(scene.camera); scene.graphDiv.emit('plotly_relayout', update); @@ -292,6 +292,7 @@ proto.recoverContext = function() { var axisProperties = [ 'xaxis', 'yaxis', 'zaxis' ]; function coordinateBound(axis, coord, d, bounds) { + var x; for(var i=0; i Date: Fri, 22 Apr 2016 19:07:40 +0200 Subject: [PATCH 03/19] #30a removing preexisting lint exemption directives and fixing former lint incompatibilities --- src/plots/gl3d/scene.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index de472460004..7c0c4727786 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -67,10 +67,9 @@ function render(scene) { var oldEventData; if(lastPicked !== null) { - var pdata = project(scene.glplot.cameraParams, selection.dataCoordinate), - hoverinfo = trace.hoverinfo; - + var pdata = project(scene.glplot.cameraParams, selection.dataCoordinate); trace = lastPicked.data; + var hoverinfo = trace.hoverinfo; var xVal = formatter('xaxis', selection.traceCoordinate[0]), yVal = formatter('yaxis', selection.traceCoordinate[1]), From 3d8fe8179639793455ebc46b3a734ea5cd423e2c Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Thu, 28 Apr 2016 15:34:03 +0200 Subject: [PATCH 04/19] #30a covering 2D WebGL plot with the plotly_relayout event on pan / zoom --- src/plots/gl2d/scene2d.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index cc0ed3df658..34a9ed8bb3e 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -268,6 +268,16 @@ proto.updateFx = function(options) { fullLayout.hovermode = options.hovermode; }; +var relayoutCallback = function(scene) { + var update = {}; + update[scene.id] = { // scene.camera has no many useful projection or scale information + lastInputTime: scene.camera.lastInputTime, // helps determine which one is the latest input (if async) + xrange: scene.xaxis.range.slice(0), + yrange: scene.yaxis.range.slice(0) + }; + scene.container.parentElement.parentElement.parentElement.emit('plotly_relayout', update); +}; + proto.cameraChanged = function() { var camera = this.camera, xrange = this.xaxis.range, @@ -285,6 +295,7 @@ proto.cameraChanged = function() { this.glplotOptions.ticks = nextTicks; this.glplotOptions.dataBox = camera.dataBox; this.glplot.update(this.glplotOptions); + relayoutCallback(this); } }; From a10caece56f95acd491c587abd60db05b24d9d96 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 29 Apr 2016 14:24:08 +0200 Subject: [PATCH 05/19] #30a test case for mouse drag and wheel on gl3d plots --- test/jasmine/tests/gl_plot_interact_test.js | 47 +++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index f59a84f3875..9d51e23e3fd 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -413,6 +413,53 @@ describe('Test gl plot interactions', function() { }); }); + describe('drag and wheel interactions', function() { + it('should update the scene camera', function(done) { + var sceneLayout = gd._fullLayout.scene, + sceneLayout2 = gd._fullLayout.scene2, + sceneTarget = gd.querySelector('.svg-container .gl-container #scene canvas'), + sceneTarget2 = gd.querySelector('.svg-container .gl-container #scene2 canvas'); + + + expect(sceneLayout.camera.eye) + .toEqual({x: 0.1, y: 0.1, z: 1}); + expect(sceneLayout2.camera.eye) + .toEqual({x: 2.5, y: 2.5, z: 2.5}); + + // Wheel scene 1 + sceneTarget.dispatchEvent(new WheelEvent('wheel', {deltaY: 1})); + + // Wheel scene 2 + sceneTarget2.dispatchEvent(new WheelEvent('wheel', {deltaY: 1})); + + setTimeout(function() { + + expect(relayoutCallback).toHaveBeenCalledTimes(2); + + relayoutCallback.calls.reset(); + + // Drag scene 1 + sceneTarget.dispatchEvent(new MouseEvent('mousedown', {x: 0, y: 0})); + sceneTarget.dispatchEvent(new MouseEvent('mousemove', { x: 100, y: 100})); + sceneTarget.dispatchEvent(new MouseEvent('mouseup', { x: 100, y: 100})); + + // Drag scene 2 + sceneTarget2.dispatchEvent(new MouseEvent('mousedown', {x: 0, y: 0 })); + sceneTarget2.dispatchEvent(new MouseEvent('mousemove', {x: 100, y: 100})); + sceneTarget2.dispatchEvent(new MouseEvent('mouseup', {x: 100, y: 100})); + + setTimeout(function() { + + expect(relayoutCallback).toHaveBeenCalledTimes(2); + + done(); + + }, MODEBAR_DELAY); + + }, MODEBAR_DELAY); + }); + }); + describe('button hoverClosest3d', function() { it('should update the scene hovermode and spikes', function() { var buttonHover = selectButton(modeBar, 'hoverClosest3d'); From 604d73968f2ad5272216a21bc53ff959899978a4 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 29 Apr 2016 16:11:55 +0200 Subject: [PATCH 06/19] #30a proper grouping by interaction type --- test/jasmine/tests/gl_plot_interact_test.js | 249 +++++++------------- 1 file changed, 86 insertions(+), 163 deletions(-) diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 9d51e23e3fd..d6f483965a9 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -228,7 +228,7 @@ describe('Test gl plot interactions', function() { }); }); - describe('gl3d modebar click handlers', function() { + describe('gl3d event handlers', function() { var modeBar, relayoutCallback; beforeEach(function(done) { @@ -265,151 +265,98 @@ describe('Test gl plot interactions', function() { }); } - describe('button zoom3d', function() { - it('should updates the scene dragmode and dragmode button', function() { - var buttonTurntable = selectButton(modeBar, 'tableRotation'), - buttonZoom3d = selectButton(modeBar, 'zoom3d'); - - assertScenes(gd._fullLayout, 'dragmode', 'turntable'); - expect(buttonTurntable.isActive()).toBe(true); - expect(buttonZoom3d.isActive()).toBe(false); - - buttonZoom3d.click(); - assertScenes(gd.layout, 'dragmode', 'zoom'); - expect(gd.layout.dragmode).toBe(undefined); - expect(gd._fullLayout.dragmode).toBe('zoom'); - expect(buttonTurntable.isActive()).toBe(false); - expect(buttonZoom3d.isActive()).toBe(true); - - buttonTurntable.click(); - assertScenes(gd._fullLayout, 'dragmode', 'turntable'); - expect(buttonTurntable.isActive()).toBe(true); - expect(buttonZoom3d.isActive()).toBe(false); - }); - }); - - describe('button pan3d', function() { - it('should updates the scene dragmode and dragmode button', function() { - var buttonTurntable = selectButton(modeBar, 'tableRotation'), - buttonPan3d = selectButton(modeBar, 'pan3d'); - - assertScenes(gd._fullLayout, 'dragmode', 'turntable'); - expect(buttonTurntable.isActive()).toBe(true); - expect(buttonPan3d.isActive()).toBe(false); - - buttonPan3d.click(); - assertScenes(gd.layout, 'dragmode', 'pan'); - expect(gd.layout.dragmode).toBe(undefined); - expect(gd._fullLayout.dragmode).toBe('zoom'); - expect(buttonTurntable.isActive()).toBe(false); - expect(buttonPan3d.isActive()).toBe(true); - - buttonTurntable.click(); - assertScenes(gd._fullLayout, 'dragmode', 'turntable'); - expect(buttonTurntable.isActive()).toBe(true); - expect(buttonPan3d.isActive()).toBe(false); - }); - }); + describe('modebar click handlers', function() { - describe('button orbitRotation', function() { - it('should updates the scene dragmode and dragmode button', function() { - var buttonTurntable = selectButton(modeBar, 'tableRotation'), - buttonOrbit = selectButton(modeBar, 'orbitRotation'); - - assertScenes(gd._fullLayout, 'dragmode', 'turntable'); - expect(buttonTurntable.isActive()).toBe(true); - expect(buttonOrbit.isActive()).toBe(false); - - buttonOrbit.click(); - assertScenes(gd.layout, 'dragmode', 'orbit'); - expect(gd.layout.dragmode).toBe(undefined); - expect(gd._fullLayout.dragmode).toBe('zoom'); - expect(buttonTurntable.isActive()).toBe(false); - expect(buttonOrbit.isActive()).toBe(true); - - buttonTurntable.click(); - assertScenes(gd._fullLayout, 'dragmode', 'turntable'); - expect(buttonTurntable.isActive()).toBe(true); - expect(buttonOrbit.isActive()).toBe(false); - }); - }); + describe('button zoom3d', function() { + it('should updates the scene dragmode and dragmode button', function() { + var buttonTurntable = selectButton(modeBar, 'tableRotation'), + buttonZoom3d = selectButton(modeBar, 'zoom3d'); - describe('buttons resetCameraDefault3d and resetCameraLastSave3d', function() { - it('should update the scene camera', function(done) { - var sceneLayout = gd._fullLayout.scene, - sceneLayout2 = gd._fullLayout.scene2, - scene = sceneLayout._scene, - scene2 = sceneLayout2._scene; + assertScenes(gd._fullLayout, 'dragmode', 'turntable'); + expect(buttonTurntable.isActive()).toBe(true); + expect(buttonZoom3d.isActive()).toBe(false); - expect(sceneLayout.camera.eye) - .toEqual({x: 0.1, y: 0.1, z: 1}); - expect(sceneLayout2.camera.eye) - .toEqual({x: 2.5, y: 2.5, z: 2.5}); - - selectButton(modeBar, 'resetCameraDefault3d').click(); - - setTimeout(function() { - - expect(relayoutCallback).toHaveBeenCalledTimes(2); // initiator: resetCameraDefault3d; 2 scenes - expect(relayoutCallback).toHaveBeenCalledWith({ - scene: { - eye: { x: 1.25, y: 1.25, z: 1.25 }, - center: { x: 0, y: 0, z: 0 }, - up: { x: 0, y: 0, z: 1 } - } - }); - expect(relayoutCallback).toHaveBeenCalledWith({ - scene2: { - center: { x: 0, y: 0, z: 0 }, - eye: { x: 1.25, y: 1.25, z: 1.25 }, - up: { x: 0, y: 0, z: 1 } - } - }); - relayoutCallback.calls.reset(); + buttonZoom3d.click(); + assertScenes(gd.layout, 'dragmode', 'zoom'); + expect(gd.layout.dragmode).toBe(undefined); + expect(gd._fullLayout.dragmode).toBe('zoom'); + expect(buttonTurntable.isActive()).toBe(false); + expect(buttonZoom3d.isActive()).toBe(true); - expect(sceneLayout.camera.eye) - .toEqual({x: 0.1, y: 0.1, z: 1}, 'does not change the layout objects'); - expect(scene.camera.eye) - .toBeCloseToArray([1.25, 1.25, 1.25], 4); - expect(sceneLayout2.camera.eye) - .toEqual({x: 2.5, y: 2.5, z: 2.5}, 'does not change the layout objects'); - expect(scene2.camera.eye) - .toBeCloseToArray([1.25, 1.25, 1.25], 4); - - selectButton(modeBar, 'resetCameraLastSave3d').click(); - - setTimeout(function() { - - expect(relayoutCallback).toHaveBeenCalledTimes(2); // initiator: resetCameraLastSave3d; 2 scenes - expect(relayoutCallback).toHaveBeenCalledWith({ - scene: { - center: { x: 0, y: 0, z: 0 }, - eye: { x: 0.1, y: 0.1, z: 1 }, - up: { x: 0, y: 0, z: 1 } - } - }); - expect(relayoutCallback).toHaveBeenCalledWith({ - scene2: { - center: { x: 0, y: 0, z: 0 }, - eye: { x: 2.5, y: 2.5, z: 2.5 }, - up: { x: 0, y: 0, z: 1 } - } - }); - - expect(sceneLayout.camera.eye) - .toEqual({x: 0.1, y: 0.1, z: 1}, 'does not change the layout objects'); - expect(scene.camera.eye) - .toBeCloseToArray([ 0.1, 0.1, 1], 4); - expect(sceneLayout2.camera.eye) - .toEqual({x: 2.5, y: 2.5, z: 2.5}, 'does not change the layout objects'); - expect(scene2.camera.eye) - .toBeCloseToArray([2.5, 2.5, 2.5], 4); + buttonTurntable.click(); + assertScenes(gd._fullLayout, 'dragmode', 'turntable'); + expect(buttonTurntable.isActive()).toBe(true); + expect(buttonZoom3d.isActive()).toBe(false); + }); + }); - done(); + describe('button pan3d', function() { + it('should updates the scene dragmode and dragmode button', function() { + var buttonTurntable = selectButton(modeBar, 'tableRotation'), + buttonPan3d = selectButton(modeBar, 'pan3d'); + + assertScenes(gd._fullLayout, 'dragmode', 'turntable'); + expect(buttonTurntable.isActive()).toBe(true); + expect(buttonPan3d.isActive()).toBe(false); + + buttonPan3d.click(); + assertScenes(gd.layout, 'dragmode', 'pan'); + expect(gd.layout.dragmode).toBe(undefined); + expect(gd._fullLayout.dragmode).toBe('zoom'); + expect(buttonTurntable.isActive()).toBe(false); + expect(buttonPan3d.isActive()).toBe(true); + + buttonTurntable.click(); + assertScenes(gd._fullLayout, 'dragmode', 'turntable'); + expect(buttonTurntable.isActive()).toBe(true); + expect(buttonPan3d.isActive()).toBe(false); + }); + }); - }, MODEBAR_DELAY); + describe('button orbitRotation', function() { + it('should updates the scene dragmode and dragmode button', function() { + var buttonTurntable = selectButton(modeBar, 'tableRotation'), + buttonOrbit = selectButton(modeBar, 'orbitRotation'); + + assertScenes(gd._fullLayout, 'dragmode', 'turntable'); + expect(buttonTurntable.isActive()).toBe(true); + expect(buttonOrbit.isActive()).toBe(false); + + buttonOrbit.click(); + assertScenes(gd.layout, 'dragmode', 'orbit'); + expect(gd.layout.dragmode).toBe(undefined); + expect(gd._fullLayout.dragmode).toBe('zoom'); + expect(buttonTurntable.isActive()).toBe(false); + expect(buttonOrbit.isActive()).toBe(true); + + buttonTurntable.click(); + assertScenes(gd._fullLayout, 'dragmode', 'turntable'); + expect(buttonTurntable.isActive()).toBe(true); + expect(buttonOrbit.isActive()).toBe(false); + }); + }); - }, MODEBAR_DELAY); + describe('button hoverClosest3d', function() { + it('should update the scene hovermode and spikes', function() { + var buttonHover = selectButton(modeBar, 'hoverClosest3d'); + + assertScenes(gd._fullLayout, 'hovermode', 'closest'); + expect(buttonHover.isActive()).toBe(true); + + buttonHover.click(); + assertScenes(gd._fullLayout, 'hovermode', false); + assertScenes(gd._fullLayout, 'xaxis.showspikes', false); + assertScenes(gd._fullLayout, 'yaxis.showspikes', false); + assertScenes(gd._fullLayout, 'zaxis.showspikes', false); + expect(buttonHover.isActive()).toBe(false); + + buttonHover.click(); + assertScenes(gd._fullLayout, 'hovermode', 'closest'); + assertScenes(gd._fullLayout, 'xaxis.showspikes', true); + assertScenes(gd._fullLayout, 'yaxis.showspikes', true); + assertScenes(gd._fullLayout, 'zaxis.showspikes', true); + expect(buttonHover.isActive()).toBe(true); + }); }); }); @@ -459,30 +406,6 @@ describe('Test gl plot interactions', function() { }, MODEBAR_DELAY); }); }); - - describe('button hoverClosest3d', function() { - it('should update the scene hovermode and spikes', function() { - var buttonHover = selectButton(modeBar, 'hoverClosest3d'); - - assertScenes(gd._fullLayout, 'hovermode', 'closest'); - expect(buttonHover.isActive()).toBe(true); - - buttonHover.click(); - assertScenes(gd._fullLayout, 'hovermode', false); - assertScenes(gd._fullLayout, 'xaxis.showspikes', false); - assertScenes(gd._fullLayout, 'yaxis.showspikes', false); - assertScenes(gd._fullLayout, 'zaxis.showspikes', false); - expect(buttonHover.isActive()).toBe(false); - - buttonHover.click(); - assertScenes(gd._fullLayout, 'hovermode', 'closest'); - assertScenes(gd._fullLayout, 'xaxis.showspikes', true); - assertScenes(gd._fullLayout, 'yaxis.showspikes', true); - assertScenes(gd._fullLayout, 'zaxis.showspikes', true); - expect(buttonHover.isActive()).toBe(true); - }); - }); - }); describe('Plots.cleanPlot', function() { From ee8ff6529be74e9fba3c95587dbdb4e6e38d5d63 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 29 Apr 2016 17:49:17 +0200 Subject: [PATCH 07/19] #30a test case for mouse drag on gl2d plots --- test/jasmine/tests/gl_plot_interact_test.js | 26 ++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index d6f483965a9..294ac7bda76 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -212,12 +212,18 @@ describe('Test gl plot interactions', function() { }); describe('gl2d plots', function() { - var mock = require('@mocks/gl2d_10.json'); + var mock = require('@mocks/gl2d_10.json'), + relayoutCallback; beforeEach(function(done) { gd = createGraphDiv(); Plotly.plot(gd, mock.data, mock.layout).then(function() { + + relayoutCallback = jasmine.createSpy('relayoutCallback'); + + gd.on('plotly_relayout', relayoutCallback); + delay(done); }); }); @@ -226,6 +232,24 @@ describe('Test gl plot interactions', function() { var nodes = d3.selectAll('canvas'); expect(nodes[0].length).toEqual(1); }); + + it('should respond to drag interactions', function(done) { + + var sceneTarget = gd.querySelector('.plot-container .gl-container canvas'); + + // Drag scene + sceneTarget.dispatchEvent(new MouseEvent('mousedown', {x: 0, y: 0})); + sceneTarget.dispatchEvent(new MouseEvent('mousemove', { x: 100, y: 100})); + sceneTarget.dispatchEvent(new MouseEvent('mouseup', { x: 100, y: 100})); + + setTimeout(function() { + + expect(relayoutCallback).toHaveBeenCalledTimes(1); + + done(); + + }, MODEBAR_DELAY); + }); }); describe('gl3d event handlers', function() { From da018795d99d668de7c3695353857a54ad9cf21a Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Mon, 2 May 2016 20:01:03 +0200 Subject: [PATCH 08/19] #30a reifying plotly_relayout callback count, as well as layout data changes on the DIV via unit testing --- test/jasmine/tests/plot_interact_test.js | 121 +++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/test/jasmine/tests/plot_interact_test.js b/test/jasmine/tests/plot_interact_test.js index 8748b9d9bb2..deddfe5b469 100644 --- a/test/jasmine/tests/plot_interact_test.js +++ b/test/jasmine/tests/plot_interact_test.js @@ -6,7 +6,15 @@ var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var customMatchers = require('../assets/custom_matchers'); +var mouseEvent = require('../assets/mouse_event'); +var selectButton = require('../assets/modebar_button'); +var MODEBAR_DELAY = 500; + +// put callback in the event queue +function delay(done) { + setTimeout(done, 0); +} describe('Test plot structure', function() { 'use strict'; @@ -142,6 +150,119 @@ describe('Test plot structure', function() { }); }); + describe('scatter drag', function() { + + var mock = require('@mocks/10.json'), + gd, modeBar, relayoutCallback; + + beforeEach(function(done) { + gd = createGraphDiv(); + + Plotly.plot(gd, mock.data, mock.layout).then(function() { + + modeBar = gd._fullLayout._modeBar; + relayoutCallback = jasmine.createSpy('relayoutCallback'); + + gd.on('plotly_relayout', relayoutCallback); + + delay(done); + }); + }); + + it('scatter plot should respond to drag interactions', function(done) { + + jasmine.addMatchers(customMatchers); + + var precision = 5; + + var buttonPan = selectButton(modeBar, 'pan2d'); + + var originalX = [-0.6225,5.5]; + var originalY = [-1.6340975059013805,7.166241526218911]; + + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + + // Switch to pan mode + expect(buttonPan.isActive()).toBe(false); // initially, zoom is active + buttonPan.click(); + expect(buttonPan.isActive()).toBe(true); // switched on dragmode + + // Switching mode must not change visible range + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + + setTimeout(function() { + + expect(relayoutCallback).toHaveBeenCalledTimes(1); + relayoutCallback.calls.reset(); + + // Drag scene along the X axis + + mouseEvent('mousedown', 110, 150); + mouseEvent('mousemove', 220, 150); + mouseEvent('mouseup', 220, 150); + + expect(gd.layout.xaxis.range).toBeCloseToArray([-2.0255729166666665,4.096927083333333], precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + + // Drag scene back along the X axis (not from the same starting point but same X delta) + + mouseEvent('mousedown', 280, 150); + mouseEvent('mousemove', 170, 150); + mouseEvent('mouseup', 170, 150); + + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + + // Drag scene along the Y axis + + mouseEvent('mousedown', 110, 150); + mouseEvent('mousemove', 110, 190); + mouseEvent('mouseup', 110, 190); + + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.3769062155984817,8.42343281652181], precision); + + // Drag scene back along the Y axis (not from the same starting point but same Y delta) + + mouseEvent('mousedown', 280, 130); + mouseEvent('mousemove', 280, 90); + mouseEvent('mouseup', 280, 90); + + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + + // Drag scene along both the X and Y axis + + mouseEvent('mousedown', 110, 150); + mouseEvent('mousemove', 220, 190); + mouseEvent('mouseup', 220, 190); + + expect(gd.layout.xaxis.range).toBeCloseToArray([-2.0255729166666665,4.096927083333333], precision); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.3769062155984817,8.42343281652181], precision); + + // Drag scene back along the X and Y axis (not from the same starting point but same delta vector) + + mouseEvent('mousedown', 280, 130); + mouseEvent('mousemove', 170, 90); + mouseEvent('mouseup', 170, 90); + + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + + setTimeout(function() { + + expect(relayoutCallback).toHaveBeenCalledTimes(6); // X and back; Y and back; XY and back + + done(); + + }, MODEBAR_DELAY); + + }, MODEBAR_DELAY); + }); + }); + describe('contour/heatmap traces', function() { var mock = require('@mocks/connectgaps_2d.json'); var gd; From 48682a4bf34579e37dbef77c4dbe6341f3980985 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Mon, 2 May 2016 22:43:04 +0200 Subject: [PATCH 09/19] #30a adding graphDiv to Scene2d similarly to Scene / thanks @etpinard --- src/plots/gl2d/index.js | 3 ++- src/plots/gl2d/scene2d.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plots/gl2d/index.js b/src/plots/gl2d/index.js index d446972fe7f..61524d3a60b 100644 --- a/src/plots/gl2d/index.js +++ b/src/plots/gl2d/index.js @@ -50,8 +50,9 @@ exports.plot = function plotGl2d(gd) { // If Scene is not instantiated, create one! if(scene === undefined) { scene = new Scene2D({ - container: gd.querySelector('.gl-container'), id: subplotId, + graphDiv: gd, + container: gd.querySelector('.gl-container'), staticPlot: gd._context.staticPlot, plotGlPixelRatio: gd._context.plotGlPixelRatio }, diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 34a9ed8bb3e..07dc7ad2265 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -27,6 +27,7 @@ var STATIC_CANVAS, STATIC_CONTEXT; function Scene2D(options, fullLayout) { this.container = options.container; + this.graphDiv = options.graphDiv; this.pixelRatio = options.plotGlPixelRatio || window.devicePixelRatio; this.id = options.id; this.staticPlot = !!options.staticPlot; @@ -275,7 +276,7 @@ var relayoutCallback = function(scene) { xrange: scene.xaxis.range.slice(0), yrange: scene.yaxis.range.slice(0) }; - scene.container.parentElement.parentElement.parentElement.emit('plotly_relayout', update); + scene.graphDiv.emit('plotly_relayout', update); }; proto.cameraChanged = function() { From 63ec931a0e4b0a7206e1892adfdcc14c4ee2ccea Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Mon, 2 May 2016 23:35:17 +0200 Subject: [PATCH 10/19] #30a bumping jasmine to 2.4 to enjoy its new facilities such as spy.toHaveBeenCalledTimes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc95cc8eec8..52ba2bb838c 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "fs-extra": "^0.28.0", "fuse.js": "^2.2.0", "glob": "^7.0.0", - "jasmine-core": "^2.3.4", + "jasmine-core": "^2.4.1", "karma": "^0.13.15", "karma-browserify": "^5.0.1", "karma-chrome-launcher": "^0.2.1", From 19c478d6acdeab6c14a4e66dcc57bb0e37ef61cd Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 3 May 2016 11:21:53 +0200 Subject: [PATCH 11/19] #30a panning test on WebGL canvas; adding the DIV update for scene2d --- src/plots/gl2d/scene2d.js | 16 +++- test/jasmine/tests/gl_plot_interact_test.js | 85 ++++++++++++++++++--- test/jasmine/tests/plot_interact_test.js | 11 ++- 3 files changed, 96 insertions(+), 16 deletions(-) diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 07dc7ad2265..4b4181077ba 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -270,12 +270,22 @@ proto.updateFx = function(options) { }; var relayoutCallback = function(scene) { - var update = {}; + + var update = {}, + xrange = scene.xaxis.range, + yrange = scene.yaxis.range; + + // Update the layout on the DIV + scene.graphDiv.layout.xaxis.range = xrange.slice(0); + scene.graphDiv.layout.yaxis.range = yrange.slice(0); + + // Make a meaningful value to be passed on to the possible 'plotly_relayout' subscriber(s) update[scene.id] = { // scene.camera has no many useful projection or scale information lastInputTime: scene.camera.lastInputTime, // helps determine which one is the latest input (if async) - xrange: scene.xaxis.range.slice(0), - yrange: scene.yaxis.range.slice(0) + xrange: xrange.slice(0), + yrange: yrange.slice(0) }; + scene.graphDiv.emit('plotly_relayout', update); }; diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 294ac7bda76..9572a7277bd 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -213,13 +213,14 @@ describe('Test gl plot interactions', function() { describe('gl2d plots', function() { var mock = require('@mocks/gl2d_10.json'), - relayoutCallback; + modeBar, relayoutCallback; beforeEach(function(done) { gd = createGraphDiv(); Plotly.plot(gd, mock.data, mock.layout).then(function() { + modeBar = gd._fullLayout._modeBar; relayoutCallback = jasmine.createSpy('relayoutCallback'); gd.on('plotly_relayout', relayoutCallback); @@ -235,18 +236,85 @@ describe('Test gl plot interactions', function() { it('should respond to drag interactions', function(done) { - var sceneTarget = gd.querySelector('.plot-container .gl-container canvas'); + jasmine.addMatchers(customMatchers); + + var precision = 5; + + var buttonPan = selectButton(modeBar, 'pan2d'); + + var originalX = [-0.022068095838587643, 5.022068095838588]; + var originalY = [-0.21331533513634046, 5.851205650049042]; + + var newX = [-0.23224043715846995,4.811895754518705]; + var newY = [-1.2962655110623016,4.768255474123081]; - // Drag scene - sceneTarget.dispatchEvent(new MouseEvent('mousedown', {x: 0, y: 0})); - sceneTarget.dispatchEvent(new MouseEvent('mousemove', { x: 100, y: 100})); - sceneTarget.dispatchEvent(new MouseEvent('mouseup', { x: 100, y: 100})); + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + + // Switch to pan mode + expect(buttonPan.isActive()).toBe(false); // initially, zoom is active + buttonPan.click(); + expect(buttonPan.isActive()).toBe(true); // switched on dragmode + + // Switching mode must not change visible range + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); setTimeout(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); + mouseEvent('mousemove', 200, 200); - done(); + relayoutCallback.calls.reset(); + + // Drag scene along the X axis + + mouseEvent('mousemove', 220, 200, {buttons: 1}); + + expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + + // Drag scene back along the X axis + + mouseEvent('mousemove', 200, 200, {buttons: 1}); + + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + + // Drag scene along the Y axis + + mouseEvent('mousemove', 200, 150, {buttons: 1}); + + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); + + // Drag scene back along the Y axis + + mouseEvent('mousemove', 200, 200, {buttons: 1}); + + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + + // Drag scene along both the X and Y axis + + mouseEvent('mousemove', 220, 150, {buttons: 1}); + + expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); + + // Drag scene back along the X and Y axis + + mouseEvent('mousemove', 200, 200, {buttons: 1}); + + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + + setTimeout(function() { + + expect(relayoutCallback).toHaveBeenCalledTimes(6); // X and back; Y and back; XY and back + + done(); + + }, MODEBAR_DELAY); }, MODEBAR_DELAY); }); @@ -391,7 +459,6 @@ describe('Test gl plot interactions', function() { sceneTarget = gd.querySelector('.svg-container .gl-container #scene canvas'), sceneTarget2 = gd.querySelector('.svg-container .gl-container #scene2 canvas'); - expect(sceneLayout.camera.eye) .toEqual({x: 0.1, y: 0.1, z: 1}); expect(sceneLayout2.camera.eye) diff --git a/test/jasmine/tests/plot_interact_test.js b/test/jasmine/tests/plot_interact_test.js index deddfe5b469..49a5486b356 100644 --- a/test/jasmine/tests/plot_interact_test.js +++ b/test/jasmine/tests/plot_interact_test.js @@ -180,6 +180,9 @@ describe('Test plot structure', function() { var originalX = [-0.6225,5.5]; var originalY = [-1.6340975059013805,7.166241526218911]; + var newX = [-2.0255729166666665,4.096927083333333]; + var newY = [-0.3769062155984817,8.42343281652181]; + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); @@ -203,7 +206,7 @@ describe('Test plot structure', function() { mouseEvent('mousemove', 220, 150); mouseEvent('mouseup', 220, 150); - expect(gd.layout.xaxis.range).toBeCloseToArray([-2.0255729166666665,4.096927083333333], precision); + expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); // Drag scene back along the X axis (not from the same starting point but same X delta) @@ -222,7 +225,7 @@ describe('Test plot structure', function() { mouseEvent('mouseup', 110, 190); expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.3769062155984817,8.42343281652181], precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); // Drag scene back along the Y axis (not from the same starting point but same Y delta) @@ -239,8 +242,8 @@ describe('Test plot structure', function() { mouseEvent('mousemove', 220, 190); mouseEvent('mouseup', 220, 190); - expect(gd.layout.xaxis.range).toBeCloseToArray([-2.0255729166666665,4.096927083333333], precision); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.3769062155984817,8.42343281652181], precision); + expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); // Drag scene back along the X and Y axis (not from the same starting point but same delta vector) From e467c880b70e09a17fe3ae57156b56ecb93a1cf0 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 3 May 2016 20:39:00 +0200 Subject: [PATCH 12/19] #30a saving the camera position on the DIV layout when the projection has changed, in sync with the plotly_relayout event --- src/plots/gl3d/scene.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 7c0c4727786..4274e7fa1d0 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -174,6 +174,7 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) { var relayoutCallback = function(scene) { var update = {}; update[scene.id] = getLayoutCamera(scene.camera); + scene.saveCamera(scene.graphDiv.layout); scene.graphDiv.emit('plotly_relayout', update); }; From b212086ca54f229e479340bc5db8f1db2ffef029 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 3 May 2016 23:00:56 +0200 Subject: [PATCH 13/19] #30a PR feedback: not expose scene id and use axis names. Plus: test the data structure of the callback. --- src/plots/gl2d/scene2d.js | 11 +++++------ test/jasmine/tests/gl_plot_interact_test.js | 10 +++++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 4b4181077ba..138efa71085 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -271,8 +271,7 @@ proto.updateFx = function(options) { var relayoutCallback = function(scene) { - var update = {}, - xrange = scene.xaxis.range, + var xrange = scene.xaxis.range, yrange = scene.yaxis.range; // Update the layout on the DIV @@ -280,11 +279,11 @@ var relayoutCallback = function(scene) { scene.graphDiv.layout.yaxis.range = yrange.slice(0); // Make a meaningful value to be passed on to the possible 'plotly_relayout' subscriber(s) - update[scene.id] = { // scene.camera has no many useful projection or scale information - lastInputTime: scene.camera.lastInputTime, // helps determine which one is the latest input (if async) - xrange: xrange.slice(0), - yrange: yrange.slice(0) + var update = { // scene.camera has no many useful projection or scale information + lastInputTime: scene.camera.lastInputTime // helps determine which one is the latest input (if async) }; + update[scene.xaxis._name] = xrange.slice(); + update[scene.yaxis._name] = yrange.slice(); scene.graphDiv.emit('plotly_relayout', update); }; diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 9572a7277bd..5ed14da4bc0 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -310,7 +310,15 @@ describe('Test gl plot interactions', function() { setTimeout(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(6); // X and back; Y and back; XY and back + // callback count expectation: X and back; Y and back; XY and back + expect(relayoutCallback).toHaveBeenCalledTimes(6); + + // a callback value structure and contents check + expect(relayoutCallback).toHaveBeenCalledWith(jasmine.objectContaining({ + lastInputTime: jasmine.any(Number), + xaxis: [jasmine.any(Number), jasmine.any(Number)], + yaxis: [jasmine.any(Number), jasmine.any(Number)] + })); done(); From e8aa9cb7e62c8d1b8eb7ab7100f4e2da554fbd9f Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 3 May 2016 23:09:18 +0200 Subject: [PATCH 14/19] #30a PR feedback: remove `delay` --- test/jasmine/tests/plot_interact_test.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/jasmine/tests/plot_interact_test.js b/test/jasmine/tests/plot_interact_test.js index 49a5486b356..58eaa47077f 100644 --- a/test/jasmine/tests/plot_interact_test.js +++ b/test/jasmine/tests/plot_interact_test.js @@ -11,11 +11,6 @@ var selectButton = require('../assets/modebar_button'); var MODEBAR_DELAY = 500; -// put callback in the event queue -function delay(done) { - setTimeout(done, 0); -} - describe('Test plot structure', function() { 'use strict'; @@ -165,7 +160,7 @@ describe('Test plot structure', function() { gd.on('plotly_relayout', relayoutCallback); - delay(done); + done(); }); }); From e02a3f680d5feed047d48e30d205ab7981641036 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Wed, 4 May 2016 00:40:01 +0200 Subject: [PATCH 15/19] #30a adding tests for the 3d callback and DIV layout camera data update --- test/jasmine/tests/gl_plot_interact_test.js | 52 +++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 5ed14da4bc0..87f11fb5529 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -211,6 +211,58 @@ describe('Test gl plot interactions', function() { }); + describe('gl3d plots', function() { + + var mock = require('@mocks/gl3d_scatter3d-connectgaps.json'), + modeBar, relayoutCallback; + + beforeEach(function(done) { + gd = createGraphDiv(); + + Plotly.plot(gd, mock.data, mock.layout).then(function() { + + modeBar = gd._fullLayout._modeBar; + relayoutCallback = jasmine.createSpy('relayoutCallback'); + + gd.on('plotly_relayout', relayoutCallback); + + delay(done); + }); + }); + + it('should respond to drag interactions', function(done) { + + // Expected shape of projection-related data + var cameraStructure = { + up: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)}, + center: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)}, + eye: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)} + }; + + setTimeout(function() { + + // One 'drag': simulating fairly thoroughly as the mouseup event is also needed here + mouseEvent('mousemove', 400, 200); + mouseEvent('mousedown', 400, 200); + mouseEvent('mousemove', 320, 320, {buttons: 1}); + mouseEvent('mouseup', 320, 320); + + // Check event emission count + expect(relayoutCallback).toHaveBeenCalledTimes(1); + + // Check structure of event callback value contents + expect(relayoutCallback).toHaveBeenCalledWith(jasmine.objectContaining({scene: cameraStructure})); + + // Check camera contents on the DIV layout + var divCamera = gd.layout.scene.camera; + expect(divCamera).toEqual(cameraStructure); + + delay(done); + + }, MODEBAR_DELAY); + }); + }); + describe('gl2d plots', function() { var mock = require('@mocks/gl2d_10.json'), modeBar, relayoutCallback; From 497541727d5dcb3ae0e319a1424a95d2643f9554 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Wed, 4 May 2016 22:55:17 +0200 Subject: [PATCH 16/19] #30a PR feedback: commenting and removing disused binding --- src/components/modebar/buttons.js | 2 ++ test/jasmine/tests/gl_plot_interact_test.js | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index b545faff14b..7f9cd5736f3 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -350,6 +350,8 @@ function handleCamera3d(gd, ev) { if(attr === 'resetDefault') scene.setCameraToDefault(); else if(attr === 'resetLastSave') { + // This handler looks in the un-updated fullLayout.scene.camera object to reset the camera + // to the last saved position. scene.setCamera(fullSceneLayout.camera); } } diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 87f11fb5529..5a2f0370673 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -214,14 +214,13 @@ describe('Test gl plot interactions', function() { describe('gl3d plots', function() { var mock = require('@mocks/gl3d_scatter3d-connectgaps.json'), - modeBar, relayoutCallback; + relayoutCallback; beforeEach(function(done) { gd = createGraphDiv(); Plotly.plot(gd, mock.data, mock.layout).then(function() { - modeBar = gd._fullLayout._modeBar; relayoutCallback = jasmine.createSpy('relayoutCallback'); gd.on('plotly_relayout', relayoutCallback); From 7c23e2e7bc1eca0b29806e45296f905fd22263d3 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 6 May 2016 12:53:14 +0200 Subject: [PATCH 17/19] #30a PR feedback: adding test case with partially set camera and solving it; test case refactor for multiple mocks, fewer globals and explicit async steps --- src/plots/gl3d/scene.js | 2 +- test/jasmine/tests/gl_plot_interact_test.js | 81 ++++++++++++--------- 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 4274e7fa1d0..5feee3b78a5 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -624,7 +624,7 @@ proto.saveCamera = function saveCamera(layout) { function same(x, y, i, j) { var vectors = ['up', 'center', 'eye'], components = ['x', 'y', 'z']; - return x[vectors[i]][components[j]] === y[vectors[i]][components[j]]; + return y[vectors[i]] && (x[vectors[i]][components[j]] === y[vectors[i]][components[j]]); } if(cameraDataLastSave === undefined) hasChanged = true; diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 5a2f0370673..32a5310b4f7 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -213,52 +213,65 @@ describe('Test gl plot interactions', function() { describe('gl3d plots', function() { - var mock = require('@mocks/gl3d_scatter3d-connectgaps.json'), - relayoutCallback; + // Expected shape of projection-related data + var cameraStructure = { + up: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)}, + center: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)}, + eye: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)} + }; + + function makePlot(mock) { + return Plotly.plot(createGraphDiv(), mock.data, mock.layout); + } - beforeEach(function(done) { - gd = createGraphDiv(); + function addEventCallback(graphDiv) { + var relayoutCallback = jasmine.createSpy('relayoutCallback'); + graphDiv.on('plotly_relayout', relayoutCallback); + return {graphDiv: graphDiv, relayoutCallback: relayoutCallback}; + } - Plotly.plot(gd, mock.data, mock.layout).then(function() { + function verifyInteractionEffects(tuple) { - relayoutCallback = jasmine.createSpy('relayoutCallback'); + // One 'drag': simulating fairly thoroughly as the mouseup event is also needed here + mouseEvent('mousemove', 400, 200); + mouseEvent('mousedown', 400, 200); + mouseEvent('mousemove', 320, 320, {buttons: 1}); + mouseEvent('mouseup', 320, 320); - gd.on('plotly_relayout', relayoutCallback); + // Check event emission count + expect(tuple.relayoutCallback).toHaveBeenCalledTimes(1); - delay(done); - }); - }); + // Check structure of event callback value contents + expect(tuple.relayoutCallback).toHaveBeenCalledWith(jasmine.objectContaining({scene: cameraStructure})); - it('should respond to drag interactions', function(done) { + // Check camera contents on the DIV layout + var divCamera = tuple.graphDiv.layout.scene.camera; - // Expected shape of projection-related data - var cameraStructure = { - up: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)}, - center: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)}, - eye: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)} - }; + expect(divCamera).toEqual(cameraStructure); - setTimeout(function() { - - // One 'drag': simulating fairly thoroughly as the mouseup event is also needed here - mouseEvent('mousemove', 400, 200); - mouseEvent('mousedown', 400, 200); - mouseEvent('mousemove', 320, 320, {buttons: 1}); - mouseEvent('mouseup', 320, 320); - - // Check event emission count - expect(relayoutCallback).toHaveBeenCalledTimes(1); + return tuple.graphDiv; + } - // Check structure of event callback value contents - expect(relayoutCallback).toHaveBeenCalledWith(jasmine.objectContaining({scene: cameraStructure})); + function markForTeardown(graphDiv) { + // TODO consider changing this test file such that the teardown needs no communication via a global variable + gd = graphDiv; + } - // Check camera contents on the DIV layout - var divCamera = gd.layout.scene.camera; - expect(divCamera).toEqual(cameraStructure); + function testEvents(plot, done) { + plot.then(function(graphDiv) { + var tuple = addEventCallback(graphDiv); + verifyInteractionEffects(tuple); + markForTeardown(graphDiv); + done(); + }); + } - delay(done); + it('should respond to drag interactions with mock of unset camera', function(done) { + testEvents(makePlot(require('@mocks/gl3d_scatter3d-connectgaps.json')), done); + }); - }, MODEBAR_DELAY); + it('should respond to drag interactions with mock of partially set camera', function(done) { + testEvents(makePlot(require('@mocks/gl3d_errorbars_zx.json')), done); }); }); From 7eef2e12e6b6c1af97bcafe2d0358afde1938b27 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 6 May 2016 20:15:11 +0200 Subject: [PATCH 18/19] #30a removing new test case from the test file that's currently ignored by CI. The plan is to move more and more tests here, i.e. continuously improve on testing coverage. This test file purposefully avoids beforeEach and afterEach to favor a simpler to follow data flow (no globals). --- .../tests/gl_plot_interact_basic_test.js | 101 ++++++++++++++++++ test/jasmine/tests/gl_plot_interact_test.js | 64 ----------- 2 files changed, 101 insertions(+), 64 deletions(-) create mode 100644 test/jasmine/tests/gl_plot_interact_basic_test.js diff --git a/test/jasmine/tests/gl_plot_interact_basic_test.js b/test/jasmine/tests/gl_plot_interact_basic_test.js new file mode 100644 index 00000000000..b72ffeb7b11 --- /dev/null +++ b/test/jasmine/tests/gl_plot_interact_basic_test.js @@ -0,0 +1,101 @@ +'use strict'; + +var Plotly = require('@lib/index'); +var Plots = require('@src/plots/plots'); + +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var mouseEvent = require('../assets/mouse_event'); + +function teardown(gd, done) { + + // The teardown function needs information of what to tear down so afterEach can not be used without global vars. + // In addition to removing the plot from the DOM it also destroy possibly present 2D or 3D scenes + + // TODO we should figure out something to only rely on public API calls + // In other words, how can users themselves properly destroy the plot through the API? + // This function is left in this file until the above todo is looked into. + var fullLayout = gd._fullLayout; + + Plots.getSubplotIds(fullLayout, 'gl3d').forEach(function(sceneId) { + var scene = fullLayout[sceneId]._scene; + if(scene.glplot) scene.destroy(); + }); + + Plots.getSubplotIds(fullLayout, 'gl2d').forEach(function(sceneId) { + var scene2d = fullLayout._plots[sceneId]._scene2d; + if(scene2d.glplot) { + scene2d.stopped = true; + scene2d.destroy(); + } + }); + + destroyGraphDiv(); + + // A test case can only be called 'done' when the above destroy methods had been performed. + // One way of helping ensure that the destroys are not forgotten is that done() is part of + // the teardown, consequently if a test case omits the teardown by accident, the test will + // visibly hang. If the teardown receives no proper arguments, it'll also visibly fail. + done(); +} + +describe('Test gl plot interactions', function() { + + describe('gl3d plots', function() { + + // Expected shape of projection-related data + var cameraStructure = { + up: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)}, + center: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)}, + eye: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)} + }; + + function makePlot(mock) { + return Plotly.plot(createGraphDiv(), mock.data, mock.layout); + } + + function addEventCallback(graphDiv) { + var relayoutCallback = jasmine.createSpy('relayoutCallback'); + graphDiv.on('plotly_relayout', relayoutCallback); + return {graphDiv: graphDiv, relayoutCallback: relayoutCallback}; + } + + function verifyInteractionEffects(tuple) { + + // One 'drag': simulating fairly thoroughly as the mouseup event is also needed here + mouseEvent('mousemove', 400, 200); + mouseEvent('mousedown', 400, 200); + mouseEvent('mousemove', 320, 320, {buttons: 1}); + mouseEvent('mouseup', 320, 320); + + // Check event emission count + expect(tuple.relayoutCallback).toHaveBeenCalledTimes(1); + + // Check structure of event callback value contents + expect(tuple.relayoutCallback).toHaveBeenCalledWith(jasmine.objectContaining({scene: cameraStructure})); + + // Check camera contents on the DIV layout + var divCamera = tuple.graphDiv.layout.scene.camera; + + expect(divCamera).toEqual(cameraStructure); + + return tuple.graphDiv; + } + + function testEvents(plot, done) { + plot.then(function(graphDiv) { + var tuple = addEventCallback(graphDiv); // TODO disuse tuple with ES6 + verifyInteractionEffects(tuple); + teardown(graphDiv, done); + }); + } + + it('should respond to drag interactions with mock of unset camera', function(done) { + testEvents(makePlot(require('@mocks/gl3d_scatter3d-connectgaps.json')), done); + }); + + it('should respond to drag interactions with mock of partially set camera', function(done) { + testEvents(makePlot(require('@mocks/gl3d_errorbars_zx.json')), done); + }); + }); +}); diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 32a5310b4f7..5ed14da4bc0 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -211,70 +211,6 @@ describe('Test gl plot interactions', function() { }); - describe('gl3d plots', function() { - - // Expected shape of projection-related data - var cameraStructure = { - up: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)}, - center: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)}, - eye: {x: jasmine.any(Number), y: jasmine.any(Number), z: jasmine.any(Number)} - }; - - function makePlot(mock) { - return Plotly.plot(createGraphDiv(), mock.data, mock.layout); - } - - function addEventCallback(graphDiv) { - var relayoutCallback = jasmine.createSpy('relayoutCallback'); - graphDiv.on('plotly_relayout', relayoutCallback); - return {graphDiv: graphDiv, relayoutCallback: relayoutCallback}; - } - - function verifyInteractionEffects(tuple) { - - // One 'drag': simulating fairly thoroughly as the mouseup event is also needed here - mouseEvent('mousemove', 400, 200); - mouseEvent('mousedown', 400, 200); - mouseEvent('mousemove', 320, 320, {buttons: 1}); - mouseEvent('mouseup', 320, 320); - - // Check event emission count - expect(tuple.relayoutCallback).toHaveBeenCalledTimes(1); - - // Check structure of event callback value contents - expect(tuple.relayoutCallback).toHaveBeenCalledWith(jasmine.objectContaining({scene: cameraStructure})); - - // Check camera contents on the DIV layout - var divCamera = tuple.graphDiv.layout.scene.camera; - - expect(divCamera).toEqual(cameraStructure); - - return tuple.graphDiv; - } - - function markForTeardown(graphDiv) { - // TODO consider changing this test file such that the teardown needs no communication via a global variable - gd = graphDiv; - } - - function testEvents(plot, done) { - plot.then(function(graphDiv) { - var tuple = addEventCallback(graphDiv); - verifyInteractionEffects(tuple); - markForTeardown(graphDiv); - done(); - }); - } - - it('should respond to drag interactions with mock of unset camera', function(done) { - testEvents(makePlot(require('@mocks/gl3d_scatter3d-connectgaps.json')), done); - }); - - it('should respond to drag interactions with mock of partially set camera', function(done) { - testEvents(makePlot(require('@mocks/gl3d_errorbars_zx.json')), done); - }); - }); - describe('gl2d plots', function() { var mock = require('@mocks/gl2d_10.json'), modeBar, relayoutCallback; From 2a13e929f476d0c769726e0bb5af0d27bc484805 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 6 May 2016 21:32:29 +0200 Subject: [PATCH 19/19] #30a excluding gl_plot_interact_basic_test.js from the CI run. Benefit of having two files is that this new file is expected to pass reliably on a local non-headless jasmine run. So we should put more and more (reliable) test cases into this one so that test coverage for at least local automated testing is increasing, even though they're excluded from CI. --- test/jasmine/karma.ciconf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/karma.ciconf.js b/test/jasmine/karma.ciconf.js index 3e044c77df8..050d332f735 100644 --- a/test/jasmine/karma.ciconf.js +++ b/test/jasmine/karma.ciconf.js @@ -14,7 +14,7 @@ function func(config) { * exclude them from the CircleCI test bundle. * */ - func.defaultConfig.exclude = ['tests/gl_plot_interact_test.js']; + func.defaultConfig.exclude = ['tests/gl_plot_interact_test.js', 'tests/gl_plot_interact_basic_test.js']; // if true, Karma captures browsers, runs the tests and exits func.defaultConfig.singleRun = true;