Skip to content

fix: stabilize y-axis width against live label changes - #851

Merged
liihuu merged 1 commit into
klinecharts:mainfrom
NemeZZiZZ:fix/stable-yaxis-width
Sep 15, 2026
Merged

liihuu merged 1 commit into
klinecharts:mainfrom
NemeZZiZZ:fix/stable-yaxis-width

Conversation

@NemeZZiZZ

Copy link
Copy Markdown
Contributor

Problem

The y-axis column width is measured live, every layout, from texts that move:

  1. Tick labels — measured raw each getAutoSize(). When scrolling across a magnitude boundary (e.g. 999,950.00 → 1,000,050.00), the widest label gains a character and the column jumps.
  2. Last-price mark — measured from the current close, which changes on every tick.
  3. Crosshair reservation — sized from range.displayTo, which moves on every frame of a scroll/zoom.

On top of that, the existing cacheYAxisWidth mechanism is a grow-only ratchet (Math.max(cache, current)) applied on scroll/zoom/data-update layout paths (Store.ts), while the deferred TaskScheduler layout recomputes without the cache — the two policies fight, so the column pumps in both directions.

Since the plot area is chart width minus this column, every width change shifts the entire chart: the scale visibly trembles while scrolling at large magnitudes.

Measured (production UMD, 3000 bars trending 600k → 2M, programmatic scroll)

Hover-scroll at the 1M digit boundary (alternate the visible window across it every 50 ms, 40 steps):

width transitions series
main 39 of 40 steps 84px ↔ 74px on every step
this PR 2 total 84px → 76px → 84px, then stable

Single-direction sweep across the boundary: both versions step once per decade crossing (unavoidable — a wider label must fit); this PR grid-rounds the step (74px → 76px) and, unlike the ratchet, allows the column to come back.

Fix

Stabilize what the width is measured from, and settle what is reported:

  1. stableWidthText — the last-price mark and the crosshair reservation are measured from a stand-in of the same shape: every digit replaced by a fixed representative (8), with the minus reserved whenever the range reaches below zero. The width then depends on how many characters the label has — which changes when the data changes magnitude, not on every frame. (Digits are tabular in the default font, and the grid below absorbs any residual difference.)
  2. widthAnchorValue — the crosshair can read anywhere in the range, so its reservation is sized from the top of the decade the range is in (9.99 × 10^⌊log10(max|from|,|to|)⌋) instead of the moving displayTo. Within a decade it does not move; it changes once when the data genuinely changes magnitude, and never under-reserves (every value in a decade is shorter than its top).
  3. Settle — the measured width is rounded up to a 4px grid (swallows sub-character noise), may grow immediately (a label must always fit), and may shrink only after 400 ms without needing the room. A scroll therefore widens the column once, if it must, and settles once when it ends.
  4. cacheYAxisWidth ratchet removed (Chart.layout option + the four Store call sites). Its grow-only clamp would mask the settle logic — the width could never come back on exactly the paths that matter — and its purpose (prevent width pumping during scroll) is now served at the source by 1–3. The measurement work itself was never skipped by the cache, so this is not a perf regression.

No public API changes; yAxis.size fixed-width configs bypass the new logic as before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants