diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index fcc88da4149..e67a48c220a 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -640,33 +640,67 @@ function _hover(gd, evt, subplot, noHoverEvent) { hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; }); + // move period positioned points to the end of list + hoverData = orderPeriod(hoverData, hovermode); + // 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 ) { - var hd = hoverData[0]; - var cd0 = hd.cd[hd.index]; - var isGrouped = (fullLayout.boxmode === 'group' || fullLayout.violinmode === 'group'); - - var xVal = hd.xVal; - var ax = hd.xa; - if(ax.type === 'category') xVal = ax._categoriesMap[xVal]; - if(ax.type === 'date') xVal = ax.d2c(xVal); - if(cd0 && cd0.t && cd0.t.posLetter === ax._id && isGrouped) { - xVal += cd0.t.dPos; - } + var initLen = hoverData.length; + var winningPoint = hoverData[0]; - var yVal = hd.yVal; - ax = hd.ya; - if(ax.type === 'category') yVal = ax._categoriesMap[yVal]; - if(ax.type === 'date') yVal = ax.d2c(yVal); - if(cd0 && cd0.t && cd0.t.posLetter === ax._id && isGrouped) { - yVal += cd0.t.dPos; - } + var customXVal = customVal('x', winningPoint, fullLayout); + var customYVal = customVal('y', winningPoint, fullLayout); + + findHoverPoints(customXVal, customYVal); + + // also find start, middle and end point for period + var axLetter = hovermode.charAt(0); + if(winningPoint.trace[axLetter + 'period']) { + var v = winningPoint[axLetter + 'LabelVal']; + var ax = winningPoint[axLetter + 'a']; + var T = {}; + T[axLetter + 'period'] = winningPoint.trace[axLetter + 'period']; + T[axLetter + 'period0'] = winningPoint.trace[axLetter + 'period0']; - findHoverPoints(xVal, yVal); + T[axLetter + 'periodalignment'] = 'start'; + var start = alignPeriod(T, ax, axLetter, [v])[0]; + + T[axLetter + 'periodalignment'] = 'middle'; + var middle = alignPeriod(T, ax, axLetter, [v])[0]; + + T[axLetter + 'periodalignment'] = 'end'; + var end = alignPeriod(T, ax, axLetter, [v])[0]; + + if(axLetter === 'x') { + findHoverPoints(start, customYVal); + findHoverPoints(middle, customYVal); + findHoverPoints(end, customYVal); + } else { + findHoverPoints(customXVal, start); + findHoverPoints(customXVal, middle); + findHoverPoints(customXVal, end); + } + + var k; + var seen = {}; + for(k = 0; k < initLen; k++) { + seen[hoverData[k].trace.index] = true; + } + + // remove non-period aditions and traces that seen before + for(k = hoverData.length - 1; k >= initLen; k--) { + if( + seen[hoverData[k].trace.index] || + !hoverData[k].trace[axLetter + 'period'] + ) { + hoverData.splice(k, 1); + } + } + } // Remove duplicated hoverData points // note that d3 also filters identical points in the rendering steps @@ -1889,3 +1923,42 @@ function plainText(s, len) { allowedTags: ['br', 'sub', 'sup', 'b', 'i', 'em'] }); } + +function orderPeriod(hoverData, hovermode) { + var axLetter = hovermode.charAt(0); + + var first = []; + var last = []; + + for(var i = 0; i < hoverData.length; i++) { + var d = hoverData[i]; + + if(d.trace[axLetter + 'period']) { + last.push(d); + } else { + first.push(d); + } + } + + return first.concat(last); +} + +function customVal(axLetter, winningPoint, fullLayout) { + var ax = winningPoint[axLetter + 'a']; + var val = winningPoint[axLetter + 'Val']; + + if(ax.type === 'category') val = ax._categoriesMap[val]; + else if(ax.type === 'date') val = ax.d2c(val); + + var cd0 = winningPoint.cd[winningPoint.index]; + if(cd0 && cd0.t && cd0.t.posLetter === ax._id) { + if( + fullLayout.boxmode === 'group' || + fullLayout.violinmode === 'group' + ) { + val += cd0.t.dPos; + } + } + + return val; +} diff --git a/src/traces/bar/hover.js b/src/traces/bar/hover.js index e6023543686..3a0f30d252c 100644 --- a/src/traces/bar/hover.js +++ b/src/traces/bar/hover.js @@ -35,8 +35,22 @@ function hoverOnBars(pointData, xval, yval, hovermode) { var posVal, sizeVal, posLetter, sizeLetter, dx, dy, pRangeCalc; - function thisBarMinPos(di) { return di[posLetter] - di.w / 2; } - function thisBarMaxPos(di) { return di[posLetter] + di.w / 2; } + function thisBarMinPos(di) { return thisBarExtPos(di, -1); } + function thisBarMaxPos(di) { return thisBarExtPos(di, 1); } + + function thisBarExtPos(di, sgn) { + var w = di.w; + var delta = sgn * w; + if(trace[posLetter + 'period']) { + var alignment = trace[posLetter + 'periodalignment']; + if(alignment === 'start') { + delta = (sgn === -1) ? 0 : w; + } else if(alignment === 'end') { + delta = (sgn === -1) ? -w : 0; + } + } + return di[posLetter] + delta / 2; + } var minPos = isClosest ? thisBarMinPos : diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index b875369975d..33740525519 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -4998,6 +4998,162 @@ describe('hovermode: (x|y)unified', function() { .then(done, done.fail); }); + [{ + xperiod: 0, + desc: 'non-period scatter points and period bars' + }, { + xperiod: 24 * 3600 * 1000, + desc: 'period scatter points and period bars' + }].forEach(function(t) { + it(t.desc, function(done) { + var fig = { + data: [ + { + name: 'bar', + type: 'bar', + x: ['1999-12', '2000-01', '2000-02'], + y: [2, 1, 3], + xhoverformat: '%b', + xperiod: 'M1' + }, + { + xperiod: t.xperiod, + name: 'scatter', + type: 'scatter', + x: [ + '2000-01-01', '2000-01-06', '2000-01-11', '2000-01-16', '2000-01-21', '2000-01-26', + '2000-02-01', '2000-02-06', '2000-02-11', '2000-02-16', '2000-02-21', '2000-02-26', + '2000-03-01', '2000-03-06', '2000-03-11', '2000-03-16', '2000-03-21', '2000-03-26' + ], + y: [ + 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, + 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, + 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, + ] + } + ], + layout: { + showlegend: false, + width: 600, + height: 400, + hovermode: 'x unified' + } + }; + + Plotly.newPlot(gd, fig) + .then(function(gd) { + _hover(gd, { xpx: 50, ypx: 200 }); + assertLabel({title: 'Dec', items: [ + 'bar : 2' + ]}); + + _hover(gd, { xpx: 100, ypx: 200 }); + assertLabel({title: 'Jan 1, 2000', items: [ + 'scatter : 1.1' + ]}); + + _hover(gd, { xpx: 150, ypx: 200 }); + assertLabel({title: 'Jan 11, 2000', items: [ + 'bar : (Jan, 1)', + 'scatter : 1.3' + ]}); + + _hover(gd, { xpx: 200, ypx: 200 }); + assertLabel({title: 'Jan 26, 2000', items: [ + 'bar : (Jan, 1)', + 'scatter : 1.6' + ]}); + + _hover(gd, { xpx: 250, ypx: 200 }); + assertLabel({title: 'Feb 11, 2000', items: [ + 'bar : (Feb, 3)', + 'scatter : 2.3' + ]}); + + _hover(gd, { xpx: 300, ypx: 200 }); + assertLabel({title: 'Feb 21, 2000', items: [ + 'bar : (Feb, 3)', + 'scatter : 2.5' + ]}); + + _hover(gd, { xpx: 350, ypx: 200 }); + assertLabel({title: 'Mar 6, 2000', items: [ + 'scatter : 3.2' + ]}); + }) + .then(done, done.fail); + }); + }); + + it('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: 'scatter', + 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 1, 2000', items: [ + 'bar : 1', + 'start : (Jan, 1)', + 'end : (Jan, 1)' + ]}); + + _hover(gd, { xpx: 360, ypx: 200 }); + assertLabel({title: 'Feb 1, 2000', items: [ + 'bar : 2', + 'start : (Feb, 2)', + 'end : (Feb, 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('should have the same traceorder as the legend', function(done) { var mock = require('@mocks/stacked_area.json'); var mockCopy = Lib.extendDeep({}, mock);