diff --git a/src/traces/candlestick/attributes.js b/src/traces/candlestick/attributes.js
index ac007ffc9af..ceb0c4df45b 100644
--- a/src/traces/candlestick/attributes.js
+++ b/src/traces/candlestick/attributes.js
@@ -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,
};
diff --git a/src/traces/candlestick/index.js b/src/traces/candlestick/index.js
index 3ec580002f0..7cfb59d8111 100644
--- a/src/traces/candlestick/index.js
+++ b/src/traces/candlestick/index.js
@@ -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')
};
diff --git a/src/traces/ohlc/attributes.js b/src/traces/ohlc/attributes.js
index eaf5101a20f..cff26eba859 100644
--- a/src/traces/ohlc/attributes.js
+++ b/src/traces/ohlc/attributes.js
@@ -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';
@@ -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(' ')
+ }
+ }),
};
diff --git a/src/traces/ohlc/hover.js b/src/traces/ohlc/hover.js
index ef4ff08d7ac..c843fdf5951 100644
--- a/src/traces/ohlc/hover.js
+++ b/src/traces/ohlc/hover.js
@@ -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');
@@ -18,10 +19,20 @@ 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;
@@ -29,21 +40,23 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
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);
}
@@ -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;
@@ -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 += '
' + 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]);
}
@@ -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('
');
+ closestPoint.extraText = textParts.join('
');
// 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
};
diff --git a/src/traces/ohlc/index.js b/src/traces/ohlc/index.js
index 8d116b066a8..58a5242644e 100644
--- a/src/traces/ohlc/index.js
+++ b/src/traces/ohlc/index.js
@@ -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')
};
diff --git a/src/traces/ohlc/ohlc_defaults.js b/src/traces/ohlc/ohlc_defaults.js
index 65c9fe14e0d..b1a6a83e10b 100644
--- a/src/traces/ohlc/ohlc_defaults.js
+++ b/src/traces/ohlc/ohlc_defaults.js
@@ -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);
diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js
index 75e963c78d9..f71777d1eca 100644
--- a/test/jasmine/tests/hover_label_test.js
+++ b/test/jasmine/tests/hover_label_test.js
@@ -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() {