├── .babelrc
├── .browserslistrc
├── .editorconfig
├── .eslintrc.json
├── .github
└── workflows
│ ├── build.yml
│ ├── coverage.yml
│ ├── storybook.yml
│ └── test.yml
├── .gitignore
├── .npmignore
├── .storybook
├── .eslintrc.json
├── images
│ ├── disc.png
│ └── favicon.ico
├── main.js
├── manager-head.html
├── manager.js
├── preview.js
├── stories
│ ├── animatedTree.stories.js
│ ├── argTypes.js
│ ├── intro.stories.mdx
│ ├── labels.stories.js
│ ├── nodes.stories.js
│ └── tree.stories.js
└── styles
│ ├── nodeProps.css
│ ├── polygon.css
│ └── styles.css
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __tests__
├── .eslintrc.json
├── Components
│ ├── __snapshots__
│ │ ├── animatedTests.js.snap
│ │ ├── animatedTreeTests.js.snap
│ │ ├── containerTests.js.snap
│ │ ├── linkTests.js.snap
│ │ ├── nodeTests.js.snap
│ │ └── treeTests.js.snap
│ ├── animatedTests.js
│ ├── animatedTreeTests.js
│ ├── containerTests.js
│ ├── linkTests.js
│ ├── nodeTests.js
│ └── treeTests.js
├── d3Tests.js
├── startup.js
└── wrapHandlersTests.js
├── dist
├── index.js
├── index.min.js
├── module
│ ├── components
│ │ ├── animated.js
│ │ ├── animatedTree.js
│ │ ├── container.js
│ │ ├── link.js
│ │ ├── node.js
│ │ └── tree.js
│ ├── d3.js
│ ├── index.js
│ └── wrapHandlers.js
├── style.css
└── style.min.css
├── docs
├── 294.6a3eb3eb.iframe.bundle.js
├── 3.b80f77cd.iframe.bundle.js
├── 388.bebd3fb0.iframe.bundle.js
├── 388.bebd3fb0.iframe.bundle.js.LICENSE.txt
├── 388.bebd3fb0.iframe.bundle.js.map
├── 421.a56bbb85.iframe.bundle.js
├── 647.43ba6e7d.iframe.bundle.js
├── 71.59fa7449.iframe.bundle.js
├── 71.59fa7449.iframe.bundle.js.map
├── 857.905d42e1.iframe.bundle.js
├── 955.c734015d.iframe.bundle.js
├── animatedTree-stories.d0248ab8.iframe.bundle.js
├── disc.png
├── favicon.ico
├── favicon.svg
├── iframe.html
├── index.html
├── index.json
├── intro-stories-mdx.d24d5b6c.iframe.bundle.js
├── labels-stories.ebb5e805.iframe.bundle.js
├── main.056e03a3.iframe.bundle.js
├── nodes-stories.a047e23c.iframe.bundle.js
├── project.json
├── runtime~main.fb760981.iframe.bundle.js
├── sb-addons
│ ├── essentials-controls-0
│ │ ├── manager-bundle.js
│ │ └── manager-bundle.js.LEGAL.txt
│ ├── essentials-toolbars-1
│ │ ├── manager-bundle.js
│ │ └── manager-bundle.js.LEGAL.txt
│ └── storybook-2
│ │ ├── manager-bundle.js
│ │ └── manager-bundle.js.LEGAL.txt
├── sb-common-assets
│ ├── fonts.css
│ ├── nunito-sans-bold-italic.woff2
│ ├── nunito-sans-bold.woff2
│ ├── nunito-sans-italic.woff2
│ └── nunito-sans-regular.woff2
├── sb-manager
│ ├── WithTooltip-V3YHNWJZ-MXTFSDU5.js
│ ├── chunk-5QAFKPS7.js
│ ├── chunk-7PRFHFSS.js
│ ├── chunk-XE6LDGTE.js
│ ├── chunk-YDUB7CS6.js
│ ├── chunk-ZEU7PDD3.js
│ ├── formatter-SWP5E3XI-7BGIK6BL.js
│ ├── globals-module-info.js
│ ├── globals.js
│ ├── index.js
│ ├── runtime.js
│ └── syntaxhighlighter-MJWPISIS-JOSCT6CQ.js
├── sb-preview
│ ├── globals.js
│ └── runtime.js
├── stories.json
└── tree-stories.a7afaf37.iframe.bundle.js
├── package-lock.json
├── package.json
├── rollup.config.mjs
├── src
├── components
│ ├── animated.js
│ ├── animatedTree.js
│ ├── container.js
│ ├── link.js
│ ├── node.js
│ └── tree.js
├── d3.js
├── index.js
└── wrapHandlers.js
└── styles
└── style.css
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/env",
4 | "@babel/react"
5 | ]
6 | }
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | last 2 Chrome version
2 | last 2 Edge version
3 | last 2 Firefox version
4 | last 2 Safari version
5 | maintained node versions
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | insert_final_newline = false
6 | trim_trailing_whitespace = true
7 |
8 | [{package.json,package-lock.json,.github/workflows/*.yml}]
9 | indent_style = space
10 | indent_size = 2
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true
4 | },
5 | "extends": [
6 | "eslint:recommended",
7 | "plugin:react/recommended"
8 | ],
9 | "parser": "@babel/eslint-parser",
10 | "parserOptions": {
11 | "ecmaVersion": 2021,
12 | "ecmaFeatures": {
13 | "jsx": true
14 | },
15 | "sourceType": "module"
16 | },
17 | "plugins": [
18 | "react"
19 | ],
20 | "rules": {
21 | "array-bracket-newline": [
22 | "error",
23 | "consistent"
24 | ],
25 | "array-bracket-spacing": [
26 | "error",
27 | "never"
28 | ],
29 | "array-element-newline": [
30 | "error",
31 | "consistent"
32 | ],
33 | "arrow-body-style": [
34 | "error",
35 | "as-needed"
36 | ],
37 | "arrow-parens": [
38 | "error",
39 | "as-needed"
40 | ],
41 | "arrow-spacing": "error",
42 | "block-spacing": "error",
43 | "brace-style": [
44 | "error",
45 | "1tbs",
46 | {
47 | "allowSingleLine": true
48 | }
49 | ],
50 | "camelcase": "error",
51 | "comma-dangle": [
52 | "error",
53 | "never"
54 | ],
55 | "comma-spacing": [
56 | "error",
57 | {
58 | "before": false,
59 | "after": true
60 | }
61 | ],
62 | "comma-style": [
63 | "error",
64 | "last"
65 | ],
66 | "computed-property-spacing": [
67 | "error",
68 | "never"
69 | ],
70 | "curly": "error",
71 | "dot-location": [
72 | "error",
73 | "property"
74 | ],
75 | "eol-last": [
76 | "error",
77 | "never"
78 | ],
79 | "func-call-spacing": [
80 | "error",
81 | "never"
82 | ],
83 | "func-names": [
84 | "error",
85 | "always"
86 | ],
87 | "func-style": [
88 | "error",
89 | "declaration"
90 | ],
91 | "function-paren-newline": [
92 | "error",
93 | "consistent"
94 | ],
95 | "indent": [
96 | "error",
97 | "tab",
98 | {
99 | "SwitchCase": 1
100 | }
101 | ],
102 | "jsx-quotes": [
103 | "error",
104 | "prefer-double"
105 | ],
106 | "key-spacing": [
107 | "error",
108 | {
109 | "mode": "strict"
110 | }
111 | ],
112 | "keyword-spacing": "error",
113 | "lines-between-class-members": [
114 | "error",
115 | "always"
116 | ],
117 | "no-array-constructor": "error",
118 | "no-bitwise": "error",
119 | "no-duplicate-imports": "error",
120 | "no-lonely-if": "error",
121 | "no-multi-assign": "error",
122 | "no-multiple-empty-lines": "error",
123 | "no-multi-spaces": [
124 | "error",
125 | {
126 | "exceptions": {
127 | "Property": false
128 | }
129 | }
130 | ],
131 | "no-trailing-spaces": "error",
132 | "no-unneeded-ternary": [
133 | "error",
134 | {
135 | "defaultAssignment": false
136 | }
137 | ],
138 | "no-useless-computed-key": "error",
139 | "no-useless-constructor": "error",
140 | "no-useless-rename": "error",
141 | "no-var": "error",
142 | "no-whitespace-before-property": "error",
143 | "object-curly-newline": [
144 | "error",
145 | {
146 | "consistent": true
147 | }
148 | ],
149 | "object-curly-spacing": [
150 | "error",
151 | "always"
152 | ],
153 | "operator-linebreak": [
154 | "error",
155 | "before"
156 | ],
157 | "padded-blocks": [
158 | "error",
159 | "never"
160 | ],
161 | "prefer-arrow-callback": "error",
162 | "prefer-rest-params": "error",
163 | "prefer-spread": "error",
164 | "prefer-template": "error",
165 | "quotes": [
166 | "error",
167 | "single"
168 | ],
169 | "react/jsx-boolean-value": "error",
170 | "react/jsx-closing-bracket-location": [
171 | "error",
172 | "after-props"
173 | ],
174 | "react/jsx-closing-tag-location": "error",
175 | "react/jsx-curly-spacing": "error",
176 | "react/jsx-equals-spacing": "error",
177 | "react/jsx-first-prop-new-line": [
178 | "error",
179 | "multiline"
180 | ],
181 | "react/jsx-handler-names": "error",
182 | "react/jsx-indent": [
183 | "error",
184 | "tab"
185 | ],
186 | "react/jsx-indent-props": [
187 | "error",
188 | "tab"
189 | ],
190 | "react/jsx-no-bind": "error",
191 | "react/jsx-curly-brace-presence": "error",
192 | "react/jsx-pascal-case": "error",
193 | "react/jsx-props-no-multi-spaces": "error",
194 | "react/jsx-tag-spacing": [
195 | "error",
196 | {
197 | "beforeSelfClosing": "never",
198 | "beforeClosing": "never"
199 | }
200 | ],
201 | "react/no-access-state-in-setstate": "error",
202 | "react/no-typos": "error",
203 | "react/no-unused-state": "error",
204 | "react/prefer-es6-class": "error",
205 | "react/prop-types": "off",
206 | "react/self-closing-comp": "error",
207 | "react/sort-comp": "error",
208 | "react/style-prop-object": "error",
209 | "rest-spread-spacing": [
210 | "error",
211 | "never"
212 | ],
213 | "semi": [
214 | "error",
215 | "always"
216 | ],
217 | "semi-spacing": "error",
218 | "semi-style": [
219 | "error",
220 | "last"
221 | ],
222 | "space-before-blocks": "error",
223 | "space-before-function-paren": [
224 | "error",
225 | "never"
226 | ],
227 | "space-in-parens": [
228 | "error",
229 | "never"
230 | ],
231 | "space-infix-ops": "error",
232 | "switch-colon-spacing": "error",
233 | "template-curly-spacing": "error"
234 | },
235 | "settings": {
236 | "react": {
237 | "version": "18"
238 | }
239 | }
240 | }
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | push:
4 | branches: [ 'master' ]
5 | pull_request:
6 | branches: [ 'master' ]
7 | permissions:
8 | contents: read
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Use Node.js 22.x
15 | uses: actions/setup-node@v4
16 | with:
17 | node-version: 22.x
18 | cache: 'npm'
19 | - name: Install
20 | run: npm ci
21 | - name: Lint
22 | run: npm run eslint
23 | - name: Build
24 | run: npm run build
25 | - name: Check Git changes
26 | uses: multani/git-changes-action@v1
--------------------------------------------------------------------------------
/.github/workflows/coverage.yml:
--------------------------------------------------------------------------------
1 | name: Coverage
2 | on:
3 | push:
4 | branches: [ 'master' ]
5 | pull_request:
6 | branches: [ 'master' ]
7 | permissions:
8 | contents: read
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Use Node.js 22.x
15 | uses: actions/setup-node@v4
16 | with:
17 | node-version: 22.x
18 | cache: 'npm'
19 | - name: Install
20 | run: npm ci
21 | - name: Test
22 | run: npm test -- --coverage
23 | - name: Coveralls
24 | uses: coverallsapp/github-action@v2.0.0
25 |
--------------------------------------------------------------------------------
/.github/workflows/storybook.yml:
--------------------------------------------------------------------------------
1 | name: Storybook
2 | on:
3 | push:
4 | branches: [ 'master' ]
5 | pull_request:
6 | branches: [ 'master' ]
7 | permissions:
8 | contents: read
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Use Node.js 22.x
15 | uses: actions/setup-node@v4
16 | with:
17 | node-version: 22.x
18 | cache: 'npm'
19 | - name: Install
20 | run: npm ci
21 | - name: Storybook
22 | run: npm run storybook-build
23 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | push:
4 | branches: [ 'master' ]
5 | pull_request:
6 | branches: [ 'master' ]
7 | permissions:
8 | contents: read
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | node-version: [ 20.x, 22.x, 23.x ]
15 | steps:
16 | - uses: actions/checkout@v4
17 | - name: Use Node.js ${{ matrix.node-version }}
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: ${{ matrix.node-version }}
21 | cache: 'npm'
22 | - name: Install
23 | run: npm ci
24 | - name: Test
25 | run: npm test
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | /coverage
3 | /node_modules/
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .babelrc
2 | .browserslistrc
3 | .editorconfig
4 | .eslintrc.json
5 | .github
6 | .gitignore
7 | .npmignore
8 | .storybook
9 | docs
10 | rollup.config.mjs
11 | src
12 | styles
13 | __tests__
14 | coverage
--------------------------------------------------------------------------------
/.storybook/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "plugin:storybook/recommended",
4 | "../.eslintrc.json"
5 | ],
6 | "rules": {
7 | "react/react-in-jsx-scope": "off"
8 | }
9 | }
--------------------------------------------------------------------------------
/.storybook/images/disc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jpb12/react-tree-graph/0a5630c8295701a10ef6ae6064fc98806d6cdc6f/.storybook/images/disc.png
--------------------------------------------------------------------------------
/.storybook/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jpb12/react-tree-graph/0a5630c8295701a10ef6ae6064fc98806d6cdc6f/.storybook/images/favicon.ico
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | export default {
2 | addons: [
3 | {
4 | name: '@storybook/addon-essentials',
5 | options: {
6 | actions: false,
7 | backgrounds: false,
8 | measure: false,
9 | outline: false,
10 | viewport: false
11 | }
12 | }
13 | ],
14 | docs: {
15 | autodocs: true
16 | },
17 | framework: {
18 | name: '@storybook/react-webpack5',
19 | options: {}
20 | },
21 | staticDirs: ['./images'],
22 | stories: ['./stories/**/*.stories.(js|mdx)'],
23 | webpackFinal(config) {
24 | config.target = 'web';
25 | return config;
26 | }
27 | };
--------------------------------------------------------------------------------
/.storybook/manager-head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/.storybook/manager.js:
--------------------------------------------------------------------------------
1 | import { addons } from '@storybook/addons';
2 | import { create } from '@storybook/theming';
3 |
4 | addons.setConfig({
5 | theme: create({
6 | base: 'light',
7 | brandTitle: 'react-tree-graph'
8 | })
9 | });
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import { Title, Subtitle, Description, Primary, Controls, Stories } from '@storybook/blocks';
2 | import '../styles/style.css';
3 |
4 | export default {
5 | parameters: {
6 | controls: { expanded: true },
7 | docs: {
8 | page: () => (
9 | <>
10 |
11 |
12 |
13 |
14 |
15 |
16 | >
17 | )
18 | },
19 | layout: 'centered',
20 | options: {
21 | storySort: {
22 | order: ['Introduction', 'Tree', 'AnimatedTree']
23 | }
24 | },
25 | viewMode: 'docs'
26 | }
27 | };
--------------------------------------------------------------------------------
/.storybook/stories/animatedTree.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { AnimatedTree } from '../../src';
3 | import { AnimatedTreeArgTypes } from './argTypes';
4 |
5 | export default {
6 | title: 'AnimatedTree/Animations',
7 | component: AnimatedTree,
8 | argTypes: AnimatedTreeArgTypes,
9 | parameters: {
10 | docs: {
11 | description: {
12 | component: 'The AnimatedTree component has all the same props as the Tree component, and additional props to customise animation behaviour. Animations are automatically triggered when changes to the `data` prop are made. This demo works by using `setTimeout` to change the `data` prop every 2 seconds.'
13 | }
14 | }
15 | }
16 | };
17 |
18 | const order = [0, 1, 0, 2];
19 |
20 | const data = [
21 | {
22 | name: 'Parent',
23 | children: [{
24 | name: 'Child One'
25 | }, {
26 | name: 'Child Two'
27 | }, {
28 | name: 'Child Three',
29 | children: [{
30 | name: 'Grandchild One'
31 | }, {
32 | name: 'Grandchild Two'
33 | }]
34 | }]
35 | },
36 | {
37 | name: 'Child Three',
38 | children: [{
39 | name: 'Grandchild One'
40 | }, {
41 | name: 'Grandchild Two'
42 | }]
43 | },
44 | {
45 | name: 'Parent',
46 | children: [{
47 | name: 'Child One'
48 | }, {
49 | name: 'Child Two'
50 | }]
51 | }
52 | ];
53 |
54 | export const Animations = {
55 | args: {
56 | height: 400,
57 | width: 600
58 | },
59 | parameters: {
60 | controls: { include: ['duration', 'easing', 'steps'] }
61 | },
62 | render: args => {
63 | const [position, setPosition] = useState(0);
64 |
65 | useEffect(() => {
66 | setTimeout(() => {
67 | if (position >= order.length - 1) {
68 | return setPosition(0);
69 | }
70 | return setPosition(position + 1);
71 | }, 2000);
72 | });
73 |
74 | return ;
75 | }
76 | };
--------------------------------------------------------------------------------
/.storybook/stories/argTypes.js:
--------------------------------------------------------------------------------
1 | import {
2 | easeBack,
3 | easeBackIn,
4 | easeBackOut,
5 | easeBounce,
6 | easeBounceIn,
7 | easeBounceInOut,
8 | easeCircle,
9 | easeCircleIn,
10 | easeCircleOut,
11 | easeCubic,
12 | easeCubicIn,
13 | easeCubicOut,
14 | easeElastic,
15 | easeElasticIn,
16 | easeElasticInOut,
17 | easeExp,
18 | easeExpIn,
19 | easeExpOut,
20 | easeLinear,
21 | easePoly,
22 | easePolyIn,
23 | easePolyOut,
24 | easeQuad,
25 | easeQuadIn,
26 | easeQuadOut,
27 | easeSin,
28 | easeSinIn,
29 | easeSinOut
30 | } from 'd3-ease';
31 |
32 |
33 | const categories = {
34 | animation: 'Animation',
35 | data: 'Data',
36 | properties: 'SVG Properties',
37 | rendering: 'Tree Rendering'
38 | };
39 |
40 | export const TreeArgTypes = {
41 | data: {
42 | table: { category: categories.data },
43 | type: { name: 'object', required: true },
44 | description: 'The data to be rendered as a tree. Must be in a format accepted by d3.hierarchy.'
45 | },
46 | getChildren: {
47 | control: { disable: true },
48 | table: {
49 | category: categories.data,
50 | defaultValue: { summary: 'node => node.children' }
51 | },
52 | description: 'A function that returns the children for a node, or null/undefined if no children exist.'
53 | },
54 | direction: {
55 | options: ['ltr', 'rtl'],
56 | table: {
57 | category: categories.rendering,
58 | defaultValue: { summary: 'ltr' }
59 | },
60 | type: { name: 'string' },
61 | description: 'The direction of the tree, left-to-right or right-to-left.'
62 | },
63 | keyProp: {
64 | table: {
65 | category: categories.data,
66 | defaultValue: { summary: 'name' }
67 | },
68 | type: { name: 'string' },
69 | description: 'The property on each node to use as a key.'
70 | },
71 | labelProp: {
72 | table: {
73 | category: categories.data,
74 | defaultValue: { summary: 'name' }
75 | },
76 | type: { name: 'string' },
77 | description: 'The property on each node to render as a label.'
78 | },
79 | height: {
80 | table: { category: categories.rendering },
81 | type: { name: 'number', required: true },
82 | description: 'The height of the rendered tree, including margins.'
83 | },
84 | width: {
85 | table: { category: categories.rendering },
86 | type: { name: 'number', required: true },
87 | description: 'The width of the rendered tree, including margins.'
88 | },
89 | margins: {
90 | table: {
91 | category: categories.rendering,
92 | defaultValue: { summary: '{ bottom: 10, left: 20, right: 150, top: 10 }' }
93 | },
94 | type: { name: 'object' },
95 | description: 'The margins around the content. The right margin should be larger to include the rendered label text.'
96 | },
97 | children: {
98 | table: { category: categories.rendering },
99 | control: { disable: true },
100 | description: 'Will be rendered as children of the SVG, before the links and nodes.'
101 | },
102 | nodeShape: {
103 | options: ['circle', 'image', 'polygon', 'rect'],
104 | table: {
105 | category: categories.rendering,
106 | defaultValue: { summary: 'circle' }
107 | },
108 | type: { name: 'select' },
109 | description: 'The shape of the node icons. Additional nodeProps must be specifed for polygon and rect.'
110 | },
111 | pathFunc: {
112 | control: { disable: true },
113 | table: {
114 | category: categories.rendering,
115 | defaultValue: { summary: 'function(x1,y1,x2,y2)' }
116 | },
117 | description: 'Function to calculate the co-ordinates of the path between nodes.'
118 | },
119 | gProps: {
120 | table: {
121 | category: categories.properties,
122 | defaultValue: { summary: '{ className: \'node\' }' }
123 | },
124 | type: { name: 'object' },
125 | description: 'Props to be added to the `` element. The default className will still be applied if a className property is not set.'
126 | },
127 | nodeProps: {
128 | table: { category: categories.properties },
129 | type: { name: 'object' },
130 | description: 'Props to be added to the ``, ``, `` or `` element. These will take priority over the default r added to circle and height, width, x and y added to image and rect.'
131 | },
132 | pathProps: {
133 | table: {
134 | category: categories.properties,
135 | defaultValue: { summary: '{ className: \'link\' }' }
136 | },
137 | type: { name: 'object' },
138 | description: 'Props to be added to the `` element. The default className will still be applied if a className property is not set.'
139 | },
140 | svgProps: {
141 | table: { category: categories.properties },
142 | type: { name: 'object' },
143 | description: 'Props to be added to the `