Skip to content

fix: re-nice linear axis with the real plot-area length after layout#4594

Merged
xuefei1313 merged 1 commit into
VisActor:developfrom
g1f9:fix/linear-axis-relayout-nice-tickcount
Jun 12, 2026
Merged

fix: re-nice linear axis with the real plot-area length after layout#4594
xuefei1313 merged 1 commit into
VisActor:developfrom
g1f9:fix/linear-axis-relayout-nice-tickcount

Conversation

@g1f9

@g1f9 g1f9 commented Jun 12, 2026

Copy link
Copy Markdown

Problem

When a cartesian linear (value) axis uses a function tick.tickCount (i.e. the tick count depends on the axis length), the ticks come out too dense and the niced ceiling too low compared to the real plot area.

The root cause is already flagged by a TODO in linear-axis-mixin.ts (setLinearScaleNice):

if (rangeSize === 1 && this._option) {
  // TODO: need to be optimized, when the range is not updated, use the size of view
  const isX = isXAxis(this._orient);
  rangeSize = isX ? this._option.getChartViewRect().width : this._option.getChartViewRect().height;
}

setLinearScaleNice runs before layout, while scale.range() is still the initial [0, 1] (rangeSize === 1). In that case it falls back to the whole chart viewRect to estimate the axis length. But viewRect is larger than the real plot area (it still includes title / legend / opposite axis / padding), so:

  • the axisLength passed to tickCount(...) is too large → tickCount too large → ticks too dense;
  • the niced domain ceiling ends up lower than it should be.

After layout the scale range is updated to the real plot-area length (onLayoutEnd → updateScaleRange), but the nice computation is never re-run, so the wrong tick count / ceiling persists.

Fix

CartesianLinearAxis.onLayoutEnd now re-runs the domain + nice computation once, after the scale range reflects the real plot-area pixel length:

  • It only takes effect when tick.tickCount is a function (length-dependent). Fixed tickCount, forceTickCount, and the default count are range-independent and are left untouched → no behavior change for existing users.
  • A cached "last nice axis length" (_lastNiceAxisLength) skips the recompute when the axis length hasn't changed, guarding against redundant work / re-entrancy across multiple layout passes.

Scope / safety

  • No-op unless tick.tickCount is a function.
  • Reuses the existing updateScaleDomain() path (the same path used when series data changes), so domain extensions, zero, soft min/max, axis breaks, etc. are all preserved.

Known limitation

The axis space is allocated during layout using the pre-layout tick labels; if re-nicing produces a wider ceiling label, the axis width is not re-measured afterwards. In practice the niced ceiling label width is essentially unchanged, and this matches the trade-off already implied by the existing TODO.

@xuefei1313 xuefei1313 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CODE REVIEW REPORT

Files reviewed: 3
Recommendation: COMMENT

MEDIUM

  1. packages/vchart/src/component/axis/cartesian/linear-axis.ts:89

reTransformDomainByLayout() is executed from onLayoutEnd(). This can refresh tick/axis attributes before the chart-level layoutEnd handlers run, but it still happens after layout bounds have already been measured in getBoundsInRect(). If the re-niced domain changes the ceiling label or formatter output width, axis allocation / autoIndent will still be based on the old pre-layout ticks and may clip or squeeze the plot area. The PR body also calls out this limitation. A more robust place would be after updateScaleRange() but before computeData('range') and bounds measurement, or otherwise trigger a relayout when the re-niced domain actually changes.

  1. packages/vchart/src/component/axis/mixin/linear-axis-mixin.ts:164

This adds subtle lifecycle behavior without a regression test. Please add a focused case under packages/vchart/__tests__/unit/component/cartesian/axis/linear-axis.test.ts: mock collectData, make getChartViewRect().height larger than the real layout rect height, use a function tick.tickCount that records axisLength, and assert that after onLayoutEnd() the domain/tick count is based on the real axis length rather than the full chart viewRect.

Verification: inspected PR metadata, full diff, related axis layout/event lifecycle, and ran git diff --check origin/develop...origin/pr-4594 with no output. Full test suite was not run.

g1f9 pushed a commit to g1f9/VChart that referenced this pull request Jun 12, 2026
…sion test

Address review on PR VisActor#4594:

1. Move the length-based re-nice from onLayoutEnd into a updateScaleRange()
   override on CartesianLinearAxis. getBoundsInRect() calls updateScaleRange()
   before computeData('range') and bounds measurement, so the axis space is
   now allocated from the corrected (real-plot-length) ticks within the same
   layout pass - no extra relayout, no clip from stale pre-layout ticks. The
   base onLayoutEnd also calls updateScaleRange(), so post-layout stays covered.

2. Add a focused regression test: a function tick.tickCount records axisLength;
   with getChartViewRect().height (500) larger than the real layout length (200),
   assert the domain/tickCount after the range update is based on the real axis
   length (200 -> [0,1000]) rather than the full chart viewRect (500 -> [0,700]).
g1f9 pushed a commit to g1f9/VChart that referenced this pull request Jun 12, 2026
Address review on PR VisActor#4594:

- Fix TS2769: collectData is protected, so spy via
  jest.spyOn(CartesianAxis.prototype as any, 'collectData') instead of a
  misplaced // @ts-ignore. Cast the axis instance to any so the member calls
  type-check.
- Drive the assertion through the real fix entry: stub getNewScaleRange() to
  return the real plot-area range and call updateScaleRange() (instead of
  calling reTransformDomainByLayout() directly), so the test fails if the
  updateScaleRange() override is removed.
…er layout

When tick.tickCount is a function (length-dependent), setLinearScaleNice runs
before layout while the scale range is still [0, 1] and falls back to the full
chart viewRect (a TODO already noted in linear-axis-mixin). The viewRect is
larger than the real plot area (it still includes title / legend / opposite
axis), so the tick count is too dense and the niced ceiling too low, and it is
never recomputed after layout.

CartesianLinearAxis.updateScaleRange() now re-nices the domain once the scale
range reflects the real plot-area pixel length. Because getBoundsInRect() calls
updateScaleRange() before computeData('range') and bounds measurement, the axis
space is allocated from the corrected ticks within the same layout pass (no
extra relayout, no clipping). It only takes effect when tickCount is a function;
fixed / default tick counts are unaffected, and a cached last-nice axis length
guards against redundant work across layout passes.

Adds a regression test asserting that, after updateScaleRange(), the domain and
tick count are based on the real axis length rather than the full chart viewRect.
@g1f9 g1f9 force-pushed the fix/linear-axis-relayout-nice-tickcount branch from ba2f17a to 073fbd3 Compare June 12, 2026 08:40
@xuefei1313 xuefei1313 merged commit 982259e into VisActor:develop Jun 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants