From 089b335d559cb03194c4f18e077186fc983065fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Thu, 11 Feb 2016 15:00:38 -0500 Subject: [PATCH 01/21] factor out isSelectable routine --- src/components/modebar/manage.js | 101 +++++++++++++++++-------------- 1 file changed, 56 insertions(+), 45 deletions(-) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 0eacce6497a..9b64bfac938 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -71,10 +71,15 @@ module.exports = function manageModeBar(gd) { // logic behind which buttons are displayed by default function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { var fullLayout = gd._fullLayout, - fullData = gd._fullData, - groups = [], - i, - trace; + fullData = gd._fullData; + + var hasCartesian = fullLayout._hasCartesian, + hasGL3D = fullLayout._hasGL3D, + hasGeo = fullLayout._hasGeo, + hasPie = fullLayout._hasPie, + hasGL2D = fullLayout._hasGL2D; + + var groups = []; function addGroup(newGroup) { var out = []; @@ -88,52 +93,42 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { groups.push(out); } + function appendButtonsToAdd(groups) { + if(buttonsToAdd.length) { + if(Array.isArray(buttonsToAdd[0])) { + for(var i = 0; i < buttonsToAdd.length; i++) { + groups.push(buttonsToAdd[i]); + } + } + else groups.push(buttonsToAdd); + } + + return groups; + } + // buttons common to all plot types addGroup(['toImage', 'sendDataToCloud']); - if(fullLayout._hasGL3D) { + if(hasGL3D) { addGroup(['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation']); addGroup(['resetCameraDefault3d', 'resetCameraLastSave3d']); addGroup(['hoverClosest3d']); } - if(fullLayout._hasGeo) { + if(hasGeo) { addGroup(['zoomInGeo', 'zoomOutGeo', 'resetGeo']); addGroup(['hoverClosestGeo']); } - var hasCartesian = fullLayout._hasCartesian, - hasGL2D = fullLayout._hasGL2D, - allAxesFixed = areAllAxesFixed(fullLayout), + var allAxesFixed = areAllAxesFixed(fullLayout), dragModeGroup = []; if((hasCartesian || hasGL2D) && !allAxesFixed) { dragModeGroup = ['zoom2d', 'pan2d']; } - if(hasCartesian) { - // look for traces that support selection - // to be updated as we add more selectPoints handlers - var selectable = false; - for(i = 0; i < fullData.length; i++) { - if(selectable) break; - trace = fullData[i]; - if(!trace._module || !trace._module.selectPoints) continue; - - if(trace.type === 'scatter') { - if(scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) { - selectable = true; - } - } - // assume that in general if the trace module has selectPoints, - // then it's selectable. Scatter is an exception to this because it must - // have markers or text, not just be a scatter type. - else selectable = true; - } - - if(selectable) { - dragModeGroup.push('select2d'); - dragModeGroup.push('lasso2d'); - } + if(hasCartesian && isSelectable(fullData)) { + dragModeGroup.push('select2d'); + dragModeGroup.push('lasso2d'); } if(dragModeGroup.length) addGroup(dragModeGroup); @@ -147,21 +142,11 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { if(hasGL2D) { addGroup(['hoverClosestGl2d']); } - if(fullLayout._hasPie) { + if(hasPie) { addGroup(['hoverClosestPie']); } - // append buttonsToAdd to the groups - if(buttonsToAdd.length) { - if(Array.isArray(buttonsToAdd[0])) { - for(i = 0; i < buttonsToAdd.length; i++) { - groups.push(buttonsToAdd[i]); - } - } - else groups.push(buttonsToAdd); - } - - return groups; + return appendButtonsToAdd(groups); } function areAllAxesFixed(fullLayout) { @@ -178,6 +163,32 @@ function areAllAxesFixed(fullLayout) { return allFixed; } +// look for traces that support selection +// to be updated as we add more selectPoints handlers +function isSelectable(fullData) { + var selectable = false; + + for(var i = 0; i < fullData.length; i++) { + if(selectable) break; + + var trace = fullData[i]; + + if(!trace._module || !trace._module.selectPoints) continue; + + if(trace.type === 'scatter') { + if(scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) { + selectable = true; + } + } + // assume that in general if the trace module has selectPoints, + // then it's selectable. Scatter is an exception to this because it must + // have markers or text, not just be a scatter type. + else selectable = true; + } + + return selectable; +} + // fill in custom buttons referring to default mode bar buttons function fillCustomButton(customButtons) { for(var i = 0; i < customButtons.length; i++) { From fcdf05a136285e45294f07798a738c36ebe45518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Thu, 11 Feb 2016 17:03:59 -0500 Subject: [PATCH 02/21] generalize is-active modebar update, - using nested property to dive into fullLayout --- src/components/modebar/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 3a536829c40..9c446686bee 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -9,9 +9,10 @@ 'use strict'; -var Plotly = require('../../plotly'); var d3 = require('d3'); +var Plotly = require('../../plotly'); +var Lib = require('../../lib'); var Icons = require('../../../build/ploticon'); @@ -204,7 +205,11 @@ proto.updateActiveButton = function(buttonClicked) { } } else { - button3.classed('active', fullLayout[dataAttr]===thisval); + var val = (dataAttr === null) ? + dataAttr : + Lib.nestedProperty(fullLayout, dataAttr).get(); + + button3.classed('active', val === thisval); } }); @@ -260,7 +265,7 @@ proto.removeAllButtons = function() { }; proto.destroy = function() { - Plotly.Lib.removeElement(this.container.querySelector('.modebar')); + Lib.removeElement(this.container.querySelector('.modebar')); }; function createModeBar(gd, buttons) { From 91ce34f2155db59790933917b839973994e8ccc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Thu, 11 Feb 2016 17:21:20 -0500 Subject: [PATCH 03/21] make dragmode and hovermode attribute of each scenes: - Use layout.dragmode and layout.hovermode as default for layout.scene?.dragmode and layout.scene?.hovermode if plot has only GL3D (backward compat) and if valid --- src/plots/cartesian/graph_interact.js | 3 +- src/plots/gl3d/layout/defaults.js | 32 ++++++++++++++++++---- src/plots/gl3d/layout/layout_attributes.js | 19 +++++++++++++ 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 6a8327ced22..9d8e5cacbfb 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -26,6 +26,7 @@ fx.layoutAttributes = { valType: 'enumerated', role: 'info', values: ['zoom', 'pan', 'select', 'lasso', 'orbit', 'turntable'], + dflt: 'zoom', description: [ 'Determines the mode of drag interactions.', '*select* and *lasso* apply only to scatter traces with', @@ -50,7 +51,7 @@ fx.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { attr, dflt); } - coerce('dragmode', layoutOut._hasGL3D ? 'turntable' : 'zoom'); + coerce('dragmode'); if(layoutOut._hasCartesian) { // flag for 'horizontal' plots: diff --git a/src/plots/gl3d/layout/defaults.js b/src/plots/gl3d/layout/defaults.js index 257ce63ef3f..7a1fa4d6261 100644 --- a/src/plots/gl3d/layout/defaults.js +++ b/src/plots/gl3d/layout/defaults.js @@ -29,8 +29,25 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { return Lib.coerce(sceneLayoutIn, sceneLayoutOut, layoutAttributes, attr, dflt); } + // some layout-wide attribute are used in all scenes + // if 3D is the only visible plot type + function getDfltFromLayout(attr) { + var isOnlyGL3D = !( + layoutOut._hasCartesian || + layoutOut._hasGeo || + layoutOut._hasGL2D || + layoutOut._hasPie + ); + + var isValid = layoutAttributes[attr].values.indexOf(layoutIn[attr]) !== -1; + + var dflt; + if(isOnlyGL3D && isValid) return layoutIn[attr]; + } + for(var i = 0; i < scenesLength; i++) { var scene = scenes[i]; + /* * Scene numbering proceeds as follows * scene @@ -85,18 +102,21 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { if(aspectMode === 'manual') sceneLayoutOut.aspectmode = 'auto'; } - /* - * scene arrangements need to be implemented: For now just splice - * along the horizontal direction. ie. - * x:[0,1] -> x:[0,0.5], x:[0.5,1] -> - * x:[0, 0.333] x:[0.333,0.666] x:[0.666, 1] - */ + /* + * scene arrangements need to be implemented: For now just splice + * along the horizontal direction. ie. + * x:[0,1] -> x:[0,0.5], x:[0.5,1] -> + * x:[0, 0.333] x:[0.333,0.666] x:[0.666, 1] + */ supplyGl3dAxisLayoutDefaults(sceneLayoutIn, sceneLayoutOut, { font: layoutOut.font, scene: scene, data: fullData }); + coerce('dragmode', getDfltFromLayout('dragmode')); + coerce('hovermode', getDfltFromLayout('hovermode')); + layoutOut[scene] = sceneLayoutOut; } }; diff --git a/src/plots/gl3d/layout/layout_attributes.js b/src/plots/gl3d/layout/layout_attributes.js index d433ac99682..d101230306c 100644 --- a/src/plots/gl3d/layout/layout_attributes.js +++ b/src/plots/gl3d/layout/layout_attributes.js @@ -139,6 +139,25 @@ module.exports = { yaxis: gl3dAxisAttrs, zaxis: gl3dAxisAttrs, + dragmode: { + valType: 'enumerated', + role: 'info', + values: ['orbit', 'turntable', 'zoom', 'pan'], + dflt: 'turntable', + description: [ + 'Determines the mode of drag interactions for this scene.' + ].join(' ') + }, + hovermode: { + valType: 'enumerated', + role: 'info', + values: ['closest', false], + dflt: 'closest', + description: [ + 'Determines the mode of hover interactions for this scene.' + ].join(' ') + }, + _deprecated: { cameraposition: { valType: 'info_array', From 74e7e33f6190cc7d3899048590ac36a1df1e3dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Thu, 11 Feb 2016 17:21:35 -0500 Subject: [PATCH 04/21] add fx and gl3d defaults tests --- test/jasmine/tests/fx_test.js | 84 +++++++++++++++++++++++++++ test/jasmine/tests/gl3dlayout_test.js | 76 +++++++++++++++++++----- 2 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 test/jasmine/tests/fx_test.js diff --git a/test/jasmine/tests/fx_test.js b/test/jasmine/tests/fx_test.js new file mode 100644 index 00000000000..b4a27fadc17 --- /dev/null +++ b/test/jasmine/tests/fx_test.js @@ -0,0 +1,84 @@ +var Fx = require('@src/plots/cartesian/graph_interact'); + + +describe('Test FX', function() { + 'use strict'; + + describe('defaults', function() { + + it('should default (blank version)', function() { + var layoutIn = {}; + var layoutOut = {}; + var fullData = [{}]; + + Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.hovermode).toBe('closest', 'hovermode to closest'); + expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); + }); + + it('should default (cartesian version)', function() { + var layoutIn = {}; + var layoutOut = { + _hasCartesian: true + }; + var fullData = [{}]; + + Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.hovermode).toBe('x', 'hovermode to x'); + expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); + expect(layoutOut._isHoriz).toBe(false, 'isHoriz to false'); + }); + + it('should default (cartesian horizontal version)', function() { + var layoutIn = {}; + var layoutOut = { + _hasCartesian: true + }; + var fullData = [{ + orientation: 'h' + }]; + + Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.hovermode).toBe('y', 'hovermode to y'); + expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); + expect(layoutOut._isHoriz).toBe(true, 'isHoriz to true'); + }); + + it('should default (gl3d version)', function() { + var layoutIn = {}; + var layoutOut = { + _hasGL3D: true + }; + var fullData = [{}]; + + Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.hovermode).toBe('closest', 'hovermode to closest'); + expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); + }); + + it('should default (geo version)', function() { + var layoutIn = {}; + var layoutOut = { + _hasGeo: true + }; + var fullData = [{}]; + + Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.hovermode).toBe('closest', 'hovermode to closest'); + expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); + }); + + it('should default (multi plot type version)', function() { + var layoutIn = {}; + var layoutOut = { + _hasCartesian: true, + _hasGL3D: true + }; + var fullData = [{}]; + + Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.hovermode).toBe('x', 'hovermode to x'); + expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); + }); + }); +}); diff --git a/test/jasmine/tests/gl3dlayout_test.js b/test/jasmine/tests/gl3dlayout_test.js index 606a9c6e703..86c32aa3d5a 100644 --- a/test/jasmine/tests/gl3dlayout_test.js +++ b/test/jasmine/tests/gl3dlayout_test.js @@ -1,19 +1,16 @@ var Gl3d = require('@src/plots/gl3d'); -describe('Test Gl3d layout defaults', function() { +fdescribe('Test Gl3d layout defaults', function() { 'use strict'; describe('supplyLayoutDefaults', function() { - var layoutIn, - layoutOut; - var supplyLayoutDefaults = Gl3d.supplyLayoutDefaults; + var layoutIn, layoutOut, fullData; beforeEach(function() { - layoutOut = { - _hasGL3D: true - }; + layoutOut = {_hasGL3D: true}; + fullData = [{scene: 'scene', type: 'scatter3d'}]; }); it('should coerce aspectmode=ratio when ratio data is valid', function() { @@ -39,7 +36,7 @@ describe('Test Gl3d layout defaults', function() { } }; - supplyLayoutDefaults(layoutIn, layoutOut, [{scene: 'scene', type: 'scatter3d'}]); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); expect(layoutOut.scene.bgcolor).toBe(expected.scene.bgcolor); @@ -68,7 +65,7 @@ describe('Test Gl3d layout defaults', function() { } }; - supplyLayoutDefaults(layoutIn, layoutOut, [{scene: 'scene', type: 'scatter3d'}]); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); }); @@ -96,7 +93,7 @@ describe('Test Gl3d layout defaults', function() { } }; - supplyLayoutDefaults(layoutIn, layoutOut, [{scene: 'scene', type: 'scatter3d'}]); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); }); @@ -124,7 +121,7 @@ describe('Test Gl3d layout defaults', function() { } }; - supplyLayoutDefaults(layoutIn, layoutOut, [{scene: 'scene', type: 'scatter3d'}]); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); }); @@ -152,12 +149,65 @@ describe('Test Gl3d layout defaults', function() { } }; - supplyLayoutDefaults(layoutIn, layoutOut, [{scene: 'scene', type: 'scatter3d'}]); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.scene.aspectmode).toBe(expected.scene.aspectmode); expect(layoutOut.scene.aspectratio).toEqual(expected.scene.aspectratio); }); + it('should coerce dragmode', function() { + layoutIn = {}; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.dragmode) + .toBe('turntable', 'to turntable by default'); + + layoutIn = { scene: { dragmode: 'orbit' } }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.dragmode) + .toBe('orbit', 'to user val if valid'); + + layoutIn = { dragmode: 'orbit' }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.dragmode) + .toBe('orbit', 'to user layout val if valid and 3d only'); + + layoutIn = { dragmode: 'orbit' }; + layoutOut._hasCartesian = true; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.dragmode) + .toBe('turntable', 'to default if not 3d only'); + + layoutIn = { dragmode: 'not gonna work' }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.dragmode) + .toBe('turntable', 'to default if not valid'); + }); - + it('should coerce hovermode', function() { + layoutIn = {}; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.hovermode) + .toBe('closest', 'to closest by default'); + + layoutIn = { scene: { hovermode: false } }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.hovermode) + .toBe(false, 'to user val if valid'); + + layoutIn = { hovermode: false }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.hovermode) + .toBe(false, 'to user layout val if valid and 3d only'); + + layoutIn = { hovermode: false }; + layoutOut._hasCartesian = true; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.hovermode) + .toBe('closest', 'to default if not 3d only'); + + layoutIn = { hovermode: 'not gonna work' }; + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.hovermode) + .toBe('closest', 'to default if not valid'); + }); }); }); From 760bda35b059dce8328d27826448702a6f4da73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Thu, 11 Feb 2016 17:22:38 -0500 Subject: [PATCH 05/21] don't show 3d hover labels when scene.hovermode is false --- src/plots/gl3d/scene.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index db742d5bb0b..5a58f4982ef 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -79,18 +79,20 @@ function render(scene) { if(hoverinfoParts.indexOf('name') === -1) lastPicked.name = undefined; } - 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]), - text: selection.textLabel, - name: lastPicked.name, - color: lastPicked.color - }, { - container: svgContainer - }); + if(scene.fullSceneLayout.hovermode) { + 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]), + text: selection.textLabel, + name: lastPicked.name, + color: lastPicked.color + }, { + container: svgContainer + }); + } } else Fx.loneUnhover(svgContainer); } From 1180a8f9a52934215fc4e7bac34d8daa45d72253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Tue, 16 Feb 2016 13:26:04 -0500 Subject: [PATCH 06/21] make 3d drag modebar button update scene.dragmode, instead of layout.dragmode --- src/components/modebar/buttons.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 2d93bc83365..0114bd7b743 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -267,7 +267,7 @@ function handleCartesian(gd, ev) { modeBarButtons.zoom3d = { name: 'zoom3d', title: 'Zoom', - attr: 'dragmode', + attr: 'scene.dragmode', val: 'zoom', icon: Icons.zoombox, click: handleDrag3d @@ -276,7 +276,7 @@ modeBarButtons.zoom3d = { modeBarButtons.pan3d = { name: 'pan3d', title: 'Pan', - attr: 'dragmode', + attr: 'scene.dragmode', val: 'pan', icon: Icons.pan, click: handleDrag3d @@ -285,7 +285,7 @@ modeBarButtons.pan3d = { modeBarButtons.orbitRotation = { name: 'orbitRotation', title: 'orbital rotation', - attr: 'dragmode', + attr: 'scene.dragmode', val: 'orbit', icon: Icons['3d_rotate'], click: handleDrag3d @@ -294,7 +294,7 @@ modeBarButtons.orbitRotation = { modeBarButtons.tableRotation = { name: 'tableRotation', title: 'turntable rotation', - attr: 'dragmode', + attr: 'scene.dragmode', val: 'turntable', icon: Icons['z-axis'], click: handleDrag3d @@ -304,14 +304,16 @@ function handleDrag3d(gd, ev) { var button = ev.currentTarget, attr = button.getAttribute('data-attr'), val = button.getAttribute('data-val') || true, + fullLayout = gd._fullLayout, + sceneIds = Plotly.Plots.getSubplotIds(fullLayout, 'gl3d'), layoutUpdate = {}; - layoutUpdate[attr] = val; + var parts = attr.split('.'); + + for(var i = 0; i < sceneIds.length; i++) { + layoutUpdate[sceneIds[i] + '.' + parts[1]] = val; + } - /* - * Dragmode will go through the relayout -> doplot -> scene.plot() - * routine where the dragmode will be set in scene.plot() - */ Plotly.relayout(gd, layoutUpdate); } From 9010006748a78ad163ee13d72a7b8b755a8447a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Tue, 16 Feb 2016 13:27:56 -0500 Subject: [PATCH 07/21] make camera modebar button update camera via fullLayou.scene?, - which is more robust then via layout (in case where users partly supply the camera object. --- src/components/modebar/buttons.js | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 0114bd7b743..6de2fb3c8d9 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -336,29 +336,19 @@ modeBarButtons.resetCameraLastSave3d = { function handleCamera3d(gd, ev) { var button = ev.currentTarget, attr = button.getAttribute('data-attr'), - layout = gd.layout, fullLayout = gd._fullLayout, sceneIds = Plotly.Plots.getSubplotIds(fullLayout, 'gl3d'); for(var i = 0; i < sceneIds.length; i++) { var sceneId = sceneIds[i], - sceneLayout = layout[sceneId], fullSceneLayout = fullLayout[sceneId], scene = fullSceneLayout._scene; - if(!sceneLayout || attr==='resetDefault') scene.setCameraToDefault(); + if(attr === 'resetDefault') scene.setCameraToDefault(); else if(attr === 'resetLastSave') { - - var cameraPos = sceneLayout.camera; - if(cameraPos) scene.setCamera(cameraPos); - else scene.setCameraToDefault(); + scene.setCamera(fullSceneLayout.camera); } } - - /* - * TODO have a sceneLastTouched in _fullLayout to only - * update the camera of the scene last touched by the user - */ } modeBarButtons.hoverClosest3d = { From 11189aeb0fb0fc6e9f5e1a867508289b7d519c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Tue, 16 Feb 2016 13:29:51 -0500 Subject: [PATCH 08/21] make hover button extend current scene layout, - the previous version overrode the current scene layout --- src/components/modebar/buttons.js | 86 +++++++++++++++++-------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 6de2fb3c8d9..a6a09189825 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -359,50 +359,58 @@ modeBarButtons.hoverClosest3d = { toggle: true, icon: Icons.tooltip_basic, gravity: 'ne', - click: function(gd, ev) { - var button = ev.currentTarget, - val = JSON.parse(button.getAttribute('data-val')) || false, - fullLayout = gd._fullLayout, - sceneIds = Plotly.Plots.getSubplotIds(fullLayout, 'gl3d'); - - var axes = ['xaxis', 'yaxis', 'zaxis'], - spikeAttrs = ['showspikes', 'spikesides', 'spikethickness', 'spikecolor']; - - // initialize 'current spike' object to be stored in the DOM - var currentSpikes = {}, - axisSpikes = {}, - layoutUpdate = {}; - - if(val) { - layoutUpdate = val; - button.setAttribute('data-val', JSON.stringify(null)); - } - else { - layoutUpdate = {'allaxes.showspikes': false}; - - for(var i = 0; i < sceneIds.length; i++) { - var sceneId = sceneIds[i], - sceneLayout = fullLayout[sceneId], - sceneSpikes = currentSpikes[sceneId] = {}; - - // copy all the current spike attrs - for(var j = 0; j < 3; j++) { - var axis = axes[j]; - axisSpikes = sceneSpikes[axis] = {}; - - for(var k = 0; k < spikeAttrs.length; k++) { - var spikeAttr = spikeAttrs[k]; - axisSpikes[spikeAttr] = sceneLayout[axis][spikeAttr]; - } + click: handleHover3d +}; + +function handleHover3d(gd, ev) { + var button = ev.currentTarget, + val = JSON.parse(button.getAttribute('data-val')) || false, + layout = gd.layout, + fullLayout = gd._fullLayout, + sceneIds = Plotly.Plots.getSubplotIds(fullLayout, 'gl3d'); + + var axes = ['xaxis', 'yaxis', 'zaxis'], + spikeAttrs = ['showspikes', 'spikesides', 'spikethickness', 'spikecolor']; + + // initialize 'current spike' object to be stored in the DOM + var currentSpikes = {}, + axisSpikes = {}, + layoutUpdate = {}; + + if(val) { + layoutUpdate = Lib.extendDeep(layout, val); + button.setAttribute('data-val', JSON.stringify(null)); + } + else { + layoutUpdate = { + 'allaxes.showspikes': false + }; + + for(var i = 0; i < sceneIds.length; i++) { + var sceneId = sceneIds[i], + sceneLayout = fullLayout[sceneId], + sceneSpikes = currentSpikes[sceneId] = {}; + + sceneSpikes.hovermode = sceneLayout.hovermode; + layoutUpdate[sceneId + '.hovermode'] = false; + + // copy all the current spike attrs + for(var j = 0; j < 3; j++) { + var axis = axes[j]; + axisSpikes = sceneSpikes[axis] = {}; + + for(var k = 0; k < spikeAttrs.length; k++) { + var spikeAttr = spikeAttrs[k]; + axisSpikes[spikeAttr] = sceneLayout[axis][spikeAttr]; } } - - button.setAttribute('data-val', JSON.stringify(currentSpikes)); } - Plotly.relayout(gd, layoutUpdate); + button.setAttribute('data-val', JSON.stringify(currentSpikes)); } -}; + + Plotly.relayout(gd, layoutUpdate); +} modeBarButtons.zoomInGeo = { name: 'zoomInGeo', From 524ce133040dde2c485eff7bcf47b0d5af8acdc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Tue, 16 Feb 2016 13:35:55 -0500 Subject: [PATCH 09/21] use camera spec of own scene to init the camera, - this fixes an to this point undiscovered bug, where the camera position of non-first scenes in multi-scene wasn't set properly. --- src/plots/gl3d/scene.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 5a58f4982ef..894ec16d6ae 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -149,7 +149,7 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) { } if(!scene.camera) { - var cameraData = fullLayout.scene.camera; + var cameraData = scene.fullSceneLayout.camera; scene.camera = createCamera(scene.container, { center: [cameraData.center.x, cameraData.center.y, cameraData.center.z], eye: [cameraData.eye.x, cameraData.eye.y, cameraData.eye.z], @@ -167,7 +167,6 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) { scene.recoverContext(); }; - scene.glplot.onrender = render.bind(null, scene); //List of scene objects @@ -203,6 +202,7 @@ function Scene(options, fullLayout) { this.fullLayout = fullLayout; this.id = options.id || 'scene'; + this.fullSceneLayout = fullLayout[this.id]; //Saved from last call to plot() this.plotArgs = [ [], {}, {} ]; From 9354fb3d17e995919022c6cc1b7d6704d69e5080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Tue, 16 Feb 2016 13:40:53 -0500 Subject: [PATCH 10/21] rename scene.proto.handleDragmode --> scene.proto.updateFx - to be consistent with scene2d - note that updateFx is called w/o passing through scene.proto.plot for Plotly.relayout when 'layout.dragmode' and/or 'layout.hovermode' are updated --- src/plot_api/plot_api.js | 4 ++-- src/plots/gl3d/scene.js | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 2a2bb78e64b..15567dc1cd9 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2275,7 +2275,7 @@ Plotly.relayout = function relayout(gd, astr, val) { * hovermode and dragmode don't need any redrawing, since they just * affect reaction to user input. everything else, assume full replot. * height, width, autosize get dealt with below. Except for the case of - * of subplots - scenes - which require scene.handleDragmode to be called. + * of subplots - scenes - which require scene.updateFx to be called. */ else if(['hovermode', 'dragmode'].indexOf(ai) !== -1) domodebar = true; else if(['hovermode','dragmode','height', @@ -2343,7 +2343,7 @@ Plotly.relayout = function relayout(gd, astr, val) { subplotIds = Plots.getSubplotIds(fullLayout, 'gl3d'); for(i = 0; i < subplotIds.length; i++) { scene = fullLayout[subplotIds[i]]._scene; - scene.handleDragmode(fullLayout.dragmode); + scene.updateFx(fullLayout.dragmode, fullLayout.hovermode); } subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d'); diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 894ec16d6ae..daa90d1b09d 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -300,7 +300,7 @@ proto.plot = function(sceneData, fullLayout, layout) { this.spikeOptions.merge(fullSceneLayout); // Update camera mode - this.handleDragmode(fullLayout.dragmode); + this.updateFx(fullSceneLayout.dragmode, fullSceneLayout.hovermode); //Update scene this.glplot.update({}); @@ -586,16 +586,16 @@ proto.saveCamera = function saveCamera(layout) { return hasChanged; }; -proto.handleDragmode = function(dragmode) { - +proto.updateFx = function(dragmode, hovermode) { var camera = this.camera; - if (camera) { + + if(camera) { // rotate and orbital are synonymous - if (dragmode === 'orbit') { + if(dragmode === 'orbit') { camera.mode = 'orbit'; camera.keyBindingMode = 'rotate'; - } else if (dragmode === 'turntable') { + } else if(dragmode === 'turntable') { camera.up = [0, 0, 1]; camera.mode = 'turntable'; camera.keyBindingMode = 'rotate'; @@ -606,6 +606,9 @@ proto.handleDragmode = function(dragmode) { camera.keyBindingMode = dragmode; } } + + // to put dragmode and hovermode on the same grounds from relayout + this.fullSceneLayout.hovermode = hovermode; }; proto.toImage = function(format) { From 48d5957b87c4a07ac30678a49d304eace6e5ee5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Tue, 16 Feb 2016 13:41:20 -0500 Subject: [PATCH 11/21] lint --- src/components/modebar/index.js | 1 - src/plot_api/plot_api.js | 31 +++++++++++++-------------- src/plots/cartesian/graph_interact.js | 20 ++++++++--------- src/plots/gl3d/layout/defaults.js | 1 - src/plots/gl3d/scene.js | 3 ++- test/jasmine/tests/gl3dlayout_test.js | 2 +- 6 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/components/modebar/index.js b/src/components/modebar/index.js index 9c446686bee..5d86d57f740 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -11,7 +11,6 @@ var d3 = require('d3'); -var Plotly = require('../../plotly'); var Lib = require('../../lib'); var Icons = require('../../../build/ploticon'); diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 15567dc1cd9..4d07ee32f8a 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -598,29 +598,28 @@ function cleanLayout(layout) { * Clean up Scene layouts */ var sceneIds = Plots.getSubplotIds(layout, 'gl3d'); - var scene, cameraposition, rotation, - radius, center, mat, eye; - for (i = 0; i < sceneIds.length; i++) { - scene = layout[sceneIds[i]]; - - /* - * Clean old Camera coords - */ - cameraposition = scene.cameraposition; - if (Array.isArray(cameraposition) && cameraposition[0].length === 4) { - rotation = cameraposition[0]; - center = cameraposition[1]; - radius = cameraposition[2]; - mat = m4FromQuat([], rotation); - eye = []; - for (j = 0; j < 3; ++j) { + for(i = 0; i < sceneIds.length; i++) { + var scene = layout[sceneIds[i]]; + + // clean old Camera coords + var cameraposition = scene.cameraposition; + if(Array.isArray(cameraposition) && cameraposition[0].length === 4) { + var rotation = cameraposition[0], + center = cameraposition[1], + radius = cameraposition[2], + mat = m4FromQuat([], rotation), + eye = []; + + for(j = 0; j < 3; ++j) { eye[j] = center[i] + radius * mat[2 + 4 * j]; } + scene.camera = { eye: {x: eye[0], y: eye[1], z: eye[2]}, center: {x: center[0], y: center[1], z: center[2]}, up: {x: mat[1], y: mat[5], z: mat[9]} }; + delete scene.cameraposition; } } diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 9d8e5cacbfb..1d0f554da53 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -14,6 +14,7 @@ var tinycolor = require('tinycolor2'); var isNumeric = require('fast-isnumeric'); var Plotly = require('../../plotly'); +var Lib = require('../../lib'); var Events = require('../../lib/events'); var prepSelect = require('./select'); @@ -43,20 +44,17 @@ fx.layoutAttributes = { }; fx.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { - var isHoriz, hovermodeDflt; - function coerce(attr, dflt) { - return Plotly.Lib.coerce(layoutIn, layoutOut, - fx.layoutAttributes, - attr, dflt); + return Lib.coerce(layoutIn, layoutOut, fx.layoutAttributes, attr, dflt); } coerce('dragmode'); + var hovermodeDflt; if(layoutOut._hasCartesian) { // flag for 'horizontal' plots: // determines the state of the mode bar 'compare' hovermode button - isHoriz = layoutOut._isHoriz = fx.isHoriz(fullData); + var isHoriz = layoutOut._isHoriz = fx.isHoriz(fullData); hovermodeDflt = isHoriz ? 'y' : 'x'; } else hovermodeDflt = 'closest'; @@ -66,14 +64,16 @@ fx.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { fx.isHoriz = function(fullData) { var isHoriz = true; - var i, trace; - for (i = 0; i < fullData.length; i++) { - trace = fullData[i]; - if (trace.orientation !== 'h') { + + for(var i = 0; i < fullData.length; i++) { + var trace = fullData[i]; + + if(trace.orientation !== 'h') { isHoriz = false; break; } } + return isHoriz; }; diff --git a/src/plots/gl3d/layout/defaults.js b/src/plots/gl3d/layout/defaults.js index 7a1fa4d6261..d31eb42a87b 100644 --- a/src/plots/gl3d/layout/defaults.js +++ b/src/plots/gl3d/layout/defaults.js @@ -41,7 +41,6 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { var isValid = layoutAttributes[attr].values.indexOf(layoutIn[attr]) !== -1; - var dflt; if(isOnlyGL3D && isValid) return layoutIn[attr]; } diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index daa90d1b09d..36eafe78fa4 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -131,7 +131,8 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) { try { scene.glplot = createPlot(glplotOptions); - } catch (e) { + } + catch (e) { /* * createPlot will throw when webgl is not enabled in the client. * Lets return an instance of the module with all functions noop'd. diff --git a/test/jasmine/tests/gl3dlayout_test.js b/test/jasmine/tests/gl3dlayout_test.js index 86c32aa3d5a..bd1d7fb0d80 100644 --- a/test/jasmine/tests/gl3dlayout_test.js +++ b/test/jasmine/tests/gl3dlayout_test.js @@ -1,7 +1,7 @@ var Gl3d = require('@src/plots/gl3d'); -fdescribe('Test Gl3d layout defaults', function() { +describe('Test Gl3d layout defaults', function() { 'use strict'; describe('supplyLayoutDefaults', function() { From bc90474b7f9c3ba2eee4ee24ef574750e025771c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Tue, 16 Feb 2016 13:42:44 -0500 Subject: [PATCH 12/21] add several 3d mode bar jasmine tests, - these will only run locally at the moment as and until https://github.com/plotly/plotly.js/issues/241 is resolved. --- test/jasmine/assets/modebar_button.js | 23 +++ test/jasmine/tests/gl_plot_interact_test.js | 170 ++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 test/jasmine/assets/modebar_button.js diff --git a/test/jasmine/assets/modebar_button.js b/test/jasmine/assets/modebar_button.js new file mode 100644 index 00000000000..dd0ad296ae4 --- /dev/null +++ b/test/jasmine/assets/modebar_button.js @@ -0,0 +1,23 @@ +'use strict'; + +var d3 = require('d3'); + +var modeBarButtons = require('@src/components/modebar/buttons'); + + +module.exports = function selectButton(modeBar, name) { + var button = d3.select(modeBar.element) + .select('[data-title="' + modeBarButtons[name].title + '"]') + .node(); + + button.click = function() { + var ev = new window.MouseEvent('click'); + button.dispatchEvent(ev); + }; + + button.isActive = function() { + return d3.select(button).classed('active'); + }; + + return button; +}; diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index 2f7bf0b04a5..a7ed9547e74 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -1,9 +1,12 @@ var d3 = require('d3'); var Plotly = require('@lib/index'); +var Plots = require('@src/plots/plots'); +var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var selectButton = require('../assets/modebar_button'); /* * WebGL interaction test cases fail on the CircleCI @@ -43,4 +46,171 @@ describe('Test plot structure', function() { }); }); + describe('gl3d modebar click handlers', function() { + var gd, modeBar; + + beforeEach(function(done) { + var mockData = [{ + type: 'scatter3d' + }, { + type: 'surface', scene: 'scene2' + }]; + + var mockLayout = { + 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; + done(); + }); + }); + + function assertScenes(cont, attr, val) { + var sceneIds = Plots.getSubplotIds(cont, 'gl3d'); + + sceneIds.forEach(function(sceneId) { + var thisVal = Lib.nestedProperty(cont[sceneId], attr).get(); + expect(thisVal).toEqual(val); + }); + } + + 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('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('buttons resetCameraDefault3d and resetCameraLastSave3d', function() { + // changes in scene objects are not instantaneous + var DELAY = 1000; + + it('should update the scene camera', function(done) { + var sceneLayout = gd._fullLayout.scene, + sceneLayout2 = gd._fullLayout.scene2, + scene = sceneLayout._scene, + scene2 = sceneLayout2._scene; + + 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(sceneLayout.camera.eye) + .toEqual({x: 0.1, y: 0.1, z: 1}, 'does not change the layout objects'); + expect(scene.camera.eye) + .toEqual([1.2500000000000002, 1.25, 1.25]); + expect(sceneLayout2.camera.eye) + .toEqual({x: 2.5, y: 2.5, z: 2.5}, 'does not change the layout objects'); + expect(scene2.camera.eye) + .toEqual([1.2500000000000002, 1.25, 1.25]); + + selectButton(modeBar, 'resetCameraLastSave3d').click(); + setTimeout(function() { + expect(sceneLayout.camera.eye) + .toEqual({x: 0.1, y: 0.1, z: 1}, 'does not change the layout objects'); + expect(scene.camera.eye) + .toEqual([ 0.10000000000000016, 0.10000000000000016, 1]); + expect(sceneLayout2.camera.eye) + .toEqual({x: 2.5, y: 2.5, z: 2.5}, 'does not change the layout objects'); + expect(scene2.camera.eye) + .toEqual([2.500000000000001, 2.5000000000000004, 2.5000000000000004]); + + done(); + }, DELAY); + }, 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); + }); + }); + + }); }); From 986e8b0028379f9703b58736af4b189cdb77e74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Tue, 16 Feb 2016 16:14:16 -0500 Subject: [PATCH 13/21] make modebar button assest abstraction more robust --- test/jasmine/assets/modebar_button.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/jasmine/assets/modebar_button.js b/test/jasmine/assets/modebar_button.js index dd0ad296ae4..3464cf7b403 100644 --- a/test/jasmine/assets/modebar_button.js +++ b/test/jasmine/assets/modebar_button.js @@ -6,17 +6,19 @@ var modeBarButtons = require('@src/components/modebar/buttons'); module.exports = function selectButton(modeBar, name) { - var button = d3.select(modeBar.element) + var button = {}; + + var node = button.node = d3.select(modeBar.element) .select('[data-title="' + modeBarButtons[name].title + '"]') .node(); button.click = function() { var ev = new window.MouseEvent('click'); - button.dispatchEvent(ev); + node.dispatchEvent(ev); }; button.isActive = function() { - return d3.select(button).classed('active'); + return d3.select(node).classed('active'); }; return button; From fe0d8bbd09fb71ff6aba14d09cd7ce99e22165f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Tue, 16 Feb 2016 17:11:45 -0500 Subject: [PATCH 14/21] add geo.proto.updateFx method: - propatet hovermode update into geo using updateFx - port relayout shortcut from modebar to Plotly.relayout - make toogle hover mode bar using stock Plotly.relayout --- src/components/modebar/buttons.js | 3 +-- src/plot_api/plot_api.js | 8 +++++++- src/plots/geo/geo.js | 11 ++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index a6a09189825..867b3f43e10 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -447,7 +447,7 @@ modeBarButtons.hoverClosestGeo = { toggle: true, icon: Icons.tooltip_basic, gravity: 'ne', - click: handleGeo + click: toggleHover }; function handleGeo(gd, ev) { @@ -468,7 +468,6 @@ function handleGeo(gd, ev) { geo.render(); } else if(attr === 'reset') geo.zoomReset(); - else if(attr === 'hovermode') geo.showHover = !geo.showHover; } } diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 4d07ee32f8a..d37e78635bd 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2336,9 +2336,9 @@ Plotly.relayout = function relayout(gd, astr, val) { // this is decoupled enough it doesn't need async regardless if(domodebar) { + var subplotIds; manageModeBar(gd); - var subplotIds; subplotIds = Plots.getSubplotIds(fullLayout, 'gl3d'); for(i = 0; i < subplotIds.length; i++) { scene = fullLayout[subplotIds[i]]._scene; @@ -2350,6 +2350,12 @@ Plotly.relayout = function relayout(gd, astr, val) { scene = fullLayout._plots[subplotIds[i]]._scene2d; scene.updateFx(fullLayout); } + + subplotIds = Plots.getSubplotIds(fullLayout, 'geo'); + for(i = 0; i < subplotIds.length; i++) { + var geo = fullLayout[subplotIds[i]]._geo; + geo.updateFx(fullLayout.hovermode); + } } } diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index ed0c5a9ac0c..d2bfd2cf912 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -39,7 +39,6 @@ function Geo(options, fullLayout) { // a subset of https://github.com/d3/d3-geo-projection addProjectionsToD3(); - this.showHover = (fullLayout.hovermode === 'closest'); this.hoverContainer = null; this.topojsonName = null; @@ -56,6 +55,7 @@ function Geo(options, fullLayout) { this.zoomReset = null; this.makeFramework(); + this.updateFx(fullLayout.hovermode); } module.exports = Geo; @@ -174,6 +174,15 @@ proto.onceTopojsonIsLoaded = function(geoData, geoLayout) { this.render(); }; +proto.updateFx = function(hovermode) { + this.showHover = (hovermode !== false); + + // TODO should more strict, any layout.hovermode other + // then false will make all geo subplot display hover text. + // Instead each geo should have its own geo.hovermode + // to control hover visibility independently of other subplots. +}; + proto.makeProjection = function(geoLayout) { var projLayout = geoLayout.projection, projType = projLayout.type, From 0c5cf5633cac8aecae0850871e4c05c6c39fb8cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Tue, 16 Feb 2016 17:15:10 -0500 Subject: [PATCH 15/21] add multi-plot-type graphs mode bar button logic: - graphs with more than one plot types get 'union buttons' which reset the view or toggle hover labels across all subplots. --- src/components/modebar/buttons.js | 39 +++++++++++++ src/components/modebar/manage.js | 18 ++++-- test/jasmine/tests/modebar_test.js | 89 +++++++++++++++++++++++++++++- 3 files changed, 140 insertions(+), 6 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 867b3f43e10..21025ce49e5 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -497,3 +497,42 @@ function toggleHover(gd) { Plotly.relayout(gd, 'hovermode', newHover); } + +// buttons when more then one plot types are present + +modeBarButtons.toggleHover = { + name: 'toggleHover', + title: 'Toggle show closest data on hover', + attr: 'hovermode', + val: null, + toggle: true, + icon: Icons.tooltip_basic, + gravity: 'ne', + click: function(gd, ev) { + toggleHover(gd); + + // the 3d hovermode update must come + // last so that layout.hovermode update does not + // override scene?.hovermode?.layout. + handleHover3d(gd, ev); + } +}; + +modeBarButtons.resetViews = { + name: 'resetViews', + title: 'Reset views', + icon: Icons.home, + click: function(gd, ev) { + var button = ev.currentTarget; + + button.setAttribute('data-attr', 'zoom'); + button.setAttribute('data-val', 'reset'); + handleCartesian(gd, ev); + + button.setAttribute('data-attr', 'resetLastSave'); + handleCamera3d(gd, ev); + + // N.B handleCamera3d also triggers a replot for + // geo subplots. + } +}; diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 9b64bfac938..217f218d298 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -109,6 +109,13 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { // buttons common to all plot types addGroup(['toImage', 'sendDataToCloud']); + // graphs with more than one plot types get 'union buttons' + // which reset the view or toggle hover labels across all subplots. + if((hasCartesian || hasGL2D || hasPie) + hasGeo + hasGL3D > 1) { + addGroup(['resetViews', 'toggleHover']); + return appendButtonsToAdd(groups); + } + if(hasGL3D) { addGroup(['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation']); addGroup(['resetCameraDefault3d', 'resetCameraLastSave3d']); @@ -136,13 +143,16 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { addGroup(['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d']); } - if(hasCartesian) { - addGroup(['hoverClosestCartesian', 'hoverCompareCartesian']); + if(hasCartesian && hasPie) { + addGroup(['toggleHover']); } - if(hasGL2D) { + else if(hasGL2D) { addGroup(['hoverClosestGl2d']); } - if(hasPie) { + else if(hasCartesian) { + addGroup(['hoverClosestCartesian', 'hoverCompareCartesian']); + } + else if(hasPie) { addGroup(['hoverClosestPie']); } diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index 06d64351469..4987af40bc1 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -202,9 +202,9 @@ describe('ModeBar', function() { gd._fullLayout._hasCartesian = true; gd._fullLayout.xaxis = {fixedrange: false}; gd._fullData = [{ - type:'scatter', + type: 'scatter', visible: true, - mode:'markers', + mode: 'markers', _module: {selectPoints: true} }]; @@ -295,6 +295,91 @@ describe('ModeBar', function() { checkButtons(modeBar, buttons, 1); }); + it('creates mode bar (cartesian + gl3d version)', function() { + var buttons = getButtons([ + ['toImage', 'sendDataToCloud'], + ['resetViews', 'toggleHover'] + ]); + + var gd = getMockGraphInfo(); + gd._fullLayout._hasCartesian = true; + gd._fullLayout._hasGL3D = true; + gd._fullLayout._hasGeo = false; + gd._fullLayout._hasGL2D = false; + gd._fullLayout._hasPie = false; + + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; + + checkButtons(modeBar, buttons, 1); + }); + + it('creates mode bar (cartesian + geo version)', function() { + var buttons = getButtons([ + ['toImage', 'sendDataToCloud'], + ['resetViews', 'toggleHover'] + ]); + + var gd = getMockGraphInfo(); + gd._fullLayout._hasCartesian = true; + gd._fullLayout._hasGL3D = false; + gd._fullLayout._hasGeo = true; + gd._fullLayout._hasGL2D = false; + gd._fullLayout._hasPie = false; + + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; + + checkButtons(modeBar, buttons, 1); + }); + + it('creates mode bar (cartesian + pie version)', function() { + var buttons = getButtons([ + ['toImage', 'sendDataToCloud'], + ['zoom2d', 'pan2d', 'select2d', 'lasso2d'], + ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'], + ['toggleHover'] + ]); + + var gd = getMockGraphInfo(); + gd._fullLayout._hasCartesian = true; + gd._fullData = [{ + type: 'scatter', + visible: true, + mode: 'markers', + _module: {selectPoints: true} + }]; + gd._fullLayout.xaxis = {fixedrange: false}; + gd._fullLayout._hasGL3D = false; + gd._fullLayout._hasGeo = false; + gd._fullLayout._hasGL2D = false; + gd._fullLayout._hasPie = true; + + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; + + checkButtons(modeBar, buttons, 1); + }); + + it('creates mode bar (gl3d + geo version)', function() { + var buttons = getButtons([ + ['toImage', 'sendDataToCloud'], + ['resetViews', 'toggleHover'] + ]); + + var gd = getMockGraphInfo(); + gd._fullLayout._hasCartesian = false; + gd._fullLayout._hasGL3D = true; + gd._fullLayout._hasGeo = true; + gd._fullLayout._hasGL2D = false; + gd._fullLayout._hasPie = false; + + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; + + checkButtons(modeBar, buttons, 1); + }); + it('throws an error if modeBarButtonsToRemove isn\'t an array', function() { var gd = getMockGraphInfo(); gd._context.modeBarButtonsToRemove = 'not gonna work'; From 266caf0dbe03d335f468ba6188cc1b2fda7510dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Tue, 16 Feb 2016 17:15:58 -0500 Subject: [PATCH 16/21] generalize toggleHover mode bar button handler, so that cartesian plots keep retrieve their 'x' and 'y' hovermode when toggling. --- src/components/modebar/buttons.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 21025ce49e5..bff47961854 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -493,7 +493,15 @@ modeBarButtons.hoverClosestPie = { }; function toggleHover(gd) { - var newHover = gd._fullLayout.hovermode ? false : 'closest'; + var fullLayout = gd._fullLayout; + + var onHoverVal; + if(fullLayout._hasCartesian) { + onHoverVal = fullLayout._isHoriz ? 'y' : 'x'; + } + else onHoverVal = 'closest'; + + var newHover = gd._fullLayout.hovermode ? false : onHoverVal; Plotly.relayout(gd, 'hovermode', newHover); } From 790bc73dfaca9ac5e2a76c2ca8e2bd3b2add2176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Tue, 16 Feb 2016 17:16:37 -0500 Subject: [PATCH 17/21] add cartesian, geo and pie mode bar click handler tests --- test/jasmine/tests/modebar_test.js | 223 +++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index 4987af40bc1..9ac840cd41d 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -3,6 +3,11 @@ var d3 = require('d3'); var createModeBar = require('@src/components/modebar'); var manageModeBar = require('@src/components/modebar/manage'); +var Plotly = require('@lib/index'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var selectButton = require('../assets/modebar_button'); + describe('ModeBar', function() { 'use strict'; @@ -553,4 +558,222 @@ describe('ModeBar', function() { }); + describe('modebar on clicks', function() { + var gd, modeBar; + + afterEach(destroyGraphDiv); + + function assertRange(actual, expected) { + var PRECISION = 4; + expect(actual[0]).toBeCloseTo(expected[0], PRECISION); + expect(actual[1]).toBeCloseTo(expected[1], PRECISION); + } + + function assertActive(buttons, activeButton) { + for(var i = 0; i < buttons.length; i++) { + expect(buttons[i].isActive()).toBe( + buttons[i] === activeButton + ); + } + } + + describe('cartesian handlers', function() { + + beforeEach(function(done) { + var mockData = [{ + type: 'scatter', + y: [2, 1, 2] + }, { + type: 'bar', + y: [2, 1, 2], + xaxis: 'x2', + yaxis: 'y2' + }]; + + var mockLayout = { + xaxis: { + anchor: 'y', + domain: [0, 0.5], + range: [0, 5] + }, + yaxis: { + anchor: 'x', + range: [0, 3] + }, + xaxis2: { + anchor: 'y2', + domain: [0.5, 1], + range: [-1, 4] + }, + yaxis2: { + anchor: 'x2', + range: [0, 4] + } + }; + + gd = createGraphDiv(); + Plotly.plot(gd, mockData, mockLayout).then(function() { + modeBar = gd._fullLayout._modeBar; + done(); + }); + }); + + describe('buttons zoomIn2d, zoomOut2d, autoScale2d and resetScale2d', function() { + it('should update axis ranges', function() { + var buttonZoomIn = selectButton(modeBar, 'zoomIn2d'), + buttonZoomOut = selectButton(modeBar, 'zoomOut2d'), + buttonAutoScale = selectButton(modeBar, 'autoScale2d'), + buttonResetScale = selectButton(modeBar, 'resetScale2d'); + + assertRange(gd._fullLayout.xaxis.range, [0, 5]); + assertRange(gd._fullLayout.yaxis.range, [0, 3]); + assertRange(gd._fullLayout.xaxis2.range, [-1, 4]); + assertRange(gd._fullLayout.yaxis2.range, [0, 4]); + + buttonZoomIn.click(); + assertRange(gd._fullLayout.xaxis.range, [1.25, 3.75]); + assertRange(gd._fullLayout.yaxis.range, [0.75, 2.25]); + assertRange(gd._fullLayout.xaxis2.range, [0.25, 2.75]); + assertRange(gd._fullLayout.yaxis2.range, [1, 3]); + + buttonZoomOut.click(); + assertRange(gd._fullLayout.xaxis.range, [0, 5]); + assertRange(gd._fullLayout.yaxis.range, [0, 3]); + assertRange(gd._fullLayout.xaxis2.range, [-1, 4]); + assertRange(gd._fullLayout.yaxis2.range, [0, 4]); + + buttonZoomIn.click(); + buttonAutoScale.click(); + assertRange(gd._fullLayout.xaxis.range, [-0.1375913, 2.137591]); + assertRange(gd._fullLayout.yaxis.range, [0.92675159, 2.073248]); + assertRange(gd._fullLayout.xaxis2.range, [-0.5, 2.5]); + assertRange(gd._fullLayout.yaxis2.range, [0, 2.105263]); + + buttonResetScale.click(); + assertRange(gd._fullLayout.xaxis.range, [0, 5]); + assertRange(gd._fullLayout.yaxis.range, [0, 3]); + assertRange(gd._fullLayout.xaxis2.range, [-1, 4]); + assertRange(gd._fullLayout.yaxis2.range, [0, 4]); + }); + }); + + describe('buttons zoom2d, pan2d, select2d and lasso2d', function() { + it('should update the layout dragmode', function() { + var zoom2d = selectButton(modeBar, 'zoom2d'), + pan2d = selectButton(modeBar, 'pan2d'), + select2d = selectButton(modeBar, 'select2d'), + lasso2d = selectButton(modeBar, 'lasso2d'), + buttons = [zoom2d, pan2d, select2d, lasso2d]; + + expect(gd._fullLayout.dragmode).toBe('zoom'); + assertActive(buttons, zoom2d); + + pan2d.click(); + expect(gd._fullLayout.dragmode).toBe('pan'); + assertActive(buttons, pan2d); + + select2d.click(); + expect(gd._fullLayout.dragmode).toBe('select'); + assertActive(buttons, select2d); + + lasso2d.click(); + expect(gd._fullLayout.dragmode).toBe('lasso'); + assertActive(buttons, lasso2d); + + zoom2d.click(); + expect(gd._fullLayout.dragmode).toBe('zoom'); + assertActive(buttons, zoom2d); + }); + }); + + describe('buttons hoverCompareCartesian and hoverClosestCartesian ', function() { + it('should update layout hovermode', function() { + var buttonCompare = selectButton(modeBar, 'hoverCompareCartesian'), + buttonClosest = selectButton(modeBar, 'hoverClosestCartesian'), + buttons = [buttonCompare, buttonClosest]; + + expect(gd._fullLayout.hovermode).toBe('x'); + assertActive(buttons, buttonCompare); + + buttonClosest.click(); + expect(gd._fullLayout.hovermode).toBe('closest'); + assertActive(buttons, buttonClosest); + + buttonCompare.click(); + expect(gd._fullLayout.hovermode).toBe('x'); + assertActive(buttons, buttonCompare); + }); + }); + }); + + describe('pie handlers', function() { + + beforeEach(function(done) { + var mockData = [{ + type: 'pie', + labels: ['apples', 'bananas', 'grapes'], + values: [10, 20, 30] + }]; + + gd = createGraphDiv(); + Plotly.plot(gd, mockData).then(function() { + modeBar = gd._fullLayout._modeBar; + done(); + }); + }); + + describe('buttons hoverClosestPie', function() { + it('should update layout hovermode', function() { + var button = selectButton(modeBar, 'hoverClosestPie'); + + expect(gd._fullLayout.hovermode).toBe('closest'); + expect(button.isActive()).toBe(true); + + button.click(); + expect(gd._fullLayout.hovermode).toBe(false); + expect(button.isActive()).toBe(false); + + button.click(); + expect(gd._fullLayout.hovermode).toBe('closest'); + expect(button.isActive()).toBe(true); + }); + }); + }); + + describe('geo handlers', function() { + + beforeEach(function(done) { + var mockData = [{ + type: 'scattergeo', + lon: [10, 20, 30], + lat: [10, 20, 30] + }]; + + gd = createGraphDiv(); + Plotly.plot(gd, mockData).then(function() { + modeBar = gd._fullLayout._modeBar; + done(); + }); + }); + + describe('buttons hoverClosestGeo', function() { + it('should update layout hovermode', function() { + var button = selectButton(modeBar, 'hoverClosestGeo'); + + expect(gd._fullLayout.hovermode).toBe('closest'); + expect(button.isActive()).toBe(true); + + button.click(); + expect(gd._fullLayout.hovermode).toBe(false); + expect(button.isActive()).toBe(false); + + button.click(); + expect(gd._fullLayout.hovermode).toBe('closest'); + expect(button.isActive()).toBe(true); + }); + }); + + }); + + }); }); From 05a1b4fb51a9adb663c4c0bfdd500f82d201a7ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Tue, 16 Feb 2016 17:24:54 -0500 Subject: [PATCH 18/21] bump down precision for circleCI --- test/jasmine/tests/modebar_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index 9ac840cd41d..933aafd28f9 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -564,7 +564,7 @@ describe('ModeBar', function() { afterEach(destroyGraphDiv); function assertRange(actual, expected) { - var PRECISION = 4; + var PRECISION = 2; expect(actual[0]).toBeCloseTo(expected[0], PRECISION); expect(actual[1]).toBeCloseTo(expected[1], PRECISION); } From 4babccbcd4f3fb21587cf50ff9993a966225c7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Wed, 17 Feb 2016 13:20:12 -0500 Subject: [PATCH 19/21] make append buttons to add step more readable --- src/components/modebar/manage.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 217f218d298..65ea0c16ff3 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -93,19 +93,6 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { groups.push(out); } - function appendButtonsToAdd(groups) { - if(buttonsToAdd.length) { - if(Array.isArray(buttonsToAdd[0])) { - for(var i = 0; i < buttonsToAdd.length; i++) { - groups.push(buttonsToAdd[i]); - } - } - else groups.push(buttonsToAdd); - } - - return groups; - } - // buttons common to all plot types addGroup(['toImage', 'sendDataToCloud']); @@ -113,7 +100,7 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { // which reset the view or toggle hover labels across all subplots. if((hasCartesian || hasGL2D || hasPie) + hasGeo + hasGL3D > 1) { addGroup(['resetViews', 'toggleHover']); - return appendButtonsToAdd(groups); + return appendButtonsToGroups(groups, buttonsToAdd); } if(hasGL3D) { @@ -156,7 +143,7 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { addGroup(['hoverClosestPie']); } - return appendButtonsToAdd(groups); + return appendButtonsToGroups(groups, buttonsToAdd); } function areAllAxesFixed(fullLayout) { @@ -199,6 +186,19 @@ function isSelectable(fullData) { return selectable; } +function appendButtonsToGroups(groups, buttons) { + if(buttons.length) { + if(Array.isArray(buttons[0])) { + for(var i = 0; i < buttons.length; i++) { + groups.push(buttons[i]); + } + } + else groups.push(buttons); + } + + return groups; +} + // fill in custom buttons referring to default mode bar buttons function fillCustomButton(customButtons) { for(var i = 0; i < customButtons.length; i++) { From 9f3d14ef3602eaa62892d6d794673935fcf1f0ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Wed, 17 Feb 2016 13:52:54 -0500 Subject: [PATCH 20/21] store previous scene state in button obj, instead of in JSON stringify/parse via the DOM element. --- src/components/modebar/buttons.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index bff47961854..cef24a656eb 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -364,7 +364,7 @@ modeBarButtons.hoverClosest3d = { function handleHover3d(gd, ev) { var button = ev.currentTarget, - val = JSON.parse(button.getAttribute('data-val')) || false, + val = button._previousVal || false, layout = gd.layout, fullLayout = gd._fullLayout, sceneIds = Plotly.Plots.getSubplotIds(fullLayout, 'gl3d'); @@ -379,7 +379,7 @@ function handleHover3d(gd, ev) { if(val) { layoutUpdate = Lib.extendDeep(layout, val); - button.setAttribute('data-val', JSON.stringify(null)); + button._previousVal = null; } else { layoutUpdate = { @@ -406,7 +406,7 @@ function handleHover3d(gd, ev) { } } - button.setAttribute('data-val', JSON.stringify(currentSpikes)); + button._previousVal = Lib.extendDeep({}, currentSpikes); } Plotly.relayout(gd, layoutUpdate); From da5ea2136e88d63821ddcb1c0806ded7ff8c2797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= <etienne@plot.ly> Date: Wed, 17 Feb 2016 13:53:00 -0500 Subject: [PATCH 21/21] :cow2: --- src/plots/gl3d/scene.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 36eafe78fa4..cb3f6a504cb 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -81,8 +81,8 @@ function render(scene) { if(scene.fullSceneLayout.hovermode) { Fx.loneHover({ - x: (0.5 + 0.5 * pdata[0]/pdata[3]) * width, - y: (0.5 - 0.5 * pdata[1]/pdata[3]) * height, + 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]),