Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
b03ccf2
add plugin start
JCQuintas Mar 6, 2025
6d80993
add chart interaction using @use-gesture/react
JCQuintas Mar 7, 2025
ba8b61a
replace tooltip logic
JCQuintas Mar 7, 2025
db75765
rename vars
JCQuintas Mar 7, 2025
04f4a3a
update axis event click handler
JCQuintas Mar 7, 2025
7f6b43e
improve
JCQuintas Mar 9, 2025
baccbf1
rename
JCQuintas Mar 9, 2025
b4f0d24
prevent useInteractionItemProps re-renders
JCQuintas Mar 10, 2025
48b461e
Fix perf related to react spring
JCQuintas Mar 10, 2025
e38fcee
Merge commit 'd3cde4cfcbc42b685da49928caefa531ea3c3711' into pointer-…
JCQuintas Mar 12, 2025
7988ff0
fix missing
JCQuintas Mar 12, 2025
29923fa
add mouse position tracker back
JCQuintas Mar 12, 2025
6abb966
migrate polar axis
JCQuintas Mar 12, 2025
7ebb091
add wheel listeners
JCQuintas Mar 12, 2025
637e193
Merge commit '06d8025ed7b5eb14b4d125924b4eeefea9af12a2' into pointer-…
JCQuintas Mar 12, 2025
6dad5f2
log
JCQuintas Mar 12, 2025
6a8ad69
fix listeners bug
JCQuintas Mar 12, 2025
989c284
clear animation on drag
JCQuintas Mar 12, 2025
d9b51cd
move zoom wheel setup to its own hook
JCQuintas Mar 13, 2025
1cb800a
zoom on pinch
JCQuintas Mar 13, 2025
a019178
move pan on drag
JCQuintas Mar 14, 2025
3c18086
cleanup cache logic
JCQuintas Mar 14, 2025
61bf657
fix mobile pinch
JCQuintas Mar 14, 2025
9595fac
reorder
JCQuintas Mar 14, 2025
223c9b1
fix mobile interaction
JCQuintas Mar 14, 2025
568f7da
allow config multiple listeners at once
JCQuintas Mar 14, 2025
c89dabd
tree shake
JCQuintas Mar 16, 2025
01899c7
remove el check
JCQuintas Mar 16, 2025
f9a56a7
use memo for storing data
JCQuintas Mar 16, 2025
b93cbd1
remove comment
JCQuintas Mar 16, 2025
73f39f3
cleanup drag
JCQuintas Mar 17, 2025
8dbaf63
fix pinch mobile
JCQuintas Mar 17, 2025
9b4933d
prevent scroll on mobile
JCQuintas Mar 17, 2025
3996fa9
Revert "tree shake"
JCQuintas Mar 17, 2025
8b1b724
Merge commit 'e5a59cd69b84265f2957563270a341bd035fa401' into pointer-…
JCQuintas Mar 17, 2025
c6be367
remove preventscroll option as it messes tapping
JCQuintas Mar 17, 2025
8873928
Merge commit 'bb6ce455efdd7abaee82eed64a9dbef8584abb23' into pointer-…
JCQuintas Mar 18, 2025
0dece29
add bar tests
JCQuintas Mar 19, 2025
f595e0c
add further tests
JCQuintas Mar 19, 2025
290e7f0
remove only
JCQuintas Mar 19, 2025
75872f4
Merge branch 'master' into pointer-events-improvement
JCQuintas Mar 19, 2025
ecafae6
fix wheel update depth issue
JCQuintas Mar 19, 2025
c5f6c8e
remove timeout on wheel need
JCQuintas Mar 19, 2025
cc9e738
add docs
JCQuintas Mar 20, 2025
0edea79
rework tap
JCQuintas Mar 20, 2025
fd88fbe
allow pointermove
JCQuintas Mar 21, 2025
02c1a29
move utils
JCQuintas Mar 24, 2025
1cccafa
use native events
JCQuintas Mar 25, 2025
c55a5e3
pan
JCQuintas Mar 25, 2025
f1a0e33
Merge commit '83b5b33ec34e1cb6b91f134c0c442b3ac9da3cf1' into pointer-…
JCQuintas Mar 25, 2025
220efd1
ensure zoom is called on RAF
JCQuintas Mar 25, 2025
7fb1449
Add debouce and automatic interacting
JCQuintas Mar 25, 2025
4c3bb03
Merge commit '4306063b7e5402a9e1b58262900a2732de464eed' into pointer-…
JCQuintas Apr 3, 2025
aedde9a
Merge commit '329bb76fde9d31e1c9a733f6a3dc14c1beff39ed' into pointer-…
JCQuintas Apr 3, 2025
4eb9075
Merge commit 'd1ed9b1a8bd879812fbc7d16a753f9b6f7714db9' into pointer-…
JCQuintas Apr 7, 2025
d3e4d6b
Merge commit '9427044825eae14d4ef6d0c2e5de1af5ce1367d6' into pointer-…
JCQuintas Apr 7, 2025
fe32424
fix type
JCQuintas Apr 7, 2025
64ae3a4
fix mobile pan too fast issue
JCQuintas Apr 7, 2025
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
2 changes: 1 addition & 1 deletion docs/data/charts/zoom-and-pan/ExternalZoomManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const initialZoomData: ZoomData[] = [
},
];
export default function ExternalZoomManagement() {
const apiRef = React.useRef(undefined) as React.MutableRefObject<
const apiRef = React.useRef(undefined) as React.RefObject<
ChartPublicAPI<[any]> | undefined
>;
const [zoomData, setZoomData] = React.useState(initialZoomData);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use client';
import * as React from 'react';
import {
ChartPlugin,
useSelector,
getSVGPoint,
selectorChartDrawingArea,
ZoomData,
selectorChartZoomOptionsLookup,
} from '@mui/x-charts/internals';
import { UseChartProZoomSignature } from '../useChartProZoom.types';
import { translateZoom } from './useZoom.utils';

export const usePanOnDrag = (
{
store,
instance,
svgRef,
}: Pick<Parameters<ChartPlugin<UseChartProZoomSignature>>[0], 'store' | 'instance' | 'svgRef'>,
setZoomDataCallback: React.Dispatch<ZoomData[] | ((prev: ZoomData[]) => ZoomData[])>,
) => {
const drawingArea = useSelector(store, selectorChartDrawingArea);
const optionsLookup = useSelector(store, selectorChartZoomOptionsLookup);

// Add event for chart panning
const isPanEnabled = React.useMemo(
() => Object.values(optionsLookup).some((v) => v.panning) || false,
[optionsLookup],
);

React.useEffect(() => {
const element = svgRef.current;

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

const panHandler = instance.addInteractionListener<readonly ZoomData[]>('drag', (state) => {
if (state.pinching) {
state.cancel();
return undefined;
}

if (!state.memo) {
state.memo = store.getSnapshot().zoom.zoomData;
}

const point = getSVGPoint(element, {
clientX: state.xy[0],
clientY: state.xy[1],
});
const originalPoint = getSVGPoint(element, {
clientX: state.initial[0],
clientY: state.initial[1],
});
const movementX = point.x - originalPoint.x;
const movementY = (point.y - originalPoint.y) * -1;
const newZoomData = translateZoom(
state.memo,
{ x: movementX, y: movementY },
{
width: drawingArea.width,
height: drawingArea.height,
},
optionsLookup,
);

setZoomDataCallback(newZoomData);
return state.memo;
});

return () => {
panHandler.cleanup();
};
}, [
instance,
svgRef,
isPanEnabled,
optionsLookup,
drawingArea.width,
drawingArea.height,
setZoomDataCallback,
store,
]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -102,35 +102,6 @@ export function getWheelScaleRatio(event: WheelEvent, step: number) {
return { scaleRatio, isZoomIn };
}

/**
* Get the scale ratio and if it's a zoom in or out from a pinch gesture.
*/
export function getPinchScaleRatio(curDiff: number, prevDiff: number, step: number) {
const scaledStep = step / 1000;
let scaleRatio: number = 0;
let isZoomIn: boolean = false;

const hasMoved = prevDiff > 0;

if (hasMoved && curDiff > prevDiff) {
// The distance between the two pointers has increased
scaleRatio = 1 + scaledStep;
isZoomIn = true;
}
if (hasMoved && curDiff < prevDiff) {
// The distance between the two pointers has decreased
scaleRatio = 1 - scaledStep;
isZoomIn = false;
}

return { scaleRatio, isZoomIn };
}

export function getDiff(eventCache: PointerEvent[]) {
const [firstEvent, secondEvent] = eventCache;
return Math.hypot(firstEvent.pageX - secondEvent.pageX, firstEvent.pageY - secondEvent.pageY);
}

/**
* Get the ratio of the point in the horizontal center of the area.
*/
Expand All @@ -142,14 +113,60 @@ export function getHorizontalCenterRatio(
return (point.x - left) / width;
}

export function preventDefault(event: TouchEvent) {
event.preventDefault();
}

/**
* Get the ratio of the point in the vertical center of the area.
*/
export function getVerticalCenterRatio(
point: { x: number; y: number },
area: { top: number; height: number },
) {
const { top, height } = area;
return ((point.y - top) / height) * -1 + 1;
}

/**
* Translate the zoom data by a given movement.
*/
export function translateZoom(
initialZoomData: readonly ZoomData[],
movement: { x: number; y: number },
drawingArea: { width: number; height: number },
optionsLookup: Record<string | number, DefaultizedZoomOptions>,
) {
return initialZoomData.map((zoom) => {
const options = optionsLookup[zoom.axisId];
if (!options || !options.panning) {
return zoom;
}
const min = zoom.start;
const max = zoom.end;
const span = max - min;
const MIN_PERCENT = options.minStart;
const MAX_PERCENT = options.maxEnd;
const displacement = options.axisDirection === 'x' ? movement.x : movement.y;
const dimension = options.axisDirection === 'x' ? drawingArea.width : drawingArea.height;
let newMinPercent = min - (displacement / dimension) * span;
let newMaxPercent = max - (displacement / dimension) * span;
if (newMinPercent < MIN_PERCENT) {
newMinPercent = MIN_PERCENT;
newMaxPercent = newMinPercent + span;
}
if (newMaxPercent > MAX_PERCENT) {
newMaxPercent = MAX_PERCENT;
newMinPercent = newMaxPercent - span;
}
if (
newMinPercent < MIN_PERCENT ||
newMaxPercent > MAX_PERCENT ||
span < options.minSpan ||
span > options.maxSpan
) {
return zoom;
}
return {
...zoom,
start: newMinPercent,
end: newMaxPercent,
};
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client';
import * as React from 'react';
import {
ChartPlugin,
useSelector,
getSVGPoint,
selectorChartDrawingArea,
ZoomData,
selectorChartZoomOptionsLookup,
} from '@mui/x-charts/internals';
import { UseChartProZoomSignature } from '../useChartProZoom.types';
import {
getHorizontalCenterRatio,
getVerticalCenterRatio,
isSpanValid,
zoomAtPoint,
} from './useZoom.utils';

export const useZoomOnPinch = (
{
store,
instance,
svgRef,
}: Pick<Parameters<ChartPlugin<UseChartProZoomSignature>>[0], 'store' | 'instance' | 'svgRef'>,
setZoomDataCallback: React.Dispatch<ZoomData[] | ((prev: ZoomData[]) => ZoomData[])>,
) => {
const drawingArea = useSelector(store, selectorChartDrawingArea);
const optionsLookup = useSelector(store, selectorChartZoomOptionsLookup);
const isZoomEnabled = Object.keys(optionsLookup).length > 0;

// Zoom on pinch
React.useEffect(() => {
const element = svgRef.current;
if (element === null || !isZoomEnabled) {
return () => {};
}

const zoomHandler = instance.addInteractionListener('pinch', (state) => {
setZoomDataCallback((prevZoomData) => {
const newZoomData = prevZoomData.map((zoom) => {
const option = optionsLookup[zoom.axisId];
if (!option) {
return zoom;
}

const scaledStep = option.step / 1000;
const isZoomIn = state.direction[0] > 0;
const scaleRatio = 1 + (isZoomIn ? scaledStep : -scaledStep);

// If the delta is 0, it means the pinch gesture is not valid.
if (state.delta[0] === 0) {
return zoom;
}

const point = getSVGPoint(element, {
clientX: state.origin[0],
clientY: state.origin[1],
});

const centerRatio =
option.axisDirection === 'x'
? getHorizontalCenterRatio(point, drawingArea)
: getVerticalCenterRatio(point, drawingArea);

const [newMinRange, newMaxRange] = zoomAtPoint(centerRatio, scaleRatio, zoom, option);

if (!isSpanValid(newMinRange, newMaxRange, isZoomIn, option)) {
return zoom;
}
return { axisId: zoom.axisId, start: newMinRange, end: newMaxRange };
});
return newZoomData;
});
});

return () => {
zoomHandler.cleanup();
};
}, [svgRef, drawingArea, isZoomEnabled, optionsLookup, instance, setZoomDataCallback]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client';
import * as React from 'react';
import {
ChartPlugin,
useSelector,
getSVGPoint,
selectorChartDrawingArea,
ZoomData,
selectorChartZoomOptionsLookup,
} from '@mui/x-charts/internals';
import { UseChartProZoomSignature } from '../useChartProZoom.types';
import {
getHorizontalCenterRatio,
getVerticalCenterRatio,
getWheelScaleRatio,
isSpanValid,
zoomAtPoint,
} from './useZoom.utils';

export const useZoomOnWheel = (
{
store,
instance,
svgRef,
}: Pick<Parameters<ChartPlugin<UseChartProZoomSignature>>[0], 'store' | 'instance' | 'svgRef'>,
setZoomDataCallback: React.Dispatch<ZoomData[] | ((prev: ZoomData[]) => ZoomData[])>,
) => {
const drawingArea = useSelector(store, selectorChartDrawingArea);
const optionsLookup = useSelector(store, selectorChartZoomOptionsLookup);
const isZoomEnabled = Object.keys(optionsLookup).length > 0;

// Add event for chart zoom in/out
React.useEffect(() => {
const element = svgRef.current;
if (element === null || !isZoomEnabled) {
return () => {};
}

const zoomOnWheelHandler = instance.addInteractionListener<readonly ZoomData[]>(
'wheel',
(state) => {
const point = getSVGPoint(element, state.event);

if (!instance.isPointInside(point)) {
return;
}

if (!state.memo) {
state.memo = store.getSnapshot().zoom.zoomData;
}

const newZoomData = state.memo.map((zoom) => {
const option = optionsLookup[zoom.axisId];
if (!option) {
return zoom;
}
const centerRatio =
option.axisDirection === 'x'
? getHorizontalCenterRatio(point, drawingArea)
: getVerticalCenterRatio(point, drawingArea);

const { scaleRatio, isZoomIn } = getWheelScaleRatio(state.event, option.step);
const [newMinRange, newMaxRange] = zoomAtPoint(centerRatio, scaleRatio, zoom, option);

if (!isSpanValid(newMinRange, newMaxRange, isZoomIn, option)) {
return zoom;
}

return { axisId: zoom.axisId, start: newMinRange, end: newMaxRange };
});

setZoomDataCallback(newZoomData);
},
);

return () => {
zoomOnWheelHandler.cleanup();
};
}, [svgRef, drawingArea, isZoomEnabled, optionsLookup, instance, setZoomDataCallback, store]);
};
Loading