├── example
├── src
│ ├── react-app-env.d.ts
│ ├── setupTests.ts
│ ├── Connector
│ │ ├── index.ts
│ │ ├── Arrow.tsx
│ │ ├── SConnector.tsx
│ │ ├── LineConnector.tsx
│ │ ├── SvgConnector.tsx
│ │ └── NarrowSConnector.tsx
│ ├── App.test.tsx
│ ├── index.css
│ ├── reportWebVitals.ts
│ ├── index.tsx
│ ├── App.css
│ ├── logo.svg
│ └── App.tsx
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── index.html
├── .gitignore
├── tsconfig.json
└── package.json
├── src
├── index.ts
├── Arrow.tsx
├── SConnector.tsx
├── LineConnector.tsx
├── SvgConnector.tsx
└── NarrowSConnector.tsx
├── .gitignore
├── tsconfig.json
├── LICENSE
├── package.json
└── README.md
/example/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/example/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/example/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tudatn/react-svg-connector/HEAD/example/public/favicon.ico
--------------------------------------------------------------------------------
/example/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tudatn/react-svg-connector/HEAD/example/public/logo192.png
--------------------------------------------------------------------------------
/example/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tudatn/react-svg-connector/HEAD/example/public/logo512.png
--------------------------------------------------------------------------------
/example/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import Connector from "./SvgConnector";
2 | import SConnector from "./SConnector";
3 | import LineConnector from "./LineConnector";
4 | import NarrowSConnector from "./NarrowSConnector";
5 |
6 | export { Connector as default, SConnector, LineConnector, NarrowSConnector };
7 |
--------------------------------------------------------------------------------
/example/src/Connector/index.ts:
--------------------------------------------------------------------------------
1 | import Connector from "./SvgConnector";
2 | import SConnector from "./SConnector";
3 | import LineConnector from "./LineConnector";
4 | import NarrowSConnector from "./NarrowSConnector";
5 |
6 | export { Connector as default, SConnector, LineConnector, NarrowSConnector };
7 |
--------------------------------------------------------------------------------
/example/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | render();
7 | const linkElement = screen.getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/example/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | /lib
26 |
27 | yarn.lock
--------------------------------------------------------------------------------
/example/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/example/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/example/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/example/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["es2015", "dom"],
5 | "outDir": "lib",
6 | "jsx": "react",
7 | "module": "commonjs",
8 | "declaration": true,
9 | "declarationMap": true,
10 | "strict": true,
11 | "noImplicitAny": false,
12 | "strictNullChecks": true,
13 | "strictFunctionTypes": true,
14 | "strictPropertyInitialization": true,
15 | "noImplicitThis": true,
16 | "alwaysStrict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noImplicitReturns": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "moduleResolution": "node",
22 | "resolveJsonModule": true,
23 | "esModuleInterop": true,
24 | "allowSyntheticDefaultImports": true
25 | },
26 | "include": ["src"],
27 | "exclude": [
28 | "node_modules",
29 | ],
30 | "files": ["./src/index.ts"]
31 | }
32 |
--------------------------------------------------------------------------------
/src/Arrow.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Point } from "./SvgConnector";
3 |
4 | export interface ArrowProps {
5 | tip: Point;
6 | size: number;
7 | stroke?: string;
8 | rotateAngle?: number;
9 | }
10 |
11 | /**
12 | * Return an arrow path for svg
13 | * @param tip arrow tip point
14 | * @param size arrow size
15 | * @param stroke arrow filled color
16 | * @param rotateAngle arrow rotation angle, default = 0
17 | */
18 |
19 | export default function Arrow(props: ArrowProps) {
20 | const path = `M
21 | ${props.tip.x} ${props.tip.y}
22 | l ${-props.size} ${-props.size / 2}
23 | v ${props.size}
24 | z
25 | `;
26 | return (
27 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/example/src/Connector/Arrow.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Point } from "./SvgConnector";
3 |
4 | export interface ArrowProps {
5 | tip: Point;
6 | size: number;
7 | stroke?: string;
8 | rotateAngle?: number;
9 | }
10 |
11 | /**
12 | * Return an arrow path for svg
13 | * @param tip arrow tip point
14 | * @param size arrow size
15 | * @param stroke arrow filled color
16 | * @param rotateAngle arrow rotation angle, default = 0
17 | */
18 |
19 | export default function Arrow(props: ArrowProps) {
20 | const path = `M
21 | ${props.tip.x} ${props.tip.y}
22 | l ${-props.size} ${-props.size / 2}
23 | v ${props.size}
24 | z
25 | `;
26 | return (
27 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Tu Dat Nguyen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-svg-connector",
3 | "version": "1.0.9",
4 | "description": "React component to draw svg connectors between any React components",
5 | "main": "lib/index.js",
6 | "directories": {
7 | "lib": "lib"
8 | },
9 | "files": [
10 | "lib"
11 | ],
12 | "dependencies": {
13 | "react": ">=16.8.0",
14 | "react-dom": ">=16.8.0"
15 | },
16 | "devDependencies": {
17 | "@types/node": "^12.0.0",
18 | "@types/react": "^17.0.0",
19 | "@types/react-dom": "^17.0.0",
20 | "typescript": "^4.1.2"
21 | },
22 | "scripts": {
23 | "clean": "rm -rf ./lib",
24 | "prebuild": "yarn clean",
25 | "build": "tsc --build \"./tsconfig.json\""
26 | },
27 | "repository": {
28 | "type": "git",
29 | "url": "git+https://github.com/tudatn/react-svg-connector.git"
30 | },
31 | "keywords": [
32 | "React",
33 | "svg",
34 | "connector",
35 | "connection",
36 | "diagram"
37 | ],
38 | "author": "Tu Dat Nguyen",
39 | "license": "MIT",
40 | "bugs": {
41 | "url": "https://github.com/tudatn/react-svg-connector/issues"
42 | },
43 | "homepage": "https://github.com/tudatn/react-svg-connector#readme"
44 | }
45 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-svg-connector",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "@types/jest": "^26.0.15",
10 | "@types/node": "^12.0.0",
11 | "@types/react": "^17.0.0",
12 | "@types/react-dom": "^17.0.0",
13 | "react": "^17.0.1",
14 | "react-dom": "^17.0.1",
15 | "react-draggable": "^4.4.3",
16 | "react-scripts": "4.0.2",
17 | "react-svg-connector": "^1.0.9",
18 | "styled-components": "^5.2.1",
19 | "typescript": "^4.1.2",
20 | "web-vitals": "^1.0.1"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest"
32 | ]
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | },
46 | "devDependencies": {
47 | "@types/styled-components": "^5.1.7"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/SConnector.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ShapeConnectorProps } from "./SvgConnector";
3 |
4 | interface SConectorProps extends ShapeConnectorProps {}
5 |
6 | /**
7 | * S shape svg connector
8 | * @param startPoint
9 | * @param endPoint
10 | * @param stroke
11 | * @param strokeWidth
12 | * @param startArrow
13 | * @param endArrow
14 | * @param arrowSize
15 | */
16 | export default function SConnector(props: SConectorProps) {
17 | const {
18 | direction,
19 | stroke,
20 | strokeWidth,
21 | startArrow,
22 | endArrow,
23 | startPoint,
24 | endPoint,
25 | arrowSize,
26 | ...rest
27 | } = props;
28 |
29 | const distanceX = props.endPoint.x - props.startPoint.x;
30 | const distanceY = props.endPoint.y - props.startPoint.y;
31 | const grids = 4;
32 | const stepX = distanceX / grids;
33 | const stepY = distanceY / grids;
34 |
35 | return (
36 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/example/src/Connector/SConnector.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ShapeConnectorProps } from "./SvgConnector";
3 |
4 | interface SConectorProps extends ShapeConnectorProps {}
5 |
6 | /**
7 | * S shape svg connector
8 | * @param startPoint
9 | * @param endPoint
10 | * @param stroke
11 | * @param strokeWidth
12 | * @param startArrow
13 | * @param endArrow
14 | * @param arrowSize
15 | */
16 | export default function SConnector(props: SConectorProps) {
17 | const {
18 | direction,
19 | stroke,
20 | strokeWidth,
21 | startArrow,
22 | endArrow,
23 | startPoint,
24 | endPoint,
25 | arrowSize,
26 | ...rest
27 | } = props;
28 |
29 | const distanceX = props.endPoint.x - props.startPoint.x;
30 | const distanceY = props.endPoint.y - props.startPoint.y;
31 | const grids = 4;
32 | const stepX = distanceX / grids;
33 | const stepY = distanceY / grids;
34 |
35 | return (
36 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/LineConnector.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Arrow from "./Arrow";
3 | import { ShapeConnectorProps } from "./SvgConnector";
4 |
5 | interface LineConnectorProps extends ShapeConnectorProps {}
6 |
7 | /**
8 | * Line svg connector
9 | * @param startPoint
10 | * @param endPoint
11 | * @param stroke
12 | * @param strokeWidth
13 | * @param startArrow
14 | * @param endArrow
15 | * @param arrowSize
16 | */
17 |
18 | export default function LineConnector(props: LineConnectorProps) {
19 | const {
20 | direction,
21 | stroke,
22 | strokeWidth,
23 | startArrow,
24 | endArrow,
25 | startPoint,
26 | endPoint,
27 | arrowSize,
28 | ...rest
29 | } = props;
30 |
31 | const deltaX = props.endPoint.x - props.startPoint.x;
32 | const deltaY = props.endPoint.y - props.startPoint.y;
33 |
34 | const alpha = Math.atan(deltaY / deltaX);
35 |
36 | let rotateAngle = (alpha * 180) / Math.PI;
37 |
38 | if (deltaX < 0) {
39 | rotateAngle = rotateAngle + 180;
40 | }
41 |
42 | const cArrowSize =
43 | props.arrowSize || (props.strokeWidth ? props.strokeWidth * 3 : 10);
44 |
45 | return (
46 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/example/src/Connector/LineConnector.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Arrow from "./Arrow";
3 | import { ShapeConnectorProps } from "./SvgConnector";
4 |
5 | interface LineConnectorProps extends ShapeConnectorProps {}
6 |
7 | /**
8 | * Line svg connector
9 | * @param startPoint
10 | * @param endPoint
11 | * @param stroke
12 | * @param strokeWidth
13 | * @param startArrow
14 | * @param endArrow
15 | * @param arrowSize
16 | */
17 |
18 | export default function LineConnector(props: LineConnectorProps) {
19 | const {
20 | direction,
21 | stroke,
22 | strokeWidth,
23 | startArrow,
24 | endArrow,
25 | startPoint,
26 | endPoint,
27 | arrowSize,
28 | ...rest
29 | } = props;
30 |
31 | const deltaX = props.endPoint.x - props.startPoint.x;
32 | const deltaY = props.endPoint.y - props.startPoint.y;
33 |
34 | const alpha = Math.atan(deltaY / deltaX);
35 |
36 | let rotateAngle = (alpha * 180) / Math.PI;
37 |
38 | if (deltaX < 0) {
39 | rotateAngle = rotateAngle + 180;
40 | }
41 |
42 | const cArrowSize =
43 | props.arrowSize || (props.strokeWidth ? props.strokeWidth * 3 : 10);
44 |
45 | return (
46 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/example/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-svg-connector
2 |
3 | React component to draw svg connectors to connect any React components
4 |
5 | ## Installation
6 | ```
7 | npm install react-svg-connector
8 |
9 | or
10 |
11 | yarn add react-svg-connector
12 | ```
13 |
14 | ## Usage
15 |
16 | Component props:
17 | - el1: first React component
18 | - el2: second React component
19 | - shape (`optional`): `'s' | 'narrow-s' | 'line'`
20 | - direction (`optional`): `'r2l' | 'l2l' | 'r2r' | 'l2r' |'b2t' | 'b2b' | 't2b' | 't2t'`
21 | - roundCorner (`optional`): `true | false`
22 | - grid (`optional`): number of grid, used to calculate `step = distanceX(Y) / grid`
23 | - minStep (`optional`): min value for the `step`
24 | - stem(`optional`): min distance from the start point
25 | - endArrow(`optional`): `true | false`
26 | - startArrow(`optional`): `true | false`
27 | - arrowSize(`optional`): arrow size
28 |
29 | All svg path props are available. Please run a full example to see all available props.
30 |
31 | `S shape`
32 |
33 |
34 |
35 |
36 | `narrow-s shape`
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | ```ts
46 | import Connector from 'react-svg-connector';
47 |
48 | // if you want to use core connector components
49 | import { SConnector, LineConnector, NarrowSConnector } from 'react-svg-connector';
50 |
51 | const Wrapper = styled.div`
52 | position: relative;
53 | height: 100vh;
54 | overflow: auto;
55 | `;
56 |
57 | const Box = styled.div`
58 | width: 150px;
59 | height: 50px;
60 | cursor: pointer;
61 | `;
62 |
63 | const Box1 = styled(Box)`
64 | background-color: green;
65 | position: absolute;
66 | top: 200px;
67 | left: 200px;
68 | `;
69 |
70 | const Box2 = styled(Box)`
71 | background-color: red;
72 | position: absolute;
73 | top: 400px;
74 | left: 500px;
75 | `;
76 |
77 | function App() {
78 | const ref1 = useRef();
79 | const ref2 = useRef();
80 |
81 | const [draw, redraw] = useState(0);
82 |
83 | useEffect(() => {
84 | redraw(Math.random());
85 | }, [ref1, ref2]);
86 |
87 | return (
88 |
89 |
97 |
98 |
99 |
100 | );
101 | }
--------------------------------------------------------------------------------
/src/SvgConnector.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from "react";
2 | import NarrowSConnector from "./NarrowSConnector";
3 | import LineConnector from "./LineConnector";
4 | import SConnector from "./SConnector";
5 |
6 | export type ShapeDirection =
7 | | "r2l"
8 | | "l2r"
9 | | "l2l"
10 | | "r2r"
11 | | "b2t"
12 | | "b2b"
13 | | "t2t"
14 | | "t2b";
15 |
16 | export interface Props extends React.SVGProps {
17 | el1: HTMLDivElement;
18 | el2: HTMLDivElement;
19 | shape: "s" | "line" | "narrow-s";
20 | direction?: ShapeDirection;
21 | grids?: number;
22 | stem?: number;
23 | roundCorner?: boolean;
24 | stroke?: string;
25 | strokeWidth?: number;
26 | minStep?: number;
27 | startArrow?: boolean;
28 | endArrow?: boolean;
29 | arrowSize?: number;
30 | }
31 |
32 | export interface Point {
33 | x: number;
34 | y: number;
35 | }
36 |
37 | export interface ShapeConnectorProps extends React.SVGProps {
38 | startPoint: Point;
39 | endPoint: Point;
40 | stroke?: string;
41 | strokeWidth?: number;
42 | startArrow?: boolean;
43 | endArrow?: boolean;
44 | arrowSize?: number;
45 | }
46 |
47 | /**
48 | * Connect elements with svg paths
49 | * @param el1 first element (HTML or React component)
50 | * @param el2 second element (HTML or React component)
51 | * @param shape s | line | narrow-s
52 | * @param direction (right, left, top, bottom) --> (right, left, top, bottom) if shape is narrow-s
53 | * @param grid number of columns in X/Y axis from the start point to the end point
54 | * @param stem min distance from the start point to the first transition
55 | * @param minStep radius of the transition curve, default is min of (deltaX/grid, deltaY/grid)
56 | * @param roundCorner true to have a curve transition
57 | * @param stroke color of the svg path
58 | * @param strokeWidth width of the svg path
59 | * @param startArrow true to have an arrow at the start point (not applicable for s shape)
60 | * @param endArrow true to have an arrow at the end point (not applicable for s shape)
61 | * @param arrowSize size of arrows
62 | */
63 |
64 | export default function SvgConnector(props: Props) {
65 | const wrapperRef = useRef(null);
66 |
67 | function getCoords(el: HTMLElement) {
68 | const parentEl = el.offsetParent;
69 | const box = el.getBoundingClientRect();
70 |
71 | return {
72 | top: box.top + window.pageYOffset + (parentEl?.scrollTop || 0),
73 | right: box.right + window.pageXOffset + (parentEl?.scrollLeft || 0),
74 | bottom: box.bottom + window.pageYOffset + (parentEl?.scrollTop || 0),
75 | left: box.left + window.pageXOffset + (parentEl?.scrollLeft || 0),
76 | };
77 | }
78 |
79 | function getNewCoordinates() {
80 | const el1Coords = getCoords(props.el1);
81 | const el2Coords = getCoords(props.el2);
82 |
83 | const el1Dimesion = {
84 | width: el1Coords.right - el1Coords.left,
85 | height: el1Coords.bottom - el1Coords.top,
86 | };
87 |
88 | const el2Dimesion = {
89 | width: el2Coords.right - el2Coords.left,
90 | height: el2Coords.bottom - el2Coords.top,
91 | };
92 |
93 | let start = {
94 | x: el1Coords.right,
95 | y: el1Coords.top + el1Dimesion.height / 2,
96 | };
97 |
98 | let end = {
99 | x: el2Coords.left,
100 | y: el2Coords.top + el2Dimesion.height / 2,
101 | };
102 |
103 | switch (props.direction) {
104 | case "l2l":
105 | start.x = el1Coords.left;
106 | break;
107 | case "l2r":
108 | start.x = el1Coords.left;
109 | end.x = el2Coords.right;
110 | break;
111 | case "r2r":
112 | start.x = el1Coords.right;
113 | end.x = el2Coords.right;
114 | break;
115 | case "b2t":
116 | start = {
117 | x: el1Coords.left + el1Dimesion.width / 2,
118 |
119 | y: el1Coords.bottom,
120 | };
121 | end = {
122 | x: el2Coords.left + el2Dimesion.width / 2,
123 | y: el2Coords.top,
124 | };
125 | break;
126 | case "b2b":
127 | start = {
128 | x: el1Coords.left + el1Dimesion.width / 2,
129 | y: el1Coords.bottom,
130 | };
131 | end = {
132 | x: el2Coords.left + el2Dimesion.width / 2,
133 | y: el2Coords.bottom,
134 | };
135 | break;
136 | case "t2t":
137 | start = {
138 | x: el1Coords.left + el1Dimesion.width / 2,
139 | y: el1Coords.top,
140 | };
141 | end = {
142 | x: el2Coords.left + el2Dimesion.width / 2,
143 | y: el2Coords.top,
144 | };
145 | break;
146 | case "t2b":
147 | start = {
148 | x: el1Coords.left + el1Dimesion.width / 2,
149 | y: el1Coords.top,
150 | };
151 | end = {
152 | x: el2Coords.left + el2Dimesion.width / 2,
153 | y: el2Coords.bottom,
154 | };
155 | break;
156 | default:
157 | break;
158 | }
159 |
160 | return { start, end };
161 | }
162 |
163 | if (!props.el1 || !props.el2) return null;
164 |
165 | const coordinates = getNewCoordinates();
166 |
167 | return (
168 |
178 | {props.shape === "line" && (
179 |
187 | )}
188 | {props.shape === "s" && (
189 |
197 | )}
198 | {props.shape === "narrow-s" && (
199 |
212 | )}
213 |
214 | );
215 | }
216 |
--------------------------------------------------------------------------------
/example/src/Connector/SvgConnector.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from "react";
2 | import NarrowSConnector from "./NarrowSConnector";
3 | import LineConnector from "./LineConnector";
4 | import SConnector from "./SConnector";
5 |
6 | export type ShapeDirection =
7 | | "r2l"
8 | | "l2r"
9 | | "l2l"
10 | | "r2r"
11 | | "b2t"
12 | | "b2b"
13 | | "t2t"
14 | | "t2b";
15 |
16 | export interface Props extends React.SVGProps {
17 | el1: HTMLDivElement;
18 | el2: HTMLDivElement;
19 | shape: "s" | "line" | "narrow-s";
20 | direction?: ShapeDirection;
21 | grids?: number;
22 | stem?: number;
23 | roundCorner?: boolean;
24 | stroke?: string;
25 | strokeWidth?: number;
26 | minStep?: number;
27 | startArrow?: boolean;
28 | endArrow?: boolean;
29 | arrowSize?: number;
30 | }
31 |
32 | export interface Point {
33 | x: number;
34 | y: number;
35 | }
36 |
37 | export interface ShapeConnectorProps extends React.SVGProps {
38 | startPoint: Point;
39 | endPoint: Point;
40 | stroke?: string;
41 | strokeWidth?: number;
42 | startArrow?: boolean;
43 | endArrow?: boolean;
44 | arrowSize?: number;
45 | }
46 |
47 | /**
48 | * Connect elements with svg paths
49 | * @param el1 first element (HTML or React component)
50 | * @param el2 second element (HTML or React component)
51 | * @param shape s | line | narrow-s
52 | * @param direction (right, left, top, bottom) --> (right, left, top, bottom) if shape is narrow-s
53 | * @param grid number of columns in X/Y axis from the start point to the end point
54 | * @param stem min distance from the start point to the first transition
55 | * @param minStep radius of the transition curve, default is min of (deltaX/grid, deltaY/grid)
56 | * @param roundCorner true to have a curve transition
57 | * @param stroke color of the svg path
58 | * @param strokeWidth width of the svg path
59 | * @param startArrow true to have an arrow at the start point (not applicable for s shape)
60 | * @param endArrow true to have an arrow at the end point (not applicable for s shape)
61 | * @param arrowSize size of arrows
62 | */
63 |
64 | export default function SvgConnector(props: Props) {
65 | const wrapperRef = useRef(null);
66 |
67 | function getCoords(el: HTMLElement) {
68 | const parentEl = el.offsetParent;
69 | const box = el.getBoundingClientRect();
70 |
71 | return {
72 | top: box.top + window.pageYOffset + (parentEl?.scrollTop || 0),
73 | right: box.right + window.pageXOffset + (parentEl?.scrollLeft || 0),
74 | bottom: box.bottom + window.pageYOffset + (parentEl?.scrollTop || 0),
75 | left: box.left + window.pageXOffset + (parentEl?.scrollLeft || 0),
76 | };
77 | }
78 |
79 | function getNewCoordinates() {
80 | const el1Coords = getCoords(props.el1);
81 | const el2Coords = getCoords(props.el2);
82 |
83 | const el1Dimesion = {
84 | width: el1Coords.right - el1Coords.left,
85 | height: el1Coords.bottom - el1Coords.top,
86 | };
87 |
88 | const el2Dimesion = {
89 | width: el2Coords.right - el2Coords.left,
90 | height: el2Coords.bottom - el2Coords.top,
91 | };
92 |
93 | let start = {
94 | x: el1Coords.right,
95 | y: el1Coords.top + el1Dimesion.height / 2,
96 | };
97 |
98 | let end = {
99 | x: el2Coords.left,
100 | y: el2Coords.top + el2Dimesion.height / 2,
101 | };
102 |
103 | switch (props.direction) {
104 | case "l2l":
105 | start.x = el1Coords.left;
106 | break;
107 | case "l2r":
108 | start.x = el1Coords.left;
109 | end.x = el2Coords.right;
110 | break;
111 | case "r2r":
112 | start.x = el1Coords.right;
113 | end.x = el2Coords.right;
114 | break;
115 | case "b2t":
116 | start = {
117 | x: el1Coords.left + el1Dimesion.width / 2,
118 |
119 | y: el1Coords.bottom,
120 | };
121 | end = {
122 | x: el2Coords.left + el2Dimesion.width / 2,
123 | y: el2Coords.top,
124 | };
125 | break;
126 | case "b2b":
127 | start = {
128 | x: el1Coords.left + el1Dimesion.width / 2,
129 | y: el1Coords.bottom,
130 | };
131 | end = {
132 | x: el2Coords.left + el2Dimesion.width / 2,
133 | y: el2Coords.bottom,
134 | };
135 | break;
136 | case "t2t":
137 | start = {
138 | x: el1Coords.left + el1Dimesion.width / 2,
139 | y: el1Coords.top,
140 | };
141 | end = {
142 | x: el2Coords.left + el2Dimesion.width / 2,
143 | y: el2Coords.top,
144 | };
145 | break;
146 | case "t2b":
147 | start = {
148 | x: el1Coords.left + el1Dimesion.width / 2,
149 | y: el1Coords.top,
150 | };
151 | end = {
152 | x: el2Coords.left + el2Dimesion.width / 2,
153 | y: el2Coords.bottom,
154 | };
155 | break;
156 | default:
157 | break;
158 | }
159 |
160 | return { start, end };
161 | }
162 |
163 | if (!props.el1 || !props.el2) return null;
164 |
165 | const coordinates = getNewCoordinates();
166 |
167 | return (
168 |
178 | {props.shape === "line" && (
179 |
187 | )}
188 | {props.shape === "s" && (
189 |
197 | )}
198 | {props.shape === "narrow-s" && (
199 |
212 | )}
213 |
214 | );
215 | }
216 |
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from "react";
2 | import Draggable from "react-draggable";
3 | import styled from "styled-components/macro";
4 | import Connector from "react-svg-connector";
5 |
6 | const Wrapper = styled.div`
7 | position: relative;
8 | height: 100vh;
9 | overflow: auto;
10 | `;
11 |
12 | const Box = styled.div<{
13 | top?: string;
14 | left?: string;
15 | right?: string;
16 | bottom?: string;
17 | }>`
18 | width: 100px;
19 | height: 50px;
20 | cursor: pointer;
21 | position: absolute;
22 | top: ${(props) => props.top || 0};
23 | bottom: ${(props) => props.bottom || 0};
24 | left: ${(props) => props.left || 0};
25 | right: ${(props) => props.right || 0};
26 | `;
27 |
28 | const Box1 = styled(Box)`
29 | background-color: green;
30 | `;
31 |
32 | const Box2 = styled(Box)`
33 | background-color: red;
34 | `;
35 |
36 | function App() {
37 | const [activeDrags, setActiveDrags] = useState(0);
38 | const [redraw, setRedraw] = useState(0);
39 |
40 | const refs = useRef<{ [key: string]: any }>({});
41 |
42 | function addRef(key: string) {
43 | if (refs.current[key]) {
44 | return refs.current[key];
45 | } else {
46 | const newRef = React.createRef();
47 | refs.current[key] = newRef;
48 | return newRef;
49 | }
50 | }
51 |
52 | function onStart() {
53 | const newActiveDrags = activeDrags + 1;
54 | setActiveDrags(newActiveDrags);
55 | }
56 |
57 | function onStop() {
58 | const newActiveDrags = activeDrags - 1;
59 | setActiveDrags(newActiveDrags);
60 | }
61 |
62 | function onDrag(e: any, data: any) {
63 | setRedraw(Math.random());
64 | }
65 |
66 | useEffect(() => {
67 | setRedraw(Math.random());
68 | }, []);
69 |
70 | return (
71 |
72 |
85 |
95 |
105 |
117 |
118 | {/* YSPACE */}
119 |
129 |
139 |
149 |
160 |
167 |
174 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 | {/* Y SPACE */}
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 | {/* Line */}
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 | {/* S shape */}
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 | );
256 | }
257 |
258 | export default App;
259 |
--------------------------------------------------------------------------------
/src/NarrowSConnector.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Arrow from "./Arrow";
3 |
4 | import { ShapeConnectorProps, ShapeDirection } from "./SvgConnector";
5 |
6 | interface NarrowSConnectorProps extends ShapeConnectorProps {
7 | grids?: number;
8 | stem?: number;
9 | roundCorner?: boolean;
10 | direction?: ShapeDirection;
11 | minStep?: number;
12 | arrowSize?: number;
13 | endArrow?: boolean;
14 | startArrow?: boolean;
15 | }
16 |
17 | /**
18 | * Custom S shape svg connector
19 | * @param startPoint
20 | * @param endPoint
21 | * @param grids number of columns in X/Y axis
22 | * @param stem min distance from the start point to the first transition
23 | * @param roundCorner true to have a curve transition
24 | * @param direction (right, left, top, bottom) --> (right, left, top, bottom)
25 | * @param minStep radius of the transition curve, default is min of (deltaX/grid, deltaY/grid)
26 | * @param arrowSize
27 | * @param endArrow
28 | * @param startArrow
29 | */
30 |
31 | export default function NarrowSConnector(props: NarrowSConnectorProps) {
32 | const {
33 | direction,
34 | stroke,
35 | strokeWidth,
36 | startArrow,
37 | endArrow,
38 | startPoint,
39 | endPoint,
40 | arrowSize,
41 | roundCorner,
42 | minStep,
43 | ...rest
44 | } = props;
45 |
46 | let coordinates = {
47 | start: props.startPoint,
48 | end: props.endPoint,
49 | };
50 |
51 | if (props.direction === "l2r" || props.direction === "t2b") {
52 | // swap elements
53 | coordinates = {
54 | start: props.endPoint,
55 | end: props.startPoint,
56 | };
57 | }
58 |
59 | const distanceX = coordinates.end.x - coordinates.start.x;
60 | const distanceY = coordinates.end.y - coordinates.start.y;
61 |
62 | let stem = props.stem || 0;
63 | const grids = props.grids || 5;
64 |
65 | const radius = props.roundCorner ? 1 : 0;
66 |
67 | const stepX = distanceX / grids;
68 | const stepY = distanceY / grids;
69 |
70 | if (stem >= Math.abs(distanceX)) {
71 | stem = Math.abs(distanceX) - Math.abs(stepX);
72 | }
73 |
74 | let step = Math.min(Math.abs(stepX), Math.abs(stepY));
75 |
76 | step = Math.min(step, props.minStep || step);
77 |
78 | const cArrowSize =
79 | props.arrowSize || (props.strokeWidth ? props.strokeWidth * 3 : 10);
80 |
81 | function corner12(direction?: ShapeDirection) {
82 | const factor = distanceX * distanceY >= 0 ? 1 : -1;
83 | const l2lFactor = props.direction === "l2l" ? -1 : 1;
84 |
85 | const pathr2l = `M
86 | ${coordinates.start.x} ${coordinates.start.y}
87 | h ${stem * l2lFactor}
88 | q ${step * radius * l2lFactor} 0
89 | ${step * radius * l2lFactor} ${step * factor * radius}
90 | V ${coordinates.end.y - step * factor * radius}
91 | q ${0} ${step * factor * radius}
92 | ${step * radius} ${step * factor * radius}
93 | H ${coordinates.end.x}
94 | `;
95 |
96 | const pathr2r = `M
97 | ${coordinates.start.x} ${coordinates.start.y}
98 | h ${stem + distanceX + step}
99 | q ${step * radius} 0
100 | ${step * radius} ${step * factor * radius}
101 | V ${coordinates.end.y - step * factor * radius}
102 | q 0 ${step * factor * radius}
103 | ${-step} ${step * factor * radius}
104 | H ${coordinates.end.x}
105 | `;
106 |
107 | let path = pathr2l; // default
108 |
109 | switch (direction) {
110 | case "r2r":
111 | path = pathr2r;
112 | break;
113 | case "b2t":
114 | path = pathr2l;
115 | break;
116 | default:
117 | break;
118 | }
119 | return (
120 |
145 | );
146 | }
147 |
148 | function corner21(direction?: ShapeDirection) {
149 | const factor = distanceX * distanceY >= 0 ? 1 : -1;
150 | const t2tFactor = props.direction === "t2t" ? -1 : 1;
151 |
152 | const pathb2t = `M
153 | ${coordinates.start.x} ${coordinates.start.y}
154 | v ${stem * t2tFactor}
155 | q 0 ${step * radius * t2tFactor}
156 | ${step * factor * radius} ${step * radius * t2tFactor}
157 | H ${coordinates.end.x - step * factor * radius}
158 | q ${step * factor * radius} 0
159 | ${step * factor * radius} ${step * radius}
160 | V ${coordinates.end.y}
161 | `;
162 |
163 | const pathb2b = `M
164 | ${coordinates.start.x} ${coordinates.start.y}
165 | v ${stem + distanceY + step}
166 | q 0 ${step * radius}
167 | ${step * factor * radius} ${step * radius}
168 | H ${coordinates.end.x - step * factor * radius}
169 | q ${step * factor * radius} 0
170 | ${step * factor * radius} ${-step}
171 | V ${coordinates.end.y}
172 | `;
173 |
174 | let path = pathb2t; // default
175 |
176 | switch (direction) {
177 | case "b2b":
178 | path = pathb2b;
179 | break;
180 | default:
181 | break;
182 | }
183 | return (
184 |
209 | );
210 | }
211 |
212 | function corner34(direction?: ShapeDirection) {
213 | const factor = distanceX * distanceY > 0 ? 1 : -1;
214 |
215 | let pathr2l = `M
216 | ${coordinates.start.x} ${coordinates.start.y}
217 | h ${stem}
218 | q ${step * radius} 0
219 | ${step * radius} ${-step * factor * radius}
220 | v ${distanceY / 2 + step * 2 * factor * radius}
221 | q 0 ${-step * factor * radius}
222 | ${-step * radius} ${-step * factor * radius}
223 | h ${distanceX - stem * 2}
224 | q ${-step * radius} 0
225 | ${-step * radius} ${-step * factor * radius}
226 | V ${coordinates.end.y + step * factor * radius}
227 | q 0 ${-step * factor * radius}
228 | ${step * radius} ${-step * factor * radius}
229 | H ${coordinates.end.x}
230 | `;
231 |
232 | let pathl2l = `M
233 | ${coordinates.start.x} ${coordinates.start.y}
234 | h ${stem * -1 + distanceX}
235 | q ${step * -1 * radius} 0
236 | ${step * -1 * radius} ${-step * factor * radius}
237 | V ${coordinates.end.y + step * factor * radius}
238 | q 0 ${-step * factor * radius}
239 | ${step * radius} ${-step * factor * radius}
240 | H ${coordinates.end.x}
241 | `;
242 |
243 | let pathr2r = `M
244 | ${coordinates.start.x} ${coordinates.start.y}
245 | h ${stem}
246 | q ${step * radius} 0
247 | ${step * radius} ${step * factor * -1 * radius}
248 | V ${coordinates.end.y + step * factor * radius}
249 | q ${0} ${step * factor * -1 * radius}
250 | ${step * -1 * radius} ${step * factor * -1 * radius}
251 | H ${coordinates.end.x}
252 | `;
253 |
254 | let path = pathr2l; // default
255 |
256 | switch (direction) {
257 | case "l2l":
258 | path = pathl2l;
259 | break;
260 | case "r2r":
261 | path = pathr2r;
262 | break;
263 | default:
264 | break;
265 | }
266 | return (
267 |
292 | );
293 | }
294 |
295 | function corner43(direction?: ShapeDirection) {
296 | const factor = distanceX * distanceY > 0 ? 1 : -1;
297 |
298 | let pathb2t = `M
299 | ${coordinates.start.x} ${coordinates.start.y}
300 | v ${stem}
301 | q 0 ${step * radius}
302 | ${-step * factor * radius} ${step * radius}
303 | h ${distanceX / 2 + step * 2 * factor * radius}
304 | q ${-step * factor * radius} 0
305 | ${-step * factor * radius} ${-step * radius}
306 | v ${distanceY - stem * 2}
307 | q 0 ${-step * radius}
308 | ${-step * factor * radius} ${-step * radius}
309 | H ${coordinates.end.x + step * factor * radius}
310 | q ${-step * factor * radius} 0
311 | ${-step * factor * radius} ${step * radius}
312 | V ${coordinates.end.y}
313 | `;
314 |
315 | let patht2t = `M
316 | ${coordinates.start.x} ${coordinates.start.y}
317 | v ${stem * -1 + distanceY}
318 | q 0 ${step * -1 * radius}
319 | ${-step * factor * radius} ${step * -1 * radius}
320 | H ${coordinates.end.x + step * factor * radius}
321 | q ${-step * factor * radius} 0
322 | ${-step * factor * radius} ${step * radius}
323 | V ${coordinates.end.y}
324 | `;
325 |
326 | let pathb2b = `M
327 | ${coordinates.start.x} ${coordinates.start.y}
328 | v ${stem}
329 | q 0 ${step * radius}
330 | ${step * factor * -1 * radius} ${step * radius}
331 | H ${coordinates.end.x + step * factor * radius}
332 | q ${step * factor * -1 * radius} 0
333 | ${step * factor * -1 * radius} ${step * -1 * radius}
334 | V ${coordinates.end.y}
335 | `;
336 |
337 | let path = pathb2t; // default
338 |
339 | switch (direction) {
340 | case "b2b":
341 | path = pathb2b;
342 | break;
343 | case "t2t":
344 | path = patht2t;
345 | break;
346 | default:
347 | break;
348 | }
349 | return (
350 |
375 | );
376 | }
377 |
378 | const ySpaceDirections = ["b2t", "b2b", "t2t", "t2b"];
379 | if (ySpaceDirections.includes(props.direction || "")) {
380 | if (distanceY >= 0) {
381 | return corner21(props.direction);
382 | } else {
383 | return corner43(props.direction);
384 | }
385 | }
386 |
387 | // corner 1 & 2
388 | if (distanceX >= 0) {
389 | return corner12(props.direction);
390 | }
391 |
392 | // corner 4 & 3
393 | else {
394 | return corner34(props.direction);
395 | }
396 | }
397 |
--------------------------------------------------------------------------------
/example/src/Connector/NarrowSConnector.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Arrow from "./Arrow";
3 |
4 | import { ShapeConnectorProps, ShapeDirection } from "./SvgConnector";
5 |
6 | interface NarrowSConnectorProps extends ShapeConnectorProps {
7 | grids?: number;
8 | stem?: number;
9 | roundCorner?: boolean;
10 | direction?: ShapeDirection;
11 | minStep?: number;
12 | arrowSize?: number;
13 | endArrow?: boolean;
14 | startArrow?: boolean;
15 | }
16 |
17 | /**
18 | * Custom S shape svg connector
19 | * @param startPoint
20 | * @param endPoint
21 | * @param grids number of columns in X/Y axis
22 | * @param stem min distance from the start point to the first transition
23 | * @param roundCorner true to have a curve transition
24 | * @param direction (right, left, top, bottom) --> (right, left, top, bottom)
25 | * @param minStep radius of the transition curve, default is min of (deltaX/grid, deltaY/grid)
26 | * @param arrowSize
27 | * @param endArrow
28 | * @param startArrow
29 | */
30 |
31 | export default function NarrowSConnector(props: NarrowSConnectorProps) {
32 | const {
33 | direction,
34 | stroke,
35 | strokeWidth,
36 | startArrow,
37 | endArrow,
38 | startPoint,
39 | endPoint,
40 | arrowSize,
41 | roundCorner,
42 | minStep,
43 | ...rest
44 | } = props;
45 |
46 | let coordinates = {
47 | start: props.startPoint,
48 | end: props.endPoint,
49 | };
50 |
51 | if (props.direction === "l2r" || props.direction === "t2b") {
52 | // swap elements
53 | coordinates = {
54 | start: props.endPoint,
55 | end: props.startPoint,
56 | };
57 | }
58 |
59 | const distanceX = coordinates.end.x - coordinates.start.x;
60 | const distanceY = coordinates.end.y - coordinates.start.y;
61 |
62 | let stem = props.stem || 0;
63 | const grids = props.grids || 5;
64 |
65 | const radius = props.roundCorner ? 1 : 0;
66 |
67 | const stepX = distanceX / grids;
68 | const stepY = distanceY / grids;
69 |
70 | if (stem >= Math.abs(distanceX)) {
71 | stem = Math.abs(distanceX) - Math.abs(stepX);
72 | }
73 |
74 | let step = Math.min(Math.abs(stepX), Math.abs(stepY));
75 |
76 | step = Math.min(step, props.minStep || step);
77 |
78 | const cArrowSize =
79 | props.arrowSize || (props.strokeWidth ? props.strokeWidth * 3 : 10);
80 |
81 | function corner12(direction?: ShapeDirection) {
82 | const factor = distanceX * distanceY >= 0 ? 1 : -1;
83 | const l2lFactor = props.direction === "l2l" ? -1 : 1;
84 |
85 | const pathr2l = `M
86 | ${coordinates.start.x} ${coordinates.start.y}
87 | h ${stem * l2lFactor}
88 | q ${step * radius * l2lFactor} 0
89 | ${step * radius * l2lFactor} ${step * factor * radius}
90 | V ${coordinates.end.y - step * factor * radius}
91 | q ${0} ${step * factor * radius}
92 | ${step * radius} ${step * factor * radius}
93 | H ${coordinates.end.x}
94 | `;
95 |
96 | const pathr2r = `M
97 | ${coordinates.start.x} ${coordinates.start.y}
98 | h ${stem + distanceX + step}
99 | q ${step * radius} 0
100 | ${step * radius} ${step * factor * radius}
101 | V ${coordinates.end.y - step * factor * radius}
102 | q 0 ${step * factor * radius}
103 | ${-step} ${step * factor * radius}
104 | H ${coordinates.end.x}
105 | `;
106 |
107 | let path = pathr2l; // default
108 |
109 | switch (direction) {
110 | case "r2r":
111 | path = pathr2r;
112 | break;
113 | case "b2t":
114 | path = pathr2l;
115 | break;
116 | default:
117 | break;
118 | }
119 | return (
120 |
145 | );
146 | }
147 |
148 | function corner21(direction?: ShapeDirection) {
149 | const factor = distanceX * distanceY >= 0 ? 1 : -1;
150 | const t2tFactor = props.direction === "t2t" ? -1 : 1;
151 |
152 | const pathb2t = `M
153 | ${coordinates.start.x} ${coordinates.start.y}
154 | v ${stem * t2tFactor}
155 | q 0 ${step * radius * t2tFactor}
156 | ${step * factor * radius} ${step * radius * t2tFactor}
157 | H ${coordinates.end.x - step * factor * radius}
158 | q ${step * factor * radius} 0
159 | ${step * factor * radius} ${step * radius}
160 | V ${coordinates.end.y}
161 | `;
162 |
163 | const pathb2b = `M
164 | ${coordinates.start.x} ${coordinates.start.y}
165 | v ${stem + distanceY + step}
166 | q 0 ${step * radius}
167 | ${step * factor * radius} ${step * radius}
168 | H ${coordinates.end.x - step * factor * radius}
169 | q ${step * factor * radius} 0
170 | ${step * factor * radius} ${-step}
171 | V ${coordinates.end.y}
172 | `;
173 |
174 | let path = pathb2t; // default
175 |
176 | switch (direction) {
177 | case "b2b":
178 | path = pathb2b;
179 | break;
180 | default:
181 | break;
182 | }
183 | return (
184 |
209 | );
210 | }
211 |
212 | function corner34(direction?: ShapeDirection) {
213 | const factor = distanceX * distanceY > 0 ? 1 : -1;
214 |
215 | let pathr2l = `M
216 | ${coordinates.start.x} ${coordinates.start.y}
217 | h ${stem}
218 | q ${step * radius} 0
219 | ${step * radius} ${-step * factor * radius}
220 | v ${distanceY / 2 + step * 2 * factor * radius}
221 | q 0 ${-step * factor * radius}
222 | ${-step * radius} ${-step * factor * radius}
223 | h ${distanceX - stem * 2}
224 | q ${-step * radius} 0
225 | ${-step * radius} ${-step * factor * radius}
226 | V ${coordinates.end.y + step * factor * radius}
227 | q 0 ${-step * factor * radius}
228 | ${step * radius} ${-step * factor * radius}
229 | H ${coordinates.end.x}
230 | `;
231 |
232 | let pathl2l = `M
233 | ${coordinates.start.x} ${coordinates.start.y}
234 | h ${stem * -1 + distanceX}
235 | q ${step * -1 * radius} 0
236 | ${step * -1 * radius} ${-step * factor * radius}
237 | V ${coordinates.end.y + step * factor * radius}
238 | q 0 ${-step * factor * radius}
239 | ${step * radius} ${-step * factor * radius}
240 | H ${coordinates.end.x}
241 | `;
242 |
243 | let pathr2r = `M
244 | ${coordinates.start.x} ${coordinates.start.y}
245 | h ${stem}
246 | q ${step * radius} 0
247 | ${step * radius} ${step * factor * -1 * radius}
248 | V ${coordinates.end.y + step * factor * radius}
249 | q ${0} ${step * factor * -1 * radius}
250 | ${step * -1 * radius} ${step * factor * -1 * radius}
251 | H ${coordinates.end.x}
252 | `;
253 |
254 | let path = pathr2l; // default
255 |
256 | switch (direction) {
257 | case "l2l":
258 | path = pathl2l;
259 | break;
260 | case "r2r":
261 | path = pathr2r;
262 | break;
263 | default:
264 | break;
265 | }
266 | return (
267 |
292 | );
293 | }
294 |
295 | function corner43(direction?: ShapeDirection) {
296 | const factor = distanceX * distanceY > 0 ? 1 : -1;
297 |
298 | let pathb2t = `M
299 | ${coordinates.start.x} ${coordinates.start.y}
300 | v ${stem}
301 | q 0 ${step * radius}
302 | ${-step * factor * radius} ${step * radius}
303 | h ${distanceX / 2 + step * 2 * factor * radius}
304 | q ${-step * factor * radius} 0
305 | ${-step * factor * radius} ${-step * radius}
306 | v ${distanceY - stem * 2}
307 | q 0 ${-step * radius}
308 | ${-step * factor * radius} ${-step * radius}
309 | H ${coordinates.end.x + step * factor * radius}
310 | q ${-step * factor * radius} 0
311 | ${-step * factor * radius} ${step * radius}
312 | V ${coordinates.end.y}
313 | `;
314 |
315 | let patht2t = `M
316 | ${coordinates.start.x} ${coordinates.start.y}
317 | v ${stem * -1 + distanceY}
318 | q 0 ${step * -1 * radius}
319 | ${-step * factor * radius} ${step * -1 * radius}
320 | H ${coordinates.end.x + step * factor * radius}
321 | q ${-step * factor * radius} 0
322 | ${-step * factor * radius} ${step * radius}
323 | V ${coordinates.end.y}
324 | `;
325 |
326 | let pathb2b = `M
327 | ${coordinates.start.x} ${coordinates.start.y}
328 | v ${stem}
329 | q 0 ${step * radius}
330 | ${step * factor * -1 * radius} ${step * radius}
331 | H ${coordinates.end.x + step * factor * radius}
332 | q ${step * factor * -1 * radius} 0
333 | ${step * factor * -1 * radius} ${step * -1 * radius}
334 | V ${coordinates.end.y}
335 | `;
336 |
337 | let path = pathb2t; // default
338 |
339 | switch (direction) {
340 | case "b2b":
341 | path = pathb2b;
342 | break;
343 | case "t2t":
344 | path = patht2t;
345 | break;
346 | default:
347 | break;
348 | }
349 | return (
350 |
375 | );
376 | }
377 |
378 | const ySpaceDirections = ["b2t", "b2b", "t2t", "t2b"];
379 | if (ySpaceDirections.includes(props.direction || "")) {
380 | if (distanceY >= 0) {
381 | return corner21(props.direction);
382 | } else {
383 | return corner43(props.direction);
384 | }
385 | }
386 |
387 | // corner 1 & 2
388 | if (distanceX >= 0) {
389 | return corner12(props.direction);
390 | }
391 |
392 | // corner 4 & 3
393 | else {
394 | return corner34(props.direction);
395 | }
396 | }
397 |
--------------------------------------------------------------------------------