diff --git a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx
index f80b2971..66cab6a0 100644
--- a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx
+++ b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx
@@ -19,25 +19,25 @@ const initialNodes: Node[] = [
id: '1',
type: 'input',
data: { label: 'Node 1' },
- position: { x: 250, y: 5 },
+ position: { x: 0, y: 0 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
- position: { x: 100, y: 100 },
+ position: { x: 0, y: 200 },
className: 'light',
},
{
id: '3',
data: { label: 'Node 3' },
- position: { x: 400, y: 100 },
+ position: { x: 200, y: 200 },
className: 'light',
},
{
id: '4',
data: { label: 'Node 4' },
- position: { x: 400, y: 200 },
+ position: { x: 200, y: 400 },
className: 'light',
},
];
@@ -88,9 +88,9 @@ const BasicFlow = () => {
className="react-flow-basic-example"
minZoom={0.2}
maxZoom={4}
- fitView
defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false}
+ fitView
>
diff --git a/packages/core/src/container/ZoomPane/index.tsx b/packages/core/src/container/ZoomPane/index.tsx
index cde4ec47..a5783a7a 100644
--- a/packages/core/src/container/ZoomPane/index.tsx
+++ b/packages/core/src/container/ZoomPane/index.tsx
@@ -173,9 +173,12 @@ const ZoomPane = ({
useEffect(() => {
if (d3Zoom) {
d3Zoom.on('start', (event: D3ZoomEvent) => {
+ if (!event.sourceEvent) {
+ return null;
+ }
+
const { onViewportChangeStart } = store.getState();
isZoomingOrPanning.current = true;
-
if (event.sourceEvent?.type === 'mousedown') {
store.setState({ paneDragging: true });
}
@@ -194,6 +197,9 @@ const ZoomPane = ({
useEffect(() => {
if (d3Zoom) {
d3Zoom.on('end', (event: D3ZoomEvent) => {
+ if (!event.sourceEvent) {
+ return null;
+ }
const { onViewportChangeEnd } = store.getState();
isZoomingOrPanning.current = false;
diff --git a/packages/interactive-minimap/src/MiniMap.tsx b/packages/interactive-minimap/src/MiniMap.tsx
index b7ef604c..e5a7927e 100644
--- a/packages/interactive-minimap/src/MiniMap.tsx
+++ b/packages/interactive-minimap/src/MiniMap.tsx
@@ -2,9 +2,19 @@
import { memo, useEffect, useRef } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
-import { zoom, D3ZoomEvent } from 'd3-zoom';
+import { zoom, D3ZoomEvent, zoomIdentity } from 'd3-zoom';
import { select } from 'd3-selection';
-import { useStore, getRectOfNodes, ReactFlowState, Rect, Panel, getBoundsOfRects } from '@reactflow/core';
+import {
+ useStore,
+ getRectOfNodes,
+ ReactFlowState,
+ Rect,
+ Panel,
+ getBoundsOfRects,
+ useStoreApi,
+ useReactFlow,
+ XYPosition,
+} from '@reactflow/core';
import MiniMapNode from './MiniMapNode';
import { MiniMapProps, GetMiniMapNodeAttribute } from './types';
@@ -46,7 +56,9 @@ function MiniMap({
maskColor = 'rgb(240, 242, 243, 0.7)',
position = 'bottom-right',
}: MiniMapProps) {
+ const store = useStoreApi();
const svg = useRef(null);
+ const initialized = useRef(false);
const { boundingRect, viewBB, nodes, rfId } = useStore(selector, shallow);
const elementWidth = (style?.width as number) ?? defaultWidth;
const elementHeight = (style?.height as number) ?? defaultHeight;
@@ -65,15 +77,69 @@ function MiniMap({
const height = viewHeight + offset * 2;
const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`;
+ const { setCenter } = useReactFlow();
+ const startTransform = useRef({ x: 0, y: 0 });
+ const viewScaleRef = useRef(0);
+ const viewScaleWRef = useRef(0);
+ const viewScaleHRef = useRef(0);
+
+ viewScaleRef.current = viewScale;
+ viewScaleWRef.current = scaledWidth;
+ viewScaleHRef.current = scaledHeight;
useEffect(() => {
- const d3ZoomInstance = zoom();
- const selection = select(svg.current as Element).call(d3ZoomInstance);
+ if (!initialized.current && svg.current) {
+ const bounds = svg.current.getBoundingClientRect() || { left: 0, top: 0 };
- d3ZoomInstance.on('zoom', (event: D3ZoomEvent) => {
- console.log(event);
- });
- }, []);
+ const selection = select(svg.current as Element);
+
+ const zoomHandler = zoom()
+ .on('start', (event: D3ZoomEvent) => {
+ const { transform } = store.getState();
+
+ const px = (event.sourceEvent.clientX - bounds.left) * viewScaleRef.current;
+ const py = (event.sourceEvent.clientY - bounds.top) * viewScaleRef.current;
+
+ startTransform.current = {
+ x: px - transform[0] / transform[2],
+ y: py - transform[1] / transform[2],
+ };
+ })
+ .on('zoom.wheel', (event: D3ZoomEvent) => {
+ const { transform, d3Selection, d3Zoom } = store.getState();
+
+ if (event.sourceEvent.type !== 'wheel' || !d3Selection || !d3Zoom) {
+ return;
+ }
+
+ const pinchDelta =
+ -event.sourceEvent.deltaY *
+ (event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) *
+ 10;
+ const zoom = transform[2] * Math.pow(2, pinchDelta);
+ d3Zoom.scaleTo(d3Selection, zoom, [startTransform.current.x, startTransform.current.y]);
+ })
+ .on('zoom', (event: D3ZoomEvent) => {
+ if (event.sourceEvent.type !== 'mousemove') {
+ return;
+ }
+
+ const { transform, d3Selection, d3Zoom } = store.getState();
+ const px = (event.sourceEvent.clientX - bounds.left) * viewScaleRef.current;
+ const py = (event.sourceEvent.clientY - bounds.top) * viewScaleRef.current;
+
+ const position = {
+ x: (px - startTransform.current.x) * transform[2],
+ y: (py - startTransform.current.y) * transform[2],
+ };
+
+ const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]);
+ d3Zoom!.transform(d3Selection!, nextTransform);
+ });
+ selection.call(zoomHandler);
+ initialized.current = true;
+ }
+ }, [setCenter]);
return (