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
11 changes: 11 additions & 0 deletions docs/data/charts/zoom-and-pan/ZoomAndPanInteractions.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ const knobs = {
knob: 'switch',
defaultValue: true,
},
pressAndDrag: {
displayName: 'Press and drag',
knob: 'switch',
defaultValue: false,
},
Comment on lines +28 to +32
Copy link
Member

Choose a reason for hiding this comment

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

Out of topic for this PR but it' would be nice to have a knob: "title" to do the following. Otherwise it's a bit herder to know if the event impacts panning or zooming

image

};

export default function ZoomAndPanInteractions() {
Expand All @@ -50,6 +55,9 @@ export default function ZoomAndPanInteractions() {
if (props.drag) {
panInteractions.push('drag');
}
if (props.pressAndDrag) {
panInteractions.push('pressAndDrag');
}

const zoomInteractionConfig = {
zoom: zoomInteractions,
Expand Down Expand Up @@ -96,6 +104,9 @@ export default function ZoomAndPanInteractions() {
if (props.drag) {
panInteractions.push('drag');
}
if (props.pressAndDrag) {
panInteractions.push('pressAndDrag');
}

const zoomConfig =
zoomInteractions.length > 0
Expand Down
11 changes: 11 additions & 0 deletions docs/data/charts/zoom-and-pan/ZoomAndPanInteractions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ const knobs = {
knob: 'switch',
defaultValue: true,
},
pressAndDrag: {
displayName: 'Press and drag',
knob: 'switch',
defaultValue: false,
},
} as const;

export default function ZoomAndPanInteractions() {
Expand All @@ -51,6 +56,9 @@ export default function ZoomAndPanInteractions() {
if (props.drag) {
panInteractions.push('drag');
}
if (props.pressAndDrag) {
panInteractions.push('pressAndDrag');
}

const zoomInteractionConfig = {
zoom: zoomInteractions,
Expand Down Expand Up @@ -97,6 +105,9 @@ export default function ZoomAndPanInteractions() {
if (props.drag) {
panInteractions.push('drag');
}
if (props.pressAndDrag) {
panInteractions.push('pressAndDrag');
}

const zoomConfig =
zoomInteractions.length > 0
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/x/api/charts/bar-chart-pro.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
"zoomInteractionConfig": {
"type": {
"name": "shape",
"description": "{ pan?: Array&lt;'drag'<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'drag' }&gt;, zoom?: Array&lt;'pinch'<br>&#124;&nbsp;'tapAndDrag'<br>&#124;&nbsp;'wheel'<br>&#124;&nbsp;{ pointerMode?: any, requiredKeys?: Array&lt;string&gt;, type: 'wheel' }<br>&#124;&nbsp;{ pointerMode?: any, requiredKeys?: array, type: 'pinch' }<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'tapAndDrag' }&gt; }"
"description": "{ pan?: Array&lt;'drag'<br>&#124;&nbsp;'pressAndDrag'<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'drag' }<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'pressAndDrag' }&gt;, zoom?: Array&lt;'pinch'<br>&#124;&nbsp;'tapAndDrag'<br>&#124;&nbsp;'wheel'<br>&#124;&nbsp;{ pointerMode?: any, requiredKeys?: Array&lt;string&gt;, type: 'wheel' }<br>&#124;&nbsp;{ pointerMode?: any, requiredKeys?: array, type: 'pinch' }<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'tapAndDrag' }&gt; }"
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/x/api/charts/line-chart-pro.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
"zoomInteractionConfig": {
"type": {
"name": "shape",
"description": "{ pan?: Array&lt;'drag'<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'drag' }&gt;, zoom?: Array&lt;'pinch'<br>&#124;&nbsp;'tapAndDrag'<br>&#124;&nbsp;'wheel'<br>&#124;&nbsp;{ pointerMode?: any, requiredKeys?: Array&lt;string&gt;, type: 'wheel' }<br>&#124;&nbsp;{ pointerMode?: any, requiredKeys?: array, type: 'pinch' }<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'tapAndDrag' }&gt; }"
"description": "{ pan?: Array&lt;'drag'<br>&#124;&nbsp;'pressAndDrag'<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'drag' }<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'pressAndDrag' }&gt;, zoom?: Array&lt;'pinch'<br>&#124;&nbsp;'tapAndDrag'<br>&#124;&nbsp;'wheel'<br>&#124;&nbsp;{ pointerMode?: any, requiredKeys?: Array&lt;string&gt;, type: 'wheel' }<br>&#124;&nbsp;{ pointerMode?: any, requiredKeys?: array, type: 'pinch' }<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'tapAndDrag' }&gt; }"
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/x/api/charts/scatter-chart-pro.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
"zoomInteractionConfig": {
"type": {
"name": "shape",
"description": "{ pan?: Array&lt;'drag'<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'drag' }&gt;, zoom?: Array&lt;'pinch'<br>&#124;&nbsp;'tapAndDrag'<br>&#124;&nbsp;'wheel'<br>&#124;&nbsp;{ pointerMode?: any, requiredKeys?: Array&lt;string&gt;, type: 'wheel' }<br>&#124;&nbsp;{ pointerMode?: any, requiredKeys?: array, type: 'pinch' }<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'tapAndDrag' }&gt; }"
"description": "{ pan?: Array&lt;'drag'<br>&#124;&nbsp;'pressAndDrag'<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'drag' }<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'pressAndDrag' }&gt;, zoom?: Array&lt;'pinch'<br>&#124;&nbsp;'tapAndDrag'<br>&#124;&nbsp;'wheel'<br>&#124;&nbsp;{ pointerMode?: any, requiredKeys?: Array&lt;string&gt;, type: 'wheel' }<br>&#124;&nbsp;{ pointerMode?: any, requiredKeys?: array, type: 'pinch' }<br>&#124;&nbsp;{ pointerMode?: 'mouse'<br>&#124;&nbsp;'touch', requiredKeys?: Array&lt;string&gt;, type: 'tapAndDrag' }&gt; }"
}
}
},
Expand Down
7 changes: 6 additions & 1 deletion packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1917,12 +1917,17 @@ BarChartPro.propTypes = {
zoomInteractionConfig: PropTypes.shape({
pan: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.oneOf(['drag']),
PropTypes.oneOf(['drag', 'pressAndDrag']),
PropTypes.shape({
pointerMode: PropTypes.oneOf(['mouse', 'touch']),
requiredKeys: PropTypes.arrayOf(PropTypes.string),
type: PropTypes.oneOf(['drag']).isRequired,
}),
PropTypes.shape({
pointerMode: PropTypes.oneOf(['mouse', 'touch']),
requiredKeys: PropTypes.arrayOf(PropTypes.string),
type: PropTypes.oneOf(['pressAndDrag']).isRequired,
}),
]).isRequired,
),
zoom: PropTypes.arrayOf(
Expand Down
7 changes: 6 additions & 1 deletion packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1930,12 +1930,17 @@ LineChartPro.propTypes = {
zoomInteractionConfig: PropTypes.shape({
pan: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.oneOf(['drag']),
PropTypes.oneOf(['drag', 'pressAndDrag']),
PropTypes.shape({
pointerMode: PropTypes.oneOf(['mouse', 'touch']),
requiredKeys: PropTypes.arrayOf(PropTypes.string),
type: PropTypes.oneOf(['drag']).isRequired,
}),
PropTypes.shape({
pointerMode: PropTypes.oneOf(['mouse', 'touch']),
requiredKeys: PropTypes.arrayOf(PropTypes.string),
type: PropTypes.oneOf(['pressAndDrag']).isRequired,
}),
]).isRequired,
),
zoom: PropTypes.arrayOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,4 +419,93 @@ describe.skipIf(isJSDOM)('<LineChartPro /> - Zoom', () => {
// When the issue happens, the ticks are gone.
expect(getAxisTickValues('x', container).length).toBeGreaterThan(0);
});

it('should pan on press and drag', async () => {
const onZoomChange = sinon.spy();
const { user, container } = render(
<LineChartPro
{...lineChartProps}
initialZoom={[{ axisId: 'x', start: 75, end: 100 }]}
onZoomChange={onZoomChange}
zoomInteractionConfig={{
pan: ['pressAndDrag'],
}}
/>,
options,
);

expect(getAxisTickValues('x', container)).to.deep.equal(['D']);

const svg = container.querySelector('svg')!;

await user.pointer([
{
keys: '[MouseLeft>]',
target: svg,
coords: { x: 15, y: 20 },
},
]);

await act(async () => new Promise((r) => setTimeout(r, 510))); // wait for press delay

// we drag one position so C should be visible
await user.pointer([
{
target: svg,
coords: { x: 135, y: 20 },
},
{
keys: '[/MouseLeft]',
target: svg,
coords: { x: 135, y: 20 },
},
]);
// Wait the animation frame
await act(async () => new Promise((r) => requestAnimationFrame(r)));

expect(onZoomChange.callCount).to.equal(1);
expect(getAxisTickValues('x', container)).to.deep.equal(['C']);
});

it('should not pan on press and drag if there is no press', async () => {
const onZoomChange = sinon.spy();
const { user, container } = render(
<LineChartPro
{...lineChartProps}
initialZoom={[{ axisId: 'x', start: 75, end: 100 }]}
onZoomChange={onZoomChange}
zoomInteractionConfig={{
pan: ['pressAndDrag'],
}}
/>,
options,
);

expect(getAxisTickValues('x', container)).to.deep.equal(['D']);

const svg = container.querySelector('svg')!;

// we drag one position so C should be visible
await user.pointer([
{
keys: '[MouseLeft>]',
target: svg,
coords: { x: 15, y: 20 },
},
{
target: svg,
coords: { x: 135, y: 20 },
},
{
keys: '[/MouseLeft]',
target: svg,
coords: { x: 135, y: 20 },
},
]);
// Wait the animation frame
await act(async () => new Promise((r) => requestAnimationFrame(r)));

expect(onZoomChange.callCount).to.equal(0);
expect(getAxisTickValues('x', container)).to.deep.equal(['D']); // no change
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1918,12 +1918,17 @@ ScatterChartPro.propTypes = {
zoomInteractionConfig: PropTypes.shape({
pan: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.oneOf(['drag']),
PropTypes.oneOf(['drag', 'pressAndDrag']),
PropTypes.shape({
pointerMode: PropTypes.oneOf(['mouse', 'touch']),
requiredKeys: PropTypes.arrayOf(PropTypes.string),
type: PropTypes.oneOf(['drag']).isRequired,
}),
PropTypes.shape({
pointerMode: PropTypes.oneOf(['mouse', 'touch']),
requiredKeys: PropTypes.arrayOf(PropTypes.string),
type: PropTypes.oneOf(['pressAndDrag']).isRequired,
}),
]).isRequired,
),
zoom: PropTypes.arrayOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type ZoomInteractionConfig = {
/**
* Defines the interactions that trigger panning.
* - `drag`: Pans the chart when dragged with the mouse.
* - `pressAndDrag`: Pans the chart by pressing and holding, then dragging. Useful for avoiding conflicts with selection gestures.
*
* @default ['drag']
*/
Expand All @@ -32,7 +33,7 @@ export type DefaultizedZoomInteractionConfig = {
};

export type ZoomInteraction = WheelInteraction | PinchInteraction | TapAndDragInteraction;
export type PanInteraction = DragInteraction;
export type PanInteraction = DragInteraction | PressAndDragInteraction;

export type ZoomInteractionName = ZoomInteraction['type'];
export type PanInteractionName = PanInteraction['type'];
Expand Down Expand Up @@ -101,6 +102,13 @@ export type TapAndDragInteraction = Unpack<
AllKeysProp
>;

export type PressAndDragInteraction = Unpack<
{
type: 'pressAndDrag';
} & AllModeProp &
AllKeysProp
>;

export type AnyInteraction = {
type: string;
pointerMode?: InteractionMode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use client';
import * as React from 'react';
import {
ChartPlugin,
useSelector,
selectorChartDrawingArea,
ZoomData,
selectorChartZoomOptionsLookup,
} from '@mui/x-charts/internals';
import { rafThrottle } from '@mui/x-internals/rafThrottle';
import { PressAndDragEvent } from '@mui/x-internal-gestures/core';
import { UseChartProZoomSignature } from '../useChartProZoom.types';
import { translateZoom } from './useZoom.utils';
import { selectorPanInteractionConfig } from '../ZoomInteractionConfig.selectors';

export const usePanOnPressAndDrag = (
{
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 startRef = React.useRef<readonly ZoomData[]>(null);
const config = useSelector(store, selectorPanInteractionConfig, ['pressAndDrag' as const]);

const isPanOnPressAndDragEnabled = React.useMemo(
() => (Object.values(optionsLookup).some((v) => v.panning) && config) || false,
[optionsLookup, config],
);

React.useEffect(() => {
if (!isPanOnPressAndDragEnabled) {
return;
}

instance.updateZoomInteractionListeners('zoomPressAndDrag', {
requiredKeys: config!.requiredKeys,
pointerMode: config!.pointerMode,
pointerOptions: {
mouse: config!.mouse,
touch: config!.touch,
},
});
}, [isPanOnPressAndDragEnabled, config, instance]);

// Add event for chart panning with press and drag
React.useEffect(() => {
const element = svgRef.current;

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

const handlePressAndDragStart = (event: PressAndDragEvent) => {
if (!(event.detail.target as SVGElement)?.closest('[data-charts-zoom-slider]')) {
startRef.current = store.value.zoom.zoomData;
}
};

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

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

setZoomDataCallback(newZoomData);
},
);

const handlePressAndDrag = (event: PressAndDragEvent) => {
const zoomData = startRef.current;
if (!zoomData) {
return;
}
throttledCallback(event, zoomData);
};

const pressAndDragHandler = instance.addInteractionListener(
'zoomPressAndDrag',
handlePressAndDrag,
);
const pressAndDragStartHandler = instance.addInteractionListener(
'zoomPressAndDragStart',
handlePressAndDragStart,
);
const pressAndDragEndHandler = instance.addInteractionListener(
'zoomPressAndDragEnd',
handlePressAndDragEnd,
);

return () => {
pressAndDragStartHandler.cleanup();
pressAndDragHandler.cleanup();
pressAndDragEndHandler.cleanup();
throttledCallback.clear();
};
}, [
instance,
svgRef,
isPanOnPressAndDragEnabled,
optionsLookup,
drawingArea.width,
drawingArea.height,
setZoomDataCallback,
store,
startRef,
]);
};
Loading
Loading