Skip to content

Commit

Permalink
Merge pull request #2959 from codrut3/ohlc_hover
Browse files Browse the repository at this point in the history
Separate tooltips for candlestick hover
  • Loading branch information
etpinard authored Oct 2, 2018
2 parents 9772ef6 + fb4a09a commit b02221f
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 22 deletions.
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 }),

hoverlabel: OHLCattrs.hoverlabel,
};
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')
};
16 changes: 15 additions & 1 deletion src/traces/ohlc/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
var extendFlat = require('../../lib').extendFlat;
var scatterAttrs = require('../scatter/attributes');
var dash = require('../../components/drawing/attributes').dash;
var fxAttrs = require('../../components/fx/attributes');

var INCREASING_COLOR = '#3D9970';
var DECREASING_COLOR = '#FF4136';
Expand Down Expand Up @@ -115,5 +116,18 @@ module.exports = {
'Sets the width of the open/close tick marks',
'relative to the *x* minimal interval.'
].join(' ')
}
},

hoverlabel: extendFlat({}, fxAttrs.hoverlabel, {
split: {
valType: 'boolean',
role: 'info',
dflt: false,
editType: 'style',
description: [
'Show hover information (open, close, high, low) in',
'separate labels.'
].join(' ')
}
}),
};
125 changes: 107 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,44 @@ 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;

if(trace.hoverlabel.split) {
return hoverSplit(pointData, xval, yval, hovermode);
}

return hoverOnPoints(pointData, xval, yval, hovermode);
}

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 +65,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 +87,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 hoverSplit(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 +182,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,
hoverSplit: hoverSplit,
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('hoverlabel.split');

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

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

it('shows correct labels in split mode', function(done) {
var pts;
Plotly.plot(gd, financeMock({
customdata: [11, 22, 33],
hoverlabel: {
split: true
}
}))
.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

0 comments on commit b02221f

Please sign in to comment.