Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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/8138_fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Fix hover labels that are a single tex expression being truncated by `hoverlabel.namelength` [[#8138](https://github.com/plotly/plotly.js/pull/8138)]
- Fix tex in hover labels being misplaced when labels are rotated in `hovermode: 'y'` [[#8138](https://github.com/plotly/plotly.js/pull/8138)]
35 changes: 20 additions & 15 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,10 @@ exports.loneHover = function loneHover(hoverItems, opts) {
fixVerticalOverlap();
alignHoverText(hoverLabel, rotateLabels, scaleX, scaleY);

hoverLabel.each(function () {
hoverLabel.each(function (d) {
var g = d3.select(this);
svgTextUtils.repositionMathGroup(g.select('text.nums'));
svgTextUtils.repositionMathGroup(g.select('text.name'));
svgTextUtils.repositionMathGroup(g.select('text.nums'), d.numsMathSize);
svgTextUtils.repositionMathGroup(g.select('text.name'), d.nameMathSize);
});
});
}
Expand Down Expand Up @@ -1053,10 +1053,10 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, hoverText.commonLabelBoundingBox);
alignHoverText(hoverLabels, rotateLabels, fullLayout._invScaleX, fullLayout._invScaleY);

hoverLabels.each(function () {
hoverLabels.each(function (d) {
var g = d3.select(this);
svgTextUtils.repositionMathGroup(g.select('text.nums'));
svgTextUtils.repositionMathGroup(g.select('text.name'));
svgTextUtils.repositionMathGroup(g.select('text.nums'), d.numsMathSize);
svgTextUtils.repositionMathGroup(g.select('text.name'), d.nameMathSize);
});
});
}
Expand Down Expand Up @@ -1103,7 +1103,7 @@ function hoverDataKey(d) {
var EXTRA_STRING_REGEX = /<extra>([\s\S]*)<\/extra>/;

function getHoverTextBBox(gd, textSel) {
return getBoundingClientRect(gd, svgTextUtils.getMathOrTextNode(textSel));
return toLayoutRect(gd, svgTextUtils.getTextClientRect(textSel));
}

function createHoverText(hoverData, opts) {
Expand Down Expand Up @@ -1220,6 +1220,7 @@ function createHoverText(hoverData, opts) {
});

var commonLabelTex = svgTextUtils.isPureTex(String(t0));
var commonMathSize;
var onCommonLabelReady;

if (commonLabelTex) {
Expand All @@ -1231,7 +1232,7 @@ function createHoverText(hoverData, opts) {
return;
}
positionCommonLabel();
svgTextUtils.repositionMathGroup(ltext);
svgTextUtils.repositionMathGroup(ltext, commonMathSize);
resolve();
};
})
Expand All @@ -1250,7 +1251,7 @@ function createHoverText(hoverData, opts) {
function positionCommonLabel() {
label.attr('transform', '');
svgTextUtils.positionText(ltext, 0, 0);
svgTextUtils.repositionMathGroup(ltext);
commonMathSize = svgTextUtils.repositionMathGroup(ltext);

var tbb = getHoverTextBBox(gd, ltext);
var lx, ly;
Expand Down Expand Up @@ -1758,10 +1759,10 @@ function createHoverText(hoverData, opts) {
function finalizePosition() {
g.attr('transform', '');
tx.call(svgTextUtils.positionText, 0, 0);
svgTextUtils.repositionMathGroup(tx);
d.numsMathSize = svgTextUtils.repositionMathGroup(tx);
if (hasName) {
tx2.call(svgTextUtils.positionText, 0, 0);
svgTextUtils.repositionMathGroup(tx2);
d.nameMathSize = svgTextUtils.repositionMathGroup(tx2);
}

g.select('path').style({
Expand Down Expand Up @@ -2650,8 +2651,10 @@ function spikesChanged(gd, oldspikepoints) {
}

function plainText(s, len) {
return svgTextUtils.plainText(s || '', {
len: len,
s = s || '';
return svgTextUtils.plainText(s, {
// truncating a tex expression drops its closing $, so it would no longer be typeset
len: svgTextUtils.isPureTex(s) ? undefined : len,
Comment on lines +2656 to +2657

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This makes hoverlabel.namelength have no effect on a name that is a single tex expression, causing it to disagree with the attribute description.

With namelength: 0, a plain name is hidden, but a tex name is now shown in full.

We should update attr description. Note the description is part of the schema, so npm run schema needs to be re-run after this.

allowedTags: ['br', 'sub', 'sup', 'b', 'i', 'em', 's', 'u']
});
}
Expand Down Expand Up @@ -2724,9 +2727,11 @@ const getTopOffset = (gd) => gd.offsetTop + gd.clientTop;
const getLeftOffset = (gd) => gd.offsetLeft + gd.clientLeft;

function getBoundingClientRect(gd, node) {
var fullLayout = gd._fullLayout;
return toLayoutRect(gd, node.getBoundingClientRect());
}

var rect = node.getBoundingClientRect();
function toLayoutRect(gd, rect) {
var fullLayout = gd._fullLayout;

var x0 = rect.left;
var y0 = rect.top;
Expand Down
3 changes: 2 additions & 1 deletion src/components/fx/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ module.exports = {
'regardless of length. 0-3 shows the first 0-3 characters, and',
'an integer >3 will show the whole name if it is less than that',
'many characters, but if it is longer, will truncate to',
'`namelength - 3` characters and add an ellipsis.'
'`namelength - 3` characters and add an ellipsis.',
'A name that is a single tex expression is always shown in full.'
].join(' ')
},
showarrow: {
Expand Down
84 changes: 61 additions & 23 deletions src/lib/svg_text_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,16 @@ const isPureTex = (str) => {
exports.isPureTex = isPureTex;

/**
* Positions a MathJax-typeset group against its source <text> node's x/y
* and text-anchor.
* Measures the typeset glyphs of a MathJax svg.
*
* @param {d3 selection} _context: the source <text> element
* @param {string} svgClass: the '<baseClass>-math' class used on newSvg
* @param {d3 selection} mathjaxGroup: the '<svgClass>-group' wrapper
* @param {d3 selection} newSvg: the typeset <svg>, already sized
* @param {string} textAnchor: _context's text-anchor to position against
* @param {number} fontSize: _context's font size, for the baseline offset
* @param {d3 selection} newSvg: the typeset <svg>
* @param {number} [width0]: the SVG's own reported width, to detect the
* Firefox v82+ overflow bug (see link below); omit to skip that check
* @param {number} [height0]: same, for height
* @return {{width: number, height: number}}
*/
function positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize, width0, height0) {
var g = newSvg.select('g');
var bb = g.node().getBoundingClientRect();
function measureMath(newSvg, width0, height0) {
var bb = newSvg.select('g').node().getBoundingClientRect();
var w = bb.width;
var h = bb.height;
var w0 = width0 === undefined ? w : width0;
Expand All @@ -68,6 +62,37 @@ function positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor,
h = bb.height;
}

return {width: w, height: h};
}

/**
* The size of a typeset group in its own units, which unlike its client rect
* does not change when the group, or something above it, is rotated or scaled.
*
* @param {d3 selection} mathjaxGroup: the '<svgClass>-group' wrapper
* @return {{width: number, height: number}}
*/
function getLocalSize(mathjaxGroup) {
var bb = mathjaxGroup.node().getBBox();
return {width: bb.width, height: bb.height};
}

/**
* Positions a MathJax-typeset group against its source <text> node's x/y
* and text-anchor.
*
* @param {d3 selection} _context: the source <text> element
* @param {string} svgClass: the '<baseClass>-math' class used on newSvg
* @param {d3 selection} mathjaxGroup: the '<svgClass>-group' wrapper
* @param {d3 selection} newSvg: the typeset <svg>, already sized
* @param {string} textAnchor: _context's text-anchor to position against
* @param {number} fontSize: _context's font size, for the baseline offset
* @param {{width: number, height: number}} size: the typeset glyphs' size
*/
function positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize, size) {
var w = size.width;
var h = size.height;

var x = +_context.attr('x');
var y = +_context.attr('y');

Expand Down Expand Up @@ -231,7 +256,7 @@ exports.convertToTspans = function(_context, gd, _callback) {
var g = newSvg.select('g');
g.attr({fill: fill, stroke: fill});

positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize, w0, h0);
positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize, measureMath(newSvg, w0, h0));

if(_callback) _callback.call(_context, mathjaxGroup);
resolve(mathjaxGroup);
Expand All @@ -247,38 +272,51 @@ exports.convertToTspans = function(_context, gd, _callback) {
* convertToTspans, to match its source <text> node's *current* x/y and
* text-anchor.
*
* The size is measured in the group's own units, so it does not change with
* the rotation or scale of whatever the group sits in.
*
* @param {d3 selection} _context: the source <text> element
* @param {{width: number, height: number}} [size]: a size returned by an
* earlier call, to use instead of measuring again
* @return {?{width: number, height: number}} the size used, or null if there
* is no typeset group to reposition
*/
exports.repositionMathGroup = function(_context) {
if(_context.empty()) return;
exports.repositionMathGroup = function(_context, size) {
if(_context.empty()) return null;

var svgClass = (_context.attr('class') ? _context.attr('class').split(' ')[0] : 'text') + '-math';
var parent = d3.select(_context.node().parentNode);
var mathjaxGroup = parent.select('g.' + svgClass + '-group');
var newSvg = mathjaxGroup.select('svg.' + svgClass);
if(mathjaxGroup.empty() || newSvg.empty()) return;
if(mathjaxGroup.empty() || newSvg.empty()) return null;

size = size || getLocalSize(mathjaxGroup);
var textAnchor = _context.attr('text-anchor');
var fontSize = parseInt(_context.node().style.fontSize, 10);
positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize);
positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize, size);
return size;
};

/**
* Finds the node whose bounding box represents a source <text> element's
* rendered content: the typeset MathJax group if convertToTspans replaced
* the text with one (the source itself is display:none in that case), or
* the <text> node otherwise.
* The client rect of a source <text> element's rendered content: the typeset
* glyphs if convertToTspans replaced the text with a MathJax group (the source
* itself is display:none in that case), or the <text> node otherwise.
*
* @param {d3 selection} textSel: the source <text> element
* @return {?Element} the node to measure, or null if textSel is empty
* @return {?DOMRect} null if textSel is empty
*/
exports.getMathOrTextNode = function(textSel) {
exports.getTextClientRect = function(textSel) {
var node = textSel.node();
if(!node) return null;

var svgClass = (textSel.attr('class') ? textSel.attr('class').split(' ')[0] : 'text') + '-math';
var mathGroup = d3.select(node.parentNode).select('g.' + svgClass + '-group');
return mathGroup.empty() ? node : mathGroup.node();
if(mathGroup.empty()) return node.getBoundingClientRect();

// Firefox can report a near-empty rect for the group itself (seen on
// rotated hover labels), but measures the glyphs inside it correctly
var glyphs = mathGroup.select('svg g');
return (glyphs.empty() ? mathGroup : glyphs).node().getBoundingClientRect();
};


Expand Down
8 changes: 4 additions & 4 deletions src/types/generated/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ export interface HoverLabel {
/** Sets the font used in hover labels. */
font?: FontArray;
/**
* Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.
* Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis. A name that is a single tex expression is always shown in full.
* @default 15
* Minimum: -1
*/
Expand Down Expand Up @@ -1588,7 +1588,7 @@ export interface CandlestickData {
/** Sets the font used in hover labels. */
font?: FontArray;
/**
* Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.
* Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis. A name that is a single tex expression is always shown in full.
* @default 15
* Minimum: -1
*/
Expand Down Expand Up @@ -6085,7 +6085,7 @@ export interface OhlcData {
/** Sets the font used in hover labels. */
font?: FontArray;
/**
* Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.
* Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis. A name that is a single tex expression is always shown in full.
* @default 15
* Minimum: -1
*/
Expand Down Expand Up @@ -16276,7 +16276,7 @@ export interface Layout {
/** Sets the font for group titles in hover (unified modes). Defaults to `hoverlabel.font`. */
grouptitlefont?: Font;
/**
* Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.
* Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis. A name that is a single tex expression is always shown in full.
* @default 15
* Minimum: -1
*/
Expand Down
Loading
Loading