diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index b84eb742273..5a7c917341b 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -313,9 +313,14 @@ function hover(gd, evt, subplot) { var fullLayout = gd._fullLayout, plotinfo = fullLayout._plots[subplot], - // list of all overlaid subplots to look at - subplots = [subplot].concat(plotinfo.overlays - .map(function(pi) { return pi.id; })), + + //If the user passed in an array of subplots, use those instead of finding overlayed plots + subplots = Array.isArray(subplot) ? + subplot : + // list of all overlaid subplots to look at + [subplot].concat(plotinfo.overlays + .map(function(pi) { return pi.id; })), + xaArray = subplots.map(function(spId) { return Plotly.Axes.getFromId(gd, spId, 'x'); }), @@ -533,7 +538,7 @@ function hover(gd, evt, subplot) { }; var hoverLabels = createHoverText(hoverData, labelOpts); - hoverAvoidOverlaps(hoverData, rotateLabels ? xaArray[0] : yaArray[0]); + hoverAvoidOverlaps(hoverData, rotateLabels ? 'xa' : 'ya'); alignHoverText(hoverLabels, rotateLabels); @@ -872,7 +877,7 @@ function createHoverText(hoverData, opts) { // first create the objects var hoverLabels = container.selectAll('g.hovertext') .data(hoverData,function(d) { - return [d.trace.index,d.index,d.x0,d.y0,d.name,d.attr||''].join(','); + return [d.trace.index,d.index,d.x0,d.y0,d.name,d.attr,d.xa,d.ya ||''].join(','); }); hoverLabels.enter().append('g') .classed('hovertext',true) @@ -978,8 +983,8 @@ function createHoverText(hoverData, opts) { stroke: contrastColor }); var tbb = tx.node().getBoundingClientRect(), - htx = xa._offset+(d.x0+d.x1)/2, - hty = ya._offset+(d.y0+d.y1)/2, + htx = d.xa._offset+(d.x0+d.x1)/2, + hty = d.ya._offset+(d.y0+d.y1)/2, dx = Math.abs(d.x1-d.x0), dy = Math.abs(d.y1-d.y0), txTotalWidth = tbb.width+HOVERARROWSIZE+HOVERTEXTPAD+tx2width, @@ -1042,18 +1047,19 @@ function createHoverText(hoverData, opts) { // information then. function hoverAvoidOverlaps(hoverData, ax) { var nummoves = 0, - pmin = ax._offset, - pmax = ax._offset+ax._length, // make groups of touching points pointgroups = hoverData .map(function(d,i) { + var axis = d[ax]; return [{ i: i, dp: 0, pos: d.pos, posref: d.posref, - size: d.by*(ax._id.charAt(0)==='x' ? YFACTOR : 1)/2 + size: d.by*(axis._id.charAt(0)==='x' ? YFACTOR : 1)/2, + pmin: axis._offset, + pmax: axis._offset+axis._length }]; }) .sort(function(a,b) { return a[0].posref-b[0].posref; }), @@ -1069,10 +1075,10 @@ function hoverAvoidOverlaps(hoverData, ax) { maxPt = grp[grp.length-1]; // overlap with the top - positive vals are overlaps - topOverlap = pmin-minPt.pos-minPt.dp+minPt.size; + topOverlap = minPt.pmin-minPt.pos-minPt.dp+minPt.size; // overlap with the bottom - positive vals are overlaps - bottomOverlap = maxPt.pos+maxPt.dp+maxPt.size-pmax; + bottomOverlap = maxPt.pos+maxPt.dp+maxPt.size-minPt.pmax; // check for min overlap first, so that we always // see the largest labels @@ -1096,7 +1102,7 @@ function hoverAvoidOverlaps(hoverData, ax) { var deleteCount = 0; for(i=0; ipmax) deleteCount++; + if(pti.pos+pti.dp+pti.size>minPt.pmax) deleteCount++; } // start by deleting points whose data is off screen @@ -1106,7 +1112,7 @@ function hoverAvoidOverlaps(hoverData, ax) { // pos has already been constrained to [pmin,pmax] // so look for points close to that to delete - if(pti.pos>pmax-1) { + if(pti.pos>minPt.pmax-1) { pti.del = true; deleteCount--; } @@ -1117,7 +1123,7 @@ function hoverAvoidOverlaps(hoverData, ax) { // pos has already been constrained to [pmin,pmax] // so look for points close to that to delete - if(pti.pos=0; i--) { if(deleteCount<=0) break; pti = grp[i]; - if(pti.pos+pti.dp+pti.size>pmax) { + if(pti.pos+pti.dp+pti.size>minPt.pmax) { pti.del = true; deleteCount--; } @@ -1158,7 +1164,9 @@ function hoverAvoidOverlaps(hoverData, ax) { p0 = g0[g0.length-1], p1 = g1[0]; topOverlap = p0.pos+p0.dp+p0.size-p1.pos-p1.dp+p1.size; - if(topOverlap>0.01) { + + //Only group points that lie on the same axes + if(topOverlap>0.01 && (p0.pmin === p1.pmin) && (p0.pmax === p1.pmax)) { // push the new point(s) added to this group out of the way for(j=g1.length-1; j>=0; j--) g1[j].dp += topOverlap; diff --git a/test/image/baselines/stacked_subplots_shared_yaxis.png b/test/image/baselines/stacked_subplots_shared_yaxis.png new file mode 100644 index 00000000000..70a3beea374 Binary files /dev/null and b/test/image/baselines/stacked_subplots_shared_yaxis.png differ diff --git a/test/image/mocks/stacked_subplots_shared_yaxis.json b/test/image/mocks/stacked_subplots_shared_yaxis.json new file mode 100644 index 00000000000..4ba82cc407d --- /dev/null +++ b/test/image/mocks/stacked_subplots_shared_yaxis.json @@ -0,0 +1,47 @@ +{ + "data": [ + { + "x": [ + 1, + 2, + 3 + ] + }, + { + "x": [ + 2.1, + 2 + ], + "xaxis": "x2" + }, + { + "x": [ + 3, + 2, + 1 + ], + "xaxis": "x3" + } + ], + "layout": { + "xaxis": { + "domain": [ + 0, + 0.3 + ] + }, + "xaxis2": { + "domain": [ + 0.35, + 0.65 + ] + }, + "xaxis3": { + "domain": [ + 0.7, + 1 + ] + }, + "hovermode": "y" + } +} \ No newline at end of file diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index ee1395464ba..150d429eaca 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -282,3 +282,107 @@ describe('hover info', function() { }); }); }); + +describe('hover info on stacked subplots', function() { + 'use strict'; + + afterEach(destroyGraphDiv); + + describe('hover info on stacked subplots with shared x-axis', function() { + var mock = require('@mocks/stacked_coupled_subplots.json'); + + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + }); + + it('responds to hover', function() { + var gd = document.getElementById('graph'); + Plotly.Fx.hover(gd, {xval: 3}, ['xy','xy2','xy3']); + + expect(gd._hoverdata.length).toEqual(2); + + expect(gd._hoverdata[0]).toEqual(jasmine.objectContaining( + { + curveNumber: 1, + pointNumber: 1, + x: 3, + y: 110 + })); + + expect(gd._hoverdata[1]).toEqual(jasmine.objectContaining( + { + curveNumber: 2, + pointNumber: 0, + x: 3, + y: 1000 + })); + + //There should be a single label on the x-axis with the shared x value, 3. + expect(d3.selectAll('g.axistext').size()).toEqual(1); + expect(d3.selectAll('g.axistext').select('text').html()).toEqual('3'); + + //There should be two points being hovered over, in two different traces, one in each plot. + expect(d3.selectAll('g.hovertext').size()).toEqual(2); + var textNodes = d3.selectAll('g.hovertext').selectAll('text'); + + expect(textNodes[0][0].innerHTML).toEqual('trace 1'); + expect(textNodes[0][1].innerHTML).toEqual('110'); + expect(textNodes[1][0].innerHTML).toEqual('trace 2'); + expect(textNodes[1][1].innerHTML).toEqual('1000'); + }); + }); + + describe('hover info on stacked subplots with shared y-axis', function() { + var mock = require('@mocks/stacked_subplots_shared_yaxis.json'); + + beforeEach(function(done) { + Plotly.plot(createGraphDiv(), mock.data, mock.layout).then(done); + }); + + it('responds to hover', function() { + var gd = document.getElementById('graph'); + Plotly.Fx.hover(gd, {yval: 0}, ['xy', 'x2y', 'x3y']); + + expect(gd._hoverdata.length).toEqual(3); + + expect(gd._hoverdata[0]).toEqual(jasmine.objectContaining( + { + curveNumber: 0, + pointNumber: 0, + x: 1, + y: 0 + })); + + expect(gd._hoverdata[1]).toEqual(jasmine.objectContaining( + { + curveNumber: 1, + pointNumber: 0, + x: 2.1, + y: 0 + })); + + expect(gd._hoverdata[2]).toEqual(jasmine.objectContaining( + { + curveNumber: 2, + pointNumber: 0, + x: 3, + y: 0 + })); + + //There should be a single label on the y-axis with the shared y value, 0. + expect(d3.selectAll('g.axistext').size()).toEqual(1); + expect(d3.selectAll('g.axistext').select('text').html()).toEqual('0'); + + //There should be three points being hovered over, in three different traces, one in each plot. + expect(d3.selectAll('g.hovertext').size()).toEqual(3); + var textNodes = d3.selectAll('g.hovertext').selectAll('text'); + + expect(textNodes[0][0].innerHTML).toEqual('trace 0'); + expect(textNodes[0][1].innerHTML).toEqual('1'); + expect(textNodes[1][0].innerHTML).toEqual('trace 1'); + expect(textNodes[1][1].innerHTML).toEqual('2.1'); + expect(textNodes[2][0].innerHTML).toEqual('trace 2'); + expect(textNodes[2][1].innerHTML).toEqual('3'); + }); + }); +}); \ No newline at end of file