Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
package.src.components.fx.hover.js Maven / Gradle / Ivy
Go to download
The open source javascript graphing library that powers plotly
'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') {
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),
};
}