Skip to content
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

Fix hover with period alignment points and improve positioning of spikes and unified hover label #5846

Merged
merged 28 commits into from
Jul 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b4bdec3
fix scattergl test
archmoj Jul 20, 2021
53fea8e
fix hover on scatter and scattergl with periods
archmoj Jul 20, 2021
2933803
fix hover on bars with period
archmoj Jul 21, 2021
775a8e0
improve and adjust jasmine tests
archmoj Jul 21, 2021
168bc75
draft log for PR 5846
archmoj Jul 21, 2021
3deca0d
drop unused line
archmoj Jul 21, 2021
7c37e98
fix typo and adjust test
archmoj Jul 21, 2021
e5a4057
scatters with period ranges
archmoj Jul 21, 2021
bd528a2
adjust scatter and bar test titles
archmoj Jul 21, 2021
85821d1
fit -> it
archmoj Jul 21, 2021
9fbd186
refactor: enter long lines
archmoj Jul 22, 2021
8a36d8f
position hover labels in respect to max and min of hoverset not the w…
archmoj Jul 22, 2021
f128cf9
for end alignment of unified hover label use minimum to avoid overlap…
archmoj Jul 22, 2021
b26fcbb
similar positioning of hover box for y unified
archmoj Jul 22, 2021
5a80515
refactor
archmoj Jul 22, 2021
9716ef0
improve unified hover box positioning
archmoj Jul 22, 2021
c62d520
edit PR log
archmoj Jul 22, 2021
21a1a93
simplify logic and handle the case of not fitting inside the subplot
archmoj Jul 22, 2021
16f4554
when the scatter point wins, OK to occlude bar & other points
archmoj Jul 22, 2021
8ed847a
fix syntax
archmoj Jul 22, 2021
30f61c8
drop out of range pixel from the end
archmoj Jul 22, 2021
dca5b1e
for x|y hovermodes display spikeline on the winning point
archmoj Jul 22, 2021
df6353d
adjust vertical and horizontal alignment of unified hover label
archmoj Jul 23, 2021
9217db4
handle end edge case
archmoj Jul 23, 2021
1c7a69d
adjustment also for winning types e.g. heatmap use scatter logic
archmoj Jul 23, 2021
f0ef7db
choose the side of the paper which is closest if unified hover did no…
archmoj Jul 23, 2021
4a0a504
update PR log
archmoj Jul 23, 2021
cd0b469
move unified hover by one pixel better help adjustment tested with pe…
archmoj Jul 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions draftlogs/5846_fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Fix hover with period alignment points and improve positioning of spikes and unified hover label
in order not to obscure referring data points [[#5846](https://github.com/plotly/plotly.js/pull/5846)]
141 changes: 102 additions & 39 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ var multipleHoverPoints = {
candlestick: true
};

var cartesianScatterPoints = {
scatter: true,
scattergl: true,
splom: true
};

// fx.hover: highlight data on hover
// evt can be a mousemove event, or an object with data about what points
// to hover on
Expand Down Expand Up @@ -574,12 +580,15 @@ function _hover(gd, evt, subplot, noHoverEvent) {

findHoverPoints();

function selectClosestPoint(pointsData, spikedistance) {
function selectClosestPoint(pointsData, spikedistance, spikeOnWinning) {
var resultPoint = null;
var minDistance = Infinity;
var thisSpikeDistance;

for(var i = 0; i < pointsData.length; i++) {
thisSpikeDistance = pointsData[i].spikeDistance;
if(spikeOnWinning && i === 0) thisSpikeDistance = -Infinity;

if(thisSpikeDistance <= minDistance && thisSpikeDistance <= spikedistance) {
resultPoint = pointsData[i];
minDistance = thisSpikeDistance;
Expand Down Expand Up @@ -616,19 +625,30 @@ function _hover(gd, evt, subplot, noHoverEvent) {
};
gd._spikepoints = newspikepoints;

var sortHoverData = function() {
hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; });

// 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);
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);
var tmpVPoint = selectClosestPoint(tmpVPointData, spikedistance, spikeOnWinning);
spikePoints.vLinePoint = fillSpikePoint(tmpVPoint);
}
}
Expand All @@ -650,14 +670,6 @@ function _hover(gd, evt, subplot, noHoverEvent) {
}
}

var sortHoverData = function() {
hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; });

// move period positioned points and box/bar-like traces to the end of the list
hoverData = orderRangePoints(hoverData, hovermode);
};
sortHoverData();

if(
helpers.isXYhover(_mode) &&
hoverData[0].length !== 0 &&
Expand Down Expand Up @@ -1071,41 +1083,89 @@ function createHoverText(hoverData, opts, gd) {
legendDraw(gd, mockLegend);

// Position the hover
var winningPoint = hoverData[0];
var ly = axLetter === 'y' ?
(winningPoint.y0 + winningPoint.y1) / 2 : Lib.mean(hoverData.map(function(c) {return (c.y0 + c.y1) / 2;}));
var lx = axLetter === 'x' ?
(winningPoint.x0 + winningPoint.x1) / 2 : Lib.mean(hoverData.map(function(c) {return (c.x0 + c.x1) / 2;}));

var legendContainer = container.select('g.legend');
var tbb = legendContainer.node().getBoundingClientRect();
lx += xa._offset;
ly += ya._offset - tbb.height / 2;

// Change horizontal alignment to end up on screen
var txWidth = tbb.width + 2 * HOVERTEXTPAD;
var anchorStartOK = lx + txWidth <= outerWidth;
var anchorEndOK = lx - txWidth >= 0;
if(!anchorStartOK && anchorEndOK) {
lx -= txWidth;
var tWidth = tbb.width + 2 * HOVERTEXTPAD;
var tHeight = tbb.height + 2 * HOVERTEXTPAD;
var winningPoint = hoverData[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, hoverData.map(function(c) { return Math.min(c.y0, c.y1); }));
lyBottom = Math.max.apply(null, hoverData.map(function(c) { return Math.max(c.y0, c.y1); }));
}
} else {
lyTop = lyBottom = Lib.mean(hoverData.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, hoverData.map(function(c) { return Math.max(c.x0, c.x1); }));
lxLeft = Math.min.apply(null, hoverData.map(function(c) { return Math.min(c.x0, c.x1); }));
}
} else {
lx += 2 * HOVERTEXTPAD;
lxRight = lxLeft = Lib.mean(hoverData.map(function(c) { return (c.x0 + c.x1) / 2; })) - tWidth / 2;
}

// Change vertical alignement to end up on screen
var txHeight = tbb.height + 2 * HOVERTEXTPAD;
var overflowTop = ly <= outerTop;
var overflowBottom = ly + txHeight >= outerHeight;
var canFit = txHeight <= outerHeight;
if(canFit) {
if(overflowTop) {
ly = ya._offset + 2 * HOVERTEXTPAD;
} else if(overflowBottom) {
ly = outerHeight - txHeight;
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;
}
}
legendContainer.attr('transform', strTranslate(lx, ly));
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;
}

Expand Down Expand Up @@ -1934,7 +1994,10 @@ function getCoord(axLetter, winningPoint, fullLayout) {
var val = winningPoint[axLetter + 'Val'];

if(ax.type === 'category') val = ax._categoriesMap[val];
else if(ax.type === 'date') val = ax.d2c(val);
else if(ax.type === 'date') {
var period = winningPoint[axLetter + 'Period'];
val = ax.d2c(period !== undefined ? period : val);
}

var cd0 = winningPoint.cd[winningPoint.index];
if(cd0 && cd0.t && cd0.t.posLetter === ax._id) {
Expand Down
21 changes: 16 additions & 5 deletions src/plots/cartesian/align_period.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@ var constants = require('../../constants/numerical');
var ONEAVGMONTH = constants.ONEAVGMONTH;

module.exports = function alignPeriod(trace, ax, axLetter, vals) {
if(ax.type !== 'date') return vals;
if(ax.type !== 'date') return {vals: vals};

var alignment = trace[axLetter + 'periodalignment'];
if(!alignment) return vals;
if(!alignment) return {vals: vals};

var period = trace[axLetter + 'period'];
var mPeriod;
if(isNumeric(period)) {
period = +period;
if(period <= 0) return vals;
if(period <= 0) return {vals: vals};
} else if(typeof period === 'string' && period.charAt(0) === 'M') {
var n = +(period.substring(1));
if(n > 0 && Math.round(n) === n) {
mPeriod = n;
} else return vals;
} else return {vals: vals};
}

var calendar = ax.calendar;
Expand All @@ -35,6 +35,9 @@ module.exports = function alignPeriod(trace, ax, axLetter, vals) {
var base = dateTime2ms(period0, calendar) || 0;

var newVals = [];
var starts = [];
var ends = [];

var len = vals.length;
for(var i = 0; i < len; i++) {
var v = vals[i];
Expand Down Expand Up @@ -77,6 +80,14 @@ module.exports = function alignPeriod(trace, ax, axLetter, vals) {
isEnd ? endTime :
(startTime + endTime) / 2
);

starts[i] = startTime;
ends[i] = endTime;
}
return newVals;

return {
vals: newVals,
starts: starts,
ends: ends
};
};
10 changes: 6 additions & 4 deletions src/traces/bar/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,24 @@ var calcSelection = require('../scatter/calc_selection');
module.exports = function calc(gd, trace) {
var xa = Axes.getFromId(gd, trace.xaxis || 'x');
var ya = Axes.getFromId(gd, trace.yaxis || 'y');
var size, pos, origPos;
var size, pos, origPos, pObj, hasPeriod;

var sizeOpts = {
msUTC: !!(trace.base || trace.base === 0)
};

var hasPeriod;
if(trace.orientation === 'h') {
size = xa.makeCalcdata(trace, 'x', sizeOpts);
origPos = ya.makeCalcdata(trace, 'y');
pos = alignPeriod(trace, ya, 'y', origPos);
pObj = alignPeriod(trace, ya, 'y', origPos);
hasPeriod = !!trace.yperiodalignment;
} else {
size = ya.makeCalcdata(trace, 'y', sizeOpts);
origPos = xa.makeCalcdata(trace, 'x');
pos = alignPeriod(trace, xa, 'x', origPos);
pObj = alignPeriod(trace, xa, 'x', origPos);
hasPeriod = !!trace.xperiodalignment;
}
pos = pObj.vals;

// create the "calculated data" to plot
var serieslen = Math.min(pos.length, size.length);
Expand All @@ -39,6 +39,8 @@ module.exports = function calc(gd, trace) {

if(hasPeriod) {
cd[i].orig_p = origPos[i]; // used by hover
cd[i].pEnd = pObj.ends[i];
cd[i].pStart = pObj.starts[i];
}

if(trace.ids) {
Expand Down
8 changes: 8 additions & 0 deletions src/traces/bar/cross_trace_calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,12 +436,20 @@ function setBarCenterAndWidth(pa, sieve) {
var barwidth = t.barwidth;
var barwidthIsArray = Array.isArray(barwidth);

var trace = calcTrace[0].trace;
var isPeriod = !!trace[pLetter + 'periodalignment'];

for(var j = 0; j < calcTrace.length; j++) {
var calcBar = calcTrace[j];

// store the actual bar width and position, for use by hover
var width = calcBar.w = barwidthIsArray ? barwidth[j] : barwidth;
calcBar[pLetter] = calcBar.p + (poffsetIsArray ? poffset[j] : poffset) + width / 2;

if(isPeriod) {
calcBar.wPeriod =
calcBar.pEnd - calcBar.pStart;
}
}
}
}
Expand Down
10 changes: 6 additions & 4 deletions src/traces/bar/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,9 @@ function hoverOnBars(pointData, xval, yval, hovermode, opts) {
function thisBarMaxPos(di) { return thisBarExtPos(di, 1); }

function thisBarExtPos(di, sgn) {
if(period) {
return di.p + sgn * Math.abs(di.p - di.orig_p);
}
return di[posLetter] + sgn * di.w / 2;
var w = (period) ? di.wPeriod : di.w;

return di[posLetter] + sgn * w / 2;
}

var minPos = isClosest || period ?
Expand Down Expand Up @@ -180,6 +179,9 @@ function hoverOnBars(pointData, xval, yval, hovermode, opts) {

var hasPeriod = di.orig_p !== undefined;
pointData[posLetter + 'LabelVal'] = hasPeriod ? di.orig_p : di.p;
if(hasPeriod) {
pointData[posLetter + 'Period'] = di.p;
}

pointData.labelLabel = hoverLabelText(pa, pointData[posLetter + 'LabelVal'], trace[posLetter + 'hoverformat']);
pointData.valueLabel = hoverLabelText(sa, pointData[sizeLetter + 'LabelVal'], trace[sizeLetter + 'hoverformat']);
Expand Down
2 changes: 1 addition & 1 deletion src/traces/box/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ function getPosArrays(trace, posLetter, posAxis, num) {

if(hasPosArray || (hasPos0 && hasPosStep)) {
var origPos = posAxis.makeCalcdata(trace, posLetter);
var pos = alignPeriod(trace, posAxis, posLetter, origPos);
var pos = alignPeriod(trace, posAxis, posLetter, origPos).vals;
return [pos, origPos];
}

Expand Down
2 changes: 1 addition & 1 deletion src/traces/candlestick/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module.exports = function(gd, trace) {
var ya = Axes.getFromId(gd, trace.yaxis);

var origX = xa.makeCalcdata(trace, 'x');
var x = alignPeriod(trace, xa, 'x', origX);
var x = alignPeriod(trace, xa, 'x', origX).vals;

var cd = calcCommon(gd, trace, origX, x, ya, ptFunc);

Expand Down
10 changes: 6 additions & 4 deletions src/traces/funnel/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ var BADNUM = require('../../constants/numerical').BADNUM;
module.exports = function calc(gd, trace) {
var xa = Axes.getFromId(gd, trace.xaxis || 'x');
var ya = Axes.getFromId(gd, trace.yaxis || 'y');
var size, pos, origPos, i, cdi;
var size, pos, origPos, pObj, hasPeriod, i, cdi;

var hasPeriod;
if(trace.orientation === 'h') {
size = xa.makeCalcdata(trace, 'x');
origPos = ya.makeCalcdata(trace, 'y');
pos = alignPeriod(trace, ya, 'y', origPos);
pObj = alignPeriod(trace, ya, 'y', origPos);
hasPeriod = !!trace.yperiodalignment;
} else {
size = ya.makeCalcdata(trace, 'y');
origPos = xa.makeCalcdata(trace, 'x');
pos = alignPeriod(trace, xa, 'x', origPos);
pObj = alignPeriod(trace, xa, 'x', origPos);
hasPeriod = !!trace.xperiodalignment;
}
pos = pObj.vals;

// create the "calculated data" to plot
var serieslen = Math.min(pos.length, size.length);
Expand Down Expand Up @@ -55,6 +55,8 @@ module.exports = function calc(gd, trace) {

if(hasPeriod) {
cd[i].orig_p = origPos[i]; // used by hover
cd[i].pEnd = pObj.ends[i];
cd[i].pStart = pObj.starts[i];
}

if(trace.ids) {
Expand Down
Loading