From 4c5405d6c8613338d41404461f46efc613f6b877 Mon Sep 17 00:00:00 2001 From: Codrut Date: Sat, 18 Aug 2018 10:14:20 +0300 Subject: [PATCH 1/3] 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. --- src/traces/candlestick/attributes.js | 4 +- src/traces/candlestick/index.js | 2 +- src/traces/ohlc/attributes.js | 14 ++- src/traces/ohlc/hover.js | 129 +++++++++++++++++++++---- src/traces/ohlc/index.js | 2 +- src/traces/ohlc/ohlc_defaults.js | 2 + test/jasmine/tests/hover_label_test.js | 52 ++++++++++ 7 files changed, 183 insertions(+), 22 deletions(-) diff --git a/src/traces/candlestick/attributes.js b/src/traces/candlestick/attributes.js index ac007ffc9af..be7e9cd8b04 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 }), + + hoveron: OHLCattrs.hoveron, }; 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..7e0f0ebc2a0 100644 --- a/src/traces/ohlc/attributes.js +++ b/src/traces/ohlc/attributes.js @@ -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(' ') + }, }; diff --git a/src/traces/ohlc/hover.js b/src/traces/ohlc/hover.js index ef4ff08d7ac..67ce047f6d9 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,24 @@ 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; @@ -29,21 +44,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 +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; @@ -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 += '
' + 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 +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('
'); + 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, + hoverOnOhlc: hoverOnOhlc, + 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..212d92d4403 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('hoveron'); + 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 af16777fbba..b956f35a5d2 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -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() { From 4622752b85a48086921ff4edb7f75d961a16906c Mon Sep 17 00:00:00 2001 From: Codrut Date: Thu, 27 Sep 2018 21:22:18 +0200 Subject: [PATCH 2/3] Rename hoveron to hoverlabel.split. --- src/traces/candlestick/attributes.js | 2 +- src/traces/ohlc/attributes.js | 24 +++++++++++++----------- src/traces/ohlc/hover.js | 14 +++++--------- src/traces/ohlc/ohlc_defaults.js | 2 +- test/jasmine/tests/hover_label_test.js | 6 ++++-- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/traces/candlestick/attributes.js b/src/traces/candlestick/attributes.js index be7e9cd8b04..ceb0c4df45b 100644 --- a/src/traces/candlestick/attributes.js +++ b/src/traces/candlestick/attributes.js @@ -52,5 +52,5 @@ module.exports = { text: OHLCattrs.text, whiskerwidth: extendFlat({}, boxAttrs.whiskerwidth, { dflt: 0 }), - hoveron: OHLCattrs.hoveron, + hoverlabel: OHLCattrs.hoverlabel, }; diff --git a/src/traces/ohlc/attributes.js b/src/traces/ohlc/attributes.js index 7e0f0ebc2a0..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'; @@ -117,15 +118,16 @@ module.exports = { ].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(' ') - }, + 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 67ce047f6d9..c843fdf5951 100644 --- a/src/traces/ohlc/hover.js +++ b/src/traces/ohlc/hover.js @@ -22,16 +22,12 @@ var DIRSYMBOL = { 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); + if(trace.hoverlabel.split) { + return hoverSplit(pointData, xval, yval, hovermode); } - return []; + return hoverOnPoints(pointData, xval, yval, hovermode); } function getClosestPoint(pointData, xval, yval, hovermode) { @@ -94,7 +90,7 @@ function getClosestPoint(pointData, xval, yval, hovermode) { return pointData; } -function hoverOnOhlc(pointData, xval, yval, hovermode) { +function hoverSplit(pointData, xval, yval, hovermode) { var cd = pointData.cd; var ya = pointData.ya; var trace = cd[0].trace; @@ -197,6 +193,6 @@ function hoverOnPoints(pointData, xval, yval, hovermode) { module.exports = { hoverPoints: hoverPoints, - hoverOnOhlc: hoverOnOhlc, + hoverSplit: hoverSplit, hoverOnPoints: hoverOnPoints }; diff --git a/src/traces/ohlc/ohlc_defaults.js b/src/traces/ohlc/ohlc_defaults.js index 212d92d4403..78acaa3a4dc 100644 --- a/src/traces/ohlc/ohlc_defaults.js +++ b/src/traces/ohlc/ohlc_defaults.js @@ -19,7 +19,7 @@ module.exports = function handleOHLC(traceIn, traceOut, coerce, layout) { var low = coerce('low'); var close = coerce('close'); - coerce('hoveron'); + coerce('hoverlabel'); 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 51c1dbac317..79e5c915431 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1054,11 +1054,13 @@ describe('hover info', function() { .then(done); }); - it('shows correct labels in ohlc mode', function(done) { + it('shows correct labels in split mode', function(done) { var pts; Plotly.plot(gd, financeMock({ customdata: [11, 22, 33], - hoveron: 'ohlc' + hoverlabel: { + split: true + } })) .then(function() { gd.on('plotly_hover', function(e) { pts = e.points; }); From fb4a09a09ca8ef90a7ca22a982b2e2a61ad0afd9 Mon Sep 17 00:00:00 2001 From: Codrut Date: Mon, 1 Oct 2018 09:31:48 +0200 Subject: [PATCH 3/3] Fix some errors. --- src/traces/ohlc/ohlc_defaults.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traces/ohlc/ohlc_defaults.js b/src/traces/ohlc/ohlc_defaults.js index 78acaa3a4dc..b1a6a83e10b 100644 --- a/src/traces/ohlc/ohlc_defaults.js +++ b/src/traces/ohlc/ohlc_defaults.js @@ -19,7 +19,7 @@ module.exports = function handleOHLC(traceIn, traceOut, coerce, layout) { var low = coerce('low'); var close = coerce('close'); - coerce('hoverlabel'); + coerce('hoverlabel.split'); var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); handleCalendarDefaults(traceIn, traceOut, ['x'], layout);