tisoap / react-flow-smart-edge

Custom Edge for React Flow that never intersects with other nodes
https://tisoap.github.io/react-flow-smart-edge/
MIT License
267 stars 37 forks source link
edge flow flowchart graph pathfinding react smart typescript

React Flow Smart Edge

Custom Edges for React Flow that never intersect with other nodes, using pathfinding.

CI Code Quality TypeScript Storybook Testing Library ESLint

Smart Edge

Install

With npm:

npm install @tisoap/react-flow-smart-edge

With yarn:

yarn add @tisoap/react-flow-smart-edge

This package is only compatible with version 11 or newer of React Flow Edge.

Support

Like this project and want to show your support? Buy me a coffee:

ko-fi

Really like this project? Sponsor me on GitHub:

GitHub Sponsors

Usage

This package ships with the following Smart Edges components:

Each one can be imported individually as a named export.

Example

import React from 'react'
import { ReactFlow } from 'reactflow'
import { SmartBezierEdge } from '@tisoap/react-flow-smart-edge'
import 'reactflow/dist/style.css'

const nodes = [
    {
        id: '1',
        data: { label: 'Node 1' },
        position: { x: 300, y: 100 }
    },
    {
        id: '2',
        data: { label: 'Node 2' },
        position: { x: 300, y: 200 }
    }
]

const edges = [
    {
        id: 'e21',
        source: '2',
        target: '1',
        type: 'smart'
    }
]

// You can give any name to your edge types
// https://reactflow.dev/docs/api/edges/custom-edges/
const edgeTypes = {
    smart: SmartBezierEdge
}

export const Graph = (props) => {
    const { children, ...rest } = props

    return (
        <ReactFlow
            defaultNodes={nodes}
            defaultEdges={edges}
            edgeTypes={edgeTypes}
            {...rest}
        >
            {children}
        </ReactFlow>
    )
}

Edge Options

All smart edges will take the exact same options as a React Flow Edge.

Custom Smart Edges

You can have more control over how the edge is rerendered by creating a custom edge and using the provided getSmartEdge function. It takes an object with the following keys:

Example

Just like you can use getBezierPath from reactflow to create a custom edge with a button, you can do the same with getSmartEdge:

import React from 'react'
import { useNodes, BezierEdge } from 'reactflow'
import { getSmartEdge } from '@tisoap/react-flow-smart-edge'

const foreignObjectSize = 200

export function SmartEdgeWithButtonLabel(props) {
    const {
        id,
        sourcePosition,
        targetPosition,
        sourceX,
        sourceY,
        targetX,
        targetY,
        style,
        markerStart,
        markerEnd
    } = props

    const nodes = useNodes()

    const getSmartEdgeResponse = getSmartEdge({
        sourcePosition,
        targetPosition,
        sourceX,
        sourceY,
        targetX,
        targetY,
        nodes
    })

    // If the value returned is null, it means "getSmartEdge" was unable to find
    // a valid path, and you should do something else instead
    if (getSmartEdgeResponse === null) {
        return <BezierEdge {...props} />
    }

    const { edgeCenterX, edgeCenterY, svgPathString } = getSmartEdgeResponse

    return (
        <>
            <path
                style={style}
                className='react-flow__edge-path'
                d={svgPathString}
                markerEnd={markerEnd}
                markerStart={markerStart}
            />
            <foreignObject
                width={foreignObjectSize}
                height={foreignObjectSize}
                x={edgeCenterX - foreignObjectSize / 2}
                y={edgeCenterY - foreignObjectSize / 2}
                requiredExtensions='http://www.w3.org/1999/xhtml'
            >
                <button
                    onClick={(event) => {
                        event.stopPropagation()
                        alert(`remove ${id}`)
                    }}
                >
                    X
                </button>
            </foreignObject>
        </>
    )
}

Advanced Custom Smart Edges

The getSmartEdge function also accepts an optional object options, which allows you to configure aspects of the path-finding algorithm. You may use it like so:

const myOptions = {
    // your configuration goes here
    nodePadding: 20,
    gridRatio: 15
}

// ...

const getSmartEdgeResponse = getSmartEdge({
    sourcePosition,
    targetPosition,
    sourceX,
    sourceY,
    targetX,
    targetY,
    nodes,
    // Pass down options in the getSmartEdge object
    options: myOptions
})

The options object accepts the following keys (they're all optional):

drawEdge

With the drawEdge option, you can change the function used to generate the final SVG path string, used to draw the line. By default it's the svgDrawSmoothLinePath function (same as used by the SmartBezierEdge), but the package also includes svgDrawStraightLinePath (same as used by the SmartStraightEdge and SmartStepEdge), or you can provide your own.

import {
    getSmartEdge,
    // Available built-in SVG draw functions
    svgDrawSmoothLinePath,
    svgDrawStraightLinePath
} from '@tisoap/react-flow-smart-edge'

// Using provided SVG draw functions:
const result = getSmartEdge({
    // ...
    options: {
        drawEdge: svgDrawSmoothLinePath
    }
})

// ...or using your own custom function
const result = getSmartEdge({
    // ...
    options: {
        drawEdge: (source, target, path) => {
            // your code goes here
            // ...
            return svgPath
        }
    }
})

The function you provided must comply with this signature:

type SVGDrawFunction = (
    source: XYPosition, // The starting {x, y} point
    target: XYPosition, // The ending  {x, y} point
    path: number[][] // The sequence of points [x, y] the line must follow
) => string // A string to be used in the "d" property of the SVG line

For inspiration on how to implement your own, you can check the drawSvgPath.ts source code.

generatePath

With the generatePath option, you can change the function used to do Pathfinding. By default, it's the pathfindingAStarDiagonal function (same as used by the SmartBezierEdge), but the package also includes pathfindingAStarNoDiagonal (used by SmartStraightEdge) and pathfindingJumpPointNoDiagonal (used by SmartStepEdge), or your can provide your own. The built-in functions use the pathfinding dependency behind the scenes.

import {
    getSmartEdge,
    // Available built-in pathfinding functions
    pathfindingAStarDiagonal,
    pathfindingAStarNoDiagonal,
    pathfindingJumpPointNoDiagonal
} from '@tisoap/react-flow-smart-edge'

// Using provided pathfinding functions:
const result = getSmartEdge({
    // ...
    options: {
        generatePath: pathfindingJumpPointNoDiagonal
    }
})

// ...or using your own custom function
const result = getSmartEdge({
    // ...
    options: {
        generatePath: (grid, start, end) => {
            // your code goes here
            // ...
            return { fullPath, smoothedPath }
        }
    }
})

The function you provide must comply with this signature:

type PathFindingFunction = (
    grid: Grid, // Grid representation of the graph
    start: XYPosition, // The starting {x, y} point
    end: XYPosition // The ending  {x, y} point
) => {
    fullPath: number[][] // Array of points [x, y] representing the full path with all points
    smoothedPath: number[][] // Array of points [x, y] representing a smaller, compressed path
} | null // The function should return null if it was unable to do pathfinding

For inspiration on how to implement your own, you can check the generatePath.ts source code and the pathfinding dependency documentation.

Advanced Examples

import {
    getSmartEdge,
    svgDrawSmoothLinePath,
    svgDrawStraightLinePath
    pathfindingAStarDiagonal,
    pathfindingAStarNoDiagonal,
    pathfindingJumpPointNoDiagonal
} from '@tisoap/react-flow-smart-edge'

// ...

// Same as importing "SmartBezierEdge" directly
const bezierResult = getSmartEdge({
    // ...
    options: {
        drawEdge: svgDrawSmoothLinePath,
        generatePath: pathfindingAStarDiagonal,
    }
})

// Same as importing "SmartStepEdge" directly
const stepResult = getSmartEdge({
    // ...
    options: {
        drawEdge: svgDrawStraightLinePath,
        generatePath: pathfindingJumpPointNoDiagonal,
    }
})

// Same as importing "SmartStraightEdge" directly
const straightResult = getSmartEdge({
    // ...
    options: {
        drawEdge: svgDrawStraightLinePath,
        generatePath: pathfindingAStarNoDiagonal,
    }
})

Storybook

You can see live Storybook examples by visiting this page, and see their source code here.

License

This project is MIT licensed.