From 0a1c4c880e71646a9ffbe8424dde89fd5e6d32a7 Mon Sep 17 00:00:00 2001 From: Sergey Slizovskiy Date: Wed, 21 Oct 2026 00:35:34 +0200 Subject: [PATCH 1/6] Fix tex hover label name truncation and rotated label placement Two follow-ups to #8051, reported on that PR: - A trace name that is a single tex expression was cut to hoverlabel.namelength (default 15) before the pure-tex check, which dropped the closing $ so it was never typeset. Skip truncation for pure-tex names. - In hovermode 'y' with several traces the labels are rotated, and positionMathGroup measured the glyphs with getBoundingClientRect, which is the larger screen-space box of the rotated shape, so the math landed far from its label. Measure in the group's own coordinates when an ancestor is rotated. The draftlog file name carries a placeholder PR number to be renamed once the PR is opened. Co-Authored-By: Claude Sonnet 5.5 --- draftlogs/0000_fix.md | 2 + src/components/fx/hover.js | 6 +- src/lib/svg_text_utils.js | 26 +++- test/jasmine/bundle_tests/mathjax_test.js | 141 ++++++++++++++++++++++ 4 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 draftlogs/0000_fix.md diff --git a/draftlogs/0000_fix.md b/draftlogs/0000_fix.md new file mode 100644 index 00000000000..83b91441931 --- /dev/null +++ b/draftlogs/0000_fix.md @@ -0,0 +1,2 @@ +- Fix hover labels that are a single tex expression being truncated by `hoverlabel.namelength` [[#0000](https://github.com/plotly/plotly.js/pull/0000)] +- Fix tex in hover labels being misplaced when labels are rotated in `hovermode: 'y'` [[#0000](https://github.com/plotly/plotly.js/pull/0000)] diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 342676cf654..41461d7b37a 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -2650,8 +2650,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, allowedTags: ['br', 'sub', 'sup', 'b', 'i', 'em', 's', 'u'] }); } diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index 21a0aa17beb..db50cf58219 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -37,6 +37,30 @@ const isPureTex = (str) => { }; exports.isPureTex = isPureTex; +/** + * Size of the typeset glyphs in their own coordinate system when an ancestor + * is rotated (e.g. a hover label in hovermode 'y'), where getBoundingClientRect + * would report the larger screen-space box of the rotated shape. + * + * @param {d3 selection} newSvg: the typeset + * @param {d3 selection} g: the inside newSvg that holds the glyphs + * @return {?{width: number, height: number}} null if no ancestor is rotated + */ +function getUnrotatedSize(newSvg, g) { + var svgNode = newSvg.node(); + var ctm = svgNode.getScreenCTM(); + if(!ctm || (Math.abs(ctm.b) < 1e-6 && Math.abs(ctm.c) < 1e-6)) return null; + + var bbox = g.node().getBBox(); + var viewBox = svgNode.viewBox.baseVal; + // the glyphs are letterboxed, so they scale by the smaller of the two ratios + var scale = Math.min( + svgNode.width.baseVal.value / viewBox.width, + svgNode.height.baseVal.value / viewBox.height + ); + return {width: bbox.width * scale, height: bbox.height * scale}; +} + /** * Positions a MathJax-typeset group against its source node's x/y * and text-anchor. @@ -53,7 +77,7 @@ exports.isPureTex = isPureTex; */ function positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize, width0, height0) { var g = newSvg.select('g'); - var bb = g.node().getBoundingClientRect(); + var bb = getUnrotatedSize(newSvg, g) || g.node().getBoundingClientRect(); var w = bb.width; var h = bb.height; var w0 = width0 === undefined ? w : width0; diff --git a/test/jasmine/bundle_tests/mathjax_test.js b/test/jasmine/bundle_tests/mathjax_test.js index 75c1c888d32..9bdd1dafe5f 100644 --- a/test/jasmine/bundle_tests/mathjax_test.js +++ b/test/jasmine/bundle_tests/mathjax_test.js @@ -593,6 +593,147 @@ describe('Test MathJax v' + mathjaxVersion + ':', function() { .then(done, done.fail); }); + it('should not truncate a pure-tex trace name to hoverlabel.namelength', function(done) { + var longTex = '$\\alpha_{1c} = 352 \\pm 11 \\text{ km s}^{-1}$'; + + Plotly.newPlot(gd, { + data: [{ + type: 'scatter', + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + name: longTex, + hoverinfo: 'x+y+name' + }], + layout: { + width: 500, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0}, + xaxis: {range: [0, 4]}, + yaxis: {range: [0, 4]} + } + }) + .then(function() { + _hover(125, 300); + return delay(30)(); + }) + .then(function() { + var mathGroup = d3Select(gd).select('g.hovertext .name-math-group'); + + expect(mathGroup.size()).toBe(1, 'name math group'); + expect(mathGroup.attr('data-unformatted')).toBe(longTex, 'full, untruncated tex'); + }) + .then(done, done.fail); + }); + + it('should not truncate a pure-tex hovertemplate name to hoverlabel.namelength', function(done) { + var longTex = '$\\alpha_{1c} = 352 \\pm 11 \\text{ km s}^{-1}$'; + + Plotly.newPlot(gd, { + data: [{ + type: 'scatter', + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + hovertemplate: 'v' + longTex + '' + }], + layout: { + width: 500, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0}, + xaxis: {range: [0, 4]}, + yaxis: {range: [0, 4]} + } + }) + .then(function() { + _hover(125, 300); + return delay(30)(); + }) + .then(function() { + var mathGroup = d3Select(gd).select('g.hovertext .name-math-group'); + + expect(mathGroup.size()).toBe(1, 'name math group'); + expect(mathGroup.attr('data-unformatted')).toBe(longTex, 'full, untruncated tex'); + }) + .then(done, done.fail); + }); + + it('should still truncate a plain-text trace name to hoverlabel.namelength', function(done) { + Plotly.newPlot(gd, { + data: [{ + type: 'scatter', + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + name: 'a rather long trace name', + hoverinfo: 'x+y+name' + }], + layout: { + width: 500, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0}, + xaxis: {range: [0, 4]}, + yaxis: {range: [0, 4]} + } + }) + .then(function() { + _hover(125, 300); + return delay(30)(); + }) + .then(function() { + var nameText = d3Select(gd).select('g.hovertext text.name').text(); + + expect(nameText.length).toBeLessThan('a rather long trace name'.length, 'truncated'); + expect(nameText).toMatch(/\.\.\.$/, 'ellipsis'); + }) + .then(done, done.fail); + }); + + it('should keep the tex name inside its box for rotated labels in hovermode y', function(done) { + Plotly.newPlot(gd, { + data: [ + {type: 'scatter', mode: 'markers', x: [1], y: [1], name: '$\\alpha_{1c} = 352 \\pm 11 \\text{ km s}^{-1}$', hoverinfo: 'x+y+name'}, + {type: 'scatter', mode: 'markers', x: [2], y: [1], name: '$\\gamma^2$', hoverinfo: 'x+y+name'} + ], + layout: { + width: 500, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0}, + xaxis: {range: [0, 4]}, + yaxis: {range: [0, 4]}, + hoverlabel: {namelength: -1}, + hovermode: 'y' + } + }) + .then(function() { + _hover(125, 300); + return delay(30)(); + }) + .then(function() { + var hoverTexts = d3Select(gd).selectAll('g.hovertext'); + + expect(hoverTexts.size()).toBe(2, 'two hover labels'); + expect(hoverTexts.attr('transform')).toContain('rotate', 'labels are rotated'); + + hoverTexts.each(function() { + var g = d3Select(this); + var mathGroup = g.select('.name-math-group'); + var box = g.select('rect'); + + expect(mathGroup.size()).toBe(1, 'name math group'); + expect(box.size()).toBe(1, 'name box'); + + var m = mathGroup.node().getBoundingClientRect(); + var b = box.node().getBoundingClientRect(); + var dx = (m.left + m.right) / 2 - (b.left + b.right) / 2; + var dy = (m.top + m.bottom) / 2 - (b.top + b.bottom) / 2; + + expect(Math.sqrt(dx * dx + dy * dy)).toBeLessThan(4, 'tex centered in its name box'); + }); + }) + .then(done, done.fail); + }); + it('should hand a pure-tex Fx.loneHover label off to MathJax, sized to its final content', function(done) { Plotly.newPlot(gd, { data: [{type: 'scatter', mode: 'markers', x: [1], y: [1]}], From fc4da9654a08c35ef982fb86ee9cb5a8a9f08c34 Mon Sep 17 00:00:00 2001 From: Sergey Slizovskiy Date: Wed, 21 Oct 2026 08:42:04 +0200 Subject: [PATCH 2/6] Rename draftlog to the PR number Co-Authored-By: Claude Sonnet 5.5 --- draftlogs/0000_fix.md | 2 -- draftlogs/8138_fix.md | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 draftlogs/0000_fix.md create mode 100644 draftlogs/8138_fix.md diff --git a/draftlogs/0000_fix.md b/draftlogs/0000_fix.md deleted file mode 100644 index 83b91441931..00000000000 --- a/draftlogs/0000_fix.md +++ /dev/null @@ -1,2 +0,0 @@ -- Fix hover labels that are a single tex expression being truncated by `hoverlabel.namelength` [[#0000](https://github.com/plotly/plotly.js/pull/0000)] -- Fix tex in hover labels being misplaced when labels are rotated in `hovermode: 'y'` [[#0000](https://github.com/plotly/plotly.js/pull/0000)] diff --git a/draftlogs/8138_fix.md b/draftlogs/8138_fix.md new file mode 100644 index 00000000000..4892486998e --- /dev/null +++ b/draftlogs/8138_fix.md @@ -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)] From 2c24c2907d3d88e6edbc6ab02d16d37a53ff7fca Mon Sep 17 00:00:00 2001 From: Sergey Slizovskiy Date: Wed, 21 Oct 2026 08:55:30 +0200 Subject: [PATCH 3/6] Limit rotation-safe tex measurement to repositionMathGroup Measuring the typeset glyphs in their own coordinates when an ancestor is rotated also moved the rotated ternary axis titles, which go through convertToTspans and failed the ternary-mathjax image baselines. Only repositionMathGroup (used by hover labels) opts in now; convertToTspans measures exactly as before. Co-Authored-By: Claude Sonnet 5.5 --- src/lib/svg_text_utils.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index db50cf58219..5ae89af6d16 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -74,10 +74,13 @@ function getUnrotatedSize(newSvg, g) { * @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 + * @param {boolean} [unrotated]: measure in the group's own coordinates when an + * ancestor is rotated. Off by default so convertToTspans' other callers keep + * the screen-space measurement they were laid out with */ -function positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize, width0, height0) { +function positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize, width0, height0, unrotated) { var g = newSvg.select('g'); - var bb = getUnrotatedSize(newSvg, g) || g.node().getBoundingClientRect(); + var bb = (unrotated && getUnrotatedSize(newSvg, g)) || g.node().getBoundingClientRect(); var w = bb.width; var h = bb.height; var w0 = width0 === undefined ? w : width0; @@ -284,7 +287,7 @@ exports.repositionMathGroup = function(_context) { 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, undefined, undefined, true); }; /** From 86b0c3e11344a8c3d5901a1b9cb582f09baf5942 Mon Sep 17 00:00:00 2001 From: Sergey Slizovskiy Date: Mon, 26 Oct 2026 09:34:54 +0100 Subject: [PATCH 4/6] Measure tex hover labels by their glyphs, not the wrapping group In Firefox, getBoundingClientRect on the g.-math-group wrapper can report a near-empty box (2x0.2px for a 130x14px formula on a rotated hover label) while the glyph group inside it measures correctly. The hover label box was sized from that, so rotated tex names got a ~8px wide box. getMathOrTextNode now returns the glyph group; the rotated-label test measures the glyphs too. Reproduced and verified in Firefox 157 and Chrome. Co-Authored-By: Claude Sonnet 5.5 --- src/lib/svg_text_utils.js | 11 ++++++++--- test/jasmine/bundle_tests/mathjax_test.js | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index 5ae89af6d16..264e4609784 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -292,8 +292,8 @@ exports.repositionMathGroup = function(_context) { /** * Finds the node whose bounding box represents a source 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 + * 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 node otherwise. * * @param {d3 selection} textSel: the source element @@ -305,7 +305,12 @@ exports.getMathOrTextNode = function(textSel) { 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; + + // Firefox can report a near-empty getBoundingClientRect for the group + // itself (seen on rotated hover labels), but measures the glyphs correctly + var glyphs = mathGroup.select('svg g'); + return (glyphs.empty() ? mathGroup : glyphs).node(); }; diff --git a/test/jasmine/bundle_tests/mathjax_test.js b/test/jasmine/bundle_tests/mathjax_test.js index 9bdd1dafe5f..48f00fec6de 100644 --- a/test/jasmine/bundle_tests/mathjax_test.js +++ b/test/jasmine/bundle_tests/mathjax_test.js @@ -723,7 +723,7 @@ describe('Test MathJax v' + mathjaxVersion + ':', function() { expect(mathGroup.size()).toBe(1, 'name math group'); expect(box.size()).toBe(1, 'name box'); - var m = mathGroup.node().getBoundingClientRect(); + var m = mathGroup.select('svg g').node().getBoundingClientRect(); var b = box.node().getBoundingClientRect(); var dx = (m.left + m.right) / 2 - (b.left + b.right) / 2; var dy = (m.top + m.bottom) / 2 - (b.top + b.bottom) / 2; From 406c21cece2a14caea7aa2dc4a7d5edd59f9da8f Mon Sep 17 00:00:00 2001 From: Sergey Slizovskiy Date: Thu, 5 Nov 2026 13:44:07 +0100 Subject: [PATCH 5/6] Measure tex hover labels once and reuse the size Review follow-up. The rotated-label fix detected rotation (getScreenCTM) and measured differently in repositionMathGroup than at first placement. Instead, hover now records the tex size measured in the neutral, unrotated state (where finalizePosition and positionCommonLabel already measure) and passes it back into repositionMathGroup, so a label is never re-measured while rotated. - positionMathGroup takes an explicit size; convertToTspans measures exactly as before, so titles, annotations and ternary are unchanged - repositionMathGroup accepts and returns a size; the unrotated flag and getUnrotatedSize are gone - getMathOrTextNode becomes getTextClientRect, so hover no longer handles MathJax's nodes; it still measures the glyphs, as Firefox can report a near-empty rect for the wrapping group - document that a tex name ignores hoverlabel.namelength, and regenerate the schema - add a test for a tex main label in rotated hovermode y Co-Authored-By: Claude Sonnet 5.5 --- src/components/fx/hover.js | 29 ++++--- src/components/fx/layout_attributes.js | 3 +- src/lib/svg_text_utils.js | 99 +++++++++++------------ src/types/generated/schema.d.ts | 8 +- test/jasmine/bundle_tests/mathjax_test.js | 46 +++++++++++ test/plot-schema.json | 90 ++++++++++----------- 6 files changed, 160 insertions(+), 115 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 41461d7b37a..968911ecb02 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -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); }); }); } @@ -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); }); }); } @@ -1103,7 +1103,7 @@ function hoverDataKey(d) { var EXTRA_STRING_REGEX = /([\s\S]*)<\/extra>/; function getHoverTextBBox(gd, textSel) { - return getBoundingClientRect(gd, svgTextUtils.getMathOrTextNode(textSel)); + return toLayoutRect(gd, svgTextUtils.getTextClientRect(textSel)); } function createHoverText(hoverData, opts) { @@ -1220,6 +1220,7 @@ function createHoverText(hoverData, opts) { }); var commonLabelTex = svgTextUtils.isPureTex(String(t0)); + var commonMathSize; var onCommonLabelReady; if (commonLabelTex) { @@ -1231,7 +1232,7 @@ function createHoverText(hoverData, opts) { return; } positionCommonLabel(); - svgTextUtils.repositionMathGroup(ltext); + svgTextUtils.repositionMathGroup(ltext, commonMathSize); resolve(); }; }) @@ -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; @@ -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({ @@ -2726,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; diff --git a/src/components/fx/layout_attributes.js b/src/components/fx/layout_attributes.js index fd7fae8dbb1..f72e9faad7c 100644 --- a/src/components/fx/layout_attributes.js +++ b/src/components/fx/layout_attributes.js @@ -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: { diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index 264e4609784..a8f7e23be4f 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -38,49 +38,16 @@ const isPureTex = (str) => { exports.isPureTex = isPureTex; /** - * Size of the typeset glyphs in their own coordinate system when an ancestor - * is rotated (e.g. a hover label in hovermode 'y'), where getBoundingClientRect - * would report the larger screen-space box of the rotated shape. + * Measures the typeset glyphs of a MathJax svg. * * @param {d3 selection} newSvg: the typeset - * @param {d3 selection} g: the inside newSvg that holds the glyphs - * @return {?{width: number, height: number}} null if no ancestor is rotated - */ -function getUnrotatedSize(newSvg, g) { - var svgNode = newSvg.node(); - var ctm = svgNode.getScreenCTM(); - if(!ctm || (Math.abs(ctm.b) < 1e-6 && Math.abs(ctm.c) < 1e-6)) return null; - - var bbox = g.node().getBBox(); - var viewBox = svgNode.viewBox.baseVal; - // the glyphs are letterboxed, so they scale by the smaller of the two ratios - var scale = Math.min( - svgNode.width.baseVal.value / viewBox.width, - svgNode.height.baseVal.value / viewBox.height - ); - return {width: bbox.width * scale, height: bbox.height * scale}; -} - -/** - * Positions a MathJax-typeset group against its source node's x/y - * and text-anchor. - * - * @param {d3 selection} _context: the source element - * @param {string} svgClass: the '-math' class used on newSvg - * @param {d3 selection} mathjaxGroup: the '-group' wrapper - * @param {d3 selection} newSvg: the typeset , already sized - * @param {string} textAnchor: _context's text-anchor to position against - * @param {number} fontSize: _context's font size, for the baseline offset * @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 - * @param {boolean} [unrotated]: measure in the group's own coordinates when an - * ancestor is rotated. Off by default so convertToTspans' other callers keep - * the screen-space measurement they were laid out with + * @return {{width: number, height: number}} */ -function positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize, width0, height0, unrotated) { - var g = newSvg.select('g'); - var bb = (unrotated && getUnrotatedSize(newSvg, g)) || 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; @@ -95,6 +62,25 @@ function positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, h = bb.height; } + return {width: w, height: h}; +} + +/** + * Positions a MathJax-typeset group against its source node's x/y + * and text-anchor. + * + * @param {d3 selection} _context: the source element + * @param {string} svgClass: the '-math' class used on newSvg + * @param {d3 selection} mathjaxGroup: the '-group' wrapper + * @param {d3 selection} newSvg: the typeset , 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'); @@ -258,7 +244,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); @@ -274,43 +260,52 @@ exports.convertToTspans = function(_context, gd, _callback) { * convertToTspans, to match its source node's *current* x/y and * text-anchor. * + * The size is measured from the page, so it changes with the rotation or + * scale of whatever the group sits in. A caller that moves the text into such + * a state should measure once beforehand and pass that size back in. + * * @param {d3 selection} _context: the source 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 || measureMath(newSvg); var textAnchor = _context.attr('text-anchor'); var fontSize = parseInt(_context.node().style.fontSize, 10); - positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize, undefined, undefined, true); + positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize, size); + return size; }; /** - * Finds the node whose bounding box represents a source 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 node otherwise. + * The client rect of a source 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 node otherwise. * * @param {d3 selection} textSel: the source 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'); - if(mathGroup.empty()) return node; + if(mathGroup.empty()) return node.getBoundingClientRect(); - // Firefox can report a near-empty getBoundingClientRect for the group - // itself (seen on rotated hover labels), but measures the glyphs correctly + // 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(); + return (glyphs.empty() ? mathGroup : glyphs).node().getBoundingClientRect(); }; diff --git a/src/types/generated/schema.d.ts b/src/types/generated/schema.d.ts index 8a855665e73..8c5cdb894ed 100644 --- a/src/types/generated/schema.d.ts +++ b/src/types/generated/schema.d.ts @@ -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 */ @@ -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 */ @@ -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 */ @@ -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 */ diff --git a/test/jasmine/bundle_tests/mathjax_test.js b/test/jasmine/bundle_tests/mathjax_test.js index 48f00fec6de..14c12a63b2e 100644 --- a/test/jasmine/bundle_tests/mathjax_test.js +++ b/test/jasmine/bundle_tests/mathjax_test.js @@ -734,6 +734,52 @@ describe('Test MathJax v' + mathjaxVersion + ':', function() { .then(done, done.fail); }); + it('should keep a pure-tex main label inside its box for rotated labels in hovermode y', function(done) { + var template = '$\\sum_{i=1}^{n} x_i$one'; + + Plotly.newPlot(gd, { + data: [ + {type: 'scatter', mode: 'markers', x: [1], y: [1], hovertemplate: template}, + {type: 'scatter', mode: 'markers', x: [2], y: [1], hovertemplate: template} + ], + layout: { + width: 500, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0}, + xaxis: {range: [0, 4]}, + yaxis: {range: [0, 4]}, + hovermode: 'y' + } + }) + .then(function() { + _hover(125, 300); + return delay(30)(); + }) + .then(function() { + var hoverTexts = d3Select(gd).selectAll('g.hovertext'); + + expect(hoverTexts.size()).toBe(2, 'two hover labels'); + expect(hoverTexts.attr('transform')).toContain('rotate', 'labels are rotated'); + + hoverTexts.each(function() { + var g = d3Select(this); + var mathGroup = g.select('.nums-math-group'); + + expect(mathGroup.size()).toBe(1, 'main label math group'); + + var m = mathGroup.select('svg g').node().getBoundingClientRect(); + var b = g.select('path').node().getBoundingClientRect(); + var tol = 1; + + expect(m.left).not.toBeLessThan(b.left - tol, 'tex inside the box, left'); + expect(m.right).not.toBeGreaterThan(b.right + tol, 'tex inside the box, right'); + expect(m.top).not.toBeLessThan(b.top - tol, 'tex inside the box, top'); + expect(m.bottom).not.toBeGreaterThan(b.bottom + tol, 'tex inside the box, bottom'); + }); + }) + .then(done, done.fail); + }); + it('should hand a pure-tex Fx.loneHover label off to MathJax, sized to its final content', function(done) { Plotly.newPlot(gd, { data: [{type: 'scatter', mode: 'markers', x: [1], y: [1]}], diff --git a/test/plot-schema.json b/test/plot-schema.json index 4d51312e82d..019087f25cc 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -3032,7 +3032,7 @@ } }, "namelength": { - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -16327,7 +16327,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -18196,7 +18196,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -19592,7 +19592,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -20962,7 +20962,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -23797,7 +23797,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -24937,7 +24937,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -26106,7 +26106,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -27521,7 +27521,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -30151,7 +30151,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -30689,7 +30689,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -32432,7 +32432,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -33948,7 +33948,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -34973,7 +34973,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -37435,7 +37435,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -39005,7 +39005,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -39820,7 +39820,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -41551,7 +41551,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -43913,7 +43913,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -45298,7 +45298,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -45968,7 +45968,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -49205,7 +49205,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -50253,7 +50253,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -51686,7 +51686,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "calc", "min": -1, @@ -52041,7 +52041,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "calc", "min": -1, @@ -52313,7 +52313,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "calc", "min": -1, @@ -53036,7 +53036,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -55529,7 +55529,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -57764,7 +57764,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -59843,7 +59843,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -62050,7 +62050,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -64158,7 +64158,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -65490,7 +65490,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -67585,7 +67585,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -69534,7 +69534,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -71613,7 +71613,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -73713,7 +73713,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -76149,7 +76149,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -76818,7 +76818,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -79298,7 +79298,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -80306,7 +80306,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -80724,7 +80724,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -82530,7 +82530,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -84518,7 +84518,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, @@ -85327,7 +85327,7 @@ }, "namelength": { "arrayOk": true, - "description": "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.", + "description": "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.", "dflt": 15, "editType": "none", "min": -1, From 152d4bf1d29f23f3138a2429aba7ce54091ff66e Mon Sep 17 00:00:00 2001 From: Sergey Slizovskiy Date: Thu, 5 Nov 2026 13:48:59 +0100 Subject: [PATCH 6/6] Measure repositioned tex in its own units repositionMathGroup now measures the typeset group with getBBox, which reports its size in the group's own units, instead of its client rect. The size no longer changes when the group or the graph is rotated or scaled, and it matches what positionText uses for plain text. Co-Authored-By: Claude Sonnet 5.5 --- src/lib/svg_text_utils.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index a8f7e23be4f..9dfb3662ef8 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -65,6 +65,18 @@ function measureMath(newSvg, width0, height0) { 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 '-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 node's x/y * and text-anchor. @@ -260,9 +272,8 @@ exports.convertToTspans = function(_context, gd, _callback) { * convertToTspans, to match its source node's *current* x/y and * text-anchor. * - * The size is measured from the page, so it changes with the rotation or - * scale of whatever the group sits in. A caller that moves the text into such - * a state should measure once beforehand and pass that size back in. + * 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 element * @param {{width: number, height: number}} [size]: a size returned by an @@ -279,7 +290,7 @@ exports.repositionMathGroup = function(_context, size) { var newSvg = mathjaxGroup.select('svg.' + svgClass); if(mathjaxGroup.empty() || newSvg.empty()) return null; - size = size || measureMath(newSvg); + 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, size);