-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generalize spikelines #2379
Generalize spikelines #2379
Changes from 1 commit
ed19b7c
e2c8c04
965bb88
d34ba81
5164cdf
a2fc07a
ad38f8d
eb98c91
31e4e34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,19 +62,16 @@ exports.getClosest = function getClosest(cd, distfn, pointData) { | |
return pointData; | ||
}; | ||
|
||
// for bar charts and others with finite-size objects: you must be inside | ||
// it to see its hover info, so distance is infinite outside. | ||
// But make distance inside be at least 1/4 MAXDIST, and a little bigger | ||
// for bigger bars, to prioritize scatter and smaller bars over big bars | ||
// | ||
// note that for closest mode, two inbox's will get added in quadrature | ||
// args are (signed) difference from the two opposite edges | ||
// count one edge as in, so that over continuous ranges you never get a gap | ||
exports.inbox = function inbox(v0, v1) { | ||
if(v0 * v1 < 0 || v0 === 0) { | ||
return constants.MAXDIST * (0.6 - 0.3 / Math.max(3, Math.abs(v0 - v1))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This built-in constant return value for |
||
} | ||
return Infinity; | ||
/* | ||
* pseudo-distance function for hover effects on areas: inside the region | ||
* distance is finite (`passVal`), outside it's Infinity. | ||
* | ||
* @param {number} v0: signed difference between the current position and the left edge | ||
* @param {number} v1: signed difference between the current position and the right edge | ||
* @param {number} passVal: the value to return on success | ||
*/ | ||
exports.inbox = function inbox(v0, v1, passVal) { | ||
return (v0 * v1 < 0 || v0 === 0) ? passVal : Infinity; | ||
}; | ||
|
||
exports.quadrature = function quadrature(dx, dy) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -144,8 +144,7 @@ exports.loneHover = function loneHover(hoverItem, opts) { | |
rotateLabels: false, | ||
bgColor: opts.bgColor || Color.background, | ||
container: container3, | ||
outerContainer: outerContainer3, | ||
hoverdistance: constants.MAXDIST | ||
outerContainer: outerContainer3 | ||
}; | ||
|
||
var hoverLabel = createHoverText([pointData], fullOpts, opts.gd); | ||
|
@@ -162,9 +161,10 @@ function _hover(gd, evt, subplot, noHoverEvent) { | |
// use those instead of finding overlayed plots | ||
var subplots = Array.isArray(subplot) ? subplot : [subplot]; | ||
|
||
var fullLayout = gd._fullLayout, | ||
plots = fullLayout._plots || [], | ||
plotinfo = plots[subplot]; | ||
var fullLayout = gd._fullLayout; | ||
var plots = fullLayout._plots || []; | ||
var plotinfo = plots[subplot]; | ||
var hasCartesian = fullLayout._has('cartesian'); | ||
|
||
// list of all overlaid subplots to look at | ||
if(plotinfo) { | ||
|
@@ -351,9 +351,29 @@ function _hover(gd, evt, subplot, noHoverEvent) { | |
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, | ||
|
@@ -418,29 +438,37 @@ function _hover(gd, evt, subplot, noHoverEvent) { | |
} | ||
|
||
// in closest mode, remove any existing (farther) points | ||
// and don't look any farther than this latest point (or points, if boxes) | ||
// 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(fullLayout._has('cartesian') && (spikedistance !== 0)) { | ||
if(hasCartesian && (spikedistance !== 0)) { | ||
if(hoverData.length === 0) { | ||
pointData.distance = spikedistance; | ||
pointData.index = false; | ||
var closestPoints = trace._module.hoverPoints(pointData, xval, yval, 'closest', 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; | ||
}); | ||
if(closestVPoints.length) { | ||
var closestVPt = closestVPoints[0]; | ||
if(isNumeric(closestVPt.x0) && isNumeric(closestVPt.y0)) { | ||
tmpPoint = fillClosestPoint(closestVPt); | ||
if(!spikePoints.vLinePoint || (spikePoints.vLinePoint.distance > tmpPoint.distance)) { | ||
tmpPoint = fillSpikePoint(closestVPt); | ||
if(!spikePoints.vLinePoint || (spikePoints.vLinePoint.spikeDistance > tmpPoint.spikeDistance)) { | ||
spikePoints.vLinePoint = tmpPoint; | ||
} | ||
} | ||
|
@@ -452,8 +480,8 @@ function _hover(gd, evt, subplot, noHoverEvent) { | |
if(closestHPoints.length) { | ||
var closestHPt = closestHPoints[0]; | ||
if(isNumeric(closestHPt.x0) && isNumeric(closestHPt.y0)) { | ||
tmpPoint = fillClosestPoint(closestHPt); | ||
if(!spikePoints.hLinePoint || (spikePoints.hLinePoint.distance > tmpPoint.distance)) { | ||
tmpPoint = fillSpikePoint(closestHPt); | ||
if(!spikePoints.hLinePoint || (spikePoints.hLinePoint.spikeDistance > tmpPoint.spikeDistance)) { | ||
spikePoints.hLinePoint = tmpPoint; | ||
} | ||
} | ||
|
@@ -464,47 +492,28 @@ function _hover(gd, evt, subplot, noHoverEvent) { | |
} | ||
|
||
function selectClosestPoint(pointsData, spikedistance) { | ||
if(!pointsData.length) return null; | ||
var resultPoint; | ||
var pointsDistances = pointsData.map(function(point, index) { | ||
var xa = point.xa, | ||
ya = point.ya, | ||
xpx = xa.c2p(xval), | ||
ypx = ya.c2p(yval), | ||
dxy = function(point) { | ||
var rad = point.kink, | ||
dx = (point.x1 + point.x0) / 2 - xpx, | ||
dy = (point.y1 + point.y0) / 2 - ypx; | ||
return Math.max(Math.sqrt(dx * dx + dy * dy) - rad, 1 - 3 / rad); | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're not replicating the distance function here, because that only works for |
||
var distance = dxy(point); | ||
return {distance: distance, index: index}; | ||
}); | ||
pointsDistances = pointsDistances | ||
.filter(function(point) { | ||
return point.distance <= spikedistance; | ||
}) | ||
.sort(function(a, b) { | ||
return a.distance - b.distance; | ||
}); | ||
if(pointsDistances.length) { | ||
resultPoint = pointsData[pointsDistances[0].index]; | ||
} else { | ||
resultPoint = null; | ||
var resultPoint = null; | ||
var minDistance = Infinity; | ||
var thisSpikeDistance; | ||
for(var i = 0; i < pointsData.length; i++) { | ||
thisSpikeDistance = pointsData[i].spikeDistance; | ||
if(thisSpikeDistance < minDistance && thisSpikeDistance <= spikedistance) { | ||
resultPoint = pointsData[i]; | ||
minDistance = thisSpikeDistance; | ||
} | ||
} | ||
return resultPoint; | ||
} | ||
|
||
function fillClosestPoint(point) { | ||
function fillSpikePoint(point) { | ||
if(!point) return null; | ||
return { | ||
xa: point.xa, | ||
ya: point.ya, | ||
x0: point.x0, | ||
x1: point.x1, | ||
y0: point.y0, | ||
y1: point.y1, | ||
x: point.xSpike !== undefined ? point.xSpike : (point.x0 + point.x1) / 2, | ||
y: point.ySpike !== undefined ? point.ySpike : (point.y0 + point.y1) / 2, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a module wants spikes NOT to just be drawn at the midpoint of the box hover labels are attached to, it just needs to specify There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Brilliant ✨ |
||
distance: point.distance, | ||
spikeDistance: point.spikeDistance, | ||
curveNumber: point.trace.index, | ||
color: point.color, | ||
pointNumber: point.index | ||
|
@@ -525,34 +534,34 @@ function _hover(gd, evt, subplot, noHoverEvent) { | |
gd._spikepoints = newspikepoints; | ||
|
||
// Now if it is not restricted by spikedistance option, set the points to draw the spikelines | ||
if(fullLayout._has('cartesian') && (spikedistance !== 0)) { | ||
if(hasCartesian && (spikedistance !== 0)) { | ||
if(hoverData.length !== 0) { | ||
var tmpHPointData = hoverData.filter(function(point) { | ||
return point.ya.showspikes; | ||
}); | ||
var tmpHPoint = selectClosestPoint(tmpHPointData, spikedistance); | ||
spikePoints.hLinePoint = fillClosestPoint(tmpHPoint); | ||
spikePoints.hLinePoint = fillSpikePoint(tmpHPoint); | ||
|
||
var tmpVPointData = hoverData.filter(function(point) { | ||
return point.xa.showspikes; | ||
}); | ||
var tmpVPoint = selectClosestPoint(tmpVPointData, spikedistance); | ||
spikePoints.vLinePoint = fillClosestPoint(tmpVPoint); | ||
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(fullLayout._has('cartesian') && ((spikePoints.hLinePoint !== null) || (spikePoints.vLinePoint !== null))) { | ||
if(hasCartesian && ((spikePoints.hLinePoint !== null) || (spikePoints.vLinePoint !== null))) { | ||
if(spikesChanged(oldspikepoints)) { | ||
createSpikelines(spikePoints, spikelineOpts); | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
if(fullLayout._has('cartesian')) { | ||
if(hasCartesian) { | ||
if(spikesChanged(oldspikepoints)) { | ||
createSpikelines(spikePoints, spikelineOpts); | ||
} | ||
|
@@ -653,20 +662,31 @@ function createHoverText(hoverData, opts, gd) { | |
// 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 = c0.distance <= opts.hoverdistance && | ||
(hovermode === 'x' || hovermode === 'y'); | ||
var showCommonLabel = ( | ||
(t0 !== undefined) && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this extra There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Related to #2379 (comment) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good to know. Thanks! |
||
(c0.distance <= opts.hoverdistance) && | ||
(hovermode === 'x' || hovermode === 'y') | ||
); | ||
|
||
// all hover traces hoverinfo must contain the hovermode | ||
// to have common labels | ||
var i, traceHoverinfo; | ||
for(i = 0; i < hoverData.length; i++) { | ||
traceHoverinfo = hoverData[i].hoverinfo || hoverData[i].trace.hoverinfo; | ||
var parts = Array.isArray(traceHoverinfo) ? traceHoverinfo : traceHoverinfo.split('+'); | ||
if(parts.indexOf('all') === -1 && | ||
parts.indexOf(hovermode) === -1) { | ||
showCommonLabel = false; | ||
break; | ||
if(showCommonLabel) { | ||
var i, traceHoverinfo; | ||
var allHaveZ = true; | ||
for(i = 0; i < hoverData.length; i++) { | ||
if(allHaveZ && hoverData[i].zLabel === undefined) allHaveZ = false; | ||
|
||
traceHoverinfo = hoverData[i].hoverinfo || hoverData[i].trace.hoverinfo; | ||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic about whether to show the common label has gotten a bit more complex... previously it was being implicitly handled by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This sounds like the most robust way forward down the road 👍 |
||
} | ||
|
||
var commonLabel = container.selectAll('g.axistext') | ||
|
@@ -1262,8 +1282,8 @@ function createSpikelines(closestPoints, opts) { | |
hLinePointX = evt.pointerX; | ||
hLinePointY = evt.pointerY; | ||
} else { | ||
hLinePointX = xa._offset + (hLinePoint.x0 + hLinePoint.x1) / 2; | ||
hLinePointY = ya._offset + (hLinePoint.y0 + hLinePoint.y1) / 2; | ||
hLinePointX = xa._offset + hLinePoint.x; | ||
hLinePointY = ya._offset + hLinePoint.y; | ||
} | ||
var dfltHLineColor = tinycolor.readability(hLinePoint.color, contrastColor) < 1.5 ? | ||
Color.contrast(contrastColor) : hLinePoint.color; | ||
|
@@ -1338,8 +1358,8 @@ function createSpikelines(closestPoints, opts) { | |
vLinePointX = evt.pointerX; | ||
vLinePointY = evt.pointerY; | ||
} else { | ||
vLinePointX = xa._offset + (vLinePoint.x0 + vLinePoint.x1) / 2; | ||
vLinePointY = ya._offset + (vLinePoint.y0 + vLinePoint.y1) / 2; | ||
vLinePointX = xa._offset + vLinePoint.x; | ||
vLinePointY = ya._offset + vLinePoint.y; | ||
} | ||
var dfltVLineColor = tinycolor.readability(vLinePoint.color, contrastColor) < 1.5 ? | ||
Color.contrast(contrastColor) : vLinePoint.color; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,8 +19,10 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { | |
var trace = cd[0].trace; | ||
var t = cd[0].t; | ||
var isClosest = (hovermode === 'closest'); | ||
var maxHoverDistance = pointData.maxHoverDistance; | ||
var maxSpikeDistance = pointData.maxSpikeDistance; | ||
|
||
var posVal, sizeVal, posLetter, sizeLetter, dx, dy; | ||
var posVal, sizeVal, posLetter, sizeLetter, dx, dy, pRangeCalc; | ||
|
||
function thisBarMinPos(di) { return di[posLetter] - di.w / 2; } | ||
function thisBarMaxPos(di) { return di[posLetter] + di.w / 2; } | ||
|
@@ -49,15 +51,24 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { | |
return Math.max(thisBarMaxPos(di), di.p + t.bardelta / 2); | ||
}; | ||
|
||
function _positionFn(_minPos, _maxPos) { | ||
return Fx.inbox(_minPos - posVal, _maxPos - posVal, | ||
maxHoverDistance - Math.min(1, Math.abs(_maxPos - _minPos) / pRangeCalc)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you mind adding a one-line comment description what this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, good call - it was actually wrong -> ad38f8d |
||
} | ||
|
||
function positionFn(di) { | ||
return Fx.inbox(minPos(di) - posVal, maxPos(di) - posVal); | ||
return _positionFn(minPos(di), maxPos(di)); | ||
} | ||
|
||
function thisBarPositionFn(di) { | ||
return _positionFn(thisBarMinPos(di), thisBarMaxPos(di)); | ||
} | ||
|
||
function sizeFn(di) { | ||
// add a gradient so hovering near the end of a | ||
// bar makes it a little closer match | ||
return Fx.inbox(di.b - sizeVal, di[sizeLetter] - sizeVal) + | ||
(di[sizeLetter] - sizeVal) / (di[sizeLetter] - di.b); | ||
return Fx.inbox(di.b - sizeVal, di[sizeLetter] - sizeVal, | ||
maxHoverDistance + (di[sizeLetter] - sizeVal) / (di[sizeLetter] - di.b) - 1); | ||
} | ||
|
||
if(trace.orientation === 'h') { | ||
|
@@ -80,7 +91,10 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { | |
var pa = pointData[posLetter + 'a']; | ||
var sa = pointData[sizeLetter + 'a']; | ||
|
||
var distfn = Fx.getDistanceFunction(hovermode, dx, dy); | ||
pRangeCalc = Math.abs(pa.r2c(pa.range[1]) - pa.r2c(pa.range[0])); | ||
|
||
function dxy(di) { return (dx(di) + dy(di)) / 2; } | ||
var distfn = Fx.getDistanceFunction(hovermode, dx, dy, dxy); | ||
Fx.getClosest(cd, distfn, pointData); | ||
|
||
// skip the rest (for this trace) if we didn't find a close point | ||
|
@@ -116,6 +130,12 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { | |
pointData[posLetter + '1'] = pa.c2p(maxPos(di), true); | ||
pointData[posLetter + 'LabelVal'] = di.p; | ||
|
||
// spikelines always want "closest" distance regardless of hovermode | ||
pointData.spikeDistance = (sizeFn(di) + thisBarPositionFn(di)) / 2 + maxSpikeDistance - maxHoverDistance; | ||
// they also want to point to the data value, regardless of where the label goes | ||
// in case of bars shifted within groups | ||
pointData[posLetter + 'Spike'] = pa.c2p(di.p, true); | ||
|
||
fillHoverText(di, trace, pointData); | ||
ErrorBars.hoverInfo(di, trace, pointData); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not using this constant anymore, we just have the defaults in
layout.hoverdistance
andlayout.spikedistance
, with which the users of this constant were in conflict.