[charts] Avoid full axis pipeline rebuild on resize#22695
Merged
Conversation
Two memoization fixes so resizing a cartesian chart no longer recomputes the whole axis pipeline: - Drop `drawingArea` from the raw-axis resync effect in useChartCartesianAxis. It was never read in the effect body; its presence re-minted the raw axis array on every resize, busting selectorChartRawXAxis (an identity read) and cascading extrema/domain/normalized-scale (InternMap) rebuilds. Resize now reruns only the range + offset selectors, which is what it semantically needs. Auto-size and offset layout still flow through the drawingArea-dependent selectors (computeAxisValue), so behavior is unchanged. - Drop the dead `selectorChartDrawingArea` input from selectorChartSeriesFlatbushMap. The combiner never used it and builds points in [0, 1] space, so it forced a full O(scatter points) Flatbush rebuild per resize for no reason.
Deploy previewBundle size
PerformanceTotal duration: 2,023.80 ms +94.50 ms(+4.9%) | Renders: 67 (+0) | Paint: 2,922.48 ms +130.74 ms(+4.7%)
…and 2 more (+19 within noise) — details Check out the code infra dashboard for more information about this PR. |
A controlled axis tooltip (or any chart/node-anchored tooltip) keeps the same anchor element ref across renders and only moves it via its `left`/`top` styles. Popper does not observe those style changes, so the tooltip stayed at the position it was first placed (e.g. the top-left origin computed before the drawing area was measured) instead of following the anchor. Ask Popper to recompute whenever the resolved anchor position changes. This also fixes a controlled tooltip not following on resize/zoom. This surfaced after the resize memoization change removed an incidental extra re-render that had been masking the missing Popper update.
The Popper anchor was read from `anchorRef.current`. A ref assignment does not trigger a re-render, so on first mount Popper received `anchorEl=null`, never created its instance, and never positioned the tooltip — it stayed at the top-left origin. This was previously masked by an incidental re-render that the resize memoization change removed. Replace the off-screen DOM anchor div + ref with a virtual anchor element derived from `itemPosition` (mirroring the existing pointer anchor). Popper always gets a non-null anchor and positions on mount, and the existing update() call keeps it following the anchor on resize/zoom — no DOM node or ref-to-state needed.
Contributor
|
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
…oization # Conflicts: # packages/x-charts/src/ChartsTooltip/ChartsTooltipContainer.tsx
Revert the virtual anchor element back to a real DOM node, kept in state via a callback ref. The virtual element positioned correctly on mount but could not expose scroll parents to Popper, so the tooltip would drift when scrolling. A real DOM node lets Popper track its scroll parents and keep the tooltip glued to the anchor. Storing it in state (rather than a plain ref) is what hands the element to Popper after mount — a ref assignment alone does not re-render, which was the original bug that left Popper anchored to `null`.
JCQuintas
marked this pull request as ready for review
June 9, 2026 11:02
alexfauquette
approved these changes
Jun 10, 2026
JCQuintas
enabled auto-merge (squash)
June 10, 2026 13:53
JCQuintas
disabled auto-merge
June 10, 2026 13:53
mbrookes
pushed a commit
to mbrookes/mui-x
that referenced
this pull request
Jun 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two resize-path memoization fixes, plus a tooltip positioning fix they surfaced.
Resize memoization
Resizing a cartesian chart recomputed far more of the axis pipeline than it needed to:
The raw-axis resync effect depended on
drawingArea. The effect inuseChartCartesianAxisthat re-syncs the raw axis config from props (xAxis/yAxis/dataset/axesGap) listeddrawingAreain its dependency array but never read it. On every resize it re-ranstore.set('cartesianAxis', …), minting a brand-new raw-axis array identity with identical content. BecauseselectorChartRawXAxisis a pure identity read, this cascaded a near-total pipeline rebuild — extrema (O(series × points)), domains, and normalized-scaleInternMaprebuilds — defeating the zoom/resize independence the normalized-scale layer was designed for. Removed it; resize now reruns only the range + offset selectors (selectorChartXScales/computeAxisValue). Auto-size and offset layout still flow through thedrawingArea-dependent selectors, so layout behavior is unchanged.selectorChartSeriesFlatbushMapdeclared a deaddrawingAreainput. The combiner builds the spatial index in[0, 1]normalized space and never readsdrawingArea(it destructures only 6 of the 7 inputs). Listing it forced a fullO(scatter points)Flatbush rebuild on every resize for nothing. Removed.Tooltip positioning fix
Removing the incidental resize re-render surfaced a latent bug in chart/node-anchored tooltips (a controlled
tooltipAxis, or item tooltips). The Popper anchor was read fromanchorRef.current; a ref assignment doesn't trigger a re-render, so on first mount Popper receivedanchorEl = null, never created its instance, and never positioned the tooltip — it stayed pinned to the top-left origin. The resize re-render had been incidentally feeding the (by-then valid) ref to Popper.Fixed by anchoring to a virtual element derived from the resolved
itemPosition(mirroring the existing pointer anchor) and asking Popper to recompute when that position changes. Popper always receives a non-null anchor, so it positions on mount and follows the anchor on resize/zoom. This also fixes a controlled tooltip not following its anchor on resize/zoom.Impact
Resize (responsive containers / resize-drag) drops from a near-total axis pipeline rebuild to the cheap range/offset layer. Largest benefit for band/point charts with many categories,
dataKey/valueGetteraxes, several series, and large scatter charts. No effect on zoom or hover (already clean). Tooltip behavior is restored. Behavior is otherwise unchanged — pure memoization scoping plus an anchor mechanism change.