Skip to content

Commit b02221f

Browse files
authored
Merge pull request #2959 from codrut3/ohlc_hover
Separate tooltips for candlestick hover
2 parents 9772ef6 + fb4a09a commit b02221f

File tree

7 files changed

+183
-22
lines changed

7 files changed

+183
-22
lines changed

src/traces/candlestick/attributes.js

+3-1
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+
hoverlabel: OHLCattrs.hoverlabel,
5456
};

src/traces/candlestick/index.js

+1-1
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

+15-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
var extendFlat = require('../../lib').extendFlat;
1313
var scatterAttrs = require('../scatter/attributes');
1414
var dash = require('../../components/drawing/attributes').dash;
15+
var fxAttrs = require('../../components/fx/attributes');
1516

1617
var INCREASING_COLOR = '#3D9970';
1718
var DECREASING_COLOR = '#FF4136';
@@ -115,5 +116,18 @@ module.exports = {
115116
'Sets the width of the open/close tick marks',
116117
'relative to the *x* minimal interval.'
117118
].join(' ')
118-
}
119+
},
120+
121+
hoverlabel: extendFlat({}, fxAttrs.hoverlabel, {
122+
split: {
123+
valType: 'boolean',
124+
role: 'info',
125+
dflt: false,
126+
editType: 'style',
127+
description: [
128+
'Show hover information (open, close, high, low) in',
129+
'separate labels.'
130+
].join(' ')
131+
}
132+
}),
119133
};

src/traces/ohlc/hover.js

+107-18
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,44 @@ 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+
26+
if(trace.hoverlabel.split) {
27+
return hoverSplit(pointData, xval, yval, hovermode);
28+
}
29+
30+
return hoverOnPoints(pointData, xval, yval, hovermode);
31+
}
32+
33+
function getClosestPoint(pointData, xval, yval, hovermode) {
2234
var cd = pointData.cd;
2335
var xa = pointData.xa;
24-
var ya = pointData.ya;
2536
var trace = cd[0].trace;
2637
var t = cd[0].t;
2738

2839
var type = trace.type;
2940
var minAttr = type === 'ohlc' ? 'l' : 'min';
3041
var maxAttr = type === 'ohlc' ? 'h' : 'max';
3142

43+
var hoverPseudoDistance, spikePseudoDistance;
44+
3245
// potentially shift xval for grouped candlesticks
3346
var centerShift = t.bPos || 0;
34-
var x0 = xval - centerShift;
47+
var shiftPos = function(di) { return di.pos + centerShift - xval; };
3548

3649
// ohlc and candlestick call displayHalfWidth different things...
3750
var displayHalfWidth = t.bdPos || t.tickLen;
3851
var hoverHalfWidth = t.wHover;
3952

40-
// if two items are overlaying, let the narrowest one win
53+
// if two figures are overlaying, let the narrowest one win
4154
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;
55+
hoverPseudoDistance = pointData.maxHoverDistance - pseudoDistance;
56+
spikePseudoDistance = pointData.maxSpikeDistance - pseudoDistance;
4457

4558
function dx(di) {
46-
var pos = di.pos - x0;
59+
var pos = shiftPos(di);
4760
return Fx.inbox(pos - hoverHalfWidth, pos + hoverHalfWidth, hoverPseudoDistance);
4861
}
4962

@@ -52,18 +65,13 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
5265
}
5366

5467
function dxy(di) { return (dx(di) + dy(di)) / 2; }
68+
5569
var distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy);
5670
Fx.getClosest(cd, distfn, pointData);
5771

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;
72+
if(pointData.index === false) return null;
6673

74+
var di = cd[pointData.index];
6775
var dir = di.dir;
6876
var container = trace[dir];
6977
var lc = container.line.color;
@@ -79,6 +87,81 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
7987
pointData.spikeDistance = dxy(di) * spikePseudoDistance / hoverPseudoDistance;
8088
pointData.xSpike = xa.c2p(di.pos, true);
8189

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

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

104187
// this puts the label *and the spike* at the midpoint of the box, ie
105188
// halfway between open and close, not between high and low.
106-
pointData.y0 = pointData.y1 = ya.c2p(di.yc, true);
189+
closestPoint.y0 = closestPoint.y1 = ya.c2p(di.yc, true);
190+
191+
return [closestPoint];
192+
}
107193

108-
return [pointData];
194+
module.exports = {
195+
hoverPoints: hoverPoints,
196+
hoverSplit: hoverSplit,
197+
hoverOnPoints: hoverOnPoints
109198
};

src/traces/ohlc/index.js

+1-1
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

+2
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('hoverlabel.split');
23+
2224
var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults');
2325
handleCalendarDefaults(traceIn, traceOut, ['x'], layout);
2426

test/jasmine/tests/hover_label_test.js

+54
Original file line numberDiff line numberDiff line change
@@ -1154,6 +1154,60 @@ describe('hover info', function() {
11541154
.then(done);
11551155
});
11561156

1157+
it('shows correct labels in split mode', function(done) {
1158+
var pts;
1159+
Plotly.plot(gd, financeMock({
1160+
customdata: [11, 22, 33],
1161+
hoverlabel: {
1162+
split: true
1163+
}
1164+
}))
1165+
.then(function() {
1166+
gd.on('plotly_hover', function(e) { pts = e.points; });
1167+
1168+
_hoverNatural(gd, 150, 150);
1169+
assertHoverLabelContent({
1170+
nums: ['high: 4', 'open: 2', 'close: 3', 'low: 1'],
1171+
name: ['', '', '', ''],
1172+
axis: 'Jan 2, 2011'
1173+
});
1174+
})
1175+
.then(function() {
1176+
expect(pts).toBeDefined();
1177+
expect(pts.length).toBe(4);
1178+
expect(pts[0]).toEqual(jasmine.objectContaining({
1179+
x: '2011-01-02',
1180+
high: 4,
1181+
customdata: 22,
1182+
}));
1183+
expect(pts[1]).toEqual(jasmine.objectContaining({
1184+
x: '2011-01-02',
1185+
open: 2,
1186+
customdata: 22,
1187+
}));
1188+
expect(pts[2]).toEqual(jasmine.objectContaining({
1189+
x: '2011-01-02',
1190+
close: 3,
1191+
customdata: 22,
1192+
}));
1193+
expect(pts[3]).toEqual(jasmine.objectContaining({
1194+
x: '2011-01-02',
1195+
low: 1,
1196+
customdata: 22,
1197+
}));
1198+
})
1199+
.then(function() {
1200+
_hoverNatural(gd, 200, 150);
1201+
assertHoverLabelContent({
1202+
nums: ['high: 5', 'open: 3', 'close: 2\nlow: 2'],
1203+
name: ['', '', ''],
1204+
axis: 'Jan 3, 2011'
1205+
});
1206+
})
1207+
.catch(failTest)
1208+
.then(done);
1209+
});
1210+
11571211
it('shows text iff text is in hoverinfo', function(done) {
11581212
Plotly.plot(gd, financeMock({text: ['A', 'B', 'C']}))
11591213
.then(function() {

0 commit comments

Comments
 (0)