Skip to content

[Bug] After one slow ASCII width table build, overflow: 'break' wraps every later font as if each character were as wide as 'a' #21812

Open

Description

@floke75

Version

6.1.0 (the latest release, with zrender 6.1.0). zrender master has the same code. 5.6.0 wraps correctly with the same repro.

Link to Minimal Reproduction

Official Editor, ECharts 6.1.0. The same page as a standalone HTML file, with the screenshots below: https://gist.github.com/floke75/f14389932cffd44dc37f5d7ee98f12f8

Steps to Reproduce

  1. Open the editor link. The code gives ECharts a plain canvas measureText through echarts.setPlatformAPI. With SLOW_MOMENT = true, it is slow (0.2 ms a call) only while one throwaway text in another font is wrapped. This stands in for a slow device. The chart itself is laid out at full speed afterwards.
  2. Look at the x-axis labels. Each one wraps at width: 90, and the grey box shows that width.
  3. Set SLOW_MOMENT = false and run it again to compare.

It also reproduces without the hook. Open the standalone repro.html (without ?slow), set DevTools > Performance > CPU to 20x slowdown, and reload. On an Apple M3, it reproduced in every run at 10x and 20x, in some runs at 6x, and in none at 1x to 4x.

Editor code
// true: measureText is slow for one moment, a stand-in for a slow device. false: labels wrap correctly,
// unless the page itself is slow (DevTools CPU throttling at 20x on an Apple M3 reproduces it with false).
const SLOW_MOMENT = true;

const ctx = document.createElement('canvas').getContext('2d');
let slow = SLOW_MOMENT;
echarts.setPlatformAPI({
  measureText(text, font) {
    if (slow) { const t = performance.now(); while (performance.now() - t < 0.2) {} }
    ctx.font = font;
    return ctx.measureText(text);
  }
});

// Some other text on the page wraps first, in another font. When building that font's ASCII width table
// takes more than 16 ms, zrender builds no table for any later font in the page.
const other = echarts.init(document.createElement('div'), null, { width: 100, height: 50 });
other.setOption({ graphic: { type: 'text', style: { text: 'Other text', width: 80, overflow: 'break', font: '12px serif' } } });
slow = false;

// The chart itself is laid out at full speed. Each label wraps at width 90 (the grey box).
option = {
  animation: false,
  xAxis: {
    type: 'category',
    data: ['illi lilla fiil tillit', 'WWW MMM WWW'],
    axisLabel: { interval: 0, fontSize: 14, width: 90, overflow: 'break', backgroundColor: '#ddd' }
  },
  yAxis: { show: false },
  series: [{ type: 'bar', data: [3, 5] }]
};

Current Behavior

After the slow moment, the labels wrap as if every ASCII character were as wide as a (7.8 px here):

  • illi lilla fiil tillit is 77.8 px wide and fits in 90 px. It is counted as 171 px and breaks into two lines.
  • WWW MMM WWW is 122 px wide and should wrap after MMM. It is counted as 85.6 px, stays on one line and runs out of its box.
With the slow moment (?slow) No hook, DevTools CPU 20x
Labels wrapped with the width of a The same result under CPU throttling

This lasts for the rest of the page's life. Every font string that is first wrapped or truncated after the slow moment is affected.

Expected Behavior

The labels wrap as they do without the slow moment, and as 5.6.0 wraps them in both cases:

Labels wrapped correctly

Environment

- OS: macOS 26.6.1, Apple M3
- Browser: Chromium 148.0.7778.96 (Playwright), and the official editor in the same browser
- Framework: none

Any additional comments?

Where it comes from, as far as I can tell (src/contain/text.ts):

  • The first time measureCharWidth sees a font, tryCreateASCIIWidthMap measures its 128 ASCII characters into a table. If one build takes more than 16 ms, or five builds take more than 2 ms, the module-level _getASCIIWidthMapLongCount reaches its maximum. After that, no table is built for any later font in the page, and the counter never resets.
  • Without a table, measureCharWidth returns asciiCharWidth, the width of a, for every ASCII character.
  • wrapText (overflow: 'break' and 'breakAll', for plain and rich text) adds up these widths and nothing corrects them afterwards, so it breaks lines in the wrong places. Truncation uses them only for its first estimate and then measures whole strings. For truncation the fallback is the same a-width estimate 5.x used, so it is no worse than 5.x.
  • The table came in with zrender fabe0ca8 (2025-06-06, "perf(trancate)"), first released in zrender 6.0.0. Before that, wrapText measured each character with getWidth(ch, font), through the per-font string cache.

The comment above tryCreateASCIIWidthMap says "PENDING: is it necessary? Re-examine it if bad case reported.", so this may be such a case.

Suggested fix. When there is no table, measure the character itself through the font's existing string cache, as 5.x did, instead of using the width of a:

--- a/src/contain/text.ts
+++ b/src/contain/text.ts
@@ -89,7 +89,7 @@
     return (0 <= charCode && charCode <= 127)
         ? (fontMeasureInfo.asciiWidthMap != null
             ? fontMeasureInfo.asciiWidthMap[charCode]
-            : fontMeasureInfo.asciiCharWidth
+            : measureWidth(fontMeasureInfo, String.fromCharCode(charCode))
         )
         : fontMeasureInfo.stWideCharWidth;
 }

This costs at most one measureText per distinct character per font, and cache lookups after that, which is what wrapping cost in 5.x. We carry this line as a local patch to 6.1.0 in a newsroom charting tool. With it, the repro wraps correctly with the hook and under 6x and 20x throttling.

Thank you for ECharts!

Activity

  1. added
    enThis issue is in English
    pendingWe are not sure about whether this is a bug/new feature.
    on Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enThis issue is in EnglishpendingWe are not sure about whether this is a bug/new feature.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions