diff --git a/README.md b/README.md
index 49df4880..df7d14bc 100644
--- a/README.md
+++ b/README.md
@@ -223,7 +223,7 @@ You can find an example of how to implement a custom node with multiple handles
# Edges
-React Flow comes with three [edge types](#edge-types--custom-edges) (`straight`, `default`, `step`). As the names indicate, the edges differ in the representation. The default type is a bezier edge. You create edges by adding them to your `elements` array of the `ReactFlow` component.
+React Flow comes with three [edge types](#edge-types--custom-edges) (`default`, `straight`, `step`, `smoothstep`). As the names indicate, the edges differ in the representation. The default type is a bezier edge. You create edges by adding them to your `elements` array of the `ReactFlow` component.
Edge example: `{ id: 'e1-2', type: 'straight', source: '1', target: '2', animated: true, label: 'edge label' }`
@@ -246,13 +246,14 @@ You can find an example with lots of different edges in the [edges example](http
### Edge Types & Custom Edges
-The basic edge types are `straight`, `default` and `step`. The default `edgeTypes` object looks like this:
+The basic edge types are `default` (bezier), `straight`, `step` and `smoothedge`. The default `edgeTypes` object looks like this:
```javascript
{
default: BezierEdge,
straight: StraightEdge,
- step: StepEdge
+ step: StepEdge,
+ smoothstep: SmoothStepEdge
}
```
diff --git a/example/src/Edges/index.js b/example/src/Edges/index.js
index d6414365..81f87136 100644
--- a/example/src/Edges/index.js
+++ b/example/src/Edges/index.js
@@ -4,13 +4,14 @@ import ReactFlow, { removeElements, addEdge, MiniMap, Controls, Background } fro
import CustomEdge from './CustomEdge';
-const onNodeDragStop = node => console.log('drag stop', node);
-const onElementClick = element => console.log('click', element);
-const onLoad = reactFlowInstance => reactFlowInstance.fitView();
+const onNodeDragStop = (node) => console.log('drag stop', node);
+const onElementClick = (element) => console.log('click', element);
+const onLoad = (reactFlowInstance) => reactFlowInstance.fitView();
const initialElements = [
{ id: '1', type: 'input', data: { label: 'Input 1' }, position: { x: 250, y: 0 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 150, y: 100 } },
+ { id: '2a', data: { label: 'Node 2a' }, position: { x: 0, y: 180 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 250, y: 200 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 300 } },
{ id: '3a', data: { label: 'Node 3a' }, position: { x: 150, y: 300 } },
@@ -19,20 +20,27 @@ const initialElements = [
{ id: '7', type: 'output', data: { label: 'Output 7' }, position: { x: 250, y: 550 } },
{ id: '8', type: 'output', data: { label: 'Output 8' }, position: { x: 525, y: 600 } },
{ id: 'e1-2', source: '1', target: '2', label: 'bezier edge (default)' },
+ { id: 'e2-2a', source: '2', target: '2a', type: 'smoothstep', label: 'smoothstep edge' },
{ id: 'e2-3', source: '2', target: '3', type: 'step', label: 'step edge' },
{ id: 'e3-4', source: '3', target: '4', type: 'straight', label: 'straight edge' },
{ id: 'e3-3a', source: '3', target: '3a', type: 'straight', label: 'label only edge', style: { stroke: 'none' } },
{ id: 'e3-5', source: '4', target: '5', animated: true, label: 'animated styled edge', style: { stroke: 'red' } },
{ id: 'e5-6', source: '5', target: '6', label: 'styled label', labelStyle: { fill: 'red', fontWeight: 700 } },
- { id: 'e5-7', source: '5', target: '7', label: 'label with styled bg', labelBgStyle: { fill: '#eee', fillOpacity: 0.7 } },
+ {
+ id: 'e5-7',
+ source: '5',
+ target: '7',
+ label: 'label with styled bg',
+ labelBgStyle: { fill: '#eee', fillOpacity: 0.7 },
+ },
{ id: 'e5-8', source: '5', target: '8', type: 'custom', label: 'custom edge' },
];
const EdgesFlow = () => {
const [elements, setElements] = useState(initialElements);
- const onElementsRemove = (elementsToRemove) => setElements(els => removeElements(elementsToRemove, els));
- const onConnect = (params) => setElements(els => addEdge(params, els));
+ const onElementsRemove = (elementsToRemove) => setElements((els) => removeElements(elementsToRemove, els));
+ const onConnect = (params) => setElements((els) => addEdge(params, els));
return (
{
onLoad={onLoad}
snapToGrid={true}
edgeTypes={{
- custom: CustomEdge
+ custom: CustomEdge,
}}
>
@@ -53,6 +61,6 @@ const EdgesFlow = () => {
);
-}
+};
-export default EdgesFlow;
\ No newline at end of file
+export default EdgesFlow;
diff --git a/example/src/Horizontal/index.js b/example/src/Horizontal/index.js
index e0968aa0..f2926d2c 100644
--- a/example/src/Horizontal/index.js
+++ b/example/src/Horizontal/index.js
@@ -2,10 +2,17 @@ import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge } from 'react-flow-renderer';
-const onLoad = reactFlowInstance => reactFlowInstance.fitView();
+const onLoad = (reactFlowInstance) => reactFlowInstance.fitView();
const initialElements = [
- { id: '1', sourcePosition: 'right', type: 'input', className: 'dark-node', data: { label: 'Input' }, position: { x: 0, y: 80 } },
+ {
+ id: '1',
+ sourcePosition: 'right',
+ type: 'input',
+ className: 'dark-node',
+ data: { label: 'Input' },
+ position: { x: 0, y: 80 },
+ },
{ id: '2', sourcePosition: 'right', targetPosition: 'left', data: { label: 'A Node' }, position: { x: 250, y: 0 } },
{ id: '3', sourcePosition: 'right', targetPosition: 'left', data: { label: 'Node 3' }, position: { x: 250, y: 160 } },
{ id: '4', sourcePosition: 'right', targetPosition: 'left', data: { label: 'Node 4' }, position: { x: 500, y: 0 } },
@@ -14,8 +21,8 @@ const initialElements = [
{ id: '7', sourcePosition: 'right', targetPosition: 'left', data: { label: 'Node 7' }, position: { x: 750, y: 50 } },
{ id: '8', sourcePosition: 'right', targetPosition: 'left', data: { label: 'Node 8' }, position: { x: 750, y: 300 } },
- { id: 'e1-2', source: '1', target: '2', animated: true, },
- { id: 'e1-3', source: '1', target: '3', animated: true, },
+ { id: 'e1-2', source: '1', target: '2', animated: true },
+ { id: 'e1-3', source: '1', target: '3', animated: true },
{ id: 'e1-4', source: '2', target: '4', label: 'edge label' },
{ id: 'e3-5', source: '3', target: '5', animated: true },
{ id: 'e3-6', source: '3', target: '6', animated: true },
@@ -25,18 +32,19 @@ const initialElements = [
const HorizontalFlow = () => {
const [elements, setElements] = useState(initialElements);
- const onElementsRemove = (elementsToRemove) =>
- setElements(els => removeElements(elementsToRemove, els));
- const onConnect = (params) => setElements(els => addEdge(params, els));
+ const onElementsRemove = (elementsToRemove) => setElements((els) => removeElements(elementsToRemove, els));
+ const onConnect = (params) => setElements((els) => addEdge(params, els));
const changeClassName = () => {
- setElements(elms => elms.map(el => {
- if (el.type === 'input') {
- el.className = el.className ? '' : 'dark-node';
- }
+ setElements((elms) =>
+ elms.map((el) => {
+ if (el.type === 'input') {
+ el.className = el.className ? '' : 'dark-node';
+ }
- return {...el};
- }))
- }
+ return { ...el };
+ })
+ );
+ };
return (
{
onLoad={onLoad}
selectNodesOnDrag={false}
>
-
);
-}
+};
export default HorizontalFlow;
diff --git a/example/src/Overview/index.js b/example/src/Overview/index.js
index c17f2e7d..6858a6eb 100644
--- a/example/src/Overview/index.js
+++ b/example/src/Overview/index.js
@@ -2,49 +2,118 @@ import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge, MiniMap, Controls, Background } from 'react-flow-renderer';
-const onNodeDragStart = node => console.log('drag start', node);
-const onNodeDragStop = node => console.log('drag stop', node);
-const onElementClick = element => console.log('click', element);
-const onSelectionChange = elements => console.log('selection change', elements);
+const onNodeDragStart = (node) => console.log('drag start', node);
+const onNodeDragStop = (node) => console.log('drag stop', node);
+const onElementClick = (element) => console.log('click', element);
+const onSelectionChange = (elements) => console.log('selection change', elements);
const onLoad = (reactFlowInstance) => {
console.log('graph loaded:', reactFlowInstance);
reactFlowInstance.fitView();
};
const initialElements = [
- { id: '1', type: 'input', data: { label: <>Welcome to React Flow!> }, position: { x: 250, y: 0 } },
- { id: '2', data: { label: <>This is a default node> }, position: { x: 100, y: 100 } },
{
- id: '3', data: { label: <>This one has a custom style> }, position: { x: 400, y: 100 },
+ id: '1',
+ type: 'input',
+ data: {
+ label: (
+ <>
+ Welcome to React Flow!
+ >
+ ),
+ },
+ position: { x: 250, y: 0 },
+ },
+ {
+ id: '2',
+ data: {
+ label: (
+ <>
+ This is a default node
+ >
+ ),
+ },
+ position: { x: 100, y: 100 },
+ },
+ {
+ id: '3',
+ data: {
+ label: (
+ <>
+ This one has a custom style
+ >
+ ),
+ },
+ position: { x: 400, y: 100 },
style: { background: '#eee', color: '#222', border: '1px solid #bbb', width: 180 },
},
{
- id: '4', position: { x: 250, y: 200 },
- data: { label: <>You can find the docs on Github> }
+ id: '4',
+ position: { x: 250, y: 200 },
+ data: {
+ label: (
+ <>
+ You can find the docs on{' '}
+
+ Github
+
+ >
+ ),
+ },
+ },
+ {
+ id: '5',
+ data: {
+ label: (
+ <>
+ Or check out the other examples
+ >
+ ),
+ },
+ position: { x: 250, y: 300 },
+ },
+ {
+ id: '6',
+ type: 'output',
+ data: {
+ label: (
+ <>
+ An output node
+ >
+ ),
+ },
+ position: { x: 100, y: 480 },
},
- { id: '5', data: { label: <>Or check out the other examples> }, position: { x: 250, y: 300 } },
- { id: '6', type: 'output', data: { label: <>An output node> }, position: { x: 100, y: 450 } },
{ id: '7', type: 'output', data: { label: 'Another output node' }, position: { x: 400, y: 450 } },
{ id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4', animated: true, label: 'animated edge' },
- { id: 'e4-5', source: '4', target: '5', },
- { id: 'e5-6', source: '5', target: '6', },
- { id: 'e5-7', source: '5', target: '7', type: 'step', label: 'a step edge', labelStyle: { fill: 'red', fontWeight: 700 } },
+ { id: 'e4-5', source: '4', target: '5' },
+ { id: 'e5-6', source: '5', target: '6', type: 'smoothstep', label: 'smooth step edge' },
+ {
+ id: 'e5-7',
+ source: '5',
+ target: '7',
+ type: 'step',
+ label: 'a step edge',
+ labelStyle: { fill: 'red', fontWeight: 700 },
+ },
];
const OverviewFlow = () => {
const [elements, setElements] = useState(initialElements);
const addRandomNode = () => {
- setElements(els => els.concat({
- id: (els.length + 1).toString(),
- data: { label: 'Added node' },
- position: { x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight }
- }));
+ setElements((els) =>
+ els.concat({
+ id: (els.length + 1).toString(),
+ data: { label: 'Added node' },
+ position: { x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight },
+ })
+ );
};
- const onElementsRemove = (elementsToRemove) => setElements(els => removeElements(elementsToRemove, els));
- const onConnect = (params) => setElements(els => addEdge(params, els));
+ const onElementsRemove = (elementsToRemove) => setElements((els) => removeElements(elementsToRemove, els));
+ const onConnect = (params) => setElements((els) => addEdge(params, els));
return (
{
snapGrid={[16, 16]}
>
{
+ nodeColor={(n) => {
if (n.type === 'input') return 'blue';
if (n.type === 'output') return 'green';
if (n.type === 'default') return 'red';
@@ -72,10 +141,7 @@ const OverviewFlow = () => {
}}
/>
-
+
{
);
-}
+};
-export default OverviewFlow;
\ No newline at end of file
+export default OverviewFlow;
diff --git a/src/components/Edges/SmoothStepEdge.tsx b/src/components/Edges/SmoothStepEdge.tsx
new file mode 100644
index 00000000..7a93a2d5
--- /dev/null
+++ b/src/components/Edges/SmoothStepEdge.tsx
@@ -0,0 +1,145 @@
+import React, { memo } from 'react';
+
+import EdgeText from './EdgeText';
+import { EdgeBezierProps, Position } from '../../types';
+
+// These are some helper methods for drawing the round corners
+// The name indicates the direction of the path. "bottomLeftCorner" goes
+// from bottom to the left and "leftBottomCorner" goes from left to the bottom.
+// We have to consider the direction of the paths because of the animated lines.
+
+const bottomLeftCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
+ `L ${cornerX},${cornerY - cornerSize}Q ${cornerX},${cornerY} ${cornerX + cornerSize},${cornerY}`;
+
+const leftBottomCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
+ `L ${cornerX + cornerSize},${cornerY}Q ${cornerX},${cornerY} ${cornerX},${cornerY - cornerSize}`;
+
+const bottomRightCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
+ `L ${cornerX},${cornerY - cornerSize}Q ${cornerX},${cornerY} ${cornerX - cornerSize},${cornerY}`;
+
+const rightBottomCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
+ `L ${cornerX - cornerSize},${cornerY}Q ${cornerX},${cornerY} ${cornerX},${cornerY - cornerSize}`;
+
+const leftTopCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
+ `L ${cornerX + cornerSize},${cornerY}Q ${cornerX},${cornerY} ${cornerX},${cornerY + cornerSize}`;
+
+const topLeftCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
+ `L ${cornerX},${cornerY + cornerSize}Q ${cornerX},${cornerY} ${cornerX + cornerSize},${cornerY}`;
+
+const topRightCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
+ `L ${cornerX},${cornerY + cornerSize}Q ${cornerX},${cornerY} ${cornerX - cornerSize},${cornerY}`;
+
+const rightTopCorner = (cornerX: number, cornerY: number, cornerSize: number): string =>
+ `L ${cornerX - cornerSize},${cornerY}Q ${cornerX},${cornerY} ${cornerX},${cornerY + cornerSize}`;
+
+export default memo(
+ ({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ label,
+ labelStyle,
+ labelShowBg,
+ labelBgStyle,
+ style,
+ sourcePosition = Position.Bottom,
+ targetPosition = Position.Top,
+ }: EdgeBezierProps) => {
+ const yOffset = Math.abs(targetY - sourceY) / 2;
+ const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
+
+ const xOffset = Math.abs(targetX - sourceX) / 2;
+ const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
+
+ const cornerWidth = Math.min(5, Math.abs(targetX - sourceX));
+ const cornerHeight = Math.min(5, Math.abs(targetY - sourceY));
+ const cornerSize = Math.min(cornerWidth, cornerHeight, xOffset, yOffset);
+
+ const leftAndRight = [Position.Left, Position.Right];
+
+ let path;
+ let firstCornerPath = null;
+ let secondCornerPath = null;
+
+ // default case: source and target positions are top or bottom
+ if (sourceX < targetX) {
+ firstCornerPath =
+ sourceY < targetY
+ ? bottomLeftCorner(sourceX, centerY, cornerSize)
+ : topLeftCorner(sourceX, centerY, cornerSize);
+ secondCornerPath =
+ sourceY < targetY
+ ? rightTopCorner(targetX, centerY, cornerSize)
+ : rightBottomCorner(targetX, centerY, cornerSize);
+ } else if (sourceX > targetX) {
+ firstCornerPath =
+ sourceY < targetY
+ ? bottomRightCorner(sourceX, centerY, cornerSize)
+ : topRightCorner(sourceX, centerY, cornerSize);
+ secondCornerPath =
+ sourceY < targetY
+ ? leftTopCorner(targetX, centerY, cornerSize)
+ : leftBottomCorner(targetX, centerY, cornerSize);
+ }
+
+ if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
+ if (sourceX < targetX) {
+ firstCornerPath =
+ sourceY < targetY
+ ? rightTopCorner(centerX, sourceY, cornerSize)
+ : rightBottomCorner(centerX, sourceY, cornerSize);
+ secondCornerPath =
+ sourceY < targetY
+ ? bottomLeftCorner(centerX, targetY, cornerSize)
+ : topLeftCorner(centerX, targetY, cornerSize);
+ }
+ } else if (leftAndRight.includes(sourcePosition) && !leftAndRight.includes(targetPosition)) {
+ if (sourceX < targetX) {
+ firstCornerPath =
+ sourceY < targetY
+ ? rightTopCorner(targetX, sourceY, cornerSize)
+ : rightBottomCorner(targetX, sourceY, cornerSize);
+ } else if (sourceX > targetX) {
+ firstCornerPath =
+ sourceY < targetY
+ ? bottomRightCorner(sourceX, targetY, cornerSize)
+ : topRightCorner(sourceX, targetY, cornerSize);
+ }
+ secondCornerPath = '';
+ } else if (!leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
+ if (sourceX < targetX) {
+ firstCornerPath =
+ sourceY < targetY
+ ? bottomLeftCorner(sourceX, targetY, cornerSize)
+ : topLeftCorner(sourceX, targetY, cornerSize);
+ } else if (sourceX > targetX) {
+ firstCornerPath =
+ sourceY < targetY
+ ? bottomRightCorner(sourceX, targetY, cornerSize)
+ : topRightCorner(sourceX, targetY, cornerSize);
+ }
+ secondCornerPath = '';
+ }
+
+ path = `M ${sourceX},${sourceY}${firstCornerPath}${secondCornerPath}L ${targetX},${targetY}`;
+
+ const text = label ? (
+
+ ) : null;
+
+ return (
+ <>
+
+ {text}
+ >
+ );
+ }
+);
diff --git a/src/container/EdgeRenderer/utils.ts b/src/container/EdgeRenderer/utils.ts
index 0f36d6f8..d3b85667 100644
--- a/src/container/EdgeRenderer/utils.ts
+++ b/src/container/EdgeRenderer/utils.ts
@@ -3,6 +3,7 @@ import { ComponentType } from 'react';
import StraightEdge from '../../components/Edges/StraightEdge';
import BezierEdge from '../../components/Edges/BezierEdge';
import StepEdge from '../../components/Edges/StepEdge';
+import SmoothStepEdge from '../../components/Edges/SmoothStepEdge';
import wrapEdge from '../../components/Edges/wrapEdge';
import { EdgeTypesType, EdgeCompProps } from '../../types';
@@ -12,6 +13,7 @@ export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType {
default: wrapEdge((edgeTypes.default || BezierEdge) as ComponentType),
straight: wrapEdge((edgeTypes.bezier || StraightEdge) as ComponentType),
step: wrapEdge((edgeTypes.step || StepEdge) as ComponentType),
+ smoothstep: wrapEdge((edgeTypes.step || SmoothStepEdge) as ComponentType),
};
const wrappedTypes = {} as EdgeTypesType;
diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx
index 82baff23..62ecc83e 100644
--- a/src/container/ReactFlow/index.tsx
+++ b/src/container/ReactFlow/index.tsx
@@ -17,6 +17,7 @@ import SelectionListener from '../../components/SelectionListener';
import BezierEdge from '../../components/Edges/BezierEdge';
import StraightEdge from '../../components/Edges/StraightEdge';
import StepEdge from '../../components/Edges/StepEdge';
+import SmoothStepEdge from '../../components/Edges/SmoothStepEdge';
import { createEdgeTypes } from '../EdgeRenderer/utils';
import Wrapper from './Wrapper';
import {
@@ -135,6 +136,7 @@ ReactFlow.defaultProps = {
default: BezierEdge,
straight: StraightEdge,
step: StepEdge,
+ smoothstep: SmoothStepEdge,
},
connectionLineType: ConnectionLineType.Bezier,
deleteKeyCode: 8,