diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 2d93bc83365..cef24a656eb 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); } @@ -334,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 = { @@ -367,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 = button._previousVal || 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._previousVal = 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._previousVal = Lib.extendDeep({}, currentSpikes); } -}; + + Plotly.relayout(gd, layoutUpdate); +} modeBarButtons.zoomInGeo = { name: 'zoomInGeo', @@ -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; } } @@ -494,7 +493,54 @@ 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); } + +// 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/index.js b/src/components/modebar/index.js index 3a536829c40..5d86d57f740 100644 --- a/src/components/modebar/index.js +++ b/src/components/modebar/index.js @@ -9,9 +9,9 @@ 'use strict'; -var Plotly = require('../../plotly'); var d3 = require('d3'); +var Lib = require('../../lib'); var Icons = require('../../../build/ploticon'); @@ -204,7 +204,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 +264,7 @@ proto.removeAllButtons = function() { }; proto.destroy = function() { - Plotly.Lib.removeElement(this.container.querySelector('.modebar')); + Lib.removeElement(this.container.querySelector('.modebar')); }; function createModeBar(gd, buttons) { diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 0eacce6497a..65ea0c16ff3 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 = []; @@ -91,49 +96,33 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { // buttons common to all plot types addGroup(['toImage', 'sendDataToCloud']); - if(fullLayout._hasGL3D) { + // 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 appendButtonsToGroups(groups, buttonsToAdd); + } + + 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); @@ -141,27 +130,20 @@ 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(fullLayout._hasPie) { - addGroup(['hoverClosestPie']); + else if(hasCartesian) { + addGroup(['hoverClosestCartesian', 'hoverCompareCartesian']); } - - // 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); + else if(hasPie) { + addGroup(['hoverClosestPie']); } - return groups; + return appendButtonsToGroups(groups, buttonsToAdd); } function areAllAxesFixed(fullLayout) { @@ -178,6 +160,45 @@ 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; +} + +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++) { diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 2a2bb78e64b..d37e78635bd 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; } } @@ -2275,7 +2274,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', @@ -2337,13 +2336,13 @@ 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; - scene.handleDragmode(fullLayout.dragmode); + scene.updateFx(fullLayout.dragmode, fullLayout.hovermode); } subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d'); @@ -2351,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/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 6a8327ced22..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'); @@ -26,6 +27,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', @@ -42,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', layoutOut._hasGL3D ? 'turntable' : 'zoom'); + 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'; @@ -65,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/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, diff --git a/src/plots/gl3d/layout/defaults.js b/src/plots/gl3d/layout/defaults.js index 257ce63ef3f..d31eb42a87b 100644 --- a/src/plots/gl3d/layout/defaults.js +++ b/src/plots/gl3d/layout/defaults.js @@ -29,8 +29,24 @@ 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; + + 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 +101,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', diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index db742d5bb0b..cb3f6a504cb 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); } @@ -129,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. @@ -147,7 +150,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], @@ -165,7 +168,6 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) { scene.recoverContext(); }; - scene.glplot.onrender = render.bind(null, scene); //List of scene objects @@ -201,6 +203,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 = [ [], {}, {} ]; @@ -298,7 +301,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({}); @@ -584,16 +587,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'; @@ -604,6 +607,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) { diff --git a/test/jasmine/assets/modebar_button.js b/test/jasmine/assets/modebar_button.js new file mode 100644 index 00000000000..3464cf7b403 --- /dev/null +++ b/test/jasmine/assets/modebar_button.js @@ -0,0 +1,25 @@ +'use strict'; + +var d3 = require('d3'); + +var modeBarButtons = require('@src/components/modebar/buttons'); + + +module.exports = function selectButton(modeBar, name) { + 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'); + node.dispatchEvent(ev); + }; + + button.isActive = function() { + return d3.select(node).classed('active'); + }; + + return button; +}; 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..bd1d7fb0d80 100644 --- a/test/jasmine/tests/gl3dlayout_test.js +++ b/test/jasmine/tests/gl3dlayout_test.js @@ -5,15 +5,12 @@ describe('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'); + }); }); }); 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); + }); + }); + + }); }); diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index 06d64351469..933aafd28f9 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'; @@ -202,9 +207,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 +300,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'; @@ -468,4 +558,222 @@ describe('ModeBar', function() { }); + describe('modebar on clicks', function() { + var gd, modeBar; + + afterEach(destroyGraphDiv); + + function assertRange(actual, expected) { + var PRECISION = 2; + 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); + }); + }); + + }); + + }); });