Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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.
  • Loading branch information
codrut3 committed Aug 30, 2018
commit 4c5405d6c8613338d41404461f46efc613f6b877
4 changes: 3 additions & 1 deletion src/traces/candlestick/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,7 @@ module.exports = {
decreasing: directionAttrs(OHLCattrs.decreasing.line.color.dflt),

text: OHLCattrs.text,
whiskerwidth: extendFlat({}, boxAttrs.whiskerwidth, { dflt: 0 })
whiskerwidth: extendFlat({}, boxAttrs.whiskerwidth, { dflt: 0 }),

hoveron: OHLCattrs.hoveron,
};
2 changes: 1 addition & 1 deletion src/traces/candlestick/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ module.exports = {
plot: require('../box/plot').plot,
layerName: 'boxlayer',
style: require('../box/style').style,
hoverPoints: require('../ohlc/hover'),
hoverPoints: require('../ohlc/hover').hoverPoints,
selectPoints: require('../ohlc/select')
};
14 changes: 13 additions & 1 deletion src/traces/ohlc/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,17 @@ module.exports = {
'Sets the width of the open/close tick marks',
'relative to the *x* minimal interval.'
].join(' ')
}
},

hoveron: {
valType: 'flaglist',
flags: ['ohlc', 'points'],
dflt: 'points',
role: 'info',
editType: 'style',
description: [
'Do the hover effects show info in separate tooltips',
'or a single tooltip?'
].join(' ')
},
};
129 changes: 111 additions & 18 deletions src/traces/ohlc/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
'use strict';

var Axes = require('../../plots/cartesian/axes');
var Lib = require('../../lib');
var Fx = require('../../components/fx');
var Color = require('../../components/color');
var fillHoverText = require('../scatter/fill_hover_text');
Expand All @@ -18,32 +19,48 @@ var DIRSYMBOL = {
decreasing: '▼'
};

module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
function hoverPoints(pointData, xval, yval, hovermode) {
var cd = pointData.cd;
var trace = cd[0].trace;
var hoveron = trace.hoveron;

if(hoveron.indexOf('ohlc') !== -1) {
return hoverOnOhlc(pointData, xval, yval, hovermode);
}
else if(hoveron.indexOf('points') !== -1) {
return hoverOnPoints(pointData, xval, yval, hovermode);
}

return [];
}

function getClosestPoint(pointData, xval, yval, hovermode) {
var cd = pointData.cd;
var xa = pointData.xa;
var ya = pointData.ya;
var trace = cd[0].trace;
var t = cd[0].t;

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

var hoverPseudoDistance, spikePseudoDistance;

// potentially shift xval for grouped candlesticks
var centerShift = t.bPos || 0;
var x0 = xval - centerShift;
var shiftPos = function(di) { return di.pos + centerShift - xval; };

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

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

function dx(di) {
var pos = di.pos - x0;
var pos = shiftPos(di);
return Fx.inbox(pos - hoverHalfWidth, pos + hoverHalfWidth, hoverPseudoDistance);
}

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

function dxy(di) { return (dx(di) + dy(di)) / 2; }

var distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy);
Fx.getClosest(cd, distfn, pointData);

// skip the rest (for this trace) if we didn't find a close point
if(pointData.index === false) return [];

// we don't make a calcdata point if we're missing any piece (x/o/h/l/c)
// so we need to fix the index here to point to the data arrays
var cdIndex = pointData.index;
var di = cd[cdIndex];
var i = pointData.index = di.i;
if(pointData.index === false) return null;

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

return pointData;
}

function hoverOnOhlc(pointData, xval, yval, hovermode) {
var cd = pointData.cd;
var ya = pointData.ya;
var trace = cd[0].trace;
var t = cd[0].t;
var closeBoxData = [];

var closestPoint = getClosestPoint(pointData, xval, yval, hovermode);
// skip the rest (for this trace) if we didn't find a close point
if(!closestPoint) return [];

var hoverinfo = trace.hoverinfo;
var hoverParts = hoverinfo.split('+');
var isAll = hoverinfo === 'all';
var hasY = isAll || hoverParts.indexOf('y') !== -1;

// similar to hoverOnPoints, we return nothing
// if all or y is not present.
if(!hasY) return [];

var attrs = ['high', 'open', 'close', 'low'];

// several attributes can have the same y-coordinate. We will
// bunch them together in a single text block. For this, we keep
// a dictionary mapping y-coord -> point data.
var usedVals = {};

for(var i = 0; i < attrs.length; i++) {
var attr = attrs[i];

var val = trace[attr][closestPoint.index];
var valPx = ya.c2p(val, true);
var pointData2;
if(val in usedVals) {
pointData2 = usedVals[val];
pointData2.yLabel += '<br>' + t.labels[attr] + Axes.hoverLabelText(ya, val);
}
else {
// copy out to a new object for each new y-value to label
pointData2 = Lib.extendFlat({}, closestPoint);

pointData2.y0 = pointData2.y1 = valPx;
pointData2.yLabelVal = val;
pointData2.yLabel = t.labels[attr] + Axes.hoverLabelText(ya, val);

pointData2.name = '';

closeBoxData.push(pointData2);
usedVals[val] = pointData2;
}
}

return closeBoxData;
}

function hoverOnPoints(pointData, xval, yval, hovermode) {
var cd = pointData.cd;
var ya = pointData.ya;
var trace = cd[0].trace;
var t = cd[0].t;

var closestPoint = getClosestPoint(pointData, xval, yval, hovermode);
// skip the rest (for this trace) if we didn't find a close point
if(!closestPoint) return [];

// we don't make a calcdata point if we're missing any piece (x/o/h/l/c)
// so we need to fix the index here to point to the data arrays
var cdIndex = closestPoint.index;
var di = cd[cdIndex];
var i = closestPoint.index = di.i;
var dir = di.dir;

function getLabelLine(attr) {
return t.labels[attr] + Axes.hoverLabelText(ya, trace[attr][i]);
}
Expand All @@ -99,11 +186,17 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {

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

// this puts the label *and the spike* at the midpoint of the box, ie
// halfway between open and close, not between high and low.
pointData.y0 = pointData.y1 = ya.c2p(di.yc, true);
closestPoint.y0 = closestPoint.y1 = ya.c2p(di.yc, true);

return [closestPoint];
}

return [pointData];
module.exports = {
hoverPoints: hoverPoints,
hoverOnOhlc: hoverOnOhlc,
hoverOnPoints: hoverOnPoints
};
2 changes: 1 addition & 1 deletion src/traces/ohlc/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ module.exports = {
calc: require('./calc').calc,
plot: require('./plot'),
style: require('./style'),
hoverPoints: require('./hover'),
hoverPoints: require('./hover').hoverPoints,
selectPoints: require('./select')
};
2 changes: 2 additions & 0 deletions src/traces/ohlc/ohlc_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ module.exports = function handleOHLC(traceIn, traceOut, coerce, layout) {
var low = coerce('low');
var close = coerce('close');

coerce('hoveron');

var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults');
handleCalendarDefaults(traceIn, traceOut, ['x'], layout);

Expand Down
52 changes: 52 additions & 0 deletions test/jasmine/tests/hover_label_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,58 @@ describe('hover info', function() {
.then(done);
});

it('shows correct labels in ohlc mode', function(done) {
var pts;
Plotly.plot(gd, financeMock({
customdata: [11, 22, 33],
hoveron: 'ohlc'
}))
.then(function() {
gd.on('plotly_hover', function(e) { pts = e.points; });

_hoverNatural(gd, 150, 150);
assertHoverLabelContent({
nums: ['high: 4', 'open: 2', 'close: 3', 'low: 1'],
name: ['', '', '', ''],
axis: 'Jan 2, 2011'
});
})
.then(function() {
expect(pts).toBeDefined();
expect(pts.length).toBe(4);
expect(pts[0]).toEqual(jasmine.objectContaining({
x: '2011-01-02',
high: 4,
customdata: 22,
}));
expect(pts[1]).toEqual(jasmine.objectContaining({
x: '2011-01-02',
open: 2,
customdata: 22,
}));
expect(pts[2]).toEqual(jasmine.objectContaining({
x: '2011-01-02',
close: 3,
customdata: 22,
}));
expect(pts[3]).toEqual(jasmine.objectContaining({
x: '2011-01-02',
low: 1,
customdata: 22,
}));
})
.then(function() {
_hoverNatural(gd, 200, 150);
assertHoverLabelContent({
nums: ['high: 5', 'open: 3', 'close: 2\nlow: 2'],
name: ['', '', ''],
axis: 'Jan 3, 2011'
});
})
.catch(failTest)
.then(done);
});

it('shows text iff text is in hoverinfo', function(done) {
Plotly.plot(gd, financeMock({text: ['A', 'B', 'C']}))
.then(function() {
Expand Down