Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ describe.skipIf(isJSDOM)('<ScatterChartPro /> - Zoom', () => {
onZoomChange={onZoomChange}
zoomInteractionConfig={{
zoom: ['tapAndDrag'],
pan: [],
}}
/>,
options,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-promise-executor-return */
/* eslint-disable no-await-in-loop */

import * as React from 'react';
import { createRenderer, fireEvent, act } from '@mui/internal-test-utils';
import { isJSDOM } from 'test/utils/skipIf';
Expand Down Expand Up @@ -75,20 +75,16 @@ describe.skipIf(isJSDOM)('ZoomInteractionConfig Keys and Modes', () => {
]);

// Wheel without modifier keys - should not zoom
for (let i = 0; i < 10; i += 1) {
fireEvent.wheel(svg, { deltaY: -1, clientX: 50, clientY: 50 });
await act(async () => new Promise((r) => requestAnimationFrame(r)));
}
fireEvent.wheel(svg, { deltaY: -10, clientX: 50, clientY: 50 });
await act(async () => new Promise((r) => requestAnimationFrame(r)));

expect(onZoomChange.callCount).to.equal(0);
expect(getAxisTickValues('x')).to.deep.equal(['A', 'B', 'C', 'D']);

await user.keyboard('{Control>}');
// Wheel with Control key - should zoom
for (let i = 0; i < 30; i += 1) {
fireEvent.wheel(svg, { deltaY: -1, clientX: 50, clientY: 50 });
await act(async () => new Promise((r) => requestAnimationFrame(r)));
}
fireEvent.wheel(svg, { deltaY: -30, clientX: 50, clientY: 50 });
await act(async () => new Promise((r) => requestAnimationFrame(r)));
await user.keyboard('{/Control}');

expect(onZoomChange.callCount).to.be.greaterThan(0);
Expand Down Expand Up @@ -315,10 +311,8 @@ describe.skipIf(isJSDOM)('ZoomInteractionConfig Keys and Modes', () => {
const svg = document.querySelector('svg:not([aria-hidden="true"])')!;

// Wheel - should not zoom since only pinch is enabled
for (let i = 0; i < 30; i += 1) {
fireEvent.wheel(svg, { deltaY: -1, clientX: 50, clientY: 50 });
await act(async () => new Promise((r) => requestAnimationFrame(r)));
}
fireEvent.wheel(svg, { deltaY: -30, clientX: 50, clientY: 50 });
await act(async () => new Promise((r) => requestAnimationFrame(r)));

expect(onZoomChange.callCount).to.equal(0);
expect(getAxisTickValues('x')).to.deep.equal(['A', 'B', 'C', 'D']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export const usePanOnDrag = (
) => {
const drawingArea = useSelector(store, selectorChartDrawingArea);
const optionsLookup = useSelector(store, selectorChartZoomOptionsLookup);
const startRef = React.useRef<readonly ZoomData[]>(null);
const config = useSelector(store, selectorPanInteractionConfig, 'drag' as const);

const isPanOnDragEnabled: boolean =
Expand All @@ -47,41 +46,47 @@ export const usePanOnDrag = (
// Add event for chart panning
React.useEffect(() => {
const element = svgRef.current;
let isInteracting = false;
const accumulatedChange = { x: 0, y: 0 };

if (element === null || !isPanOnDragEnabled) {
return () => {};
}

const handlePanStart = (event: PanEvent) => {
if (!(event.detail.target as SVGElement)?.closest('[data-charts-zoom-slider]')) {
startRef.current = store.state.zoom.zoomData;
isInteracting = true;
}
};
const handlePanEnd = () => {
startRef.current = null;
isInteracting = false;
};

const throttledCallback = rafThrottle((event: PanEvent, zoomData: readonly ZoomData[]) => {
const newZoomData = translateZoom(
zoomData,
{ x: event.detail.activeDeltaX, y: -event.detail.activeDeltaY },
{
width: drawingArea.width,
height: drawingArea.height,
},
optionsLookup,
const throttledCallback = rafThrottle(() => {
const x = accumulatedChange.x;
const y = accumulatedChange.y;
accumulatedChange.x = 0;
accumulatedChange.y = 0;
Comment on lines +66 to +69
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 👍

setZoomDataCallback((prev) =>
translateZoom(
prev,
{ x, y: -y },
{
width: drawingArea.width,
height: drawingArea.height,
},
optionsLookup,
),
);

setZoomDataCallback(newZoomData);
});

const handlePan = (event: PanEvent) => {
const zoomData = startRef.current;

if (!zoomData) {
if (!isInteracting) {
return;
}
throttledCallback(event, zoomData);
accumulatedChange.x += event.detail.deltaX;
accumulatedChange.y += event.detail.deltaY;
throttledCallback();
};

const panHandler = instance.addInteractionListener('zoomPan', handlePan);
Expand All @@ -103,6 +108,5 @@ export const usePanOnDrag = (
drawingArea.height,
setZoomDataCallback,
store,
startRef,
]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export const usePanOnPressAndDrag = (
) => {
const drawingArea = useSelector(store, selectorChartDrawingArea);
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 });
const config = useSelector(store, selectorPanInteractionConfig, 'pressAndDrag' as const);

const isPanOnPressAndDragEnabled: boolean =
Expand Down Expand Up @@ -54,36 +55,40 @@ export const usePanOnPressAndDrag = (

const handlePressAndDragStart = (event: PressAndDragEvent) => {
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 };
}
};

const handlePressAndDragEnd = () => {
startRef.current = null;
isInteracting.current = false;
};

const throttledCallback = rafThrottle(
(event: PressAndDragEvent, zoomData: readonly ZoomData[]) => {
const newZoomData = translateZoom(
zoomData,
{ x: event.detail.activeDeltaX, y: -event.detail.activeDeltaY },
const throttledCallback = rafThrottle(() => {
const x = accumulatedChange.current.x;
const y = accumulatedChange.current.y;
accumulatedChange.current.x = 0;
accumulatedChange.current.y = 0;
setZoomDataCallback((prev) =>
translateZoom(
prev,
{ x, y: -y },
{
width: drawingArea.width,
height: drawingArea.height,
},
optionsLookup,
);

setZoomDataCallback(newZoomData);
},
);
),
);
});

const handlePressAndDrag = (event: PressAndDragEvent) => {
const zoomData = startRef.current;
if (!zoomData) {
if (!isInteracting.current) {
return;
}
throttledCallback(event, zoomData);
accumulatedChange.current.x += event.detail.deltaX;
accumulatedChange.current.y += event.detail.deltaY;
throttledCallback();
};

const pressAndDragHandler = instance.addInteractionListener(
Expand Down Expand Up @@ -114,6 +119,6 @@ export const usePanOnPressAndDrag = (
drawingArea.height,
setZoomDataCallback,
store,
startRef,
isInteracting,
]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,24 @@ export const useChartProZoom: ChartPlugin<UseChartProZoomSignature> = (pluginDat
});
}, [store, zoomInteractionConfig]);

// This is debounced. We want to run it only once after the interaction ends.
const removeIsInteracting = React.useMemo(
() =>
debounce(
() =>
store.set('zoom', {
...store.state.zoom,
isInteracting: false,
}),
166,
),
[store],
);

// Manage controlled state
React.useEffect(() => {
if (paramsZoomData === undefined) {
return undefined;
return;
}

if (process.env.NODE_ENV !== 'production' && !store.state.zoom.isControlled) {
Expand All @@ -64,47 +78,28 @@ export const useChartProZoom: ChartPlugin<UseChartProZoomSignature> = (pluginDat
isInteracting: true,
zoomData: paramsZoomData,
});

const timeout = setTimeout(() => {
store.set('zoom', {
...store.state.zoom,
isInteracting: false,
});
}, 166);

return () => {
clearTimeout(timeout);
};
}, [store, paramsZoomData]);

// This is debounced. We want to run it only once after the interaction ends.
const removeIsInteracting = React.useMemo(
() =>
debounce(
() =>
store.set('zoom', {
...store.state.zoom,
isInteracting: false,
}),
166,
),
[store],
);
removeIsInteracting();
}, [store, paramsZoomData, removeIsInteracting]);

const setZoomDataCallback = React.useCallback(
(zoomData: ZoomData[] | ((prev: ZoomData[]) => ZoomData[])) => {
const newZoomData =
typeof zoomData === 'function' ? zoomData([...store.state.zoom.zoomData]) : zoomData;
onZoomChange?.(newZoomData);

onZoomChange(newZoomData);
if (store.state.zoom.isControlled) {
return;
store.set('zoom', {
...store.state.zoom,
isInteracting: true,
});
} else {
store.set('zoom', {
...store.state.zoom,
isInteracting: true,
zoomData: newZoomData,
});
removeIsInteracting();
}
removeIsInteracting();
store.set('zoom', {
...store.state.zoom,
isInteracting: true,
zoomData: newZoomData,
});
},
[onZoomChange, store, removeIsInteracting],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,11 @@ export const useChartInteractionListener: ChartPlugin<UseChartInteractionListene
new PanGesture({
name: 'zoomPan',
threshold: 0,
maxPointers: 1,
preventIf: ['zoomTapAndDrag', 'zoomPressAndDrag'],
}),
new PinchGesture({
name: 'zoomPinch',
threshold: 5,
preventIf: ['pan', 'zoomPan'],
}),
new TurnWheelGesture({
name: 'zoomTurnWheel',
Expand Down
Loading
Loading