diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 3f5f80444d5..55c98d78910 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -337,7 +337,7 @@ Plotly.plot = function(gd, data, layout, config) { Registry.getComponentMethod('updatemenus', 'draw')(gd); } - Lib.syncOrAsync([ + var seq = [ Plots.previousPromises, addFrames, drawFramework, @@ -347,8 +347,11 @@ Plotly.plot = function(gd, data, layout, config) { subroutines.layoutStyles, drawAxes, drawData, - finalDraw - ], gd); + finalDraw, + Plots.rehover + ]; + + Lib.syncOrAsync(seq, gd); // even if everything we did was synchronous, return a promise // so that the caller doesn't care which route we took @@ -1201,8 +1204,7 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { if(flags.fullReplot) { seq.push(Plotly.plot); - } - else { + } else { seq.push(Plots.previousPromises); Plots.supplyDefaults(gd); @@ -1211,6 +1213,8 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { if(flags.docolorbars) seq.push(subroutines.doColorBars); } + seq.push(Plots.rehover); + Queue.add(gd, restyle, [gd, specs.undoit, specs.traces], restyle, [gd, specs.redoit, specs.traces] @@ -1695,9 +1699,11 @@ Plotly.relayout = function relayout(gd, astr, val) { } var aobj = {}; - if(typeof astr === 'string') aobj[astr] = val; - else if(Lib.isPlainObject(astr)) aobj = astr; - else { + if(typeof astr === 'string') { + aobj[astr] = val; + } else if(Lib.isPlainObject(astr)) { + aobj = astr; + } else { Lib.warn('Relayout fail.', astr, val); return Promise.reject(); } @@ -1715,8 +1721,7 @@ Plotly.relayout = function relayout(gd, astr, val) { if(flags.layoutReplot) { seq.push(subroutines.layoutReplot); - } - else if(Object.keys(aobj).length) { + } else if(Object.keys(aobj).length) { seq.push(Plots.previousPromises); Plots.supplyDefaults(gd); @@ -1727,6 +1732,8 @@ Plotly.relayout = function relayout(gd, astr, val) { if(flags.docamera) seq.push(subroutines.doCamera); } + seq.push(Plots.rehover); + Queue.add(gd, relayout, [gd, specs.undoit], relayout, [gd, specs.redoit] @@ -2127,6 +2134,8 @@ Plotly.update = function update(gd, traceUpdate, layoutUpdate, traces) { if(relayoutFlags.doCamera) seq.push(subroutines.doCamera); } + seq.push(Plots.rehover); + Queue.add(gd, update, [gd, restyleSpecs.undoit, relayoutSpecs.undoit, restyleSpecs.traces], update, [gd, restyleSpecs.redoit, relayoutSpecs.redoit, restyleSpecs.traces] diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 7fea9cb893e..835873e5e39 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -114,9 +114,20 @@ fx.init = function(gd) { xa._length, ya._length, 'ns', 'ew'); maindrag.onmousemove = function(evt) { + // This is on `gd._fullLayout`, *not* fullLayout because the reference + // changes by the time this is called again. + gd._fullLayout._rehover = function() { + if(gd._fullLayout._hoversubplot === subplot) { + fx.hover(gd, evt, subplot); + } + }; + fx.hover(gd, evt, subplot); - fullLayout._lasthover = maindrag; - fullLayout._hoversubplot = subplot; + + // Note that we have *not* used the cached fullLayout variable here + // since that may be outdated when this is called as a callback later on + gd._fullLayout._lasthover = maindrag; + gd._fullLayout._hoversubplot = subplot; }; /* @@ -129,6 +140,11 @@ fx.init = function(gd) { maindrag.onmouseout = function(evt) { if(gd._dragging) return; + // When the mouse leaves this maindrag, unset the hovered subplot. + // This may cause problems if it leaves the subplot directly *onto* + // another subplot, but that's a tiny corner case at the moment. + gd._fullLayout._hoversubplot = null; + dragElement.unhover(gd, evt); }; diff --git a/src/plots/plots.js b/src/plots/plots.js index 3253aba48f7..3b2e3301586 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1934,7 +1934,7 @@ plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts) } } - var seq = [plots.previousPromises, interruptPreviousTransitions, prepareTransitions, executeTransitions]; + var seq = [plots.previousPromises, interruptPreviousTransitions, prepareTransitions, plots.rehover, executeTransitions]; var transitionStarting = Lib.syncOrAsync(seq, gd); @@ -2057,6 +2057,12 @@ plots.doCalcdata = function(gd, traces) { } }; +plots.rehover = function(gd) { + if(gd._fullLayout._rehover) { + gd._fullLayout._rehover(); + } +}; + plots.generalUpdatePerTraceModule = function(subplot, subplotCalcData, subplotLayout) { var traceHashOld = subplot.traceHash, traceHash = {}, diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 2e76a3ab0e5..3561e6d6082 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -10,6 +10,7 @@ var destroyGraphDiv = require('../assets/destroy_graph_div'); var mouseEvent = require('../assets/mouse_event'); var click = require('../assets/click'); var doubleClick = require('../assets/double_click'); +var fail = require('../assets/fail_test'); describe('hover info', function() { 'use strict'; @@ -811,3 +812,78 @@ describe('hover on fill', function() { }).then(done); }); }); + +describe('hover updates', function() { + 'use strict'; + + afterEach(destroyGraphDiv); + + function assertLabelsCorrect(mousePos, labelPos, labelText) { + return new Promise(function(resolve) { + if(mousePos) { + mouseEvent('mousemove', mousePos[0], mousePos[1]); + } + + setTimeout(function() { + var hoverText = d3.selectAll('g.hovertext'); + if(labelPos) { + expect(hoverText.size()).toEqual(1); + expect(hoverText.text()).toEqual(labelText); + + var transformParts = hoverText.attr('transform').split('('); + expect(transformParts[0]).toEqual('translate'); + var transformCoords = transformParts[1].split(')')[0].split(','); + expect(+transformCoords[0]).toBeCloseTo(labelPos[0], -1, labelText + ':x'); + expect(+transformCoords[1]).toBeCloseTo(labelPos[1], -1, labelText + ':y'); + } else { + expect(hoverText.size()).toEqual(0); + } + + resolve(); + }, constants.HOVERMINTIME); + }); + } + + it('should update the labels on animation', function(done) { + var mock = { + data: [ + {x: [0.5], y: [0.5], showlegend: false}, + {x: [0], y: [0], showlegend: false}, + ], + layout: { + margin: {t: 0, r: 0, b: 0, l: 0}, + width: 200, + height: 200, + xaxis: {range: [0, 1]}, + yaxis: {range: [0, 1]}, + } + }; + + var gd = createGraphDiv(); + Plotly.plot(gd, mock).then(function() { + // The label text gets concatenated together when queried. Such is life. + return assertLabelsCorrect([100, 100], [103, 100], 'trace 00.5'); + }).then(function() { + return Plotly.animate(gd, [{ + data: [{x: [0], y: [0]}, {x: [0.5], y: [0.5]}], + traces: [0, 1], + }], {frame: {redraw: false, duration: 0}}); + }).then(function() { + // No mouse event this time. Just change the data and check the label. + // Ditto on concatenation. This is "trace 1" + "0.5" + return assertLabelsCorrect(null, [103, 100], 'trace 10.5'); + }).then(function() { + // Restyle to move the point out of the window: + return Plotly.relayout(gd, {'xaxis.range': [2, 3]}); + }).then(function() { + // Assert label removed: + return assertLabelsCorrect(null, null); + }).then(function() { + // Move back to the original xaxis range: + return Plotly.relayout(gd, {'xaxis.range': [0, 1]}); + }).then(function() { + // Assert label restored: + return assertLabelsCorrect(null, [103, 100], 'trace 10.5'); + }).catch(fail).then(done); + }); +});