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() {