Skip to content

[charts] Avoid full axis pipeline rebuild on resize#22695

Merged
JCQuintas merged 8 commits into
mui:masterfrom
JCQuintas:charts-resize-memoization
Jun 10, 2026
Merged

[charts] Avoid full axis pipeline rebuild on resize#22695
JCQuintas merged 8 commits into
mui:masterfrom
JCQuintas:charts-resize-memoization

Conversation

@JCQuintas

@JCQuintas JCQuintas commented Jun 5, 2026

Copy link
Copy Markdown
Member

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:

  1. The raw-axis resync effect depended on drawingArea. The effect in useChartCartesianAxis that re-syncs the raw axis config from props (xAxis/yAxis/dataset/axesGap) listed drawingArea in its dependency array but never read it. On every resize it re-ran store.set('cartesianAxis', …), minting a brand-new raw-axis array identity with identical content. Because selectorChartRawXAxis is a pure identity read, this cascaded a near-total pipeline rebuild — extrema (O(series × points)), domains, and normalized-scale InternMap rebuilds — 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 the drawingArea-dependent selectors, so layout behavior is unchanged.

  2. selectorChartSeriesFlatbushMap declared a dead drawingArea input. The combiner builds the spatial index in [0, 1] normalized space and never reads drawingArea (it destructures only 6 of the 7 inputs). Listing it forced a full O(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 from anchorRef.current; a ref assignment doesn't trigger a re-render, so on first mount Popper received anchorEl = 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/valueGetter axes, 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.

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.
@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 5, 2026
@JCQuintas JCQuintas self-assigned this Jun 5, 2026
@code-infra-dashboard

code-infra-dashboard Bot commented Jun 5, 2026

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-22695--material-ui-x.netlify.app/

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 🔺+28B(+0.01%) 🔺+29B(+0.02%)
@mui/x-charts-pro 🔺+30B(+0.01%) 🔺+26B(+0.02%)
@mui/x-charts-premium 🔺+30B(0.00%) 🔺+26B(+0.01%)
@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,023.80 ms +94.50 ms(+4.9%) | Renders: 67 (+0) | Paint: 2,922.48 ms +130.74 ms(+4.7%)

Test Duration Renders
RangeBarChart with big data amount 96.59 ms 🔺+17.82 ms(+22.6%) 3 (+0)
BarChart with big data amount 86.33 ms 🔺+15.60 ms(+22.1%) 3 (+0)
BarChartPro with big data amount 48.85 ms 🔺+14.29 ms(+41.3%) 3 (+0)
ScatterChart with big data amount 32.69 ms 🔺+8.93 ms(+37.6%) 2 (+0)
PieChart with big data amount 15.56 ms 🔺+5.59 ms(+56.1%) 2 (+0)

…and 2 more (+19 within noise) — details


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

JCQuintas added 2 commits June 5, 2026 16:38
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.
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jun 9, 2026
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
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
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jun 9, 2026
JCQuintas added 3 commits June 9, 2026 12:49
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
JCQuintas marked this pull request as ready for review June 9, 2026 11:02
@JCQuintas
JCQuintas requested a review from alexfauquette as a code owner June 9, 2026 11:02
@JCQuintas
JCQuintas enabled auto-merge (squash) June 10, 2026 13:53
@JCQuintas
JCQuintas disabled auto-merge June 10, 2026 13:53
@JCQuintas
JCQuintas merged commit 2c95754 into mui:master Jun 10, 2026
21 checks passed
@JCQuintas
JCQuintas deleted the charts-resize-memoization branch June 10, 2026 13:55
mbrookes pushed a commit to mbrookes/mui-x that referenced this pull request Jun 27, 2026
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.

2 participants