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);
+ });
+ });
+});