diff --git a/README.md b/README.md
index a1d06b9c..9fee03ef 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,7 @@ const BasicFlow = () => ;
- `minZoom`: default: `0.5`
- `maxZoom`: default: `2`
- `defaultZoom`: default: `1`
+- `defaultPosition`: default: `[0, 0]`
- `snapToGrid`: default: `false`
- `snapGrid`: [x, y] array - default: `[16, 16]`
- `onlyRenderVisibleNodes`: default: `true`
@@ -88,7 +89,9 @@ const BasicFlow = () => ;
- `onNodeContextMenu(evt: MouseEvent, node: Node)`: node context menu
- `onConnect({ source, target })`: called when user connects two nodes
- `onLoad(reactFlowInstance)`: called after flow is initialized
-- `onMove()`: called when user pans or zooms
+- `onMove()`: called when user is panning or zooming
+- `onMoveStart()`: called when user starts panning or zooming
+- `onMoveEnd()`: called when user ends panning or zooming
- `onSelectionChange(elements: Elements)`: called when user selects one or multiple elements
#### Interaction
diff --git a/example/src/Overview/index.js b/example/src/Overview/index.js
index 6d834393..d2247c38 100644
--- a/example/src/Overview/index.js
+++ b/example/src/Overview/index.js
@@ -11,6 +11,8 @@ const onLoad = (reactFlowInstance) => {
reactFlowInstance.fitView();
};
+const onMoveEnd = (transform) => console.log('zoom/move end', transform);
+
const initialElements = [
{
id: '1',
@@ -116,6 +118,7 @@ const OverviewFlow = () => {
onNodeDragStart={onNodeDragStart}
onNodeDragStop={onNodeDragStop}
onSelectionChange={onSelectionChange}
+ onMoveEnd={onMoveEnd}
style={{ width: '100%', height: '100%' }}
onLoad={onLoad}
connectionLineStyle={{ stroke: '#ddd' }}
diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx
index f76d83c7..080d3921 100644
--- a/src/container/GraphView/index.tsx
+++ b/src/container/GraphView/index.tsx
@@ -21,6 +21,7 @@ import {
Edge,
Connection,
ConnectionLineType,
+ FlowTransform,
} from '../../types';
export interface GraphViewProps {
@@ -35,7 +36,9 @@ export interface GraphViewProps {
onNodeDragStop?: (node: Node) => void;
onConnect?: (connection: Connection | Edge) => void;
onLoad?: OnLoadFunc;
- onMove?: () => void;
+ onMove?: (flowTransform?: FlowTransform) => void;
+ onMoveStart?: (flowTransform?: FlowTransform) => void;
+ onMoveEnd?: (flowTransform?: FlowTransform) => void;
selectionKeyCode: number;
nodeTypes: NodeTypesType;
edgeTypes: EdgeTypesType;
@@ -52,6 +55,7 @@ export interface GraphViewProps {
minZoom: number;
maxZoom: number;
defaultZoom: number;
+ defaultPosition: [number, number];
arrowHeadColor: string;
markerEndId?: string;
zoomOnScroll: boolean;
@@ -63,6 +67,8 @@ const GraphView = memo(
nodeTypes,
edgeTypes,
onMove,
+ onMoveStart,
+ onMoveEnd,
onLoad,
onElementClick,
onNodeMouseEnter,
@@ -88,6 +94,7 @@ const GraphView = memo(
minZoom,
maxZoom,
defaultZoom,
+ defaultPosition,
arrowHeadColor,
markerEndId,
zoomOnScroll,
@@ -106,7 +113,7 @@ const GraphView = memo(
const setNodesDraggable = useStoreActions((actions) => actions.setNodesDraggable);
const setNodesConnectable = useStoreActions((actions) => actions.setNodesConnectable);
const setElementsSelectable = useStoreActions((actions) => actions.setElementsSelectable);
- const updateTransform = useStoreActions((actions) => actions.updateTransform);
+ const setInitTransform = useStoreActions((actions) => actions.setInitTransform);
const setMinMaxZoom = useStoreActions((actions) => actions.setMinMaxZoom);
const fitView = useStoreActions((actions) => actions.fitView);
const zoom = useStoreActions((actions) => actions.zoom);
@@ -139,10 +146,6 @@ const GraphView = memo(
setOnConnect(onConnect);
}
- if (defaultZoom !== 1) {
- updateTransform({ x: 0, y: 0, k: defaultZoom });
- }
-
if (rendererNode.current) {
resizeObserver = new ResizeObserver((entries) => {
for (let _ of entries) {
@@ -162,7 +165,15 @@ const GraphView = memo(
};
}, []);
- useD3Zoom({ zoomPane, onMove, selectionKeyPressed, zoomOnScroll, zoomOnDoubleClick });
+ useD3Zoom({
+ zoomPane,
+ onMove,
+ onMoveStart,
+ onMoveEnd,
+ selectionKeyPressed,
+ zoomOnScroll,
+ zoomOnDoubleClick,
+ });
useEffect(() => {
if (d3Initialised && onLoad) {
@@ -174,6 +185,18 @@ const GraphView = memo(
getElements,
});
}
+
+ if (d3Initialised) {
+ const initialTransform = {
+ x: defaultPosition[0],
+ y: defaultPosition[1],
+ k: defaultZoom,
+ };
+
+ if (initialTransform.x !== 0 || initialTransform.y !== 0 || initialTransform.k !== 1) {
+ setInitTransform(initialTransform);
+ }
+ }
}, [d3Initialised, onLoad]);
useEffect(() => {
diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx
index ea4c2a01..c69b4a69 100644
--- a/src/container/ReactFlow/index.tsx
+++ b/src/container/ReactFlow/index.tsx
@@ -26,6 +26,7 @@ import {
Edge,
Connection,
ConnectionLineType,
+ FlowTransform,
} from '../../types';
import '../../style.css';
@@ -42,7 +43,9 @@ export interface ReactFlowProps extends Omit, 'on
onNodeDragStop?: (node: Node) => void;
onConnect?: (connection: Edge | Connection) => void;
onLoad?: OnLoadFunc;
- onMove?: () => void;
+ onMove?: (flowTransform?: FlowTransform) => void;
+ onMoveStart?: (flowTransform?: FlowTransform) => void;
+ onMoveEnd?: (flowTransform?: FlowTransform) => void;
onSelectionChange?: (elements: Elements | null) => void;
nodeTypes: NodeTypesType;
edgeTypes: EdgeTypesType;
@@ -60,6 +63,7 @@ export interface ReactFlowProps extends Omit, 'on
minZoom: number;
maxZoom: number;
defaultZoom: number;
+ defaultPosition: [number, number];
arrowHeadColor: string;
markerEndId?: string;
zoomOnScroll: boolean;
@@ -76,6 +80,8 @@ const ReactFlow = ({
edgeTypes,
onLoad,
onMove,
+ onMoveStart,
+ onMoveEnd,
onElementsRemove,
onConnect,
onNodeMouseEnter,
@@ -99,6 +105,7 @@ const ReactFlow = ({
minZoom,
maxZoom,
defaultZoom,
+ defaultPosition,
arrowHeadColor,
markerEndId,
zoomOnScroll,
@@ -114,6 +121,8 @@ const ReactFlow = ({
;
selectionKeyPressed: boolean;
zoomOnScroll: boolean;
zoomOnDoubleClick: boolean;
- onMove?: () => void;
+ onMove?: (flowTransform?: FlowTransform) => void;
+ onMoveStart?: (flowTransform?: FlowTransform) => void;
+ onMoveEnd?: (flowTransform?: FlowTransform) => void;
}
-export default ({ zoomPane, onMove, zoomOnScroll, zoomOnDoubleClick, selectionKeyPressed }: UseD3ZoomParams): void => {
+const viewChanged = (prevTransform: FlowTransform, eventTransform: any): boolean =>
+ prevTransform.x !== eventTransform.x ||
+ prevTransform.y !== eventTransform.y ||
+ prevTransform.zoom !== eventTransform.k;
+
+const eventToFlowTransform = (eventTransform: any): FlowTransform => ({
+ x: eventTransform.x,
+ y: eventTransform.y,
+ zoom: eventTransform.k,
+});
+
+export default ({
+ zoomPane,
+ onMove,
+ onMoveStart,
+ onMoveEnd,
+ zoomOnScroll,
+ zoomOnDoubleClick,
+ selectionKeyPressed,
+}: UseD3ZoomParams): void => {
+ const prevTransform = useRef({ x: 0, y: 0, zoom: 0 });
const d3Zoom = useStoreState((s) => s.d3Zoom);
const initD3 = useStoreActions((actions) => actions.initD3);
@@ -32,12 +55,47 @@ export default ({ zoomPane, onMove, zoomOnScroll, zoomOnDoubleClick, selectionKe
updateTransform(event.transform);
if (onMove) {
- onMove();
+ const flowTransform = eventToFlowTransform(event.transform);
+ onMove(flowTransform);
}
});
}
}
- }, [selectionKeyPressed, d3Zoom]);
+ }, [selectionKeyPressed, d3Zoom, updateTransform, onMove]);
+
+ useEffect(() => {
+ if (d3Zoom) {
+ if (onMoveStart) {
+ d3Zoom.on('start', () => {
+ if (viewChanged(prevTransform.current, event.transform)) {
+ const flowTransform = eventToFlowTransform(event.transform);
+ prevTransform.current = flowTransform;
+
+ onMoveStart(flowTransform);
+ }
+ });
+ } else {
+ d3Zoom.on('start', null);
+ }
+ }
+ }, [d3Zoom, onMoveStart]);
+
+ useEffect(() => {
+ if (d3Zoom) {
+ if (onMoveEnd) {
+ d3Zoom.on('end', () => {
+ if (viewChanged(prevTransform.current, event.transform)) {
+ const flowTransform = eventToFlowTransform(event.transform);
+ prevTransform.current = flowTransform;
+
+ onMoveEnd(flowTransform);
+ }
+ });
+ } else {
+ d3Zoom.on('end', null);
+ }
+ }
+ }, [d3Zoom, onMoveEnd]);
useEffect(() => {
if (d3Zoom) {
diff --git a/src/store/index.ts b/src/store/index.ts
index 0f42280f..7729f9cd 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -105,6 +105,8 @@ export interface StoreModel {
updateTransform: Action;
+ setInitTransform: Action;
+
updateSize: Action;
initD3: Action;
@@ -342,6 +344,18 @@ export const storeModel: StoreModel = {
state.transform[2] = transform.k;
}),
+ setInitTransform: action((state, transform) => {
+ state.transform[0] = transform.x;
+ state.transform[1] = transform.y;
+ state.transform[2] = transform.k;
+
+ if (state.d3Selection) {
+ const updatedTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.k);
+ // we need to sync the d3 zoom transform with the updated transform
+ state.d3Selection.property('__zoom', updatedTransform);
+ }
+ }),
+
updateSize: action((state, size) => {
state.width = size.width;
state.height = size.height;
diff --git a/src/types/index.ts b/src/types/index.ts
index ecf91b8c..a362abaf 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -241,3 +241,9 @@ export type NodePosUpdate = {
id: ElementId;
pos: XYPosition;
};
+
+export type FlowTransform = {
+ x: number;
+ y: number;
+ zoom: number;
+};