diff --git a/devtools/test_dashboard/test_gl3d.js b/devtools/test_dashboard/test_gl3d.js index c4d26b11675..902cb8e0305 100644 --- a/devtools/test_dashboard/test_gl3d.js +++ b/devtools/test_dashboard/test_gl3d.js @@ -24,11 +24,11 @@ plots['projection-traces'] = require('@mocks/gl3d_projection-traces.json'); plots['opacity-scaling-spikes'] = require('@mocks/gl3d_opacity-scaling-spikes.json'); plots['text-weirdness'] = require('@mocks/gl3d_text-weirdness.json'); plots['wire-surface'] = require('@mocks/gl3d_wire-surface.json'); -plots['triangle-mesh3d'] = require('@mocks/gl3d_triangle.json'); +plots['triangle'] = require('@mocks/gl3d_triangle.json'); plots['snowden'] = require('@mocks/gl3d_snowden.json'); plots['bunny'] = require('@mocks/gl3d_bunny.json'); plots['ribbons'] = require('@mocks/gl3d_ribbons.json'); -plots['scatter-time'] = require('@mocks/gl3d_scatter-date.json'); +plots['scatter-date'] = require('@mocks/gl3d_scatter-date.json'); plots['cufflinks'] = require('@mocks/gl3d_cufflinks.json'); plots['chrisp-nan-1'] = require('@mocks/gl3d_chrisp-nan-1.json'); plots['marker-arrays'] = require('@mocks/gl3d_marker-arrays.json'); diff --git a/package.json b/package.json index b6060ebb3a0..fb9e9e298f0 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "gl-mat4": "^1.1.2", "gl-mesh3d": "^1.0.7", "gl-plot2d": "^1.1.6", - "gl-plot3d": "^1.3.0", + "gl-plot3d": "^1.5.0", "gl-scatter2d": "^1.0.5", "gl-scatter2d-fancy": "^1.1.1", "gl-scatter3d": "^1.0.4", diff --git a/src/plots/gl3d/index.js b/src/plots/gl3d/index.js index 5132c3cf995..dd500d98881 100644 --- a/src/plots/gl3d/index.js +++ b/src/plots/gl3d/index.js @@ -51,8 +51,9 @@ exports.plot = function plotGl3d(gd) { // If Scene is not instantiated, create one! if(scene === undefined) { scene = new Scene({ - container: gd.querySelector('.gl-container'), id: sceneId, + graphDiv: gd, + container: gd.querySelector('.gl-container'), staticPlot: gd._context.staticPlot, plotGlPixelRatio: gd._context.plotGlPixelRatio }, diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index cb3f6a504cb..24bca5395f5 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -34,7 +34,7 @@ var STATIC_CANVAS, STATIC_CONTEXT; function render(scene) { - //Update size of svg container + // update size of svg container var svgContainer = scene.svgContainer; var clientRect = scene.container.getBoundingClientRect(); var width = clientRect.width, height = clientRect.height; @@ -45,7 +45,7 @@ function render(scene) { computeTickMarks(scene); scene.glplot.axes.update(scene.axesOptions); - //Check if pick has changed + // check if pick has changed var keys = Object.keys(scene.traces); var lastPicked = null; var selection = scene.glplot.selection; @@ -59,22 +59,28 @@ function render(scene) { } function formatter(axisName, val) { - if(val === undefined) return undefined; if(typeof val === 'string') return val; var axis = scene.fullSceneLayout[axisName]; return Axes.tickText(axis, axis.c2l(val), 'hover').text; } + var oldEventData; + if(lastPicked !== null) { var pdata = project(scene.glplot.cameraParams, selection.dataCoordinate), - hoverinfo = lastPicked.data.hoverinfo; + trace = lastPicked.data, + hoverinfo = trace.hoverinfo; + + var xVal = formatter('xaxis', selection.traceCoordinate[0]), + yVal = formatter('yaxis', selection.traceCoordinate[1]), + zVal = formatter('zaxis', selection.traceCoordinate[2]); if(hoverinfo !== 'all') { var hoverinfoParts = hoverinfo.split('+'); - if(hoverinfoParts.indexOf('x') === -1) selection.traceCoordinate[0] = undefined; - if(hoverinfoParts.indexOf('y') === -1) selection.traceCoordinate[1] = undefined; - if(hoverinfoParts.indexOf('z') === -1) selection.traceCoordinate[2] = undefined; + if(hoverinfoParts.indexOf('x') === -1) xVal = undefined; + if(hoverinfoParts.indexOf('y') === -1) yVal = undefined; + if(hoverinfoParts.indexOf('z') === -1) zVal = undefined; if(hoverinfoParts.indexOf('text') === -1) selection.textLabel = undefined; if(hoverinfoParts.indexOf('name') === -1) lastPicked.name = undefined; } @@ -83,9 +89,9 @@ function render(scene) { Fx.loneHover({ x: (0.5 + 0.5 * pdata[0] / pdata[3]) * width, y: (0.5 - 0.5 * pdata[1] / pdata[3]) * height, - xLabel: formatter('xaxis', selection.traceCoordinate[0]), - yLabel: formatter('yaxis', selection.traceCoordinate[1]), - zLabel: formatter('zaxis', selection.traceCoordinate[2]), + xLabel: xVal, + yLabel: yVal, + zLabel: zVal, text: selection.textLabel, name: lastPicked.name, color: lastPicked.color @@ -93,8 +99,32 @@ function render(scene) { container: svgContainer }); } + + var eventData = { + points: [{ + x: xVal, + y: yVal, + z: zVal, + data: trace._input, + fullData: trace, + curveNumber: trace.index, + pointNumber: selection.data.index + }] + }; + + if(selection.buttons && selection.distance < 5) { + scene.graphDiv.emit('plotly_click', eventData); + } + else { + scene.graphDiv.emit('plotly_hover', eventData); + } + + oldEventData = eventData; + } + else { + Fx.loneUnhover(svgContainer); + scene.graphDiv.emit('plotly_unhover', oldEventData); } - else Fx.loneUnhover(svgContainer); } function initializeGLPlot(scene, fullLayout, canvas, gl) { @@ -110,9 +140,9 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) { autoBounds: false }; - //For static plots, we reuse the WebGL context as WebKit doesn't collect them - //reliably - if (scene.staticMode) { + // for static plots, we reuse the WebGL context + // as WebKit doesn't collect them reliably + if(scene.staticMode) { if(!STATIC_CONTEXT) { STATIC_CANVAS = document.createElement('canvas'); try { @@ -178,11 +208,14 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) { function Scene(options, fullLayout) { - //Create sub container for plot + // create sub container for plot var sceneContainer = document.createElement('div'); var plotContainer = options.container; - //Create SVG container for hover text + // keep a ref to the graph div to fire hover+click events + this.graphDiv = options.graphDiv; + + // create SVG container for hover text var svgContainer = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg'); diff --git a/src/plots/plots.js b/src/plots/plots.js index 3b1f9ad9d0a..575bad28f51 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -476,11 +476,10 @@ plots.supplyDefaults = function(gd) { }; function cleanScenes(newFullLayout, oldFullLayout) { - var oldSceneKey, - oldSceneKeys = plots.getSubplotIds(oldFullLayout, 'gl3d'); + var oldSceneKeys = plots.getSubplotIds(oldFullLayout, 'gl3d'); - for (var i = 0; i < oldSceneKeys.length; i++) { - oldSceneKey = oldSceneKeys[i]; + for(var i = 0; i < oldSceneKeys.length; i++) { + var oldSceneKey = oldSceneKeys[i]; if(!newFullLayout[oldSceneKey] && !!oldFullLayout[oldSceneKey]._scene) { oldFullLayout[oldSceneKey]._scene.destroy(); } diff --git a/test/jasmine/assets/mouse_event.js b/test/jasmine/assets/mouse_event.js index 053e98a8a1b..5c09af0de76 100644 --- a/test/jasmine/assets/mouse_event.js +++ b/test/jasmine/assets/mouse_event.js @@ -1,11 +1,16 @@ -module.exports = function(type, x, y) { - var options = { +module.exports = function(type, x, y, opts) { + var fullOpts = { bubbles: true, clientX: x, clientY: y }; + // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + if(opts && opts.buttons) { + fullOpts.buttons = opts.buttons; + } + var el = document.elementFromPoint(x,y); - var ev = new window.MouseEvent(type, options); + var ev = new window.MouseEvent(type, fullOpts); el.dispatchEvent(ev); }; diff --git a/test/jasmine/karma.conf.js b/test/jasmine/karma.conf.js index 54eed46b6be..c51d51ceee0 100644 --- a/test/jasmine/karma.conf.js +++ b/test/jasmine/karma.conf.js @@ -39,9 +39,7 @@ func.defaultConfig = { testFileGlob ], - // list of files to exclude - exclude: [ - ], + exclude: [], // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 6a2657d2e18..8aa12b16ad1 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -6,6 +6,7 @@ var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var mouseEvent = require('../assets/mouse_event'); var selectButton = require('../assets/modebar_button'); var customMatchers = require('../assets/custom_matchers'); @@ -15,26 +16,131 @@ var customMatchers = require('../assets/custom_matchers'); * */ +var PLOT_DELAY = 200; +var MOUSE_DELAY = 20; +var MODEBAR_DELAY = 500; -describe('Test plot structure', function() { + +describe('Test gl plot interactions', function() { 'use strict'; + var gd; + beforeEach(function() { jasmine.addMatchers(customMatchers); }); - afterEach(destroyGraphDiv); + afterEach(function() { + var fullLayout = gd._fullLayout, + sceneIds; + + sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'); + sceneIds.forEach(function(id) { + fullLayout[id]._scene.destroy(); + }); + + sceneIds = Plots.getSubplotIds(fullLayout, 'gl2d'); + sceneIds.forEach(function(id) { + var scene2d = fullLayout._plots[id]._scene2d; + scene2d.stopped = true; + scene2d.destroy(); + }); + + destroyGraphDiv(); + }); + + function delay(done) { + setTimeout(function() { + done(); + }, PLOT_DELAY); + } describe('gl3d plots', function() { var mock = require('@mocks/gl3d_marker-arrays.json'); + function mouseEventScatter3d(type, opts) { + mouseEvent(type, 605, 271, opts); + } + beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + gd = createGraphDiv(); + Plotly.plot(gd, mock.data, mock.layout).then(function() { + delay(done); + }); }); - it('has one *canvas* node', function() { - var nodes = d3.selectAll('canvas'); - expect(nodes[0].length).toEqual(1); + describe('scatter3d hover', function() { + + var node, ptData; + + beforeEach(function(done) { + gd.on('plotly_hover', function(eventData) { + ptData = eventData.points[0]; + }); + + mouseEventScatter3d('mouseover'); + + setTimeout(function() { + done(); + }, MOUSE_DELAY); + }); + + it('should have', function() { + node = d3.selectAll('canvas'); + expect(node[0].length).toEqual(1, 'one canvas node'); + + node = d3.selectAll('g.hovertext'); + expect(node.size()).toEqual(1, 'one hover text group'); + + node = d3.selectAll('g.hovertext').selectAll('tspan')[0]; + expect(node[0].innerHTML).toEqual('x: 140.72', 'x val on hover'); + expect(node[1].innerHTML).toEqual('y: −96.97', 'y val on hover'); + expect(node[2].innerHTML).toEqual('z: −96.97', 'z val on hover'); + + expect(Object.keys(ptData)).toEqual([ + 'x', 'y', 'z', + 'data', 'fullData', 'curveNumber', 'pointNumber' + ], 'correct hover data fields'); + + expect(ptData.x).toBe('140.72', 'x val hover data'); + expect(ptData.y).toBe('−96.97', 'y val hover data'); + expect(ptData.z).toEqual('−96.97', 'z val hover data'); + expect(ptData.curveNumber).toEqual(0, 'curveNumber hover data'); + expect(ptData.pointNumber).toEqual(2, 'pointNumber hover data'); + }); + + }); + + describe('scatter3d click events', function() { + var ptData; + + beforeEach(function(done) { + gd.on('plotly_click', function(eventData) { + ptData = eventData.points[0]; + }); + + // N.B. gl3d click events are 'mouseover' events + // with button 1 pressed + mouseEventScatter3d('mouseover', {buttons: 1}); + + setTimeout(function() { + done(); + }, MOUSE_DELAY); + }); + + it('should have', function() { + expect(Object.keys(ptData)).toEqual([ + 'x', 'y', 'z', + 'data', 'fullData', 'curveNumber', 'pointNumber' + ], 'correct hover data fields'); + + + 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'); + }); }); }); @@ -42,7 +148,11 @@ describe('Test plot structure', function() { var mock = require('@mocks/gl2d_10.json'); beforeEach(function(done) { - Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + gd = createGraphDiv(); + + Plotly.plot(gd, mock.data, mock.layout).then(function() { + delay(done); + }); }); it('has one *canvas* node', function() { @@ -52,7 +162,7 @@ describe('Test plot structure', function() { }); describe('gl3d modebar click handlers', function() { - var gd, modeBar; + var modeBar; beforeEach(function(done) { var mockData = [{ @@ -69,7 +179,8 @@ describe('Test plot structure', function() { gd = createGraphDiv(); Plotly.plot(gd, mockData, mockLayout).then(function() { modeBar = gd._fullLayout._modeBar; - done(); + + delay(done); }); }); @@ -152,9 +263,6 @@ describe('Test plot structure', function() { }); describe('buttons resetCameraDefault3d and resetCameraLastSave3d', function() { - // changes in scene objects are not instantaneous - var DELAY = 500; - it('should update the scene camera', function(done) { var sceneLayout = gd._fullLayout.scene, sceneLayout2 = gd._fullLayout.scene2, @@ -189,8 +297,8 @@ describe('Test plot structure', function() { .toBeCloseToArray([2.5, 2.5, 2.5], 4); done(); - }, DELAY); - }, DELAY); + }, MODEBAR_DELAY); + }, MODEBAR_DELAY); }); });