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

unified hover label #4620

Merged
merged 28 commits into from
Mar 13, 2020
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
18ddd5d
hover: move text logic into function getHoverLabelText()
antoinerg Mar 6, 2020
1c92511
introduce hovermode 'x unified' and 'y unified'
antoinerg Mar 6, 2020
e43b197
unified hoverlabel: add smart defaults for spikelines
antoinerg Mar 6, 2020
2bc9c77
unified hoverlabel: test label's background color
antoinerg Mar 9, 2020
3fe5951
unified hoverlabel: do not display trace name if it's empty
antoinerg Mar 9, 2020
88bb33a
unified hoverlabel: fix positioning for edge cases
antoinerg Mar 9, 2020
07fb8af
unified hoverlabel: test finance trace
antoinerg Mar 9, 2020
a704769
unified hoverlabel: fix for cases when hoverData is empty
antoinerg Mar 9, 2020
9ea8bfd
unified hoverlabel: revert changes to legend/defaults.js
antoinerg Mar 10, 2020
36bc6b5
unified hoverlabel: use stricter comparison operators
antoinerg Mar 10, 2020
e82e010
unified hoverlabel: :hocho: unused mock/baseline
antoinerg Mar 10, 2020
818223d
hover: revert unnecessary modification
antoinerg Mar 10, 2020
4bf9052
unified hoverlabel: title font should be the same as fullLayout.font
antoinerg Mar 10, 2020
609d5ca
unified hoverlabel: do not process MathJax in hover since it's not su…
antoinerg Mar 10, 2020
ec8c373
unified hoverlabel: style legend item using marker's style
antoinerg Mar 10, 2020
0822b93
unified hoverlabel: inherit traceorder from the the legend
antoinerg Mar 11, 2020
ae5b9cc
unified hoverlabel: fix filtering logic
antoinerg Mar 11, 2020
c0e1bf7
unified hoverlabel: fix waterfall symbol
antoinerg Mar 11, 2020
bca4a70
unified hoverlabel: test waterfall symbol
antoinerg Mar 11, 2020
8308111
unified hoverlabel: only apply bar-like styling of symbol to waterfall
antoinerg Mar 11, 2020
33a037b
unified hoverlabel: fix code style
antoinerg Mar 11, 2020
a9c899b
unified hoverlabel: return early if nothing is hovered on
antoinerg Mar 11, 2020
06ad9a7
unified hoverlabel: swap hovermode in orientationaxes block
antoinerg Mar 11, 2020
53c5714
unified hoverlabel: rank points based on trace's index
antoinerg Mar 11, 2020
dd86a60
unified hoverlabel: fix test
antoinerg Mar 11, 2020
a857d71
unified hoverlabel: update description
antoinerg Mar 13, 2020
05188d6
unified hoverlabel: fix description
antoinerg Mar 13, 2020
af4a07c
unified hoverlabel: remove modebar hover buttons when activated
antoinerg Mar 13, 2020
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: 1 addition & 1 deletion src/components/fx/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ exports.p2c = function p2c(axArray, v) {

exports.getDistanceFunction = function getDistanceFunction(mode, dx, dy, dxy) {
if(mode === 'closest') return dxy || exports.quadrature(dx, dy);
return mode === 'x' ? dx : dy;
return mode.charAt(0) === 'x' ? dx : dy;
};

exports.getClosest = function getClosest(cd, distfn, pointData) {
Expand Down
266 changes: 191 additions & 75 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ 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;
Expand Down Expand Up @@ -244,7 +247,7 @@ function _hover(gd, evt, subplot, noHoverEvent) {

if(hovermode && !supportsCompare) hovermode = 'closest';

if(['x', 'y', 'closest'].indexOf(hovermode) === -1 || !gd.calcdata ||
if(['x', 'y', 'closest', 'x unified', 'y unified'].indexOf(hovermode) === -1 || !gd.calcdata ||
gd.querySelector('.zoombox') || gd._dragging) {
return dragElement.unhoverRaw(gd, evt);
}
Expand Down Expand Up @@ -388,6 +391,9 @@ function _hover(gd, evt, subplot, noHoverEvent) {

// within one trace mode can sometimes be overridden
mode = hovermode;
if(['x unified', 'y unified'].indexOf(mode) !== -1) {
mode = mode.charAt(0);
}

// container for new point, also used to pass info into module.hoverPoints
pointData = {
Expand Down Expand Up @@ -661,9 +667,10 @@ function _hover(gd, evt, subplot, noHoverEvent) {

var hoverLabels = createHoverText(hoverData, labelOpts, gd);

hoverAvoidOverlaps(hoverLabels, rotateLabels ? 'xa' : 'ya', fullLayout);

alignHoverText(hoverLabels, rotateLabels);
if(['x unified', 'y unified'].indexOf(hovermode) === -1) {
hoverAvoidOverlaps(hoverLabels, rotateLabels ? 'xa' : 'ya', fullLayout);
alignHoverText(hoverLabels, rotateLabels);
}

// TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true
// we should improve the "fx" API so other plots can use it without these hack.
Expand Down Expand Up @@ -712,7 +719,7 @@ function createHoverText(hoverData, opts, gd) {
var c0 = hoverData[0];
var xa = c0.xa;
var ya = c0.ya;
var commonAttr = hovermode === 'y' ? 'yLabel' : 'xLabel';
var commonAttr = hovermode.charAt(0) === 'y' ? 'yLabel' : 'xLabel';
var t0 = c0[commonAttr];
var t00 = (String(t0) || '').split(' ')[0];
var outerContainerBB = outerContainer.node().getBoundingClientRect();
Expand Down Expand Up @@ -906,11 +913,113 @@ function createHoverText(hoverData, opts, gd) {

// remove the "close but not quite" points
// because of error bars, only take up to a space
hoverData = hoverData.filter(function(d) {
hoverData = filterClosePoints(hoverData);
});

function filterClosePoints(hoverData) {
return hoverData.filter(function(d) {
return (d.zLabelVal !== undefined) ||
(d[commonAttr] || '').split(' ')[0] === t00;
});
});
}

// Show a single hover label
if(['x unified', 'y unified'].indexOf(hovermode) !== -1) {
// Delete leftover hover labels from other hovermodes
container.selectAll('g.hovertext').remove();

// similarly to compare mode, we remove the "close but not quite together" points
if((t0 !== undefined) && (c0.distance <= opts.hoverdistance)) hoverData = filterClosePoints(hoverData);

// Return early if nothing is hovered on
if(hoverData.length === 0) return;

// mock legend
var mockLayoutIn = {
showlegend: true,
legend: {
title: {text: t0, font: fullLayout.font},
font: fullLayout.font,
bgcolor: fullLayout.paper_bgcolor,
borderwidth: 1,
tracegroupgap: 7,
traceorder: fullLayout.legend ? fullLayout.legend.traceorder : undefined,
orientation: 'v'
}
};
var mockLayoutOut = {};
legendSupplyDefaults(mockLayoutIn, mockLayoutOut, gd._fullData);
var legendOpts = mockLayoutOut.legend;

// prepare items for the legend
legendOpts.entries = [];
for(var j = 0; j < hoverData.length; j++) {
var texts = getHoverLabelText(hoverData[j], true, hovermode, fullLayout, t0);
var text = texts[0];
var name = texts[1];
var pt = hoverData[j];
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;

legendOpts.entries.push([pt]);
}
legendOpts.entries.sort(function(a, b) { return a[0].trace.index - b[0].trace.index;});
legendOpts.layer = container;

// Draw unified hover label
legendDraw(gd, legendOpts);

// Position the hover
var ly = Lib.mean(hoverData.map(function(c) {return (c.y0 + c.y1) / 2;}));
var lx = 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;
} else {
lx += 2 * HOVERTEXTPAD;
}

// 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;
}
}
legendContainer.attr('transform', 'translate(' + lx + ',' + ly + ')');

return legendContainer;
}

// show all the individual labels

Expand Down Expand Up @@ -941,8 +1050,6 @@ function createHoverText(hoverData, opts, gd) {
// and figure out sizes
hoverLabels.each(function(d) {
var g = d3.select(this).attr('transform', '');
var name = '';
var text = '';

// combine possible non-opaque trace color with bgColor
var color0 = d.bgcolor || d.color;
Expand All @@ -959,72 +1066,9 @@ function createHoverText(hoverData, opts, gd) {
// find a contrasting color for border and text
var contrastColor = d.borderColor || Color.contrast(numsColor);

// 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);
}

if(d.zLabel !== undefined) {
if(d.xLabel !== undefined) text += 'x: ' + d.xLabel + '<br>';
if(d.yLabel !== undefined) text += 'y: ' + d.yLabel + '<br>';
if(d.trace.type !== 'choropleth' && d.trace.type !== 'choroplethmapbox') {
text += (text ? 'z: ' : '') + d.zLabel;
}
} else if(showCommonLabel && d[hovermode + 'Label'] === t0) {
text = d[(hovermode === 'x' ? 'y' : 'x') + '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 ? '<br>' : '') + 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 ? '<br>' : '') + 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(text === '' && !d.hovertemplate) {
// if 'name' is also empty, remove entire label
if(name === '') g.remove();
text = name;
}

// hovertemplate
var d3locale = fullLayout._d3locale;
var hovertemplate = d.hovertemplate || false;
var hovertemplateLabels = d.hovertemplateLabels || d;
var eventData = d.eventData[0] || {};
if(hovertemplate) {
text = Lib.hovertemplateString(
hovertemplate,
hovertemplateLabels,
d3locale,
eventData,
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 '';
});
}
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')
Expand Down Expand Up @@ -1123,6 +1167,78 @@ function createHoverText(hoverData, opts, gd) {
return hoverLabels;
}

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);
}

if(d.zLabel !== undefined) {
if(d.xLabel !== undefined) text += 'x: ' + d.xLabel + '<br>';
if(d.yLabel !== undefined) text += 'y: ' + d.yLabel + '<br>';
if(d.trace.type !== 'choropleth' && d.trace.type !== 'choroplethmapbox') {
text += (text ? 'z: ' : '') + d.zLabel;
}
} else if(showCommonLabel && d[hovermode.charAt(0) + 'Label'] === t0) {
text = d[(hovermode.charAt(0) === 'x' ? 'y' : 'x') + '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 ? '<br>' : '') + 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 ? '<br>' : '') + 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 d3locale = fullLayout._d3locale;
var hovertemplate = d.hovertemplate || false;
var hovertemplateLabels = d.hovertemplateLabels || d;
var eventData = d.eventData[0] || {};
if(hovertemplate) {
text = Lib.hovertemplateString(
hovertemplate,
hovertemplateLabels,
d3locale,
eventData,
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. Indicentally,
Expand Down
12 changes: 11 additions & 1 deletion src/components/fx/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,20 @@ module.exports = {
hovermode: {
valType: 'enumerated',
role: 'info',
values: ['x', 'y', 'closest', false],
values: ['x', 'y', 'closest', false, 'x unified', 'y unified'],
Copy link
Contributor

@archmoj archmoj Mar 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@antoinerg would you please update the description of hovermode in respect to these two new flags?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolaskruchten could you tackle this one (ie. updating the description of hovermode to include x unified and y unified) ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please insert, before the existing paragraph, something like:

If closest, a single hoverlabel will appear for the closest point within the hoverdistance.
If x (or y), multiple hoverlabels will appear for multiple points at the closest x- (or y-) coordinate within the hoverdistance, with the caveat that no more than one hoverlabel will appear per trace.
If x unified (or y unified), a single hoverlabel will appear multiple points at the closest x- (or y-) coordinate within the hoverdistance, with the caveat that no more than one hoverlabel will appear per trace. In this mode, spikelines are enabled by default perpendicular to the specified axis.
If false, hover interactions are disabled.

Edit to taste and/or to correct inaccuracies :)

editType: 'modebar',
description: [
'Determines the mode of hover interactions.',
'If `closest`, a single hoverlabel will appear',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put flags between two * and attributes between ` chars.
E.g. this line should be

'If *closest*, a single `hoverlabel` will appear',

'for the closest point within the `hoverdistance`.',
'If `x` (or `y`), multiple hoverlabels will appear for multiple points',
'at the closest `x`- (or `y`-) coordinate within the `hoverdistance`,',
'with the caveat that no more than one hoverlabel will appear per trace.',
'If `x unified` (or `y unified`), a single hoverlabel will appear',
'multiple points at the closest x- (or y-) coordinate within the `hoverdistance`',
'with the caveat that no more than one hoverlabel will appear per trace.',
'In this mode, spikelines are enabled by default perpendicular to the specified axis.',
'If false, hover interactions are disabled.',
'If `clickmode` includes the *select* flag,',
'`hovermode` defaults to *closest*.',
'If `clickmode` lacks the *select* flag,',
Expand Down
6 changes: 5 additions & 1 deletion src/components/fx/layout_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {

var hoverMode = coerce('hovermode', hovermodeDflt);
if(hoverMode) {
var dflt;
if(['x unified', 'y unified'].indexOf(hoverMode) !== -1) {
dflt = -1;
}
coerce('hoverdistance');
coerce('spikedistance');
coerce('spikedistance', dflt);
}

// if only mapbox or geo subplots is present on graph,
Expand Down
Loading