'use strict'; var Axes = require('../../plots/cartesian/axes'); var Lib = require('../../lib'); var Fx = require('../../components/fx'); var Color = require('../../components/color'); var fillText = require('../../lib').fillText; var delta = require('../../constants/delta.js'); var DIRSYMBOL = { increasing: delta.INCREASING.SYMBOL, decreasing: delta.DECREASING.SYMBOL }; 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 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 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 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]))); hoverPseudoDistance = pointData.maxHoverDistance - pseudoDistance; spikePseudoDistance = pointData.maxSpikeDistance - pseudoDistance; function dx(di) { var pos = shiftPos(di); return Fx.inbox(pos - hoverHalfWidth, pos + hoverHalfWidth, hoverPseudoDistance); } function dy(di) { var min = di[minAttr]; var max = di[maxAttr]; return min === max || Fx.inbox(min - yval, max - yval, hoverPseudoDistance); } function dxy(di) { return (dx(di) + dy(di)) / 2; } var distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy); Fx.getClosest(cd, distfn, pointData); if(pointData.index === false) return null; var di = cd[pointData.index]; if(di.empty) return null; var dir = di.dir; var container = trace[dir]; var lc = container.line.color; if(Color.opacity(lc) && container.line.width) pointData.color = lc; else pointData.color = container.fillcolor; pointData.x0 = xa.c2p(di.pos + centerShift - displayHalfWidth, true); pointData.x1 = xa.c2p(di.pos + centerShift + displayHalfWidth, true); pointData.xLabelVal = di.orig_p !== undefined ? di.orig_p : di.pos; 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 cdIndex = closestPoint.index; var di = cd[cdIndex]; var hoverinfo = di.hi || 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, trace.yhoverformat); } 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, trace.yhoverformat); 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], trace.yhoverformat); } var hoverinfo = di.hi || trace.hoverinfo; var hoverParts = hoverinfo.split('+'); var isAll = hoverinfo === 'all'; var hasY = isAll || hoverParts.indexOf('y') !== -1; var hasText = isAll || hoverParts.indexOf('text') !== -1; var textParts = hasY ? [ getLabelLine('open'), getLabelLine('high'), getLabelLine('low'), getLabelLine('close') + ' ' + DIRSYMBOL[dir] ] : []; if(hasText) fillText(di, trace, textParts); // don't make .yLabelVal or .text, since we're managing hoverinfo // put it all in .extraText 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. closestPoint.y0 = closestPoint.y1 = ya.c2p(di.yc, true); return [closestPoint]; } module.exports = { hoverPoints: hoverPoints, hoverSplit: hoverSplit, hoverOnPoints: hoverOnPoints };