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,