Skip to content

AG-17505, AG-17507 Fix duplicate async data calls on lazy zoom - #7896

Draft
lsjroberts wants to merge 6 commits into
latestfrom
AG-17505-AG-17507-async-data
Draft

AG-17505, AG-17507 Fix duplicate async data calls on lazy zoom#7896
lsjroberts wants to merge 6 commits into
latestfrom
AG-17505-AG-17507-async-data

Conversation

@lsjroberts

@lsjroberts lsjroberts commented Aug 24, 2026

Copy link
Copy Markdown
Member

A lazily loaded chart has no axis domain until its first response lands, so
`unsafeClamp` read `domainMin`/`domainMax` off `undefined` and threw on the
first pointer event. The casts asserted `[Date, Date]`, so the compiler could
not see it.

Fixed here rather than separately because it blocks the verification of the
initial-window behaviour in the following commit.
A `rangeX` memento needs the axis domain to convert to a ratio, which a lazily
loaded chart does not have on its first render. The restore fell back to the
full range and discarded the memento, so the requested window was ignored
outright on every axis whose domain is not known from its options, and a
corrective second request followed once data arrived.

The requested range is already stated in data space, so it needs no domain to
interpret: keep the memento pending until it can be converted, and request the
range itself in the meantime. That both honours the request on the first fetch
and gives the axis the domain it needs to resolve the memento, so lazy
`rangeX` now restores the same zoom as it does from static data.

Also stop a domainless scale's `undefined`/invalid-Date inversions reaching
`dataSource.getData` as window bounds.
@github-actions

Copy link
Copy Markdown
Contributor
Live-test this PR in Plunker

Paste these two <script> tags into a Plunker (or any vanilla-JS host) to load the UMD bundles built from this PR:

<script src="https://ag-grid.github.io/ag-charts/pr-7896/ag-charts-community.min.js"></script>
<script src="https://ag-grid.github.io/ag-charts/pr-7896/ag-charts-enterprise.min.js"></script>

Bundles are removed automatically when the PR is closed. Updated on every push.

@lsjroberts
lsjroberts marked this pull request as ready for review August 24, 2026 10:14
private getPendingWindow(): AgDataSourceCallbackParams | undefined {
const range = this.ctx.zoomManager?.getPendingRangeX();
if (!range) return;

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.

ℹ️ [P2] One-sided range windows are discarded

A zoom range may omit either endpoint, but this check requires both to be present. Consequently, a valid start-only or end-only initial range produces an unbounded first request and requires a corrective request after data loads. Preserve whichever valid endpoint is supplied and leave the other callback parameter undefined.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in a996726. getPendingWindow now normalises each bound independently and bails only when both are unusable, so a one-sided range is forwarded as a half-open window.

Normalising the bounds alone was not enough to remove the corrective request, though: a windowEnd of undefined never compares equal to the resolved domain-edge value, so shouldRefresh still saw a changed window and fetched again. shouldRefresh now treats a bound the pending request left open as satisfied by the resolved bound that replaces it, since the open bound already asked for everything up to the domain edge.

Also unwraps a grouping value's value, which was discarded for the same reason. bigint still falls through — windowStart does not accept one.

Covered by requests a %s-only range once and requests the range once for a grouping-valued range in dataSource.test.ts.

private getPendingWindow(): AgDataSourceCallbackParams | undefined {
const range = this.ctx.zoomManager?.getPendingRangeX();
if (!range) return;

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.

ℹ️ [P2] Non-finite window bounds pass validation

isNumber accepts values such as NaN and infinities, and isDate may accept an invalid Date object. These values are now forwarded directly to getData, whereas resolving through the scale would not produce a usable window. Validate finite numbers and valid dates before constructing the request.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Guards tightened in a996726isFiniteNumber and isValidDate in place of isNumber and isDate.

For the record, though, these values cannot reach getData by the route described. A memento is decoded through JSON.parse(JSON.stringify(...)), which turns Infinity and NaN into null, and guardMemento then rejects the whole memento with Option rangeX.end cannot be set to null — measured, not assumed. An invalid Date throws earlier still, during encode. So the change is defence-in-depth on a function that no longer has a single caller, not a fix for a reachable defect, and there is no test for it.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Codex review complete; 2 issues found (P0: 0 | P1: 0 | P2: 2 | P3: 0)

View full review

AG-17505 Fix double async data call on initialState.rangeX/ratioX

PR: #7896
Author: lsjroberts | Base: latest ← Head: AG-17505-AG-17507-async-data
Diff: 4 files changed, +161 -2

Summary

This PR defers range-based zoom restoration until axis domains exist and uses pending data-space ranges for the initial data-source request.

Findings

P0: 0 | P1: 0 | P2: 2 | P3: 0

2 of 2 finding(s) also posted inline; all findings are listed below.

ℹ️ [P2] One-sided range windows are discarded

packages/ag-charts-community/src/chart/update/dataWindowProcessor.ts:204

A zoom range may omit either endpoint, but this check requires both to be present. Consequently, a valid start-only or end-only initial range produces an unbounded first request and requires a corrective request after data loads. Preserve whichever valid endpoint is supplied and leave the other callback parameter undefined.

ℹ️ [P2] Non-finite window bounds pass validation

packages/ag-charts-community/src/chart/update/dataWindowProcessor.ts:204

isNumber accepts values such as NaN and infinities, and isDate may accept an invalid Date object. These values are now forwarded directly to getData, whereas resolving through the scale would not produce a usable window. Validate finite numbers and valid dates before constructing the request.

Verdict

Assessment: correct
Confidence: 0.82

The main lazy-domain restoration flow is sound, but pending-window validation mishandles partial and invalid ranges.

Required Actions:

  • Support one-sided pending ranges
  • Reject non-finite numbers and invalid dates

The pending-window path required both bounds and both to be plain values, so a
range stating only one endpoint, or stating it as a grouping value, fell back to
an unbounded first request and a correction once the domain existed. Bounds are
now normalised individually, and a bound left open by the pending request is
treated as satisfied by the resolved bound that replaces it.
A grouping value is the one bound shape carrying its value in a nested field,
so keying the unwrap on `value` alone would also strip a serialisable date or
bigint down to its raw payload.
@lsjroberts
lsjroberts force-pushed the AG-17505-AG-17507-async-data branch from d78c1da to 2121810 Compare August 24, 2026 13:56
…ount

A band-positioned time axis addresses its domain by ordinal, so the ratios
spanning a value range depend on how many points that range holds. The
preserveDomain strategy interpolated those ratios linearly in domain-value
space, which restored a different value range whenever a response changed the
density inside the window: the window jumped forwards and the load it triggered
cost an extra request.

Snapshot the range itself for those axes and re-convert it through the scale,
the same round-trip a zoom memento restore uses.

Note that on such an axis the zoom ratio is the share of bands visible, which
is also the navigator mask width, so holding the value range means the mask now
moves with the response density. Holding the mask instead is what
`zoom.onDataChange.strategy: 'preserveRatios'` selects.
A re-derived zoom differs from the applied one by a few ULPs, which an exact
comparison reads as a new window to fetch.
@lsjroberts
lsjroberts marked this pull request as draft August 25, 2026 14:15
@lsjroberts lsjroberts changed the title AG-17505 Fix double async data call on initialState.rangeX/ratioX AG-17505, AG-17507 Fix duplicate async data calls on lazy zoom Aug 26, 2026
@lsjroberts

Copy link
Copy Markdown
Member Author

on hold while away, will review when back

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.

1 participant