├── .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 | <Subtitle/> 12 | <Description/> 13 | <Primary/> 14 | <Controls/> 15 | <Stories includePrimary={false}/> 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 <AnimatedTree data={data[order[position]]} {...args}/>; 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 `<g>` 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 `<circle>`, `<image>`, `<polygon>` or `<rect>` 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 `<path>` 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 `<svg>` element.' 144 | }, 145 | textProps: { 146 | table: { category: categories.properties }, 147 | type: { name: 'object' }, 148 | description: 'Props to be added to the `<text>` element.' 149 | } 150 | }; 151 | 152 | export const AnimatedTreeArgTypes = { 153 | duration: { 154 | table: { 155 | category: categories.animation, 156 | defaultValue: { summary: 500 } 157 | }, 158 | type: { name: 'number' }, 159 | description: 'The duration in milliseconds of animations.' 160 | }, 161 | easing: { 162 | mapping: { 163 | easeBack, 164 | easeBackIn, 165 | easeBackOut, 166 | easeBounce, 167 | easeBounceIn, 168 | easeBounceInOut, 169 | easeCircle, 170 | easeCircleIn, 171 | easeCircleOut, 172 | easeCubic, 173 | easeCubicIn, 174 | easeCubicOut, 175 | easeElastic, 176 | easeElasticIn, 177 | easeElasticInOut, 178 | easeExp, 179 | easeExpIn, 180 | easeExpOut, 181 | easeLinear, 182 | easePoly, 183 | easePolyIn, 184 | easePolyOut, 185 | easeQuad, 186 | easeQuadIn, 187 | easeQuadOut, 188 | easeSin, 189 | easeSinIn, 190 | easeSinOut 191 | }, 192 | options: [ 193 | 'easeBack', 194 | 'easeBackIn', 195 | 'easeBackOut', 196 | 'easeBounce', 197 | 'easeBounceIn', 198 | 'easeBounceInOut', 199 | 'easeCircle', 200 | 'easeCircleIn', 201 | 'easeCircleOut', 202 | 'easeCubic', 203 | 'easeCubicIn', 204 | 'easeCubicOut', 205 | 'easeElastic', 206 | 'easeElasticIn', 207 | 'easeElasticInOut', 208 | 'easeExp', 209 | 'easeExpIn', 210 | 'easeExpOut', 211 | 'easeLinear', 212 | 'easePoly', 213 | 'easePolyIn', 214 | 'easePolyOut', 215 | 'easeQuad', 216 | 'easeQuadIn', 217 | 'easeQuadOut', 218 | 'easeSin', 219 | 'easeSinIn', 220 | 'easeSinOut' 221 | ], 222 | table: { 223 | category: categories.animation, 224 | defaultValue: { summary: 'easeQuadOut' } 225 | }, 226 | type: { name: 'select' }, 227 | description: 'The easing function for animations. Takes in a number between 0 and 1 and returns a number between 0 and 1. The options here are all from the d3-ease library.' 228 | }, 229 | steps: { 230 | table: { 231 | category: categories.animation, 232 | defaultValue: { summary: 20 } 233 | }, 234 | type: { name: 'number' }, 235 | description: 'The number of steps in animations. A higher number will result in a smoother animation, but too high will cause performance issues.' 236 | }, 237 | ...TreeArgTypes 238 | }; -------------------------------------------------------------------------------- /.storybook/stories/intro.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/blocks'; 2 | 3 | <Meta title="Introduction"/> 4 | 5 | # react-tree-graph [![Github](https://img.shields.io/github/stars/jpb12/react-tree-graph?style=social)](https://github.com/jpb12/react-tree-graph) 6 | 7 | [![Build Status](https://img.shields.io/github/actions/workflow/status/jpb12/react-tree-graph/build.yml)](https://github.com/jpb12/react-tree-graph/actions/workflows/build.yml?query=branch%3Amaster) [![Coverage Status](https://coveralls.io/repos/github/jpb12/react-tree-graph/badge.svg?branch=master)](https://coveralls.io/github/jpb12/react-tree-graph?branch=master) [![npm version](https://img.shields.io/npm/v/react-tree-graph.svg)](https://www.npmjs.com/package/react-tree-graph) [![npm](https://img.shields.io/npm/dt/react-tree-graph.svg)](https://www.npmjs.com/package/react-tree-graph) [![bundle size](https://img.shields.io/bundlephobia/minzip/react-tree-graph)](https://bundlephobia.com/result?p=react-tree-graph) [![license](https://img.shields.io/npm/l/react-tree-graph)](https://github.com/jpb12/react-tree-graph/blob/master/LICENSE) 8 | 9 | A simple react component which renders data as a tree using svg. 10 | 11 | The source code for these examples can be found on [github](https://github.com/jpb12/react-tree-graph/tree/master/.storybook/stories). 12 | 13 | ## Installation 14 | 15 | ```sh 16 | npm install react-tree-graph --save 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```javascript 22 | import { Tree } from 'react-tree-graph'; 23 | 24 | const data = { 25 | name: 'Parent', 26 | children: [{ 27 | name: 'Child One' 28 | }, { 29 | name: 'Child Two' 30 | }] 31 | }; 32 | 33 | <Tree 34 | data={data} 35 | height={400} 36 | width={400}/>); 37 | 38 | import { AnimatedTree } from 'react-tree-graph'; 39 | 40 | <AnimatedTree 41 | data={data} 42 | height={400} 43 | width={400}/>); 44 | ``` 45 | 46 | If you are using webpack, and have [css-loader](https://www.npmjs.com/package/css-loader), you can include some default styles with: 47 | 48 | ```javascript 49 | import 'react-tree-graph/dist/style.css' 50 | ``` 51 | 52 | Alternatively, both the JavaScript and CSS can be included directly from the dist folder with script tags. -------------------------------------------------------------------------------- /.storybook/stories/labels.stories.js: -------------------------------------------------------------------------------- 1 | import { Tree } from '../../src'; 2 | import { TreeArgTypes } from './argTypes'; 3 | 4 | export default { 5 | title: 'Tree/Labels', 6 | component: Tree, 7 | argTypes: TreeArgTypes, 8 | parameters: { 9 | docs: { 10 | description: { 11 | component: 'Setting a `labelProp` allows multiple nodes to have the same label. You can also achieve the same result by setting a `keyProp` instead.' 12 | } 13 | } 14 | } 15 | }; 16 | 17 | export const Duplicate = { 18 | args: { 19 | height: 400, 20 | width: 600, 21 | data: { 22 | name: 'Parent', 23 | label: 'Parent', 24 | children: [{ 25 | label: 'Child', 26 | name: 'Child One' 27 | }, { 28 | label: 'Child', 29 | name: 'Child Two' 30 | }] 31 | }, 32 | labelProp: 'label' 33 | }, 34 | parameters: { 35 | controls: { include: ['data', 'labelProp'] } 36 | } 37 | }; 38 | 39 | export const JSX = { 40 | args: { 41 | height: 400, 42 | width: 600, 43 | data: { 44 | name: 'Parent', 45 | label: 'String', 46 | children: [{ 47 | label: <><rect height="18" width="32" y="-15"/><text dx="2">JSX</text></>, 48 | name: 'Child One' 49 | }, { 50 | label: () => <text>Custom component</text>, 51 | name: 'Child Two' 52 | }] 53 | }, 54 | labelProp: 'label' 55 | }, 56 | parameters: { 57 | controls: { include: ['data', 'labelProp'] }, 58 | docs: { 59 | description: { 60 | story: 'Setting a `labelProp` allows labels to be JSX. They must return valid SVG elements.' 61 | } 62 | } 63 | } 64 | }; -------------------------------------------------------------------------------- /.storybook/stories/nodes.stories.js: -------------------------------------------------------------------------------- 1 | import { Tree } from '../../src'; 2 | import { TreeArgTypes } from './argTypes'; 3 | import '../styles/nodeProps.css'; 4 | import '../styles/polygon.css'; 5 | 6 | export default { 7 | title: 'Tree/Nodes', 8 | subtitle: 'Rectangular Nodes', 9 | component: Tree, 10 | argTypes: TreeArgTypes 11 | }; 12 | 13 | const defaultArgs = { 14 | height: 400, 15 | width: 600, 16 | data: { 17 | name: 'Parent', 18 | children: [{ 19 | name: 'Child One' 20 | }, { 21 | name: 'Child Two' 22 | }] 23 | } 24 | }; 25 | 26 | export const RectangularNodes = { 27 | args: { 28 | ...defaultArgs, 29 | nodeShape: 'rect', 30 | nodeProps: { rx: 2 } 31 | }, 32 | parameters: { 33 | componentSubtitle: 'Rectangular Nodes', 34 | controls: { include: ['data', 'nodeShape', 'nodeProps'] } 35 | } 36 | }; 37 | 38 | export const PolygonNodes = { 39 | args: { 40 | ...defaultArgs, 41 | nodeShape: 'polygon', 42 | nodeProps: { 43 | points: [ 44 | 10, 45 | 0, 46 | 12.351141009169893, 47 | 6.76393202250021, 48 | 19.510565162951536, 49 | 6.9098300562505255, 50 | 13.804226065180615, 51 | 11.23606797749979, 52 | 15.877852522924734, 53 | 18.090169943749473, 54 | 10, 55 | 14, 56 | 4.12214747707527, 57 | 18.090169943749473, 58 | 6.195773934819385, 59 | 11.23606797749979, 60 | 0.4894348370484636, 61 | 6.909830056250527, 62 | 7.648858990830107, 63 | 6.76393202250021 64 | ].join(','), 65 | transform: 'translate(-10,-10)' 66 | }, 67 | svgProps: { className: 'star' }, 68 | textProps: { dx: 10.5 } 69 | }, 70 | parameters: { 71 | controls: { include: ['data', 'nodeShape', 'nodeProps', 'svgProps', 'textProps'] }, 72 | docs: { 73 | description: { 74 | story: 'For polygons, you will have to pass additional props to position the polygon and text. The polygon should be translated by half it\'s width and height, and the text should be offset by half the polygon\'s width plus some spacing for a gap.' 75 | } 76 | } 77 | } 78 | }; 79 | 80 | export const ImageNodes = { 81 | args: { 82 | ...defaultArgs, 83 | nodeShape: 'image', 84 | nodeProps: { 85 | height: 20, 86 | width: 20, 87 | href: 'disc.png' 88 | } 89 | }, 90 | parameters: { 91 | controls: { include: ['data', 'nodeShape', 'nodeProps'] } 92 | } 93 | }; 94 | 95 | export const CustomNodeProps = { 96 | args: { 97 | ...defaultArgs, 98 | data: { 99 | name: 'Parent', 100 | children: [ 101 | { 102 | label: 'First Child', 103 | labelProp: 'label', 104 | name: 'Child One', 105 | shape: 'rect' 106 | }, { 107 | name: 'Child Two', 108 | gProps: { 109 | className: 'red-node' 110 | } 111 | } 112 | ] 113 | }, 114 | gProps: { 115 | onClick: (event, node) => alert(`Clicked ${node}!`) 116 | } 117 | }, 118 | parameters: { 119 | controls: { include: ['data', 'nodeProps', 'gProps', 'pathProps', 'textProps', 'labelProp', 'keyProp', 'nodeShape'] }, 120 | docs: { 121 | description: { 122 | story: 'You can override props for individual nodes by setting them inside the `data` prop. `nodeProps`, `gProps`, `pathProps` (taken from the target node) and `textProps` on each node will be combined with those passed into `<Tree>`. `keyProp`, `labelProp` and `shape` (overrides `nodeShape`) will override those passed into `<Tree>`' 123 | } 124 | } 125 | } 126 | }; -------------------------------------------------------------------------------- /.storybook/stories/tree.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tree } from '../../src'; 3 | import { TreeArgTypes } from './argTypes'; 4 | import '../styles/styles.css'; 5 | 6 | export default { 7 | title: 'Tree', 8 | component: Tree, 9 | argTypes: TreeArgTypes, 10 | parameters: { 11 | docs: { 12 | description: { 13 | component: 'The Tree component should be used when animations are not needed. The only required props are data, height and width.' 14 | } 15 | } 16 | } 17 | }; 18 | 19 | export const Simple = { 20 | args: { 21 | height: 400, 22 | width: 600, 23 | data: { 24 | name: 'Parent', 25 | children: [{ 26 | name: 'Child One' 27 | }, { 28 | name: 'Child Two' 29 | }] 30 | } 31 | } 32 | }; 33 | 34 | export const Events = { 35 | args: { 36 | ...Simple.args, 37 | gProps: { 38 | onClick: (event, nodeKey) => alert(`Left clicked ${nodeKey}`), 39 | onContextMenu: (event, nodeKey) => { 40 | event.preventDefault(); 41 | alert(`Right clicked ${nodeKey}`); 42 | } 43 | } 44 | }, 45 | parameters: { 46 | controls: { include: ['data', 'gProps', 'pathProps', 'svgProps', 'textProps'] }, 47 | docs: { 48 | description: { 49 | story: 'Click on a node to trigger the custom event. You can also configure custom events on any of the rendered SVG elements.' 50 | } 51 | } 52 | } 53 | }; 54 | 55 | export const CustomChildren = { 56 | args: { 57 | ...Simple.args, 58 | children: <text dy="15" dx="5">Custom Title</text> 59 | }, 60 | parameters: { 61 | controls: { include: ['data', 'children'] }, 62 | docs: { 63 | description: { 64 | story: 'Children will be rendered before the tree.' 65 | } 66 | } 67 | } 68 | }; 69 | 70 | export const CustomPaths = { 71 | args: { 72 | ...Simple.args, 73 | pathFunc: (x1, y1, x2, y2) => `M${x1},${y1} ${x2},${y2}` 74 | }, 75 | parameters: { 76 | controls: { include: ['data', 'pathFunc'] }, 77 | docs: { 78 | description: { 79 | story: 'You can pass in a custom function for calculating the shape of a path between two nodes.' 80 | } 81 | } 82 | } 83 | }; 84 | 85 | export const RightToLeft = { 86 | args: { 87 | ...Simple.args, 88 | direction: 'rtl' 89 | }, 90 | parameters: { 91 | controls: { include: ['data', 'direction'] }, 92 | docs: { 93 | description: { 94 | story: 'The tree can be rendered right-to-left.' 95 | } 96 | } 97 | } 98 | }; 99 | 100 | export const Transformations = { 101 | args: { 102 | ...Simple.args, 103 | width: 400, 104 | svgProps: { transform: 'rotate(90)' } 105 | }, 106 | parameters: { 107 | controls: { include: ['data', 'svgProps'] }, 108 | docs: { 109 | description: { 110 | story: 'You can apply transformations to the tree, such as rotating it to display vertically.' 111 | } 112 | } 113 | } 114 | }; 115 | 116 | export const CustomStyles = { 117 | args: { 118 | ...Simple.args, 119 | svgProps: { className: 'custom' } 120 | }, 121 | parameters: { 122 | controls: { include: ['data', 'svgProps'] }, 123 | docs: { 124 | description: { 125 | story: 'CSS used here is available at https://github.com/jpb12/react-tree-graph/blob/master/.storybook/styles/styles.css' 126 | } 127 | } 128 | }, 129 | render: args => <div className="custom-container"><Tree {...args}/></div> 130 | }; -------------------------------------------------------------------------------- /.storybook/styles/nodeProps.css: -------------------------------------------------------------------------------- 1 | .red-node { 2 | fill: red; 3 | stroke: red; 4 | } -------------------------------------------------------------------------------- /.storybook/styles/polygon.css: -------------------------------------------------------------------------------- 1 | svg.star polygon { 2 | fill: white; 3 | stroke: black; 4 | } 5 | -------------------------------------------------------------------------------- /.storybook/styles/styles.css: -------------------------------------------------------------------------------- 1 | div.custom-container { 2 | background-color: #242424; 3 | padding: 20px; 4 | } 5 | 6 | svg.custom .node circle { 7 | fill: #F3F3FF; 8 | stroke: #2593B8; 9 | stroke-width: 1.5px; 10 | } 11 | 12 | svg.custom .node text { 13 | font-size: 11px; 14 | background-color: #444; 15 | fill: #F4F4F4; 16 | text-shadow: 0 1px 4px black; 17 | } 18 | 19 | svg.custom .node { 20 | cursor: pointer; 21 | } 22 | 23 | svg.custom path.link { 24 | fill: none; 25 | stroke: #2593B8; 26 | stroke-width: 1.5px; 27 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 8.0.3 (13 Jan 2025) 4 | 5 | * Fixing defaultProps error in react 19 6 | 7 | ## 8.0.2 (30 Apr 2024) 8 | 9 | * Support for react 19 10 | * Dropped support for react < 16.8 (already broken in a pervious update) 11 | 12 | ## 8.0.1 (10 Jan 2023) 13 | 14 | * Fixing overriding of unrelated props 15 | 16 | ## 8.0.0 (25 Oct 2022) 17 | 18 | * Breaking change: Fixed x and y co-ordinates being flipped in pathfunc 19 | * Right-to-left support 20 | * Fixed margins not being applied properly 21 | 22 | ## 7.0.6 (21 Oct 2022) 23 | 24 | * Fixing cleanup of finished animations 25 | 26 | ## 7.0.5 (21 Oct 2022) 27 | 28 | * Fixing cleanup of unfinished animations 29 | 30 | ## 7.0.4 (21 Oct 2022) 31 | 32 | * Reducing bundle size 33 | * Rewriting to use functional components 34 | 35 | ## 7.0.3 (11 Feb 2022) 36 | 37 | * Moving RFDC from dependency to dev dependency 38 | 39 | ## 7.0.2 (9 Feb 2022) 40 | 41 | * Fixing preinstall script causes install failure 42 | 43 | ## 7.0.1 (9 Feb 2022) 44 | 45 | * Fixing non-string node labels 46 | 47 | ## 7.0.0 (10 Jan 2022) 48 | 49 | * Breaking change: Single default export replaced with two named exports 50 | * Support for tree shaking 51 | * Significantly reduces bundle size, reduced even further if not using animations 52 | 53 | ### Migrating 54 | 55 | * If you were using `animated=true`, use the named `AnimatedTree` export 56 | * Otherwise, use the named `Tree` export 57 | * The `animated` prop is no longer used and can be removed 58 | 59 | ## 6.1.0 (29 Nov 2021) 60 | 61 | * Support for react 18 62 | 63 | ## 6.0.1 (17 Jun 2021) 64 | 65 | * Removing unneeded files from npm package 66 | 67 | ## 6.0.0 (9 Feb 2021) 68 | 69 | * Breaking change: Dropped support for IE (reduces bundle size by about 1/3) 70 | * Updated to d3 2.0.0 71 | 72 | ## 5.1.1 (8 Feb 2021) 73 | 74 | * Added support for react 17 as a peer dependency 75 | 76 | ## 5.1.0 (27 Jun 2020) 77 | 78 | * Adding support for image 79 | 80 | ## 5.0.0 (19 Jun 2020) 81 | 82 | * Breaking change: Adding support for rect and polygon 83 | * Breaking change: Allowing textProps to override default offsets 84 | * Breaking change: Fixing incorrect default offsets 85 | * Breaking change: Wrapping nodes and links in a <g> node for easier transformations 86 | 87 | ### Migrating 88 | 89 | * If you were using `circleProps`, use `nodeProps` instead. The format is the same 90 | * If you were using `nodeRadius`, instead pass an `r` prop through `nodeProps` 91 | * If you were using `nodeOffset`, instead pass a `dy` prop through `textProps` 92 | * If you had css selectors relying on the `path` and `g` nodes being immediate children of `svg`, you will have to modify these due to the additional `g` node inbetween 93 | * If you weren't using `nodeOffset`, node text position will change slightly 94 | 95 | ## 4.1.1 (5 Jun 2020) 96 | 97 | * Fixed incorrect proptype (thanks @josh-stevens) 98 | 99 | ## 4.1.0 (16 Mar 2020) 100 | 101 | * Added pathFunc prop to configure custom paths 102 | 103 | ## 4.0.1 (12 Aug 2019) 104 | 105 | * Fixing default classname being removed when any props configured 106 | 107 | ## 4.0.0 (11 Feb 2019) 108 | 109 | * Breaking change: additional parameters are now passed in after the event parameter 110 | * Added support for additional parameters in arbitrary event handlers 111 | 112 | ## 3.3.0 (7 Feb 2019) 113 | 114 | * onContextMenu handlers for nodes and links now have the same additional parameters as onClick (thanks @Linton-Samuel-Dawson) 115 | 116 | ## 3.2.0 (24 Sep 2018) 117 | 118 | * Adding rendering of custom children 119 | 120 | ## 3.1.1 (21 Apr 2018) 121 | 122 | * Replaced webpack with rollup for smaller bundle size and better performance 123 | 124 | ## 3.1.0 (21 Dec 2017) 125 | 126 | * Changed babel transform settings to reduce minified bundle size 127 | 128 | ## 3.0.0 (16 Dec 2017) 129 | 130 | * New props for adding any prop to any DOM element 131 | * circleProps 132 | * gProps 133 | * pathProps 134 | * svgProps 135 | * textProps 136 | * Redundant props have been removed 137 | * linkClassName 138 | * linkClassHandler 139 | * nodeClassName 140 | * nodeClassHandler 141 | * treeClassName 142 | * treeClickHandler 143 | 144 | ## 2.0.0 (12 Jul 2017) 145 | 146 | * Animations 147 | * Significant performance improvements on large trees (tested with > 150 nodes) 148 | * Added nodes now animate from the position of the closest, previously visible, ancestor 149 | * Removed nodes now animate to the position of the closest, remaining ancestor 150 | * Renamed Class props to ClassName props 151 | * Added importing of polyfills for IE support 152 | 153 | ## 1.7.2 (7 Jul 2017) 154 | 155 | * Fixing initial position of added animated nodes when root moves 156 | 157 | ## 1.7.1 (4 Jul 2017) 158 | 159 | * Updating built files to include change in previous version 160 | 161 | ## 1.7.0 (26 Jun 2017) 162 | 163 | * Added treeClass and treeClickHandler props 164 | 165 | ## 1.6.0 (24 Jun 2017) 166 | 167 | * Adding animations 168 | 169 | ## 1.5.0 (13 May 2017) 170 | 171 | * Removed warnings in react 15.5+ 172 | 173 | ## 1.4.0 (29 Apr 2017) 174 | 175 | * Added getChildren prop 176 | 177 | ## 1.3.0 (14 Apr 2017) 178 | 179 | * Node and click handlers now have event as a second parameter (thanks @ronaldborman) 180 | 181 | ## 1.2.3 (11 Apr 2017) 182 | 183 | * Updating built files to include change in previous version 184 | 185 | ## 1.2.2 (11 Apr 2017) 186 | 187 | * Fixed undefined being passed into Link's onClick handler 188 | 189 | ## 1.2.1 (29 Mar 2017) 190 | 191 | * Using d3-hierarchy instead of d3. This should significantly reduce bundle size 192 | 193 | ## 1.2.0 (5 Mar 2017) 194 | 195 | * Upgraded dependencies, including webpack to 2.x 196 | * Included a CSS file for basic styling 197 | 198 | ## 1.1.0 (14 Dec 2016) 199 | 200 | * Upgraded d3 dependency to 4.4.0 201 | * Included a minified file 202 | 203 | ## 1.0.1 (11 Dec 2016) 204 | 205 | * Removing an npm shrinkwrap file. Its presence caused duplicate dependencies to be installed when react-tree-graph was installed 206 | 207 | ## 1.0.0 (11 Dec 2016) 208 | 209 | * Initial release 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 James Brierley 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-tree-graph [![Github](https://img.shields.io/github/stars/jpb12/react-tree-graph?style=social)](https://github.com/jpb12/react-tree-graph) 2 | ================================================================================================================================================ 3 | 4 | [![Build Status](https://img.shields.io/github/actions/workflow/status/jpb12/react-tree-graph/build.yml)](https://github.com/jpb12/react-tree-graph/actions/workflows/build.yml?query=branch%3Amaster) [![Coverage Status](https://coveralls.io/repos/github/jpb12/react-tree-graph/badge.svg?branch=master)](https://coveralls.io/github/jpb12/react-tree-graph?branch=master) [![npm version](https://img.shields.io/npm/v/react-tree-graph.svg)](https://www.npmjs.com/package/react-tree-graph) [![npm](https://img.shields.io/npm/dt/react-tree-graph.svg)](https://www.npmjs.com/package/react-tree-graph) [![bundle size](https://img.shields.io/bundlephobia/minzip/react-tree-graph)](https://bundlephobia.com/result?p=react-tree-graph) [![license](https://img.shields.io/npm/l/react-tree-graph)](https://github.com/jpb12/react-tree-graph/blob/master/LICENSE) [![Storybook](https://cdn.jsdelivr.net/gh/storybookjs/brand@main/badge/badge-storybook.svg)](https://jpb12.github.io/react-tree-graph) 5 | 6 | A simple react component which renders data as a tree using svg. 7 | 8 | Supports react 16.8+. 9 | 10 | Check out the [examples](https://jpb12.github.io/react-tree-graph) and the [demo](https://jpb12.github.io/tree-viewer/). 11 | 12 | Older Versions 13 | -------------- 14 | [7.X](https://github.com/jpb12/react-tree-graph/tree/v7.0.6) 15 | [6.X](https://github.com/jpb12/react-tree-graph/tree/v6.1.0) 16 | [5.X](https://github.com/jpb12/react-tree-graph/tree/v5.1.1) 17 | [4.X](https://github.com/jpb12/react-tree-graph/tree/v4.1.1) 18 | [3.X](https://github.com/jpb12/react-tree-graph/tree/v3.3.0) 19 | [2.X](https://github.com/jpb12/react-tree-graph/tree/v2.0.0) 20 | [1.X](https://github.com/jpb12/react-tree-graph/tree/v1.7.2) 21 | 22 | Installation 23 | ---------- 24 | ```sh 25 | npm install react-tree-graph --save 26 | ``` 27 | 28 | Usage 29 | ----- 30 | 31 | ```javascript 32 | import { Tree } from 'react-tree-graph'; 33 | 34 | const data = { 35 | name: 'Parent', 36 | children: [{ 37 | name: 'Child One' 38 | }, { 39 | name: 'Child Two' 40 | }] 41 | }; 42 | 43 | <Tree 44 | data={data} 45 | height={400} 46 | width={400}/>); 47 | 48 | import { AnimatedTree } from 'react-tree-graph'; 49 | 50 | <AnimatedTree 51 | data={data} 52 | height={400} 53 | width={400}/>); 54 | ``` 55 | 56 | If you are using webpack, and have [css-loader](https://www.npmjs.com/package/css-loader), you can include some default styles with: 57 | 58 | ```javascript 59 | import 'react-tree-graph/dist/style.css' 60 | ``` 61 | 62 | Alternatively, both the JavaScript and CSS can be included directly from the dist folder with script tags. 63 | 64 | Configuration 65 | ------------- 66 | 67 | Tree 68 | 69 | | Property | Type | Mandatory | Default | Description | 70 | |:---|:---|:---|:---|:---| 71 | | `data` | object | yes | | The data to be rendered as a tree. Must be in a format accepted by [d3.hierarchy](https://github.com/d3/d3-hierarchy/blob/master/README.md#hierarchy). | 72 | | `margins` | object | | `{ bottom : 10, left : 20, right : 150, top : 10}` | The margins around the content. The right margin should be larger to include the rendered label text. | 73 | | `height` | number | yes | | The height of the rendered tree, including margins. | 74 | | `width` | number | yes | | The width of the rendered tree, including margins. | 75 | | `direction` | `ltr`,`rtl` | | `ltr` | The direction the tree will be rendered in. Either left-to-right or right-to-left. | 76 | | `children` | node | | | Will be rendered as children of the SVG, before the links and nodes. | 77 | | `getChildren` | function(node) | | node => node.children | A function that returns the children for a node, or null/undefined if no children exist. | 78 | | `keyProp` | string | | "name" | The property on each node to use as a key. | 79 | | `labelProp` | string | | "name" | The property on each node to render as a label. | 80 | | `nodeShape` | `circle`,`image`,`polygon`,`rect` | | `circle` | The shape of the node icons. | 81 | | `nodeProps` | object | | `{}` | Props to be added to the `<circle>`, `<image>`, `<polygon>` or `<rect>` element. These will take priority over the default `r` added to `circle` and `height`, `width`, `x` and `y` added to `image` and `rect`. | 82 | | `gProps` | object | | `{ className: 'node' }` | Props to be added to the `<g>` element. The default className will still be applied if a className property is not set. | 83 | | `pathProps` | object | | `{ className: 'link' }` | Props to be added to the `<path>` element. The default className will still be applied if a className property is not set. | 84 | | `pathFunc` | function(x1,y1,x2,y2) | | curved | Function to calculate the co-ordinates of the path between nodes. | 85 | | `svgProps` | object | | `{}` | Props to be added to the `<svg>` element. | 86 | | `textProps` | object | | `{}` | Props to be added to the `<text>` element. | 87 | 88 | AnimatedTree has the following properties in addition to the above. 89 | 90 | | Property | Type | Mandatory | Default | Description | 91 | |:---|:---|:---|:---|:---| 92 | | `duration` | number | | 500 | The duration in milliseconds of animations. | 93 | | `easing` | function(interval) | | [d3-ease](https://www.npmjs.com/package/d3-ease).easeQuadOut | The easing function for animations. Takes in a number between 0 and 1, and returns a number between 0 and 1. | 94 | | `steps` | number | | 20 | The number of steps in animations. A higher number will result in a smoother animation, but too high will cause performance issues. | 95 | 96 | ### Events 97 | 98 | Event handlers in `nodeProps`, `gProps` and `textProps` will be called with the node ID as an additional parameter. 99 | 100 | `function(event, nodeId) { ... }` 101 | 102 | Event handlers in `pathProps` will be called with the source and target node IDs as additional parameters. 103 | 104 | `function(event, sourceNodeId, targetNodeId) { ... }` 105 | 106 | ### Overriding props 107 | 108 | The following properties can also be overridden by setting then for individual nodes. 109 | 110 | | Global Prop | Node Prop | 111 | |:---|:---| 112 | | `keyProp` | `keyProp` | 113 | | `labelProp` | `labelProp` | 114 | | `nodeShape` | `shape` | 115 | 116 | The following object properties, if set on individual nodes, will be combined with the object properties set on the tree. If a property exists in both objects, the value from the node will be taken. 117 | 118 | | Prop | Description | 119 | |:---|:---| 120 | | `nodeProps` | | 121 | | `gProps` | | 122 | | `pathProps` | Props for a path are taken from the target node. | 123 | | `textProps` | | 124 | 125 | TypeScript 126 | ---------- 127 | 128 | [Type definitions](https://www.npmjs.com/package/@types/react-tree-graph) are available as a separate package. (thanks @PCOffline) 129 | -------------------------------------------------------------------------------- /__tests__/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true, 4 | "node": true 5 | }, 6 | "extends": "../.eslintrc.json" 7 | } -------------------------------------------------------------------------------- /__tests__/Components/__snapshots__/animatedTests.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<Animated> does not animate when props other than nodes or links change 1`] = ` 4 | <Container 5 | direction="ltr" 6 | duration={1} 7 | easing={[Function]} 8 | gProps={{}} 9 | getChildren={[Function]} 10 | height={100} 11 | keyProp="name" 12 | labelProp="name" 13 | links={ 14 | [ 15 | { 16 | "source": { 17 | "data": { 18 | "name": "Colour", 19 | }, 20 | "x": 1, 21 | "y": 2, 22 | }, 23 | "target": { 24 | "data": { 25 | "name": "Black", 26 | }, 27 | "x": 1, 28 | "y": 2, 29 | }, 30 | }, 31 | ] 32 | } 33 | margins={ 34 | { 35 | "left": 20, 36 | "top": 10, 37 | } 38 | } 39 | nodeProps={{}} 40 | nodeShape="rect" 41 | nodes={ 42 | [ 43 | { 44 | "data": { 45 | "name": "Colour", 46 | }, 47 | "x": 1, 48 | "y": 2, 49 | }, 50 | { 51 | "data": { 52 | "name": "Black", 53 | }, 54 | "x": 1, 55 | "y": 2, 56 | }, 57 | ] 58 | } 59 | pathProps={{}} 60 | steps={1} 61 | svgProps={{}} 62 | textProps={{}} 63 | width={100} 64 | /> 65 | `; 66 | 67 | exports[`<Animated> renders correctly and sets initial state 1`] = ` 68 | <Container 69 | direction="ltr" 70 | duration={1} 71 | easing={[Function]} 72 | gProps={{}} 73 | getChildren={[Function]} 74 | height={100} 75 | keyProp="name" 76 | labelProp="name" 77 | links={ 78 | [ 79 | { 80 | "source": { 81 | "data": { 82 | "name": "Colour", 83 | }, 84 | "x": 1, 85 | "y": 2, 86 | }, 87 | "target": { 88 | "data": { 89 | "name": "Black", 90 | }, 91 | "x": 1, 92 | "y": 2, 93 | }, 94 | }, 95 | ] 96 | } 97 | margins={ 98 | { 99 | "left": 20, 100 | "top": 10, 101 | } 102 | } 103 | nodeProps={{}} 104 | nodeShape="circle" 105 | nodes={ 106 | [ 107 | { 108 | "data": { 109 | "name": "Colour", 110 | }, 111 | "x": 1, 112 | "y": 2, 113 | }, 114 | { 115 | "data": { 116 | "name": "Black", 117 | }, 118 | "x": 1, 119 | "y": 2, 120 | }, 121 | ] 122 | } 123 | pathProps={{}} 124 | steps={1} 125 | svgProps={{}} 126 | textProps={{}} 127 | width={100} 128 | /> 129 | `; 130 | -------------------------------------------------------------------------------- /__tests__/Components/__snapshots__/animatedTreeTests.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<AnimatedTree> renders correctly 1`] = ` 4 | <Animated 5 | direction="ltr" 6 | duration={500} 7 | easing={[Function]} 8 | gProps={ 9 | { 10 | "className": "node", 11 | } 12 | } 13 | getChildren={[Function]} 14 | height={100} 15 | keyProp="name" 16 | labelProp="name" 17 | links={ 18 | [ 19 | { 20 | "source": { 21 | "children": [ 22 | Node { 23 | "data": { 24 | "name": "Black", 25 | }, 26 | "depth": 1, 27 | "height": 0, 28 | "parent": Node { 29 | "children": [Circular], 30 | "data": { 31 | "children": [ 32 | { 33 | "name": "Black", 34 | }, 35 | ], 36 | "name": "Colour", 37 | }, 38 | "depth": 0, 39 | "height": 1, 40 | "parent": null, 41 | "x": 40, 42 | "y": 0, 43 | }, 44 | "x": 40, 45 | "y": 30, 46 | }, 47 | ], 48 | "data": { 49 | "children": [ 50 | { 51 | "name": "Black", 52 | }, 53 | ], 54 | "name": "Colour", 55 | }, 56 | "depth": 0, 57 | "height": 1, 58 | "parent": null, 59 | "x": 0, 60 | "y": 40, 61 | }, 62 | "target": { 63 | "data": { 64 | "name": "Black", 65 | }, 66 | "depth": 1, 67 | "height": 0, 68 | "parent": Node { 69 | "children": [ 70 | Node { 71 | "data": { 72 | "name": "Black", 73 | }, 74 | "depth": 1, 75 | "height": 0, 76 | "parent": [Circular], 77 | "x": 40, 78 | "y": 30, 79 | }, 80 | ], 81 | "data": { 82 | "children": [ 83 | { 84 | "name": "Black", 85 | }, 86 | ], 87 | "name": "Colour", 88 | }, 89 | "depth": 0, 90 | "height": 1, 91 | "parent": null, 92 | "x": 40, 93 | "y": 0, 94 | }, 95 | "x": 30, 96 | "y": 40, 97 | }, 98 | }, 99 | ] 100 | } 101 | margins={ 102 | { 103 | "bottom": 10, 104 | "left": 20, 105 | "right": 150, 106 | "top": 10, 107 | } 108 | } 109 | nodeProps={{}} 110 | nodeShape="circle" 111 | nodes={ 112 | [ 113 | { 114 | "children": [ 115 | Node { 116 | "data": { 117 | "name": "Black", 118 | }, 119 | "depth": 1, 120 | "height": 0, 121 | "parent": Node { 122 | "children": [Circular], 123 | "data": { 124 | "children": [ 125 | { 126 | "name": "Black", 127 | }, 128 | ], 129 | "name": "Colour", 130 | }, 131 | "depth": 0, 132 | "height": 1, 133 | "parent": null, 134 | "x": 40, 135 | "y": 0, 136 | }, 137 | "x": 40, 138 | "y": 30, 139 | }, 140 | ], 141 | "data": { 142 | "children": [ 143 | { 144 | "name": "Black", 145 | }, 146 | ], 147 | "name": "Colour", 148 | }, 149 | "depth": 0, 150 | "height": 1, 151 | "parent": null, 152 | "x": 0, 153 | "y": 40, 154 | }, 155 | { 156 | "data": { 157 | "name": "Black", 158 | }, 159 | "depth": 1, 160 | "height": 0, 161 | "parent": Node { 162 | "children": [ 163 | Node { 164 | "data": { 165 | "name": "Black", 166 | }, 167 | "depth": 1, 168 | "height": 0, 169 | "parent": [Circular], 170 | "x": 40, 171 | "y": 30, 172 | }, 173 | ], 174 | "data": { 175 | "children": [ 176 | { 177 | "name": "Black", 178 | }, 179 | ], 180 | "name": "Colour", 181 | }, 182 | "depth": 0, 183 | "height": 1, 184 | "parent": null, 185 | "x": 40, 186 | "y": 0, 187 | }, 188 | "x": 30, 189 | "y": 40, 190 | }, 191 | ] 192 | } 193 | pathProps={ 194 | { 195 | "className": "link", 196 | } 197 | } 198 | steps={20} 199 | svgProps={{}} 200 | textProps={{}} 201 | width={200} 202 | /> 203 | `; 204 | -------------------------------------------------------------------------------- /__tests__/Components/__snapshots__/containerTests.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<Container> html tree props added 1`] = ` 4 | <svg 5 | className="test-class" 6 | height={100} 7 | stoke="none" 8 | width={200} 9 | > 10 | <g 11 | transform="translate(20, 10)" 12 | /> 13 | </svg> 14 | `; 15 | 16 | exports[`<Container> renders children 1`] = ` 17 | <svg 18 | height={100} 19 | width={200} 20 | > 21 | <text> 22 | Extra child 23 | </text> 24 | <g 25 | transform="translate(20, 10)" 26 | > 27 | <Link 28 | key="Black" 29 | keyProp="name" 30 | pathProps={{}} 31 | source={ 32 | { 33 | "data": { 34 | "name": "Colour", 35 | }, 36 | "x": 1, 37 | "y": 2, 38 | } 39 | } 40 | target={ 41 | { 42 | "data": { 43 | "name": "Black", 44 | }, 45 | "x": 100, 46 | "y": 50, 47 | } 48 | } 49 | x1={1} 50 | x2={100} 51 | y1={2} 52 | y2={50} 53 | /> 54 | <Node 55 | direction="ltr" 56 | gProps={{}} 57 | key="Colour" 58 | keyProp="name" 59 | labelProp="name" 60 | name="Colour" 61 | nodeProps={{}} 62 | shape="circle" 63 | textProps={{}} 64 | x={1} 65 | y={2} 66 | /> 67 | <Node 68 | direction="ltr" 69 | gProps={{}} 70 | key="Black" 71 | keyProp="name" 72 | labelProp="name" 73 | name="Black" 74 | nodeProps={{}} 75 | shape="circle" 76 | textProps={{}} 77 | x={100} 78 | y={50} 79 | /> 80 | </g> 81 | </svg> 82 | `; 83 | 84 | exports[`<Container> renders correctly 1`] = ` 85 | <svg 86 | height={100} 87 | width={200} 88 | > 89 | <g 90 | transform="translate(20, 10)" 91 | > 92 | <Link 93 | key="Black" 94 | keyProp="name" 95 | pathProps={{}} 96 | source={ 97 | { 98 | "data": { 99 | "name": "Colour", 100 | }, 101 | "x": 1, 102 | "y": 2, 103 | } 104 | } 105 | target={ 106 | { 107 | "data": { 108 | "name": "Black", 109 | }, 110 | "x": 100, 111 | "y": 50, 112 | } 113 | } 114 | x1={1} 115 | x2={100} 116 | y1={2} 117 | y2={50} 118 | /> 119 | <Node 120 | direction="ltr" 121 | gProps={{}} 122 | key="Colour" 123 | keyProp="name" 124 | labelProp="name" 125 | name="Colour" 126 | nodeProps={{}} 127 | shape="circle" 128 | textProps={{}} 129 | x={1} 130 | y={2} 131 | /> 132 | <Node 133 | direction="ltr" 134 | gProps={{}} 135 | key="Black" 136 | keyProp="name" 137 | labelProp="name" 138 | name="Black" 139 | nodeProps={{}} 140 | shape="circle" 141 | textProps={{}} 142 | x={100} 143 | y={50} 144 | /> 145 | </g> 146 | </svg> 147 | `; 148 | -------------------------------------------------------------------------------- /__tests__/Components/__snapshots__/linkTests.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<Link> renders correctly 1`] = ` 4 | <path 5 | className="Link" 6 | d="M1,2C3,2 3,9 5,9" 7 | /> 8 | `; 9 | 10 | exports[`<Link> renders correctly with custom path 1`] = ` 11 | <path 12 | className="Link" 13 | d="M1,2 5,9" 14 | /> 15 | `; 16 | -------------------------------------------------------------------------------- /__tests__/Components/__snapshots__/nodeTests.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<Node> htmlProps applied to all elements 1`] = ` 4 | <g 5 | className="g" 6 | direction={null} 7 | transform="translate(1, 2)" 8 | > 9 | <circle 10 | className="circle" 11 | r={5} 12 | /> 13 | <text 14 | className="text" 15 | dx={5.5} 16 | dy={5} 17 | > 18 | Test Node 19 | </text> 20 | </g> 21 | `; 22 | 23 | exports[`<Node> renders circle correctly 1`] = ` 24 | <g 25 | className="test" 26 | direction={null} 27 | transform="translate(1, 2)" 28 | > 29 | <circle 30 | r={5} 31 | /> 32 | <text 33 | dx={5.5} 34 | dy={5} 35 | > 36 | Test Node 37 | </text> 38 | </g> 39 | `; 40 | 41 | exports[`<Node> renders circle correctly with custom radius 1`] = ` 42 | <g 43 | className="test" 44 | direction={null} 45 | transform="translate(1, 2)" 46 | > 47 | <circle 48 | r={10} 49 | /> 50 | <text 51 | dx={10.5} 52 | dy={5} 53 | > 54 | Test Node 55 | </text> 56 | </g> 57 | `; 58 | 59 | exports[`<Node> renders custom label correctly 1`] = ` 60 | <g 61 | className="test" 62 | direction={null} 63 | transform="translate(1, 2)" 64 | > 65 | <circle 66 | r={5} 67 | /> 68 | <g 69 | transform="translate(5.5, 5)" 70 | > 71 | <circle 72 | r="5" 73 | /> 74 | </g> 75 | </g> 76 | `; 77 | 78 | exports[`<Node> renders image correctly 1`] = ` 79 | <g 80 | className="test" 81 | direction={null} 82 | transform="translate(1, 2)" 83 | > 84 | <image 85 | height={10} 86 | href="http://example.com" 87 | width={10} 88 | x={-5} 89 | y={-5} 90 | /> 91 | <text 92 | dx={5.5} 93 | dy={5} 94 | > 95 | Test Node 96 | </text> 97 | </g> 98 | `; 99 | 100 | exports[`<Node> renders image correctly with custom size 1`] = ` 101 | <g 102 | className="test" 103 | direction={null} 104 | transform="translate(1, 2)" 105 | > 106 | <image 107 | height={20} 108 | href="http://example.com" 109 | width={30} 110 | x={-15} 111 | y={-10} 112 | /> 113 | <text 114 | dx={15.5} 115 | dy={5} 116 | > 117 | Test Node 118 | </text> 119 | </g> 120 | `; 121 | 122 | exports[`<Node> renders rect correctly 1`] = ` 123 | <g 124 | className="test" 125 | direction={null} 126 | transform="translate(1, 2)" 127 | > 128 | <rect 129 | height={10} 130 | width={10} 131 | x={-5} 132 | y={-5} 133 | /> 134 | <text 135 | dx={5.5} 136 | dy={5} 137 | > 138 | Test Node 139 | </text> 140 | </g> 141 | `; 142 | 143 | exports[`<Node> renders rect correctly with custom size 1`] = ` 144 | <g 145 | className="test" 146 | direction={null} 147 | transform="translate(1, 2)" 148 | > 149 | <rect 150 | height={20} 151 | width={30} 152 | x={-15} 153 | y={-10} 154 | /> 155 | <text 156 | dx={15.5} 157 | dy={5} 158 | > 159 | Test Node 160 | </text> 161 | </g> 162 | `; 163 | 164 | exports[`<Node> renders rtl correctly 1`] = ` 165 | <g 166 | className="test" 167 | direction="rtl" 168 | transform="translate(1, 2)" 169 | > 170 | <circle 171 | r={5} 172 | /> 173 | <text 174 | dx={-5.5} 175 | dy={5} 176 | > 177 | Test Node 178 | </text> 179 | </g> 180 | `; 181 | -------------------------------------------------------------------------------- /__tests__/Components/__snapshots__/treeTests.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`<Tree> renders correctly 1`] = ` 4 | <Container 5 | direction="ltr" 6 | gProps={ 7 | { 8 | "className": "node", 9 | } 10 | } 11 | getChildren={[Function]} 12 | height={100} 13 | keyProp="name" 14 | labelProp="name" 15 | links={ 16 | [ 17 | { 18 | "source": { 19 | "children": [ 20 | Node { 21 | "data": { 22 | "name": "Black", 23 | }, 24 | "depth": 1, 25 | "height": 0, 26 | "parent": Node { 27 | "children": [Circular], 28 | "data": { 29 | "children": [ 30 | { 31 | "name": "Black", 32 | }, 33 | ], 34 | "name": "Colour", 35 | }, 36 | "depth": 0, 37 | "height": 1, 38 | "parent": null, 39 | "x": 40, 40 | "y": 0, 41 | }, 42 | "x": 40, 43 | "y": 30, 44 | }, 45 | ], 46 | "data": { 47 | "children": [ 48 | { 49 | "name": "Black", 50 | }, 51 | ], 52 | "name": "Colour", 53 | }, 54 | "depth": 0, 55 | "height": 1, 56 | "parent": null, 57 | "x": 0, 58 | "y": 40, 59 | }, 60 | "target": { 61 | "data": { 62 | "name": "Black", 63 | }, 64 | "depth": 1, 65 | "height": 0, 66 | "parent": Node { 67 | "children": [ 68 | Node { 69 | "data": { 70 | "name": "Black", 71 | }, 72 | "depth": 1, 73 | "height": 0, 74 | "parent": [Circular], 75 | "x": 40, 76 | "y": 30, 77 | }, 78 | ], 79 | "data": { 80 | "children": [ 81 | { 82 | "name": "Black", 83 | }, 84 | ], 85 | "name": "Colour", 86 | }, 87 | "depth": 0, 88 | "height": 1, 89 | "parent": null, 90 | "x": 40, 91 | "y": 0, 92 | }, 93 | "x": 30, 94 | "y": 40, 95 | }, 96 | }, 97 | ] 98 | } 99 | margins={ 100 | { 101 | "bottom": 10, 102 | "left": 20, 103 | "right": 150, 104 | "top": 10, 105 | } 106 | } 107 | nodeProps={{}} 108 | nodeShape="circle" 109 | nodes={ 110 | [ 111 | { 112 | "children": [ 113 | Node { 114 | "data": { 115 | "name": "Black", 116 | }, 117 | "depth": 1, 118 | "height": 0, 119 | "parent": Node { 120 | "children": [Circular], 121 | "data": { 122 | "children": [ 123 | { 124 | "name": "Black", 125 | }, 126 | ], 127 | "name": "Colour", 128 | }, 129 | "depth": 0, 130 | "height": 1, 131 | "parent": null, 132 | "x": 40, 133 | "y": 0, 134 | }, 135 | "x": 40, 136 | "y": 30, 137 | }, 138 | ], 139 | "data": { 140 | "children": [ 141 | { 142 | "name": "Black", 143 | }, 144 | ], 145 | "name": "Colour", 146 | }, 147 | "depth": 0, 148 | "height": 1, 149 | "parent": null, 150 | "x": 0, 151 | "y": 40, 152 | }, 153 | { 154 | "data": { 155 | "name": "Black", 156 | }, 157 | "depth": 1, 158 | "height": 0, 159 | "parent": Node { 160 | "children": [ 161 | Node { 162 | "data": { 163 | "name": "Black", 164 | }, 165 | "depth": 1, 166 | "height": 0, 167 | "parent": [Circular], 168 | "x": 40, 169 | "y": 30, 170 | }, 171 | ], 172 | "data": { 173 | "children": [ 174 | { 175 | "name": "Black", 176 | }, 177 | ], 178 | "name": "Colour", 179 | }, 180 | "depth": 0, 181 | "height": 1, 182 | "parent": null, 183 | "x": 40, 184 | "y": 0, 185 | }, 186 | "x": 30, 187 | "y": 40, 188 | }, 189 | ] 190 | } 191 | pathProps={ 192 | { 193 | "className": "link", 194 | } 195 | } 196 | svgProps={{}} 197 | textProps={{}} 198 | width={200} 199 | /> 200 | `; 201 | -------------------------------------------------------------------------------- /__tests__/Components/animatedTests.js: -------------------------------------------------------------------------------- 1 | import React, { act } from 'react'; 2 | import { mount, shallow } from 'enzyme'; 3 | 4 | import { easeQuadOut } from 'd3-ease'; 5 | import Animated from '../../src/components/animated'; 6 | import Container from '../../src/components/container'; 7 | 8 | jest.useFakeTimers(); 9 | 10 | const nodes = [ 11 | { 12 | x: 1, 13 | y: 2, 14 | data: { 15 | name: 'Colour' 16 | } 17 | }, { 18 | x: 100, 19 | y: 50, 20 | data: { 21 | name: 'Black' 22 | } 23 | } 24 | ]; 25 | 26 | const links = [{ 27 | source: nodes[0], 28 | target: nodes[1] 29 | }]; 30 | 31 | const defaultProps = { 32 | direction: 'ltr', 33 | getChildren: n => n.children, 34 | height: 100, 35 | width: 100, 36 | keyProp: 'name', 37 | labelProp: 'name', 38 | nodeShape: 'circle', 39 | duration: 1, 40 | easing: easeQuadOut, 41 | links: links, 42 | nodes: nodes, 43 | margins: { top: 10, left: 20 }, 44 | steps: 1, 45 | nodeProps: {}, 46 | gProps: {}, 47 | pathProps: {}, 48 | svgProps: {}, 49 | textProps: {} 50 | }; 51 | 52 | describe('<Animated>', () => { 53 | test('renders correctly and sets initial state', () => { 54 | const tree = shallow(<Animated {...defaultProps}/>); 55 | 56 | expect(tree).toMatchSnapshot(); 57 | 58 | expect(tree.find(Container).props().nodes[1].x).toBe(1); 59 | expect(tree.find(Container).props().nodes[1].y).toBe(2); 60 | 61 | expect(tree.find(Container).props().links[0].target.x).toBe(1); 62 | expect(tree.find(Container).props().links[0].target.y).toBe(2); 63 | }); 64 | 65 | test('animates node when moved', () => { 66 | const tree = mount(<Animated {...defaultProps} steps={2} duration={100}/>); 67 | 68 | act(() => jest.advanceTimersByTime(100)); 69 | tree.update(); 70 | 71 | expect(tree.find(Container).props().nodes[1].x).toBe(100); 72 | expect(tree.find(Container).props().nodes[1].y).toBe(50); 73 | 74 | tree.setProps({ 75 | nodes: [ 76 | nodes[0], 77 | { 78 | x: 120, 79 | y: 80, 80 | data: { 81 | name: 'Black' 82 | } 83 | } 84 | ] 85 | }); 86 | 87 | act(() => jest.advanceTimersByTime(50)); 88 | tree.update(); 89 | 90 | expect(tree.find(Container).props().nodes[1].x).toBe(115); 91 | expect(tree.find(Container).props().nodes[1].y).toBe(72.5); 92 | 93 | act(() => jest.advanceTimersByTime(50)); 94 | tree.update(); 95 | 96 | expect(tree.find(Container).props().nodes[1].x).toBe(120); 97 | expect(tree.find(Container).props().nodes[1].y).toBe(80); 98 | }); 99 | 100 | test('animates link when moved', () => { 101 | const tree = mount(<Animated {...defaultProps} steps={2} duration={100}/>); 102 | 103 | act(() => jest.advanceTimersByTime(100)); 104 | tree.update(); 105 | 106 | expect(tree.find(Container).props().links[0].source.x).toBe(1); 107 | expect(tree.find(Container).props().links[0].source.y).toBe(2); 108 | expect(tree.find(Container).props().links[0].target.x).toBe(100); 109 | expect(tree.find(Container).props().links[0].target.y).toBe(50); 110 | 111 | tree.setProps({ 112 | links: [{ 113 | source: { 114 | x: 5, 115 | y: 10, 116 | data: { 117 | name: 'Colour' 118 | } 119 | }, 120 | target: { 121 | x: 200, 122 | y: 100, 123 | data: { 124 | name: 'Black' 125 | } 126 | } 127 | }] 128 | }); 129 | 130 | act(() => jest.advanceTimersByTime(50)); 131 | tree.update(); 132 | 133 | expect(tree.find(Container).props().links[0].source.x).toBe(4); 134 | expect(tree.find(Container).props().links[0].source.y).toBe(8); 135 | expect(tree.find(Container).props().links[0].target.x).toBe(175); 136 | expect(tree.find(Container).props().links[0].target.y).toBe(87.5); 137 | 138 | act(() => jest.advanceTimersByTime(50)); 139 | tree.update(); 140 | 141 | expect(tree.find(Container).props().links[0].source.x).toBe(5); 142 | expect(tree.find(Container).props().links[0].source.y).toBe(10); 143 | expect(tree.find(Container).props().links[0].target.x).toBe(200); 144 | expect(tree.find(Container).props().links[0].target.y).toBe(100); 145 | }); 146 | 147 | test('animates node when added', () => { 148 | const tree = mount(<Animated {...defaultProps} steps={2} duration={100}/>); 149 | 150 | tree.setProps({ 151 | nodes: [ 152 | nodes[0], 153 | nodes[1], 154 | { 155 | x: 120, 156 | y: 80, 157 | data: { 158 | name: 'Purple' 159 | } 160 | } 161 | ] 162 | }); 163 | 164 | act(() => jest.advanceTimersByTime(50)); 165 | tree.update(); 166 | 167 | expect(tree.find(Container).props().nodes[2].x).toBe(90.25); 168 | expect(tree.find(Container).props().nodes[2].y).toBe(60.5); 169 | 170 | act(() => jest.advanceTimersByTime(50)); 171 | tree.update(); 172 | 173 | expect(tree.find(Container).props().nodes[2].x).toBe(120); 174 | expect(tree.find(Container).props().nodes[2].y).toBe(80); 175 | }); 176 | 177 | test('animates node from parent when added', () => { 178 | const tree = mount(<Animated {...defaultProps} steps={2} duration={100}/>); 179 | 180 | act(() => jest.advanceTimersByTime(100)); 181 | 182 | tree.setProps({ 183 | nodes: [ 184 | nodes[0], 185 | { 186 | x: 100, 187 | y: 50, 188 | data: { 189 | name: 'Black' 190 | }, 191 | children: [{ 192 | data: { 193 | name: 'Purple' 194 | } 195 | }] 196 | }, 197 | { 198 | x: 120, 199 | y: 80, 200 | data: { 201 | name: 'Purple' 202 | } 203 | } 204 | ] 205 | }); 206 | 207 | act(() => jest.advanceTimersByTime(50)); 208 | tree.update(); 209 | 210 | expect(tree.find(Container).props().nodes[2].x).toBe(115); 211 | expect(tree.find(Container).props().nodes[2].y).toBe(72.5); 212 | 213 | act(() => jest.advanceTimersByTime(50)); 214 | tree.update(); 215 | 216 | expect(tree.find(Container).props().nodes[2].x).toBe(120); 217 | expect(tree.find(Container).props().nodes[2].y).toBe(80); 218 | }); 219 | 220 | test('animates link when added', () => { 221 | const tree = mount(<Animated {...defaultProps} steps={2} duration={100}/>); 222 | 223 | tree.setProps({ 224 | links: [ 225 | links[0], 226 | { 227 | source: { 228 | x: 5, 229 | y: 10, 230 | data: { 231 | name: 'Colour' 232 | } 233 | }, 234 | target: { 235 | x: 200, 236 | y: 100, 237 | data: { 238 | name: 'Purple' 239 | } 240 | } 241 | } 242 | ] 243 | }); 244 | 245 | act(() => jest.advanceTimersByTime(50)); 246 | tree.update(); 247 | 248 | expect(tree.find(Container).props().links[1].source.x).toBe(4); 249 | expect(tree.find(Container).props().links[1].source.y).toBe(8); 250 | expect(tree.find(Container).props().links[1].target.x).toBe(150.25); 251 | expect(tree.find(Container).props().links[1].target.y).toBe(75.5); 252 | 253 | act(() => jest.advanceTimersByTime(50)); 254 | tree.update(); 255 | 256 | expect(tree.find(Container).props().links[1].source.x).toBe(5); 257 | expect(tree.find(Container).props().links[1].source.y).toBe(10); 258 | expect(tree.find(Container).props().links[1].target.x).toBe(200); 259 | expect(tree.find(Container).props().links[1].target.y).toBe(100); 260 | }); 261 | 262 | test('animates node when removed', () => { 263 | const tree = mount(<Animated {...defaultProps} steps={2} duration={100}/>); 264 | 265 | act(() => jest.advanceTimersByTime(100)); 266 | 267 | tree.setProps({ 268 | nodes: [ 269 | nodes[0] 270 | ] 271 | }); 272 | 273 | act(() => jest.advanceTimersByTime(50)); 274 | tree.update(); 275 | 276 | expect(tree.find(Container).props().nodes[1].x).toBe(25.75); 277 | expect(tree.find(Container).props().nodes[1].y).toBe(14); 278 | 279 | act(() => jest.advanceTimersByTime(50)); 280 | tree.update(); 281 | 282 | expect(tree.find(Container).props().nodes.length).toBe(1); 283 | }); 284 | 285 | test('animates link when removed', () => { 286 | const tree = mount(<Animated {...defaultProps} steps={2} duration={100}/>); 287 | 288 | act(() => jest.advanceTimersByTime(100)); 289 | 290 | tree.setProps({ 291 | links: [] 292 | }); 293 | 294 | act(() => jest.advanceTimersByTime(50)); 295 | tree.update(); 296 | 297 | expect(tree.find(Container).props().links[0].source.x).toBe(75.25); 298 | expect(tree.find(Container).props().links[0].source.y).toBe(38); 299 | expect(tree.find(Container).props().links[0].target.x).toBe(100); 300 | expect(tree.find(Container).props().links[0].target.y).toBe(50); 301 | 302 | act(() => jest.advanceTimersByTime(50)); 303 | tree.update(); 304 | 305 | expect(tree.find(Container).props().links.length).toBe(0); 306 | }); 307 | 308 | test('animates from inital value on mount', () => { 309 | const tree = mount(<Animated {...defaultProps} duration={100}/>); 310 | 311 | expect(tree.find(Container).props().nodes[1].x).toBe(1); 312 | expect(tree.find(Container).props().nodes[1].y).toBe(2); 313 | 314 | act(() => jest.advanceTimersByTime(100)); 315 | tree.update(); 316 | 317 | expect(tree.find(Container).props().nodes[1].x).toBe(100); 318 | expect(tree.find(Container).props().nodes[1].y).toBe(50); 319 | }); 320 | 321 | test('does not animate when props other than nodes or links change', () => { 322 | const tree = shallow(<Animated {...defaultProps}/>); 323 | 324 | tree.setProps({ nodeShape: 'rect' }); 325 | 326 | expect(tree).toMatchSnapshot(); 327 | }); 328 | }); -------------------------------------------------------------------------------- /__tests__/Components/animatedTreeTests.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import AnimatedTree from '../../src/components/animatedTree'; 5 | 6 | describe('<AnimatedTree>', () => { 7 | test('renders correctly', () => { 8 | const props = { 9 | data: { 10 | name: 'Colour', 11 | children: [{ 12 | name: 'Black' 13 | }] 14 | }, 15 | height: 100, 16 | width: 200 17 | }; 18 | 19 | const tree = shallow(<AnimatedTree {...props}/>); 20 | 21 | expect(tree).toMatchSnapshot(); 22 | }); 23 | }); -------------------------------------------------------------------------------- /__tests__/Components/containerTests.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import Container from '../../src/components/container'; 5 | import Link from '../../src/components/link'; 6 | import Node from '../../src/components/node'; 7 | 8 | const nodes = [ 9 | { 10 | x: 1, 11 | y: 2, 12 | data: { 13 | name: 'Colour' 14 | } 15 | }, { 16 | x: 100, 17 | y: 50, 18 | data: { 19 | name: 'Black' 20 | } 21 | } 22 | ]; 23 | 24 | const defaultProps = { 25 | nodes: [], 26 | links: [], 27 | direction: 'ltr', 28 | height: 100, 29 | keyProp: 'name', 30 | labelProp: 'name', 31 | margins: { top: 10, left: 20 }, 32 | nodeShape: 'circle', 33 | width: 200, 34 | nodeProps: {}, 35 | gProps: {}, 36 | pathProps: {}, 37 | svgProps: {}, 38 | textProps: {} 39 | }; 40 | 41 | describe('<Container>', () => { 42 | test('renders correctly', () => { 43 | const props = { 44 | nodes: nodes, 45 | links: [{ 46 | source: nodes[0], 47 | target: nodes[1] 48 | }] 49 | }; 50 | 51 | const tree = shallow(<Container {...defaultProps} {...props}/>); 52 | 53 | expect(tree).toMatchSnapshot(); 54 | }); 55 | 56 | test('renders children', () => { 57 | const props = { 58 | nodes: nodes, 59 | links: [{ 60 | source: nodes[0], 61 | target: nodes[1] 62 | }] 63 | }; 64 | 65 | const tree = shallow(<Container {...defaultProps} {...props}><text>Extra child</text></Container>); 66 | 67 | expect(tree).toMatchSnapshot(); 68 | }); 69 | 70 | test('html tree props added', () => { 71 | const props = { 72 | svgProps: { 73 | className: 'test-class', 74 | stoke: 'none' 75 | } 76 | }; 77 | 78 | const tree = shallow(<Container {...defaultProps} {...props}/>); 79 | 80 | expect(tree).toMatchSnapshot(); 81 | }); 82 | 83 | test('path props combined', () => { 84 | const props = { 85 | links: [{ 86 | 87 | source: nodes[0], 88 | target: { ...nodes[1], data: { name: 1 } } 89 | }, { 90 | name: 2, 91 | source: nodes[0], 92 | target: { ...nodes[1], data: { name: 1, pathProps: { className: 'override' } } } 93 | }], 94 | pathProps: { 95 | className: 'default' 96 | } 97 | }; 98 | 99 | const tree = shallow(<Container {...defaultProps} {...props}/>); 100 | 101 | const links = tree.find(Link); 102 | expect(links.length).toBe(2); 103 | expect(links.at(0).props().pathProps).toEqual({ className: 'default' }); 104 | expect(links.at(1).props().pathProps).toEqual({ className: 'override' }); 105 | }); 106 | 107 | test('node props combined', () => { 108 | function onClick() { } 109 | 110 | const props = { 111 | nodes: [ 112 | { ...nodes[0], data: { name: 1 } }, 113 | { ...nodes[1], data: { name: 2, gProps: { className: 'override' } } } 114 | ] 115 | }; 116 | 117 | const tree = shallow(<Container {...defaultProps} {...props} gProps={{ className: 'default', onClick }}/>); 118 | 119 | const domNodes = tree.find(Node); 120 | expect(domNodes.length).toBe(2); 121 | expect(domNodes.at(0).props().gProps).toEqual({ className: 'default', onClick }); 122 | expect(domNodes.at(1).props().gProps).toEqual({ className: 'override', onClick }); 123 | }); 124 | }); -------------------------------------------------------------------------------- /__tests__/Components/linkTests.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import Link from '../../src/components/link'; 5 | 6 | const defaultProps = { 7 | source: { 8 | data: { 9 | id: 'origin' 10 | } 11 | }, 12 | target: { 13 | data: { 14 | id: 'target' 15 | } 16 | }, 17 | keyProp: 'id', 18 | pathProps: { 19 | className: 'Link' 20 | }, 21 | x1: 1, 22 | x2: 5, 23 | y1: 2, 24 | y2: 9 25 | }; 26 | 27 | function straightPath(x1, y1, x2, y2) { 28 | return `M${x1},${y1} ${x2},${y2}`; 29 | } 30 | 31 | describe('<Link>', () => { 32 | test('renders correctly', () => { 33 | const tree = shallow(<Link {...defaultProps}/>); 34 | expect(tree).toMatchSnapshot(); 35 | }); 36 | 37 | test('renders correctly with custom path', () => { 38 | const tree = shallow(<Link {...defaultProps} pathFunc={straightPath}/>); 39 | expect(tree).toMatchSnapshot(); 40 | }); 41 | 42 | test('click event has correct parameters', () => { 43 | const clickMock = jest.fn(); 44 | const event = {}; 45 | 46 | const props = { 47 | pathProps: { 48 | onClick: clickMock 49 | } 50 | }; 51 | 52 | const tree = shallow(<Link {...defaultProps} {...props}/>); 53 | tree.find('path').simulate('click', event); 54 | 55 | expect(clickMock).toHaveBeenCalledTimes(1); 56 | expect(clickMock).toHaveBeenCalledWith(event, 'origin', 'target'); 57 | }); 58 | 59 | test('right click event has correct parameters', () => { 60 | const rightClickMock = jest.fn(); 61 | const event = {}; 62 | 63 | const props = { 64 | pathProps: { 65 | onContextMenu: rightClickMock 66 | } 67 | }; 68 | 69 | const tree = shallow(<Link {...defaultProps} {...props}/>); 70 | tree.find('path').simulate('contextmenu', event); 71 | 72 | expect(rightClickMock).toHaveBeenCalledTimes(1); 73 | expect(rightClickMock).toHaveBeenCalledWith(event, 'origin', 'target'); 74 | }); 75 | 76 | test('clicking with no prop handler does nothing', () => { 77 | const tree = shallow(<Link {...defaultProps}/>); 78 | tree.find('path').simulate('click'); 79 | }); 80 | }); -------------------------------------------------------------------------------- /__tests__/Components/nodeTests.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import Node from '../../src/components/node'; 5 | 6 | const defaultProps = { 7 | x: 1, 8 | y: 2, 9 | keyProp: '', 10 | labelProp: 'name', 11 | direction: 'ltr', 12 | shape: 'circle', 13 | gProps: { 14 | className: 'test' 15 | }, 16 | nodeProps: {}, 17 | textProps: {}, 18 | name: 'Test Node' 19 | }; 20 | 21 | describe('<Node>', () => { 22 | test('renders circle correctly', () => { 23 | const tree = shallow(<Node {...defaultProps}/>); 24 | expect(tree).toMatchSnapshot(); 25 | }); 26 | 27 | test('renders circle correctly with custom radius', () => { 28 | const tree = shallow(<Node {...defaultProps} nodeProps={{ r: 10 }}/>); 29 | expect(tree).toMatchSnapshot(); 30 | }); 31 | 32 | test('renders rect correctly', () => { 33 | const tree = shallow(<Node {...defaultProps} shape="rect"/>); 34 | expect(tree).toMatchSnapshot(); 35 | }); 36 | 37 | test('renders rect correctly with custom size', () => { 38 | const tree = shallow(<Node {...defaultProps} shape="rect" nodeProps={{ height: 20, width: 30 }}/>); 39 | expect(tree).toMatchSnapshot(); 40 | }); 41 | 42 | test('renders image correctly', () => { 43 | const tree = shallow(<Node {...defaultProps} shape="image" nodeProps={{ href: 'http://example.com' }}/>); 44 | expect(tree).toMatchSnapshot(); 45 | }); 46 | 47 | test('renders image correctly with custom size', () => { 48 | const tree = shallow( 49 | <Node {...defaultProps} shape="image" nodeProps={{ href: 'http://example.com', height: 20, width: 30 }}/> 50 | ); 51 | expect(tree).toMatchSnapshot(); 52 | }); 53 | 54 | test('renders custom label correctly', () => { 55 | const tree = shallow( 56 | <Node {...defaultProps} labelProp="label" label={<circle r="5"/>}/> 57 | ); 58 | expect(tree).toMatchSnapshot(); 59 | }); 60 | 61 | test('renders rtl correctly', () => { 62 | const tree = shallow(<Node {...defaultProps} direction="rtl"/>); 63 | expect(tree).toMatchSnapshot(); 64 | }); 65 | 66 | test('click event has correct parameters', () => { 67 | const clickMock = jest.fn(); 68 | const event = {}; 69 | 70 | const props = { 71 | keyProp: 'id', 72 | gProps: { 73 | onClick: clickMock 74 | }, 75 | id: 'testKey' 76 | }; 77 | 78 | const tree = shallow(<Node {...defaultProps} {...props}/>); 79 | tree.find('g').simulate('click', event); 80 | 81 | expect(clickMock).toHaveBeenCalledTimes(1); 82 | expect(clickMock).toHaveBeenCalledWith(event, 'testKey'); 83 | }); 84 | 85 | test('right click event has correct parameters', () => { 86 | const rightClickMock = jest.fn(); 87 | const event = {}; 88 | 89 | const props = { 90 | keyProp: 'id', 91 | gProps: { 92 | onContextMenu: rightClickMock 93 | }, 94 | id: 'testKey' 95 | }; 96 | 97 | const tree = shallow(<Node {...defaultProps} {...props}/>); 98 | tree.find('g').simulate('contextmenu', event); 99 | 100 | expect(rightClickMock).toHaveBeenCalledTimes(1); 101 | expect(rightClickMock).toHaveBeenCalledWith(event, 'testKey'); 102 | }); 103 | 104 | 105 | test('clicking with no prop handler does nothing', () => { 106 | const props = { 107 | keyProp: 'id', 108 | id: 'testKey' 109 | }; 110 | 111 | const tree = shallow(<Node {...defaultProps} {...props}/>); 112 | tree.find('g').simulate('click'); 113 | }); 114 | 115 | test('htmlProps applied to all elements', () => { 116 | const props = { 117 | gProps: { 118 | className: 'g' 119 | }, 120 | nodeProps: { 121 | className: 'circle' 122 | }, 123 | textProps: { 124 | className: 'text' 125 | } 126 | }; 127 | 128 | const tree = shallow(<Node {...defaultProps} {...props}/>); 129 | expect(tree).toMatchSnapshot(); 130 | }); 131 | }); -------------------------------------------------------------------------------- /__tests__/Components/treeTests.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import Tree from '../../src/components/tree'; 5 | 6 | describe('<Tree>', () => { 7 | test('renders correctly', () => { 8 | const props = { 9 | data: { 10 | name: 'Colour', 11 | children: [{ 12 | name: 'Black' 13 | }] 14 | }, 15 | height: 100, 16 | width: 200 17 | }; 18 | 19 | const tree = shallow(<Tree {...props}/>); 20 | 21 | expect(tree).toMatchSnapshot(); 22 | }); 23 | }); -------------------------------------------------------------------------------- /__tests__/d3Tests.js: -------------------------------------------------------------------------------- 1 | import rfdc from 'rfdc'; 2 | import getTreeData from '../src/d3'; 3 | 4 | const clone = rfdc(); 5 | 6 | const defaultProps = { 7 | getChildren: n => n.children, 8 | direction: 'ltr', 9 | height: 100, 10 | width: 300 11 | }; 12 | 13 | describe('getTreeData', () => { 14 | test('does not mutate prop data', () => { 15 | const data = { 16 | name: 'Colour', 17 | children: [{ 18 | name: 'Black' 19 | }] 20 | }; 21 | const clonedData = clone(data); 22 | getTreeData({ ...defaultProps, data }); 23 | expect(data).toMatchObject(clonedData); 24 | }); 25 | 26 | test('calculates tree data correctly', () => { 27 | const data = { 28 | name: 'Colour', 29 | children: [{ 30 | name: 'Black' 31 | }] 32 | }; 33 | const result = getTreeData({ ...defaultProps, data }); 34 | expect(result).toMatchObject({ 35 | nodes: [{ 36 | x: 0, 37 | y: 40 38 | }, { 39 | x: 130, 40 | y: 40 41 | }] 42 | }); 43 | }); 44 | 45 | test('calculates rtl tree data correctly', () => { 46 | const data = { 47 | name: 'Colour', 48 | children: [{ 49 | name: 'Black' 50 | }] 51 | }; 52 | const result = getTreeData({ ...defaultProps, data, direction: 'rtl' }); 53 | expect(result).toMatchObject({ 54 | nodes: [{ 55 | x: 130, 56 | y: 40 57 | }, { 58 | x: 0, 59 | y: 40 60 | }] 61 | }); 62 | }); 63 | }); -------------------------------------------------------------------------------- /__tests__/startup.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from '@cfaester/enzyme-adapter-react-18'; 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); -------------------------------------------------------------------------------- /__tests__/wrapHandlersTests.js: -------------------------------------------------------------------------------- 1 | import wrapHandlers from '../src/wrapHandlers'; 2 | 3 | describe('wrapHandlers', () => { 4 | test('does nothing if no events', () => { 5 | const result = wrapHandlers({ x: 5 }, 1, 2); 6 | expect(result).toMatchObject({ x: 5 }); 7 | }); 8 | 9 | test.each(['onBlur', 'onClick', 'onContextMenu', 'onFocus'])( 10 | 'wraps %s', 11 | name => { 12 | const handlerMock = jest.fn(); 13 | const result = wrapHandlers({ [name]: handlerMock }, 1, 2); 14 | result[name](0); 15 | expect(handlerMock).toBeCalledWith(0, 1, 2); 16 | } 17 | ); 18 | }); -------------------------------------------------------------------------------- /dist/index.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@babel/runtime/helpers/extends"),require("d3-ease"),require("react"),require("d3-hierarchy")):"function"==typeof define&&define.amd?define(["exports","@babel/runtime/helpers/extends","d3-ease","react","d3-hierarchy"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).ReactTreeGraph={},e._extends,e.d3,e.React,e.d3)}(this,(function(e,t,r,n,o){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}var s=a(t),d=a(n);function i(e){const t=e.margins||{bottom:10,left:"rtl"!==e.direction?20:150,right:"rtl"!==e.direction?150:20,top:10},r=e.width-t.left-t.right,n=e.height-t.top-t.bottom,a=o.hierarchy(e.data,e.getChildren),s=o.tree().size([n,r])(a);return{links:s.links().map((t=>({...t,source:{...t.source,x:"rtl"!==e.direction?t.source.y:r-t.source.y,y:t.source.x},target:{...t.target,x:"rtl"!==e.direction?t.target.y:r-t.target.y,y:t.target.x}}))),margins:t,nodes:s.descendants().map((t=>({...t,x:"rtl"!==e.direction?t.y:r-t.y,y:t.x})))}}const l=/on[A-Z]/;function p(e,...t){const r=Object.keys(e).filter((t=>l.test(t)&&"function"==typeof e[t])).reduce(((r,n)=>(r[n]=function(e,t){return r=>e(r,...t)}(e[n],t),r)),{});return{...e,...r}}function c(e,t,r,n){return`M${e},${t}C${(e+r)/2},${t} ${(e+r)/2},${n} ${r},${n}`}function u(e){const t=p(e.pathProps,e.source.data[e.keyProp],e.target.data[e.keyProp]),r=(e.pathFunc||c)(e.x1,e.y1,e.x2,e.y2);return d.default.createElement("path",s.default({},t,{d:r}))}function h(e){let t=.5,r=e.nodeProps;switch(e.shape){case"circle":r={r:5,...r},t+=r.r;break;case"image":case"rect":r={height:10,width:10,...r},r={x:-r.width/2,y:-r.height/2,...r},t+=r.width/2}"rtl"===e.direction&&(t=-t);const n=p(r,e[e.keyProp]),o=p(e.gProps,e[e.keyProp]),a=p(e.textProps,e[e.keyProp]),i="string"==typeof e[e.labelProp]?d.default.createElement("text",s.default({dx:t,dy:5},a),e[e.labelProp]):d.default.createElement("g",s.default({transform:`translate(${t}, 5)`},a),e[e.labelProp]);return d.default.createElement("g",s.default({},o,{transform:`translate(${e.x}, ${e.y})`,direction:"rtl"===e.direction?"rtl":null}),d.default.createElement(e.shape,n),i)}function P(e){return d.default.createElement("svg",s.default({},e.svgProps,{height:e.height,width:e.width}),e.children,d.default.createElement("g",{transform:`translate(${e.margins.left}, ${e.margins.top})`},e.links.map((t=>d.default.createElement(u,{key:t.target.data[e.keyProp],keyProp:e.keyProp,pathFunc:e.pathFunc,source:t.source,target:t.target,x1:t.source.x,x2:t.target.x,y1:t.source.y,y2:t.target.y,pathProps:{...e.pathProps,...t.target.data.pathProps}}))),e.nodes.map((t=>d.default.createElement(h,s.default({key:t.data[e.keyProp],keyProp:e.keyProp,labelProp:e.labelProp,direction:e.direction,shape:e.nodeShape,x:t.x,y:t.y},t.data,{nodeProps:{...e.nodeProps,...t.data.nodeProps},gProps:{...e.gProps,...t.data.gProps},textProps:{...e.textProps,...t.data.textProps}}))))))}function f(e){const t=e.nodes[0].x,r=e.nodes[0].y,[o,a]=n.useState({nodes:e.nodes.map((e=>({...e,x:t,y:r}))),links:e.links.map((e=>({source:{...e.source,x:t,y:r},target:{...e.target,x:t,y:r}})))}),[i,l]=n.useState(null);function p(t,r,n){let o=t;for(;o;){let t=n.nodes.find((e=>c(o,e)));if(t)return t;o=r.nodes.find((t=>(e.getChildren(t)||[]).some((e=>c(o,e)))))}return n.nodes[0]}function c(t,r){return t.data[e.keyProp]===r.data[e.keyProp]}function u(t,r){return t.source.data[e.keyProp]===r.source.data[e.keyProp]&&t.target.data[e.keyProp]===r.target.data[e.keyProp]}function h(t,r,n){return t+(r-t)*e.easing(n)}return n.useEffect((function(){clearInterval(i);let t=0;const r=function(e,t){const r=t.nodes.filter((t=>e.nodes.every((e=>!c(t,e))))).map((r=>({base:r,old:p(r,t,e),new:r}))),n=t.nodes.filter((t=>e.nodes.some((e=>c(t,e))))).map((t=>({base:t,old:e.nodes.find((e=>c(t,e))),new:t}))),o=e.nodes.filter((e=>t.nodes.every((t=>!c(e,t))))).map((r=>({base:r,old:r,new:p(r,e,t)}))),a=t.links.filter((t=>e.links.every((e=>!u(t,e))))).map((r=>({base:r,old:p(r.target,t,e),new:r}))),s=t.links.filter((t=>e.links.some((e=>u(t,e))))).map((t=>({base:t,old:e.links.find((e=>u(t,e))),new:t}))),d=e.links.filter((e=>t.links.every((t=>!u(e,t))))).map((r=>({base:r,old:r,new:p(r.target,e,t)})));return{nodes:n.concat(r).concat(o),links:s.concat(a).concat(d)}}(o,e),n=setInterval((()=>{if(t++,t===e.steps)return clearInterval(n),void a({nodes:e.nodes,links:e.links});a(function(e,t){return{nodes:e.nodes.map((e=>function(e,t,r,n){return{...e,x:h(t.x,r.x,n),y:h(t.y,r.y,n)}}(e.base,e.old,e.new,t))),links:e.links.map((e=>function(e,t,r,n){return{source:{...e.source,x:h(t.source?t.source.x:t.x,r.source?r.source.x:r.x,n),y:h(t.source?t.source.y:t.y,r.source?r.source.y:r.y,n)},target:{...e.target,x:h(t.target?t.target.x:t.x,r.target?r.target.x:r.x,n),y:h(t.target?t.target.y:t.y,r.target?r.target.y:r.y,n)}}}(e.base,e.old,e.new,t)))}}(r,t/e.steps))}),e.duration/e.steps);return l(n),()=>clearInterval(i)}),[e.nodes,e.links]),d.default.createElement(P,s.default({},e,o))}e.AnimatedTree=function(e){const t={direction:"ltr",duration:500,easing:r.easeQuadOut,getChildren:e=>e.children,steps:20,keyProp:"name",labelProp:"name",nodeShape:"circle",nodeProps:{},gProps:{},pathProps:{},svgProps:{},textProps:{},...e};return d.default.createElement(f,s.default({duration:t.duration,easing:t.easing,getChildren:t.getChildren,direction:t.direction,height:t.height,keyProp:t.keyProp,labelProp:t.labelProp,nodeShape:t.nodeShape,nodeProps:t.nodeProps,pathFunc:t.pathFunc,steps:t.steps,width:t.width,gProps:{className:"node",...t.gProps},pathProps:{className:"link",...t.pathProps},svgProps:t.svgProps,textProps:t.textProps},i(t)),t.children)},e.Tree=function(e){const t={direction:"ltr",getChildren:e=>e.children,keyProp:"name",labelProp:"name",nodeShape:"circle",nodeProps:{},gProps:{},pathProps:{},svgProps:{},textProps:{},...e};return d.default.createElement(P,s.default({getChildren:t.getChildren,direction:t.direction,height:t.height,keyProp:t.keyProp,labelProp:t.labelProp,nodeShape:t.nodeShape,nodeProps:t.nodeProps,pathFunc:t.pathFunc,width:t.width,gProps:{className:"node",...t.gProps},pathProps:{className:"link",...t.pathProps},svgProps:t.svgProps,textProps:t.textProps},i(t)),t.children)}})); 2 | -------------------------------------------------------------------------------- /dist/module/components/animated.js: -------------------------------------------------------------------------------- 1 | import _extends from '@babel/runtime/helpers/extends'; 2 | import React, { useState, useEffect } from 'react'; 3 | import Container from './container.js'; 4 | 5 | function Animated(props) { 6 | const initialX = props.nodes[0].x; 7 | const initialY = props.nodes[0].y; 8 | const [state, setState] = useState({ 9 | nodes: props.nodes.map(n => ({ 10 | ...n, 11 | x: initialX, 12 | y: initialY 13 | })), 14 | links: props.links.map(l => ({ 15 | source: { 16 | ...l.source, 17 | x: initialX, 18 | y: initialY 19 | }, 20 | target: { 21 | ...l.target, 22 | x: initialX, 23 | y: initialY 24 | } 25 | })) 26 | }); 27 | const [animation, setAnimation] = useState(null); 28 | useEffect(animate, [props.nodes, props.links]); 29 | function animate() { 30 | // Stop previous animation if one is already in progress. We will start the next animation 31 | // from the position we are currently in 32 | clearInterval(animation); 33 | let counter = 0; 34 | 35 | // Do as much one-time calculation outside of the animation step, which needs to be fast 36 | const animationContext = getAnimationContext(state, props); 37 | const interval = setInterval(() => { 38 | counter++; 39 | if (counter === props.steps) { 40 | clearInterval(interval); 41 | setState({ 42 | nodes: props.nodes, 43 | links: props.links 44 | }); 45 | return; 46 | } 47 | setState(calculateNewState(animationContext, counter / props.steps)); 48 | }, props.duration / props.steps); 49 | setAnimation(interval); 50 | return () => clearInterval(animation); 51 | } 52 | function getAnimationContext(initialState, newState) { 53 | // Nodes/links that are in both states need to be moved from the old position to the new one 54 | // Nodes/links only in the initial state are being removed, and should be moved to the position 55 | // of the closest ancestor that still exists, or the new root 56 | // Nodes/links only in the new state are being added, and should be moved from the position of 57 | // the closest ancestor that previously existed, or the old root 58 | 59 | // The base determines which node/link the data (like classes and labels) comes from for rendering 60 | 61 | // We only run this once at the start of the animation, so optimisation is less important 62 | const addedNodes = newState.nodes.filter(n1 => initialState.nodes.every(n2 => !areNodesSame(n1, n2))).map(n1 => ({ 63 | base: n1, 64 | old: getClosestAncestor(n1, newState, initialState), 65 | new: n1 66 | })); 67 | const changedNodes = newState.nodes.filter(n1 => initialState.nodes.some(n2 => areNodesSame(n1, n2))).map(n1 => ({ 68 | base: n1, 69 | old: initialState.nodes.find(n2 => areNodesSame(n1, n2)), 70 | new: n1 71 | })); 72 | const removedNodes = initialState.nodes.filter(n1 => newState.nodes.every(n2 => !areNodesSame(n1, n2))).map(n1 => ({ 73 | base: n1, 74 | old: n1, 75 | new: getClosestAncestor(n1, initialState, newState) 76 | })); 77 | const addedLinks = newState.links.filter(l1 => initialState.links.every(l2 => !areLinksSame(l1, l2))).map(l1 => ({ 78 | base: l1, 79 | old: getClosestAncestor(l1.target, newState, initialState), 80 | new: l1 81 | })); 82 | const changedLinks = newState.links.filter(l1 => initialState.links.some(l2 => areLinksSame(l1, l2))).map(l1 => ({ 83 | base: l1, 84 | old: initialState.links.find(l2 => areLinksSame(l1, l2)), 85 | new: l1 86 | })); 87 | const removedLinks = initialState.links.filter(l1 => newState.links.every(l2 => !areLinksSame(l1, l2))).map(l1 => ({ 88 | base: l1, 89 | old: l1, 90 | new: getClosestAncestor(l1.target, initialState, newState) 91 | })); 92 | return { 93 | nodes: changedNodes.concat(addedNodes).concat(removedNodes), 94 | links: changedLinks.concat(addedLinks).concat(removedLinks) 95 | }; 96 | } 97 | function getClosestAncestor(node, stateWithNode, stateWithoutNode) { 98 | let oldParent = node; 99 | while (oldParent) { 100 | let newParent = stateWithoutNode.nodes.find(n => areNodesSame(oldParent, n)); 101 | if (newParent) { 102 | return newParent; 103 | } 104 | oldParent = stateWithNode.nodes.find(n => (props.getChildren(n) || []).some(c => areNodesSame(oldParent, c))); 105 | } 106 | return stateWithoutNode.nodes[0]; 107 | } 108 | function areNodesSame(a, b) { 109 | return a.data[props.keyProp] === b.data[props.keyProp]; 110 | } 111 | function areLinksSame(a, b) { 112 | return a.source.data[props.keyProp] === b.source.data[props.keyProp] && a.target.data[props.keyProp] === b.target.data[props.keyProp]; 113 | } 114 | function calculateNewState(animationContext, interval) { 115 | return { 116 | nodes: animationContext.nodes.map(n => calculateNodePosition(n.base, n.old, n.new, interval)), 117 | links: animationContext.links.map(l => calculateLinkPosition(l.base, l.old, l.new, interval)) 118 | }; 119 | } 120 | function calculateLinkPosition(link, start, end, interval) { 121 | return { 122 | source: { 123 | ...link.source, 124 | x: calculateNewValue(start.source ? start.source.x : start.x, end.source ? end.source.x : end.x, interval), 125 | y: calculateNewValue(start.source ? start.source.y : start.y, end.source ? end.source.y : end.y, interval) 126 | }, 127 | target: { 128 | ...link.target, 129 | x: calculateNewValue(start.target ? start.target.x : start.x, end.target ? end.target.x : end.x, interval), 130 | y: calculateNewValue(start.target ? start.target.y : start.y, end.target ? end.target.y : end.y, interval) 131 | } 132 | }; 133 | } 134 | function calculateNodePosition(node, start, end, interval) { 135 | return { 136 | ...node, 137 | x: calculateNewValue(start.x, end.x, interval), 138 | y: calculateNewValue(start.y, end.y, interval) 139 | }; 140 | } 141 | function calculateNewValue(start, end, interval) { 142 | return start + (end - start) * props.easing(interval); 143 | } 144 | return /*#__PURE__*/React.createElement(Container, _extends({}, props, state)); 145 | } 146 | 147 | export { Animated as default }; 148 | -------------------------------------------------------------------------------- /dist/module/components/animatedTree.js: -------------------------------------------------------------------------------- 1 | import _extends from '@babel/runtime/helpers/extends'; 2 | import { easeQuadOut } from 'd3-ease'; 3 | import React from 'react'; 4 | import getTreeData from '../d3.js'; 5 | import Animated from './animated.js'; 6 | 7 | function AnimatedTree(props) { 8 | const propsWithDefaults = { 9 | direction: 'ltr', 10 | duration: 500, 11 | easing: easeQuadOut, 12 | getChildren: n => n.children, 13 | steps: 20, 14 | keyProp: 'name', 15 | labelProp: 'name', 16 | nodeShape: 'circle', 17 | nodeProps: {}, 18 | gProps: {}, 19 | pathProps: {}, 20 | svgProps: {}, 21 | textProps: {}, 22 | ...props 23 | }; 24 | return /*#__PURE__*/React.createElement(Animated, _extends({ 25 | duration: propsWithDefaults.duration, 26 | easing: propsWithDefaults.easing, 27 | getChildren: propsWithDefaults.getChildren, 28 | direction: propsWithDefaults.direction, 29 | height: propsWithDefaults.height, 30 | keyProp: propsWithDefaults.keyProp, 31 | labelProp: propsWithDefaults.labelProp, 32 | nodeShape: propsWithDefaults.nodeShape, 33 | nodeProps: propsWithDefaults.nodeProps, 34 | pathFunc: propsWithDefaults.pathFunc, 35 | steps: propsWithDefaults.steps, 36 | width: propsWithDefaults.width, 37 | gProps: { 38 | className: 'node', 39 | ...propsWithDefaults.gProps 40 | }, 41 | pathProps: { 42 | className: 'link', 43 | ...propsWithDefaults.pathProps 44 | }, 45 | svgProps: propsWithDefaults.svgProps, 46 | textProps: propsWithDefaults.textProps 47 | }, getTreeData(propsWithDefaults)), propsWithDefaults.children); 48 | } 49 | 50 | export { AnimatedTree as default }; 51 | -------------------------------------------------------------------------------- /dist/module/components/container.js: -------------------------------------------------------------------------------- 1 | import _extends from '@babel/runtime/helpers/extends'; 2 | import React from 'react'; 3 | import Link from './link.js'; 4 | import Node from './node.js'; 5 | 6 | function Container(props) { 7 | return /*#__PURE__*/React.createElement("svg", _extends({}, props.svgProps, { 8 | height: props.height, 9 | width: props.width 10 | }), props.children, /*#__PURE__*/React.createElement("g", { 11 | transform: `translate(${props.margins.left}, ${props.margins.top})` 12 | }, props.links.map(link => /*#__PURE__*/React.createElement(Link, { 13 | key: link.target.data[props.keyProp], 14 | keyProp: props.keyProp, 15 | pathFunc: props.pathFunc, 16 | source: link.source, 17 | target: link.target, 18 | x1: link.source.x, 19 | x2: link.target.x, 20 | y1: link.source.y, 21 | y2: link.target.y, 22 | pathProps: { 23 | ...props.pathProps, 24 | ...link.target.data.pathProps 25 | } 26 | })), props.nodes.map(node => /*#__PURE__*/React.createElement(Node, _extends({ 27 | key: node.data[props.keyProp], 28 | keyProp: props.keyProp, 29 | labelProp: props.labelProp, 30 | direction: props.direction, 31 | shape: props.nodeShape, 32 | x: node.x, 33 | y: node.y 34 | }, node.data, { 35 | nodeProps: { 36 | ...props.nodeProps, 37 | ...node.data.nodeProps 38 | }, 39 | gProps: { 40 | ...props.gProps, 41 | ...node.data.gProps 42 | }, 43 | textProps: { 44 | ...props.textProps, 45 | ...node.data.textProps 46 | } 47 | }))))); 48 | } 49 | 50 | export { Container as default }; 51 | -------------------------------------------------------------------------------- /dist/module/components/link.js: -------------------------------------------------------------------------------- 1 | import _extends from '@babel/runtime/helpers/extends'; 2 | import React from 'react'; 3 | import wrapHandlers from '../wrapHandlers.js'; 4 | 5 | function diagonal(x1, y1, x2, y2) { 6 | return `M${x1},${y1}C${(x1 + x2) / 2},${y1} ${(x1 + x2) / 2},${y2} ${x2},${y2}`; 7 | } 8 | function Link(props) { 9 | const wrappedProps = wrapHandlers(props.pathProps, props.source.data[props.keyProp], props.target.data[props.keyProp]); 10 | const pathFunc = props.pathFunc || diagonal; 11 | const d = pathFunc(props.x1, props.y1, props.x2, props.y2); 12 | return /*#__PURE__*/React.createElement("path", _extends({}, wrappedProps, { 13 | d: d 14 | })); 15 | } 16 | 17 | export { Link as default }; 18 | -------------------------------------------------------------------------------- /dist/module/components/node.js: -------------------------------------------------------------------------------- 1 | import _extends from '@babel/runtime/helpers/extends'; 2 | import React from 'react'; 3 | import wrapHandlers from '../wrapHandlers.js'; 4 | 5 | function Node(props) { 6 | function getTransform() { 7 | return `translate(${props.x}, ${props.y})`; 8 | } 9 | let offset = 0.5; 10 | let nodePropsWithDefaults = props.nodeProps; 11 | switch (props.shape) { 12 | case 'circle': 13 | nodePropsWithDefaults = { 14 | r: 5, 15 | ...nodePropsWithDefaults 16 | }; 17 | offset += nodePropsWithDefaults.r; 18 | break; 19 | case 'image': 20 | case 'rect': 21 | nodePropsWithDefaults = { 22 | height: 10, 23 | width: 10, 24 | ...nodePropsWithDefaults 25 | }; 26 | nodePropsWithDefaults = { 27 | x: -nodePropsWithDefaults.width / 2, 28 | y: -nodePropsWithDefaults.height / 2, 29 | ...nodePropsWithDefaults 30 | }; 31 | offset += nodePropsWithDefaults.width / 2; 32 | break; 33 | } 34 | if (props.direction === 'rtl') { 35 | offset = -offset; 36 | } 37 | const wrappedNodeProps = wrapHandlers(nodePropsWithDefaults, props[props.keyProp]); 38 | const wrappedGProps = wrapHandlers(props.gProps, props[props.keyProp]); 39 | const wrappedTextProps = wrapHandlers(props.textProps, props[props.keyProp]); 40 | const label = typeof props[props.labelProp] === 'string' ? /*#__PURE__*/React.createElement("text", _extends({ 41 | dx: offset, 42 | dy: 5 43 | }, wrappedTextProps), props[props.labelProp]) : /*#__PURE__*/React.createElement("g", _extends({ 44 | transform: `translate(${offset}, 5)` 45 | }, wrappedTextProps), props[props.labelProp]); 46 | return /*#__PURE__*/React.createElement("g", _extends({}, wrappedGProps, { 47 | transform: getTransform(), 48 | direction: props.direction === 'rtl' ? 'rtl' : null 49 | }), /*#__PURE__*/React.createElement(props.shape, wrappedNodeProps), label); 50 | } 51 | 52 | export { Node as default }; 53 | -------------------------------------------------------------------------------- /dist/module/components/tree.js: -------------------------------------------------------------------------------- 1 | import _extends from '@babel/runtime/helpers/extends'; 2 | import React from 'react'; 3 | import getTreeData from '../d3.js'; 4 | import Container from './container.js'; 5 | 6 | function Tree(props) { 7 | const propsWithDefaults = { 8 | direction: 'ltr', 9 | getChildren: n => n.children, 10 | keyProp: 'name', 11 | labelProp: 'name', 12 | nodeShape: 'circle', 13 | nodeProps: {}, 14 | gProps: {}, 15 | pathProps: {}, 16 | svgProps: {}, 17 | textProps: {}, 18 | ...props 19 | }; 20 | return /*#__PURE__*/React.createElement(Container, _extends({ 21 | getChildren: propsWithDefaults.getChildren, 22 | direction: propsWithDefaults.direction, 23 | height: propsWithDefaults.height, 24 | keyProp: propsWithDefaults.keyProp, 25 | labelProp: propsWithDefaults.labelProp, 26 | nodeShape: propsWithDefaults.nodeShape, 27 | nodeProps: propsWithDefaults.nodeProps, 28 | pathFunc: propsWithDefaults.pathFunc, 29 | width: propsWithDefaults.width, 30 | gProps: { 31 | className: 'node', 32 | ...propsWithDefaults.gProps 33 | }, 34 | pathProps: { 35 | className: 'link', 36 | ...propsWithDefaults.pathProps 37 | }, 38 | svgProps: propsWithDefaults.svgProps, 39 | textProps: propsWithDefaults.textProps 40 | }, getTreeData(propsWithDefaults)), propsWithDefaults.children); 41 | } 42 | 43 | export { Tree as default }; 44 | -------------------------------------------------------------------------------- /dist/module/d3.js: -------------------------------------------------------------------------------- 1 | import { hierarchy, tree } from 'd3-hierarchy'; 2 | 3 | function getTreeData(props) { 4 | const margins = props.margins || { 5 | bottom: 10, 6 | left: props.direction !== 'rtl' ? 20 : 150, 7 | right: props.direction !== 'rtl' ? 150 : 20, 8 | top: 10 9 | }; 10 | const contentWidth = props.width - margins.left - margins.right; 11 | const contentHeight = props.height - margins.top - margins.bottom; 12 | const data = hierarchy(props.data, props.getChildren); 13 | const root = tree().size([contentHeight, contentWidth])(data); 14 | 15 | // d3 gives us a top to down tree, but we will display it left to right/right to left, so x and y need to be swapped 16 | const links = root.links().map(link => ({ 17 | ...link, 18 | source: { 19 | ...link.source, 20 | x: props.direction !== 'rtl' ? link.source.y : contentWidth - link.source.y, 21 | y: link.source.x 22 | }, 23 | target: { 24 | ...link.target, 25 | x: props.direction !== 'rtl' ? link.target.y : contentWidth - link.target.y, 26 | y: link.target.x 27 | } 28 | })); 29 | const nodes = root.descendants().map(node => ({ 30 | ...node, 31 | x: props.direction !== 'rtl' ? node.y : contentWidth - node.y, 32 | y: node.x 33 | })); 34 | return { 35 | links, 36 | margins, 37 | nodes 38 | }; 39 | } 40 | 41 | export { getTreeData as default }; 42 | -------------------------------------------------------------------------------- /dist/module/index.js: -------------------------------------------------------------------------------- 1 | export { default as AnimatedTree } from './components/animatedTree.js'; 2 | export { default as Tree } from './components/tree.js'; 3 | -------------------------------------------------------------------------------- /dist/module/wrapHandlers.js: -------------------------------------------------------------------------------- 1 | const regex = /on[A-Z]/; 2 | function wrapper(func, args) { 3 | return event => func(event, ...args); 4 | } 5 | 6 | // Wraps any event handlers passed in as props with a function that passes additional arguments 7 | function wrapHandlers(props, ...args) { 8 | const handlers = Object.keys(props).filter(propName => regex.test(propName) && typeof props[propName] === 'function'); 9 | const wrappedHandlers = handlers.reduce((acc, handler) => { 10 | acc[handler] = wrapper(props[handler], args); 11 | return acc; 12 | }, {}); 13 | return { 14 | ...props, 15 | ...wrappedHandlers 16 | }; 17 | } 18 | 19 | export { wrapHandlers as default }; 20 | -------------------------------------------------------------------------------- /dist/style.css: -------------------------------------------------------------------------------- 1 | .node circle, .node rect { 2 | fill: white; 3 | stroke: black; 4 | } 5 | 6 | path.link { 7 | fill: none; 8 | stroke: black; 9 | } -------------------------------------------------------------------------------- /dist/style.min.css: -------------------------------------------------------------------------------- 1 | .node circle,.node rect{fill:#fff;stroke:#000}path.link{fill:none;stroke:#000} -------------------------------------------------------------------------------- /docs/294.6a3eb3eb.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkreact_tree_graph=self.webpackChunkreact_tree_graph||[]).push([[294],{"./node_modules/@mdx-js/react/index.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{MDXContext:()=>_lib_index_js__WEBPACK_IMPORTED_MODULE_0__.BN,MDXProvider:()=>_lib_index_js__WEBPACK_IMPORTED_MODULE_0__.xA,useMDXComponents:()=>_lib_index_js__WEBPACK_IMPORTED_MODULE_0__.RP,withMDXComponents:()=>_lib_index_js__WEBPACK_IMPORTED_MODULE_0__.gz});var _lib_index_js__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/@mdx-js/react/lib/index.js")},"./node_modules/@mdx-js/react/lib/index.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.d(__webpack_exports__,{BN:()=>MDXContext,RP:()=>useMDXComponents,gz:()=>withMDXComponents,xA:()=>MDXProvider});var react__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/react/index.js");const MDXContext=react__WEBPACK_IMPORTED_MODULE_0__.createContext({});function withMDXComponents(Component){return function boundMDXComponent(props){const allComponents=useMDXComponents(props.components);return react__WEBPACK_IMPORTED_MODULE_0__.createElement(Component,{...props,allComponents})}}function useMDXComponents(components){const contextComponents=react__WEBPACK_IMPORTED_MODULE_0__.useContext(MDXContext);return react__WEBPACK_IMPORTED_MODULE_0__.useMemo((()=>"function"==typeof components?components(contextComponents):{...contextComponents,...components}),[contextComponents,components])}const emptyObject={};function MDXProvider({components,children,disableParentContext}){let allComponents;return allComponents=disableParentContext?"function"==typeof components?components({}):components||emptyObject:useMDXComponents(components),react__WEBPACK_IMPORTED_MODULE_0__.createElement(MDXContext.Provider,{value:allComponents},children)}}}]); -------------------------------------------------------------------------------- /docs/3.b80f77cd.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkreact_tree_graph=self.webpackChunkreact_tree_graph||[]).push([[3],{"./node_modules/@storybook/addon-docs/dist/DocsRenderer-NNNQARDV.mjs":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.d(__webpack_exports__,{DocsRenderer:()=>DocsRenderer});var react=__webpack_require__("./node_modules/react/index.js"),react_18=__webpack_require__("./node_modules/@storybook/react-dom-shim/dist/react-18.mjs"),dist=__webpack_require__("./node_modules/@storybook/blocks/dist/index.mjs"),defaultComponents={code:dist.XA,a:dist.zE,...dist.Sw},ErrorBoundary=class extends react.Component{constructor(){super(...arguments),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(err){let{showException}=this.props;showException(err)}render(){let{hasError}=this.state,{children}=this.props;return hasError?null:react.createElement(react.Fragment,null,children)}},DocsRenderer=class{constructor(){this.render=async(context,docsParameter,element)=>{let components={...defaultComponents,...docsParameter?.components},TDocs=dist.kQ;return new Promise(((resolve,reject)=>{__webpack_require__.e(294).then(__webpack_require__.bind(__webpack_require__,"./node_modules/@mdx-js/react/index.js")).then((({MDXProvider})=>(0,react_18.d)(react.createElement(ErrorBoundary,{showException:reject,key:Math.random()},react.createElement(MDXProvider,{components},react.createElement(TDocs,{context,docsParameter}))),element))).then((()=>resolve()))}))},this.unmount=element=>{(0,react_18.H)(element)}}}}}]); -------------------------------------------------------------------------------- /docs/388.bebd3fb0.iframe.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * The buffer module from node.js, for the browser. 3 | * 4 | * @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org> 5 | * @license MIT 6 | */ 7 | 8 | /*! 9 | * is-plain-object <https://github.com/jonschlinkert/is-plain-object> 10 | * 11 | * Copyright (c) 2014-2017, Jon Schlinkert. 12 | * Released under the MIT License. 13 | */ 14 | 15 | /*! 16 | * isobject <https://github.com/jonschlinkert/isobject> 17 | * 18 | * Copyright (c) 2014-2017, Jon Schlinkert. 19 | * Released under the MIT License. 20 | */ 21 | 22 | /** 23 | * @license 24 | * Lodash (Custom Build) <https://lodash.com/> 25 | * Build: `lodash modularize exports="es" -o ./` 26 | * Copyright OpenJS Foundation and other contributors <https://openjsf.org/> 27 | * Released under MIT license <https://lodash.com/license> 28 | * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE> 29 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 30 | */ 31 | 32 | /** 33 | * @license React 34 | * react-dom.production.min.js 35 | * 36 | * Copyright (c) Facebook, Inc. and its affiliates. 37 | * 38 | * This source code is licensed under the MIT license found in the 39 | * LICENSE file in the root directory of this source tree. 40 | */ 41 | 42 | /** 43 | * @license React 44 | * react-is.production.min.js 45 | * 46 | * Copyright (c) Facebook, Inc. and its affiliates. 47 | * 48 | * This source code is licensed under the MIT license found in the 49 | * LICENSE file in the root directory of this source tree. 50 | */ 51 | 52 | /** 53 | * @license React 54 | * react-jsx-runtime.production.min.js 55 | * 56 | * Copyright (c) Facebook, Inc. and its affiliates. 57 | * 58 | * This source code is licensed under the MIT license found in the 59 | * LICENSE file in the root directory of this source tree. 60 | */ 61 | 62 | /** 63 | * @license React 64 | * react.production.min.js 65 | * 66 | * Copyright (c) Facebook, Inc. and its affiliates. 67 | * 68 | * This source code is licensed under the MIT license found in the 69 | * LICENSE file in the root directory of this source tree. 70 | */ 71 | 72 | /** 73 | * @license React 74 | * scheduler.production.min.js 75 | * 76 | * Copyright (c) Facebook, Inc. and its affiliates. 77 | * 78 | * This source code is licensed under the MIT license found in the 79 | * LICENSE file in the root directory of this source tree. 80 | */ 81 | -------------------------------------------------------------------------------- /docs/421.a56bbb85.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkreact_tree_graph=self.webpackChunkreact_tree_graph||[]).push([[421],{"./node_modules/@storybook/components/dist/syntaxhighlighter-MJWPISIS.mjs":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{SyntaxHighlighter:()=>_chunk_VZ2J7KYM_mjs__WEBPACK_IMPORTED_MODULE_0__.bF,createCopyToClipboardFunction:()=>_chunk_VZ2J7KYM_mjs__WEBPACK_IMPORTED_MODULE_0__.zH,default:()=>_chunk_VZ2J7KYM_mjs__WEBPACK_IMPORTED_MODULE_0__.L0});var _chunk_VZ2J7KYM_mjs__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/@storybook/components/dist/chunk-VZ2J7KYM.mjs")}}]); -------------------------------------------------------------------------------- /docs/647.43ba6e7d.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkreact_tree_graph=self.webpackChunkreact_tree_graph||[]).push([[647],{"./node_modules/@storybook/components/dist/WithTooltip-V3YHNWJZ.mjs":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{WithToolTipState:()=>_chunk_NE5YGCQB_mjs__WEBPACK_IMPORTED_MODULE_0__.vb,WithTooltip:()=>_chunk_NE5YGCQB_mjs__WEBPACK_IMPORTED_MODULE_0__.vb,WithTooltipPure:()=>_chunk_NE5YGCQB_mjs__WEBPACK_IMPORTED_MODULE_0__.o4});var _chunk_NE5YGCQB_mjs__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/@storybook/components/dist/chunk-NE5YGCQB.mjs")}}]); -------------------------------------------------------------------------------- /docs/animatedTree-stories.d0248ab8.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkreact_tree_graph=self.webpackChunkreact_tree_graph||[]).push([[372],{"./.storybook/stories/animatedTree.stories.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{Animations:()=>Animations,__namedExportsOrder:()=>__namedExportsOrder,default:()=>animatedTree_stories});var react=__webpack_require__("./node_modules/react/index.js"),quad=__webpack_require__("./node_modules/d3-ease/src/quad.js"),d3=__webpack_require__("./src/d3.js"),container=__webpack_require__("./src/components/container.js"),jsx_runtime=__webpack_require__("./node_modules/react/jsx-runtime.js");function Animated(props){const initialX=props.nodes[0].x,initialY=props.nodes[0].y,[state,setState]=(0,react.useState)({nodes:props.nodes.map((n=>({...n,x:initialX,y:initialY}))),links:props.links.map((l=>({source:{...l.source,x:initialX,y:initialY},target:{...l.target,x:initialX,y:initialY}})))}),[animation,setAnimation]=(0,react.useState)(null);function getClosestAncestor(node,stateWithNode,stateWithoutNode){let oldParent=node;for(;oldParent;){let newParent=stateWithoutNode.nodes.find((n=>areNodesSame(oldParent,n)));if(newParent)return newParent;oldParent=stateWithNode.nodes.find((n=>(props.getChildren(n)||[]).some((c=>areNodesSame(oldParent,c)))))}return stateWithoutNode.nodes[0]}function areNodesSame(a,b){return a.data[props.keyProp]===b.data[props.keyProp]}function areLinksSame(a,b){return a.source.data[props.keyProp]===b.source.data[props.keyProp]&&a.target.data[props.keyProp]===b.target.data[props.keyProp]}function calculateNewValue(start,end,interval){return start+(end-start)*props.easing(interval)}return(0,react.useEffect)((function animate(){clearInterval(animation);let counter=0;const animationContext=function getAnimationContext(initialState,newState){const addedNodes=newState.nodes.filter((n1=>initialState.nodes.every((n2=>!areNodesSame(n1,n2))))).map((n1=>({base:n1,old:getClosestAncestor(n1,newState,initialState),new:n1}))),changedNodes=newState.nodes.filter((n1=>initialState.nodes.some((n2=>areNodesSame(n1,n2))))).map((n1=>({base:n1,old:initialState.nodes.find((n2=>areNodesSame(n1,n2))),new:n1}))),removedNodes=initialState.nodes.filter((n1=>newState.nodes.every((n2=>!areNodesSame(n1,n2))))).map((n1=>({base:n1,old:n1,new:getClosestAncestor(n1,initialState,newState)}))),addedLinks=newState.links.filter((l1=>initialState.links.every((l2=>!areLinksSame(l1,l2))))).map((l1=>({base:l1,old:getClosestAncestor(l1.target,newState,initialState),new:l1}))),changedLinks=newState.links.filter((l1=>initialState.links.some((l2=>areLinksSame(l1,l2))))).map((l1=>({base:l1,old:initialState.links.find((l2=>areLinksSame(l1,l2))),new:l1}))),removedLinks=initialState.links.filter((l1=>newState.links.every((l2=>!areLinksSame(l1,l2))))).map((l1=>({base:l1,old:l1,new:getClosestAncestor(l1.target,initialState,newState)})));return{nodes:changedNodes.concat(addedNodes).concat(removedNodes),links:changedLinks.concat(addedLinks).concat(removedLinks)}}(state,props),interval=setInterval((()=>{if(counter++,counter===props.steps)return clearInterval(interval),void setState({nodes:props.nodes,links:props.links});setState(function calculateNewState(animationContext,interval){return{nodes:animationContext.nodes.map((n=>function calculateNodePosition(node,start,end,interval){return{...node,x:calculateNewValue(start.x,end.x,interval),y:calculateNewValue(start.y,end.y,interval)}}(n.base,n.old,n.new,interval))),links:animationContext.links.map((l=>function calculateLinkPosition(link,start,end,interval){return{source:{...link.source,x:calculateNewValue(start.source?start.source.x:start.x,end.source?end.source.x:end.x,interval),y:calculateNewValue(start.source?start.source.y:start.y,end.source?end.source.y:end.y,interval)},target:{...link.target,x:calculateNewValue(start.target?start.target.x:start.x,end.target?end.target.x:end.x,interval),y:calculateNewValue(start.target?start.target.y:start.y,end.target?end.target.y:end.y,interval)}}}(l.base,l.old,l.new,interval)))}}(animationContext,counter/props.steps))}),props.duration/props.steps);return setAnimation(interval),()=>clearInterval(animation)}),[props.nodes,props.links]),(0,jsx_runtime.jsx)(container.A,{...props,...state})}function AnimatedTree(props){const propsWithDefaults={direction:"ltr",duration:500,easing:quad.yv,getChildren:n=>n.children,steps:20,keyProp:"name",labelProp:"name",nodeShape:"circle",nodeProps:{},gProps:{},pathProps:{},svgProps:{},textProps:{},...props};return(0,jsx_runtime.jsx)(Animated,{duration:propsWithDefaults.duration,easing:propsWithDefaults.easing,getChildren:propsWithDefaults.getChildren,direction:propsWithDefaults.direction,height:propsWithDefaults.height,keyProp:propsWithDefaults.keyProp,labelProp:propsWithDefaults.labelProp,nodeShape:propsWithDefaults.nodeShape,nodeProps:propsWithDefaults.nodeProps,pathFunc:propsWithDefaults.pathFunc,steps:propsWithDefaults.steps,width:propsWithDefaults.width,gProps:{className:"node",...propsWithDefaults.gProps},pathProps:{className:"link",...propsWithDefaults.pathProps},svgProps:propsWithDefaults.svgProps,textProps:propsWithDefaults.textProps,...(0,d3.A)(propsWithDefaults),children:propsWithDefaults.children})}Animated.displayName="Animated",Animated.__docgenInfo={description:"",methods:[],displayName:"Animated"},AnimatedTree.displayName="AnimatedTree",AnimatedTree.__docgenInfo={description:"",methods:[],displayName:"AnimatedTree"};const animatedTree_stories={title:"AnimatedTree/Animations",component:AnimatedTree,argTypes:__webpack_require__("./.storybook/stories/argTypes.js").z,parameters:{docs:{description:{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."}}}},order=[0,1,0,2],data=[{name:"Parent",children:[{name:"Child One"},{name:"Child Two"},{name:"Child Three",children:[{name:"Grandchild One"},{name:"Grandchild Two"}]}]},{name:"Child Three",children:[{name:"Grandchild One"},{name:"Grandchild Two"}]},{name:"Parent",children:[{name:"Child One"},{name:"Child Two"}]}],Animations={args:{height:400,width:600},parameters:{controls:{include:["duration","easing","steps"]}},render:args=>{const[position,setPosition]=(0,react.useState)(0);return(0,react.useEffect)((()=>{setTimeout((()=>setPosition(position>=order.length-1?0:position+1)),2e3)})),(0,jsx_runtime.jsx)(AnimatedTree,{data:data[order[position]],...args})}};Animations.parameters={...Animations.parameters,docs:{...Animations.parameters?.docs,source:{originalSource:"{\n args: {\n height: 400,\n width: 600\n },\n parameters: {\n controls: {\n include: ['duration', 'easing', 'steps']\n }\n },\n render: args => {\n const [position, setPosition] = useState(0);\n useEffect(() => {\n setTimeout(() => {\n if (position >= order.length - 1) {\n return setPosition(0);\n }\n return setPosition(position + 1);\n }, 2000);\n });\n return <AnimatedTree data={data[order[position]]} {...args} />;\n }\n}",...Animations.parameters?.docs?.source}}};const __namedExportsOrder=["Animations"]}}]); -------------------------------------------------------------------------------- /docs/disc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpb12/react-tree-graph/0a5630c8295701a10ef6ae6064fc98806d6cdc6f/docs/disc.png -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpb12/react-tree-graph/0a5630c8295701a10ef6ae6064fc98806d6cdc6f/docs/favicon.ico -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="164" height="164"> 2 | <svg width="164" height="164" viewBox="0 0 164 164" fill="none" xmlns="http://www.w3.org/2000/svg"> 3 | <path d="M22.467 147.762 17.5 15.402a8.062 8.062 0 0 1 7.553-8.35L137.637.016a8.061 8.061 0 0 1 8.565 8.047v144.23a8.063 8.063 0 0 1-8.424 8.054l-107.615-4.833a8.062 8.062 0 0 1-7.695-7.752Z" fill="#FF4785"></path> 4 | <path fill-rule="evenodd" clip-rule="evenodd" d="m128.785.57-15.495.968-.755 18.172a1.203 1.203 0 0 0 1.928 1.008l7.06-5.354 5.962 4.697a1.202 1.202 0 0 0 1.946-.987L128.785.569Zm-12.059 60.856c-2.836 2.203-23.965 3.707-23.965.57.447-11.969-4.912-12.494-7.889-12.494-2.828 0-7.59.855-7.59 7.267 0 6.534 6.96 10.223 15.13 14.553 11.607 6.15 25.654 13.594 25.654 32.326 0 17.953-14.588 27.871-33.194 27.871-19.201 0-35.981-7.769-34.086-34.702.744-3.163 25.156-2.411 25.156 0-.298 11.114 2.232 14.383 8.633 14.383 4.912 0 7.144-2.708 7.144-7.267 0-6.9-7.252-10.973-15.595-15.657C64.827 81.933 51.53 74.468 51.53 57.34c0-17.098 11.76-28.497 32.747-28.497 20.988 0 32.449 11.224 32.449 32.584Z" fill="#fff"></path> 5 | </svg> 6 | <style>@media (prefers-color-scheme: light) { :root { filter: none; } }</style> 7 | </svg> -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | 6 | <title>@storybook/cli - Storybook 7 | 8 | 9 | 10 | 11 | 12 | 19 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 44 | 56 | 57 | 62 | 63 | 64 | 65 | 66 |
67 | 68 | 69 | 106 | 107 | 108 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/index.json: -------------------------------------------------------------------------------- 1 | {"v":4,"entries":{"introduction--docs":{"id":"introduction--docs","title":"Introduction","name":"Docs","importPath":"./.storybook/stories/intro.stories.mdx","type":"docs","tags":["stories-mdx","stories-mdx-docsOnly","docs"],"storiesImports":[]},"tree--docs":{"id":"tree--docs","title":"Tree","name":"Docs","importPath":"./.storybook/stories/tree.stories.js","type":"docs","tags":["docs","autodocs"],"storiesImports":[]},"tree--simple":{"type":"story","id":"tree--simple","name":"Simple","title":"Tree","importPath":"./.storybook/stories/tree.stories.js","tags":["story"]},"tree--events":{"type":"story","id":"tree--events","name":"Events","title":"Tree","importPath":"./.storybook/stories/tree.stories.js","tags":["story"]},"tree--custom-children":{"type":"story","id":"tree--custom-children","name":"Custom Children","title":"Tree","importPath":"./.storybook/stories/tree.stories.js","tags":["story"]},"tree--custom-paths":{"type":"story","id":"tree--custom-paths","name":"Custom Paths","title":"Tree","importPath":"./.storybook/stories/tree.stories.js","tags":["story"]},"tree--right-to-left":{"type":"story","id":"tree--right-to-left","name":"Right To Left","title":"Tree","importPath":"./.storybook/stories/tree.stories.js","tags":["story"]},"tree--transformations":{"type":"story","id":"tree--transformations","name":"Transformations","title":"Tree","importPath":"./.storybook/stories/tree.stories.js","tags":["story"]},"tree--custom-styles":{"type":"story","id":"tree--custom-styles","name":"Custom Styles","title":"Tree","importPath":"./.storybook/stories/tree.stories.js","tags":["story"]},"tree-labels--docs":{"id":"tree-labels--docs","title":"Tree/Labels","name":"Docs","importPath":"./.storybook/stories/labels.stories.js","type":"docs","tags":["docs","autodocs"],"storiesImports":[]},"tree-labels--duplicate":{"type":"story","id":"tree-labels--duplicate","name":"Duplicate","title":"Tree/Labels","importPath":"./.storybook/stories/labels.stories.js","tags":["story"]},"tree-labels--jsx":{"type":"story","id":"tree-labels--jsx","name":"JSX","title":"Tree/Labels","importPath":"./.storybook/stories/labels.stories.js","tags":["story"]},"tree-nodes--docs":{"id":"tree-nodes--docs","title":"Tree/Nodes","name":"Docs","importPath":"./.storybook/stories/nodes.stories.js","type":"docs","tags":["docs","autodocs"],"storiesImports":[]},"tree-nodes--rectangular-nodes":{"type":"story","id":"tree-nodes--rectangular-nodes","name":"Rectangular Nodes","title":"Tree/Nodes","importPath":"./.storybook/stories/nodes.stories.js","tags":["story"]},"tree-nodes--polygon-nodes":{"type":"story","id":"tree-nodes--polygon-nodes","name":"Polygon Nodes","title":"Tree/Nodes","importPath":"./.storybook/stories/nodes.stories.js","tags":["story"]},"tree-nodes--image-nodes":{"type":"story","id":"tree-nodes--image-nodes","name":"Image Nodes","title":"Tree/Nodes","importPath":"./.storybook/stories/nodes.stories.js","tags":["story"]},"tree-nodes--custom-node-props":{"type":"story","id":"tree-nodes--custom-node-props","name":"Custom Node Props","title":"Tree/Nodes","importPath":"./.storybook/stories/nodes.stories.js","tags":["story"]},"animatedtree-animations--docs":{"id":"animatedtree-animations--docs","title":"AnimatedTree/Animations","name":"Docs","importPath":"./.storybook/stories/animatedTree.stories.js","type":"docs","tags":["docs","autodocs"],"storiesImports":[]},"animatedtree-animations--animations":{"type":"story","id":"animatedtree-animations--animations","name":"Animations","title":"AnimatedTree/Animations","importPath":"./.storybook/stories/animatedTree.stories.js","tags":["story"]}}} 2 | -------------------------------------------------------------------------------- /docs/intro-stories-mdx.d24d5b6c.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkreact_tree_graph=self.webpackChunkreact_tree_graph||[]).push([[553],{"./node_modules/@mdx-js/react/lib/index.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.d(__webpack_exports__,{BN:()=>MDXContext,RP:()=>useMDXComponents,gz:()=>withMDXComponents,xA:()=>MDXProvider});var react__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/react/index.js");const MDXContext=react__WEBPACK_IMPORTED_MODULE_0__.createContext({});function withMDXComponents(Component){return function boundMDXComponent(props){const allComponents=useMDXComponents(props.components);return react__WEBPACK_IMPORTED_MODULE_0__.createElement(Component,{...props,allComponents})}}function useMDXComponents(components){const contextComponents=react__WEBPACK_IMPORTED_MODULE_0__.useContext(MDXContext);return react__WEBPACK_IMPORTED_MODULE_0__.useMemo((()=>"function"==typeof components?components(contextComponents):{...contextComponents,...components}),[contextComponents,components])}const emptyObject={};function MDXProvider({components,children,disableParentContext}){let allComponents;return allComponents=disableParentContext?"function"==typeof components?components({}):components||emptyObject:useMDXComponents(components),react__WEBPACK_IMPORTED_MODULE_0__.createElement(MDXContext.Provider,{value:allComponents},children)}},"./.storybook/stories/intro.stories.mdx":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{__namedExportsOrder:()=>__namedExportsOrder,__page:()=>__page,default:()=>__WEBPACK_DEFAULT_EXPORT__});__webpack_require__("./node_modules/react/index.js");var C_Git_react_tree_graph_node_modules_storybook_addon_docs_dist_shims_mdx_react_shim__WEBPACK_IMPORTED_MODULE_2__=__webpack_require__("./node_modules/@mdx-js/react/lib/index.js"),_storybook_blocks__WEBPACK_IMPORTED_MODULE_3__=__webpack_require__("./node_modules/@storybook/blocks/dist/index.mjs"),react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__("./node_modules/react/jsx-runtime.js");function _createMdxContent(props){const _components=Object.assign({h1:"h1",a:"a",img:"img",p:"p",h2:"h2",pre:"pre",code:"code"},(0,C_Git_react_tree_graph_node_modules_storybook_addon_docs_dist_shims_mdx_react_shim__WEBPACK_IMPORTED_MODULE_2__.RP)(),props.components);return(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxs)(react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.Fragment,{children:[(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_storybook_blocks__WEBPACK_IMPORTED_MODULE_3__.W8,{title:"Introduction"}),"\n",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxs)(_components.h1,{id:"react-tree-graph-github",children:["react-tree-graph ",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.a,{href:"https://github.com/jpb12/react-tree-graph",target:"_blank",rel:"nofollow noopener noreferrer",children:(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.img,{src:"https://img.shields.io/github/stars/jpb12/react-tree-graph?style=social",alt:"Github"})})]}),"\n",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxs)(_components.p,{children:[(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.a,{href:"https://github.com/jpb12/react-tree-graph/actions/workflows/build.yml?query=branch%3Amaster",target:"_blank",rel:"nofollow noopener noreferrer",children:(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.img,{src:"https://img.shields.io/github/actions/workflow/status/jpb12/react-tree-graph/build.yml",alt:"Build Status"})})," ",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.a,{href:"https://coveralls.io/github/jpb12/react-tree-graph?branch=master",target:"_blank",rel:"nofollow noopener noreferrer",children:(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.img,{src:"https://coveralls.io/repos/github/jpb12/react-tree-graph/badge.svg?branch=master",alt:"Coverage Status"})})," ",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.a,{href:"https://www.npmjs.com/package/react-tree-graph",target:"_blank",rel:"nofollow noopener noreferrer",children:(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.img,{src:"https://img.shields.io/npm/v/react-tree-graph.svg",alt:"npm version"})})," ",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.a,{href:"https://www.npmjs.com/package/react-tree-graph",target:"_blank",rel:"nofollow noopener noreferrer",children:(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.img,{src:"https://img.shields.io/npm/dt/react-tree-graph.svg",alt:"npm"})})," ",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.a,{href:"https://bundlephobia.com/result?p=react-tree-graph",target:"_blank",rel:"nofollow noopener noreferrer",children:(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.img,{src:"https://img.shields.io/bundlephobia/minzip/react-tree-graph",alt:"bundle size"})})," ",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.a,{href:"https://github.com/jpb12/react-tree-graph/blob/master/LICENSE",target:"_blank",rel:"nofollow noopener noreferrer",children:(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.img,{src:"https://img.shields.io/npm/l/react-tree-graph",alt:"license"})})]}),"\n",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.p,{children:"A simple react component which renders data as a tree using svg."}),"\n",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxs)(_components.p,{children:["The source code for these examples can be found on ",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.a,{href:"https://github.com/jpb12/react-tree-graph/tree/master/.storybook/stories",target:"_blank",rel:"nofollow noopener noreferrer",children:"github"}),"."]}),"\n",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.h2,{id:"installation",children:"Installation"}),"\n",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.pre,{children:(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.code,{className:"language-sh",children:"npm install react-tree-graph --save\n"})}),"\n",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.h2,{id:"usage",children:"Usage"}),"\n",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.pre,{children:(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.code,{className:"language-javascript",children:"import { Tree } from 'react-tree-graph';\r\n\r\nconst data = {\r\n\tname: 'Parent',\r\n\tchildren: [{\r\n\t\tname: 'Child One'\r\n\t}, {\r\n\t\tname: 'Child Two'\r\n\t}]\r\n};\r\n\r\n);\r\n\r\nimport { AnimatedTree } from 'react-tree-graph';\r\n\r\n);\n"})}),"\n",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxs)(_components.p,{children:["If you are using webpack, and have ",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.a,{href:"https://www.npmjs.com/package/css-loader",target:"_blank",rel:"nofollow noopener noreferrer",children:"css-loader"}),", you can include some default styles with:"]}),"\n",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.pre,{children:(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.code,{className:"language-javascript",children:"import 'react-tree-graph/dist/style.css'\n"})}),"\n",(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_components.p,{children:"Alternatively, both the JavaScript and CSS can be included directly from the dist folder with script tags."})]})}const __page=()=>{throw new Error("Docs-only story")};__page.parameters={docsOnly:!0};const componentMeta={title:"Introduction",tags:["stories-mdx"],includeStories:["__page"]};componentMeta.parameters=componentMeta.parameters||{},componentMeta.parameters.docs={...componentMeta.parameters.docs||{},page:function MDXContent(props={}){const{wrapper:MDXLayout}=Object.assign({},(0,C_Git_react_tree_graph_node_modules_storybook_addon_docs_dist_shims_mdx_react_shim__WEBPACK_IMPORTED_MODULE_2__.RP)(),props.components);return MDXLayout?(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(MDXLayout,{...props,children:(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_createMdxContent,{...props})}):_createMdxContent(props)}};const __WEBPACK_DEFAULT_EXPORT__=componentMeta,__namedExportsOrder=["__page"]}}]); -------------------------------------------------------------------------------- /docs/labels-stories.ebb5e805.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkreact_tree_graph=self.webpackChunkreact_tree_graph||[]).push([[230],{"./.storybook/stories/labels.stories.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{Duplicate:()=>Duplicate,JSX:()=>JSX,__namedExportsOrder:()=>__namedExportsOrder,default:()=>__WEBPACK_DEFAULT_EXPORT__});var _src__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__("./src/components/tree.js"),_argTypes__WEBPACK_IMPORTED_MODULE_2__=__webpack_require__("./.storybook/stories/argTypes.js"),react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/react/jsx-runtime.js");const __WEBPACK_DEFAULT_EXPORT__={title:"Tree/Labels",component:_src__WEBPACK_IMPORTED_MODULE_1__.A,argTypes:_argTypes__WEBPACK_IMPORTED_MODULE_2__.c,parameters:{docs:{description:{component:"Setting a `labelProp` allows multiple nodes to have the same label. You can also achieve the same result by setting a `keyProp` instead."}}}},Duplicate={args:{height:400,width:600,data:{name:"Parent",label:"Parent",children:[{label:"Child",name:"Child One"},{label:"Child",name:"Child Two"}]},labelProp:"label"},parameters:{controls:{include:["data","labelProp"]}}},JSX={args:{height:400,width:600,data:{name:"Parent",label:"String",children:[{label:(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)(react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.Fragment,{children:[(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("rect",{height:"18",width:"32",y:"-15"}),(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("text",{dx:"2",children:"JSX"})]}),name:"Child One"},{label:()=>(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("text",{children:"Custom component"}),name:"Child Two"}]},labelProp:"label"},parameters:{controls:{include:["data","labelProp"]},docs:{description:{story:"Setting a `labelProp` allows labels to be JSX. They must return valid SVG elements."}}}};Duplicate.parameters={...Duplicate.parameters,docs:{...Duplicate.parameters?.docs,source:{originalSource:"{\n args: {\n height: 400,\n width: 600,\n data: {\n name: 'Parent',\n label: 'Parent',\n children: [{\n label: 'Child',\n name: 'Child One'\n }, {\n label: 'Child',\n name: 'Child Two'\n }]\n },\n labelProp: 'label'\n },\n parameters: {\n controls: {\n include: ['data', 'labelProp']\n }\n }\n}",...Duplicate.parameters?.docs?.source}}},JSX.parameters={...JSX.parameters,docs:{...JSX.parameters?.docs,source:{originalSource:"{\n args: {\n height: 400,\n width: 600,\n data: {\n name: 'Parent',\n label: 'String',\n children: [{\n label: <>JSX,\n name: 'Child One'\n }, {\n label: () => Custom component,\n name: 'Child Two'\n }]\n },\n labelProp: 'label'\n },\n parameters: {\n controls: {\n include: ['data', 'labelProp']\n },\n docs: {\n description: {\n story: 'Setting a `labelProp` allows labels to be JSX. They must return valid SVG elements.'\n }\n }\n }\n}",...JSX.parameters?.docs?.source}}};const __namedExportsOrder=["Duplicate","JSX"]},"./src/components/tree.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.d(__webpack_exports__,{A:()=>Tree});__webpack_require__("./node_modules/react/index.js");var _d3__WEBPACK_IMPORTED_MODULE_3__=__webpack_require__("./src/d3.js"),_container__WEBPACK_IMPORTED_MODULE_2__=__webpack_require__("./src/components/container.js"),react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__("./node_modules/react/jsx-runtime.js");function Tree(props){const propsWithDefaults={direction:"ltr",getChildren:n=>n.children,keyProp:"name",labelProp:"name",nodeShape:"circle",nodeProps:{},gProps:{},pathProps:{},svgProps:{},textProps:{},...props};return(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(_container__WEBPACK_IMPORTED_MODULE_2__.A,{getChildren:propsWithDefaults.getChildren,direction:propsWithDefaults.direction,height:propsWithDefaults.height,keyProp:propsWithDefaults.keyProp,labelProp:propsWithDefaults.labelProp,nodeShape:propsWithDefaults.nodeShape,nodeProps:propsWithDefaults.nodeProps,pathFunc:propsWithDefaults.pathFunc,width:propsWithDefaults.width,gProps:{className:"node",...propsWithDefaults.gProps},pathProps:{className:"link",...propsWithDefaults.pathProps},svgProps:propsWithDefaults.svgProps,textProps:propsWithDefaults.textProps,...(0,_d3__WEBPACK_IMPORTED_MODULE_3__.A)(propsWithDefaults),children:propsWithDefaults.children})}Tree.displayName="Tree",Tree.__docgenInfo={description:"",methods:[],displayName:"Tree"}}}]); -------------------------------------------------------------------------------- /docs/main.056e03a3.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunkreact_tree_graph=self.webpackChunkreact_tree_graph||[]).push([[792],{"./.storybook/stories lazy recursive ^\\.\\/.*$ include: (?:[\\\\/]\\.storybook[\\\\/]stories(?:[\\\\/](?%21\\.)(?:(?:(?%21(?:^%7C[\\\\/])\\.).)*?)[\\\\/]%7C[\\\\/]%7C$)(?%21\\.)(?=.)[^\\\\/]*?\\.stories\\.(js%7Cmdx))$":(module,__unused_webpack_exports,__webpack_require__)=>{var map={"./animatedTree.stories":["./.storybook/stories/animatedTree.stories.js",955,372],"./animatedTree.stories.js":["./.storybook/stories/animatedTree.stories.js",955,372],"./intro.stories.mdx":["./.storybook/stories/intro.stories.mdx",553],"./labels.stories":["./.storybook/stories/labels.stories.js",955,230],"./labels.stories.js":["./.storybook/stories/labels.stories.js",955,230],"./nodes.stories":["./.storybook/stories/nodes.stories.js",955,342],"./nodes.stories.js":["./.storybook/stories/nodes.stories.js",955,342],"./tree.stories":["./.storybook/stories/tree.stories.js",955,627],"./tree.stories.js":["./.storybook/stories/tree.stories.js",955,627]};function webpackAsyncContext(req){if(!__webpack_require__.o(map,req))return Promise.resolve().then((()=>{var e=new Error("Cannot find module '"+req+"'");throw e.code="MODULE_NOT_FOUND",e}));var ids=map[req],id=ids[0];return Promise.all(ids.slice(1).map(__webpack_require__.e)).then((()=>__webpack_require__(id)))}webpackAsyncContext.keys=()=>Object.keys(map),webpackAsyncContext.id="./.storybook/stories lazy recursive ^\\.\\/.*$ include: (?:[\\\\/]\\.storybook[\\\\/]stories(?:[\\\\/](?%21\\.)(?:(?:(?%21(?:^%7C[\\\\/])\\.).)*?)[\\\\/]%7C[\\\\/]%7C$)(?%21\\.)(?=.)[^\\\\/]*?\\.stories\\.(js%7Cmdx))$",module.exports=webpackAsyncContext},"./storybook-config-entry.js":(__unused_webpack_module,__unused_webpack___webpack_exports__,__webpack_require__)=>{"use strict";var external_STORYBOOK_MODULE_GLOBAL_=__webpack_require__("@storybook/global"),external_STORYBOOK_MODULE_PREVIEW_API_=__webpack_require__("@storybook/preview-api"),external_STORYBOOK_MODULE_CHANNELS_=__webpack_require__("@storybook/channels");const importers=[async path=>{if(!/^\.[\\/](?:\.storybook[\\/]stories(?:[\\/](?!\.)(?:(?:(?!(?:^|[\\/])\.).)*?)[\\/]|[\\/]|$)(?!\.)(?=.)[^\\/]*?\.stories\.(js|mdx))$/.exec(path))return;const pathRemainder=path.substring(21);return __webpack_require__("./.storybook/stories lazy recursive ^\\.\\/.*$ include: (?:[\\\\/]\\.storybook[\\\\/]stories(?:[\\\\/](?%21\\.)(?:(?:(?%21(?:^%7C[\\\\/])\\.).)*?)[\\\\/]%7C[\\\\/]%7C$)(?%21\\.)(?=.)[^\\\\/]*?\\.stories\\.(js%7Cmdx))$")("./"+pathRemainder)}];const channel=(0,external_STORYBOOK_MODULE_CHANNELS_.createBrowserChannel)({page:"preview"});external_STORYBOOK_MODULE_PREVIEW_API_.addons.setChannel(channel),"DEVELOPMENT"===external_STORYBOOK_MODULE_GLOBAL_.global.CONFIG_TYPE&&(window.__STORYBOOK_SERVER_CHANNEL__=channel);const preview=new external_STORYBOOK_MODULE_PREVIEW_API_.PreviewWeb;window.__STORYBOOK_PREVIEW__=preview,window.__STORYBOOK_STORY_STORE__=preview.storyStore,window.__STORYBOOK_ADDONS_CHANNEL__=channel,window.__STORYBOOK_CLIENT_API__=new external_STORYBOOK_MODULE_PREVIEW_API_.ClientApi({storyStore:preview.storyStore}),preview.initialize({importFn:async function importFn(path){for(let i=0;iimporters[i](path),x());if(moduleExports)return moduleExports}var x},getProjectAnnotations:()=>(0,external_STORYBOOK_MODULE_PREVIEW_API_.composeConfigs)([__webpack_require__("./node_modules/@storybook/react/dist/entry-preview.mjs"),__webpack_require__("./node_modules/@storybook/react/dist/entry-preview-docs.mjs"),__webpack_require__("./node_modules/@storybook/addon-essentials/dist/docs/preview.js"),__webpack_require__("./node_modules/@storybook/addon-essentials/dist/highlight/preview.js"),__webpack_require__("./.storybook/preview.js")])})},"./.storybook/preview.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{"use strict";__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{default:()=>preview});var dist=__webpack_require__("./node_modules/@storybook/blocks/dist/index.mjs"),injectStylesIntoStyleTag=__webpack_require__("./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js"),injectStylesIntoStyleTag_default=__webpack_require__.n(injectStylesIntoStyleTag),styleDomAPI=__webpack_require__("./node_modules/style-loader/dist/runtime/styleDomAPI.js"),styleDomAPI_default=__webpack_require__.n(styleDomAPI),insertBySelector=__webpack_require__("./node_modules/style-loader/dist/runtime/insertBySelector.js"),insertBySelector_default=__webpack_require__.n(insertBySelector),setAttributesWithoutAttributes=__webpack_require__("./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js"),setAttributesWithoutAttributes_default=__webpack_require__.n(setAttributesWithoutAttributes),insertStyleElement=__webpack_require__("./node_modules/style-loader/dist/runtime/insertStyleElement.js"),insertStyleElement_default=__webpack_require__.n(insertStyleElement),styleTagTransform=__webpack_require__("./node_modules/style-loader/dist/runtime/styleTagTransform.js"),styleTagTransform_default=__webpack_require__.n(styleTagTransform),style=__webpack_require__("./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[8].use[1]!./styles/style.css"),options={};options.styleTagTransform=styleTagTransform_default(),options.setAttributes=setAttributesWithoutAttributes_default(),options.insert=insertBySelector_default().bind(null,"head"),options.domAPI=styleDomAPI_default(),options.insertStyleElement=insertStyleElement_default();injectStylesIntoStyleTag_default()(style.A,options);style.A&&style.A.locals&&style.A.locals;var jsx_runtime=__webpack_require__("./node_modules/react/jsx-runtime.js");const preview={parameters:{controls:{expanded:!0},docs:{page:()=>(0,jsx_runtime.jsxs)(jsx_runtime.Fragment,{children:[(0,jsx_runtime.jsx)(dist.hE,{}),(0,jsx_runtime.jsx)(dist.Pd,{}),(0,jsx_runtime.jsx)(dist.VY,{}),(0,jsx_runtime.jsx)(dist.Tn,{}),(0,jsx_runtime.jsx)(dist.H2,{}),(0,jsx_runtime.jsx)(dist.om,{includePrimary:!1})]})},layout:"centered",options:{storySort:{order:["Introduction","Tree","AnimatedTree"]}},viewMode:"docs"}}},"./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[8].use[1]!./styles/style.css":(module,__webpack_exports__,__webpack_require__)=>{"use strict";__webpack_require__.d(__webpack_exports__,{A:()=>__WEBPACK_DEFAULT_EXPORT__});var _node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/css-loader/dist/runtime/sourceMaps.js"),_node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default=__webpack_require__.n(_node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0__),_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__("./node_modules/css-loader/dist/runtime/api.js"),___CSS_LOADER_EXPORT___=__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__)()(_node_modules_css_loader_dist_runtime_sourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default());___CSS_LOADER_EXPORT___.push([module.id,".node circle, .node rect {\n\tfill: white;\n\tstroke: black;\n}\n\npath.link {\n\tfill: none;\n\tstroke: black;\n}","",{version:3,sources:["webpack://./styles/style.css"],names:[],mappings:"AAAA;CACC,WAAW;CACX,aAAa;AACd;;AAEA;CACC,UAAU;CACV,aAAa;AACd",sourcesContent:[".node circle, .node rect {\r\n\tfill: white;\r\n\tstroke: black;\r\n}\r\n\r\npath.link {\r\n\tfill: none;\r\n\tstroke: black;\r\n}"],sourceRoot:""}]);const __WEBPACK_DEFAULT_EXPORT__=___CSS_LOADER_EXPORT___},"./node_modules/memoizerific sync recursive":module=>{function webpackEmptyContext(req){var e=new Error("Cannot find module '"+req+"'");throw e.code="MODULE_NOT_FOUND",e}webpackEmptyContext.keys=()=>[],webpackEmptyContext.resolve=webpackEmptyContext,webpackEmptyContext.id="./node_modules/memoizerific sync recursive",module.exports=webpackEmptyContext},"@storybook/channels":module=>{"use strict";module.exports=__STORYBOOK_MODULE_CHANNELS__},"@storybook/client-logger":module=>{"use strict";module.exports=__STORYBOOK_MODULE_CLIENT_LOGGER__},"@storybook/core-events":module=>{"use strict";module.exports=__STORYBOOK_MODULE_CORE_EVENTS__},"@storybook/global":module=>{"use strict";module.exports=__STORYBOOK_MODULE_GLOBAL__},"@storybook/preview-api":module=>{"use strict";module.exports=__STORYBOOK_MODULE_PREVIEW_API__}},__webpack_require__=>{__webpack_require__.O(0,[388],(()=>{return moduleId="./storybook-config-entry.js",__webpack_require__(__webpack_require__.s=moduleId);var moduleId}));__webpack_require__.O()}]); -------------------------------------------------------------------------------- /docs/project.json: -------------------------------------------------------------------------------- 1 | {"generatedAt":1736784754951,"hasCustomBabel":false,"hasCustomWebpack":true,"hasStaticDirs":true,"hasStorybookEslint":true,"refCount":0,"packageManager":{"type":"npm","version":"10.9.2"},"preview":{"usesGlobals":false},"framework":{"name":"@storybook/react-webpack5","options":{}},"builder":"@storybook/builder-webpack5","renderer":"@storybook/react","language":"javascript","storybookPackages":{"@storybook/react-webpack5":{"version":"7.6.20"},"eslint-plugin-storybook":{"version":"0.11.2"},"storybook":{"version":"7.6.20"}},"addons":{"@storybook/addon-essentials":{"options":{"actions":false,"backgrounds":false,"measure":false,"outline":false,"viewport":false},"version":"7.6.20"}}} 2 | -------------------------------------------------------------------------------- /docs/runtime~main.fb760981.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var deferred,leafPrototypes,getProto,inProgress,__webpack_modules__={},__webpack_module_cache__={};function __webpack_require__(moduleId){var cachedModule=__webpack_module_cache__[moduleId];if(void 0!==cachedModule)return cachedModule.exports;var module=__webpack_module_cache__[moduleId]={id:moduleId,loaded:!1,exports:{}};return __webpack_modules__[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}__webpack_require__.m=__webpack_modules__,__webpack_require__.amdO={},deferred=[],__webpack_require__.O=(result,chunkIds,fn,priority)=>{if(!chunkIds){var notFulfilled=1/0;for(i=0;i=priority)&&Object.keys(__webpack_require__.O).every((key=>__webpack_require__.O[key](chunkIds[j])))?chunkIds.splice(j--,1):(fulfilled=!1,priority0&&deferred[i-1][2]>priority;i--)deferred[i]=deferred[i-1];deferred[i]=[chunkIds,fn,priority]},__webpack_require__.n=module=>{var getter=module&&module.__esModule?()=>module.default:()=>module;return __webpack_require__.d(getter,{a:getter}),getter},getProto=Object.getPrototypeOf?obj=>Object.getPrototypeOf(obj):obj=>obj.__proto__,__webpack_require__.t=function(value,mode){if(1&mode&&(value=this(value)),8&mode)return value;if("object"==typeof value&&value){if(4&mode&&value.__esModule)return value;if(16&mode&&"function"==typeof value.then)return value}var ns=Object.create(null);__webpack_require__.r(ns);var def={};leafPrototypes=leafPrototypes||[null,getProto({}),getProto([]),getProto(getProto)];for(var current=2&mode&&value;"object"==typeof current&&!~leafPrototypes.indexOf(current);current=getProto(current))Object.getOwnPropertyNames(current).forEach((key=>def[key]=()=>value[key]));return def.default=()=>value,__webpack_require__.d(ns,def),ns},__webpack_require__.d=(exports,definition)=>{for(var key in definition)__webpack_require__.o(definition,key)&&!__webpack_require__.o(exports,key)&&Object.defineProperty(exports,key,{enumerable:!0,get:definition[key]})},__webpack_require__.f={},__webpack_require__.e=chunkId=>Promise.all(Object.keys(__webpack_require__.f).reduce(((promises,key)=>(__webpack_require__.f[key](chunkId,promises),promises)),[])),__webpack_require__.u=chunkId=>(({230:"labels-stories",342:"nodes-stories",372:"animatedTree-stories",553:"intro-stories-mdx",627:"tree-stories"}[chunkId]||chunkId)+"."+{3:"b80f77cd",71:"59fa7449",230:"ebb5e805",294:"6a3eb3eb",342:"a047e23c",372:"d0248ab8",421:"a56bbb85",553:"d24d5b6c",627:"a7afaf37",647:"43ba6e7d",857:"905d42e1",955:"c734015d"}[chunkId]+".iframe.bundle.js"),__webpack_require__.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),__webpack_require__.o=(obj,prop)=>Object.prototype.hasOwnProperty.call(obj,prop),inProgress={},__webpack_require__.l=(url,done,key,chunkId)=>{if(inProgress[url])inProgress[url].push(done);else{var script,needAttach;if(void 0!==key)for(var scripts=document.getElementsByTagName("script"),i=0;i{script.onerror=script.onload=null,clearTimeout(timeout);var doneFns=inProgress[url];if(delete inProgress[url],script.parentNode&&script.parentNode.removeChild(script),doneFns&&doneFns.forEach((fn=>fn(event))),prev)return prev(event)},timeout=setTimeout(onScriptComplete.bind(null,void 0,{type:"timeout",target:script}),12e4);script.onerror=onScriptComplete.bind(null,script.onerror),script.onload=onScriptComplete.bind(null,script.onload),needAttach&&document.head.appendChild(script)}},__webpack_require__.r=exports=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(exports,"__esModule",{value:!0})},__webpack_require__.nmd=module=>(module.paths=[],module.children||(module.children=[]),module),__webpack_require__.p="",(()=>{var installedChunks={354:0};__webpack_require__.f.j=(chunkId,promises)=>{var installedChunkData=__webpack_require__.o(installedChunks,chunkId)?installedChunks[chunkId]:void 0;if(0!==installedChunkData)if(installedChunkData)promises.push(installedChunkData[2]);else if(354!=chunkId){var promise=new Promise(((resolve,reject)=>installedChunkData=installedChunks[chunkId]=[resolve,reject]));promises.push(installedChunkData[2]=promise);var url=__webpack_require__.p+__webpack_require__.u(chunkId),error=new Error;__webpack_require__.l(url,(event=>{if(__webpack_require__.o(installedChunks,chunkId)&&(0!==(installedChunkData=installedChunks[chunkId])&&(installedChunks[chunkId]=void 0),installedChunkData)){var errorType=event&&("load"===event.type?"missing":event.type),realSrc=event&&event.target&&event.target.src;error.message="Loading chunk "+chunkId+" failed.\n("+errorType+": "+realSrc+")",error.name="ChunkLoadError",error.type=errorType,error.request=realSrc,installedChunkData[1](error)}}),"chunk-"+chunkId,chunkId)}else installedChunks[chunkId]=0},__webpack_require__.O.j=chunkId=>0===installedChunks[chunkId];var webpackJsonpCallback=(parentChunkLoadingFunction,data)=>{var moduleId,chunkId,[chunkIds,moreModules,runtime]=data,i=0;if(chunkIds.some((id=>0!==installedChunks[id]))){for(moduleId in moreModules)__webpack_require__.o(moreModules,moduleId)&&(__webpack_require__.m[moduleId]=moreModules[moduleId]);if(runtime)var result=runtime(__webpack_require__)}for(parentChunkLoadingFunction&&parentChunkLoadingFunction(data);i 6 | * 7 | * Copyright (c) 2014-2017, Jon Schlinkert. 8 | * Released under the MIT License. 9 | */ 10 | /** 11 | * @license 12 | * Lodash (Custom Build) 13 | * Build: `lodash modularize exports="es" -o ./` 14 | * Copyright OpenJS Foundation and other contributors 15 | * Released under MIT license 16 | * Based on Underscore.js 1.8.3 17 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 18 | */ 19 | -------------------------------------------------------------------------------- /docs/sb-addons/essentials-toolbars-1/manager-bundle.js: -------------------------------------------------------------------------------- 1 | try{ 2 | (()=>{var l=__REACT__,{Children:le,Component:ne,Fragment:ie,Profiler:se,PureComponent:ce,StrictMode:ue,Suspense:me,__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:pe,cloneElement:de,createContext:be,createElement:Se,createFactory:Te,createRef:ye,forwardRef:_e,isValidElement:fe,lazy:Ce,memo:ve,useCallback:v,useContext:Ie,useDebugValue:Oe,useEffect:E,useImperativeHandle:xe,useLayoutEffect:Ee,useMemo:ge,useReducer:he,useRef:L,useState:R,version:ke}=__REACT__;var Pe=__STORYBOOK_API__,{ActiveTabs:Me,Consumer:Ne,ManagerContext:we,Provider:He,addons:g,combineParameters:Ve,controlOrMetaKey:De,controlOrMetaSymbol:Fe,eventMatchesShortcut:Ge,eventToShortcut:We,isMacLike:Ke,isShortcutTaken:Ye,keyToSymbol:$e,merge:ze,mockChannel:Ue,optionOrAltSymbol:je,shortcutMatchesShortcut:qe,shortcutToHumanString:Ze,types:B,useAddonState:Je,useArgTypes:Qe,useArgs:Xe,useChannel:et,useGlobalTypes:P,useGlobals:h,useParameter:tt,useSharedState:ot,useStoryPrepared:rt,useStorybookApi:M,useStorybookState:at}=__STORYBOOK_API__;var ct=__STORYBOOK_COMPONENTS__,{A:ut,ActionBar:mt,AddonPanel:pt,Badge:dt,Bar:bt,Blockquote:St,Button:Tt,ClipboardCode:yt,Code:_t,DL:ft,Div:Ct,DocumentWrapper:vt,ErrorFormatter:It,FlexBar:Ot,Form:xt,H1:Et,H2:gt,H3:ht,H4:kt,H5:At,H6:Lt,HR:Rt,IconButton:N,IconButtonSkeleton:Bt,Icons:k,Img:Pt,LI:Mt,Link:Nt,ListItem:wt,Loader:Ht,OL:Vt,P:Dt,Placeholder:Ft,Pre:Gt,ResetWrapper:Wt,ScrollArea:Kt,Separator:w,Spaced:Yt,Span:$t,StorybookIcon:zt,StorybookLogo:Ut,Symbols:jt,SyntaxHighlighter:qt,TT:Zt,TabBar:Jt,TabButton:Qt,TabWrapper:Xt,Table:eo,Tabs:to,TabsState:oo,TooltipLinkList:H,TooltipMessage:ro,TooltipNote:ao,UL:lo,WithTooltip:V,WithTooltipPure:no,Zoom:io,codeCommon:so,components:co,createCopyToClipboardFunction:uo,getStoryHref:mo,icons:po,interleaveSeparators:bo,nameSpaceClassNames:So,resetComponents:To,withReset:yo}=__STORYBOOK_COMPONENTS__;var G=({active:o,title:t,icon:e,description:r,onClick:a})=>l.createElement(N,{active:o,title:r,onClick:a},e&&l.createElement(k,{icon:e}),t?`\xA0${t}`:null),W=["reset"],K=o=>o.filter(t=>!W.includes(t.type)).map(t=>t.value),b="addon-toolbars",Y=async(o,t,e)=>{e&&e.next&&await o.setAddonShortcut(b,{label:e.next.label,defaultShortcut:e.next.keys,actionName:`${t}:next`,action:e.next.action}),e&&e.previous&&await o.setAddonShortcut(b,{label:e.previous.label,defaultShortcut:e.previous.keys,actionName:`${t}:previous`,action:e.previous.action}),e&&e.reset&&await o.setAddonShortcut(b,{label:e.reset.label,defaultShortcut:e.reset.keys,actionName:`${t}:reset`,action:e.reset.action})},$=o=>t=>{let{id:e,toolbar:{items:r,shortcuts:a}}=t,d=M(),[S,i]=h(),n=L([]),s=S[e],I=v(()=>{i({[e]:""})},[i]),O=v(()=>{let p=n.current,c=p.indexOf(s),m=c===p.length-1?0:c+1,T=n.current[m];i({[e]:T})},[n,s,i]),u=v(()=>{let p=n.current,c=p.indexOf(s),m=c>-1?c:0,T=m===0?p.length-1:m-1,y=n.current[T];i({[e]:y})},[n,s,i]);return E(()=>{a&&Y(d,e,{next:{...a.next,action:O},previous:{...a.previous,action:u},reset:{...a.reset,action:I}})},[d,e,a,O,u,I]),E(()=>{n.current=K(r)},[]),l.createElement(o,{cycleValues:n.current,...t})},D=({currentValue:o,items:t})=>o!=null&&t.find(e=>e.value===o&&e.type!=="reset"),z=({currentValue:o,items:t})=>{let e=D({currentValue:o,items:t});if(e)return e.icon},U=({currentValue:o,items:t})=>{let e=D({currentValue:o,items:t});if(e)return e.title},j=({left:o,right:t,title:e,value:r,icon:a,hideIcon:d,onClick:S,currentValue:i})=>{let n=a&&l.createElement(k,{style:{opacity:1},icon:a}),s={id:r??"_reset",active:i===r,right:t,title:e,left:o,onClick:S};return a&&!d&&(s.left=n),s},q=$(({id:o,name:t,description:e,toolbar:{icon:r,items:a,title:d,preventDynamicIcon:S,dynamicTitle:i}})=>{let[n,s]=h(),[I,O]=R(!1),u=n[o],p=!!u,c=r,m=d;S||(c=z({currentValue:u,items:a})||c),i&&(m=U({currentValue:u,items:a})||m),!m&&!c&&console.warn(`Toolbar '${t}' has no title or icon`);let T=v(y=>{s({[o]:y})},[u,s]);return l.createElement(V,{placement:"top",tooltip:({onHide:y})=>{let F=a.filter(({type:x})=>{let A=!0;return x==="reset"&&!u&&(A=!1),A}).map(x=>j({...x,currentValue:u,onClick:()=>{T(x.value),y()}}));return l.createElement(H,{links:F})},closeOnOutsideClick:!0,onVisibleChange:O},l.createElement(G,{active:I||p,description:e||"",icon:c,title:m||""}))}),Z={type:"item",value:""},J=(o,t)=>({...t,name:t.name||o,description:t.description||o,toolbar:{...t.toolbar,items:t.toolbar.items.map(e=>{let r=typeof e=="string"?{value:e,title:e}:e;return r.type==="reset"&&t.toolbar.icon&&(r.icon=t.toolbar.icon,r.hideIcon=!0),{...Z,...r}})}}),Q=()=>{let o=P(),t=Object.keys(o).filter(e=>!!o[e].toolbar);return t.length?l.createElement(l.Fragment,null,l.createElement(w,null),t.map(e=>{let r=J(e,o[e]);return l.createElement(q,{key:e,id:e,...r})})):null};g.register(b,()=>g.add(b,{title:b,type:B.TOOL,match:()=>!0,render:()=>l.createElement(Q,null)}));})(); 3 | }catch(e){ console.error("[Storybook] One of your manager-entries failed: " + import.meta.url, e); } 4 | -------------------------------------------------------------------------------- /docs/sb-addons/essentials-toolbars-1/manager-bundle.js.LEGAL.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpb12/react-tree-graph/0a5630c8295701a10ef6ae6064fc98806d6cdc6f/docs/sb-addons/essentials-toolbars-1/manager-bundle.js.LEGAL.txt -------------------------------------------------------------------------------- /docs/sb-addons/storybook-2/manager-bundle.js: -------------------------------------------------------------------------------- 1 | try{ 2 | (()=>{var O=__STORYBOOK_ADDONS__,{addons:a,types:d,mockChannel:h}=__STORYBOOK_ADDONS__;var S=__STORYBOOK_THEMING__,{CacheProvider:f,ClassNames:b,Global:u,ThemeProvider:x,background:y,color:C,convert:N,create:o,createCache:R,createGlobal:k,createReset:B,css:D,darken:G,ensure:K,ignoreSsrWarning:Y,isPropValid:v,jsx:P,keyframes:A,lighten:E,styled:H,themes:I,typography:M,useTheme:j,withTheme:w}=__STORYBOOK_THEMING__;a.setConfig({theme:o({base:"light",brandTitle:"react-tree-graph"})});})(); 3 | }catch(e){ console.error("[Storybook] One of your manager-entries failed: " + import.meta.url, e); } 4 | -------------------------------------------------------------------------------- /docs/sb-addons/storybook-2/manager-bundle.js.LEGAL.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpb12/react-tree-graph/0a5630c8295701a10ef6ae6064fc98806d6cdc6f/docs/sb-addons/storybook-2/manager-bundle.js.LEGAL.txt -------------------------------------------------------------------------------- /docs/sb-common-assets/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Nunito Sans'; 3 | font-style: normal; 4 | font-weight: 400; 5 | font-display: swap; 6 | src: url('./nunito-sans-regular.woff2') format('woff2'); 7 | } 8 | 9 | @font-face { 10 | font-family: 'Nunito Sans'; 11 | font-style: italic; 12 | font-weight: 400; 13 | font-display: swap; 14 | src: url('./nunito-sans-italic.woff2') format('woff2'); 15 | } 16 | 17 | @font-face { 18 | font-family: 'Nunito Sans'; 19 | font-style: normal; 20 | font-weight: 700; 21 | font-display: swap; 22 | src: url('./nunito-sans-bold.woff2') format('woff2'); 23 | } 24 | 25 | @font-face { 26 | font-family: 'Nunito Sans'; 27 | font-style: italic; 28 | font-weight: 700; 29 | font-display: swap; 30 | src: url('./nunito-sans-bold-italic.woff2') format('woff2'); 31 | } 32 | -------------------------------------------------------------------------------- /docs/sb-common-assets/nunito-sans-bold-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpb12/react-tree-graph/0a5630c8295701a10ef6ae6064fc98806d6cdc6f/docs/sb-common-assets/nunito-sans-bold-italic.woff2 -------------------------------------------------------------------------------- /docs/sb-common-assets/nunito-sans-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpb12/react-tree-graph/0a5630c8295701a10ef6ae6064fc98806d6cdc6f/docs/sb-common-assets/nunito-sans-bold.woff2 -------------------------------------------------------------------------------- /docs/sb-common-assets/nunito-sans-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpb12/react-tree-graph/0a5630c8295701a10ef6ae6064fc98806d6cdc6f/docs/sb-common-assets/nunito-sans-italic.woff2 -------------------------------------------------------------------------------- /docs/sb-common-assets/nunito-sans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpb12/react-tree-graph/0a5630c8295701a10ef6ae6064fc98806d6cdc6f/docs/sb-common-assets/nunito-sans-regular.woff2 -------------------------------------------------------------------------------- /docs/sb-manager/WithTooltip-V3YHNWJZ-MXTFSDU5.js: -------------------------------------------------------------------------------- 1 | import{WithToolTipState,WithTooltipPure}from"./chunk-7PRFHFSS.js";import"./chunk-YDUB7CS6.js";import"./chunk-ZEU7PDD3.js";export{WithToolTipState,WithToolTipState as WithTooltip,WithTooltipPure}; 2 | -------------------------------------------------------------------------------- /docs/sb-manager/chunk-ZEU7PDD3.js: -------------------------------------------------------------------------------- 1 | var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf,__hasOwnProp=Object.prototype.hasOwnProperty;var __require=(x=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(x,{get:(a,b)=>(typeof require<"u"?require:a)[b]}):x)(function(x){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+x+'" is not supported')});var __esm=(fn,res)=>function(){return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res};var __commonJS=(cb,mod)=>function(){return mod||(0,cb[__getOwnPropNames(cb)[0]])((mod={exports:{}}).exports,mod),mod.exports};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toESM=(mod,isNodeMode,target)=>(target=mod!=null?__create(__getProtoOf(mod)):{},__copyProps(isNodeMode||!mod||!mod.__esModule?__defProp(target,"default",{value:mod,enumerable:!0}):target,mod)),__toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var require_memoizerific=__commonJS({"../../node_modules/memoizerific/memoizerific.js"(exports,module){(function(f){if(typeof exports=="object"&&typeof module<"u")module.exports=f();else if(typeof define=="function"&&define.amd)define([],f);else{var g;typeof window<"u"?g=window:typeof global<"u"?g=global:typeof self<"u"?g=self:g=this,g.memoizerific=f()}})(function(){var define2,module2,exports2;return function e(t,n,r){function s(o2,u){if(!n[o2]){if(!t[o2]){var a=typeof __require=="function"&&__require;if(!u&&a)return a(o2,!0);if(i)return i(o2,!0);var f=new Error("Cannot find module '"+o2+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o2]={exports:{}};t[o2][0].call(l.exports,function(e2){var n2=t[o2][1][e2];return s(n2||e2)},l,l.exports,e,t,n,r)}return n[o2].exports}for(var i=typeof __require=="function"&&__require,o=0;o=0)return this.lastItem=this.list[index],this.list[index].val},Similar.prototype.set=function(key,val){var index;return this.lastItem&&this.isEqual(this.lastItem.key,key)?(this.lastItem.val=val,this):(index=this.indexOf(key),index>=0?(this.lastItem=this.list[index],this.list[index].val=val,this):(this.lastItem={key,val},this.list.push(this.lastItem),this.size++,this))},Similar.prototype.delete=function(key){var index;if(this.lastItem&&this.isEqual(this.lastItem.key,key)&&(this.lastItem=void 0),index=this.indexOf(key),index>=0)return this.size--,this.list.splice(index,1)[0]},Similar.prototype.has=function(key){var index;return this.lastItem&&this.isEqual(this.lastItem.key,key)?!0:(index=this.indexOf(key),index>=0?(this.lastItem=this.list[index],!0):!1)},Similar.prototype.forEach=function(callback,thisArg){var i;for(i=0;i0&&(lruPath[argsLengthMinusOne]={cacheItem:currentCache,arg:arguments[argsLengthMinusOne]},isMemoized?moveToMostRecentLru(lru,lruPath):lru.push(lruPath),lru.length>limit&&removeCachedResult(lru.shift())),memoizerific.wasMemoized=isMemoized,memoizerific.numArgs=argsLengthMinusOne+1,fnResult};return memoizerific.limit=limit,memoizerific.wasMemoized=!1,memoizerific.cache=cache,memoizerific.lru=lru,memoizerific}};function moveToMostRecentLru(lru,lruPath){var lruLen=lru.length,lruPathLen=lruPath.length,isMatch,i,ii;for(i=0;i=0&&(currentLru=removedLru[i],tmp=currentLru.cacheItem.get(currentLru.arg),!tmp||!tmp.size);i--)currentLru.cacheItem.delete(currentLru.arg)}function isEqual(val1,val2){return val1===val2||val1!==val1&&val2!==val2}},{"map-or-similar":1}]},{},[3])(3)})}});var __create2=Object.create,__defProp2=Object.defineProperty,__getOwnPropDesc2=Object.getOwnPropertyDescriptor,__getOwnPropNames2=Object.getOwnPropertyNames,__getProtoOf2=Object.getPrototypeOf,__hasOwnProp2=Object.prototype.hasOwnProperty,__commonJS2=(cb,mod)=>function(){return mod||(0,cb[__getOwnPropNames2(cb)[0]])((mod={exports:{}}).exports,mod),mod.exports},__copyProps2=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames2(from))!__hasOwnProp2.call(to,key)&&key!==except&&__defProp2(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc2(from,key))||desc.enumerable});return to},__toESM2=(mod,isNodeMode,target)=>(target=mod!=null?__create2(__getProtoOf2(mod)):{},__copyProps2(isNodeMode||!mod||!mod.__esModule?__defProp2(target,"default",{value:mod,enumerable:!0}):target,mod));export{__esm,__commonJS,__export,__toESM,__toCommonJS,require_memoizerific,__commonJS2,__toESM2}; 2 | -------------------------------------------------------------------------------- /docs/sb-manager/globals-module-info.js: -------------------------------------------------------------------------------- 1 | var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var globals_module_info_exports={};__export(globals_module_info_exports,{globalsModuleInfoMap:()=>globalsModuleInfoMap});module.exports=__toCommonJS(globals_module_info_exports);var exports_default={react:["Children","Component","Fragment","Profiler","PureComponent","StrictMode","Suspense","__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED","cloneElement","createContext","createElement","createFactory","createRef","forwardRef","isValidElement","lazy","memo","useCallback","useContext","useDebugValue","useEffect","useImperativeHandle","useLayoutEffect","useMemo","useReducer","useRef","useState","version"],"react-dom":["__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED","createPortal","findDOMNode","flushSync","hydrate","render","unmountComponentAtNode","unstable_batchedUpdates","unstable_createPortal","unstable_renderSubtreeIntoContainer","version"],"@storybook/components":["A","ActionBar","AddonPanel","Badge","Bar","Blockquote","Button","ClipboardCode","Code","DL","Div","DocumentWrapper","ErrorFormatter","FlexBar","Form","H1","H2","H3","H4","H5","H6","HR","IconButton","IconButtonSkeleton","Icons","Img","LI","Link","ListItem","Loader","OL","P","Placeholder","Pre","ResetWrapper","ScrollArea","Separator","Spaced","Span","StorybookIcon","StorybookLogo","Symbols","SyntaxHighlighter","TT","TabBar","TabButton","TabWrapper","Table","Tabs","TabsState","TooltipLinkList","TooltipMessage","TooltipNote","UL","WithTooltip","WithTooltipPure","Zoom","codeCommon","components","createCopyToClipboardFunction","getStoryHref","icons","interleaveSeparators","nameSpaceClassNames","resetComponents","withReset"],"@storybook/channels":["Channel","PostMessageTransport","WebsocketTransport","createBrowserChannel","createPostMessageChannel","createWebSocketChannel"],"@storybook/core-events":["CHANNEL_CREATED","CONFIG_ERROR","CURRENT_STORY_WAS_SET","DOCS_PREPARED","DOCS_RENDERED","FORCE_REMOUNT","FORCE_RE_RENDER","GLOBALS_UPDATED","IGNORED_EXCEPTION","NAVIGATE_URL","PLAY_FUNCTION_THREW_EXCEPTION","PRELOAD_ENTRIES","PREVIEW_BUILDER_PROGRESS","PREVIEW_KEYDOWN","REGISTER_SUBSCRIPTION","REQUEST_WHATS_NEW_DATA","RESET_STORY_ARGS","RESULT_WHATS_NEW_DATA","SELECT_STORY","SET_CONFIG","SET_CURRENT_STORY","SET_GLOBALS","SET_INDEX","SET_STORIES","SET_WHATS_NEW_CACHE","SHARED_STATE_CHANGED","SHARED_STATE_SET","STORIES_COLLAPSE_ALL","STORIES_EXPAND_ALL","STORY_ARGS_UPDATED","STORY_CHANGED","STORY_ERRORED","STORY_INDEX_INVALIDATED","STORY_MISSING","STORY_PREPARED","STORY_RENDERED","STORY_RENDER_PHASE_CHANGED","STORY_SPECIFIED","STORY_THREW_EXCEPTION","STORY_UNCHANGED","TELEMETRY_ERROR","TOGGLE_WHATS_NEW_NOTIFICATIONS","UPDATE_GLOBALS","UPDATE_QUERY_PARAMS","UPDATE_STORY_ARGS"],"@storybook/router":["BaseLocationProvider","DEEPLY_EQUAL","Link","Location","LocationProvider","Match","Route","buildArgsParam","deepDiff","getMatch","parsePath","queryFromLocation","queryFromString","stringifyQuery","useNavigate"],"@storybook/theming":["CacheProvider","ClassNames","Global","ThemeProvider","background","color","convert","create","createCache","createGlobal","createReset","css","darken","ensure","ignoreSsrWarning","isPropValid","jsx","keyframes","lighten","styled","themes","typography","useTheme","withTheme"],"@storybook/api":["ActiveTabs","Consumer","ManagerContext","Provider","addons","combineParameters","controlOrMetaKey","controlOrMetaSymbol","eventMatchesShortcut","eventToShortcut","isMacLike","isShortcutTaken","keyToSymbol","merge","mockChannel","optionOrAltSymbol","shortcutMatchesShortcut","shortcutToHumanString","types","useAddonState","useArgTypes","useArgs","useChannel","useGlobalTypes","useGlobals","useParameter","useSharedState","useStoryPrepared","useStorybookApi","useStorybookState"],"@storybook/manager-api":["ActiveTabs","Consumer","ManagerContext","Provider","addons","combineParameters","controlOrMetaKey","controlOrMetaSymbol","eventMatchesShortcut","eventToShortcut","isMacLike","isShortcutTaken","keyToSymbol","merge","mockChannel","optionOrAltSymbol","shortcutMatchesShortcut","shortcutToHumanString","types","useAddonState","useArgTypes","useArgs","useChannel","useGlobalTypes","useGlobals","useParameter","useSharedState","useStoryPrepared","useStorybookApi","useStorybookState"],"@storybook/addons":["addons","types","mockChannel"],"@storybook/client-logger":["deprecate","logger","once","pretty"],"@storybook/types":["Addon_TypesEnum"]};var globalsNameReferenceMap={react:"__REACT__","react-dom":"__REACT_DOM__","@storybook/components":"__STORYBOOK_COMPONENTS__","@storybook/channels":"__STORYBOOK_CHANNELS__","@storybook/core-events":"__STORYBOOK_CORE_EVENTS__","@storybook/router":"__STORYBOOK_ROUTER__","@storybook/theming":"__STORYBOOK_THEMING__","@storybook/api":"__STORYBOOK_API__","@storybook/manager-api":"__STORYBOOK_API__","@storybook/addons":"__STORYBOOK_ADDONS__","@storybook/client-logger":"__STORYBOOK_CLIENT_LOGGER__","@storybook/types":"__STORYBOOK_TYPES__"},globalPackages=Object.keys(globalsNameReferenceMap);var globalsModuleInfoMap=globalPackages.reduce((acc,key)=>(acc[key]={type:"esm",varName:globalsNameReferenceMap[key],namedExports:exports_default[key],defaultExport:!0},acc),{});0&&(module.exports={globalsModuleInfoMap}); 2 | -------------------------------------------------------------------------------- /docs/sb-manager/globals.js: -------------------------------------------------------------------------------- 1 | var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var globals_exports={};__export(globals_exports,{globalPackages:()=>globalPackages,globalsNameReferenceMap:()=>globalsNameReferenceMap});module.exports=__toCommonJS(globals_exports);var globalsNameReferenceMap={react:"__REACT__","react-dom":"__REACT_DOM__","@storybook/components":"__STORYBOOK_COMPONENTS__","@storybook/channels":"__STORYBOOK_CHANNELS__","@storybook/core-events":"__STORYBOOK_CORE_EVENTS__","@storybook/router":"__STORYBOOK_ROUTER__","@storybook/theming":"__STORYBOOK_THEMING__","@storybook/api":"__STORYBOOK_API__","@storybook/manager-api":"__STORYBOOK_API__","@storybook/addons":"__STORYBOOK_ADDONS__","@storybook/client-logger":"__STORYBOOK_CLIENT_LOGGER__","@storybook/types":"__STORYBOOK_TYPES__"},globalPackages=Object.keys(globalsNameReferenceMap);0&&(module.exports={globalPackages,globalsNameReferenceMap}); 2 | -------------------------------------------------------------------------------- /docs/sb-manager/index.js: -------------------------------------------------------------------------------- 1 | import{Provider,Root,renderStorybookUI}from"./chunk-XE6LDGTE.js";import"./chunk-5QAFKPS7.js";import"./chunk-7PRFHFSS.js";import"./chunk-YDUB7CS6.js";import"./chunk-ZEU7PDD3.js";export{Provider,Root,renderStorybookUI}; 2 | -------------------------------------------------------------------------------- /docs/sb-manager/runtime.js: -------------------------------------------------------------------------------- 1 | import{CHANNEL_CREATED,Provider,TELEMETRY_ERROR,UncaughtManagerError,addons,createBrowserChannel,dist_exports as dist_exports2,dist_exports2 as dist_exports3,dist_exports3 as dist_exports4,dist_exports4 as dist_exports5,dist_exports5 as dist_exports6,dist_exports6 as dist_exports8,mockChannel,renderStorybookUI,typesX}from"./chunk-XE6LDGTE.js";import"./chunk-5QAFKPS7.js";import"./chunk-7PRFHFSS.js";import{dist_exports,dist_exports2 as dist_exports7,require_react,require_react_dom,scope}from"./chunk-YDUB7CS6.js";import{__commonJS,__toESM}from"./chunk-ZEU7PDD3.js";var require_browser_dtector_umd_min=__commonJS({"../../node_modules/browser-dtector/browser-dtector.umd.min.js"(exports,module){(function(e,o){typeof exports=="object"&&typeof module<"u"?module.exports=o():typeof define=="function"&&define.amd?define(o):(e=typeof globalThis<"u"?globalThis:e||self).BrowserDetector=o()})(exports,function(){"use strict";function e(e2,o2){for(var r2=0;r21&&arguments[1]!==void 0?arguments[1]:-1,"})?")),r2=Number(e2).toString().match(o2);return r2?r2[0]:null},i=function(){return typeof window<"u"?window.navigator:null},t=function(){function t2(e2){var o2;(function(e3,o3){if(!(e3 instanceof o3))throw new TypeError("Cannot call a class as a function")})(this,t2),this.userAgent=e2||((o2=i())===null||o2===void 0?void 0:o2.userAgent)||null}var a,l,s;return a=t2,l=[{key:"parseUserAgent",value:function(e2){var t3,a2,l2,s2={},c=e2||this.userAgent||"",d=c.toLowerCase().replace(/\s\s+/g," "),u=/(edge)\/([\w.]+)/.exec(d)||/(edg)[/]([\w.]+)/.exec(d)||/(opr)[/]([\w.]+)/.exec(d)||/(opt)[/]([\w.]+)/.exec(d)||/(fxios)[/]([\w.]+)/.exec(d)||/(edgios)[/]([\w.]+)/.exec(d)||/(jsdom)[/]([\w.]+)/.exec(d)||/(samsungbrowser)[/]([\w.]+)/.exec(d)||/(electron)[/]([\w.]+)/.exec(d)||/(chrome)[/]([\w.]+)/.exec(d)||/(crios)[/]([\w.]+)/.exec(d)||/(opios)[/]([\w.]+)/.exec(d)||/(version)(applewebkit)[/]([\w.]+).*(safari)[/]([\w.]+)/.exec(d)||/(webkit)[/]([\w.]+).*(version)[/]([\w.]+).*(safari)[/]([\w.]+)/.exec(d)||/(applewebkit)[/]([\w.]+).*(safari)[/]([\w.]+)/.exec(d)||/(webkit)[/]([\w.]+)/.exec(d)||/(opera)(?:.*version|)[/]([\w.]+)/.exec(d)||/(msie) ([\w.]+)/.exec(d)||/(fennec)[/]([\w.]+)/.exec(d)||d.indexOf("trident")>=0&&/(rv)(?::| )([\w.]+)/.exec(d)||d.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(d)||[],f=/(ipad)/.exec(d)||/(ipod)/.exec(d)||/(iphone)/.exec(d)||/(jsdom)/.exec(d)||/(windows phone)/.exec(d)||/(xbox)/.exec(d)||/(win)/.exec(d)||/(tablet)/.exec(d)||/(android)/.test(d)&&/(mobile)/.test(d)===!1&&["androidTablet"]||/(android)/.exec(d)||/(mac)/.exec(d)||/(linux)/.exec(d)||/(cros)/.exec(d)||[],p=u[5]||u[3]||u[1]||null,w=f[0]||null,x=u[4]||u[2]||null,b=i();p==="chrome"&&typeof(b==null||(t3=b.brave)===null||t3===void 0?void 0:t3.isBrave)=="function"&&(p="brave"),p&&(s2[p]=!0),w&&(s2[w]=!0);var v=!!(s2.tablet||s2.android||s2.androidTablet),m=!!(s2.ipad||s2.tablet||s2.androidTablet),g=!!(s2.android||s2.androidTablet||s2.tablet||s2.ipad||s2.ipod||s2.iphone||s2["windows phone"]),h=!!(s2.cros||s2.mac||s2.linux||s2.win),y=!!(s2.brave||s2.chrome||s2.crios||s2.opr||s2.safari||s2.edg||s2.electron),A=!!(s2.msie||s2.rv);return{name:(a2=o[p])!==null&&a2!==void 0?a2:null,platform:(l2=r[w])!==null&&l2!==void 0?l2:null,userAgent:c,version:x,shortVersion:x?n(parseFloat(x),2):null,isAndroid:v,isTablet:m,isMobile:g,isDesktop:h,isWebkit:y,isIE:A}}},{key:"getBrowserInfo",value:function(){var e2=this.parseUserAgent();return{name:e2.name,platform:e2.platform,userAgent:e2.userAgent,version:e2.version,shortVersion:e2.shortVersion}}}],s=[{key:"VERSION",get:function(){return"3.4.0"}}],l&&e(a.prototype,l),s&&e(a,s),Object.defineProperty(a,"prototype",{writable:!1}),t2}();return t})}});var REACT=__toESM(require_react()),REACT_DOM=__toESM(require_react_dom());var globalsNameValueMap={react:REACT,"react-dom":REACT_DOM,"@storybook/components":dist_exports8,"@storybook/channels":dist_exports5,"@storybook/core-events":dist_exports3,"@storybook/router":dist_exports2,"@storybook/theming":dist_exports7,"@storybook/api":dist_exports6,"@storybook/manager-api":dist_exports6,"@storybook/addons":{addons,types:typesX,mockChannel},"@storybook/client-logger":dist_exports,"@storybook/types":dist_exports4};var globalsNameReferenceMap={react:"__REACT__","react-dom":"__REACT_DOM__","@storybook/components":"__STORYBOOK_COMPONENTS__","@storybook/channels":"__STORYBOOK_CHANNELS__","@storybook/core-events":"__STORYBOOK_CORE_EVENTS__","@storybook/router":"__STORYBOOK_ROUTER__","@storybook/theming":"__STORYBOOK_THEMING__","@storybook/api":"__STORYBOOK_API__","@storybook/manager-api":"__STORYBOOK_API__","@storybook/addons":"__STORYBOOK_ADDONS__","@storybook/client-logger":"__STORYBOOK_CLIENT_LOGGER__","@storybook/types":"__STORYBOOK_TYPES__"},globalPackages=Object.keys(globalsNameReferenceMap);var import_browser_dtector=__toESM(require_browser_dtector_umd_min()),browserInfo;function getBrowserInfo(){return browserInfo||(browserInfo=new import_browser_dtector.default(scope.navigator?.userAgent).getBrowserInfo()),browserInfo}var errorMessages=["ResizeObserver loop completed with undelivered notifications.","ResizeObserver loop limit exceeded","Script error."],shouldSkipError=error=>errorMessages.includes(error?.message);function prepareForTelemetry(originalError){let error=originalError;return(originalError.target===scope||originalError.currentTarget===scope||originalError.srcElement===scope)&&(error=new Error(originalError.message),error.name=originalError.name||error.name),originalError.fromStorybook||(error=new UncaughtManagerError({error})),error.browserInfo=getBrowserInfo(),error}var{FEATURES,CONFIG_TYPE}=scope,ReactProvider=class extends Provider{constructor(){super();let channel=createBrowserChannel({page:"manager"});addons.setChannel(channel),channel.emit(CHANNEL_CREATED),this.addons=addons,this.channel=channel,scope.__STORYBOOK_ADDONS_CHANNEL__=channel,FEATURES?.storyStoreV7&&CONFIG_TYPE==="DEVELOPMENT"&&(this.serverChannel=this.channel,addons.setServerChannel(this.serverChannel))}getElements(type){return this.addons.getElements(type)}getConfig(){return this.addons.getConfig()}handleAPI(api){this.addons.loadAddons(api)}};globalPackages.forEach(key=>{scope[globalsNameReferenceMap[key]]=globalsNameValueMap[key]});scope.sendTelemetryError=error=>{shouldSkipError(error)||scope.__STORYBOOK_ADDONS_CHANNEL__.emit(TELEMETRY_ERROR,prepareForTelemetry(error))};scope.addEventListener("error",args=>{let error=args.error||args;scope.sendTelemetryError(error)});scope.addEventListener("unhandledrejection",({reason})=>{scope.sendTelemetryError(reason)});var{document}=scope,rootEl=document.getElementById("root");renderStorybookUI(rootEl,new ReactProvider); 2 | -------------------------------------------------------------------------------- /docs/sb-manager/syntaxhighlighter-MJWPISIS-JOSCT6CQ.js: -------------------------------------------------------------------------------- 1 | import{SyntaxHighlighter2,createCopyToClipboardFunction,syntaxhighlighter_default}from"./chunk-5QAFKPS7.js";import"./chunk-YDUB7CS6.js";import"./chunk-ZEU7PDD3.js";export{SyntaxHighlighter2 as SyntaxHighlighter,createCopyToClipboardFunction,syntaxhighlighter_default as default}; 2 | -------------------------------------------------------------------------------- /docs/sb-preview/globals.js: -------------------------------------------------------------------------------- 1 | "use strict";var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var globals_exports={};__export(globals_exports,{globalPackages:()=>globalPackages,globalsNameReferenceMap:()=>globalsNameReferenceMap});module.exports=__toCommonJS(globals_exports);var globalsNameReferenceMap={"@storybook/addons":"__STORYBOOK_MODULE_ADDONS__","@storybook/global":"__STORYBOOK_MODULE_GLOBAL__","@storybook/channel-postmessage":"__STORYBOOK_MODULE_CHANNEL_POSTMESSAGE__","@storybook/channel-websocket":"__STORYBOOK_MODULE_CHANNEL_WEBSOCKET__","@storybook/channels":"__STORYBOOK_MODULE_CHANNELS__","@storybook/client-api":"__STORYBOOK_MODULE_CLIENT_API__","@storybook/client-logger":"__STORYBOOK_MODULE_CLIENT_LOGGER__","@storybook/core-client":"__STORYBOOK_MODULE_CORE_CLIENT__","@storybook/core-events":"__STORYBOOK_MODULE_CORE_EVENTS__","@storybook/preview-web":"__STORYBOOK_MODULE_PREVIEW_WEB__","@storybook/preview-api":"__STORYBOOK_MODULE_PREVIEW_API__","@storybook/store":"__STORYBOOK_MODULE_STORE__","@storybook/types":"__STORYBOOK_MODULE_TYPES__"},globalPackages=Object.keys(globalsNameReferenceMap);0&&(module.exports={globalPackages,globalsNameReferenceMap}); 2 | -------------------------------------------------------------------------------- /docs/stories.json: -------------------------------------------------------------------------------- 1 | {"v":3,"stories":{"introduction--docs":{"id":"introduction--docs","title":"Introduction","name":"Docs","importPath":"./.storybook/stories/intro.stories.mdx","tags":["stories-mdx","stories-mdx-docsOnly","docs"],"storiesImports":[],"kind":"Introduction","story":"Docs","parameters":{"__id":"introduction--docs","docsOnly":true,"fileName":"./.storybook/stories/intro.stories.mdx"}},"tree--docs":{"id":"tree--docs","title":"Tree","name":"Docs","importPath":"./.storybook/stories/tree.stories.js","tags":["docs","autodocs"],"storiesImports":[],"kind":"Tree","story":"Docs","parameters":{"__id":"tree--docs","docsOnly":true,"fileName":"./.storybook/stories/tree.stories.js"}},"tree--simple":{"id":"tree--simple","name":"Simple","title":"Tree","importPath":"./.storybook/stories/tree.stories.js","tags":["story"],"kind":"Tree","story":"Simple","parameters":{"__id":"tree--simple","docsOnly":false,"fileName":"./.storybook/stories/tree.stories.js"}},"tree--events":{"id":"tree--events","name":"Events","title":"Tree","importPath":"./.storybook/stories/tree.stories.js","tags":["story"],"kind":"Tree","story":"Events","parameters":{"__id":"tree--events","docsOnly":false,"fileName":"./.storybook/stories/tree.stories.js"}},"tree--custom-children":{"id":"tree--custom-children","name":"Custom Children","title":"Tree","importPath":"./.storybook/stories/tree.stories.js","tags":["story"],"kind":"Tree","story":"Custom Children","parameters":{"__id":"tree--custom-children","docsOnly":false,"fileName":"./.storybook/stories/tree.stories.js"}},"tree--custom-paths":{"id":"tree--custom-paths","name":"Custom Paths","title":"Tree","importPath":"./.storybook/stories/tree.stories.js","tags":["story"],"kind":"Tree","story":"Custom Paths","parameters":{"__id":"tree--custom-paths","docsOnly":false,"fileName":"./.storybook/stories/tree.stories.js"}},"tree--right-to-left":{"id":"tree--right-to-left","name":"Right To Left","title":"Tree","importPath":"./.storybook/stories/tree.stories.js","tags":["story"],"kind":"Tree","story":"Right To Left","parameters":{"__id":"tree--right-to-left","docsOnly":false,"fileName":"./.storybook/stories/tree.stories.js"}},"tree--transformations":{"id":"tree--transformations","name":"Transformations","title":"Tree","importPath":"./.storybook/stories/tree.stories.js","tags":["story"],"kind":"Tree","story":"Transformations","parameters":{"__id":"tree--transformations","docsOnly":false,"fileName":"./.storybook/stories/tree.stories.js"}},"tree--custom-styles":{"id":"tree--custom-styles","name":"Custom Styles","title":"Tree","importPath":"./.storybook/stories/tree.stories.js","tags":["story"],"kind":"Tree","story":"Custom Styles","parameters":{"__id":"tree--custom-styles","docsOnly":false,"fileName":"./.storybook/stories/tree.stories.js"}},"tree-labels--docs":{"id":"tree-labels--docs","title":"Tree/Labels","name":"Docs","importPath":"./.storybook/stories/labels.stories.js","tags":["docs","autodocs"],"storiesImports":[],"kind":"Tree/Labels","story":"Docs","parameters":{"__id":"tree-labels--docs","docsOnly":true,"fileName":"./.storybook/stories/labels.stories.js"}},"tree-labels--duplicate":{"id":"tree-labels--duplicate","name":"Duplicate","title":"Tree/Labels","importPath":"./.storybook/stories/labels.stories.js","tags":["story"],"kind":"Tree/Labels","story":"Duplicate","parameters":{"__id":"tree-labels--duplicate","docsOnly":false,"fileName":"./.storybook/stories/labels.stories.js"}},"tree-labels--jsx":{"id":"tree-labels--jsx","name":"JSX","title":"Tree/Labels","importPath":"./.storybook/stories/labels.stories.js","tags":["story"],"kind":"Tree/Labels","story":"JSX","parameters":{"__id":"tree-labels--jsx","docsOnly":false,"fileName":"./.storybook/stories/labels.stories.js"}},"tree-nodes--docs":{"id":"tree-nodes--docs","title":"Tree/Nodes","name":"Docs","importPath":"./.storybook/stories/nodes.stories.js","tags":["docs","autodocs"],"storiesImports":[],"kind":"Tree/Nodes","story":"Docs","parameters":{"__id":"tree-nodes--docs","docsOnly":true,"fileName":"./.storybook/stories/nodes.stories.js"}},"tree-nodes--rectangular-nodes":{"id":"tree-nodes--rectangular-nodes","name":"Rectangular Nodes","title":"Tree/Nodes","importPath":"./.storybook/stories/nodes.stories.js","tags":["story"],"kind":"Tree/Nodes","story":"Rectangular Nodes","parameters":{"__id":"tree-nodes--rectangular-nodes","docsOnly":false,"fileName":"./.storybook/stories/nodes.stories.js"}},"tree-nodes--polygon-nodes":{"id":"tree-nodes--polygon-nodes","name":"Polygon Nodes","title":"Tree/Nodes","importPath":"./.storybook/stories/nodes.stories.js","tags":["story"],"kind":"Tree/Nodes","story":"Polygon Nodes","parameters":{"__id":"tree-nodes--polygon-nodes","docsOnly":false,"fileName":"./.storybook/stories/nodes.stories.js"}},"tree-nodes--image-nodes":{"id":"tree-nodes--image-nodes","name":"Image Nodes","title":"Tree/Nodes","importPath":"./.storybook/stories/nodes.stories.js","tags":["story"],"kind":"Tree/Nodes","story":"Image Nodes","parameters":{"__id":"tree-nodes--image-nodes","docsOnly":false,"fileName":"./.storybook/stories/nodes.stories.js"}},"tree-nodes--custom-node-props":{"id":"tree-nodes--custom-node-props","name":"Custom Node Props","title":"Tree/Nodes","importPath":"./.storybook/stories/nodes.stories.js","tags":["story"],"kind":"Tree/Nodes","story":"Custom Node Props","parameters":{"__id":"tree-nodes--custom-node-props","docsOnly":false,"fileName":"./.storybook/stories/nodes.stories.js"}},"animatedtree-animations--docs":{"id":"animatedtree-animations--docs","title":"AnimatedTree/Animations","name":"Docs","importPath":"./.storybook/stories/animatedTree.stories.js","tags":["docs","autodocs"],"storiesImports":[],"kind":"AnimatedTree/Animations","story":"Docs","parameters":{"__id":"animatedtree-animations--docs","docsOnly":true,"fileName":"./.storybook/stories/animatedTree.stories.js"}},"animatedtree-animations--animations":{"id":"animatedtree-animations--animations","name":"Animations","title":"AnimatedTree/Animations","importPath":"./.storybook/stories/animatedTree.stories.js","tags":["story"],"kind":"AnimatedTree/Animations","story":"Animations","parameters":{"__id":"animatedtree-animations--animations","docsOnly":false,"fileName":"./.storybook/stories/animatedTree.stories.js"}}}} 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tree-graph", 3 | "version": "8.0.3", 4 | "description": "A react library for generating a graphical tree from data using d3", 5 | "main": "dist/index.js", 6 | "module": "dist/module/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/jpb12/react-tree-graph.git" 10 | }, 11 | "keywords": [ 12 | "d3", 13 | "graph", 14 | "react", 15 | "svg", 16 | "tree", 17 | "ui" 18 | ], 19 | "author": "James Brierley", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/jpb12/react-tree-graph/issues" 23 | }, 24 | "homepage": "https://jpb12.github.io/react-tree-graph", 25 | "devDependencies": { 26 | "@babel/core": "^7.26.0", 27 | "@babel/eslint-parser": "^7.26.5", 28 | "@babel/plugin-transform-runtime": "^7.25.9", 29 | "@babel/preset-env": "^7.26.0", 30 | "@babel/preset-react": "^7.26.3", 31 | "@cfaester/enzyme-adapter-react-18": "^0.8.0", 32 | "@rollup/plugin-babel": "^6.0.4", 33 | "@rollup/plugin-terser": "^0.4.4", 34 | "@storybook/addon-essentials": "^7.6.20", 35 | "@storybook/react-webpack5": "^7.6.20", 36 | "babel-jest": "^29.7.0", 37 | "enzyme": "^3.11.0", 38 | "enzyme-to-json": "^3.6.2", 39 | "eslint": "^8.57.1", 40 | "eslint-plugin-react": "^7.37.4", 41 | "eslint-plugin-storybook": "^0.11.2", 42 | "jest": "^29.7.0", 43 | "jest-environment-jsdom": "^29.7.0", 44 | "postcss": "^8.4.49", 45 | "prettier": "^3.4.2", 46 | "react": "^18.3.1", 47 | "react-dom": "^18.3.1", 48 | "rfdc": "^1.4.1", 49 | "rollup": "^4.30.1", 50 | "rollup-plugin-clear": "^2.0.7", 51 | "rollup-plugin-ignore": "^1.0.10", 52 | "rollup-plugin-postcss": "^4.0.2", 53 | "rollup-plugin-prettier": "^4.1.1", 54 | "rollup-plugin-progress": "^1.1.2", 55 | "storybook": "^7.6.20" 56 | }, 57 | "dependencies": { 58 | "@babel/runtime": "^7.26.0", 59 | "d3-ease": "^2.0.0", 60 | "d3-hierarchy": "^2.0.0" 61 | }, 62 | "jest": { 63 | "collectCoverageFrom": [ 64 | "src/**/*.js" 65 | ], 66 | "snapshotSerializers": [ 67 | "enzyme-to-json/serializer" 68 | ], 69 | "setupFiles": [ 70 | "./__tests__/startup.js" 71 | ], 72 | "testEnvironment": "jsdom", 73 | "testPathIgnorePatterns": [ 74 | "startup.js" 75 | ] 76 | }, 77 | "peerDependencies": { 78 | "react": "^16.8 || ^17 || ^18 || ^19" 79 | }, 80 | "scripts": { 81 | "build": "rollup --config", 82 | "coverage": "cat ./coverage/lcov.info | coveralls", 83 | "eslint": "eslint src __tests__ .storybook", 84 | "storybook-build": "storybook build -c .storybook -o docs", 85 | "storybook-watch": "storybook dev -c .storybook --port 9000", 86 | "test": "jest", 87 | "version": "npm run build" 88 | }, 89 | "sideEffects": false 90 | } 91 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { babel } from '@rollup/plugin-babel'; 2 | import terser from '@rollup/plugin-terser'; 3 | import clear from 'rollup-plugin-clear'; 4 | import ignore from 'rollup-plugin-ignore'; 5 | import postcss from 'rollup-plugin-postcss'; 6 | import prettier from 'rollup-plugin-prettier'; 7 | import progress from 'rollup-plugin-progress'; 8 | import rfdc from 'rfdc'; 9 | 10 | const clone = rfdc(); 11 | 12 | const defaultOutput = { 13 | globals: { 14 | '@babel/runtime/helpers/extends': '_extends', 15 | 'd3-ease': 'd3', 16 | 'd3-hierarchy': 'd3', 17 | react: 'React' 18 | }, 19 | interop: 'auto' 20 | }; 21 | 22 | const defaultConfig = { 23 | external: [ 24 | '@babel/runtime/helpers/extends', 25 | 'd3-hierarchy', 26 | 'd3-ease', 27 | 'react' 28 | ], 29 | input: 'src/index.js', 30 | plugins: [ 31 | babel({ 32 | babelHelpers: 'runtime', 33 | exclude: 'node_modules/**', 34 | plugins: ['@babel/plugin-transform-runtime'] 35 | }), 36 | clear({ 37 | targets: ['dist'] 38 | }), 39 | progress() 40 | ] 41 | }; 42 | 43 | const devConfig = clone(defaultConfig); 44 | devConfig.output = { 45 | ...defaultOutput, 46 | file: 'dist/index.js', 47 | format: 'umd', 48 | name: 'ReactTreeGraph' 49 | }; 50 | devConfig.plugins.push(prettier({ 51 | parser: 'babel', 52 | singleQuote: true, 53 | useTabs: true 54 | })); 55 | devConfig.plugins.unshift(postcss({ 56 | extract: 'style.css' 57 | })); 58 | 59 | const moduleConfig = clone(defaultConfig); 60 | moduleConfig.output = { 61 | ...defaultOutput, 62 | dir: 'dist/module', 63 | format: 'esm', 64 | preserveModules: true 65 | }; 66 | moduleConfig.plugins.unshift(ignore(['../styles/style.css'])); 67 | 68 | const prodConfig = clone(defaultConfig); 69 | prodConfig.output = { 70 | ...defaultOutput, 71 | file: 'dist/index.min.js', 72 | format: 'umd', 73 | name: 'ReactTreeGraph' 74 | }; 75 | prodConfig.plugins.push(terser()); 76 | prodConfig.plugins.unshift(postcss({ 77 | extract: 'style.min.css', 78 | minimize: true 79 | })); 80 | 81 | export default [ 82 | devConfig, 83 | moduleConfig, 84 | prodConfig 85 | ]; -------------------------------------------------------------------------------- /src/components/animated.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Container from './container'; 3 | 4 | export default function Animated(props) { 5 | const initialX = props.nodes[0].x; 6 | const initialY = props.nodes[0].y; 7 | 8 | const [state, setState] = useState({ 9 | nodes: props.nodes.map(n => ({ ...n, x: initialX, y: initialY })), 10 | links: props.links.map(l => ({ 11 | source: { ...l.source, x: initialX, y: initialY }, 12 | target: { ...l.target, x: initialX, y: initialY } 13 | })) 14 | }); 15 | 16 | const [animation, setAnimation] = useState(null); 17 | 18 | useEffect(animate, [props.nodes, props.links]); 19 | 20 | function animate() { 21 | // Stop previous animation if one is already in progress. We will start the next animation 22 | // from the position we are currently in 23 | clearInterval(animation); 24 | 25 | let counter = 0; 26 | 27 | // Do as much one-time calculation outside of the animation step, which needs to be fast 28 | const animationContext = getAnimationContext(state, props); 29 | 30 | const interval = setInterval(() => { 31 | counter++; 32 | 33 | if (counter === props.steps) { 34 | clearInterval(interval); 35 | setState({ nodes: props.nodes, links: props.links }); 36 | return; 37 | } 38 | 39 | setState(calculateNewState(animationContext, counter / props.steps)); 40 | }, props.duration / props.steps); 41 | setAnimation(interval); 42 | 43 | return () => clearInterval(animation); 44 | } 45 | 46 | function getAnimationContext(initialState, newState) { 47 | // Nodes/links that are in both states need to be moved from the old position to the new one 48 | // Nodes/links only in the initial state are being removed, and should be moved to the position 49 | // of the closest ancestor that still exists, or the new root 50 | // Nodes/links only in the new state are being added, and should be moved from the position of 51 | // the closest ancestor that previously existed, or the old root 52 | 53 | // The base determines which node/link the data (like classes and labels) comes from for rendering 54 | 55 | // We only run this once at the start of the animation, so optimisation is less important 56 | const addedNodes = newState.nodes 57 | .filter(n1 => initialState.nodes.every(n2 => !areNodesSame(n1, n2))) 58 | .map(n1 => ({ base: n1, old: getClosestAncestor(n1, newState, initialState), new: n1 })); 59 | const changedNodes = newState.nodes 60 | .filter(n1 => initialState.nodes.some(n2 => areNodesSame(n1, n2))) 61 | .map(n1 => ({ base: n1, old: initialState.nodes.find(n2 => areNodesSame(n1, n2)), new: n1 })); 62 | const removedNodes = initialState.nodes 63 | .filter(n1 => newState.nodes.every(n2 => !areNodesSame(n1, n2))) 64 | .map(n1 => ({ base: n1, old: n1, new: getClosestAncestor(n1, initialState, newState) })); 65 | 66 | const addedLinks = newState.links 67 | .filter(l1 => initialState.links.every(l2 => !areLinksSame(l1, l2))) 68 | .map(l1 => ({ base: l1, old: getClosestAncestor(l1.target, newState, initialState), new: l1 })); 69 | const changedLinks = newState.links 70 | .filter(l1 => initialState.links.some(l2 => areLinksSame(l1, l2))) 71 | .map(l1 => ({ base: l1, old: initialState.links.find(l2 => areLinksSame(l1, l2)), new: l1 })); 72 | const removedLinks = initialState.links 73 | .filter(l1 => newState.links.every(l2 => !areLinksSame(l1, l2))) 74 | .map(l1 => ({ base: l1, old: l1, new: getClosestAncestor(l1.target, initialState, newState) })); 75 | 76 | return { 77 | nodes: changedNodes.concat(addedNodes).concat(removedNodes), 78 | links: changedLinks.concat(addedLinks).concat(removedLinks) 79 | }; 80 | } 81 | 82 | function getClosestAncestor(node, stateWithNode, stateWithoutNode) { 83 | let oldParent = node; 84 | 85 | while (oldParent) { 86 | let newParent = stateWithoutNode.nodes.find(n => areNodesSame(oldParent, n)); 87 | 88 | if (newParent) { 89 | return newParent; 90 | } 91 | 92 | oldParent = stateWithNode.nodes.find(n => (props.getChildren(n) || []).some(c => areNodesSame(oldParent, c))); 93 | } 94 | 95 | return stateWithoutNode.nodes[0]; 96 | } 97 | 98 | function areNodesSame(a, b) { 99 | return a.data[props.keyProp] === b.data[props.keyProp]; 100 | } 101 | 102 | function areLinksSame(a, b) { 103 | return a.source.data[props.keyProp] === b.source.data[props.keyProp] && a.target.data[props.keyProp] === b.target.data[props.keyProp]; 104 | } 105 | 106 | function calculateNewState(animationContext, interval) { 107 | return { 108 | nodes: animationContext.nodes.map(n => calculateNodePosition(n.base, n.old, n.new, interval)), 109 | links: animationContext.links.map(l => calculateLinkPosition(l.base, l.old, l.new, interval)) 110 | }; 111 | } 112 | 113 | function calculateLinkPosition(link, start, end, interval) { 114 | return { 115 | source: { 116 | ...link.source, 117 | x: calculateNewValue(start.source ? start.source.x : start.x, end.source ? end.source.x : end.x, interval), 118 | y: calculateNewValue(start.source ? start.source.y : start.y, end.source ? end.source.y : end.y, interval) 119 | }, 120 | target: { 121 | ...link.target, 122 | x: calculateNewValue(start.target ? start.target.x : start.x, end.target ? end.target.x : end.x, interval), 123 | y: calculateNewValue(start.target ? start.target.y : start.y, end.target ? end.target.y : end.y, interval) 124 | } 125 | }; 126 | } 127 | 128 | function calculateNodePosition(node, start, end, interval) { 129 | return { 130 | ...node, 131 | x: calculateNewValue(start.x, end.x, interval), 132 | y: calculateNewValue(start.y, end.y, interval) 133 | }; 134 | } 135 | 136 | function calculateNewValue(start, end, interval) { 137 | return start + (end - start) * props.easing(interval); 138 | } 139 | 140 | return ; 141 | } -------------------------------------------------------------------------------- /src/components/animatedTree.js: -------------------------------------------------------------------------------- 1 | import { easeQuadOut } from 'd3-ease'; 2 | import React from 'react'; 3 | import getTreeData from '../d3'; 4 | import Animated from './animated'; 5 | 6 | export default function AnimatedTree(props) { 7 | const propsWithDefaults = { 8 | direction: 'ltr', 9 | duration: 500, 10 | easing: easeQuadOut, 11 | getChildren: n => n.children, 12 | steps: 20, 13 | keyProp: 'name', 14 | labelProp: 'name', 15 | nodeShape: 'circle', 16 | nodeProps: {}, 17 | gProps: {}, 18 | pathProps: {}, 19 | svgProps: {}, 20 | textProps: {}, 21 | ...props 22 | }; 23 | 24 | return ( 25 | 43 | { propsWithDefaults.children } 44 | 45 | ); 46 | } -------------------------------------------------------------------------------- /src/components/container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from './link'; 3 | import Node from './node'; 4 | 5 | export default function Container(props) { 6 | return ( 7 | 8 | { props.children } 9 | 10 | { props.links.map(link => 11 | ) 22 | } 23 | { props.nodes.map(node => 24 | ) 36 | } 37 | 38 | 39 | ); 40 | } -------------------------------------------------------------------------------- /src/components/link.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import wrapHandlers from '../wrapHandlers'; 3 | 4 | function diagonal(x1, y1, x2, y2) { 5 | return `M${x1},${y1}C${(x1 + x2) / 2},${y1} ${(x1 + x2) / 2},${y2} ${x2},${y2}`; 6 | } 7 | 8 | export default function Link(props) { 9 | const wrappedProps = wrapHandlers( 10 | props.pathProps, 11 | props.source.data[props.keyProp], 12 | props.target.data[props.keyProp] 13 | ); 14 | 15 | const pathFunc = props.pathFunc || diagonal; 16 | const d = pathFunc( 17 | props.x1, 18 | props.y1, 19 | props.x2, 20 | props.y2 21 | ); 22 | 23 | return ; 24 | } -------------------------------------------------------------------------------- /src/components/node.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import wrapHandlers from '../wrapHandlers'; 3 | 4 | export default function Node(props) { 5 | function getTransform() { 6 | return `translate(${props.x}, ${props.y})`; 7 | } 8 | 9 | let offset = 0.5; 10 | let nodePropsWithDefaults = props.nodeProps; 11 | switch (props.shape) { 12 | case 'circle': 13 | nodePropsWithDefaults = { r: 5, ...nodePropsWithDefaults }; 14 | offset += nodePropsWithDefaults.r; 15 | break; 16 | case 'image': 17 | case 'rect': 18 | nodePropsWithDefaults = { height: 10, width: 10, ...nodePropsWithDefaults }; 19 | nodePropsWithDefaults = { x: -nodePropsWithDefaults.width / 2, y: -nodePropsWithDefaults.height / 2, ...nodePropsWithDefaults }; 20 | offset += nodePropsWithDefaults.width / 2; 21 | break; 22 | } 23 | 24 | if (props.direction === 'rtl') { 25 | offset = -offset; 26 | } 27 | 28 | const wrappedNodeProps = wrapHandlers( 29 | nodePropsWithDefaults, 30 | props[props.keyProp] 31 | ); 32 | 33 | const wrappedGProps = wrapHandlers( 34 | props.gProps, 35 | props[props.keyProp] 36 | ); 37 | 38 | const wrappedTextProps = wrapHandlers( 39 | props.textProps, 40 | props[props.keyProp] 41 | ); 42 | 43 | const label = typeof props[props.labelProp] === 'string' 44 | ? {props[props.labelProp]} 45 | : {props[props.labelProp]}; 46 | 47 | 48 | return ( 49 | 50 | 51 | { label } 52 | 53 | ); 54 | } -------------------------------------------------------------------------------- /src/components/tree.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import getTreeData from '../d3'; 3 | import Container from './container'; 4 | 5 | export default function Tree(props) { 6 | const propsWithDefaults = { 7 | direction: 'ltr', 8 | getChildren: n => n.children, 9 | keyProp: 'name', 10 | labelProp: 'name', 11 | nodeShape: 'circle', 12 | nodeProps: {}, 13 | gProps: {}, 14 | pathProps: {}, 15 | svgProps: {}, 16 | textProps: {}, 17 | ...props 18 | }; 19 | 20 | return ( 21 | 36 | { propsWithDefaults.children } 37 | 38 | ); 39 | } -------------------------------------------------------------------------------- /src/d3.js: -------------------------------------------------------------------------------- 1 | import { hierarchy, tree } from 'd3-hierarchy'; 2 | 3 | export default function getTreeData(props) { 4 | const margins = props.margins 5 | || { 6 | bottom: 10, 7 | left: props.direction !== 'rtl' ? 20 : 150, 8 | right: props.direction !== 'rtl' ? 150 : 20, 9 | top: 10 10 | }; 11 | 12 | const contentWidth = props.width - margins.left - margins.right; 13 | const contentHeight = props.height - margins.top - margins.bottom; 14 | 15 | const data = hierarchy(props.data, props.getChildren); 16 | 17 | const root = tree().size([contentHeight, contentWidth])(data); 18 | 19 | // d3 gives us a top to down tree, but we will display it left to right/right to left, so x and y need to be swapped 20 | const links = root.links().map(link => ({ 21 | ...link, 22 | source: { 23 | ...link.source, 24 | x: props.direction !== 'rtl' ? link.source.y : contentWidth - link.source.y, 25 | y: link.source.x 26 | }, 27 | target: { 28 | ...link.target, 29 | x: props.direction !== 'rtl' ? link.target.y : contentWidth - link.target.y, 30 | y: link.target.x 31 | } 32 | })); 33 | 34 | const nodes = root.descendants().map(node => ({ 35 | ...node, 36 | x: props.direction !== 'rtl' ? node.y : contentWidth - node.y, 37 | y: node.x 38 | })); 39 | 40 | return { 41 | links, 42 | margins, 43 | nodes 44 | }; 45 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import '../styles/style.css'; 2 | 3 | export { default as AnimatedTree } from './components/animatedTree'; 4 | export { default as Tree } from './components/tree'; -------------------------------------------------------------------------------- /src/wrapHandlers.js: -------------------------------------------------------------------------------- 1 | const regex = /on[A-Z]/; 2 | 3 | function wrapper(func, args) { 4 | return event => func(event, ...args); 5 | } 6 | 7 | // Wraps any event handlers passed in as props with a function that passes additional arguments 8 | export default function wrapHandlers(props, ...args) { 9 | const handlers = Object.keys(props).filter(propName => regex.test(propName) && typeof props[propName] === 'function'); 10 | const wrappedHandlers = handlers.reduce((acc, handler) => { 11 | acc[handler] = wrapper(props[handler], args); 12 | return acc; 13 | }, {}); 14 | return { ...props, ...wrappedHandlers }; 15 | } -------------------------------------------------------------------------------- /styles/style.css: -------------------------------------------------------------------------------- 1 | .node circle, .node rect { 2 | fill: white; 3 | stroke: black; 4 | } 5 | 6 | path.link { 7 | fill: none; 8 | stroke: black; 9 | } --------------------------------------------------------------------------------