Skip to content

Commit 4c5405d

Browse files
committed
Add the option to display candlestick hoverinfo in separate tooltips.
This commit adds a new attribute to ohlc and candlestick figures called 'hoveron' (as in box hover). Setting 'hoveron' to 'ohlc' shows at most 4 tooltips, for high, open, close and low. If several values should appear at the same coordinate, they are shown together in a single tooltip.
1 parent def6aa5 commit 4c5405d

File tree

7 files changed

+183
-22
lines changed

7 files changed

+183
-22
lines changed

src/traces/candlestick/attributes.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,7 @@ module.exports = {
5050
decreasing: directionAttrs(OHLCattrs.decreasing.line.color.dflt),
5151

5252
text: OHLCattrs.text,
53-
whiskerwidth: extendFlat({}, boxAttrs.whiskerwidth, { dflt: 0 })
53+
whiskerwidth: extendFlat({}, boxAttrs.whiskerwidth, { dflt: 0 }),
54+
55+
hoveron: OHLCattrs.hoveron,
5456
};

src/traces/candlestick/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,6 @@ module.exports = {
3838
plot: require('../box/plot').plot,
3939
layerName: 'boxlayer',
4040
style: require('../box/style').style,
41-
hoverPoints: require('../ohlc/hover'),
41+
hoverPoints: require('../ohlc/hover').hoverPoints,
4242
selectPoints: require('../ohlc/select')
4343
};

src/traces/ohlc/attributes.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,5 +115,17 @@ module.exports = {
115115
'Sets the width of the open/close tick marks',
116116
'relative to the *x* minimal interval.'
117117
].join(' ')
118-
}
118+
},
119+
120+
hoveron: {
121+
valType: 'flaglist',
122+
flags: ['ohlc', 'points'],
123+
dflt: 'points',
124+
role: 'info',
125+
editType: 'style',
126+
description: [
127+
'Do the hover effects show info in separate tooltips',
128+
'or a single tooltip?'
129+
].join(' ')
130+
},
119131
};

src/traces/ohlc/hover.js

Lines changed: 111 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
'use strict';
1010

1111
var Axes = require('../../plots/cartesian/axes');
12+
var Lib = require('../../lib');
1213
var Fx = require('../../components/fx');
1314
var Color = require('../../components/color');
1415
var fillHoverText = require('../scatter/fill_hover_text');
@@ -18,32 +19,48 @@ var DIRSYMBOL = {
1819
decreasing: '▼'
1920
};
2021

21-
module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
22+
function hoverPoints(pointData, xval, yval, hovermode) {
23+
var cd = pointData.cd;
24+
var trace = cd[0].trace;
25+
var hoveron = trace.hoveron;
26+
27+
if(hoveron.indexOf('ohlc') !== -1) {
28+
return hoverOnOhlc(pointData, xval, yval, hovermode);
29+
}
30+
else if(hoveron.indexOf('points') !== -1) {
31+
return hoverOnPoints(pointData, xval, yval, hovermode);
32+
}
33+
34+
return [];
35+
}
36+
37+
function getClosestPoint(pointData, xval, yval, hovermode) {
2238
var cd = pointData.cd;
2339
var xa = pointData.xa;
24-
var ya = pointData.ya;
2540
var trace = cd[0].trace;
2641
var t = cd[0].t;
2742

2843
var type = trace.type;
2944
var minAttr = type === 'ohlc' ? 'l' : 'min';
3045
var maxAttr = type === 'ohlc' ? 'h' : 'max';
3146

47+
var hoverPseudoDistance, spikePseudoDistance;
48+
3249
// potentially shift xval for grouped candlesticks
3350
var centerShift = t.bPos || 0;
34-
var x0 = xval - centerShift;
51+
var shiftPos = function(di) { return di.pos + centerShift - xval; };
3552

3653
// ohlc and candlestick call displayHalfWidth different things...
3754
var displayHalfWidth = t.bdPos || t.tickLen;
3855
var hoverHalfWidth = t.wHover;
3956

40-
// if two items are overlaying, let the narrowest one win
57+
// if two figures are overlaying, let the narrowest one win
4158
var pseudoDistance = Math.min(1, displayHalfWidth / Math.abs(xa.r2c(xa.range[1]) - xa.r2c(xa.range[0])));
42-
var hoverPseudoDistance = pointData.maxHoverDistance - pseudoDistance;
43-
var spikePseudoDistance = pointData.maxSpikeDistance - pseudoDistance;
59+
hoverPseudoDistance = pointData.maxHoverDistance - pseudoDistance;
60+
spikePseudoDistance = pointData.maxSpikeDistance - pseudoDistance;
4461

4562
function dx(di) {
46-
var pos = di.pos - x0;
63+
var pos = shiftPos(di);
4764
return Fx.inbox(pos - hoverHalfWidth, pos + hoverHalfWidth, hoverPseudoDistance);
4865
}
4966

@@ -52,18 +69,13 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
5269
}
5370

5471
function dxy(di) { return (dx(di) + dy(di)) / 2; }
72+
5573
var distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy);
5674
Fx.getClosest(cd, distfn, pointData);
5775

58-
// skip the rest (for this trace) if we didn't find a close point
59-
if(pointData.index === false) return [];
60-
61-
// we don't make a calcdata point if we're missing any piece (x/o/h/l/c)
62-
// so we need to fix the index here to point to the data arrays
63-
var cdIndex = pointData.index;
64-
var di = cd[cdIndex];
65-
var i = pointData.index = di.i;
76+
if(pointData.index === false) return null;
6677

78+
var di = cd[pointData.index];
6779
var dir = di.dir;
6880
var container = trace[dir];
6981
var lc = container.line.color;
@@ -79,6 +91,81 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
7991
pointData.spikeDistance = dxy(di) * spikePseudoDistance / hoverPseudoDistance;
8092
pointData.xSpike = xa.c2p(di.pos, true);
8193

94+
return pointData;
95+
}
96+
97+
function hoverOnOhlc(pointData, xval, yval, hovermode) {
98+
var cd = pointData.cd;
99+
var ya = pointData.ya;
100+
var trace = cd[0].trace;
101+
var t = cd[0].t;
102+
var closeBoxData = [];
103+
104+
var closestPoint = getClosestPoint(pointData, xval, yval, hovermode);
105+
// skip the rest (for this trace) if we didn't find a close point
106+
if(!closestPoint) return [];
107+
108+
var hoverinfo = trace.hoverinfo;
109+
var hoverParts = hoverinfo.split('+');
110+
var isAll = hoverinfo === 'all';
111+
var hasY = isAll || hoverParts.indexOf('y') !== -1;
112+
113+
// similar to hoverOnPoints, we return nothing
114+
// if all or y is not present.
115+
if(!hasY) return [];
116+
117+
var attrs = ['high', 'open', 'close', 'low'];
118+
119+
// several attributes can have the same y-coordinate. We will
120+
// bunch them together in a single text block. For this, we keep
121+
// a dictionary mapping y-coord -> point data.
122+
var usedVals = {};
123+
124+
for(var i = 0; i < attrs.length; i++) {
125+
var attr = attrs[i];
126+
127+
var val = trace[attr][closestPoint.index];
128+
var valPx = ya.c2p(val, true);
129+
var pointData2;
130+
if(val in usedVals) {
131+
pointData2 = usedVals[val];
132+
pointData2.yLabel += '<br>' + t.labels[attr] + Axes.hoverLabelText(ya, val);
133+
}
134+
else {
135+
// copy out to a new object for each new y-value to label
136+
pointData2 = Lib.extendFlat({}, closestPoint);
137+
138+
pointData2.y0 = pointData2.y1 = valPx;
139+
pointData2.yLabelVal = val;
140+
pointData2.yLabel = t.labels[attr] + Axes.hoverLabelText(ya, val);
141+
142+
pointData2.name = '';
143+
144+
closeBoxData.push(pointData2);
145+
usedVals[val] = pointData2;
146+
}
147+
}
148+
149+
return closeBoxData;
150+
}
151+
152+
function hoverOnPoints(pointData, xval, yval, hovermode) {
153+
var cd = pointData.cd;
154+
var ya = pointData.ya;
155+
var trace = cd[0].trace;
156+
var t = cd[0].t;
157+
158+
var closestPoint = getClosestPoint(pointData, xval, yval, hovermode);
159+
// skip the rest (for this trace) if we didn't find a close point
160+
if(!closestPoint) return [];
161+
162+
// we don't make a calcdata point if we're missing any piece (x/o/h/l/c)
163+
// so we need to fix the index here to point to the data arrays
164+
var cdIndex = closestPoint.index;
165+
var di = cd[cdIndex];
166+
var i = closestPoint.index = di.i;
167+
var dir = di.dir;
168+
82169
function getLabelLine(attr) {
83170
return t.labels[attr] + Axes.hoverLabelText(ya, trace[attr][i]);
84171
}
@@ -99,11 +186,17 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
99186

100187
// don't make .yLabelVal or .text, since we're managing hoverinfo
101188
// put it all in .extraText
102-
pointData.extraText = textParts.join('<br>');
189+
closestPoint.extraText = textParts.join('<br>');
103190

104191
// this puts the label *and the spike* at the midpoint of the box, ie
105192
// halfway between open and close, not between high and low.
106-
pointData.y0 = pointData.y1 = ya.c2p(di.yc, true);
193+
closestPoint.y0 = closestPoint.y1 = ya.c2p(di.yc, true);
194+
195+
return [closestPoint];
196+
}
107197

108-
return [pointData];
198+
module.exports = {
199+
hoverPoints: hoverPoints,
200+
hoverOnOhlc: hoverOnOhlc,
201+
hoverOnPoints: hoverOnPoints
109202
};

src/traces/ohlc/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ module.exports = {
3434
calc: require('./calc').calc,
3535
plot: require('./plot'),
3636
style: require('./style'),
37-
hoverPoints: require('./hover'),
37+
hoverPoints: require('./hover').hoverPoints,
3838
selectPoints: require('./select')
3939
};

src/traces/ohlc/ohlc_defaults.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ module.exports = function handleOHLC(traceIn, traceOut, coerce, layout) {
1919
var low = coerce('low');
2020
var close = coerce('close');
2121

22+
coerce('hoveron');
23+
2224
var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults');
2325
handleCalendarDefaults(traceIn, traceOut, ['x'], layout);
2426

test/jasmine/tests/hover_label_test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,6 +1051,58 @@ describe('hover info', function() {
10511051
.then(done);
10521052
});
10531053

1054+
it('shows correct labels in ohlc mode', function(done) {
1055+
var pts;
1056+
Plotly.plot(gd, financeMock({
1057+
customdata: [11, 22, 33],
1058+
hoveron: 'ohlc'
1059+
}))
1060+
.then(function() {
1061+
gd.on('plotly_hover', function(e) { pts = e.points; });
1062+
1063+
_hoverNatural(gd, 150, 150);
1064+
assertHoverLabelContent({
1065+
nums: ['high: 4', 'open: 2', 'close: 3', 'low: 1'],
1066+
name: ['', '', '', ''],
1067+
axis: 'Jan 2, 2011'
1068+
});
1069+
})
1070+
.then(function() {
1071+
expect(pts).toBeDefined();
1072+
expect(pts.length).toBe(4);
1073+
expect(pts[0]).toEqual(jasmine.objectContaining({
1074+
x: '2011-01-02',
1075+
high: 4,
1076+
customdata: 22,
1077+
}));
1078+
expect(pts[1]).toEqual(jasmine.objectContaining({
1079+
x: '2011-01-02',
1080+
open: 2,
1081+
customdata: 22,
1082+
}));
1083+
expect(pts[2]).toEqual(jasmine.objectContaining({
1084+
x: '2011-01-02',
1085+
close: 3,
1086+
customdata: 22,
1087+
}));
1088+
expect(pts[3]).toEqual(jasmine.objectContaining({
1089+
x: '2011-01-02',
1090+
low: 1,
1091+
customdata: 22,
1092+
}));
1093+
})
1094+
.then(function() {
1095+
_hoverNatural(gd, 200, 150);
1096+
assertHoverLabelContent({
1097+
nums: ['high: 5', 'open: 3', 'close: 2\nlow: 2'],
1098+
name: ['', '', ''],
1099+
axis: 'Jan 3, 2011'
1100+
});
1101+
})
1102+
.catch(failTest)
1103+
.then(done);
1104+
});
1105+
10541106
it('shows text iff text is in hoverinfo', function(done) {
10551107
Plotly.plot(gd, financeMock({text: ['A', 'B', 'C']}))
10561108
.then(function() {

0 commit comments

Comments
 (0)