Repository navigation
Description
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
- Open the editor link. The code gives ECharts a plain canvas
measureTextthroughecharts.setPlatformAPI. WithSLOW_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. - Look at the x-axis labels. Each one wraps at
width: 90, and the grey box shows that width. - Set
SLOW_MOMENT = falseand 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 tillitis 77.8 px wide and fits in 90 px. It is counted as 171 px and breaks into two lines.WWW MMM WWWis 122 px wide and should wrap afterMMM. 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 |
|---|---|
![]() |
![]() |
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:
Environment
- OS: macOS 26.6.1, Apple M3
- Browser: Chromium 148.0.7778.96 (Playwright), and the official editor in the same browser
- Framework: noneAny additional comments?
Where it comes from, as far as I can tell (src/contain/text.ts):
- The first time
measureCharWidthsees a font,tryCreateASCIIWidthMapmeasures 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_getASCIIWidthMapLongCountreaches its maximum. After that, no table is built for any later font in the page, and the counter never resets. - Without a table,
measureCharWidthreturnsasciiCharWidth, the width ofa, 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 samea-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,
wrapTextmeasured each character withgetWidth(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