Skip to content

Commit fe5209d

Browse files
committed
extract zoom interaction logic
1 parent 7c41b8d commit fe5209d

10 files changed

Lines changed: 447 additions & 282 deletions

File tree

Lines changed: 7 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
'use client';
2-
import * as React from 'react';
32
import {
43
type ChartPlugin,
54
selectorChartDrawingArea,
65
type ZoomData,
76
selectorChartZoomOptionsLookup,
87
} from '@mui/x-charts/internals';
9-
import { rafThrottle } from '@mui/x-internals/rafThrottle';
10-
import { type PanEvent } from '@mui/x-internal-gestures/core';
118
import { type UseChartProZoomSignature } from '../useChartProZoom.types';
129
import { translateZoom } from './useZoom.utils';
1310
import { selectorPanInteractionConfig } from '../ZoomInteractionConfig.selectors';
11+
import { usePanGesture } from '../../zoomGestures/usePanGesture';
1412

1513
export const usePanOnDrag = (
1614
{
@@ -19,93 +17,27 @@ export const usePanOnDrag = (
1917
}: Pick<Parameters<ChartPlugin<UseChartProZoomSignature>>[0], 'store' | 'instance'>,
2018
setZoomDataCallback: React.Dispatch<ZoomData[] | ((prev: ZoomData[]) => ZoomData[])>,
2119
) => {
22-
const { chartsLayerContainerRef } = instance;
2320
const drawingArea = store.use(selectorChartDrawingArea);
2421
const optionsLookup = store.use(selectorChartZoomOptionsLookup);
2522
const config = store.use(selectorPanInteractionConfig, 'drag' as const);
2623

2724
const isPanOnDragEnabled: boolean =
2825
Object.values(optionsLookup).some((v) => v.panning) && Boolean(config);
2926

30-
React.useEffect(() => {
31-
if (!isPanOnDragEnabled) {
32-
return;
33-
}
34-
35-
instance.updateZoomInteractionListeners('zoomPan', {
36-
requiredKeys: config!.requiredKeys,
37-
pointerMode: config!.pointerMode,
38-
pointerOptions: {
39-
mouse: config!.mouse,
40-
touch: config!.touch,
41-
},
42-
});
43-
}, [isPanOnDragEnabled, config, instance]);
44-
45-
// Add event for chart panning
46-
React.useEffect(() => {
47-
const element = chartsLayerContainerRef.current;
48-
let isInteracting = false;
49-
const accumulatedChange = { x: 0, y: 0 };
50-
51-
if (element === null || !isPanOnDragEnabled) {
52-
return () => {};
53-
}
54-
55-
const handlePanStart = (event: PanEvent) => {
56-
if (!(event.detail.target as SVGElement)?.closest('[data-charts-zoom-slider]')) {
57-
isInteracting = true;
58-
}
59-
};
60-
const handlePanEnd = () => {
61-
isInteracting = false;
62-
};
63-
64-
const throttledCallback = rafThrottle(() => {
65-
const x = accumulatedChange.x;
66-
const y = accumulatedChange.y;
67-
accumulatedChange.x = 0;
68-
accumulatedChange.y = 0;
27+
usePanGesture(instance, {
28+
enabled: isPanOnDragEnabled,
29+
onPan: (delta) => {
6930
setZoomDataCallback((prev) =>
7031
translateZoom(
7132
prev,
72-
{ x, y: -y },
33+
{ x: delta.x, y: -delta.y },
7334
{
7435
width: drawingArea.width,
7536
height: drawingArea.height,
7637
},
7738
optionsLookup,
7839
),
7940
);
80-
});
81-
82-
const handlePan = (event: PanEvent) => {
83-
if (!isInteracting) {
84-
return;
85-
}
86-
accumulatedChange.x += event.detail.deltaX;
87-
accumulatedChange.y += event.detail.deltaY;
88-
throttledCallback();
89-
};
90-
91-
const panHandler = instance.addInteractionListener('zoomPan', handlePan);
92-
const panStartHandler = instance.addInteractionListener('zoomPanStart', handlePanStart);
93-
const panEndHandler = instance.addInteractionListener('zoomPanEnd', handlePanEnd);
94-
95-
return () => {
96-
panStartHandler.cleanup();
97-
panHandler.cleanup();
98-
panEndHandler.cleanup();
99-
throttledCallback.clear();
100-
};
101-
}, [
102-
instance,
103-
chartsLayerContainerRef,
104-
isPanOnDragEnabled,
105-
optionsLookup,
106-
drawingArea.width,
107-
drawingArea.height,
108-
setZoomDataCallback,
109-
store,
110-
]);
41+
},
42+
});
11143
};
Lines changed: 7 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
'use client';
2-
import * as React from 'react';
32
import {
43
type ChartPlugin,
54
selectorChartDrawingArea,
65
type ZoomData,
76
selectorChartZoomOptionsLookup,
87
} from '@mui/x-charts/internals';
9-
import { rafThrottle } from '@mui/x-internals/rafThrottle';
10-
import { type PressAndDragEvent } from '@mui/x-internal-gestures/core';
118
import { type UseChartProZoomSignature } from '../useChartProZoom.types';
129
import { translateZoom } from './useZoom.utils';
1310
import { selectorPanInteractionConfig } from '../ZoomInteractionConfig.selectors';
11+
import { usePanOnPressGesture } from '../../zoomGestures/usePanOnPressGesture';
1412

1513
export const usePanOnPressAndDrag = (
1614
{
@@ -19,105 +17,27 @@ export const usePanOnPressAndDrag = (
1917
}: Pick<Parameters<ChartPlugin<UseChartProZoomSignature>>[0], 'store' | 'instance'>,
2018
setZoomDataCallback: React.Dispatch<ZoomData[] | ((prev: ZoomData[]) => ZoomData[])>,
2119
) => {
22-
const { chartsLayerContainerRef } = instance;
2320
const drawingArea = store.use(selectorChartDrawingArea);
2421
const optionsLookup = store.use(selectorChartZoomOptionsLookup);
25-
const isInteracting = React.useRef<boolean>(false);
26-
const accumulatedChange = React.useRef<{ x: number; y: number }>({ x: 0, y: 0 });
2722
const config = store.use(selectorPanInteractionConfig, 'pressAndDrag' as const);
2823

2924
const isPanOnPressAndDragEnabled: boolean =
3025
Object.values(optionsLookup).some((v) => v.panning) && Boolean(config);
3126

32-
React.useEffect(() => {
33-
if (!isPanOnPressAndDragEnabled) {
34-
return;
35-
}
36-
37-
instance.updateZoomInteractionListeners('zoomPressAndDrag', {
38-
requiredKeys: config!.requiredKeys,
39-
pointerMode: config!.pointerMode,
40-
pointerOptions: {
41-
mouse: config!.mouse,
42-
touch: config!.touch,
43-
},
44-
});
45-
}, [isPanOnPressAndDragEnabled, config, instance]);
46-
47-
// Add event for chart panning with press and drag
48-
React.useEffect(() => {
49-
const element = chartsLayerContainerRef.current;
50-
51-
if (element === null || !isPanOnPressAndDragEnabled) {
52-
return () => {};
53-
}
54-
55-
const handlePressAndDragStart = (event: PressAndDragEvent) => {
56-
if (!(event.detail.target as SVGElement)?.closest('[data-charts-zoom-slider]')) {
57-
isInteracting.current = true;
58-
accumulatedChange.current = { x: 0, y: 0 };
59-
}
60-
};
61-
62-
const handlePressAndDragEnd = () => {
63-
isInteracting.current = false;
64-
};
65-
66-
const throttledCallback = rafThrottle(() => {
67-
const x = accumulatedChange.current.x;
68-
const y = accumulatedChange.current.y;
69-
accumulatedChange.current.x = 0;
70-
accumulatedChange.current.y = 0;
27+
usePanOnPressGesture(instance, {
28+
enabled: isPanOnPressAndDragEnabled,
29+
onPan: (delta) => {
7130
setZoomDataCallback((prev) =>
7231
translateZoom(
7332
prev,
74-
{ x, y: -y },
33+
{ x: delta.x, y: -delta.y },
7534
{
7635
width: drawingArea.width,
7736
height: drawingArea.height,
7837
},
7938
optionsLookup,
8039
),
8140
);
82-
});
83-
84-
const handlePressAndDrag = (event: PressAndDragEvent) => {
85-
if (!isInteracting.current) {
86-
return;
87-
}
88-
accumulatedChange.current.x += event.detail.deltaX;
89-
accumulatedChange.current.y += event.detail.deltaY;
90-
throttledCallback();
91-
};
92-
93-
const pressAndDragHandler = instance.addInteractionListener(
94-
'zoomPressAndDrag',
95-
handlePressAndDrag,
96-
);
97-
const pressAndDragStartHandler = instance.addInteractionListener(
98-
'zoomPressAndDragStart',
99-
handlePressAndDragStart,
100-
);
101-
const pressAndDragEndHandler = instance.addInteractionListener(
102-
'zoomPressAndDragEnd',
103-
handlePressAndDragEnd,
104-
);
105-
106-
return () => {
107-
pressAndDragStartHandler.cleanup();
108-
pressAndDragHandler.cleanup();
109-
pressAndDragEndHandler.cleanup();
110-
throttledCallback.clear();
111-
};
112-
}, [
113-
instance,
114-
chartsLayerContainerRef,
115-
isPanOnPressAndDragEnabled,
116-
optionsLookup,
117-
drawingArea.width,
118-
drawingArea.height,
119-
setZoomDataCallback,
120-
store,
121-
isInteracting,
122-
]);
41+
},
42+
});
12343
};
Lines changed: 9 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
'use client';
2-
import * as React from 'react';
32
import {
43
type ChartPlugin,
5-
getChartPoint,
64
selectorChartDrawingArea,
75
type ZoomData,
86
selectorChartZoomOptionsLookup,
97
} from '@mui/x-charts/internals';
10-
import { type PinchEvent } from '@mui/x-internal-gestures/core';
11-
import { rafThrottle } from '@mui/x-internals/rafThrottle';
128
import { type UseChartProZoomSignature } from '../useChartProZoom.types';
139
import {
1410
getHorizontalCenterRatio,
@@ -17,6 +13,7 @@ import {
1713
zoomAtPoint,
1814
} from './useZoom.utils';
1915
import { selectorZoomInteractionConfig } from '../ZoomInteractionConfig.selectors';
16+
import { usePinchGesture } from '../../zoomGestures/usePinchGesture';
2017

2118
export const useZoomOnPinch = (
2219
{
@@ -25,50 +22,26 @@ export const useZoomOnPinch = (
2522
}: Pick<Parameters<ChartPlugin<UseChartProZoomSignature>>[0], 'store' | 'instance'>,
2623
setZoomDataCallback: React.Dispatch<ZoomData[] | ((prev: ZoomData[]) => ZoomData[])>,
2724
) => {
28-
const { chartsLayerContainerRef } = instance;
2925
const drawingArea = store.use(selectorChartDrawingArea);
3026
const optionsLookup = store.use(selectorChartZoomOptionsLookup);
3127
const config = store.use(selectorZoomInteractionConfig, 'pinch' as const);
3228

3329
const isZoomOnPinchEnabled: boolean = Object.keys(optionsLookup).length > 0 && Boolean(config);
3430

35-
React.useEffect(() => {
36-
if (!isZoomOnPinchEnabled) {
37-
return;
38-
}
39-
40-
instance.updateZoomInteractionListeners('zoomPinch', {
41-
requiredKeys: config!.requiredKeys,
42-
});
43-
}, [config, isZoomOnPinchEnabled, instance]);
44-
45-
// Zoom on pinch
46-
React.useEffect(() => {
47-
const element = chartsLayerContainerRef.current;
48-
if (element === null || !isZoomOnPinchEnabled) {
49-
return () => {};
50-
}
51-
52-
const rafThrottledCallback = rafThrottle((event: PinchEvent) => {
53-
// If the delta is 0, it means the pinch gesture is not valid.
54-
if (event.detail.direction === 0) {
55-
return;
56-
}
5731

32+
usePinchGesture(instance, {
33+
enabled: isZoomOnPinchEnabled,
34+
requiredKeys: config?.requiredKeys,
35+
onPinch: (point, deltaScale, direction) => {
5836
setZoomDataCallback((prev) => {
5937
return prev.map((zoom) => {
6038
const option = optionsLookup[zoom.axisId];
6139
if (!option) {
6240
return zoom;
6341
}
6442

65-
const isZoomIn = event.detail.direction > 0;
66-
const scaleRatio = 1 + event.detail.deltaScale;
67-
68-
const point = getChartPoint(element, {
69-
clientX: event.detail.centroid.x,
70-
clientY: event.detail.centroid.y,
71-
});
43+
const isZoomIn = direction > 0;
44+
const scaleRatio = 1 + deltaScale;
7245

7346
const centerRatio =
7447
option.axisDirection === 'x'
@@ -83,21 +56,7 @@ export const useZoomOnPinch = (
8356
return { axisId: zoom.axisId, start: newMinRange, end: newMaxRange };
8457
});
8558
});
86-
});
59+
},
60+
});
8761

88-
const zoomHandler = instance.addInteractionListener('zoomPinch', rafThrottledCallback);
89-
90-
return () => {
91-
zoomHandler.cleanup();
92-
rafThrottledCallback.clear();
93-
};
94-
}, [
95-
chartsLayerContainerRef,
96-
drawingArea,
97-
isZoomOnPinchEnabled,
98-
optionsLookup,
99-
store,
100-
instance,
101-
setZoomDataCallback,
102-
]);
10362
};

0 commit comments

Comments
 (0)