From 79da5a34c17cca5fa117abe130787572c0d2dd01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 10 Mar 2017 14:53:18 -0500 Subject: [PATCH 1/5] lint --- test/jasmine/tests/gl_plot_interact_basic_test.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/jasmine/tests/gl_plot_interact_basic_test.js b/test/jasmine/tests/gl_plot_interact_basic_test.js index 0397084d1a6..097f63ee1a9 100644 --- a/test/jasmine/tests/gl_plot_interact_basic_test.js +++ b/test/jasmine/tests/gl_plot_interact_basic_test.js @@ -49,11 +49,10 @@ function verifyInteractionEffects(tuple) { } function testEvents(plot) { - return plot - .then(function(graphDiv) { - var tuple = addEventCallback(graphDiv); // TODO disuse tuple with ES6 - verifyInteractionEffects(tuple); - }); + return plot.then(function(graphDiv) { + var tuple = addEventCallback(graphDiv); + verifyInteractionEffects(tuple); + }); } describe('gl3d plots', function() { @@ -71,13 +70,13 @@ describe('gl3d plots', function() { it('should respond to drag interactions with mock of unset camera', function(done) { testEvents(makePlot(gd, require('@mocks/gl3d_scatter3d-connectgaps.json'))) - .then(null, failTest) // current linter balks on .catch with 'dot-notation'; fixme a linter + .catch(failTest) .then(done); }); it('should respond to drag interactions with mock of partially set camera', function(done) { testEvents(makePlot(gd, require('@mocks/gl3d_errorbars_zx.json'))) - .then(null, failTest) + .catch(failTest) .then(done); }); }); From a9d36218ac6be8d6b830de771a75087d60e619ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 10 Mar 2017 14:54:27 -0500 Subject: [PATCH 2/5] clean up gl_plot suite - remove deeply nested trees - split suite into multiple root-level 'describe's - reduce modebar delay --- test/jasmine/tests/gl_plot_interact_test.js | 1491 ++++++++++--------- 1 file changed, 755 insertions(+), 736 deletions(-) diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 746c9c5474c..153ca03a35a 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -11,188 +11,163 @@ var mouseEvent = require('../assets/mouse_event'); var selectButton = require('../assets/modebar_button'); var customMatchers = require('../assets/custom_matchers'); -var MODEBAR_DELAY = 500; - - -describe('Test gl plot interactions', function() { - 'use strict'; - - var gd; +// useful to put callback in the event queue +function delay() { + return new Promise(function(resolve) { + setTimeout(resolve, 0); + }); +} - beforeEach(function() { - jasmine.addMatchers(customMatchers); +function waitForModeBar() { + return new Promise(function(resolve) { + setTimeout(resolve, 200); }); +} - afterEach(function() { - var fullLayout = gd._fullLayout, - sceneIds; +function countCanvases() { + return d3.selectAll('canvas').size(); +} - sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'); - sceneIds.forEach(function(id) { - var scene = fullLayout[id]._scene; +describe('Test gl3d plots', function() { + var gd, ptData; - if(scene.glplot) scene.destroy(); - }); + var mock = require('@mocks/gl3d_marker-arrays.json'); - sceneIds = Plots.getSubplotIds(fullLayout, 'gl2d'); - sceneIds.forEach(function(id) { - var scene2d = fullLayout._plots[id]._scene2d; + // lines, markers, text, error bars and surfaces each + // correspond to one glplot object + var mock2 = Lib.extendDeep({}, mock); + mock2.data[0].mode = 'lines+markers+text'; + mock2.data[0].error_z = { value: 10 }; + mock2.data[0].surfaceaxis = 2; + mock2.layout.showlegend = true; - if(scene2d.glplot) { - scene2d.stopped = true; - scene2d.destroy(); - } - }); + function mouseEventScatter3d(type, opts) { + mouseEvent(type, 605, 271, opts); + } - destroyGraphDiv(); - }); + function assertHoverText(xLabel, yLabel, zLabel) { + var node = d3.selectAll('g.hovertext'); + expect(node.size()).toEqual(1, 'hover text group'); - // put callback in the event queue - function delay(done) { - setTimeout(done, 0); + var tspan = d3.selectAll('g.hovertext').selectAll('tspan')[0]; + expect(tspan[0].innerHTML).toEqual(xLabel, 'x val'); + expect(tspan[1].innerHTML).toEqual(yLabel, 'y val'); + expect(tspan[2].innerHTML).toEqual(zLabel, 'z val'); } - describe('gl3d plots', function() { - var mock = require('@mocks/gl3d_marker-arrays.json'); - - function mouseEventScatter3d(type, opts) { - mouseEvent(type, 605, 271, opts); - } + function assertEventData(x, y, z, curveNumber, pointNumber) { + expect(Object.keys(ptData)).toEqual([ + 'x', 'y', 'z', + 'data', 'fullData', 'curveNumber', 'pointNumber' + ], 'correct hover data fields'); + + expect(ptData.x).toEqual(x, 'x val'); + expect(ptData.y).toEqual(y, 'y val'); + expect(ptData.z).toEqual(z, 'z val'); + expect(ptData.curveNumber).toEqual(curveNumber, 'curveNumber'); + expect(ptData.pointNumber).toEqual(pointNumber, 'pointNumber'); + } - function countCanvases() { - return d3.selectAll('canvas').size(); - } + beforeEach(function() { + gd = createGraphDiv(); + ptData = {}; + }); - beforeEach(function(done) { - gd = createGraphDiv(); + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); - var mockCopy = Lib.extendDeep({}, mock); + it('should display correct hover labels and emit correct event data', function(done) { + var _mock = Lib.extendDeep({}, mock2); - // lines, markers, text, error bars and surfaces each - // correspond to one glplot object - mockCopy.data[0].mode = 'lines+markers+text'; - mockCopy.data[0].error_z = { value: 10 }; - mockCopy.data[0].surfaceaxis = 2; - mockCopy.layout.showlegend = true; + function _hover() { + mouseEventScatter3d('mouseover'); + return delay; + } - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - delay(done); + Plotly.plot(gd, _mock) + .then(delay) + .then(function() { + gd.on('plotly_hover', function(eventData) { + ptData = eventData.points[0]; }); - }); - - describe('scatter3d hover', function() { - - var ptData; - - beforeEach(function(done) { - gd.on('plotly_hover', function(eventData) { - ptData = eventData.points[0]; - }); - - mouseEventScatter3d('mouseover'); + }) + .then(_hover) + .then(delay) + .then(function() { + assertHoverText('x: 140.72', 'y: −96.97', 'z: −96.97'); + assertEventData('140.72', '−96.97', '−96.97', 0, 2); - delay(done); + return Plotly.restyle(gd, { + x: [['2016-01-11', '2016-01-12', '2017-01-01', '2017-02']] }); + }) + .then(_hover) + .then(function() { + assertHoverText('x: Jan 1, 2017', 'y: −96.97', 'z: −96.97'); - function assertHoverText(xLabel, yLabel, zLabel) { - var node = d3.selectAll('g.hovertext'); - expect(node.size()).toEqual(1, 'hover text group'); - - var tspan = d3.selectAll('g.hovertext').selectAll('tspan')[0]; - expect(tspan[0].innerHTML).toEqual(xLabel, 'x val'); - expect(tspan[1].innerHTML).toEqual(yLabel, 'y val'); - expect(tspan[2].innerHTML).toEqual(zLabel, 'z val'); - } - - it('makes the right hover text and point data', function(done) { - - function hover() { - mouseEventScatter3d('mouseover'); - return delay; - } - - assertHoverText('x: 140.72', 'y: −96.97', 'z: −96.97'); - - expect(Object.keys(ptData)).toEqual([ - 'x', 'y', 'z', - 'data', 'fullData', 'curveNumber', 'pointNumber' - ], 'correct hover data fields'); - - expect(ptData.x).toBe('140.72', 'x val'); - expect(ptData.y).toBe('−96.97', 'y val'); - expect(ptData.z).toEqual('−96.97', 'z val'); - expect(ptData.curveNumber).toEqual(0, 'curveNumber'); - expect(ptData.pointNumber).toEqual(2, 'pointNumber'); - - Plotly.restyle(gd, { - x: [['2016-01-11', '2016-01-12', '2017-01-01', '2017-02']] - }) - .then(hover) - .then(function() { - assertHoverText('x: Jan 1, 2017', 'y: −96.97', 'z: −96.97'); - - return Plotly.restyle(gd, { - x: [[new Date(2017, 2, 1), new Date(2017, 2, 2), new Date(2017, 2, 3), new Date(2017, 2, 4)]] - }); - }) - .then(hover) - .then(function() { - assertHoverText('x: Mar 3, 2017', 'y: −96.97', 'z: −96.97'); - - return Plotly.update(gd, { - y: [['a', 'b', 'c', 'd']], - z: [[10, 1e3, 1e5, 1e10]] - }, { - 'scene.zaxis.type': 'log' - }); - }) - .then(hover) - .then(function() { - assertHoverText('x: Mar 3, 2017', 'y: c', 'z: 100k'); - - return Plotly.relayout(gd, 'scene.xaxis.calendar', 'chinese'); - }) - .then(hover) - .then(function() { - assertHoverText('x: 二 6, 2017', 'y: c', 'z: 100k'); - }) - .then(done); + return Plotly.restyle(gd, { + x: [[new Date(2017, 2, 1), new Date(2017, 2, 2), new Date(2017, 2, 3), new Date(2017, 2, 4)]] }); - }); - - describe('scatter3d click events', function() { - var ptData; + }) + .then(_hover) + .then(function() { + assertHoverText('x: Mar 3, 2017', 'y: −96.97', 'z: −96.97'); - beforeEach(function(done) { - gd.on('plotly_click', function(eventData) { - ptData = eventData.points[0]; - }); + return Plotly.update(gd, { + y: [['a', 'b', 'c', 'd']], + z: [[10, 1e3, 1e5, 1e10]] + }, { + 'scene.zaxis.type': 'log' + }); + }) + .then(_hover) + .then(function() { + assertHoverText('x: Mar 3, 2017', 'y: c', 'z: 100k'); - // N.B. gl3d click events are 'mouseover' events - // with button 1 pressed - mouseEventScatter3d('mouseover', {buttons: 1}); + return Plotly.relayout(gd, 'scene.xaxis.calendar', 'chinese'); + }) + .then(_hover) + .then(function() { + assertHoverText('x: 二 6, 2017', 'y: c', 'z: 100k'); + }) + .then(done); - delay(done); - }); + }); - it('should have', function() { - expect(Object.keys(ptData)).toEqual([ - 'x', 'y', 'z', - 'data', 'fullData', 'curveNumber', 'pointNumber' - ], 'correct hover data fields'); + it('should emit correct event data on click', function(done) { + var _mock = Lib.extendDeep({}, mock2); + // N.B. gl3d click events are 'mouseover' events + // with button 1 pressed + function _click() { + mouseEventScatter3d('mouseover', {buttons: 1}); + return delay; + } - expect(ptData.x).toBe('140.72', 'x val click data'); - expect(ptData.y).toBe('−96.97', 'y val click data'); - expect(ptData.z).toEqual('−96.97', 'z val click data'); - expect(ptData.curveNumber).toEqual(0, 'curveNumber click data'); - expect(ptData.pointNumber).toEqual(2, 'pointNumber click data'); + Plotly.plot(gd, _mock) + .then(delay) + .then(function() { + gd.on('plotly_click', function(eventData) { + ptData = eventData.points[0]; }); - }); + }) + .then(_click) + .then(delay) + .then(function() { + assertEventData('140.72', '−96.97', '−96.97', 0, 2); + }) + .then(done); + }); - it('should be able to reversibly change trace type', function(done) { - var sceneLayout = { aspectratio: { x: 1, y: 1, z: 1 } }; + it('should be able to reversibly change trace type', function(done) { + var _mock = Lib.extendDeep({}, mock2); + var sceneLayout = { aspectratio: { x: 1, y: 1, z: 1 } }; + Plotly.plot(gd, _mock) + .then(delay) + .then(function() { expect(countCanvases()).toEqual(1); expect(gd.layout.scene).toEqual(sceneLayout); expect(gd.layout.xaxis).toBeUndefined(); @@ -200,677 +175,719 @@ describe('Test gl plot interactions', function() { expect(gd._fullLayout._has('gl3d')).toBe(true); expect(gd._fullLayout.scene._scene).toBeDefined(); - Plotly.restyle(gd, 'type', 'scatter').then(function() { - expect(countCanvases()).toEqual(0); - expect(gd.layout.scene).toEqual(sceneLayout); - expect(gd.layout.xaxis).toBeDefined(); - expect(gd.layout.yaxis).toBeDefined(); - expect(gd._fullLayout._has('gl3d')).toBe(false); - expect(gd._fullLayout.scene).toBeUndefined(); - - return Plotly.restyle(gd, 'type', 'scatter3d'); - }).then(function() { - expect(countCanvases()).toEqual(1); - expect(gd.layout.scene).toEqual(sceneLayout); - expect(gd.layout.xaxis).toBeDefined(); - expect(gd.layout.yaxis).toBeDefined(); - expect(gd._fullLayout._has('gl3d')).toBe(true); - expect(gd._fullLayout.scene._scene).toBeDefined(); - - done(); - }); - }); + return Plotly.restyle(gd, 'type', 'scatter'); + }) + .then(function() { + expect(countCanvases()).toEqual(0); + expect(gd.layout.scene).toEqual(sceneLayout); + expect(gd.layout.xaxis).toBeDefined(); + expect(gd.layout.yaxis).toBeDefined(); + expect(gd._fullLayout._has('gl3d')).toBe(false); + expect(gd._fullLayout.scene).toBeUndefined(); - it('should be able to delete the last trace', function(done) { - Plotly.deleteTraces(gd, [0]).then(function() { - expect(countCanvases()).toEqual(0); - expect(gd._fullLayout._has('gl3d')).toBe(false); - expect(gd._fullLayout.scene).toBeUndefined(); + return Plotly.restyle(gd, 'type', 'scatter3d'); + }) + .then(function() { + expect(countCanvases()).toEqual(1); + expect(gd.layout.scene).toEqual(sceneLayout); + expect(gd.layout.xaxis).toBeDefined(); + expect(gd.layout.yaxis).toBeDefined(); + expect(gd._fullLayout._has('gl3d')).toBe(true); + expect(gd._fullLayout.scene._scene).toBeDefined(); - done(); - }); - }); + }) + .then(done); + }); - it('should be able to toggle visibility', function(done) { - var objects = gd._fullLayout.scene._scene.glplot.objects; + it('should be able to delete the last trace', function(done) { + var _mock = Lib.extendDeep({}, mock2); - expect(objects.length).toEqual(5); + Plotly.plot(gd, _mock) + .then(delay) + .then(function() { + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + expect(countCanvases()).toEqual(0); + expect(gd._fullLayout._has('gl3d')).toBe(false); + expect(gd._fullLayout.scene).toBeUndefined(); + }) + .then(done); + }); - Plotly.restyle(gd, 'visible', 'legendonly').then(function() { - expect(objects.length).toEqual(0); + it('should be able to toggle visibility', function(done) { + var _mock = Lib.extendDeep({}, mock2); + var objects; - return Plotly.restyle(gd, 'visible', true); - }).then(function() { - expect(objects.length).toEqual(5); + Plotly.plot(gd, _mock) + .then(delay) + .then(function() { + objects = gd._fullLayout.scene._scene.glplot.objects; + expect(objects.length).toEqual(5); - done(); - }); - }); + return Plotly.restyle(gd, 'visible', 'legendonly'); + }) + .then(function() { + expect(objects.length).toEqual(0); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + expect(objects.length).toEqual(5); + }) + .then(done); }); - describe('gl2d plots', function() { - var mock = require('@mocks/gl2d_10.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); +describe('Test gl3d modebar handlers', function() { + var gd, modeBar; - delay(done); - }); - }); + function assertScenes(cont, attr, val) { + var sceneIds = Plots.getSubplotIds(cont, 'gl3d'); - it('has one *canvas* node', function() { - var nodes = d3.selectAll('canvas'); - expect(nodes[0].length).toEqual(1); + sceneIds.forEach(function(sceneId) { + var thisVal = Lib.nestedProperty(cont[sceneId], attr).get(); + expect(thisVal).toEqual(val); }); + } - it('should respond to drag interactions', function(done) { - - function mouseTo(p0, p1) { - mouseEvent('mousemove', p0[0], p0[1]); - mouseEvent('mousedown', p0[0], p0[1], { buttons: 1 }); - mouseEvent('mousemove', p1[0], p1[1], { buttons: 1 }); - mouseEvent('mouseup', p1[0], p1[1]); - } - - jasmine.addMatchers(customMatchers); - - var precision = 5; - - var buttonPan = selectButton(modeBar, 'pan2d'); - - var originalX = [-0.022068095838587643, 5.022068095838588]; - var originalY = [-0.21331533513634046, 5.851205650049042]; + function assertCameraEye(sceneLayout, eyeX, eyeY, eyeZ) { + expect(sceneLayout.camera.eye.x).toEqual(eyeX); + expect(sceneLayout.camera.eye.y).toEqual(eyeY); + expect(sceneLayout.camera.eye.z).toEqual(eyeZ); - var newX = [-0.23224043715846995, 4.811895754518705]; - var newY = [-1.2962655110623016, 4.768255474123081]; + var camera = sceneLayout._scene.getCamera(); + expect(camera.eye.x).toBeCloseTo(eyeX); + expect(camera.eye.y).toBeCloseTo(eyeY); + expect(camera.eye.z).toBeCloseTo(eyeZ); + } - expect(gd.layout.xaxis.autorange).toBe(true); - expect(gd.layout.yaxis.autorange).toBe(true); - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); - // Switch to pan mode - expect(buttonPan.isActive()).toBe(false); // initially, zoom is active - buttonPan.click(); - expect(buttonPan.isActive()).toBe(true); // switched on dragmode + beforeEach(function(done) { + gd = createGraphDiv(); - // Switching mode must not change visible range - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + var mock = { + data: [ + { type: 'scatter3d' }, + { type: 'surface', scene: 'scene2' } + ], + layout: { + scene: { camera: { eye: { x: 0.1, y: 0.1, z: 1 }}}, + scene2: { camera: { eye: { x: 2.5, y: 2.5, z: 2.5 }}} + } + }; - setTimeout(function() { - relayoutCallback.calls.reset(); + Plotly.plot(gd, mock) + .then(delay) + .then(function() { + modeBar = gd._fullLayout._modeBar; + }) + .then(done); + }); - // Drag scene along the X axis - mouseTo([200, 200], [220, 200]); + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); - expect(gd.layout.xaxis.autorange).toBe(false); - expect(gd.layout.yaxis.autorange).toBe(false); + it('button zoom3d should updates the scene dragmode and dragmode button', function() { + var buttonTurntable = selectButton(modeBar, 'tableRotation'); + var 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); + }); - expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + it('button pan3d 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); + }); - // Drag scene back along the X axis - mouseTo([220, 200], [200, 200]); + it('button orbitRotation 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); + }); - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + it('button hoverClosest3d 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); + }); - // Drag scene along the Y axis - mouseTo([200, 200], [200, 150]); + it('button resetCameraDefault3d should reset camera to default', function(done) { + var buttonDefault = selectButton(modeBar, 'resetCameraDefault3d'); - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); + expect(gd._fullLayout.scene._scene.cameraInitial.eye).toEqual({ x: 0.1, y: 0.1, z: 1 }); + expect(gd._fullLayout.scene2._scene.cameraInitial.eye).toEqual({ x: 2.5, y: 2.5, z: 2.5 }); - // Drag scene back along the Y axis - mouseTo([200, 150], [200, 200]); + gd.once('plotly_relayout', function() { + assertScenes(gd._fullLayout, 'camera.eye.x', 1.25); + assertScenes(gd._fullLayout, 'camera.eye.y', 1.25); + assertScenes(gd._fullLayout, 'camera.eye.z', 1.25); - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + expect(gd._fullLayout.scene._scene.getCamera().eye.z).toBeCloseTo(1.25); + expect(gd._fullLayout.scene2._scene.getCamera().eye.z).toBeCloseTo(1.25); - // Drag scene along both the X and Y axis - mouseTo([200, 200], [220, 150]); + done(); + }); - expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); + buttonDefault.click(); + }); - // Drag scene back along the X and Y axis - mouseTo([220, 150], [200, 200]); + it('button resetCameraLastSave3d should reset camera to default', function(done) { + var buttonDefault = selectButton(modeBar, 'resetCameraDefault3d'); + var buttonLastSave = selectButton(modeBar, 'resetCameraLastSave3d'); - expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); - expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + Plotly.relayout(gd, { + 'scene.camera.eye.z': 4, + 'scene2.camera.eye.z': 5 + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); - setTimeout(function() { + return new Promise(function(resolve) { + gd.once('plotly_relayout', resolve); + buttonLastSave.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 1); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 2.5); - // callback count expectation: X and back; Y and back; XY and back - expect(relayoutCallback).toHaveBeenCalledTimes(6); + return new Promise(function(resolve) { + gd.once('plotly_relayout', resolve); + buttonDefault.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 1.25, 1.25, 1.25); + assertCameraEye(gd._fullLayout.scene2, 1.25, 1.25, 1.25); - // 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)] - })); + return new Promise(function(resolve) { + gd.once('plotly_relayout', resolve); + buttonLastSave.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 1); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 2.5); - done(); + delete gd._fullLayout.scene._scene.cameraInitial; + delete gd._fullLayout.scene2._scene.cameraInitial; - }, MODEBAR_DELAY); + Plotly.relayout(gd, { + 'scene.bgcolor': '#d3d3d3', + 'scene.camera.eye.z': 4, + 'scene2.camera.eye.z': 5 + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); - }, MODEBAR_DELAY); - }); + return new Promise(function(resolve) { + gd.once('plotly_relayout', resolve); + buttonDefault.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 1.25, 1.25, 1.25); + assertCameraEye(gd._fullLayout.scene2, 1.25, 1.25, 1.25); - it('should be able to toggle visibility', function(done) { - var OBJECT_PER_TRACE = 5; + return new Promise(function(resolve) { + gd.once('plotly_relayout', resolve); + buttonLastSave.click(); + }); + }) + .then(function() { + assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); + assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); + }) + .then(done); - var objects = function() { - return gd._fullLayout._plots.xy._scene2d.glplot.objects; - }; + }); +}); - expect(objects().length).toEqual(OBJECT_PER_TRACE); +describe('Test gl3d drag and wheel interactions', function() { + var gd, relayoutCallback; - Plotly.restyle(gd, 'visible', 'legendonly').then(function() { - expect(objects().length).toEqual(OBJECT_PER_TRACE); - expect(objects()[0].data.length).toEqual(0); - - return Plotly.restyle(gd, 'visible', true); - }) - .then(function() { - expect(objects().length).toEqual(OBJECT_PER_TRACE); - expect(objects()[0].data.length).not.toEqual(0); - - return Plotly.restyle(gd, 'visible', false); - }) - .then(function() { - expect(gd._fullLayout._plots.xy._scene2d).toBeUndefined(); - - return Plotly.restyle(gd, 'visible', true); - }) - .then(function() { - expect(objects().length).toEqual(OBJECT_PER_TRACE); - expect(objects()[0].data.length).not.toEqual(0); - }) - .then(done); + function scroll(target) { + return new Promise(function(resolve) { + target.dispatchEvent(new WheelEvent('wheel', {deltaY: 1})); + setTimeout(resolve, 0); }); + } - it('should clear orphan cartesian subplots on addTraces', function(done) { - - Plotly.newPlot(gd, [], { - xaxis: { title: 'X' }, - yaxis: { title: 'Y' } - }) - .then(function() { - return Plotly.addTraces(gd, [{ - type: 'scattergl', - x: [1, 2, 3, 4, 5, 6, 7], - y: [0, 5, 8, 9, 8, 5, 0] - }]); - }) - .then(function() { - expect(d3.select('.subplot.xy').size()).toEqual(0); - expect(d3.select('.xtitle').size()).toEqual(0); - expect(d3.select('.ytitle').size()).toEqual(0); - }) - .then(done); - + function drag(target) { + return new Promise(function(resolve) { + target.dispatchEvent(new MouseEvent('mousedown', {x: 0, y: 0})); + target.dispatchEvent(new MouseEvent('mousemove', { x: 100, y: 100})); + target.dispatchEvent(new MouseEvent('mouseup', { x: 100, y: 100})); + setTimeout(resolve, 0); }); - }); - - describe('gl3d event handlers', function() { - var modeBar, relayoutCallback; + } - beforeEach(function(done) { - var mockData = [{ - type: 'scatter3d' - }, { - type: 'surface', scene: 'scene2' - }]; + beforeEach(function(done) { + gd = createGraphDiv(); - var mockLayout = { + var mock = { + data: [ + { type: 'scatter3d' }, + { type: 'surface', scene: 'scene2' } + ], + layout: { scene: { camera: { eye: { x: 0.1, y: 0.1, z: 1 }}}, scene2: { camera: { eye: { x: 2.5, y: 2.5, z: 2.5 }}} - }; - - gd = createGraphDiv(); - Plotly.plot(gd, mockData, mockLayout).then(function() { - - modeBar = gd._fullLayout._modeBar; - - relayoutCallback = jasmine.createSpy('relayoutCallback'); - - gd.on('plotly_relayout', relayoutCallback); + } + }; - delay(done); - }); - }); + Plotly.plot(gd, mock) + .then(delay) + .then(function() { + relayoutCallback = jasmine.createSpy('relayoutCallback'); + gd.on('plotly_relayout', relayoutCallback); + }) + .then(done); + }); - function assertScenes(cont, attr, val) { - var sceneIds = Plots.getSubplotIds(cont, 'gl3d'); + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); - sceneIds.forEach(function(sceneId) { - var thisVal = Lib.nestedProperty(cont[sceneId], attr).get(); - expect(thisVal).toEqual(val); - }); - } + 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'); - describe('modebar click handlers', function() { + 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}); - it('button zoom3d should updates the scene dragmode and dragmode button', function() { - var buttonTurntable = selectButton(modeBar, 'tableRotation'), - buttonZoom3d = selectButton(modeBar, 'zoom3d'); + scroll(sceneTarget).then(function() { + expect(relayoutCallback).toHaveBeenCalledTimes(1); + relayoutCallback.calls.reset(); - assertScenes(gd._fullLayout, 'dragmode', 'turntable'); - expect(buttonTurntable.isActive()).toBe(true); - expect(buttonZoom3d.isActive()).toBe(false); + return scroll(sceneTarget2); + }) + .then(function() { + expect(relayoutCallback).toHaveBeenCalledTimes(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); + return drag(sceneTarget2); + }) + .then(function() { + expect(relayoutCallback).toHaveBeenCalledTimes(1); + relayoutCallback.calls.reset(); - buttonTurntable.click(); - assertScenes(gd._fullLayout, 'dragmode', 'turntable'); - expect(buttonTurntable.isActive()).toBe(true); - expect(buttonZoom3d.isActive()).toBe(false); - }); + return drag(sceneTarget); + }) + .then(function() { + expect(relayoutCallback).toHaveBeenCalledTimes(1); + relayoutCallback.calls.reset(); - it('button pan3d 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); + return Plotly.relayout(gd, { + 'scene.dragmode': false, + 'scene2.dragmode': false }); + }) + .then(function() { + expect(relayoutCallback).toHaveBeenCalledTimes(1); + relayoutCallback.calls.reset(); - it('button orbitRotation 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); - }); + return drag(sceneTarget); + }) + .then(function() { + return drag(sceneTarget2); + }) + .then(function() { + expect(relayoutCallback).toHaveBeenCalledTimes(0); - it('button hoverClosest3d 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); + return Plotly.relayout(gd, { + 'scene.dragmode': 'orbit', + 'scene2.dragmode': 'turntable' }); + }) + .then(function() { + expect(relayoutCallback).toHaveBeenCalledTimes(1); + relayoutCallback.calls.reset(); - it('button resetCameraDefault3d should reset camera to default', function(done) { - var buttonDefault = selectButton(modeBar, 'resetCameraDefault3d'); + return drag(sceneTarget); + }) + .then(function() { + return drag(sceneTarget2); + }) + .then(function() { + expect(relayoutCallback).toHaveBeenCalledTimes(2); + }) + .then(done); + }); +}); - expect(gd._fullLayout.scene._scene.cameraInitial.eye).toEqual({ x: 0.1, y: 0.1, z: 1 }); - expect(gd._fullLayout.scene2._scene.cameraInitial.eye).toEqual({ x: 2.5, y: 2.5, z: 2.5 }); +describe('Test gl2d plots', function() { + var gd; - gd.once('plotly_relayout', function() { - assertScenes(gd._fullLayout, 'camera.eye.x', 1.25); - assertScenes(gd._fullLayout, 'camera.eye.y', 1.25); - assertScenes(gd._fullLayout, 'camera.eye.z', 1.25); + var mock = require('@mocks/gl2d_10.json'); - expect(gd._fullLayout.scene._scene.getCamera().eye.z).toBeCloseTo(1.25); - expect(gd._fullLayout.scene2._scene.getCamera().eye.z).toBeCloseTo(1.25); + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); - done(); - }); + beforeEach(function() { + gd = createGraphDiv(); + }); - buttonDefault.click(); - }); + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); - it('button resetCameraLastSave3d should reset camera to default', function(done) { - var buttonDefault = selectButton(modeBar, 'resetCameraDefault3d'); - var buttonLastSave = selectButton(modeBar, 'resetCameraLastSave3d'); - - function assertCameraEye(sceneLayout, eyeX, eyeY, eyeZ) { - expect(sceneLayout.camera.eye.x).toEqual(eyeX); - expect(sceneLayout.camera.eye.y).toEqual(eyeY); - expect(sceneLayout.camera.eye.z).toEqual(eyeZ); - - var camera = sceneLayout._scene.getCamera(); - expect(camera.eye.x).toBeCloseTo(eyeX); - expect(camera.eye.y).toBeCloseTo(eyeY); - expect(camera.eye.z).toBeCloseTo(eyeZ); - } - - Plotly.relayout(gd, { - 'scene.camera.eye.z': 4, - 'scene2.camera.eye.z': 5 - }) - .then(function() { - assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); - assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); - - return new Promise(function(resolve) { - gd.once('plotly_relayout', resolve); - buttonLastSave.click(); - }); - }) - .then(function() { - assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 1); - assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 2.5); - - return new Promise(function(resolve) { - gd.once('plotly_relayout', resolve); - buttonDefault.click(); - }); - }) - .then(function() { - assertCameraEye(gd._fullLayout.scene, 1.25, 1.25, 1.25); - assertCameraEye(gd._fullLayout.scene2, 1.25, 1.25, 1.25); - - return new Promise(function(resolve) { - gd.once('plotly_relayout', resolve); - buttonLastSave.click(); - }); - }) - .then(function() { - assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 1); - assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 2.5); - - delete gd._fullLayout.scene._scene.cameraInitial; - delete gd._fullLayout.scene2._scene.cameraInitial; - - Plotly.relayout(gd, { - 'scene.bgcolor': '#d3d3d3', - 'scene.camera.eye.z': 4, - 'scene2.camera.eye.z': 5 - }); - }) - .then(function() { - assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); - assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); - - return new Promise(function(resolve) { - gd.once('plotly_relayout', resolve); - buttonDefault.click(); - }); - }) - .then(function() { - assertCameraEye(gd._fullLayout.scene, 1.25, 1.25, 1.25); - assertCameraEye(gd._fullLayout.scene2, 1.25, 1.25, 1.25); - - return new Promise(function(resolve) { - gd.once('plotly_relayout', resolve); - buttonLastSave.click(); - }); - }) - .then(function() { - assertCameraEye(gd._fullLayout.scene, 0.1, 0.1, 4); - assertCameraEye(gd._fullLayout.scene2, 2.5, 2.5, 5); - }) - .then(done); + it('should respond to drag interactions', function(done) { + var _mock = Lib.extendDeep({}, mock); + var relayoutCallback = jasmine.createSpy('relayoutCallback'); + + var originalX = [-0.022068095838587643, 5.022068095838588]; + var originalY = [-0.21331533513634046, 5.851205650049042]; + var newX = [-0.23224043715846995, 4.811895754518705]; + var newY = [-1.2962655110623016, 4.768255474123081]; + var precision = 5; + + function mouseTo(p0, p1) { + mouseEvent('mousemove', p0[0], p0[1]); + mouseEvent('mousedown', p0[0], p0[1], { buttons: 1 }); + mouseEvent('mousemove', p1[0], p1[1], { buttons: 1 }); + mouseEvent('mouseup', p1[0], p1[1]); + } - }); - }); + Plotly.plot(gd, _mock) + .then(delay) + .then(function() { + expect(gd.layout.xaxis.autorange).toBe(true); + expect(gd.layout.yaxis.autorange).toBe(true); + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); - describe('drag and wheel interactions', function() { + // Switch to pan mode + var buttonPan = selectButton(gd._fullLayout._modeBar, 'pan2d'); + expect(buttonPan.isActive()).toBe(false, 'initially, zoom is active'); + buttonPan.click(); + expect(buttonPan.isActive()).toBe(true, 'switched on dragmode'); - function scroll(target) { - return new Promise(function(resolve) { - target.dispatchEvent(new WheelEvent('wheel', {deltaY: 1})); - setTimeout(resolve, 0); - }); - } + // Switching mode must not change visible range + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + }) + .then(waitForModeBar) + .then(function() { + gd.on('plotly_relayout', relayoutCallback); - function drag(target) { - return new Promise(function(resolve) { - target.dispatchEvent(new MouseEvent('mousedown', {x: 0, y: 0})); - target.dispatchEvent(new MouseEvent('mousemove', { x: 100, y: 100})); - target.dispatchEvent(new MouseEvent('mouseup', { x: 100, y: 100})); - setTimeout(resolve, 0); - }); - } + // Drag scene along the X axis + mouseTo([200, 200], [220, 200]); - 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}); - - scroll(sceneTarget).then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - - return scroll(sceneTarget2); - }) - .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - - return drag(sceneTarget2); - }) - .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - - return drag(sceneTarget); - }) - .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - - return Plotly.relayout(gd, { - 'scene.dragmode': false, - 'scene2.dragmode': false - }); - }) - .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - - return drag(sceneTarget); - }) - .then(function() { - return drag(sceneTarget2); - }) - .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(0); - - return Plotly.relayout(gd, { - 'scene.dragmode': 'orbit', - 'scene2.dragmode': 'turntable' - }); - }) - .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(1); - relayoutCallback.calls.reset(); - - return drag(sceneTarget); - }) - .then(function() { - return drag(sceneTarget2); - }) - .then(function() { - expect(relayoutCallback).toHaveBeenCalledTimes(2); - }) - .then(done); - }); - }); - }); + expect(gd.layout.xaxis.autorange).toBe(false); + expect(gd.layout.yaxis.autorange).toBe(false); - describe('Removal of gl contexts', function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); - var mockData2d = [{ - type: 'scattergl', - x: [1, 2, 3], - y: [2, 1, 3] - }]; + // Drag scene back along the X axis + mouseTo([220, 200], [200, 200]); + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); - var mockData3d = [{ - type: 'scatter3d', - x: [1, 2, 3], - y: [2, 1, 3], - z: [3, 2, 1] - }]; + // Drag scene along the Y axis + mouseTo([200, 200], [200, 150]); - describe('Plots.cleanPlot', function() { + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); - it('should remove gl context from the graph div of a gl3d plot', function(done) { - gd = createGraphDiv(); + // Drag scene back along the Y axis + mouseTo([200, 150], [200, 200]); - Plotly.plot(gd, mockData3d).then(function() { - expect(gd._fullLayout.scene._scene.glplot).toBeDefined(); + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); - Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout); - expect(gd._fullLayout.scene._scene.glplot).toBe(null); + // Drag scene along both the X and Y axis + mouseTo([200, 200], [220, 150]); - done(); - }); - }); + expect(gd.layout.xaxis.range).toBeCloseToArray(newX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(newY, precision); - it('should remove gl context from the graph div of a gl2d plot', function(done) { - gd = createGraphDiv(); + // Drag scene back along the X and Y axis + mouseTo([220, 150], [200, 200]); - Plotly.plot(gd, mockData2d).then(function() { - expect(gd._fullLayout._plots.xy._scene2d.glplot).toBeDefined(); + expect(gd.layout.xaxis.range).toBeCloseToArray(originalX, precision); + expect(gd.layout.yaxis.range).toBeCloseToArray(originalY, precision); + }) + .then(waitForModeBar) + .then(function() { + // 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)] + })); + }) + .then(done); + }); - Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout); - expect(gd._fullLayout._plots).toEqual({}); + it('should be able to toggle visibility', function(done) { + var _mock = Lib.extendDeep({}, mock); + var OBJECT_PER_TRACE = 5; - done(); - }); - }); - }); + var objects = function() { + return gd._fullLayout._plots.xy._scene2d.glplot.objects; + }; - describe('Plotly.newPlot', function() { + Plotly.plot(gd, _mock) + .then(delay) + .then(function() { + expect(objects().length).toEqual(OBJECT_PER_TRACE); - var mockData2dNew = [{ - type: 'scattergl', - x: [1, 3, 2], - y: [2, 3, 1] - }]; + return Plotly.restyle(gd, 'visible', 'legendonly'); + }) + .then(function() { + expect(objects().length).toEqual(OBJECT_PER_TRACE); + expect(objects()[0].data.length).toEqual(0); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + expect(objects().length).toEqual(OBJECT_PER_TRACE); + expect(objects()[0].data.length).not.toEqual(0); - var mockData3dNew = [{ - type: 'scatter3d', - x: [2, 1, 3], - y: [1, 2, 3], - z: [2, 1, 3] - }]; + return Plotly.restyle(gd, 'visible', false); + }) + .then(function() { + expect(gd._fullLayout._plots.xy._scene2d).toBeUndefined(); + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + expect(objects().length).toEqual(OBJECT_PER_TRACE); + expect(objects()[0].data.length).not.toEqual(0); + }) + .then(done); + }); - it('should remove gl context from the graph div of a gl3d plot', function(done) { - gd = createGraphDiv(); + it('should clear orphan cartesian subplots on addTraces', function(done) { - Plotly.plot(gd, mockData3d).then(function() { + Plotly.newPlot(gd, [], { + xaxis: { title: 'X' }, + yaxis: { title: 'Y' } + }) + .then(function() { + return Plotly.addTraces(gd, [{ + type: 'scattergl', + x: [1, 2, 3, 4, 5, 6, 7], + y: [0, 5, 8, 9, 8, 5, 0] + }]); + }) + .then(function() { + expect(d3.select('.subplot.xy').size()).toEqual(0); + expect(d3.select('.xtitle').size()).toEqual(0); + expect(d3.select('.ytitle').size()).toEqual(0); + }) + .then(done); - var firstGlplotObject = gd._fullLayout.scene._scene.glplot; - var firstGlContext = firstGlplotObject.gl; - var firstCanvas = firstGlContext.canvas; + }); +}); - expect(firstGlplotObject).toBeDefined(); +describe('Test removal of gl contexts', function() { + var gd; - Plotly.newPlot(gd, mockData3dNew, {}).then(function() { + beforeEach(function() { + gd = createGraphDiv(); + }); - var secondGlplotObject = gd._fullLayout.scene._scene.glplot; - var secondGlContext = secondGlplotObject.gl; - var secondCanvas = secondGlContext.canvas; + afterEach(destroyGraphDiv); - expect(secondGlplotObject).not.toBe(firstGlplotObject); - expect(firstGlplotObject.gl === null); - expect(secondGlContext instanceof WebGLRenderingContext); - expect(secondGlContext).not.toBe(firstGlContext); + it('Plots.cleanPlot should remove gl context from the graph div of a gl3d plot', function(done) { + Plotly.plot(gd, [{ + type: 'scatter3d', + x: [1, 2, 3], + y: [2, 1, 3], + z: [3, 2, 1] + }]) + .then(function() { + expect(gd._fullLayout.scene._scene.glplot).toBeDefined(); - // The same canvas can't possibly be reassinged a new WebGL context, but let's leave room - // for the implementation to make the context get lost and have the old canvas stick around - // in a disused state. - expect(firstCanvas.parentNode === null || - firstCanvas !== secondCanvas && firstGlContext.isContextLost()); + Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout); + expect(gd._fullLayout.scene._scene.glplot).toBe(null); + }) + .then(done); + }); - done(); + it('Plots.cleanPlot should remove gl context from the graph div of a gl2d plot', function(done) { + Plotly.plot(gd, [{ + type: 'scattergl', + x: [1, 2, 3], + y: [2, 1, 3] + }]) + .then(function() { + expect(gd._fullLayout._plots.xy._scene2d.glplot).toBeDefined(); - }); - }); - }); + Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout); + expect(gd._fullLayout._plots).toEqual({}); + }) + .then(done); + }); - it('should remove gl context from the graph div of a gl2d plot', function(done) { - gd = createGraphDiv(); + it('Plotly.newPlot should remove gl context from the graph div of a gl3d plot', function(done) { + var firstGlplotObject, firstGlContext, firstCanvas; - Plotly.plot(gd, mockData2d).then(function() { + Plotly.plot(gd, [{ + type: 'scatter3d', + x: [1, 2, 3], + y: [2, 1, 3], + z: [3, 2, 1] + }]) + .then(function() { + firstGlplotObject = gd._fullLayout.scene._scene.glplot; + firstGlContext = firstGlplotObject.gl; + firstCanvas = firstGlContext.canvas; - var firstGlplotObject = gd._fullLayout._plots.xy._scene2d.glplot; - var firstGlContext = firstGlplotObject.gl; - var firstCanvas = firstGlContext.canvas; + expect(firstGlplotObject).toBeDefined(); - expect(firstGlplotObject).toBeDefined(); - expect(firstGlContext).toBeDefined(); - expect(firstGlContext instanceof WebGLRenderingContext); + return Plotly.newPlot(gd, [{ + type: 'scatter3d', + x: [2, 1, 3], + y: [1, 2, 3], + z: [2, 1, 3] + }], {}); + }) + .then(function() { + var secondGlplotObject = gd._fullLayout.scene._scene.glplot; + var secondGlContext = secondGlplotObject.gl; + var secondCanvas = secondGlContext.canvas; + + expect(secondGlplotObject).not.toBe(firstGlplotObject); + expect(firstGlplotObject.gl === null); + expect(secondGlContext instanceof WebGLRenderingContext); + expect(secondGlContext).not.toBe(firstGlContext); + + // The same canvas can't possibly be reassinged a new WebGL context, but let's leave room + // for the implementation to make the context get lost and have the old canvas stick around + // in a disused state. + expect( + firstCanvas.parentNode === null || + firstCanvas !== secondCanvas && firstGlContext.isContextLost() + ); + }) + .then(done); + }); - Plotly.newPlot(gd, mockData2dNew, {}).then(function() { + it('Plotly.newPlot should remove gl context from the graph div of a gl2d plot', function(done) { + var firstGlplotObject, firstGlContext, firstCanvas; - var secondGlplotObject = gd._fullLayout._plots.xy._scene2d.glplot; - var secondGlContext = secondGlplotObject.gl; - var secondCanvas = secondGlContext.canvas; + Plotly.plot(gd, [{ + type: 'scattergl', + x: [1, 2, 3], + y: [2, 1, 3] + }]) + .then(function() { + firstGlplotObject = gd._fullLayout._plots.xy._scene2d.glplot; + firstGlContext = firstGlplotObject.gl; + firstCanvas = firstGlContext.canvas; - expect(Object.keys(gd._fullLayout._plots).length === 1); - expect(secondGlplotObject).not.toBe(firstGlplotObject); - expect(firstGlplotObject.gl === null); - expect(secondGlContext instanceof WebGLRenderingContext); - expect(secondGlContext).not.toBe(firstGlContext); - expect(firstCanvas.parentNode === null || - firstCanvas !== secondCanvas && firstGlContext.isContextLost()); + expect(firstGlplotObject).toBeDefined(); + expect(firstGlContext).toBeDefined(); + expect(firstGlContext instanceof WebGLRenderingContext); - done(); - }); - }); - }); - }); + return Plotly.newPlot(gd, [{ + type: 'scattergl', + x: [1, 2, 3], + y: [2, 1, 3] + }], {}); + }) + .then(function() { + var secondGlplotObject = gd._fullLayout._plots.xy._scene2d.glplot; + var secondGlContext = secondGlplotObject.gl; + var secondCanvas = secondGlContext.canvas; + + expect(Object.keys(gd._fullLayout._plots).length === 1); + expect(secondGlplotObject).not.toBe(firstGlplotObject); + expect(firstGlplotObject.gl === null); + expect(secondGlContext instanceof WebGLRenderingContext); + expect(secondGlContext).not.toBe(firstGlContext); + + expect( + firstCanvas.parentNode === null || + firstCanvas !== secondCanvas && firstGlContext.isContextLost() + ); + }) + .then(done); }); }); @@ -881,33 +898,35 @@ describe('Test gl plot side effects', function() { gd = createGraphDiv(); }); - afterEach(destroyGraphDiv); + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); - describe('when present with rangeslider', function() { - it('should not draw the rangeslider', function(done) { - var data = [{ - x: [1, 2, 3], - y: [2, 3, 4], - type: 'scattergl' - }, { - x: [1, 2, 3], - y: [2, 3, 4], - type: 'scatter' - }]; - - var layout = { - xaxis: { rangeslider: { visible: true } } - }; - - Plotly.plot(gd, data, layout).then(function() { - var rangeSlider = document.getElementsByClassName('range-slider')[0]; - expect(rangeSlider).not.toBeDefined(); - done(); - }); - }); + it('should not draw the rangeslider', function(done) { + var data = [{ + x: [1, 2, 3], + y: [2, 3, 4], + type: 'scattergl' + }, { + x: [1, 2, 3], + y: [2, 3, 4], + type: 'scatter' + }]; + + var layout = { + xaxis: { rangeslider: { visible: true } } + }; + + Plotly.plot(gd, data, layout).then(function() { + var rangeSlider = document.getElementsByClassName('range-slider')[0]; + expect(rangeSlider).not.toBeDefined(); + }) + .then(done); }); it('should be able to replot from a blank graph', function(done) { + function countCanvases(cnt) { var nodes = d3.selectAll('canvas'); expect(nodes.size()).toEqual(cnt); @@ -943,7 +962,7 @@ describe('Test gl plot side effects', function() { }); }); -describe('gl2d interaction', function() { +describe('Test gl2d interactions', function() { var gd; beforeAll(function() { From 739f20f4c41a26f91b24310ad3f0e92a4aa4a402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 10 Mar 2017 15:27:59 -0500 Subject: [PATCH 3/5] sort gl3d primitive object on updates - ensuring that visibility toggles don't reorder the traces --- src/plots/gl3d/scene.js | 5 ++ src/traces/mesh3d/convert.js | 1 + src/traces/scatter3d/convert.js | 5 ++ src/traces/surface/convert.js | 1 + test/jasmine/tests/gl_plot_interact_test.js | 51 +++++++++++++++++++-- 5 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 0373a665a19..0d05b8813ca 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -419,6 +419,11 @@ proto.plot = function(sceneData, fullLayout, layout) { delete this.traces[traceIds[i]]; } + // order object per trace index + this.glplot.objects.sort(function(a, b) { + return a._trace.data.index - b._trace.data.index; + }); + // Update ranges (needs to be called *after* objects are added due to updates) var sceneBounds = [[0, 0, 0], [0, 0, 0]], axisDataRange = [], diff --git a/src/traces/mesh3d/convert.js b/src/traces/mesh3d/convert.js index 13768b0b62d..dee2f0647ca 100644 --- a/src/traces/mesh3d/convert.js +++ b/src/traces/mesh3d/convert.js @@ -152,6 +152,7 @@ function createMesh3DTrace(scene, data) { var gl = scene.glplot.gl; var mesh = createMesh({gl: gl}); var result = new Mesh3DTrace(scene, mesh, data.uid); + mesh._trace = result; result.update(data); scene.glplot.add(mesh); return result; diff --git a/src/traces/scatter3d/convert.js b/src/traces/scatter3d/convert.js index fa1a0a8f2dd..0b45b090f3b 100644 --- a/src/traces/scatter3d/convert.js +++ b/src/traces/scatter3d/convert.js @@ -314,6 +314,7 @@ proto.update = function(data) { if(this.linePlot) this.linePlot.update(lineOptions); else { this.linePlot = createLinePlot(lineOptions); + this.linePlot._trace = this; this.scene.glplot.add(this.linePlot); } } else if(this.linePlot) { @@ -345,6 +346,7 @@ proto.update = function(data) { if(this.scatterPlot) this.scatterPlot.update(scatterOptions); else { this.scatterPlot = createScatterPlot(scatterOptions); + this.scatterPlot._trace = this; this.scatterPlot.highlightScale = 1; this.scene.glplot.add(this.scatterPlot); } @@ -375,6 +377,7 @@ proto.update = function(data) { if(this.textMarkers) this.textMarkers.update(textOptions); else { this.textMarkers = createScatterPlot(textOptions); + this.textMarkers._trace = this; this.textMarkers.highlightScale = 1; this.scene.glplot.add(this.textMarkers); } @@ -403,6 +406,7 @@ proto.update = function(data) { } } else if(options.errorBounds) { this.errorBars = createErrorBars(errorOptions); + this.errorBars._trace = this; this.scene.glplot.add(this.errorBars); } @@ -419,6 +423,7 @@ proto.update = function(data) { } else { delaunayOptions.gl = gl; this.delaunayMesh = createMesh(delaunayOptions); + this.delaunayMesh._trace = this; this.scene.glplot.add(this.delaunayMesh); } } else if(this.delaunayMesh) { diff --git a/src/traces/surface/convert.js b/src/traces/surface/convert.js index 839c0b8b700..1a352a9df6e 100644 --- a/src/traces/surface/convert.js +++ b/src/traces/surface/convert.js @@ -368,6 +368,7 @@ function createSurfaceTrace(scene, data) { var gl = scene.glplot.gl; var surface = createSurface({ gl: gl }); var result = new SurfaceTrace(scene, surface, data.uid); + surface._trace = result; result.update(data); scene.glplot.add(surface); return result; diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 153ca03a35a..16f93722f1f 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -217,23 +217,64 @@ describe('Test gl3d plots', function() { it('should be able to toggle visibility', function(done) { var _mock = Lib.extendDeep({}, mock2); - var objects; + _mock.data[0].x = [0, 1, 3]; + _mock.data[0].y = [0, 1, 2]; + _mock.data.push({ + type: 'surface', + z: [[1, 2, 3], [1, 2, 3], [2, 1, 2]] + }, { + type: 'mesh3d', + x: [0, 1, 2, 0], y: [0, 0, 1, 2], z: [0, 2, 0, 1], + i: [0, 0, 0, 1], j: [1, 2, 3, 2], k: [2, 3, 1, 3] + }); + + // scatter3d traces are made of 5 gl-vis objects, + // surface and mesh3d are made of 1 gl-vis object each. + var order0 = [0, 0, 0, 0, 0, 1, 2]; + + function assertObjects(expected) { + var objects = gd._fullLayout.scene._scene.glplot.objects; + var actual = objects.map(function(o) { + return o._trace.data.index; + }); + + expect(actual).toEqual(expected); + } Plotly.plot(gd, _mock) .then(delay) .then(function() { - objects = gd._fullLayout.scene._scene.glplot.objects; - expect(objects.length).toEqual(5); + assertObjects(order0); return Plotly.restyle(gd, 'visible', 'legendonly'); }) .then(function() { - expect(objects.length).toEqual(0); + assertObjects([]); return Plotly.restyle(gd, 'visible', true); }) .then(function() { - expect(objects.length).toEqual(5); + assertObjects(order0); + + return Plotly.restyle(gd, 'visible', false, [0]); + }) + .then(function() { + assertObjects([1, 2]); + + return Plotly.restyle(gd, 'visible', true, [0]); + }) + .then(function() { + assertObjects(order0); + + return Plotly.restyle(gd, 'visible', 'legendonly', [1]); + }) + .then(function() { + assertObjects([0, 0, 0, 0, 0, 2]); + + return Plotly.restyle(gd, 'visible', true, [1]); + }) + .then(function() { + assertObjects(order0); }) .then(done); }); From 5b9cd2a46153edc993a111db930e7f5df5e5237f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 13 Mar 2017 18:06:21 -0400 Subject: [PATCH 4/5] fix hover / click wrapper promise delay --- test/jasmine/tests/gl_plot_interact_test.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 16f93722f1f..4e17b5b2e80 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -83,7 +83,7 @@ describe('Test gl3d plots', function() { function _hover() { mouseEventScatter3d('mouseover'); - return delay; + return delay(); } Plotly.plot(gd, _mock) @@ -94,7 +94,6 @@ describe('Test gl3d plots', function() { }); }) .then(_hover) - .then(delay) .then(function() { assertHoverText('x: 140.72', 'y: −96.97', 'z: −96.97'); assertEventData('140.72', '−96.97', '−96.97', 0, 2); @@ -143,7 +142,7 @@ describe('Test gl3d plots', function() { // with button 1 pressed function _click() { mouseEventScatter3d('mouseover', {buttons: 1}); - return delay; + return delay(); } Plotly.plot(gd, _mock) @@ -154,7 +153,6 @@ describe('Test gl3d plots', function() { }); }) .then(_click) - .then(delay) .then(function() { assertEventData('140.72', '−96.97', '−96.97', 0, 2); }) From 27e2a0c9181a0deb064cc3fecd1b83584ece7faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 13 Mar 2017 18:38:45 -0400 Subject: [PATCH 5/5] bring back additional delay() after first hover/click to pass on CI --- test/jasmine/tests/gl_plot_interact_test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 4e17b5b2e80..0e042857d70 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -94,6 +94,7 @@ describe('Test gl3d plots', function() { }); }) .then(_hover) + .then(delay) .then(function() { assertHoverText('x: 140.72', 'y: −96.97', 'z: −96.97'); assertEventData('140.72', '−96.97', '−96.97', 0, 2); @@ -153,6 +154,7 @@ describe('Test gl3d plots', function() { }); }) .then(_click) + .then(delay) .then(function() { assertEventData('140.72', '−96.97', '−96.97', 0, 2); })