Skip to content

[charts] Stable progressive scatter batching + responsive zoom/pan#22862

Open
JCQuintas wants to merge 14 commits into
mui:masterfrom
JCQuintas:progressive-scatter-zoom-perf
Open

[charts] Stable progressive scatter batching + responsive zoom/pan#22862
JCQuintas wants to merge 14 commits into
mui:masterfrom
JCQuintas:progressive-scatter-zoom-perf

Conversation

@JCQuintas

@JCQuintas JCQuintas commented Jun 17, 2026

Copy link
Copy Markdown
Member

Progressive scatter: stable batching + responsive zoom/pan

Builds on the progressive scatter renderer to fix point popping while panning and keep interactions responsive.

Fix: points popping in/out while panning

Root cause: ScatterAsync batched over the viewport-filtered coords array (batch membership = how many lower-dataIndex points were currently visible), while the scheduler sizes batches over the total point count. As the visible set shifted each pan frame, a point's batch changed → mid-screen popping.

Fix: batch over dataIndex ranges and decide visibility per point at render time.

  • scatterRenderData.selectors: packed Float64Array is now indexed by dataIndex, stride 3 [x, y, visible]. Off-screen points keep their slot instead of being dropped, so a point's batch is fixed across zoom/pan.
  • ScatterAsyncBatch: skips points flagged invisible; dataIndex = start + local.
  • ScatterAsync: batch count is total-based, matching the scheduler.

Perf: cheaper interaction frames

While interacting, the first level is capped to a short stable dataIndex prefix (INTERACTION_POINT_BUDGET per series) instead of the full batch, cutting per-frame <circle> reconciliation. The rest fills in once the interaction settles. Stable prefix = no popping; a uniform sample for unsorted data.

Docs

Progressive scatter demo is now zoomable to exercise the interaction path.

Closes #22817
Closes #22731

Only the first level of the progressive scatter render is ever visible
during a zoom/pan interaction, and the per-frame work is minimized:

- `selectorProgressiveSeriesRevealedBatches` clamps the revealed batches
  to the first level while interacting, so no frame can show more than
  the first level regardless of how far the reveal had progressed.
- The scheduler resets the revealed rounds to the first level and pauses
  while interacting, then resumes the progressive fill from there once
  the interaction settles (after a short inactivity delay).
- `ScatterAsync` mounts only the first batch while interacting; the other
  batches would otherwise render an empty `<g>` yet still re-render every
  frame, since their store subscription bypasses `React.memo`.
- `ScatterAsyncBatch` skips the per-marker highlight state and interaction
  handlers while interacting; they are useless mid-drag and were the
  dominant per-frame cost for large datasets.
While zooming/panning, render only a short stable dataIndex prefix
(INTERACTION_POINT_BUDGET points per series) for the first level instead
of the full batch, cutting per-frame element reconciliation. The rest
fills in once the interaction settles.
@JCQuintas JCQuintas requested a review from alexfauquette as a code owner June 17, 2026 13:35
@JCQuintas JCQuintas self-assigned this Jun 17, 2026
@JCQuintas JCQuintas added scope: charts Changes related to the charts. performance type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. labels Jun 17, 2026
@code-infra-dashboard

code-infra-dashboard Bot commented Jun 17, 2026

Copy link
Copy Markdown

Deploy preview

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 🔺+494B(+0.13%) 🔺+207B(+0.17%)
@mui/x-charts-pro 🔺+506B(+0.10%) 🔺+201B(+0.13%)
@mui/x-charts-premium 🔺+491B(+0.08%) 🔺+211B(+0.11%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)
@mui/x-license 0B(0.00%) 0B(0.00%)

Details of bundle changes

Performance

Total duration: 2,137.99 ms +215.05 ms(+11.2%) | Renders: 63 (+0)

Test Duration Renders
Heatmap: 100x100 grid 732.66 ms 🔺+150.62 ms(+25.9%) 2 (+0)
BarChartPro with big data amount 52.81 ms 🔺+9.88 ms(+23.0%) 2 (+0)
RadialBarChart stacked with multiple series 22.32 ms 🔺+5.17 ms(+30.1%) 2 (+0)

23 tests within noise — details

Metric alarms

Test Metric Change
Heatmap: 100x100 grid bench:paint 🔺 +169.11 ms
BarChartPro with big data amount bench:paint 🔺 +15.33 ms
RadialBarChart stacked with multiple series bench:paint 🔺 +9.35 ms

Check out the code infra dashboard for more information about this PR.

…tion

Collapsing the revealed rounds to the first level on every zoom/pan
hid already-painted points for the whole gesture and forced a settle-
delayed re-reveal on each pan, flickering on repeated panning. Skip the
collapse when the wave is already complete; ScatterAsync still caps the
mounted batches while interacting, so the gesture stays cheap.
…l clamp

Extract pure helpers (packScatterSeriesCoords, getRevealedBatchCount) from the
selectors so the dataIndex-indexed packing, visibility flag, batch-view range
math, and interaction clamp are covered by unit tests.
Comment on lines +87 to +92
export function packScatterSeriesCoords(
data: readonly { x: number | Date; y: number | Date }[],
getXPosition: (value: number | Date) => number,
getYPosition: (value: number | Date) => number,
bounds: ScatterVisibilityBounds,
): ScatterSeriesRenderData {

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.

This is ever so slightly less speeeeed than the previous iteration.

New implementation stores coords for all items instead of only visible range, but since interacting got faster (due to rendering only first batch), this is technically a net improvement

This "architectural" change is what makes the panning stable when zoomed in.

You can check an "unstable" panning on the PR in #22817

Unstable

Screen.Recording.2026-06-17.at.16.50.22.mov

Stable

Screen.Recording.2026-06-17.at.16.51.52.mov

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

Labels

performance scope: charts Changes related to the charts. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[charts] Improve progressive scatter zoom

1 participant