diff --git a/src/traces/bar/hover.js b/src/traces/bar/hover.js index f65f947461c..48c5596bd5e 100644 --- a/src/traces/bar/hover.js +++ b/src/traces/bar/hover.js @@ -15,42 +15,57 @@ var Color = require('../../components/color'); module.exports = function hoverPoints(pointData, xval, yval, hovermode) { - var cd = pointData.cd, - trace = cd[0].trace, - t = cd[0].t, - xa = pointData.xa, - ya = pointData.ya, - barDelta = (hovermode === 'closest') ? - t.barwidth / 2 : - t.bargroupwidth / 2, - barPos; - - if(hovermode !== 'closest') barPos = function(di) { return di.p; }; - else if(trace.orientation === 'h') barPos = function(di) { return di.y; }; - else barPos = function(di) { return di.x; }; - - var dx, dy; + var cd = pointData.cd; + var trace = cd[0].trace; + var t = cd[0].t; + var xa = pointData.xa; + var ya = pointData.ya; + + var posVal, thisBarMinPos, thisBarMaxPos, minPos, maxPos, dx, dy; + + var positionFn = function(di) { + return Fx.inbox(minPos(di) - posVal, maxPos(di) - posVal); + }; + if(trace.orientation === 'h') { + posVal = yval; + thisBarMinPos = function(di) { return di.y - di.w / 2; }; + thisBarMaxPos = function(di) { return di.y + di.w / 2; }; dx = function(di) { // add a gradient so hovering near the end of a // bar makes it a little closer match return Fx.inbox(di.b - xval, di.x - xval) + (di.x - xval) / (di.x - di.b); }; - dy = function(di) { - var centerPos = barPos(di) - yval; - return Fx.inbox(centerPos - barDelta, centerPos + barDelta); - }; + dy = positionFn; } else { + posVal = xval; + thisBarMinPos = function(di) { return di.x - di.w / 2; }; + thisBarMaxPos = function(di) { return di.x + di.w / 2; }; dy = function(di) { return Fx.inbox(di.b - yval, di.y - yval) + (di.y - yval) / (di.y - di.b); }; - dx = function(di) { - var centerPos = barPos(di) - xval; - return Fx.inbox(centerPos - barDelta, centerPos + barDelta); - }; + dx = positionFn; } + minPos = (hovermode === 'closest') ? + thisBarMinPos : + function(di) { + /* + * In compare mode, accept a bar if you're on it *or* its group. + * Nearly always it's the group that matters, but in case the bar + * was explicitly set wider than its group we'd better accept the + * whole bar. + */ + return Math.min(thisBarMinPos(di), di.p - t.bargroupwidth / 2); + }; + + maxPos = (hovermode === 'closest') ? + thisBarMaxPos : + function(di) { + return Math.max(thisBarMaxPos(di), di.p + t.bargroupwidth / 2); + }; + var distfn = Fx.getDistanceFunction(hovermode, dx, dy); Fx.getClosest(cd, distfn, pointData); @@ -58,7 +73,8 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { if(pointData.index === false) return; // the closest data point - var di = cd[pointData.index], + var index = pointData.index, + di = cd[index], mc = di.mcc || trace.marker.color, mlc = di.mlcc || trace.marker.line.color, mlw = di.mlw || trace.marker.line.width; @@ -70,16 +86,16 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { pointData.x0 = pointData.x1 = xa.c2p(di.x, true); pointData.xLabelVal = size; - pointData.y0 = ya.c2p(barPos(di) - barDelta, true); - pointData.y1 = ya.c2p(barPos(di) + barDelta, true); + pointData.y0 = ya.c2p(minPos(di), true); + pointData.y1 = ya.c2p(maxPos(di), true); pointData.yLabelVal = di.p; } else { pointData.y0 = pointData.y1 = ya.c2p(di.y, true); pointData.yLabelVal = size; - pointData.x0 = xa.c2p(barPos(di) - barDelta, true); - pointData.x1 = xa.c2p(barPos(di) + barDelta, true); + pointData.x0 = xa.c2p(minPos(di), true); + pointData.x1 = xa.c2p(maxPos(di), true); pointData.xLabelVal = di.p; } diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index ff69c21f01f..8f64743e833 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -48,9 +48,7 @@ module.exports = function plot(gd, plotinfo, cdbar) { var t = d[0].t, trace = d[0].trace, poffset = t.poffset, - poffsetIsArray = Array.isArray(poffset), - barwidth = t.barwidth, - barwidthIsArray = Array.isArray(barwidth); + poffsetIsArray = Array.isArray(poffset); d3.select(this).selectAll('g.point') .data(Lib.identity) @@ -61,7 +59,7 @@ module.exports = function plot(gd, plotinfo, cdbar) { // log values go off-screen by plotwidth // so you see them continue if you drag the plot var p0 = di.p + ((poffsetIsArray) ? poffset[i] : poffset), - p1 = p0 + ((barwidthIsArray) ? barwidth[i] : barwidth), + p1 = p0 + di.w, s0 = di.b, s1 = s0 + di.s; diff --git a/src/traces/bar/set_positions.js b/src/traces/bar/set_positions.js index cbed38a0408..3c1405ee98c 100644 --- a/src/traces/bar/set_positions.js +++ b/src/traces/bar/set_positions.js @@ -236,7 +236,7 @@ function setOffsetAndWidth(gd, pa, sieve) { applyAttributes(sieve); // store the bar center in each calcdata item - setBarCenter(gd, pa, sieve); + setBarCenterAndWidth(gd, pa, sieve); // update position axes updatePositionAxis(gd, pa, sieve); @@ -286,7 +286,7 @@ function setOffsetAndWidthInGroupMode(gd, pa, sieve) { applyAttributes(sieve); // store the bar center in each calcdata item - setBarCenter(gd, pa, sieve); + setBarCenterAndWidth(gd, pa, sieve); // update position axes updatePositionAxis(gd, pa, sieve, overlap); @@ -377,7 +377,7 @@ function applyAttributes(sieve) { } -function setBarCenter(gd, pa, sieve) { +function setBarCenterAndWidth(gd, pa, sieve) { var calcTraces = sieve.traces, pLetter = getAxisLetter(pa); @@ -392,9 +392,11 @@ function setBarCenter(gd, pa, sieve) { for(var j = 0; j < calcTrace.length; j++) { var calcBar = calcTrace[j]; + // store the actual bar width and position, for use by hover + var width = calcBar.w = (barwidthIsArray) ? barwidth[j] : barwidth; calcBar[pLetter] = calcBar.p + ((poffsetIsArray) ? poffset[j] : poffset) + - ((barwidthIsArray) ? barwidth[j] : barwidth) / 2; + width / 2; } diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js index 6bf078d4e43..3092ab3baa6 100644 --- a/test/jasmine/tests/bar_test.js +++ b/test/jasmine/tests/bar_test.js @@ -10,6 +10,7 @@ var Axes = PlotlyInternal.Axes; var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var customMatchers = require('../assets/custom_matchers'); +var failTest = require('../assets/fail_test'); describe('Bar.supplyDefaults', function() { 'use strict'; @@ -1207,9 +1208,12 @@ describe('bar hover', function() { }; } - function _hover(gd, xval, yval, closest) { + function _hover(gd, xval, yval, hovermode) { var pointData = getPointData(gd); - var pt = Bar.hoverPoints(pointData, xval, yval, closest)[0]; + var pts = Bar.hoverPoints(pointData, xval, yval, hovermode); + if(!pts) return false; + + var pt = pts[0]; return { style: [pt.index, pt.color, pt.xLabelVal, pt.yLabelVal], @@ -1290,6 +1294,95 @@ describe('bar hover', function() { }); }); + describe('with special width/offset combinations', function() { + + beforeEach(function() { + gd = createGraphDiv(); + }); + + it('should return correct hover data (single bar, trace width)', function(done) { + Plotly.plot(gd, [{ + type: 'bar', + x: [1], + y: [2], + width: 10, + marker: { color: 'red' } + }], { + xaxis: { range: [-200, 200] } + }) + .then(function() { + // all these x, y, hovermode should give the same (the only!) hover label + [ + [0, 0, 'closest'], + [-3.9, 1, 'closest'], + [5.9, 1.9, 'closest'], + [-3.9, -10, 'x'], + [5.9, 19, 'x'] + ].forEach(function(hoverSpec) { + var out = _hover(gd, hoverSpec[0], hoverSpec[1], hoverSpec[2]); + + expect(out.style).toEqual([0, 'red', 1, 2], hoverSpec); + assertPos(out.pos, [264, 278, 14, 14], hoverSpec); + }); + + // then a few that are off the edge so yield nothing + [ + [1, -0.1, 'closest'], + [1, 2.1, 'closest'], + [-4.1, 1, 'closest'], + [6.1, 1, 'closest'], + [-4.1, 1, 'x'], + [6.1, 1, 'x'] + ].forEach(function(hoverSpec) { + var out = _hover(gd, hoverSpec[0], hoverSpec[1], hoverSpec[2]); + + expect(out).toBe(false, hoverSpec); + }); + }) + .catch(failTest) + .then(done); + }); + + it('should return correct hover data (two bars, array width)', function(done) { + Plotly.plot(gd, [{ + type: 'bar', + x: [1, 200], + y: [2, 1], + width: [10, 20], + marker: { color: 'red' } + }, { + type: 'bar', + x: [1, 200], + y: [1, 2], + width: [20, 10], + marker: { color: 'green' } + }], { + xaxis: { range: [-200, 300] }, + width: 500, + height: 500 + }) + .then(function() { + var out = _hover(gd, -36, 1.5, 'closest'); + + expect(out.style).toEqual([0, 'red', 1, 2]); + assertPos(out.pos, [99, 106, 13, 13]); + + out = _hover(gd, 164, 0.8, 'closest'); + + expect(out.style).toEqual([1, 'red', 200, 1]); + assertPos(out.pos, [222, 235, 168, 168]); + + out = _hover(gd, 125, 0.8, 'x'); + + expect(out.style).toEqual([1, 'red', 200, 1]); + assertPos(out.pos, [203, 304, 168, 168]); + }) + .catch(failTest) + .then(done); + }); + + }); + }); function mockBarPlot(dataWithoutTraceType, layout) {