'use strict'; var d3 = require('@plotly/d3'); var isNumeric = require('fast-isnumeric'); var tinycolor = require('tinycolor2'); var Lib = require('../../lib'); var pushUnique = Lib.pushUnique; var strTranslate = Lib.strTranslate; var strRotate = Lib.strRotate; var Events = require('../../lib/events'); var svgTextUtils = require('../../lib/svg_text_utils'); var overrideCursor = require('../../lib/override_cursor'); var Drawing = require('../drawing'); var Color = require('../color'); var dragElement = require('../dragelement'); var Axes = require('../../plots/cartesian/axes'); var zindexSeparator = require('../../plots/cartesian/constants').zindexSeparator; var Registry = require('../../registry'); var helpers = require('./helpers'); var constants = require('./constants'); var legendSupplyDefaults = require('../legend/defaults'); var legendDraw = require('../legend/draw'); // hover labels for multiple horizontal bars get tilted by some angle, // then need to be offset differently if they overlap var YANGLE = constants.YANGLE; var YA_RADIANS = Math.PI * YANGLE / 180; // expansion of projected height var YFACTOR = 1 / Math.sin(YA_RADIANS); // to make the appropriate post-rotation x offset, // you need both x and y offsets var YSHIFTX = Math.cos(YA_RADIANS); var YSHIFTY = Math.sin(YA_RADIANS); // size and display constants for hover text var HOVERARROWSIZE = constants.HOVERARROWSIZE; var HOVERTEXTPAD = constants.HOVERTEXTPAD; var multipleHoverPoints = { box: true, ohlc: true, violin: true, candlestick: true }; var cartesianScatterPoints = { scatter: true, scattergl: true, splom: true }; function distanceSort(a, b) { return a.distance - b.distance; } // fx.hover: highlight data on hover // evt can be a mousemove event, or an object with data about what points // to hover on // {xpx,ypx[,hovermode]} - pixel locations from top left // (with optional overriding hovermode) // {xval,yval[,hovermode]} - data values // [{curveNumber,(pointNumber|xval and/or yval)}] - // array of specific points to highlight // pointNumber is a single integer if gd.data[curveNumber] is 1D, // or a two-element array if it's 2D // xval and yval are data values, // 1D data may specify either or both, // 2D data must specify both // subplot is an id string (default "xy") // makes use of gl.hovermode, which can be: // x (find the points with the closest x values, ie a column), // closest (find the single closest point) // internally there are two more that occasionally get used: // y (pick out a row - only used for multiple horizontal bar charts) // array (used when the user specifies an explicit // array of points to hover on) // // We wrap the hovers in a timer, to limit their frequency. // The actual rendering is done by private function _hover. exports.hover = function hover(gd, evt, subplot, noHoverEvent) { gd = Lib.getGraphDiv(gd); // The 'target' property changes when bubbling out of Shadow DOM. // Throttling can delay reading the target, so we save the current value. var eventTarget = evt.target; Lib.throttle( gd._fullLayout._uid + constants.HOVERID, constants.HOVERMINTIME, function() { _hover(gd, evt, subplot, noHoverEvent, eventTarget); } ); }; /* * Draw a single hover item or an array of hover item in a pre-existing svg container somewhere * hoverItem should have keys: * - x and y (or x0, x1, y0, and y1): * the pixel position to mark, relative to opts.container * - xLabel, yLabel, zLabel, text, and name: * info to go in the label * - color: * the background color for the label. * - idealAlign (optional): * 'left' or 'right' for which side of the x/y box to try to put this on first * - borderColor (optional): * color for the border, defaults to strongest contrast with color * - fontFamily (optional): * string, the font for this label, defaults to constants.HOVERFONT * - fontSize (optional): * the label font size, defaults to constants.HOVERFONTSIZE * - fontColor (optional): * defaults to borderColor * opts should have keys: * - bgColor: * the background color this is against, used if the trace is * non-opaque, and for the name, which goes outside the box * - container: * a or element to add the hover label to * - outerContainer: * normally a parent of `container`, sets the bounding box to use to * constrain the hover label and determine whether to show it on the left or right * opts can have optional keys: * - anchorIndex: the index of the hover item used as an anchor for positioning. The other hover items will be pushed up or down to prevent overlap. */ exports.loneHover = function loneHover(hoverItems, opts) { var multiHover = true; if(!Array.isArray(hoverItems)) { multiHover = false; hoverItems = [hoverItems]; } var gd = opts.gd; var gTop = getTopOffset(gd); var gLeft = getLeftOffset(gd); var pointsData = hoverItems.map(function(hoverItem) { var _x0 = hoverItem._x0 || hoverItem.x0 || hoverItem.x || 0; var _x1 = hoverItem._x1 || hoverItem.x1 || hoverItem.x || 0; var _y0 = hoverItem._y0 || hoverItem.y0 || hoverItem.y || 0; var _y1 = hoverItem._y1 || hoverItem.y1 || hoverItem.y || 0; var eventData = hoverItem.eventData; if(eventData) { var x0 = Math.min(_x0, _x1); var x1 = Math.max(_x0, _x1); var y0 = Math.min(_y0, _y1); var y1 = Math.max(_y0, _y1); var trace = hoverItem.trace; if(Registry.traceIs(trace, 'gl3d')) { var container = gd._fullLayout[trace.scene]._scene.container; var dx = container.offsetLeft; var dy = container.offsetTop; x0 += dx; x1 += dx; y0 += dy; y1 += dy; } // TODO: handle heatmapgl eventData.bbox = { x0: x0 + gLeft, x1: x1 + gLeft, y0: y0 + gTop, y1: y1 + gTop }; if(opts.inOut_bbox) { opts.inOut_bbox.push(eventData.bbox); } } else { eventData = false; } return { color: hoverItem.color || Color.defaultLine, x0: hoverItem.x0 || hoverItem.x || 0, x1: hoverItem.x1 || hoverItem.x || 0, y0: hoverItem.y0 || hoverItem.y || 0, y1: hoverItem.y1 || hoverItem.y || 0, xLabel: hoverItem.xLabel, yLabel: hoverItem.yLabel, zLabel: hoverItem.zLabel, text: hoverItem.text, name: hoverItem.name, idealAlign: hoverItem.idealAlign, // optional extra bits of styling borderColor: hoverItem.borderColor, fontFamily: hoverItem.fontFamily, fontSize: hoverItem.fontSize, fontColor: hoverItem.fontColor, fontWeight: hoverItem.fontWeight, fontStyle: hoverItem.fontStyle, fontVariant: hoverItem.fontVariant, nameLength: hoverItem.nameLength, textAlign: hoverItem.textAlign, // filler to make createHoverText happy trace: hoverItem.trace || { index: 0, hoverinfo: '' }, xa: {_offset: 0}, ya: {_offset: 0}, index: 0, hovertemplate: hoverItem.hovertemplate || false, hovertemplateLabels: hoverItem.hovertemplateLabels || false, eventData: eventData }; }); var rotateLabels = false; var hoverText = createHoverText(pointsData, { gd: gd, hovermode: 'closest', rotateLabels: rotateLabels, bgColor: opts.bgColor || Color.background, container: d3.select(opts.container), outerContainer: opts.outerContainer || opts.container }); var hoverLabel = hoverText.hoverLabels; // Fix vertical overlap var tooltipSpacing = 5; var lastBottomY = 0; var anchor = 0; hoverLabel .sort(function(a, b) {return a.y0 - b.y0;}) .each(function(d, i) { var topY = d.y0 - d.by / 2; if((topY - tooltipSpacing) < lastBottomY) { d.offset = (lastBottomY - topY) + tooltipSpacing; } else { d.offset = 0; } lastBottomY = topY + d.by + d.offset; if(i === opts.anchorIndex || 0) anchor = d.offset; }) .each(function(d) { d.offset -= anchor; }); var scaleX = gd._fullLayout._invScaleX; var scaleY = gd._fullLayout._invScaleY; alignHoverText(hoverLabel, rotateLabels, scaleX, scaleY); return multiHover ? hoverLabel : hoverLabel.node(); }; // The actual implementation is here: function _hover(gd, evt, subplot, noHoverEvent, eventTarget) { if(!subplot) subplot = 'xy'; if(typeof subplot === 'string') { // drop zindex from subplot id subplot = subplot.split(zindexSeparator)[0]; } // if the user passed in an array of subplots, // use those instead of finding overlayed plots var subplots = Array.isArray(subplot) ? subplot : [subplot]; var spId; var fullLayout = gd._fullLayout; var hoversubplots = fullLayout.hoversubplots; var plots = fullLayout._plots || []; var plotinfo = plots[subplot]; var hasCartesian = fullLayout._has('cartesian'); var hovermode = evt.hovermode || fullLayout.hovermode; var hovermodeHasX = (hovermode || '').charAt(0) === 'x'; var hovermodeHasY = (hovermode || '').charAt(0) === 'y'; var firstXaxis; var firstYaxis; if(hasCartesian && (hovermodeHasX || hovermodeHasY) && hoversubplots === 'axis') { var subplotsLength = subplots.length; for(var p = 0; p < subplotsLength; p++) { spId = subplots[p]; if(plots[spId]) { // 'cartesian' case firstXaxis = Axes.getFromId(gd, spId, 'x'); firstYaxis = Axes.getFromId(gd, spId, 'y'); var subplotsWith = ( hovermodeHasX ? firstXaxis : firstYaxis )._subplotsWith; if(subplotsWith && subplotsWith.length) { for(var q = 0; q < subplotsWith.length; q++) { pushUnique(subplots, subplotsWith[q]); } } } } } // list of all overlaid subplots to look at if(plotinfo && hoversubplots !== 'single') { var overlayedSubplots = plotinfo.overlays.map(function(pi) { return pi.id; }); subplots = subplots.concat(overlayedSubplots); } var len = subplots.length; var xaArray = new Array(len); var yaArray = new Array(len); var supportsCompare = false; for(var i = 0; i < len; i++) { spId = subplots[i]; if(plots[spId]) { // 'cartesian' case supportsCompare = true; xaArray[i] = plots[spId].xaxis; yaArray[i] = plots[spId].yaxis; } else if(fullLayout[spId] && fullLayout[spId]._subplot) { // other subplot types var _subplot = fullLayout[spId]._subplot; xaArray[i] = _subplot.xaxis; yaArray[i] = _subplot.yaxis; } else { Lib.warn('Unrecognized subplot: ' + spId); return; } } if(hovermode && !supportsCompare) hovermode = 'closest'; if(['x', 'y', 'closest', 'x unified', 'y unified'].indexOf(hovermode) === -1 || !gd.calcdata || gd.querySelector('.zoombox') || gd._dragging) { return dragElement.unhoverRaw(gd, evt); } var hoverdistance = fullLayout.hoverdistance; if(hoverdistance === -1) hoverdistance = Infinity; var spikedistance = fullLayout.spikedistance; if(spikedistance === -1) spikedistance = Infinity; // hoverData: the set of candidate points we've found to highlight var hoverData = []; // searchData: the data to search in. Mostly this is just a copy of // gd.calcdata, filtered to the subplot and overlays we're on // but if a point array is supplied it will be a mapping // of indicated curves var searchData = []; // [x|y]valArray: the axis values of the hover event // mapped onto each of the currently selected overlaid subplots var xvalArray, yvalArray; var itemnum, curvenum, cd, trace, subplotId, subploti, _mode, xval, yval, pointData, closedataPreviousLength; // spikePoints: the set of candidate points we've found to draw spikes to var spikePoints = { hLinePoint: null, vLinePoint: null }; // does subplot have one (or more) horizontal traces? // This is used to determine whether we rotate the labels or not var hasOneHorizontalTrace = false; // Figure out what we're hovering on: // mouse location or user-supplied data if(Array.isArray(evt)) { // user specified an array of points to highlight hovermode = 'array'; for(itemnum = 0; itemnum < evt.length; itemnum++) { cd = gd.calcdata[evt[itemnum].curveNumber || 0]; if(cd) { trace = cd[0].trace; if(cd[0].trace.hoverinfo !== 'skip') { searchData.push(cd); if(trace.orientation === 'h') { hasOneHorizontalTrace = true; } } } } } else { // take into account zorder var zorderedCalcdata = gd.calcdata.slice(); zorderedCalcdata.sort(function(a, b) { var aZorder = a[0].trace.zorder || 0; var bZorder = b[0].trace.zorder || 0; return aZorder - bZorder; }); for(curvenum = 0; curvenum < zorderedCalcdata.length; curvenum++) { cd = zorderedCalcdata[curvenum]; trace = cd[0].trace; if(trace.hoverinfo !== 'skip' && helpers.isTraceInSubplots(trace, subplots)) { searchData.push(cd); if(trace.orientation === 'h') { hasOneHorizontalTrace = true; } } } // [x|y]px: the pixels (from top left) of the mouse location // on the currently selected plot area // add pointerX|Y property for drawing the spikes in spikesnap 'cursor' situation var hasUserCalledHover = !eventTarget; var xpx, ypx; if(hasUserCalledHover) { if('xpx' in evt) xpx = evt.xpx; else xpx = xaArray[0]._length / 2; if('ypx' in evt) ypx = evt.ypx; else ypx = yaArray[0]._length / 2; } else { // fire the beforehover event and quit if it returns false // note that we're only calling this on real mouse events, so // manual calls to fx.hover will always run. if(Events.triggerHandler(gd, 'plotly_beforehover', evt) === false) { return; } var dbb = eventTarget.getBoundingClientRect(); xpx = evt.clientX - dbb.left; ypx = evt.clientY - dbb.top; fullLayout._calcInverseTransform(gd); var transformedCoords = Lib.apply3DTransform(fullLayout._invTransform)(xpx, ypx); xpx = transformedCoords[0]; ypx = transformedCoords[1]; // in case hover was called from mouseout into hovertext, // it's possible you're not actually over the plot anymore if(xpx < 0 || xpx > xaArray[0]._length || ypx < 0 || ypx > yaArray[0]._length) { return dragElement.unhoverRaw(gd, evt); } } evt.pointerX = xpx + xaArray[0]._offset; evt.pointerY = ypx + yaArray[0]._offset; if('xval' in evt) xvalArray = helpers.flat(subplots, evt.xval); else xvalArray = helpers.p2c(xaArray, xpx); if('yval' in evt) yvalArray = helpers.flat(subplots, evt.yval); else yvalArray = helpers.p2c(yaArray, ypx); if(!isNumeric(xvalArray[0]) || !isNumeric(yvalArray[0])) { Lib.warn('Fx.hover failed', evt, gd); return dragElement.unhoverRaw(gd, evt); } } // the pixel distance to beat as a matching point // in 'x' or 'y' mode this resets for each trace var distance = Infinity; // find the closest point in each trace // this is minimum dx and/or dy, depending on mode // and the pixel position for the label (labelXpx, labelYpx) function findHoverPoints(customXVal, customYVal) { for(curvenum = 0; curvenum < searchData.length; curvenum++) { cd = searchData[curvenum]; // filter out invisible or broken data if(!cd || !cd[0] || !cd[0].trace) continue; trace = cd[0].trace; if(trace.visible !== true || trace._length === 0) continue; // Explicitly bail out for these two. I don't know how to otherwise prevent // the rest of this function from running and failing if(['carpet', 'contourcarpet'].indexOf(trace._module.name) !== -1) continue; // within one trace mode can sometimes be overridden _mode = hovermode; if(helpers.isUnifiedHover(_mode)) { _mode = _mode.charAt(0); } if(trace.type === 'splom') { // splom traces do not generate overlay subplots, // it is safe to assume here splom traces correspond to the 0th subplot subploti = 0; subplotId = subplots[subploti]; } else { subplotId = helpers.getSubplot(trace); subploti = subplots.indexOf(subplotId); } // container for new point, also used to pass info into module.hoverPoints pointData = { // trace properties cd: cd, trace: trace, xa: xaArray[subploti], ya: yaArray[subploti], // max distances for hover and spikes - for points that want to show but do not // want to override other points, set distance/spikeDistance equal to max*Distance // and it will not get filtered out but it will be guaranteed to have a greater // distance than any point that calculated a real distance. maxHoverDistance: hoverdistance, maxSpikeDistance: spikedistance, // point properties - override all of these index: false, // point index in trace - only used by plotly.js hoverdata consumers distance: Math.min(distance, hoverdistance), // pixel distance or pseudo-distance // distance/pseudo-distance for spikes. This distance should always be calculated // as if in "closest" mode, and should only be set if this point should // generate a spike. spikeDistance: Infinity, // in some cases the spikes have different positioning from the hover label // they don't need x0/x1, just one position xSpike: undefined, ySpike: undefined, // where and how to display the hover label color: Color.defaultLine, // trace color name: trace.name, x0: undefined, x1: undefined, y0: undefined, y1: undefined, xLabelVal: undefined, yLabelVal: undefined, zLabelVal: undefined, text: undefined }; // add ref to subplot object (non-cartesian case) if(fullLayout[subplotId]) { pointData.subplot = fullLayout[subplotId]._subplot; } // add ref to splom scene if(fullLayout._splomScenes && fullLayout._splomScenes[trace.uid]) { pointData.scene = fullLayout._splomScenes[trace.uid]; } // for a highlighting array, figure out what // we're searching for with this element if(_mode === 'array') { var selection = evt[curvenum]; if('pointNumber' in selection) { pointData.index = selection.pointNumber; _mode = 'closest'; } else { _mode = ''; if('xval' in selection) { xval = selection.xval; _mode = 'x'; } if('yval' in selection) { yval = selection.yval; _mode = _mode ? 'closest' : 'y'; } } } else if(customXVal !== undefined && customYVal !== undefined) { xval = customXVal; yval = customYVal; } else { xval = xvalArray[subploti]; yval = yvalArray[subploti]; } closedataPreviousLength = hoverData.length; // Now if there is range to look in, find the points to hover. if(hoverdistance !== 0) { if(trace._module && trace._module.hoverPoints) { var newPoints = trace._module.hoverPoints(pointData, xval, yval, _mode, { finiteRange: true, hoverLayer: fullLayout._hoverlayer, // options for splom when hovering on same axis hoversubplots: hoversubplots, gd: gd }); if(newPoints) { var newPoint; for(var newPointNum = 0; newPointNum < newPoints.length; newPointNum++) { newPoint = newPoints[newPointNum]; if(isNumeric(newPoint.x0) && isNumeric(newPoint.y0)) { hoverData.push(cleanPoint(newPoint, hovermode)); } } } } else { Lib.log('Unrecognized trace type in hover:', trace); } } // in closest mode, remove any existing (farther) points // and don't look any farther than this latest point (or points, some // traces like box & violin make multiple hover labels at once) if(hovermode === 'closest' && hoverData.length > closedataPreviousLength) { hoverData.splice(0, closedataPreviousLength); distance = hoverData[0].distance; } // Now if there is range to look in, find the points to draw the spikelines // Do it only if there is no hoverData if(hasCartesian && (spikedistance !== 0)) { if(hoverData.length === 0) { pointData.distance = spikedistance; pointData.index = false; var closestPoints = trace._module.hoverPoints(pointData, xval, yval, 'closest', { hoverLayer: fullLayout._hoverlayer }); if(closestPoints) { closestPoints = closestPoints.filter(function(point) { // some hover points, like scatter fills, do not allow spikes, // so will generate a hover point but without a valid spikeDistance return point.spikeDistance <= spikedistance; }); } if(closestPoints && closestPoints.length) { var tmpPoint; var closestVPoints = closestPoints.filter(function(point) { return point.xa.showspikes && point.xa.spikesnap !== 'hovered data'; }); if(closestVPoints.length) { var closestVPt = closestVPoints[0]; if(isNumeric(closestVPt.x0) && isNumeric(closestVPt.y0)) { tmpPoint = fillSpikePoint(closestVPt); if(!spikePoints.vLinePoint || (spikePoints.vLinePoint.spikeDistance > tmpPoint.spikeDistance)) { spikePoints.vLinePoint = tmpPoint; } } } var closestHPoints = closestPoints.filter(function(point) { return point.ya.showspikes && point.ya.spikesnap !== 'hovered data'; }); if(closestHPoints.length) { var closestHPt = closestHPoints[0]; if(isNumeric(closestHPt.x0) && isNumeric(closestHPt.y0)) { tmpPoint = fillSpikePoint(closestHPt); if(!spikePoints.hLinePoint || (spikePoints.hLinePoint.spikeDistance > tmpPoint.spikeDistance)) { spikePoints.hLinePoint = tmpPoint; } } } } } } } } findHoverPoints(); function selectClosestPoint(pointsData, spikedistance, spikeOnWinning) { var resultPoint = null; var minDistance = Infinity; var thisSpikeDistance; for(var i = 0; i < pointsData.length; i++) { if(firstXaxis && firstXaxis._id !== pointsData[i].xa._id) continue; if(firstYaxis && firstYaxis._id !== pointsData[i].ya._id) continue; thisSpikeDistance = pointsData[i].spikeDistance; if(spikeOnWinning && i === 0) thisSpikeDistance = -Infinity; if(thisSpikeDistance <= minDistance && thisSpikeDistance <= spikedistance) { resultPoint = pointsData[i]; minDistance = thisSpikeDistance; } } return resultPoint; } function fillSpikePoint(point) { if(!point) return null; return { xa: point.xa, ya: point.ya, x: point.xSpike !== undefined ? point.xSpike : (point.x0 + point.x1) / 2, y: point.ySpike !== undefined ? point.ySpike : (point.y0 + point.y1) / 2, distance: point.distance, spikeDistance: point.spikeDistance, curveNumber: point.trace.index, color: point.color, pointNumber: point.index }; } var spikelineOpts = { fullLayout: fullLayout, container: fullLayout._hoverlayer, event: evt }; var oldspikepoints = gd._spikepoints; var newspikepoints = { vLinePoint: spikePoints.vLinePoint, hLinePoint: spikePoints.hLinePoint }; gd._spikepoints = newspikepoints; var sortHoverData = function() { // When sorting keep the points in the main subplot at the top // then add points in other subplots var hoverDataInSubplot = hoverData.filter(function(a) { return ( (firstXaxis && firstXaxis._id === a.xa._id) && (firstYaxis && firstYaxis._id === a.ya._id) ); }); var hoverDataOutSubplot = hoverData.filter(function(a) { return !( (firstXaxis && firstXaxis._id === a.xa._id) && (firstYaxis && firstYaxis._id === a.ya._id) ); }); hoverDataInSubplot.sort(distanceSort); hoverDataOutSubplot.sort(distanceSort); hoverData = hoverDataInSubplot.concat(hoverDataOutSubplot); // move period positioned points and box/bar-like traces to the end of the list hoverData = orderRangePoints(hoverData, hovermode); }; sortHoverData(); var axLetter = hovermode.charAt(0); var spikeOnWinning = (axLetter === 'x' || axLetter === 'y') && hoverData[0] && cartesianScatterPoints[hoverData[0].trace.type]; // Now if it is not restricted by spikedistance option, set the points to draw the spikelines if(hasCartesian && (spikedistance !== 0)) { if(hoverData.length !== 0) { var tmpHPointData = hoverData.filter(function(point) { return point.ya.showspikes; }); var tmpHPoint = selectClosestPoint(tmpHPointData, spikedistance, spikeOnWinning); spikePoints.hLinePoint = fillSpikePoint(tmpHPoint); var tmpVPointData = hoverData.filter(function(point) { return point.xa.showspikes; }); var tmpVPoint = selectClosestPoint(tmpVPointData, spikedistance, spikeOnWinning); spikePoints.vLinePoint = fillSpikePoint(tmpVPoint); } } // if hoverData is empty check for the spikes to draw and quit if there are none if(hoverData.length === 0) { var result = dragElement.unhoverRaw(gd, evt); if(hasCartesian && ((spikePoints.hLinePoint !== null) || (spikePoints.vLinePoint !== null))) { if(spikesChanged(oldspikepoints)) { createSpikelines(gd, spikePoints, spikelineOpts); } } return result; } if(hasCartesian) { if(spikesChanged(oldspikepoints)) { createSpikelines(gd, spikePoints, spikelineOpts); } } if( helpers.isXYhover(_mode) && hoverData[0].length !== 0 && hoverData[0].trace.type !== 'splom' // TODO: add support for splom ) { // pick winning point var winningPoint = hoverData[0]; // discard other points if(multipleHoverPoints[winningPoint.trace.type]) { hoverData = hoverData.filter(function(d) { return d.trace.index === winningPoint.trace.index; }); } else { hoverData = [winningPoint]; } var initLen = hoverData.length; var winX = getCoord('x', winningPoint, fullLayout); var winY = getCoord('y', winningPoint, fullLayout); // in compare mode, select every point at position findHoverPoints(winX, winY); var finalPoints = []; var seen = {}; var id = 0; var insert = function(newHd) { var key = multipleHoverPoints[newHd.trace.type] ? hoverDataKey(newHd) : newHd.trace.index; if(!seen[key]) { id++; seen[key] = id; finalPoints.push(newHd); } else { var oldId = seen[key] - 1; var oldHd = finalPoints[oldId]; if(oldId > 0 && Math.abs(newHd.distance) < Math.abs(oldHd.distance) ) { // replace with closest finalPoints[oldId] = newHd; } } }; var k; // insert the winnig point(s) first for(k = 0; k < initLen; k++) { insert(hoverData[k]); } // override from the end for(k = hoverData.length - 1; k > initLen - 1; k--) { insert(hoverData[k]); } hoverData = finalPoints; sortHoverData(); } // lastly, emit custom hover/unhover events var oldhoverdata = gd._hoverdata; var newhoverdata = []; var gTop = getTopOffset(gd); var gLeft = getLeftOffset(gd); // pull out just the data that's useful to // other people and send it to the event for(itemnum = 0; itemnum < hoverData.length; itemnum++) { var pt = hoverData[itemnum]; var eventData = helpers.makeEventData(pt, pt.trace, pt.cd); if(pt.hovertemplate !== false) { var ht = false; if(pt.cd[pt.index] && pt.cd[pt.index].ht) { ht = pt.cd[pt.index].ht; } pt.hovertemplate = ht || pt.trace.hovertemplate || false; } if(pt.xa && pt.ya) { var _x0 = pt.x0 + pt.xa._offset; var _x1 = pt.x1 + pt.xa._offset; var _y0 = pt.y0 + pt.ya._offset; var _y1 = pt.y1 + pt.ya._offset; var x0 = Math.min(_x0, _x1); var x1 = Math.max(_x0, _x1); var y0 = Math.min(_y0, _y1); var y1 = Math.max(_y0, _y1); eventData.bbox = { x0: x0 + gLeft, x1: x1 + gLeft, y0: y0 + gTop, y1: y1 + gTop }; } pt.eventData = [eventData]; newhoverdata.push(eventData); } gd._hoverdata = newhoverdata; var rotateLabels = ( (hovermode === 'y' && (searchData.length > 1 || hoverData.length > 1)) || (hovermode === 'closest' && hasOneHorizontalTrace && hoverData.length > 1) ); var bgColor = Color.combine( fullLayout.plot_bgcolor || Color.background, fullLayout.paper_bgcolor ); var hoverText = createHoverText(hoverData, { gd: gd, hovermode: hovermode, rotateLabels: rotateLabels, bgColor: bgColor, container: fullLayout._hoverlayer, outerContainer: fullLayout._paper.node(), commonLabelOpts: fullLayout.hoverlabel, hoverdistance: fullLayout.hoverdistance }); var hoverLabels = hoverText.hoverLabels; if(!helpers.isUnifiedHover(hovermode)) { hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, hoverText.commonLabelBoundingBox); alignHoverText(hoverLabels, rotateLabels, fullLayout._invScaleX, fullLayout._invScaleY); } // TODO: tagName hack is needed to appease geo.js's hack of using eventTarget=true // we should improve the "fx" API so other plots can use it without these hack. if(eventTarget && eventTarget.tagName) { var hasClickToShow = Registry.getComponentMethod('annotations', 'hasClickToShow')(gd, newhoverdata); overrideCursor(d3.select(eventTarget), hasClickToShow ? 'pointer' : ''); } // don't emit events if called manually if(!eventTarget || noHoverEvent || !hoverChanged(gd, evt, oldhoverdata)) return; if(oldhoverdata) { gd.emit('plotly_unhover', { event: evt, points: oldhoverdata }); } gd.emit('plotly_hover', { event: evt, points: gd._hoverdata, xaxes: xaArray, yaxes: yaArray, xvals: xvalArray, yvals: yvalArray }); } function hoverDataKey(d) { return [d.trace.index, d.index, d.x0, d.y0, d.name, d.attr, d.xa ? d.xa._id : '', d.ya ? d.ya._id : ''].join(','); } var EXTRA_STRING_REGEX = /([\s\S]*)<\/extra>/; function createHoverText(hoverData, opts) { var gd = opts.gd; var fullLayout = gd._fullLayout; var hovermode = opts.hovermode; var rotateLabels = opts.rotateLabels; var bgColor = opts.bgColor; var container = opts.container; var outerContainer = opts.outerContainer; var commonLabelOpts = opts.commonLabelOpts || {}; // Early exit if no labels are drawn if(hoverData.length === 0) return [[]]; // opts.fontFamily/Size are used for the common label // and as defaults for each hover label, though the individual labels // can override this. var fontFamily = opts.fontFamily || constants.HOVERFONT; var fontSize = opts.fontSize || constants.HOVERFONTSIZE; var fontWeight = opts.fontWeight || fullLayout.font.weight; var fontStyle = opts.fontStyle || fullLayout.font.style; var fontVariant = opts.fontVariant || fullLayout.font.variant; var fontTextcase = opts.fontTextcase || fullLayout.font.textcase; var fontLineposition = opts.fontLineposition || fullLayout.font.lineposition; var fontShadow = opts.fontShadow || fullLayout.font.shadow; var c0 = hoverData[0]; var xa = c0.xa; var ya = c0.ya; var axLetter = hovermode.charAt(0); var axLabel = axLetter + 'Label'; var t0 = c0[axLabel]; // search in array for the label if(t0 === undefined && xa.type === 'multicategory') { for(var q = 0; q < hoverData.length; q++) { t0 = hoverData[q][axLabel]; if(t0 !== undefined) break; } } var outerContainerBB = getBoundingClientRect(gd, outerContainer); var outerTop = outerContainerBB.top; var outerWidth = outerContainerBB.width; var outerHeight = outerContainerBB.height; // show the common label, if any, on the axis // never show a common label in array mode, // even if sometimes there could be one var showCommonLabel = ( (t0 !== undefined) && (c0.distance <= opts.hoverdistance) && (hovermode === 'x' || hovermode === 'y') ); // all hover traces hoverinfo must contain the hovermode // to have common labels if(showCommonLabel) { var allHaveZ = true; var i, traceHoverinfo; for(i = 0; i < hoverData.length; i++) { if(allHaveZ && hoverData[i].zLabel === undefined) allHaveZ = false; traceHoverinfo = hoverData[i].hoverinfo || hoverData[i].trace.hoverinfo; if(traceHoverinfo) { var parts = Array.isArray(traceHoverinfo) ? traceHoverinfo : traceHoverinfo.split('+'); if(parts.indexOf('all') === -1 && parts.indexOf(hovermode) === -1) { showCommonLabel = false; break; } } } // xyz labels put all info in their main label, so have no need of a common label if(allHaveZ) showCommonLabel = false; } var commonLabel = container.selectAll('g.axistext') .data(showCommonLabel ? [0] : []); commonLabel.enter().append('g') .classed('axistext', true); commonLabel.exit().remove(); // set rect (without arrow) behind label below for later collision detection var commonLabelRect = { minX: 0, maxX: 0, minY: 0, maxY: 0 }; commonLabel.each(function() { var label = d3.select(this); var lpath = Lib.ensureSingle(label, 'path', '', function(s) { s.style({'stroke-width': '1px'}); }); var ltext = Lib.ensureSingle(label, 'text', '', function(s) { // prohibit tex interpretation until we can handle // tex and regular text together s.attr('data-notex', 1); }); var commonBgColor = commonLabelOpts.bgcolor || Color.defaultLine; var commonStroke = commonLabelOpts.bordercolor || Color.contrast(commonBgColor); var contrastColor = Color.contrast(commonBgColor); var commonLabelOptsFont = commonLabelOpts.font; var commonLabelFont = { weight: commonLabelOptsFont.weight || fontWeight, style: commonLabelOptsFont.style || fontStyle, variant: commonLabelOptsFont.variant || fontVariant, textcase: commonLabelOptsFont.textcase || fontTextcase, lineposition: commonLabelOptsFont.lineposition || fontLineposition, shadow: commonLabelOptsFont.shadow || fontShadow, family: commonLabelOptsFont.family || fontFamily, size: commonLabelOptsFont.size || fontSize, color: commonLabelOptsFont.color || contrastColor }; lpath.style({ fill: commonBgColor, stroke: commonStroke }); ltext.text(t0) .call(Drawing.font, commonLabelFont) .call(svgTextUtils.positionText, 0, 0) .call(svgTextUtils.convertToTspans, gd); label.attr('transform', ''); var tbb = getBoundingClientRect(gd, ltext.node()); var lx, ly; if(hovermode === 'x') { var topsign = xa.side === 'top' ? '-' : ''; ltext.attr('text-anchor', 'middle') .call(svgTextUtils.positionText, 0, (xa.side === 'top' ? (outerTop - tbb.bottom - HOVERARROWSIZE - HOVERTEXTPAD) : (outerTop - tbb.top + HOVERARROWSIZE + HOVERTEXTPAD))); lx = xa._offset + (c0.x0 + c0.x1) / 2; ly = ya._offset + (xa.side === 'top' ? 0 : ya._length); var halfWidth = tbb.width / 2 + HOVERTEXTPAD; var tooltipMidX = lx; if(lx < halfWidth) { tooltipMidX = halfWidth; } else if(lx > (fullLayout.width - halfWidth)) { tooltipMidX = fullLayout.width - halfWidth; } lpath.attr('d', 'M' + (lx - tooltipMidX) + ',0' + 'L' + (lx - tooltipMidX + HOVERARROWSIZE) + ',' + topsign + HOVERARROWSIZE + 'H' + halfWidth + 'v' + topsign + (HOVERTEXTPAD * 2 + tbb.height) + 'H' + (-halfWidth) + 'V' + topsign + HOVERARROWSIZE + 'H' + (lx - tooltipMidX - HOVERARROWSIZE) + 'Z'); lx = tooltipMidX; commonLabelRect.minX = lx - halfWidth; commonLabelRect.maxX = lx + halfWidth; if(xa.side === 'top') { // label on negative y side commonLabelRect.minY = ly - (HOVERTEXTPAD * 2 + tbb.height); commonLabelRect.maxY = ly - HOVERTEXTPAD; } else { commonLabelRect.minY = ly + HOVERTEXTPAD; commonLabelRect.maxY = ly + (HOVERTEXTPAD * 2 + tbb.height); } } else { var anchor; var sgn; var leftsign; if(ya.side === 'right') { anchor = 'start'; sgn = 1; leftsign = ''; lx = xa._offset + xa._length; } else { anchor = 'end'; sgn = -1; leftsign = '-'; lx = xa._offset; } ly = ya._offset + (c0.y0 + c0.y1) / 2; ltext.attr('text-anchor', anchor); lpath.attr('d', 'M0,0' + 'L' + leftsign + HOVERARROWSIZE + ',' + HOVERARROWSIZE + 'V' + (HOVERTEXTPAD + tbb.height / 2) + 'h' + leftsign + (HOVERTEXTPAD * 2 + tbb.width) + 'V-' + (HOVERTEXTPAD + tbb.height / 2) + 'H' + leftsign + HOVERARROWSIZE + 'V-' + HOVERARROWSIZE + 'Z'); commonLabelRect.minY = ly - (HOVERTEXTPAD + tbb.height / 2); commonLabelRect.maxY = ly + (HOVERTEXTPAD + tbb.height / 2); if(ya.side === 'right') { commonLabelRect.minX = lx + HOVERARROWSIZE; commonLabelRect.maxX = lx + HOVERARROWSIZE + (HOVERTEXTPAD * 2 + tbb.width); } else { // label on negative x side commonLabelRect.minX = lx - HOVERARROWSIZE - (HOVERTEXTPAD * 2 + tbb.width); commonLabelRect.maxX = lx - HOVERARROWSIZE; } var halfHeight = tbb.height / 2; var lty = outerTop - tbb.top - halfHeight; var clipId = 'clip' + fullLayout._uid + 'commonlabel' + ya._id; var clipPath; if(lx < (tbb.width + 2 * HOVERTEXTPAD + HOVERARROWSIZE)) { clipPath = 'M-' + (HOVERARROWSIZE + HOVERTEXTPAD) + '-' + halfHeight + 'h-' + (tbb.width - HOVERTEXTPAD) + 'V' + halfHeight + 'h' + (tbb.width - HOVERTEXTPAD) + 'Z'; var ltx = tbb.width - lx + HOVERTEXTPAD; svgTextUtils.positionText(ltext, ltx, lty); // shift each line (except the longest) so that start-of-line // is always visible if(anchor === 'end') { ltext.selectAll('tspan').each(function() { var s = d3.select(this); var dummy = Drawing.tester.append('text') .text(s.text()) .call(Drawing.font, commonLabelFont); var dummyBB = getBoundingClientRect(gd, dummy.node()); if(Math.round(dummyBB.width) < Math.round(tbb.width)) { s.attr('x', ltx - dummyBB.width); } dummy.remove(); }); } } else { svgTextUtils.positionText(ltext, sgn * (HOVERTEXTPAD + HOVERARROWSIZE), lty); clipPath = null; } var textClip = fullLayout._topclips.selectAll('#' + clipId).data(clipPath ? [0] : []); textClip.enter().append('clipPath').attr('id', clipId).append('path'); textClip.exit().remove(); textClip.select('path').attr('d', clipPath); Drawing.setClipUrl(ltext, clipPath ? clipId : null, gd); } label.attr('transform', strTranslate(lx, ly)); }); // Show a single hover label if(helpers.isUnifiedHover(hovermode)) { // Delete leftover hover labels from other hovermodes container.selectAll('g.hovertext').remove(); var groupedHoverData = hoverData.filter(function(data) {return data.hoverinfo !== 'none';}); // Return early if nothing is hovered on if(groupedHoverData.length === 0) return []; // mock legend var hoverlabel = fullLayout.hoverlabel; var font = hoverlabel.font; var mockLayoutIn = { showlegend: true, legend: { title: {text: t0, font: font}, font: font, bgcolor: hoverlabel.bgcolor, bordercolor: hoverlabel.bordercolor, borderwidth: 1, tracegroupgap: 7, traceorder: fullLayout.legend ? fullLayout.legend.traceorder : undefined, orientation: 'v' } }; var mockLayoutOut = { font: font }; legendSupplyDefaults(mockLayoutIn, mockLayoutOut, gd._fullData); var mockLegend = mockLayoutOut.legend; // prepare items for the legend mockLegend.entries = []; for(var j = 0; j < groupedHoverData.length; j++) { var pt = groupedHoverData[j]; if(pt.hoverinfo === 'none') continue; var texts = getHoverLabelText(pt, true, hovermode, fullLayout, t0); var text = texts[0]; var name = texts[1]; pt.name = name; if(name !== '') { pt.text = name + ' : ' + text; } else { pt.text = text; } // pass through marker's calcdata to style legend items var cd = pt.cd[pt.index]; if(cd) { if(cd.mc) pt.mc = cd.mc; if(cd.mcc) pt.mc = cd.mcc; if(cd.mlc) pt.mlc = cd.mlc; if(cd.mlcc) pt.mlc = cd.mlcc; if(cd.mlw) pt.mlw = cd.mlw; if(cd.mrc) pt.mrc = cd.mrc; if(cd.dir) pt.dir = cd.dir; } pt._distinct = true; mockLegend.entries.push([pt]); } mockLegend.entries.sort(function(a, b) { return a[0].trace.index - b[0].trace.index;}); mockLegend.layer = container; // Draw unified hover label mockLegend._inHover = true; mockLegend._groupTitleFont = hoverlabel.grouptitlefont; legendDraw(gd, mockLegend); // Position the hover var legendContainer = container.select('g.legend'); var tbb = getBoundingClientRect(gd, legendContainer.node()); var tWidth = tbb.width + 2 * HOVERTEXTPAD; var tHeight = tbb.height + 2 * HOVERTEXTPAD; var winningPoint = groupedHoverData[0]; var avgX = (winningPoint.x0 + winningPoint.x1) / 2; var avgY = (winningPoint.y0 + winningPoint.y1) / 2; // When a scatter (or e.g. heatmap) point wins, it's OK for the hovelabel to occlude the bar and other points. var pointWon = !( Registry.traceIs(winningPoint.trace, 'bar-like') || Registry.traceIs(winningPoint.trace, 'box-violin') ); var lyBottom, lyTop; if(axLetter === 'y') { if(pointWon) { lyTop = avgY - HOVERTEXTPAD; lyBottom = avgY + HOVERTEXTPAD; } else { lyTop = Math.min.apply(null, groupedHoverData.map(function(c) { return Math.min(c.y0, c.y1); })); lyBottom = Math.max.apply(null, groupedHoverData.map(function(c) { return Math.max(c.y0, c.y1); })); } } else { lyTop = lyBottom = Lib.mean(groupedHoverData.map(function(c) { return (c.y0 + c.y1) / 2; })) - tHeight / 2; } var lxRight, lxLeft; if(axLetter === 'x') { if(pointWon) { lxRight = avgX + HOVERTEXTPAD; lxLeft = avgX - HOVERTEXTPAD; } else { lxRight = Math.max.apply(null, groupedHoverData.map(function(c) { return Math.max(c.x0, c.x1); })); lxLeft = Math.min.apply(null, groupedHoverData.map(function(c) { return Math.min(c.x0, c.x1); })); } } else { lxRight = lxLeft = Lib.mean(groupedHoverData.map(function(c) { return (c.x0 + c.x1) / 2; })) - tWidth / 2; } var xOffset = xa._offset; var yOffset = ya._offset; lyBottom += yOffset; lxRight += xOffset; lxLeft += xOffset - tWidth; lyTop += yOffset - tHeight; var lx, ly; // top and left positions of the hover box // horizontal alignment to end up on screen if(lxRight + tWidth < outerWidth && lxRight >= 0) { lx = lxRight; } else if(lxLeft + tWidth < outerWidth && lxLeft >= 0) { lx = lxLeft; } else if(xOffset + tWidth < outerWidth) { lx = xOffset; // subplot left corner } else { // closest left or right side of the paper if(lxRight - avgX < avgX - lxLeft + tWidth) { lx = outerWidth - tWidth; } else { lx = 0; } } lx += HOVERTEXTPAD; // vertical alignement to end up on screen if(lyBottom + tHeight < outerHeight && lyBottom >= 0) { ly = lyBottom; } else if(lyTop + tHeight < outerHeight && lyTop >= 0) { ly = lyTop; } else if(yOffset + tHeight < outerHeight) { ly = yOffset; // subplot top corner } else { // closest top or bottom side of the paper if(lyBottom - avgY < avgY - lyTop + tHeight) { ly = outerHeight - tHeight; } else { ly = 0; } } ly += HOVERTEXTPAD; legendContainer.attr('transform', strTranslate(lx - 1, ly - 1)); return legendContainer; } // show all the individual labels // first create the objects var hoverLabels = container.selectAll('g.hovertext') .data(hoverData, function(d) { // N.B. when multiple items have the same result key-function value, // only the first of those items in hoverData gets rendered return hoverDataKey(d); }); hoverLabels.enter().append('g') .classed('hovertext', true) .each(function() { var g = d3.select(this); // trace name label (rect and text.name) g.append('rect') .call(Color.fill, Color.addOpacity(bgColor, 0.8)); g.append('text').classed('name', true); // trace data label (path and text.nums) g.append('path') .style('stroke-width', '1px'); g.append('text').classed('nums', true) .call(Drawing.font, { weight: fontWeight, style: fontStyle, variant: fontVariant, textcase: fontTextcase, lineposition: fontLineposition, shadow: fontShadow, family: fontFamily, size: fontSize }); }); hoverLabels.exit().remove(); // then put the text in, position the pointer to the data, // and figure out sizes hoverLabels.each(function(d) { var g = d3.select(this).attr('transform', ''); var dColor = d.color; if(Array.isArray(dColor)) { dColor = dColor[d.eventData[0].pointNumber]; } // combine possible non-opaque trace color with bgColor var color0 = d.bgcolor || dColor; // color for 'nums' part of the label var numsColor = Color.combine( Color.opacity(color0) ? color0 : Color.defaultLine, bgColor ); // color for 'name' part of the label var nameColor = Color.combine( Color.opacity(dColor) ? dColor : Color.defaultLine, bgColor ); // find a contrasting color for border and text var contrastColor = d.borderColor || Color.contrast(numsColor); var texts = getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g); var text = texts[0]; var name = texts[1]; // main label var tx = g.select('text.nums') .call(Drawing.font, { family: d.fontFamily || fontFamily, size: d.fontSize || fontSize, color: d.fontColor || contrastColor, weight: d.fontWeight || fontWeight, style: d.fontStyle || fontStyle, variant: d.fontVariant || fontVariant, textcase: d.fontTextcase || fontTextcase, lineposition: d.fontLineposition || fontLineposition, shadow: d.fontShadow || fontShadow, }) .text(text) .attr('data-notex', 1) .call(svgTextUtils.positionText, 0, 0) .call(svgTextUtils.convertToTspans, gd); var tx2 = g.select('text.name'); var tx2width = 0; var tx2height = 0; // secondary label for non-empty 'name' if(name && name !== text) { tx2.call(Drawing.font, { family: d.fontFamily || fontFamily, size: d.fontSize || fontSize, color: nameColor, weight: d.fontWeight || fontWeight, style: d.fontStyle || fontStyle, variant: d.fontVariant || fontVariant, textcase: d.fontTextcase || fontTextcase, lineposition: d.fontLineposition || fontLineposition, shadow: d.fontShadow || fontShadow, }).text(name) .attr('data-notex', 1) .call(svgTextUtils.positionText, 0, 0) .call(svgTextUtils.convertToTspans, gd); var t2bb = getBoundingClientRect(gd, tx2.node()); tx2width = t2bb.width + 2 * HOVERTEXTPAD; tx2height = t2bb.height + 2 * HOVERTEXTPAD; } else { tx2.remove(); g.select('rect').remove(); } g.select('path').style({ fill: numsColor, stroke: contrastColor }); var htx = d.xa._offset + (d.x0 + d.x1) / 2; var hty = d.ya._offset + (d.y0 + d.y1) / 2; var dx = Math.abs(d.x1 - d.x0); var dy = Math.abs(d.y1 - d.y0); var tbb = getBoundingClientRect(gd, tx.node()); var tbbWidth = tbb.width / fullLayout._invScaleX; var tbbHeight = tbb.height / fullLayout._invScaleY; d.ty0 = (outerTop - tbb.top) / fullLayout._invScaleY; d.bx = tbbWidth + 2 * HOVERTEXTPAD; d.by = Math.max(tbbHeight + 2 * HOVERTEXTPAD, tx2height); d.anchor = 'start'; d.txwidth = tbbWidth; d.tx2width = tx2width; d.offset = 0; var txTotalWidth = (tbbWidth + HOVERARROWSIZE + HOVERTEXTPAD + tx2width) * fullLayout._invScaleX; var anchorStartOK, anchorEndOK; if(rotateLabels) { d.pos = htx; anchorStartOK = hty + dy / 2 + txTotalWidth <= outerHeight; anchorEndOK = hty - dy / 2 - txTotalWidth >= 0; if((d.idealAlign === 'top' || !anchorStartOK) && anchorEndOK) { hty -= dy / 2; d.anchor = 'end'; } else if(anchorStartOK) { hty += dy / 2; d.anchor = 'start'; } else { d.anchor = 'middle'; } d.crossPos = hty; } else { d.pos = hty; anchorStartOK = htx + dx / 2 + txTotalWidth <= outerWidth; anchorEndOK = htx - dx / 2 - txTotalWidth >= 0; if((d.idealAlign === 'left' || !anchorStartOK) && anchorEndOK) { htx -= dx / 2; d.anchor = 'end'; } else if(anchorStartOK) { htx += dx / 2; d.anchor = 'start'; } else { d.anchor = 'middle'; var txHalfWidth = txTotalWidth / 2; var overflowR = htx + txHalfWidth - outerWidth; var overflowL = htx - txHalfWidth; if(overflowR > 0) htx -= overflowR; if(overflowL < 0) htx += -overflowL; } d.crossPos = htx; } tx.attr('text-anchor', d.anchor); if(tx2width) tx2.attr('text-anchor', d.anchor); g.attr('transform', strTranslate(htx, hty) + (rotateLabels ? strRotate(YANGLE) : '')); }); return { hoverLabels: hoverLabels, commonLabelBoundingBox: commonLabelRect }; } function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) { var name = ''; var text = ''; // to get custom 'name' labels pass cleanPoint if(d.nameOverride !== undefined) d.name = d.nameOverride; if(d.name) { if(d.trace._meta) { d.name = Lib.templateString(d.name, d.trace._meta); } name = plainText(d.name, d.nameLength); } var h0 = hovermode.charAt(0); var h1 = h0 === 'x' ? 'y' : 'x'; if(d.zLabel !== undefined) { if(d.xLabel !== undefined) text += 'x: ' + d.xLabel + '
'; if(d.yLabel !== undefined) text += 'y: ' + d.yLabel + '
'; if( d.trace.type !== 'choropleth' && d.trace.type !== 'choroplethmapbox' && d.trace.type !== 'choroplethmap' ) { text += (text ? 'z: ' : '') + d.zLabel; } } else if(showCommonLabel && d[h0 + 'Label'] === t0) { text = d[h1 + 'Label'] || ''; } else if(d.xLabel === undefined) { if(d.yLabel !== undefined && d.trace.type !== 'scattercarpet') { text = d.yLabel; } } else if(d.yLabel === undefined) text = d.xLabel; else text = '(' + d.xLabel + ', ' + d.yLabel + ')'; if((d.text || d.text === 0) && !Array.isArray(d.text)) { text += (text ? '
' : '') + d.text; } // used by other modules (initially just ternary) that // manage their own hoverinfo independent of cleanPoint // the rest of this will still apply, so such modules // can still put things in (x|y|z)Label, text, and name // and hoverinfo will still determine their visibility if(d.extraText !== undefined) text += (text ? '
' : '') + d.extraText; // if 'text' is empty at this point, // and hovertemplate is not defined, // put 'name' in main label and don't show secondary label if(g && text === '' && !d.hovertemplate) { // if 'name' is also empty, remove entire label if(name === '') g.remove(); text = name; } // hovertemplate var hovertemplate = d.hovertemplate || false; if(hovertemplate) { var labels = d.hovertemplateLabels || d; if(d[h0 + 'Label'] !== t0) { labels[h0 + 'other'] = labels[h0 + 'Val']; labels[h0 + 'otherLabel'] = labels[h0 + 'Label']; } text = Lib.hovertemplateString( hovertemplate, labels, fullLayout._d3locale, d.eventData[0] || {}, d.trace._meta ); text = text.replace(EXTRA_STRING_REGEX, function(match, extra) { // assign name for secondary text label name = plainText(extra, d.nameLength); // remove from main text label return ''; }); } return [text, name]; } // Make groups of touching points, and within each group // move each point so that no labels overlap, but the average // label position is the same as it was before moving. Incidentally, // this is equivalent to saying all the labels are on equal linear // springs about their initial position. Initially, each point is // its own group, but as we find overlaps we will clump the points. // // Also, there are hard constraints at the edges of the graphs, // that push all groups to the middle so they are visible. I don't // know what happens if the group spans all the way from one edge to // the other, though it hardly matters - there's just too much // information then. function hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, commonLabelBoundingBox) { var axKey = rotateLabels ? 'xa' : 'ya'; var crossAxKey = rotateLabels ? 'ya' : 'xa'; var nummoves = 0; var axSign = 1; var nLabels = hoverLabels.size(); // make groups of touching points var pointgroups = new Array(nLabels); var k = 0; // get extent of axis hover label var axisLabelMinX = commonLabelBoundingBox.minX; var axisLabelMaxX = commonLabelBoundingBox.maxX; var axisLabelMinY = commonLabelBoundingBox.minY; var axisLabelMaxY = commonLabelBoundingBox.maxY; var pX = function(x) { return x * fullLayout._invScaleX; }; var pY = function(y) { return y * fullLayout._invScaleY; }; hoverLabels.each(function(d) { var ax = d[axKey]; var crossAx = d[crossAxKey]; var axIsX = ax._id.charAt(0) === 'x'; var rng = ax.range; if(k === 0 && rng && ((rng[0] > rng[1]) !== axIsX)) { axSign = -1; } var pmin = 0; var pmax = (axIsX ? fullLayout.width : fullLayout.height); // in hovermode avoid overlap between hover labels and axis label if(fullLayout.hovermode === 'x' || fullLayout.hovermode === 'y') { // extent of rect behind hover label on cross axis: var offsets = getHoverLabelOffsets(d, rotateLabels); var anchor = d.anchor; var horzSign = anchor === 'end' ? -1 : 1; var labelMin; var labelMax; if(anchor === 'middle') { // use extent of centered rect either on x or y axis depending on current axis labelMin = d.crossPos + (axIsX ? pY(offsets.y - d.by / 2) : pX(d.bx / 2 + d.tx2width / 2)); labelMax = labelMin + (axIsX ? pY(d.by) : pX(d.bx)); } else { // use extend of path (see alignHoverText function) without arrow if(axIsX) { labelMin = d.crossPos + pY(HOVERARROWSIZE + offsets.y) - pY(d.by / 2 - HOVERARROWSIZE); labelMax = labelMin + pY(d.by); } else { var startX = pX(horzSign * HOVERARROWSIZE + offsets.x); var endX = startX + pX(horzSign * d.bx); labelMin = d.crossPos + Math.min(startX, endX); labelMax = d.crossPos + Math.max(startX, endX); } } if(axIsX) { if(axisLabelMinY !== undefined && axisLabelMaxY !== undefined && Math.min(labelMax, axisLabelMaxY) - Math.max(labelMin, axisLabelMinY) > 1) { // has at least 1 pixel overlap with axis label if(crossAx.side === 'left') { pmin = crossAx._mainLinePosition; pmax = fullLayout.width; } else { pmax = crossAx._mainLinePosition; } } } else { if(axisLabelMinX !== undefined && axisLabelMaxX !== undefined && Math.min(labelMax, axisLabelMaxX) - Math.max(labelMin, axisLabelMinX) > 1) { // has at least 1 pixel overlap with axis label if(crossAx.side === 'top') { pmin = crossAx._mainLinePosition; pmax = fullLayout.height; } else { pmax = crossAx._mainLinePosition; } } } } pointgroups[k++] = [{ datum: d, traceIndex: d.trace.index, dp: 0, pos: d.pos, posref: d.posref, size: d.by * (axIsX ? YFACTOR : 1) / 2, pmin: pmin, pmax: pmax }]; }); pointgroups.sort(function(a, b) { return (a[0].posref - b[0].posref) || // for equal positions, sort trace indices increasing or decreasing // depending on whether the axis is reversed or not... so stacked // traces will generally keep their order even if one trace adds // nothing to the stack. (axSign * (b[0].traceIndex - a[0].traceIndex)); }); var donepositioning, topOverlap, bottomOverlap, i, j, pti, sumdp; function constrainGroup(grp) { var minPt = grp[0]; var maxPt = grp[grp.length - 1]; // overlap with the top - positive vals are overlaps topOverlap = minPt.pmin - minPt.pos - minPt.dp + minPt.size; // overlap with the bottom - positive vals are overlaps bottomOverlap = maxPt.pos + maxPt.dp + maxPt.size - minPt.pmax; // check for min overlap first, so that we always // see the largest labels // allow for .01px overlap, so we don't get an // infinite loop from rounding errors if(topOverlap > 0.01) { for(j = grp.length - 1; j >= 0; j--) grp[j].dp += topOverlap; donepositioning = false; } if(bottomOverlap < 0.01) return; if(topOverlap < -0.01) { // make sure we're not pushing back and forth for(j = grp.length - 1; j >= 0; j--) grp[j].dp -= bottomOverlap; donepositioning = false; } if(!donepositioning) return; // no room to fix positioning, delete off-screen points // first see how many points we need to delete var deleteCount = 0; for(i = 0; i < grp.length; i++) { pti = grp[i]; if(pti.pos + pti.dp + pti.size > minPt.pmax) deleteCount++; } // start by deleting points whose data is off screen for(i = grp.length - 1; i >= 0; i--) { if(deleteCount <= 0) break; pti = grp[i]; // pos has already been constrained to [pmin,pmax] // so look for points close to that to delete if(pti.pos > minPt.pmax - 1) { pti.del = true; deleteCount--; } } for(i = 0; i < grp.length; i++) { if(deleteCount <= 0) break; pti = grp[i]; // pos has already been constrained to [pmin,pmax] // so look for points close to that to delete if(pti.pos < minPt.pmin + 1) { pti.del = true; deleteCount--; // shift the whole group minus into this new space bottomOverlap = pti.size * 2; for(j = grp.length - 1; j >= 0; j--) grp[j].dp -= bottomOverlap; } } // then delete points that go off the bottom for(i = grp.length - 1; i >= 0; i--) { if(deleteCount <= 0) break; pti = grp[i]; if(pti.pos + pti.dp + pti.size > minPt.pmax) { pti.del = true; deleteCount--; } } } // loop through groups, combining them if they overlap, // until nothing moves while(!donepositioning && nummoves <= nLabels) { // to avoid infinite loops, don't move more times // than there are traces nummoves++; // assume nothing will move in this iteration, // reverse this if it does donepositioning = true; i = 0; while(i < pointgroups.length - 1) { // the higher (g0) and lower (g1) point group var g0 = pointgroups[i]; var g1 = pointgroups[i + 1]; // the lowest point in the higher group (p0) // the highest point in the lower group (p1) var p0 = g0[g0.length - 1]; var p1 = g1[0]; topOverlap = p0.pos + p0.dp + p0.size - p1.pos - p1.dp + p1.size; if(topOverlap > 0.01) { // push the new point(s) added to this group out of the way for(j = g1.length - 1; j >= 0; j--) g1[j].dp += topOverlap; // add them to the group g0.push.apply(g0, g1); pointgroups.splice(i + 1, 1); // adjust for minimum average movement sumdp = 0; for(j = g0.length - 1; j >= 0; j--) sumdp += g0[j].dp; bottomOverlap = sumdp / g0.length; for(j = g0.length - 1; j >= 0; j--) g0[j].dp -= bottomOverlap; donepositioning = false; } else i++; } // check if we're going off the plot on either side and fix pointgroups.forEach(constrainGroup); } // now put these offsets into hoverData for(i = pointgroups.length - 1; i >= 0; i--) { var grp = pointgroups[i]; for(j = grp.length - 1; j >= 0; j--) { var pt = grp[j]; var hoverPt = pt.datum; hoverPt.offset = pt.dp; hoverPt.del = pt.del; } } } function getHoverLabelOffsets(hoverLabel, rotateLabels) { var offsetX = 0; var offsetY = hoverLabel.offset; if(rotateLabels) { offsetY *= -YSHIFTY; offsetX = hoverLabel.offset * YSHIFTX; } return { x: offsetX, y: offsetY }; } /** * Calculate the shift in x for text and text2 elements */ function getTextShiftX(hoverLabel) { var alignShift = {start: 1, end: -1, middle: 0}[hoverLabel.anchor]; var textShiftX = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD); var text2ShiftX = textShiftX + alignShift * (hoverLabel.txwidth + HOVERTEXTPAD); var isMiddle = hoverLabel.anchor === 'middle'; if(isMiddle) { textShiftX -= hoverLabel.tx2width / 2; text2ShiftX += hoverLabel.txwidth / 2 + HOVERTEXTPAD; } return { alignShift: alignShift, textShiftX: textShiftX, text2ShiftX: text2ShiftX }; } function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) { var pX = function(x) { return x * scaleX; }; var pY = function(y) { return y * scaleY; }; // finally set the text positioning relative to the data and draw the // box around it hoverLabels.each(function(d) { var g = d3.select(this); if(d.del) return g.remove(); var tx = g.select('text.nums'); var anchor = d.anchor; var horzSign = anchor === 'end' ? -1 : 1; var shiftX = getTextShiftX(d); var offsets = getHoverLabelOffsets(d, rotateLabels); var offsetX = offsets.x; var offsetY = offsets.y; var isMiddle = anchor === 'middle'; g.select('path') .attr('d', isMiddle ? // middle aligned: rect centered on data ('M-' + pX(d.bx / 2 + d.tx2width / 2) + ',' + pY(offsetY - d.by / 2) + 'h' + pX(d.bx) + 'v' + pY(d.by) + 'h-' + pX(d.bx) + 'Z') : // left or right aligned: side rect with arrow to data ('M0,0L' + pX(horzSign * HOVERARROWSIZE + offsetX) + ',' + pY(HOVERARROWSIZE + offsetY) + 'v' + pY(d.by / 2 - HOVERARROWSIZE) + 'h' + pX(horzSign * d.bx) + 'v-' + pY(d.by) + 'H' + pX(horzSign * HOVERARROWSIZE + offsetX) + 'V' + pY(offsetY - HOVERARROWSIZE) + 'Z')); var posX = offsetX + shiftX.textShiftX; var posY = offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD; var textAlign = d.textAlign || 'auto'; if(textAlign !== 'auto') { if(textAlign === 'left' && anchor !== 'start') { tx.attr('text-anchor', 'start'); posX = isMiddle ? -d.bx / 2 - d.tx2width / 2 + HOVERTEXTPAD : -d.bx - HOVERTEXTPAD; } else if(textAlign === 'right' && anchor !== 'end') { tx.attr('text-anchor', 'end'); posX = isMiddle ? d.bx / 2 - d.tx2width / 2 - HOVERTEXTPAD : d.bx + HOVERTEXTPAD; } } tx.call(svgTextUtils.positionText, pX(posX), pY(posY)); if(d.tx2width) { g.select('text.name') .call(svgTextUtils.positionText, pX(shiftX.text2ShiftX + shiftX.alignShift * HOVERTEXTPAD + offsetX), pY(offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD)); g.select('rect') .call(Drawing.setRect, pX(shiftX.text2ShiftX + (shiftX.alignShift - 1) * d.tx2width / 2 + offsetX), pY(offsetY - d.by / 2 - 1), pX(d.tx2width), pY(d.by + 2)); } }); } function cleanPoint(d, hovermode) { var index = d.index; var trace = d.trace || {}; var cd0 = d.cd[0]; var cd = d.cd[index] || {}; function pass(v) { return v || (isNumeric(v) && v === 0); } var getVal = Array.isArray(index) ? function(calcKey, traceKey) { var v = Lib.castOption(cd0, index, calcKey); return pass(v) ? v : Lib.extractOption({}, trace, '', traceKey); } : function(calcKey, traceKey) { return Lib.extractOption(cd, trace, calcKey, traceKey); }; function fill(key, calcKey, traceKey) { var val = getVal(calcKey, traceKey); if(pass(val)) d[key] = val; } fill('hoverinfo', 'hi', 'hoverinfo'); fill('bgcolor', 'hbg', 'hoverlabel.bgcolor'); fill('borderColor', 'hbc', 'hoverlabel.bordercolor'); fill('fontFamily', 'htf', 'hoverlabel.font.family'); fill('fontSize', 'hts', 'hoverlabel.font.size'); fill('fontColor', 'htc', 'hoverlabel.font.color'); fill('fontWeight', 'htw', 'hoverlabel.font.weight'); fill('fontStyle', 'hty', 'hoverlabel.font.style'); fill('fontVariant', 'htv', 'hoverlabel.font.variant'); fill('nameLength', 'hnl', 'hoverlabel.namelength'); fill('textAlign', 'hta', 'hoverlabel.align'); d.posref = (hovermode === 'y' || (hovermode === 'closest' && trace.orientation === 'h')) ? (d.xa._offset + (d.x0 + d.x1) / 2) : (d.ya._offset + (d.y0 + d.y1) / 2); // then constrain all the positions to be on the plot d.x0 = Lib.constrain(d.x0, 0, d.xa._length); d.x1 = Lib.constrain(d.x1, 0, d.xa._length); d.y0 = Lib.constrain(d.y0, 0, d.ya._length); d.y1 = Lib.constrain(d.y1, 0, d.ya._length); // and convert the x and y label values into formatted text if(d.xLabelVal !== undefined) { d.xLabel = ('xLabel' in d) ? d.xLabel : Axes.hoverLabelText(d.xa, d.xLabelVal, trace.xhoverformat); d.xVal = d.xa.c2d(d.xLabelVal); } if(d.yLabelVal !== undefined) { d.yLabel = ('yLabel' in d) ? d.yLabel : Axes.hoverLabelText(d.ya, d.yLabelVal, trace.yhoverformat); d.yVal = d.ya.c2d(d.yLabelVal); } // Traces like heatmaps generate the zLabel in their hoverPoints function if(d.zLabelVal !== undefined && d.zLabel === undefined) { d.zLabel = String(d.zLabelVal); } // for box means and error bars, add the range to the label if(!isNaN(d.xerr) && !(d.xa.type === 'log' && d.xerr <= 0)) { var xeText = Axes.tickText(d.xa, d.xa.c2l(d.xerr), 'hover').text; if(d.xerrneg !== undefined) { d.xLabel += ' +' + xeText + ' / -' + Axes.tickText(d.xa, d.xa.c2l(d.xerrneg), 'hover').text; } else d.xLabel += ' ± ' + xeText; // small distance penalty for error bars, so that if there are // traces with errors and some without, the error bar label will // hoist up to the point if(hovermode === 'x') d.distance += 1; } if(!isNaN(d.yerr) && !(d.ya.type === 'log' && d.yerr <= 0)) { var yeText = Axes.tickText(d.ya, d.ya.c2l(d.yerr), 'hover').text; if(d.yerrneg !== undefined) { d.yLabel += ' +' + yeText + ' / -' + Axes.tickText(d.ya, d.ya.c2l(d.yerrneg), 'hover').text; } else d.yLabel += ' ± ' + yeText; if(hovermode === 'y') d.distance += 1; } var infomode = d.hoverinfo || d.trace.hoverinfo; if(infomode && infomode !== 'all') { infomode = Array.isArray(infomode) ? infomode : infomode.split('+'); if(infomode.indexOf('x') === -1) d.xLabel = undefined; if(infomode.indexOf('y') === -1) d.yLabel = undefined; if(infomode.indexOf('z') === -1) d.zLabel = undefined; if(infomode.indexOf('text') === -1) d.text = undefined; if(infomode.indexOf('name') === -1) d.name = undefined; } return d; } function createSpikelines(gd, closestPoints, opts) { var container = opts.container; var fullLayout = opts.fullLayout; var gs = fullLayout._size; var evt = opts.event; var showY = !!closestPoints.hLinePoint; var showX = !!closestPoints.vLinePoint; var xa, ya; // Remove old spikeline items container.selectAll('.spikeline').remove(); if(!(showX || showY)) return; var contrastColor = Color.combine(fullLayout.plot_bgcolor, fullLayout.paper_bgcolor); // Horizontal line (to y-axis) if(showY) { var hLinePoint = closestPoints.hLinePoint; var hLinePointX, hLinePointY; xa = hLinePoint && hLinePoint.xa; ya = hLinePoint && hLinePoint.ya; var ySnap = ya.spikesnap; if(ySnap === 'cursor') { hLinePointX = evt.pointerX; hLinePointY = evt.pointerY; } else { hLinePointX = xa._offset + hLinePoint.x; hLinePointY = ya._offset + hLinePoint.y; } var dfltHLineColor = tinycolor.readability(hLinePoint.color, contrastColor) < 1.5 ? Color.contrast(contrastColor) : hLinePoint.color; var yMode = ya.spikemode; var yThickness = ya.spikethickness; var yColor = ya.spikecolor || dfltHLineColor; var xEdge = Axes.getPxPosition(gd, ya); var xBase, xEndSpike; if(yMode.indexOf('toaxis') !== -1 || yMode.indexOf('across') !== -1) { if(yMode.indexOf('toaxis') !== -1) { xBase = xEdge; xEndSpike = hLinePointX; } if(yMode.indexOf('across') !== -1) { var xAcross0 = ya._counterDomainMin; var xAcross1 = ya._counterDomainMax; if(ya.anchor === 'free') { xAcross0 = Math.min(xAcross0, ya.position); xAcross1 = Math.max(xAcross1, ya.position); } xBase = gs.l + xAcross0 * gs.w; xEndSpike = gs.l + xAcross1 * gs.w; } // Foreground horizontal line (to y-axis) container.insert('line', ':first-child') .attr({ x1: xBase, x2: xEndSpike, y1: hLinePointY, y2: hLinePointY, 'stroke-width': yThickness, stroke: yColor, 'stroke-dasharray': Drawing.dashStyle(ya.spikedash, yThickness) }) .classed('spikeline', true) .classed('crisp', true); // Background horizontal Line (to y-axis) container.insert('line', ':first-child') .attr({ x1: xBase, x2: xEndSpike, y1: hLinePointY, y2: hLinePointY, 'stroke-width': yThickness + 2, stroke: contrastColor }) .classed('spikeline', true) .classed('crisp', true); } // Y axis marker if(yMode.indexOf('marker') !== -1) { container.insert('circle', ':first-child') .attr({ cx: xEdge + (ya.side !== 'right' ? yThickness : -yThickness), cy: hLinePointY, r: yThickness, fill: yColor }) .classed('spikeline', true); } } if(showX) { var vLinePoint = closestPoints.vLinePoint; var vLinePointX, vLinePointY; xa = vLinePoint && vLinePoint.xa; ya = vLinePoint && vLinePoint.ya; var xSnap = xa.spikesnap; if(xSnap === 'cursor') { vLinePointX = evt.pointerX; vLinePointY = evt.pointerY; } else { vLinePointX = xa._offset + vLinePoint.x; vLinePointY = ya._offset + vLinePoint.y; } var dfltVLineColor = tinycolor.readability(vLinePoint.color, contrastColor) < 1.5 ? Color.contrast(contrastColor) : vLinePoint.color; var xMode = xa.spikemode; var xThickness = xa.spikethickness; var xColor = xa.spikecolor || dfltVLineColor; var yEdge = Axes.getPxPosition(gd, xa); var yBase, yEndSpike; if(xMode.indexOf('toaxis') !== -1 || xMode.indexOf('across') !== -1) { if(xMode.indexOf('toaxis') !== -1) { yBase = yEdge; yEndSpike = vLinePointY; } if(xMode.indexOf('across') !== -1) { var yAcross0 = xa._counterDomainMin; var yAcross1 = xa._counterDomainMax; if(xa.anchor === 'free') { yAcross0 = Math.min(yAcross0, xa.position); yAcross1 = Math.max(yAcross1, xa.position); } yBase = gs.t + (1 - yAcross1) * gs.h; yEndSpike = gs.t + (1 - yAcross0) * gs.h; } // Foreground vertical line (to x-axis) container.insert('line', ':first-child') .attr({ x1: vLinePointX, x2: vLinePointX, y1: yBase, y2: yEndSpike, 'stroke-width': xThickness, stroke: xColor, 'stroke-dasharray': Drawing.dashStyle(xa.spikedash, xThickness) }) .classed('spikeline', true) .classed('crisp', true); // Background vertical line (to x-axis) container.insert('line', ':first-child') .attr({ x1: vLinePointX, x2: vLinePointX, y1: yBase, y2: yEndSpike, 'stroke-width': xThickness + 2, stroke: contrastColor }) .classed('spikeline', true) .classed('crisp', true); } // X axis marker if(xMode.indexOf('marker') !== -1) { container.insert('circle', ':first-child') .attr({ cx: vLinePointX, cy: yEdge - (xa.side !== 'top' ? xThickness : -xThickness), r: xThickness, fill: xColor }) .classed('spikeline', true); } } } function hoverChanged(gd, evt, oldhoverdata) { // don't emit any events if nothing changed if(!oldhoverdata || oldhoverdata.length !== gd._hoverdata.length) return true; for(var i = oldhoverdata.length - 1; i >= 0; i--) { var oldPt = oldhoverdata[i]; var newPt = gd._hoverdata[i]; if(oldPt.curveNumber !== newPt.curveNumber || String(oldPt.pointNumber) !== String(newPt.pointNumber) || String(oldPt.pointNumbers) !== String(newPt.pointNumbers) ) { return true; } } return false; } function spikesChanged(gd, oldspikepoints) { // don't relayout the plot because of new spikelines if spikelines points didn't change if(!oldspikepoints) return true; if(oldspikepoints.vLinePoint !== gd._spikepoints.vLinePoint || oldspikepoints.hLinePoint !== gd._spikepoints.hLinePoint ) return true; return false; } function plainText(s, len) { return svgTextUtils.plainText(s || '', { len: len, allowedTags: ['br', 'sub', 'sup', 'b', 'i', 'em', 's', 'u'] }); } function orderRangePoints(hoverData, hovermode) { var axLetter = hovermode.charAt(0); var first = []; var second = []; var last = []; for(var i = 0; i < hoverData.length; i++) { var d = hoverData[i]; if( Registry.traceIs(d.trace, 'bar-like') || Registry.traceIs(d.trace, 'box-violin') ) { last.push(d); } else if(d.trace[axLetter + 'period']) { second.push(d); } else { first.push(d); } } return first.concat(second).concat(last); } function getCoord(axLetter, winningPoint, fullLayout) { var ax = winningPoint[axLetter + 'a']; var val = winningPoint[axLetter + 'Val']; var cd0 = winningPoint.cd[0]; if(ax.type === 'category' || ax.type === 'multicategory') val = ax._categoriesMap[val]; else if(ax.type === 'date') { var periodalignment = winningPoint.trace[axLetter + 'periodalignment']; if(periodalignment) { var d = winningPoint.cd[winningPoint.index]; var start = d[axLetter + 'Start']; if(start === undefined) start = d[axLetter]; var end = d[axLetter + 'End']; if(end === undefined) end = d[axLetter]; var diff = end - start; if(periodalignment === 'end') { val += diff; } else if(periodalignment === 'middle') { val += diff / 2; } } val = ax.d2c(val); } if(cd0 && cd0.t && cd0.t.posLetter === ax._id) { if( fullLayout.boxmode === 'group' || fullLayout.violinmode === 'group' ) { val += cd0.t.dPos; } } return val; } // Top/left hover offsets relative to graph div. As long as hover content is // a sibling of the graph div, it will be positioned correctly relative to // the offset parent, whatever that may be. function getTopOffset(gd) { return gd.offsetTop + gd.clientTop; } function getLeftOffset(gd) { return gd.offsetLeft + gd.clientLeft; } function getBoundingClientRect(gd, node) { var fullLayout = gd._fullLayout; var rect = node.getBoundingClientRect(); var x0 = rect.left; var y0 = rect.top; var x1 = x0 + rect.width; var y1 = y0 + rect.height; var A = Lib.apply3DTransform(fullLayout._invTransform)(x0, y0); var B = Lib.apply3DTransform(fullLayout._invTransform)(x1, y1); var Ax = A[0]; var Ay = A[1]; var Bx = B[0]; var By = B[1]; return { x: Ax, y: Ay, width: Bx - Ax, height: By - Ay, top: Math.min(Ay, By), left: Math.min(Ax, Bx), right: Math.max(Ax, Bx), bottom: Math.max(Ay, By), }; }