diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 947285096dd..57541162afa 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -39,6 +39,13 @@ var YSHIFTY = Math.sin(YA_RADIANS); var HOVERARROWSIZE = constants.HOVERARROWSIZE; var HOVERTEXTPAD = constants.HOVERTEXTPAD; +var multipleHoverPoints = { + box: true, + ohlc: true, + violin: true, + candlestick: true +}; + // fx.hover: highlight data on hover // evt can be a mousemove event, or an object with data about what points // to hover on @@ -651,39 +658,58 @@ function _hover(gd, evt, subplot, noHoverEvent) { }; sortHoverData(); - // If in compare mode, select every point at position if( helpers.isXYhover(_mode) && hoverData[0].length !== 0 && hoverData[0].trace.type !== 'splom' // TODO: add support for splom ) { + // pick winning point var winningPoint = hoverData[0]; + // discard other points + if(multipleHoverPoints[winningPoint.trace.type]) { + hoverData = hoverData.filter(function(d) { + return d.trace.index === winningPoint.trace.index; + }); + } else { + hoverData = [winningPoint]; + } + var initLen = hoverData.length; - var customXVal = customVal('x', winningPoint, fullLayout); - var customYVal = customVal('y', winningPoint, fullLayout); + var winX = getCoord('x', winningPoint, fullLayout); + var winY = getCoord('y', winningPoint, fullLayout); - findHoverPoints(customXVal, customYVal); + // in compare mode, select every point at position + findHoverPoints(winX, winY); var finalPoints = []; var seen = {}; - var insert = function(hd) { - var type = hd.trace.type; - var key = ( - type === 'box' || - type === 'violin' || - type === 'ohlc' || - type === 'candlestick' - ) ? hoverDataKey(hd) : hd.trace.index; + var id = 0; + var insert = function(newHd) { + var key = multipleHoverPoints[newHd.trace.type] ? hoverDataKey(newHd) : newHd.trace.index; if(!seen[key]) { - seen[key] = true; - finalPoints.push(hd); + id++; + seen[key] = id; + finalPoints.push(newHd); + } else { + var oldId = seen[key] - 1; + var oldHd = finalPoints[oldId]; + if(oldId > 0 && + Math.abs(newHd.distance) < + Math.abs(oldHd.distance) + ) { + // replace with closest + finalPoints[oldId] = newHd; + } } }; - // insert the winnig point first - insert(winningPoint); + var k; + // insert the winnig point(s) first + for(k = 0; k < initLen; k++) { + insert(hoverData[k]); + } // override from the end - for(var k = hoverData.length - 1; k > 0; k--) { + for(k = hoverData.length - 1; k > initLen - 1; k--) { insert(hoverData[k]); } hoverData = finalPoints; @@ -1045,8 +1071,9 @@ function createHoverText(hoverData, opts, gd) { legendDraw(gd, mockLegend); // Position the hover - var ly = Lib.mean(hoverData.map(function(c) {return (c.y0 + c.y1) / 2;})); - var lx = Lib.mean(hoverData.map(function(c) {return (c.x0 + c.x1) / 2;})); + var winningPoint = hoverData[0]; + var ly = (winningPoint.y0 + winningPoint.y1) / 2; + var lx = (winningPoint.x0 + winningPoint.x1) / 2; var legendContainer = container.select('g.legend'); var tbb = legendContainer.node().getBoundingClientRect(); lx += xa._offset; @@ -1892,7 +1919,7 @@ function orderRangePoints(hoverData, hovermode) { return first.concat(second).concat(last); } -function customVal(axLetter, winningPoint, fullLayout) { +function getCoord(axLetter, winningPoint, fullLayout) { var ax = winningPoint[axLetter + 'a']; var val = winningPoint[axLetter + 'Val']; diff --git a/src/traces/bar/hover.js b/src/traces/bar/hover.js index 2c0ba4d86ba..deb78362095 100644 --- a/src/traces/bar/hover.js +++ b/src/traces/bar/hover.js @@ -35,14 +35,35 @@ function hoverOnBars(pointData, xval, yval, hovermode, opts) { var posVal, sizeVal, posLetter, sizeLetter, dx, dy, pRangeCalc; + if(trace.orientation === 'h') { + posVal = yval; + sizeVal = xval; + posLetter = 'y'; + sizeLetter = 'x'; + dx = sizeFn; + dy = positionFn; + } else { + posVal = xval; + sizeVal = yval; + posLetter = 'x'; + sizeLetter = 'y'; + dy = sizeFn; + dx = positionFn; + } + + var period = trace[posLetter + 'period']; + function thisBarMinPos(di) { return thisBarExtPos(di, -1); } function thisBarMaxPos(di) { return thisBarExtPos(di, 1); } function thisBarExtPos(di, sgn) { - return di[posLetter] + 0.5 * sgn * di.w; + if(period) { + return di.p + sgn * Math.abs(di.p - di.orig_p); + } + return di[posLetter] + sgn * di.w / 2; } - var minPos = isClosest ? + var minPos = isClosest || period ? thisBarMinPos : function(di) { /* @@ -60,7 +81,7 @@ function hoverOnBars(pointData, xval, yval, hovermode, opts) { return Math.min(thisBarMinPos(di), di.p - t.bardelta / 2); }; - var maxPos = isClosest ? + var maxPos = isClosest || period ? thisBarMaxPos : function(di) { return Math.max(thisBarMaxPos(di), di.p + t.bardelta / 2); @@ -118,22 +139,6 @@ function hoverOnBars(pointData, xval, yval, hovermode, opts) { return Fx.inbox(b - v, s - v, maxSpikeDistance + (s - v) / (s - b) - 1); } - if(trace.orientation === 'h') { - posVal = yval; - sizeVal = xval; - posLetter = 'y'; - sizeLetter = 'x'; - dx = sizeFn; - dy = positionFn; - } else { - posVal = xval; - sizeVal = yval; - posLetter = 'x'; - sizeLetter = 'y'; - dy = sizeFn; - dx = positionFn; - } - var pa = pointData[posLetter + 'a']; var sa = pointData[sizeLetter + 'a']; diff --git a/src/traces/scatter/hover.js b/src/traces/scatter/hover.js index b9dc41387c4..b4b6a62dd23 100644 --- a/src/traces/scatter/hover.js +++ b/src/traces/scatter/hover.js @@ -28,13 +28,14 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { var rad = Math.max(3, di.mrc || 0); var kink = 1 - 1 / rad; var dxRaw = Math.abs(xa.c2p(di.x) - xpx); - var d = (dxRaw < rad) ? (kink * dxRaw / rad) : (dxRaw - rad + kink); - return d; + if(di.orig_x !== undefined) dxRaw += xa.c2p(di.orig_x) - xa.c2p(di.x); + return (dxRaw < rad) ? (kink * dxRaw / rad) : (dxRaw - rad + kink); }; var dy = function(di) { var rad = Math.max(3, di.mrc || 0); var kink = 1 - 1 / rad; var dyRaw = Math.abs(ya.c2p(di.y) - ypx); + if(di.orig_y !== undefined) dyRaw += ya.c2p(di.orig_y) - ya.c2p(di.y); return (dyRaw < rad) ? (kink * dyRaw / rad) : (dyRaw - rad + kink); }; var dxy = function(di) { diff --git a/src/traces/scattergl/hover.js b/src/traces/scattergl/hover.js index 899825c9cb0..e1b2a16ecd5 100644 --- a/src/traces/scattergl/hover.js +++ b/src/traces/scattergl/hover.js @@ -48,9 +48,11 @@ function hoverPoints(pointData, xval, yval, hovermode) { for(i = 0; i < ids.length; i++) { ptx = x[ids[i]]; dx = Math.abs(xa.c2p(ptx) - xpx); + if(trace._origX && trace._origX[i] !== undefined) dx += xa.c2p(trace._origX[i]) - xa.c2p(ptx); if(dx < minDist) { minDist = dx; dy = ya.c2p(y[ids[i]]) - ypx; + if(trace._origY && trace._origY[i] !== undefined) dy += ya.c2p(trace._origY[i]) - ya.c2p(pty); dxy = Math.sqrt(dx * dx + dy * dy); id = ids[i]; } diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 9db86bcb2d5..9ea0ac24305 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -2967,13 +2967,6 @@ describe('hover on traces with (x|y)period positioning', function() { nums: '(Q1, 1)' }); }) - .then(function() { _hover(380, 395); }) - .then(function() { - assertHoverLabelContent({ - name: 'middle (M3)', - nums: '(Q1, 2)' - }); - }) .then(function() { _hover(415, 425); }) .then(function() { assertHoverLabelContent({ @@ -3010,13 +3003,6 @@ describe('hover on traces with (x|y)period positioning', function() { nums: '(Jan 2001, 1)' }); }) - .then(function() { _hover(665, 395); }) - .then(function() { - assertHoverLabelContent({ - name: 'middle (M12)', - nums: '(Jul 2001, 2)' - }); - }) .then(function() { _hover(700, 425); }) .then(function() { assertHoverLabelContent({ @@ -5168,7 +5154,7 @@ describe('hovermode: (x|y)unified', function() { xperiod: 0, desc: 'non-period scatter points and period bars' }, { - xperiod: 24 * 3600 * 1000, + xperiod: 5 * 24 * 3600 * 1000, desc: 'period scatter points and period bars' }].forEach(function(t) { it(t.desc, function(done) { @@ -5213,12 +5199,23 @@ describe('hovermode: (x|y)unified', function() { 'bar : 2' ]}); + _hover(gd, { xpx: 75, ypx: 200 }); + assertLabel({title: 'Dec', items: [ + 'bar : 2' + ]}); + _hover(gd, { xpx: 100, ypx: 200 }); assertLabel({title: 'Jan 1, 2000', items: [ - 'bar : (Dec, 2)', + 'bar : (Jan, 1)', 'scatter : 1.1' ]}); + _hover(gd, { xpx: 125, ypx: 200 }); + assertLabel({title: 'Jan 6, 2000', items: [ + 'bar : (Jan, 1)', + 'scatter : 1.2' + ]}); + _hover(gd, { xpx: 150, ypx: 200 }); assertLabel({title: 'Jan 11, 2000', items: [ 'bar : (Jan, 1)', @@ -5231,18 +5228,35 @@ describe('hovermode: (x|y)unified', function() { 'scatter : 1.6' ]}); + _hover(gd, { xpx: 225, ypx: 200 }); + assertLabel({title: 'Feb 1, 2000', items: [ + 'bar : (Feb, 3)', + 'scatter : 2.1' + ]}); + _hover(gd, { xpx: 250, ypx: 200 }); assertLabel({title: 'Feb 11, 2000', items: [ 'bar : (Feb, 3)', 'scatter : 2.3' ]}); + _hover(gd, { xpx: 275, ypx: 200 }); + assertLabel({title: 'Feb 16, 2000', items: [ + 'bar : (Feb, 3)', + 'scatter : 2.4' + ]}); + _hover(gd, { xpx: 300, ypx: 200 }); assertLabel({title: 'Feb 21, 2000', items: [ 'bar : (Feb, 3)', 'scatter : 2.5' ]}); + _hover(gd, { xpx: 325, ypx: 200 }); + assertLabel({title: 'Mar 1, 2000', items: [ + 'scatter : 3.1' + ]}); + _hover(gd, { xpx: 350, ypx: 200 }); assertLabel({title: 'Mar 6, 2000', items: [ 'scatter : 3.2' @@ -5252,68 +5266,226 @@ describe('hovermode: (x|y)unified', function() { }); }); - it('period points alignments', function(done) { + ['scatter', 'scattergl'].forEach(function(scatterType) { + it(scatterType + ' period points alignments', function(done) { + Plotly.newPlot(gd, { + data: [ + { + name: 'bar', + type: 'bar', + x: ['2000-01', '2000-02'], + y: [1, 2], + xhoverfrmat: '%b', + xperiod: 'M1' + }, + { + name: 'start', + type: scatterType, + x: ['2000-01', '2000-02'], + y: [1, 2], + xhoverformat: '%b', + xperiod: 'M1', + xperiodalignment: 'start' + }, + { + name: 'end', + type: 'scatter', + x: ['2000-01', '2000-02'], + y: [1, 2], + xhoverformat: '%b', + xperiod: 'M1', + xperiodalignment: 'end' + }, + ], + layout: { + showlegend: false, + width: 600, + height: 400, + hovermode: 'x unified' + } + }) + .then(function(gd) { + _hover(gd, { xpx: 40, ypx: 200 }); + assertLabel({title: 'Jan', items: [ + 'bar : (Jan 1, 2000, 1)', + 'start : 1', + 'end : 1' + ]}); + + _hover(gd, { xpx: 100, ypx: 200 }); + assertLabel({title: 'Jan', items: [ + 'bar : (Jan 1, 2000, 1)', + 'start : 1', + 'end : 1' + ]}); + + _hover(gd, { xpx: 360, ypx: 200 }); + assertLabel({title: 'Feb', items: [ + 'bar : (Feb 1, 2000, 2)', + 'start : 2', + 'end : 2' + ]}); + + _hover(gd, { xpx: 400, ypx: 200 }); + assertLabel({title: 'Feb', items: [ + 'bar : (Feb 1, 2000, 2)', + 'start : 2', + 'end : 2' + ]}); + }) + .then(done, done.fail); + }); + }); + + it('period with hover distance -1 include closest not farthest', function(done) { Plotly.newPlot(gd, { data: [ { name: 'bar', type: 'bar', - x: ['2000-01', '2000-02'], - y: [1, 2], - xhoverfrmat: '%b', - xperiod: 'M1' - }, - { - name: 'start', - type: 'scatter', - x: ['2000-01', '2000-02'], - y: [1, 2], - xhoverformat: '%b', - xperiod: 'M1', - xperiodalignment: 'start' + x: [ + '2017-01', + '2017-04', + '2017-07', + '2017-10', + '2018-01', + ], + xhoverformat: 'Q%q', + xperiod: 'M3', + y: [ + 0, + 3, + 6, + 9, + 12 + ] }, { - name: 'end', + name: 'scatter', type: 'scatter', - x: ['2000-01', '2000-02'], - y: [1, 2], + x: [ + '2017-01', + '2017-02', + '2017-03', + '2017-04', + '2017-05', + '2017-06', + '2017-07', + '2017-08', + '2017-09', + '2017-10', + '2017-11', + '2017-12', + '2018-01', + '2018-02', + '2018-03' + ], xhoverformat: '%b', xperiod: 'M1', - xperiodalignment: 'end' - }, + y: [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15 + ] + } ], layout: { showlegend: false, width: 600, height: 400, - hovermode: 'x unified' + hovermode: 'x unified', + hoverdistance: -1, + xaxis: { + dtick: 'M1', + showgrid: true, + ticklabelmode: 'period', + type: 'date' + } } }) .then(function(gd) { - _hover(gd, { xpx: 40, ypx: 200 }); + _hover(gd, { xpx: 25, ypx: 200 }); assertLabel({title: 'Jan', items: [ - 'bar : (Jan 1, 2000, 1)', - 'start : 1' + 'bar : (Q1, 0)', + 'scatter : 1' + ]}); + + _hover(gd, { xpx: 50, ypx: 200 }); + assertLabel({title: 'Feb', items: [ + 'bar : (Q1, 0)', + 'scatter : 2' + ]}); + + _hover(gd, { xpx: 75, ypx: 200 }); + assertLabel({title: 'Mar', items: [ + 'bar : (Q1, 0)', + 'scatter : 3' ]}); _hover(gd, { xpx: 100, ypx: 200 }); - assertLabel({title: 'Jan', items: [ - 'bar : (Jan 1, 2000, 1)', - 'start : 1' + assertLabel({title: 'Apr', items: [ + 'bar : (Q2, 3)', + 'scatter : 4' ]}); - _hover(gd, { xpx: 360, ypx: 200 }); - assertLabel({title: 'Jan', items: [ - 'bar : (Feb 1, 2000, 2)', - 'start : (Feb, 2)', - 'end : 1' + _hover(gd, { xpx: 125, ypx: 200 }); + assertLabel({title: 'May', items: [ + 'bar : (Q2, 3)', + 'scatter : 5' ]}); - _hover(gd, { xpx: 400, ypx: 200 }); - assertLabel({title: 'Feb', items: [ - 'bar : (Feb 1, 2000, 2)', - 'start : 2', - 'end : 2' + _hover(gd, { xpx: 150, ypx: 200 }); + assertLabel({title: 'Jun', items: [ + 'bar : (Q2, 3)', + 'scatter : 6' + ]}); + + _hover(gd, { xpx: 200, ypx: 200 }); + assertLabel({title: 'Jul', items: [ + 'bar : (Q3, 6)', + 'scatter : 7' + ]}); + + _hover(gd, { xpx: 225, ypx: 200 }); + assertLabel({title: 'Aug', items: [ + 'bar : (Q3, 6)', + 'scatter : 8' + ]}); + + _hover(gd, { xpx: 250, ypx: 200 }); + assertLabel({title: 'Sep', items: [ + 'bar : (Q3, 6)', + 'scatter : 9' + ]}); + + _hover(gd, { xpx: 275, ypx: 200 }); + assertLabel({title: 'Oct', items: [ + 'bar : (Q4, 9)', + 'scatter : 10' + ]}); + + _hover(gd, { xpx: 300, ypx: 200 }); + assertLabel({title: 'Nov', items: [ + 'bar : (Q4, 9)', + 'scatter : 11' + ]}); + + _hover(gd, { xpx: 325, ypx: 200 }); + assertLabel({title: 'Dec', items: [ + 'bar : (Q4, 9)', + 'scatter : 12' ]}); }) .then(done, done.fail); diff --git a/test/jasmine/tests/violin_test.js b/test/jasmine/tests/violin_test.js index a8dbd926bcf..4b111891583 100644 --- a/test/jasmine/tests/violin_test.js +++ b/test/jasmine/tests/violin_test.js @@ -537,8 +537,8 @@ describe('Test violin hover:', function() { name: ['', '', '', '', '', ''], axis: 'Sat', hoverLabelPos: [ - [364, 270], [387, 270], [339, 270], - [346, 270], [349, 270], [352, 270] + [364, 270], [352, 270], [339, 270], + [346, 270], [349, 270], [387, 270] ] }, { desc: 'single horizontal violin',