diff --git a/README.md b/README.md
index 9dac10e3..f176c9a5 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,9 @@ const BasicFlow = () => ;
- `onlyRenderVisibleNodes`: default: `true`
- `isInteractive`: default: `true`. If the graph is not interactive you can't drag any nodes
- `selectNodesOnDrag`: default: `true`
+- `minZoom`: default: `0.5`
+- `maxZoom`: default: `2`
+- `defaultZoom`: default: `1`
## React Flow Instance
diff --git a/cypress/integration/flow/basic.spec.js b/cypress/integration/flow/basic.spec.js
index e160f369..afe43550 100644
--- a/cypress/integration/flow/basic.spec.js
+++ b/cypress/integration/flow/basic.spec.js
@@ -127,8 +127,8 @@ describe('Basic Graph Rendering', () => {
cy.window().then((win) => {
cy.get('.react-flow__zoompane')
- .trigger('mousedown', { which: 1, view: win })
- .trigger('mousemove', newPosition)
+ .trigger('mousedown', 'topLeft', { which: 1, view: win })
+ .trigger('mousemove', 'bottomLeft')
.trigger('mouseup', { force: true, view: win })
.then(() => {
const styleAfterDrag = Cypress.$('.react-flow__nodes').css('transform');
diff --git a/example/src/Basic/index.js b/example/src/Basic/index.js
index da5cfe53..2cb4f068 100644
--- a/example/src/Basic/index.js
+++ b/example/src/Basic/index.js
@@ -48,6 +48,9 @@ const BasicFlow = () => {
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
+ defaultZoom={1.5}
+ minZoom={0.2}
+ maxZoom={4}
>
diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx
index 1dca125c..36ab0396 100644
--- a/src/container/GraphView/index.tsx
+++ b/src/container/GraphView/index.tsx
@@ -43,6 +43,9 @@ export interface GraphViewProps {
onlyRenderVisibleNodes: boolean;
isInteractive: boolean;
selectNodesOnDrag: boolean;
+ minZoom: number;
+ maxZoom: number;
+ defaultZoom: number;
}
const GraphView = memo(
@@ -66,6 +69,9 @@ const GraphView = memo(
onlyRenderVisibleNodes,
isInteractive,
selectNodesOnDrag,
+ minZoom,
+ maxZoom,
+ defaultZoom,
}: GraphViewProps) => {
const zoomPane = useRef(null);
const rendererNode = useRef(null);
@@ -79,6 +85,9 @@ const GraphView = memo(
const setOnConnect = useStoreActions((a) => a.setOnConnect);
const setSnapGrid = useStoreActions((actions) => actions.setSnapGrid);
const setInteractive = useStoreActions((actions) => actions.setInteractive);
+ const updateTransform = useStoreActions((actions) => actions.updateTransform);
+ const setMinMaxZoom = useStoreActions((actions) => actions.setMinMaxZoom);
+
const selectionKeyPressed = useKeyPress(selectionKeyCode);
const rendererClasses = classnames('react-flow__renderer', { 'is-interactive': isInteractive });
@@ -106,6 +115,10 @@ const GraphView = memo(
setOnConnect(onConnect);
}
+ if (defaultZoom !== 1) {
+ updateTransform({ x: 0, y: 0, k: defaultZoom });
+ }
+
return () => {
window.onresize = null;
};
@@ -132,6 +145,10 @@ const GraphView = memo(
setInteractive(isInteractive);
}, [isInteractive]);
+ useEffect(() => {
+ setMinMaxZoom({ minZoom, maxZoom });
+ }, [minZoom, maxZoom]);
+
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode });
useElementUpdater(elements);
diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx
index 1339529e..de742138 100644
--- a/src/container/ReactFlow/index.tsx
+++ b/src/container/ReactFlow/index.tsx
@@ -54,6 +54,9 @@ export interface ReactFlowProps extends Omit, 'on
onlyRenderVisibleNodes: boolean;
isInteractive: boolean;
selectNodesOnDrag: boolean;
+ minZoom: number;
+ maxZoom: number;
+ defaultZoom: number;
}
const ReactFlow = ({
@@ -80,6 +83,9 @@ const ReactFlow = ({
onlyRenderVisibleNodes,
isInteractive,
selectNodesOnDrag,
+ minZoom,
+ maxZoom,
+ defaultZoom,
}: ReactFlowProps) => {
const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), []);
const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), []);
@@ -107,6 +113,9 @@ const ReactFlow = ({
onlyRenderVisibleNodes={onlyRenderVisibleNodes}
isInteractive={isInteractive}
selectNodesOnDrag={selectNodesOnDrag}
+ minZoom={minZoom}
+ maxZoom={maxZoom}
+ defaultZoom={defaultZoom}
/>
{onSelectionChange && }
{children}
@@ -136,6 +145,9 @@ ReactFlow.defaultProps = {
onlyRenderVisibleNodes: true,
isInteractive: true,
selectNodesOnDrag: true,
+ minZoom: 0.5,
+ maxZoom: 2,
+ defaultZoom: 1,
};
export default ReactFlow;
diff --git a/src/hooks/useD3Zoom.ts b/src/hooks/useD3Zoom.ts
index 0cd64333..95f3a0d9 100644
--- a/src/hooks/useD3Zoom.ts
+++ b/src/hooks/useD3Zoom.ts
@@ -10,10 +10,6 @@ interface UseD3ZoomParams {
onMove?: () => void;
}
-const d3ZoomInstance = zoom()
- .scaleExtent([0.5, 2])
- .filter(() => !event.button);
-
export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): void => {
const transform = useStoreState((s) => s.transform);
const d3Selection = useStoreState((s) => s.d3Selection);
@@ -24,16 +20,21 @@ export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): voi
useEffect(() => {
if (zoomPane.current) {
- const selection = select(zoomPane.current).call(d3ZoomInstance);
- initD3({ zoom: d3ZoomInstance, selection });
+ const nextD3ZoomInstance = zoom();
+ const selection = select(zoomPane.current).call(nextD3ZoomInstance);
+ initD3({ zoom: nextD3ZoomInstance, selection });
}
}, []);
useEffect(() => {
+ if (!d3Zoom) {
+ return;
+ }
+
if (selectionKeyPressed) {
- d3ZoomInstance.on('zoom', null);
+ d3Zoom.on('zoom', null);
} else {
- d3ZoomInstance.on('zoom', () => {
+ d3Zoom.on('zoom', () => {
if (event.sourceEvent && event.sourceEvent.target !== zoomPane.current) {
return;
}
@@ -53,7 +54,7 @@ export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): voi
}
return () => {
- d3ZoomInstance.on('zoom', null);
+ d3Zoom.on('zoom', null);
};
- }, [selectionKeyPressed]);
+ }, [selectionKeyPressed, d3Zoom]);
};
diff --git a/src/store/index.ts b/src/store/index.ts
index 9fec04bb..b0d00dc8 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -47,6 +47,11 @@ type D3Init = {
selection: D3Selection;
};
+type SetMinMaxZoom = {
+ minZoom: number;
+ maxZoom: number;
+};
+
type SetSnapGrid = {
snapToGrid: boolean;
snapGrid: [number, number];
@@ -64,6 +69,8 @@ export interface StoreModel {
d3Zoom: ZoomBehavior | null;
d3Selection: D3Selection | null;
d3Initialised: boolean;
+ minZoom: number;
+ maxZoom: number;
nodesSelectionActive: boolean;
selectionActive: boolean;
@@ -104,6 +111,8 @@ export interface StoreModel {
initD3: Action;
+ setMinMaxZoom: Action;
+
setSnapGrid: Action;
setConnectionPosition: Action;
@@ -129,6 +138,8 @@ const storeModel: StoreModel = {
d3Zoom: null,
d3Selection: null,
d3Initialised: false,
+ minZoom: 0.5,
+ maxZoom: 2,
nodesSelectionActive: false,
selectionActive: false,
@@ -326,10 +337,22 @@ const storeModel: StoreModel = {
initD3: action((state, { zoom, selection }) => {
state.d3Zoom = zoom;
+
+ state.d3Zoom.scaleExtent([state.minZoom, state.maxZoom]);
+
state.d3Selection = selection;
state.d3Initialised = true;
}),
+ setMinMaxZoom: action((state, { minZoom, maxZoom }) => {
+ state.minZoom = minZoom;
+ state.maxZoom = maxZoom;
+
+ if (state.d3Zoom) {
+ state.d3Zoom.scaleExtent([minZoom, maxZoom]);
+ }
+ }),
+
setConnectionPosition: action((state, position) => {
state.connectionPosition = position;
}),