From 2e2ec59feb6bda805689f264a3dc87a29d6b9745 Mon Sep 17 00:00:00 2001 From: Minwook Shin <163576506+minwookshin@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:02:28 -0400 Subject: [PATCH 1/2] Expire fallback font measurements after fonts load --- draftlogs/partial-redraw-font-cache_fix.md | 1 + src/components/drawing/index.js | 11 ++++++- test/jasmine/tests/drawing_test.js | 37 ++++++++++++++++++++++ test/jasmine/tests/treemap_test.js | 35 ++++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 draftlogs/partial-redraw-font-cache_fix.md diff --git a/draftlogs/partial-redraw-font-cache_fix.md b/draftlogs/partial-redraw-font-cache_fix.md new file mode 100644 index 00000000000..f427f89c59e --- /dev/null +++ b/draftlogs/partial-redraw-font-cache_fix.md @@ -0,0 +1 @@ +- Refresh text measurements cached during web font loading [[#PR_NUMBER](https://github.com/plotly/plotly.js/pull/PR_NUMBER)] diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index 707c7b2cfb6..f9b7bf4ff4a 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -1662,7 +1662,16 @@ drawing.bBox = function (node, inTester, hash) { } // cache this bbox - if (hash) drawing.savedBBoxes[hash] = bb; + if (hash) { + const cache = drawing.savedBBoxes; + cache[hash] = bb; + if (document.fonts && document.fonts.status === 'loading') { + // Reuse fallback measurements during the draw, but not after the fonts settle. + document.fonts.ready.then(() => { + if (cache[hash] === bb) delete cache[hash]; + }); + } + } savedBBoxesCount++; return Lib.extendFlat({}, bb); diff --git a/test/jasmine/tests/drawing_test.js b/test/jasmine/tests/drawing_test.js index def7497704b..15bad843a83 100644 --- a/test/jasmine/tests/drawing_test.js +++ b/test/jasmine/tests/drawing_test.js @@ -351,6 +351,43 @@ describe('Drawing', function() { describe('bBox', function() { afterEach(destroyGraphDiv); + it('remeasures cached fallback text after fonts load outside a plot redraw', async () => { + Drawing.makeTester(); + const stable = Drawing.tester.append('text').text('Stable text') + .call(Drawing.font, { family: 'Arial', size: 20 }) + .call(svgTextUtils.convertToTspans).node(); + const stableMeasure = spyOn(stable, 'getBoundingClientRect').and.callThrough(); + Drawing.bBox(stable, true); + + const family = `PartialRedrawFont${Date.now()}`; + const face = new FontFace(family, + 'url(/base/node_modules/@plotly/mathjax-v3/es5/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff)'); + document.fonts.add(face); + const text = Drawing.tester.append('text').text('WWWWWWWW iiiiiiii') + .call(Drawing.font, { family: `${family}, Arial`, size: 20 }) + .call(svgTextUtils.convertToTspans).node(); + const measure = spyOn(text, 'getBoundingClientRect').and.callThrough(); + try { + const fallback = Drawing.bBox(text, true); + expect(face.status).toBe('loading'); + expect(Drawing.bBox(text, true)).toEqual(fallback); + expect(measure).toHaveBeenCalledTimes(1); + await face.loaded; + await document.fonts.ready; + const loaded = Drawing.bBox(text, true); + expect(loaded.width).not.toBe(fallback.width); + expect(measure).toHaveBeenCalledTimes(2); + expect(Drawing.bBox(text, true)).toEqual(loaded); + expect(measure).toHaveBeenCalledTimes(2); + Drawing.bBox(stable, true); + expect(stableMeasure).toHaveBeenCalledTimes(1); + } finally { + document.fonts.delete(face); + text.remove(); + stable.remove(); + } + }); + function assertBBox(actual, expected) { [ 'height', 'top', 'bottom', diff --git a/test/jasmine/tests/treemap_test.js b/test/jasmine/tests/treemap_test.js index 72ec8fa7675..a70bcc9f3c8 100644 --- a/test/jasmine/tests/treemap_test.js +++ b/test/jasmine/tests/treemap_test.js @@ -1162,6 +1162,41 @@ describe('Test treemap clicks:', function () { afterEach(destroyGraphDiv); + it('remeasures text first drawn by a partial zoom after its font loads', async () => { + const family = `TreemapPartialFont${Date.now()}`; + const face = new FontFace(family, + 'url(/base/node_modules/@plotly/mathjax-v3/es5/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff)'); + document.fonts.add(face); + const label = 'WWWWWWWW iiiiiiii WWWWWWWW iiiiiiii'; + const zoom = level => Plotly.animate(gd, { data: [{ level }], traces: [0] }, { + transition: { duration: 0 }, frame: { duration: 0, redraw: false } + }); + const transforms = () => Array.from(gd.querySelectorAll(SLICES_TEXT_SELECTOR)) + .filter(node => node.style.fontFamily.includes(family)).map(node => node.getAttribute('transform')); + try { + await Plotly.newPlot(gd, [{ + type: 'treemap', ids: ['root', 'A', 'B', 'a1', 'a2', 'b1'], + labels: ['root', 'A', 'B', label, label, label], + parents: ['', 'root', 'root', 'A', 'A', 'B'], values: [0, 0, 0, 30, 20, 25], + level: 'B', textfont: { family: ['Arial', 'Arial', 'Arial', family, family, 'Arial'] } + }], { width: 700, height: 500 }); + expect(face.status).toBe('unloaded'); + await zoom('root'); + await face.loaded; + await document.fonts.ready; + await zoom('B'); + await zoom('root'); + const loaded = transforms(); + expect(loaded.length).toBe(2); + Drawing.savedBBoxes = {}; + await zoom('B'); + await zoom('root'); + expect(transforms()).toEqual(loaded, 'the next partial zoom matches a fresh measurement with the loaded font'); + } finally { + document.fonts.delete(face); + } + }); + function setupListeners(opts) { opts = opts || {}; From 2deb14170178be28ce1b9a20c665e19138b93932 Mon Sep 17 00:00:00 2001 From: Minwook Shin <163576506+minwookshin@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:29:50 -0400 Subject: [PATCH 2/2] Link draftlog to PR #8106 --- draftlogs/8106_fix.md | 1 + draftlogs/partial-redraw-font-cache_fix.md | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 draftlogs/8106_fix.md delete mode 100644 draftlogs/partial-redraw-font-cache_fix.md diff --git a/draftlogs/8106_fix.md b/draftlogs/8106_fix.md new file mode 100644 index 00000000000..ea2e9f9c19e --- /dev/null +++ b/draftlogs/8106_fix.md @@ -0,0 +1 @@ +- Refresh text measurements cached during web font loading [[#8106](https://github.com/plotly/plotly.js/pull/8106)] diff --git a/draftlogs/partial-redraw-font-cache_fix.md b/draftlogs/partial-redraw-font-cache_fix.md deleted file mode 100644 index f427f89c59e..00000000000 --- a/draftlogs/partial-redraw-font-cache_fix.md +++ /dev/null @@ -1 +0,0 @@ -- Refresh text measurements cached during web font loading [[#PR_NUMBER](https://github.com/plotly/plotly.js/pull/PR_NUMBER)]