Skip to content

Conversation

@JCQuintas
Copy link
Member

@JCQuintas JCQuintas commented Oct 31, 2025

Main goal is to remove this line startRef.current = store.state.zoom.zoomData; which prevented other interactions to run alongside the pan interaction, since the pan interaction would always "reset" any other changes.

This is mostly an internal change

You can test this in the http://localhost:3001/x/react-charts/zoom-and-pan/#zoom-synchronization
by reverting the changes in the file: packages/x-charts-pro/src/internals/plugins/useChartProZoom/useChartProZoom.ts

@JCQuintas JCQuintas self-assigned this Oct 31, 2025
@JCQuintas JCQuintas added the type: bug It doesn't behave as expected. label Oct 31, 2025
@JCQuintas JCQuintas added plan: Pro Impact at least one Pro user. scope: charts Changes related to the charts. labels Oct 31, 2025
Comment on lines 88 to 89
accumulatedChange.current.x += event.detail.deltaX;
accumulatedChange.current.y += event.detail.deltaY;
Copy link
Member Author

Choose a reason for hiding this comment

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

We store the accumulated changes outside the raf throttle

Comment on lines 67 to 70
const x = accumulatedChange.current.x;
const y = accumulatedChange.current.y;
accumulatedChange.current.x = 0;
accumulatedChange.current.y = 0;
Copy link
Member Author

Choose a reason for hiding this comment

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

we consume the delta changes when the throttle runs

@mui-bot
Copy link

mui-bot commented Oct 31, 2025

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

Bundle size report

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 ▼-42B(-0.01%) ▼-4B(0.00%)
@mui/x-charts-pro ▼-37B(-0.01%) 🔺+55B(+0.04%)
@mui/x-charts-premium ▼-33B(-0.01%) 🔺+47B(+0.04%)
@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%)

Details of bundle changes

Generated by 🚫 dangerJS against fdfe812

Comment on lines 91 to 95
onZoomChange?.(newZoomData);
store.set('zoom', {
...store.state.zoom,
isInteracting: true,
});
Copy link
Member Author

@JCQuintas JCQuintas Oct 31, 2025

Choose a reason for hiding this comment

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

we now set isInteracting: true when we send the new zoom to the onZoomChange, this would lead to small de-sync in data over time due to animations turning on/off

import { expect } from 'vitest';
import { rafThrottle } from './rafThrottle';

describe('rafThrottle', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

extra tests just in case 🤷

@codspeed-hq
Copy link

codspeed-hq bot commented Oct 31, 2025

CodSpeed Performance Report

Merging #20163 will not alter performance

Comparing JCQuintas:fix-zoom-lag (fdfe812) with master (f775e82)1

Summary

✅ 13 untouched

Footnotes

  1. No successful run was found on master (0741821) during the generation of this report, so f775e82 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Oct 31, 2025
@github-actions
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Oct 31, 2025
if (!(event.detail.target as SVGElement)?.closest('[data-charts-zoom-slider]')) {
startRef.current = store.state.zoom.zoomData;
isInteracting.current = true;
accumulatedChange.current = { x: 0, y: 0 };
Copy link
Member

Choose a reason for hiding this comment

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

Couldn't we lose some updates?

Since rafThrottle will delay the reading of accumulatedChange, if another handlePanStart is called before the callback of throttledCallback fires, the callback will fire with accumulatedChange.current = { x: 0, y: 0 }.

To fix it, I think we need to pass accumulatedChange.current to the throttledCallback so that it retains the reference to the old value. (It works because we're updating accumulatedChange.current by mutating x and y. If we were updating the object directly (accumulatedChange.current = { x, y } it wouldn't work.

Copy link
Member Author

Choose a reason for hiding this comment

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

Couldn't we lose some updates?

In a real environment it would be very unlikely to loose any update at all.

I've changed it to initialize/change inside the hook.

const optionsLookup = useSelector(store, selectorChartZoomOptionsLookup);
const startRef = React.useRef<readonly ZoomData[]>(null);
const isInteracting = React.useRef<boolean>(false);
const accumulatedChange = React.useRef<{ x: number; y: number }>({ x: 0, y: 0 });
Copy link
Member

Choose a reason for hiding this comment

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

I don't think these needs to be refs. Since they're only used inside the useEffect we can create const variable with interior mutability to achieve the same effect.

Basically, we create these variables inside the useEffect similar to a ref: const accumulatedChange = { current: { x: 0, y: 0 }}.

It would also allow us to unit test this logic, which I think we should because it's prone to errors as explained here.

Comment on lines +66 to +69
const x = accumulatedChange.x;
const y = accumulatedChange.y;
accumulatedChange.x = 0;
accumulatedChange.y = 0;
Copy link
Member

Choose a reason for hiding this comment

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

I think this works, but I'm not sure it's very logical.

IMO, it would be make more sense for throttledCallback to accept the accumulatedChange object and for handlePanStart to create a new { x: 0, y: 0} object when it's fired.

We don't need to reset values inside the throttled function.

Copy link
Member Author

Choose a reason for hiding this comment

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

The throttle would still fail to always have the correct ref, since older values are dropped.

current:

// - start 
// - move, update accumulate + scheduleThrottledFn
// - move, update accumulate + dropOldThrottledFn + scheduleThrottledFn
// - start (values keep accumulating)
// - move, update accumulate + dropOldThrottledFn + scheduleThrottledFn
// - run scheduled fn + reset accumulate

your suggestion:

// - start, new accumulate (ref1)
// - move, update accumulate + scheduleThrottledFn(ref1)
// - move, update accumulate + dropOldThrottledFn(ref1) + scheduleThrottledFn(ref1)
// - start, new accumulate (ref2)
//   \/ (ref1) could be completely discarded
// - move, update accumulate + dropOldThrottledFn(ref1) + scheduleThrottledFn(ref2)
// - run scheduled fn(ref2)

Copy link
Member

Choose a reason for hiding this comment

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

You're right. Your solution looks good, then 👍

@JCQuintas JCQuintas merged commit da98471 into mui:master Nov 3, 2025
22 of 23 checks passed
@JCQuintas JCQuintas deleted the fix-zoom-lag branch November 3, 2025 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

plan: Pro Impact at least one Pro user. scope: charts Changes related to the charts. type: bug It doesn't behave as expected.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants