diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 30502d91cd2..32a3d0d980e 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -175,9 +175,10 @@ function _hover(gd, evt, subplot, noHoverEvent) { subplots = subplots.concat(overlayedSubplots); } - var len = subplots.length, - xaArray = new Array(len), - yaArray = new Array(len); + var len = subplots.length; + var xaArray = new Array(len); + var yaArray = new Array(len); + var supportsCompare = false; for(var i = 0; i < len; i++) { var spId = subplots[i]; @@ -185,6 +186,7 @@ function _hover(gd, evt, subplot, noHoverEvent) { // 'cartesian' case var plotObj = plots[spId]; if(plotObj) { + supportsCompare = true; // TODO make sure that fullLayout_plots axis refs // get updated properly so that we don't have @@ -203,6 +205,8 @@ function _hover(gd, evt, subplot, noHoverEvent) { var hovermode = evt.hovermode || fullLayout.hovermode; + if(hovermode && !supportsCompare) hovermode = 'closest'; + if(['x', 'y', 'closest'].indexOf(hovermode) === -1 || !gd.calcdata || gd.querySelector('.zoombox') || gd._dragging) { return dragElement.unhoverRaw(gd, evt); diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 1d3e2c643d8..73ebb04db55 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -302,6 +302,10 @@ function handleDrag3d(gd, ev) { layoutUpdate[sceneIds[i] + '.' + parts[1]] = val; } + // for multi-type subplots + var val2d = (val === 'pan') ? val : 'zoom'; + layoutUpdate.dragmode = val2d; + Plotly.relayout(gd, layoutUpdate); } diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 8fb85bfe7db..d455fcf8856 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -82,10 +82,13 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { var hasTernary = fullLayout._has('ternary'); var hasMapbox = fullLayout._has('mapbox'); var hasPolar = fullLayout._has('polar'); + var allAxesFixed = areAllAxesFixed(fullLayout); var groups = []; function addGroup(newGroup) { + if(!newGroup.length) return; + var out = []; for(var i = 0; i < newGroup.length; i++) { @@ -100,55 +103,71 @@ 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 || hasTernary) + hasGeo + hasGL3D > 1) { - addGroup(['resetViews', 'toggleHover']); - return appendButtonsToGroups(groups, buttonsToAdd); - } + var zoomGroup = []; + var hoverGroup = []; + var resetGroup = []; + var dragModeGroup = []; - if(hasGL3D) { - addGroup(['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation']); - addGroup(['resetCameraDefault3d', 'resetCameraLastSave3d']); - addGroup(['hoverClosest3d']); + if((hasCartesian || hasGL2D || hasPie || hasTernary) + hasGeo + hasGL3D + hasMapbox + hasPolar > 1) { + // graphs with more than one plot types get 'union buttons' + // which reset the view or toggle hover labels across all subplots. + hoverGroup = ['toggleHover']; + resetGroup = ['resetViews']; + } + else if(hasGeo) { + zoomGroup = ['zoomInGeo', 'zoomOutGeo']; + hoverGroup = ['hoverClosestGeo']; + resetGroup = ['resetGeo']; + } + else if(hasGL3D) { + hoverGroup = ['hoverClosest3d']; + resetGroup = ['resetCameraDefault3d', 'resetCameraLastSave3d']; + } + else if(hasMapbox) { + hoverGroup = ['toggleHover']; + resetGroup = ['resetViewMapbox']; + } + else if(hasGL2D) { + hoverGroup = ['hoverClosestGl2d']; + } + else if(hasPie) { + hoverGroup = ['hoverClosestPie']; + } + else { // hasPolar, hasTernary + // always show at least one hover icon. + hoverGroup = ['toggleHover']; + } + // if we have cartesian, allow switching between closest and compare + // regardless of what other types are on the plot, since they'll all + // just treat any truthy hovermode as 'closest' + if(hasCartesian) { + hoverGroup = ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian']; } - var allAxesFixed = areAllAxesFixed(fullLayout), - dragModeGroup = []; + if((hasCartesian || hasGL2D) && !allAxesFixed) { + zoomGroup = ['zoomIn2d', 'zoomOut2d', 'autoScale2d']; + if(resetGroup[0] !== 'resetViews') resetGroup = ['resetScale2d']; + } - if(((hasCartesian || hasGL2D) && !allAxesFixed) || hasTernary) { + if(hasGL3D) { + dragModeGroup = ['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation']; + } + else if(((hasCartesian || hasGL2D) && !allAxesFixed) || hasTernary) { dragModeGroup = ['zoom2d', 'pan2d']; } - if(hasMapbox || hasGeo) { + else if(hasMapbox || hasGeo) { dragModeGroup = ['pan2d']; } - if(hasPolar) { + else if(hasPolar) { dragModeGroup = ['zoom2d']; } if(isSelectable(fullData)) { - dragModeGroup.push('select2d'); - dragModeGroup.push('lasso2d'); - } - if(dragModeGroup.length) addGroup(dragModeGroup); - - if((hasCartesian || hasGL2D) && !allAxesFixed && !hasTernary) { - addGroup(['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d']); + dragModeGroup.push('select2d', 'lasso2d'); } - if(hasCartesian && hasPie) { - addGroup(['toggleHover']); - } else if(hasGL2D) { - addGroup(['hoverClosestGl2d']); - } else if(hasCartesian) { - addGroup(['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian']); - } else if(hasPie) { - addGroup(['hoverClosestPie']); - } else if(hasMapbox) { - addGroup(['resetViewMapbox', 'toggleHover']); - } else if(hasGeo) { - addGroup(['zoomInGeo', 'zoomOutGeo', 'resetGeo']); - addGroup(['hoverClosestGeo']); - } + addGroup(dragModeGroup); + addGroup(zoomGroup.concat(resetGroup)); + addGroup(hoverGroup); return appendButtonsToGroups(groups, buttonsToAdd); } diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index cf2b607e824..2b0f2ab0169 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -328,12 +328,12 @@ proto.adjustLayout = function(ternaryLayout, graphSize) { _this.layers.bgrid.attr('transform', bTransform); var aTransform = 'translate(' + (x0 + w / 2) + ',' + y0 + - ')rotate(30)translate(0,-' + aaxis._offset + ')'; + ')rotate(30)translate(0,' + -aaxis._offset + ')'; _this.layers.aaxis.attr('transform', aTransform); _this.layers.agrid.attr('transform', aTransform); var cTransform = 'translate(' + (x0 + w / 2) + ',' + y0 + - ')rotate(-30)translate(0,-' + caxis._offset + ')'; + ')rotate(-30)translate(0,' + -caxis._offset + ')'; _this.layers.caxis.attr('transform', cTransform); _this.layers.cgrid.attr('transform', cTransform); diff --git a/test/image/strict-d3.js b/test/image/strict-d3.js index 44197876c94..28154ea7c98 100644 --- a/test/image/strict-d3.js +++ b/test/image/strict-d3.js @@ -46,12 +46,18 @@ selProto.style = function() { return originalSelStyle.apply(sel, arguments); }; -function checkAttrVal(sel, key) { +function checkAttrVal(sel, key, val) { // setting the transform attribute on a does not // work in Chrome, IE and Edge if(sel.node().nodeName === 'clipPath' && key === 'transform') { throw new Error('d3 selection.attr called with key \'transform\' on a clipPath node'); } + + // make sure no double-negative string get into the DOM, + // their handling differs from browsers to browsers + if(/--/.test(val) && isNumeric(val.split('--')[1].charAt(0))) { + throw new Error('d3 selection.attr called with value ' + val + ' which includes a double negative'); + } } function checkStyleVal(sel, key, val) { diff --git a/test/jasmine/tests/gl3d_plot_interact_test.js b/test/jasmine/tests/gl3d_plot_interact_test.js index df339ea19b6..646f7016c6f 100644 --- a/test/jasmine/tests/gl3d_plot_interact_test.js +++ b/test/jasmine/tests/gl3d_plot_interact_test.js @@ -483,7 +483,7 @@ describe('@gl Test gl3d modebar handlers', function() { buttonZoom3d.click(); assertScenes(gd._fullLayout, 'dragmode', 'zoom'); - expect(gd.layout.dragmode).toBe(undefined); + expect(gd.layout.dragmode).toBe('zoom'); // for multi-type subplots expect(gd._fullLayout.dragmode).toBe('zoom'); expect(buttonTurntable.isActive()).toBe(false); expect(buttonZoom3d.isActive()).toBe(true); @@ -504,8 +504,8 @@ describe('@gl Test gl3d modebar handlers', function() { buttonPan3d.click(); assertScenes(gd._fullLayout, 'dragmode', 'pan'); - expect(gd.layout.dragmode).toBe(undefined); - expect(gd._fullLayout.dragmode).toBe('zoom'); + expect(gd.layout.dragmode).toBe('pan'); // for multi-type subplots + expect(gd._fullLayout.dragmode).toBe('pan'); expect(buttonTurntable.isActive()).toBe(false); expect(buttonPan3d.isActive()).toBe(true); @@ -525,7 +525,7 @@ describe('@gl Test gl3d modebar handlers', function() { buttonOrbit.click(); assertScenes(gd._fullLayout, 'dragmode', 'orbit'); - expect(gd.layout.dragmode).toBe(undefined); + expect(gd.layout.dragmode).toBe('zoom'); // fallback for multi-type subplots expect(gd._fullLayout.dragmode).toBe('zoom'); expect(buttonTurntable.isActive()).toBe(false); expect(buttonOrbit.isActive()).toBe(true); diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index d2d2bef0b7e..e88c8988383 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1375,6 +1375,8 @@ describe('hover on fill', function() { var gd = createGraphDiv(); Plotly.plot(gd, mock.data, mock.layout).then(function() { + expect(gd._fullLayout.hovermode).toBe('closest'); + // hover over a point when that's closest, even if you're over // a fill, because by default we have hoveron='points+fills' return assertLabelsCorrect([237, 150], [240.0, 144], @@ -1402,7 +1404,35 @@ describe('hover on fill', function() { }).then(function() { // then make sure we can still select a *different* item afterward return assertLabelsCorrect([237, 218], [266.75, 265], 'trace 1'); - }).then(done); + }) + .catch(fail) + .then(done); + }); + + it('should act like closest mode on ternary when cartesian is in compare mode', function(done) { + var mock = Lib.extendDeep({}, require('@mocks/ternary_fill.json')); + var gd = createGraphDiv(); + + mock.data.push({y: [7, 8, 9]}); + mock.layout.xaxis = {domain: [0.8, 1], visible: false}; + mock.layout.yaxis = {domain: [0.8, 1], visible: false}; + + Plotly.plot(gd, mock.data, mock.layout).then(function() { + expect(gd._fullLayout.hovermode).toBe('x'); + + // hover over a point when that's closest, even if you're over + // a fill, because by default we have hoveron='points+fills' + return assertLabelsCorrect([237, 150], [240.0, 144], + 'trace 2Component A: 0.8Component B: 0.1Component C: 0.1'); + }).then(function() { + // hovers over fills + return assertLabelsCorrect([237, 170], [247.7, 166], 'trace 2'); + }).then(function() { + // hover on the cartesian trace in the corner + return assertLabelsCorrect([363, 122], [363, 122], 'trace 38'); + }) + .catch(fail) + .then(done); }); }); diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index e798ff369c4..718a4aefc14 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -175,7 +175,11 @@ describe('ModeBar', function() { expectedButtonCount += group.length; }); - expect(modeBar.hasButtons(buttons)).toBe(true, 'modeBar.hasButtons'); + var actualButtons = modeBar.buttons.map(function(group) { + return group.map(function(button) { return button.name; }).join(', '); + }).join(' - '); + + expect(modeBar.hasButtons(buttons)).toBe(true, 'modeBar.hasButtons: ' + actualButtons); expect(countGroups(modeBar)).toBe(expectedGroupCount, 'correct group count'); expect(countButtons(modeBar)).toBe(expectedButtonCount, 'correct button count'); expect(countLogo(modeBar)).toBe(1, 'correct logo count'); @@ -323,7 +327,8 @@ describe('ModeBar', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['pan2d'], - ['resetViewMapbox', 'toggleHover'] + ['resetViewMapbox'], + ['toggleHover'] ]); var gd = getMockGraphInfo(); @@ -339,7 +344,8 @@ describe('ModeBar', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], ['pan2d', 'select2d', 'lasso2d'], - ['resetViewMapbox', 'toggleHover'] + ['resetViewMapbox'], + ['toggleHover'] ]); var gd = getMockGraphInfo(); @@ -393,7 +399,9 @@ describe('ModeBar', function() { it('creates mode bar (cartesian + gl3d version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], - ['resetViews', 'toggleHover'] + ['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation'], + ['resetViews'], + ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian'] ]); var gd = getMockGraphInfo(); @@ -405,14 +413,41 @@ describe('ModeBar', function() { checkButtons(modeBar, buttons, 1); }); - it('creates mode bar (cartesian + geo version)', function() { + it('creates mode bar (cartesian + geo unselectable version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], - ['resetViews', 'toggleHover'] + ['zoom2d', 'pan2d'], + ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetViews'], + ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian'] ]); - var gd = getMockGraphInfo(); + var gd = getMockGraphInfo(['x'], ['y']); + gd._fullLayout._basePlotModules = [{ name: 'cartesian' }, { name: 'geo' }]; + gd._fullLayout.xaxis = {fixedrange: false}; + + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; + + checkButtons(modeBar, buttons, 1); + }); + + it('creates mode bar (cartesian + geo selectable version)', function() { + var buttons = getButtons([ + ['toImage', 'sendDataToCloud'], + ['zoom2d', 'pan2d', 'select2d', 'lasso2d'], + ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetViews'], + ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian'] + ]); + + var gd = getMockGraphInfo(['x'], ['y']); gd._fullLayout._basePlotModules = [{ name: 'cartesian' }, { name: 'geo' }]; + gd._fullLayout.xaxis = {fixedrange: false}; + gd._fullData = [{ + type: 'scatter', + visible: true, + mode: 'markers', + _module: {selectPoints: true} + }]; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -425,7 +460,7 @@ describe('ModeBar', function() { ['toImage', 'sendDataToCloud'], ['zoom2d', 'pan2d', 'select2d', 'lasso2d'], ['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'], - ['toggleHover'] + ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian'] ]); var gd = getMockGraphInfo(['x'], ['y']); @@ -447,7 +482,9 @@ describe('ModeBar', function() { it('creates mode bar (gl3d + geo version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], - ['resetViews', 'toggleHover'] + ['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation'], + ['resetViews'], + ['toggleHover'] ]); var gd = getMockGraphInfo(); @@ -462,7 +499,8 @@ describe('ModeBar', function() { it('creates mode bar (un-selectable ternary version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], - ['zoom2d', 'pan2d'] + ['zoom2d', 'pan2d'], + ['toggleHover'] ]); var gd = getMockGraphInfo(); @@ -477,7 +515,8 @@ describe('ModeBar', function() { it('creates mode bar (selectable ternary version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], - ['zoom2d', 'pan2d', 'select2d', 'lasso2d'] + ['zoom2d', 'pan2d', 'select2d', 'lasso2d'], + ['toggleHover'] ]); var gd = getMockGraphInfo(); @@ -514,7 +553,9 @@ describe('ModeBar', function() { it('creates mode bar (ternary + gl3d version)', function() { var buttons = getButtons([ ['toImage', 'sendDataToCloud'], - ['resetViews', 'toggleHover'] + ['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation'], + ['resetViews'], + ['toggleHover'] ]); var gd = getMockGraphInfo(); diff --git a/test/jasmine/tests/ternary_test.js b/test/jasmine/tests/ternary_test.js index c8e3ccc21be..1c9cc67de3e 100644 --- a/test/jasmine/tests/ternary_test.js +++ b/test/jasmine/tests/ternary_test.js @@ -382,6 +382,34 @@ describe('ternary plots', function() { .then(done); }); + it('should render a-axis and c-axis with negative offsets', function(done) { + var gd = createGraphDiv(); + + Plotly.plot(gd, [{ + type: 'scatterternary', + a: [2, 1, 1], + b: [1, 2, 1], + c: [1, 1, 2.12345] + }], { + ternary: { + domain: { + x: [0.67, 1], + y: [0.5, 1] + }, + }, + margin: {t: 25, l: 25, r: 25, b: 25}, + height: 450, + width: 1000 + }) + .then(function() { + var subplot = gd._fullLayout.ternary._subplot; + expect(subplot.aaxis._offset < 0).toBe(true); + expect(subplot.caxis._offset < 0).toBe(true); + }) + .catch(fail) + .then(done); + }); + function countTernarySubplot() { return d3.selectAll('.ternary').size(); }