diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index daa77e3700d..a2df199da32 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -330,12 +330,14 @@ function _hover(gd, evt, subplot, noHoverEvent) { // user specified an array of points to highlight hovermode = 'array'; for(itemnum = 0; itemnum < evt.length; itemnum++) { - cd = gd.calcdata[evt[itemnum].curveNumber||0]; - trace = cd[0].trace; - if(cd[0].trace.hoverinfo !== 'skip') { - searchData.push(cd); - if(trace.orientation === 'h') { - hasOneHorizontalTrace = true; + cd = gd.calcdata[evt[itemnum].curveNumber || 0]; + if(cd) { + trace = cd[0].trace; + if(cd[0].trace.hoverinfo !== 'skip') { + searchData.push(cd); + if(trace.orientation === 'h') { + hasOneHorizontalTrace = true; + } } } } diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index 236bfe2b7ea..0f16db25475 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -100,6 +100,8 @@ function plotBoxAndWhiskers(sel, axes, trace, t) { paths.exit().remove(); paths.each(function(d) { + if(d.empty) return 'M0,0Z'; + var pos = d.pos; var posc = posAxis.c2p(pos + bPos, true) + bPosPxOffset; var pos0 = posAxis.c2p(pos + bPos - bdPos0, true) + bPosPxOffset; diff --git a/src/traces/box/style.js b/src/traces/box/style.js index 1495fb8566f..67fc7cd3b61 100644 --- a/src/traces/box/style.js +++ b/src/traces/box/style.js @@ -32,14 +32,15 @@ function style(gd, cd) { if(trace.type === 'candlestick') { allBoxes.each(function(boxData) { + if(boxData.empty) return; + var thisBox = d3.select(this); var container = trace[boxData.dir]; // dir = 'increasing' or 'decreasing' styleBox(thisBox, container.line.width, container.line.color, container.fillcolor); // TODO: custom selection style for candlesticks thisBox.style('opacity', trace.selectedpoints && !boxData.selected ? 0.3 : 1); }); - } - else { + } else { styleBox(allBoxes, lineWidth, trace.line.color, trace.fillcolor); el.selectAll('path.mean') .style({ diff --git a/src/traces/ohlc/calc.js b/src/traces/ohlc/calc.js index fe60922d525..ac6b69f2543 100644 --- a/src/traces/ohlc/calc.js +++ b/src/traces/ohlc/calc.js @@ -89,6 +89,8 @@ function calcCommon(gd, trace, x, ya, ptFunc) { if(hasTextArray) pt.tx = trace.text[i]; cd.push(pt); + } else { + cd.push({empty: true}); } } diff --git a/src/traces/ohlc/hover.js b/src/traces/ohlc/hover.js index 3d2e82ea06b..c8bd1f896dd 100644 --- a/src/traces/ohlc/hover.js +++ b/src/traces/ohlc/hover.js @@ -61,7 +61,9 @@ function getClosestPoint(pointData, xval, yval, hovermode) { } function dy(di) { - return Fx.inbox(di[minAttr] - yval, di[maxAttr] - yval, hoverPseudoDistance); + var min = di[minAttr]; + var max = di[maxAttr]; + return min === max || Fx.inbox(min - yval, max - yval, hoverPseudoDistance); } function dxy(di) { return (dx(di) + dy(di)) / 2; } diff --git a/src/traces/ohlc/plot.js b/src/traces/ohlc/plot.js index 85adb7eaada..a6a8bfe3a39 100644 --- a/src/traces/ohlc/plot.js +++ b/src/traces/ohlc/plot.js @@ -37,6 +37,8 @@ module.exports = function plot(gd, plotinfo, cdOHLC, ohlcLayer) { paths.exit().remove(); paths.attr('d', function(d) { + if(d.empty) return 'M0,0Z'; + var x = xa.c2p(d.pos, true); var xo = xa.c2p(d.pos - tickLen, true); var xc = xa.c2p(d.pos + tickLen, true); diff --git a/src/traces/ohlc/style.js b/src/traces/ohlc/style.js index db85aea96bb..00a8c0552d1 100644 --- a/src/traces/ohlc/style.js +++ b/src/traces/ohlc/style.js @@ -23,6 +23,8 @@ module.exports = function style(gd, cd) { var trace = d[0].trace; d3.select(this).selectAll('path').each(function(di) { + if(di.empty) return; + var dirLine = trace[di.dir].line; d3.select(this) .style('fill', 'none') diff --git a/test/jasmine/tests/finance_test.js b/test/jasmine/tests/finance_test.js index 9d294eae626..f6babcaabc4 100644 --- a/test/jasmine/tests/finance_test.js +++ b/test/jasmine/tests/finance_test.js @@ -6,6 +6,8 @@ var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var supplyAllDefaults = require('../assets/supply_defaults'); +var hover = require('../assets/hover'); +var assertHoverLabelContent = require('../assets/custom_assertions').assertHoverLabelContent; var failTest = require('../assets/fail_test'); var mock0 = { @@ -426,15 +428,22 @@ describe('finance charts calc', function() { addJunk(trace1); var out = _calcRaw([trace0, trace1]); - var indices = [0, 1, 2, 3, 4, 5, 6, 7]; + var indices = [0, 1, 2, 3, 4, 5, 6, 7, undefined, undefined, undefined, undefined]; var i = 'increasing'; var d = 'decreasing'; - var directions = [i, d, d, i, d, i, d, i]; + var directions = [i, d, d, i, d, i, d, i, undefined, undefined, undefined, undefined]; + var empties = [ + undefined, undefined, undefined, undefined, + undefined, undefined, undefined, undefined, + true, true, true, true + ]; expect(mapGet(out[0], 'pos')).toEqual(indices); expect(mapGet(out[0], 'dir')).toEqual(directions); expect(mapGet(out[1], 'pos')).toEqual(indices); expect(mapGet(out[1], 'dir')).toEqual(directions); + expect(mapGet(out[0], 'empty')).toEqual(empties); + expect(mapGet(out[1], 'empty')).toEqual(empties); }); it('should work with *filter* transforms', function() { @@ -1104,8 +1113,8 @@ describe('finance trace hover:', function() { margin: {t: 0, b: 0, l: 0, r: 0, pad: 0} }, specs.layout || {}); - var xval = 'xval' in specs ? specs.xvals : 0; - var yval = 'yval' in specs ? specs.yvals : 1; + var xval = 'xval' in specs ? specs.xval : 0; + var yval = 'yval' in specs ? specs.yval : 1; var hovermode = layout.hovermode || 'x'; return Plotly.plot(gd, data, layout).then(function() { @@ -1127,7 +1136,6 @@ describe('finance trace hover:', function() { var actual = results[0]; var exp = specs.exp; - for(var k in exp) { var msg = '- key ' + k; expect(actual[k]).toBe(exp[k], msg); @@ -1178,6 +1186,21 @@ describe('finance trace hover:', function() { exp: { extraText: 'A' } + }, { + type: type, + desc: 'when high === low in *closest* mode', + traces: [{ + high: [6, null, 7, 8], + close: [4, null, 7, 8], + low: [5, null, 7, 8], + open: [3, null, 7, 8] + }], + layout: {hovermode: 'closest'}, + xval: 2, + yval: 6.9, + exp: { + extraText: 'open: 7
high: 7
low: 7
close: 7 ▲' + } }] .forEach(function(specs) { it('should generate correct hover labels ' + type + ' - ' + specs.desc, function(done) { @@ -1186,3 +1209,65 @@ describe('finance trace hover:', function() { }); }); }); + +describe('finance trace hover via Fx.hover():', function() { + var gd; + + beforeEach(function() { gd = createGraphDiv(); }); + + afterEach(destroyGraphDiv); + + ['candlestick', 'ohlc'].forEach(function(type) { + it('should pick correct ' + type + ' item', function(done) { + var x = ['hover ok!', 'time2', 'hover off by 1', 'time4']; + + Plotly.newPlot(gd, [{ + x: x, + high: [6, null, 7, 8], + close: [4, null, 7, 8], + low: [5, null, 7, 8], + open: [3, null, 7, 8], + type: type + }, { + x: x, + y: [1, null, 2, 3], + type: 'bar' + }], { + xaxis: { rangeslider: {visible: false} }, + width: 500, + height: 500 + }) + .then(function() { + gd.on('plotly_hover', function(d) { + Plotly.Fx.hover(gd, [ + {curveNumber: 0, pointNumber: d.points[0].pointNumber}, + {curveNumber: 1, pointNumber: d.points[0].pointNumber} + ]); + }); + }) + .then(function() { hover(281, 252); }) + .then(function() { + assertHoverLabelContent({ + nums: [ + 'hover off by 1\nopen: 7\nhigh: 7\nlow: 7\nclose: 7 ▲', + '(hover off by 1, 2)' + ], + name: ['trace 0', 'trace 1'] + }, 'hover over 3rd items (aka 2nd visible items)'); + }) + .then(function() { + Lib.clearThrottle(); + return Plotly.react(gd, [gd.data[0]], gd.layout); + }) + .then(function() { hover(281, 252); }) + .then(function() { + assertHoverLabelContent({ + nums: 'hover off by 1\nopen: 7\nhigh: 7\nlow: 7\nclose: 7 ▲', + name: '' + }, 'after removing 2nd trace'); + }) + .catch(failTest) + .then(done); + }); + }); +});