diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 92c7dd8b784..e05fce31bfd 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -262,6 +262,17 @@ exports.loneHover = function loneHover(hoverItems, opts) { return multiHover ? hoverLabel : hoverLabel.node(); }; +// can either axis of this trace draw a spike to a point that is not hovered? +// 'hovered data' spikes snap to the hover label, so they need hoverData to exist. +function canSpikeToClosest(pointData) { + var xa = pointData.xa; + var ya = pointData.ya; + return Boolean( + (xa && xa.showspikes && xa.spikesnap !== 'hovered data') || + (ya && ya.showspikes && ya.spikesnap !== 'hovered data') + ); +} + // The actual implementation is here: function _hover(gd, evt, subplot, noHoverEvent, eventTarget) { if (!subplot) subplot = 'xy'; @@ -659,8 +670,10 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) { } // Now if there is range to look in, find the points to draw the spikelines - // Do it only if there is no hoverData - if (hasCartesian && spikedistance !== 0) { + // Do it only if there is no hoverData, and only if one of this trace's axes + // can actually draw a spike to a non-hovered point - the search below is + // unbounded when spikedistance is -1 (the default), so it is expensive. + if (hasCartesian && spikedistance !== 0 && canSpikeToClosest(pointData)) { if (hoverData.length === 0) { pointData.distance = spikedistance; pointData.index = false; diff --git a/test/jasmine/tests/hover_spikeline_test.js b/test/jasmine/tests/hover_spikeline_test.js index 311c0e7b503..9da880d7b07 100644 --- a/test/jasmine/tests/hover_spikeline_test.js +++ b/test/jasmine/tests/hover_spikeline_test.js @@ -856,3 +856,73 @@ describe('spikeline hover', function() { }); }); }); + +describe('spikeline search', function() { + 'use strict'; + + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + // the search for a point to spike to is unbounded when spikedistance is -1 + // (the default), so it must not run when no spike can come out of it + function countHoverPointsCalls(layout) { + return Plotly.newPlot(gd, [{ + x: [1, 2, 3], + y: [1, 2, 3], + mode: 'markers' + }], Lib.extendFlat({width: 400, height: 400}, layout)) + .then(function() { + var spy = spyOn(gd.calcdata[0][0].trace._module, 'hoverPoints').and.callThrough(); + + Lib.clearThrottle(); + // hover on empty space, so there is no hoverData + Fx.hover(gd, {xpx: 40, ypx: 40}, 'xy'); + + return spy.calls.count(); + }); + } + + it('does not look for spike points when no axis shows spikes', function(done) { + countHoverPointsCalls({}) + .then(function(count) { + expect(count).toBe(1); + }) + .then(done, done.fail); + }); + + it('does not look for spike points when spikesnap is "hovered data"', function(done) { + countHoverPointsCalls({ + xaxis: {showspikes: true, spikesnap: 'hovered data'}, + yaxis: {showspikes: true, spikesnap: 'hovered data'} + }) + .then(function(count) { + expect(count).toBe(1); + }) + .then(done, done.fail); + }); + + it('looks for spike points when an axis snaps spikes to data', function(done) { + countHoverPointsCalls({ + xaxis: {showspikes: true, spikesnap: 'data'} + }) + .then(function(count) { + expect(count).toBe(2); + }) + .then(done, done.fail); + }); + + it('looks for spike points when an axis snaps spikes to the cursor', function(done) { + countHoverPointsCalls({ + yaxis: {showspikes: true, spikesnap: 'cursor'} + }) + .then(function(count) { + expect(count).toBe(2); + }) + .then(done, done.fail); + }); +});