Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate tooltips for candlestick hover #2959

Merged
merged 5 commits into from
Oct 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
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
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