├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── semantic.yml └── workflows │ ├── cd.yml │ ├── ci.yml │ ├── codeql-analysis.yml │ └── maintenance.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── codecov.yml ├── commitlint.config.js ├── default.css ├── docs ├── .vitepress │ ├── components.d.ts │ ├── components │ │ ├── Demo.vue │ │ ├── GraphView.vue │ │ ├── Home.vue │ │ ├── Status.vue │ │ └── UsedIn.vue │ ├── config.ts │ └── demo │ │ ├── link.ts │ │ ├── model.ts │ │ ├── node.ts │ │ └── random-graph.ts ├── api │ ├── index.md │ └── samples │ │ ├── include-unlinked.ts │ │ ├── labels.ts │ │ ├── link-filter.ts │ │ ├── node-type-filter.ts │ │ ├── node-types.ts │ │ ├── resize.ts │ │ ├── restart.ts │ │ ├── setup.ts │ │ └── shutdown.ts ├── config │ ├── index.md │ └── samples │ │ ├── alphas.ts │ │ ├── callbacks.ts │ │ ├── dynamic-node-radius.ts │ │ ├── forces.ts │ │ ├── initial.ts │ │ ├── link-length.ts │ │ ├── marker.ts │ │ ├── modifiers │ │ ├── drag.ts │ │ ├── links.ts │ │ ├── nodes.ts │ │ ├── simulation.ts │ │ └── zoom.ts │ │ ├── position-initializers.ts │ │ ├── resizing.ts │ │ ├── static-node-radius.ts │ │ └── zoom.ts ├── demo │ └── index.md ├── guide │ ├── index.md │ └── samples │ │ ├── custom-model.ts │ │ ├── style-import.ts │ │ ├── styling.css │ │ └── styling.ts ├── index.md ├── public │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── logo.png │ ├── logo.svg │ └── robots.txt ├── tsconfig.json └── vite.config.ts ├── package.json ├── release.config.js ├── renovate.json ├── src ├── config │ ├── alpha.ts │ ├── callbacks.ts │ ├── config.ts │ ├── filter.ts │ ├── forces.ts │ ├── initial.ts │ ├── marker.ts │ ├── modifiers.ts │ ├── position.ts │ ├── simulation.ts │ └── zoom.ts ├── controller.ts ├── lib │ ├── canvas.ts │ ├── drag.ts │ ├── filter.ts │ ├── link.ts │ ├── marker.ts │ ├── node.ts │ ├── paths.ts │ ├── simulation.ts │ ├── types.ts │ ├── utils.ts │ └── zoom.ts ├── main.ts └── model │ ├── graph.ts │ ├── link.ts │ ├── node.ts │ └── shared.ts ├── test ├── __snapshots__ │ ├── config.test.ts.snap │ └── controller.test.ts.snap ├── config.test.ts ├── controller.test.ts ├── lib │ └── filter.test.ts ├── test-data.ts └── tsconfig.json ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | max_line_length = 80 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .nyc_output 3 | CHANGELOG.md 4 | coverage 5 | dist 6 | node_modules 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@yeger"], 3 | "overrides": [ 4 | { 5 | "files": ["docs/**/*"], 6 | "rules": { 7 | "no-restricted-imports": [ 8 | "error", 9 | { 10 | "patterns": [ 11 | { 12 | "group": ["src/**"], 13 | "message": "Use d3-graph-controller instead." 14 | } 15 | ] 16 | } 17 | ] 18 | } 19 | }, 20 | { 21 | "files": ["docs/**/samples/**/*"], 22 | "rules": { 23 | "@typescript-eslint/consistent-type-imports": "off", 24 | "@typescript-eslint/no-unused-vars": "off", 25 | "import/first": "off", 26 | "no-console": "off", 27 | "unused-imports/no-unused-vars": "off" 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | # Always validate the PR title AND all the commits 2 | titleAndCommits: true 3 | # Allows use of Merge commits (eg on github: "Merge branch 'master' into feature/ride-unicorns") 4 | # this is only relevant when using commitsOnly: true (or titleAndCommits: true) 5 | allowMergeCommits: false 6 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - 'docs/**' 8 | release: 9 | types: [created] 10 | 11 | jobs: 12 | prepare: 13 | name: Prepare 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Setup 17 | uses: DerYeger/yarn-setup-action@master 18 | with: 19 | node-version: 16 20 | build: 21 | name: Build 22 | runs-on: ubuntu-latest 23 | needs: prepare 24 | steps: 25 | - name: Setup 26 | uses: DerYeger/yarn-setup-action@master 27 | with: 28 | node-version: 16 29 | - name: Build 30 | run: yarn build 31 | - name: Upload 32 | uses: actions/upload-artifact@v3 33 | with: 34 | name: dist 35 | path: dist 36 | deploy-docs: 37 | name: Deploy Docs 38 | runs-on: ubuntu-latest 39 | needs: build 40 | steps: 41 | - name: Setup 42 | uses: DerYeger/yarn-setup-action@master 43 | with: 44 | node-version: 16 45 | - name: Download 46 | uses: actions/download-artifact@v3 47 | with: 48 | name: dist 49 | path: dist 50 | - name: Build 51 | run: yarn docs:build 52 | - name: Deploy 53 | uses: JamesIves/github-pages-deploy-action@v4.4.1 54 | with: 55 | branch: gh-pages 56 | folder: ./docs/.vitepress/dist 57 | clean: true 58 | single-commit: true 59 | git-config-name: Jan Müller 60 | git-config-email: janmueller3698@gmail.com 61 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | prepare: 7 | name: Prepare 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Setup 11 | uses: DerYeger/yarn-setup-action@master 12 | with: 13 | node-version: 16 14 | build: 15 | name: Build 16 | runs-on: ubuntu-latest 17 | needs: prepare 18 | steps: 19 | - name: Setup 20 | uses: DerYeger/yarn-setup-action@master 21 | with: 22 | node-version: 16 23 | - name: Build 24 | run: yarn build 25 | - name: Upload 26 | uses: actions/upload-artifact@v3 27 | with: 28 | name: dist 29 | path: dist 30 | build-docs: 31 | name: Build Docs 32 | runs-on: ubuntu-latest 33 | needs: build 34 | steps: 35 | - name: Setup 36 | uses: DerYeger/yarn-setup-action@master 37 | with: 38 | node-version: 16 39 | - name: Download 40 | uses: actions/download-artifact@v3 41 | with: 42 | name: dist 43 | path: dist 44 | - name: Build 45 | run: yarn docs 46 | lint: 47 | name: Lint 48 | runs-on: ubuntu-latest 49 | needs: prepare 50 | steps: 51 | - name: Setup 52 | uses: DerYeger/yarn-setup-action@master 53 | with: 54 | node-version: 16 55 | - name: Lint 56 | run: yarn lint 57 | test: 58 | name: Test 59 | runs-on: ubuntu-latest 60 | needs: prepare 61 | steps: 62 | - name: Setup 63 | uses: DerYeger/yarn-setup-action@master 64 | with: 65 | node-version: 16 66 | - name: Test 67 | run: yarn test 68 | - name: Upload coverage to Codecov 69 | uses: codecov/codecov-action@v3 70 | with: 71 | token: ${{ secrets.CODECOV_TOKEN }} 72 | # release: 73 | # name: Release 74 | # runs-on: ubuntu-latest 75 | # needs: [build, build-docs, lint, test] 76 | # if: github.event_name == 'push' && github.ref == 'refs/heads/master' 77 | # steps: 78 | # - name: Setup 79 | # uses: DerYeger/yarn-setup-action@master 80 | # with: 81 | # node-version: 16 82 | # - name: Download 83 | # uses: actions/download-artifact@v3 84 | # with: 85 | # name: dist 86 | # path: dist 87 | # - name: Semantic release 88 | # uses: cycjimmy/semantic-release-action@v3.2.0 89 | # with: 90 | # extra_plugins: | 91 | # @semantic-release/changelog 92 | # @semantic-release/git 93 | # env: 94 | # GITHUB_TOKEN: ${{ secrets.PAT }} 95 | # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 96 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '24 10 * * 3' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [javascript] 25 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 26 | # Learn more: 27 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v3 32 | 33 | # Initializes the CodeQL tools for scanning. 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v2 36 | with: 37 | languages: ${{ matrix.language }} 38 | # If you wish to specify custom queries, you can do so here or in a config file. 39 | # By default, queries listed here will override any specified in a config file. 40 | # Prefix the list here with "+" to use these queries and those in the config file. 41 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 42 | 43 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 44 | # If this step fails, then you should remove it and run the build manually (see below) 45 | - name: Autobuild 46 | uses: github/codeql-action/autobuild@v2 47 | 48 | # ℹ️ Command-line programs to run using the OS shell. 49 | # 📚 https://git.io/JvXDl 50 | 51 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 52 | # and modify them (or add more) to build your code if your project 53 | # uses a compiled language 54 | 55 | # - run: | 56 | # make bootstrap 57 | # make release 58 | 59 | - name: Perform CodeQL Analysis 60 | uses: github/codeql-action/analyze@v2 61 | -------------------------------------------------------------------------------- /.github/workflows/maintenance.yml: -------------------------------------------------------------------------------- 1 | name: Maintenance 2 | 3 | on: 4 | workflow_dispatch: {} 5 | schedule: 6 | - cron: '0 3 * * *' 7 | 8 | jobs: 9 | cleanup: 10 | name: Cleanup 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Delete assets of old releases 14 | uses: dev-drprasad/delete-older-releases@v0.2.0 15 | with: 16 | keep_latest: 3 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | stale: 20 | name: Stale 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Close stale issues 24 | uses: actions/stale@v6 25 | with: 26 | repo-token: ${{ secrets.GITHUB_TOKEN }} 27 | stale-issue-message: 'This issue has been marked stale because there was no activity for 21 days. Without further action, it will be closed in 3 days.' 28 | days-before-stale: 21 29 | days-before-close: 3 30 | exempt-assignees: DerYeger 31 | exempt-issue-labels: bug, dependencies, enhancement, renovate 32 | exempt-pr-labels: bug, dependencies, enhancement, renovate 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/webstorm+all,node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=webstorm+all,node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | .pnpm-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Snowpack dependency directory (https://snowpack.dev/) 51 | web_modules/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | .env.production 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vitepress build output 101 | .vitepress/dist 102 | 103 | # Serverless directories 104 | .serverless/ 105 | 106 | # FuseBox cache 107 | .fusebox/ 108 | 109 | # DynamoDB Local files 110 | .dynamodb/ 111 | 112 | # TernJS port file 113 | .tern-port 114 | 115 | # Stores VSCode versions used for testing VSCode extensions 116 | .vscode-test 117 | 118 | # yarn v2 119 | .yarn/cache 120 | .yarn/unplugged 121 | .yarn/build-state.yml 122 | .yarn/install-state.gz 123 | .pnp.* 124 | 125 | ### Node Patch ### 126 | # Serverless Webpack directories 127 | .webpack/ 128 | 129 | # Optional stylelint cache 130 | .stylelintcache 131 | 132 | # SvelteKit build / generate output 133 | .svelte-kit 134 | 135 | ### WebStorm+all ### 136 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 137 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 138 | 139 | # User-specific stuff 140 | .idea/**/workspace.xml 141 | .idea/**/tasks.xml 142 | .idea/**/usage.statistics.xml 143 | .idea/**/dictionaries 144 | .idea/**/shelf 145 | 146 | # AWS User-specific 147 | .idea/**/aws.xml 148 | 149 | # Generated files 150 | .idea/**/contentModel.xml 151 | 152 | # Sensitive or high-churn files 153 | .idea/**/dataSources/ 154 | .idea/**/dataSources.ids 155 | .idea/**/dataSources.local.xml 156 | .idea/**/sqlDataSources.xml 157 | .idea/**/dynamic.xml 158 | .idea/**/uiDesigner.xml 159 | .idea/**/dbnavigator.xml 160 | 161 | # Gradle 162 | .idea/**/gradle.xml 163 | .idea/**/libraries 164 | 165 | # Gradle and Maven with auto-import 166 | # When using Gradle or Maven with auto-import, you should exclude module files, 167 | # since they will be recreated, and may cause churn. Uncomment if using 168 | # auto-import. 169 | # .idea/artifacts 170 | # .idea/compiler.xml 171 | # .idea/jarRepositories.xml 172 | # .idea/modules.xml 173 | # .idea/*.iml 174 | # .idea/modules 175 | # *.iml 176 | # *.ipr 177 | 178 | # CMake 179 | cmake-build-*/ 180 | 181 | # Mongo Explorer plugin 182 | .idea/**/mongoSettings.xml 183 | 184 | # File-based project format 185 | *.iws 186 | 187 | # IntelliJ 188 | out/ 189 | 190 | # mpeltonen/sbt-idea plugin 191 | .idea_modules/ 192 | 193 | # JIRA plugin 194 | atlassian-ide-plugin.xml 195 | 196 | # Cursive Clojure plugin 197 | .idea/replstate.xml 198 | 199 | # Crashlytics plugin (for Android Studio and IntelliJ) 200 | com_crashlytics_export_strings.xml 201 | crashlytics.properties 202 | crashlytics-build.properties 203 | fabric.properties 204 | 205 | # Editor-based Rest Client 206 | .idea/httpRequests 207 | 208 | # Android studio 3.1+ serialized cache file 209 | .idea/caches/build_file_checksums.ser 210 | 211 | ### WebStorm+all Patch ### 212 | # Ignores the whole .idea folder and all .iml files 213 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 214 | 215 | .idea/ 216 | 217 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 218 | 219 | *.iml 220 | modules.xml 221 | .idea/misc.xml 222 | *.ipr 223 | 224 | # Sonarlint plugin 225 | .idea/sonarlint 226 | 227 | # End of https://www.toptal.com/developers/gitignore/api/webstorm+all,node 228 | 229 | .temp 230 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | yarn build 6 | yarn test 7 | yarn docs 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.3.28](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.27...v2.3.28) (2022-11-26) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **deps:** update commitlint monorepo to v17.3.0 ([9e79ce5](https://github.com/DerYeger/d3-graph-controller/commit/9e79ce5c1179d826d1e9b5941d35c93b8d3682f0)) 7 | 8 | ## [2.3.27](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.26...v2.3.27) (2022-11-26) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **deps:** update vitest monorepo to v0.25.3 ([19f1ec4](https://github.com/DerYeger/d3-graph-controller/commit/19f1ec4cad175b9f2ff8dd8bb4b407008831e65d)) 14 | 15 | ## [2.3.26](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.25...v2.3.26) (2022-11-19) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **deps:** update vitest monorepo to v0.25.2 ([6131447](https://github.com/DerYeger/d3-graph-controller/commit/6131447d91f1b2230a7eb349826fe35d50ddd137)) 21 | 22 | ## [2.3.25](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.24...v2.3.25) (2022-11-12) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **deps:** update all non-major dependencies ([38652db](https://github.com/DerYeger/d3-graph-controller/commit/38652db1ab94f58249f539ced713faf4ca9148dd)) 28 | 29 | ## [2.3.24](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.23...v2.3.24) (2022-11-12) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **deps:** update vitest monorepo to v0.25.1 ([f3e2ed8](https://github.com/DerYeger/d3-graph-controller/commit/f3e2ed8ce7040ef7fee51724fbe40899f56e6835)) 35 | 36 | ## [2.3.23](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.22...v2.3.23) (2022-11-12) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * **deps:** update dependency vue to v3.2.45 ([3669862](https://github.com/DerYeger/d3-graph-controller/commit/3669862dfa6554ed03ecb799cb4a0494c38732cb)) 42 | 43 | ## [2.3.22](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.21...v2.3.22) (2022-11-05) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * **deps:** update commitlint monorepo to v17.2.0 ([217e69a](https://github.com/DerYeger/d3-graph-controller/commit/217e69a4d7dfb01d7f7bf3b0afc79a54d827cf72)) 49 | 50 | ## [2.3.21](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.20...v2.3.21) (2022-11-05) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * **deps:** update vitest monorepo to v0.24.5 ([062902a](https://github.com/DerYeger/d3-graph-controller/commit/062902acb6236b558e8ddcb89820a30f7cbe443e)) 56 | 57 | ## [2.3.20](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.19...v2.3.20) (2022-10-29) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * **deps:** update dependency @types/node to v18 ([d409cc7](https://github.com/DerYeger/d3-graph-controller/commit/d409cc778006c0c70b5d0650a0ec7ab9fefa687c)) 63 | 64 | ## [2.3.19](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.18...v2.3.19) (2022-10-15) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * **deps:** update dependency vue to v3.2.41 ([41100ac](https://github.com/DerYeger/d3-graph-controller/commit/41100acc59dc411ec5c19eb4c82c89041bf757d1)) 70 | 71 | ## [2.3.18](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.17...v2.3.18) (2022-10-15) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * **deps:** update all non-major dependencies ([cd7d0e8](https://github.com/DerYeger/d3-graph-controller/commit/cd7d0e8745433d74b166691383ceed70855b325f)) 77 | 78 | ## [2.3.17](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.16...v2.3.17) (2022-10-01) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * **deps:** update dependency vue to v3.2.40 ([cf630b4](https://github.com/DerYeger/d3-graph-controller/commit/cf630b4125d1deb86b5e23d6209b6342cce2630d)) 84 | 85 | ## [2.3.16](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.15...v2.3.16) (2022-09-24) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * **deps:** update actions/stale action to v6 ([bddbf65](https://github.com/DerYeger/d3-graph-controller/commit/bddbf650d22d0a4fd07b1ff8935246db47450571)) 91 | 92 | ## [2.3.15](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.14...v2.3.15) (2022-09-17) 93 | 94 | 95 | ### Bug Fixes 96 | 97 | * **deps:** update dependency ts-deepmerge to v4 ([cdbc426](https://github.com/DerYeger/d3-graph-controller/commit/cdbc426a27045b0ebd52e8d1b1b5789ea279f6c5)) 98 | 99 | ## [2.3.14](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.13...v2.3.14) (2022-09-17) 100 | 101 | 102 | ### Bug Fixes 103 | 104 | * **deps:** update commitlint monorepo ([40a8299](https://github.com/DerYeger/d3-graph-controller/commit/40a8299cf8241994bca8c4ee15c13c79d7c19b20)) 105 | 106 | ## [2.3.13](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.12...v2.3.13) (2022-09-10) 107 | 108 | 109 | ### Bug Fixes 110 | 111 | * **deps:** update dependency vue to v3.2.39 ([41f1342](https://github.com/DerYeger/d3-graph-controller/commit/41f134227209b8d53ab3716aa39c1ddfb55147d1)) 112 | 113 | ## [2.3.12](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.11...v2.3.12) (2022-09-10) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * **deps:** update all non-major dependencies ([e90a7db](https://github.com/DerYeger/d3-graph-controller/commit/e90a7db81a5ce99c8fb3fb7d1933734ab7ebc624)) 119 | 120 | ## [2.3.11](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.10...v2.3.11) (2022-09-03) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * **deps:** update dependency vue to v3.2.38 ([d5d1d6c](https://github.com/DerYeger/d3-graph-controller/commit/d5d1d6c69809d5847deb297fe1b417852ce28727)) 126 | 127 | ## [2.3.10](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.9...v2.3.10) (2022-09-03) 128 | 129 | 130 | ### Bug Fixes 131 | 132 | * **deps:** update all non-major dependencies ([ebab156](https://github.com/DerYeger/d3-graph-controller/commit/ebab156dbfdedd21173b3815e813c5e5408679a2)) 133 | 134 | ## [2.3.9](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.8...v2.3.9) (2022-09-01) 135 | 136 | 137 | ### Bug Fixes 138 | 139 | * **release:** schedule release ([f6ac0ea](https://github.com/DerYeger/d3-graph-controller/commit/f6ac0ea8294fb8db36d8f2dbbaffbd7608911fb4)) 140 | 141 | ## [2.3.8](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.7...v2.3.8) (2022-08-24) 142 | 143 | 144 | ### Bug Fixes 145 | 146 | * **deps:** update dependency @types/node to v16.11.56 ([32a0dee](https://github.com/DerYeger/d3-graph-controller/commit/32a0dee1e0eea673d6e8ee5888f3b3e62d381f95)) 147 | 148 | ## [2.3.7](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.6...v2.3.7) (2022-08-24) 149 | 150 | 151 | ### Bug Fixes 152 | 153 | * **deps:** update all non-major dependencies ([51f769c](https://github.com/DerYeger/d3-graph-controller/commit/51f769c268493e64214ff1ad148eaea68570e289)) 154 | 155 | ## [2.3.6](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.5...v2.3.6) (2022-08-23) 156 | 157 | 158 | ### Bug Fixes 159 | 160 | * **deps:** update all non-major dependencies ([5f687b7](https://github.com/DerYeger/d3-graph-controller/commit/5f687b737fad745b673d8ecf5ec46dea447d97d4)) 161 | 162 | ## [2.3.5](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.4...v2.3.5) (2022-08-22) 163 | 164 | 165 | ### Bug Fixes 166 | 167 | * **deps:** update dependency vecti to v2.1.4 ([a59d7ea](https://github.com/DerYeger/d3-graph-controller/commit/a59d7ead89d2f4337423fc599dcb3e367156815c)) 168 | 169 | ## [2.3.4](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.3...v2.3.4) (2022-08-22) 170 | 171 | 172 | ### Bug Fixes 173 | 174 | * **deps:** update all non-major dependencies ([a4ab647](https://github.com/DerYeger/d3-graph-controller/commit/a4ab647749377ec61e09ccfcdc3fab2db54e6bd6)) 175 | 176 | ## [2.3.3](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.2...v2.3.3) (2022-08-21) 177 | 178 | 179 | ### Bug Fixes 180 | 181 | * **deps:** update all non-major dependencies ([1a78f15](https://github.com/DerYeger/d3-graph-controller/commit/1a78f150c609169aa4bc8e5f6f2516c550cc2634)) 182 | 183 | ## [2.3.2](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.1...v2.3.2) (2022-08-20) 184 | 185 | 186 | ### Bug Fixes 187 | 188 | * **deps:** update all non-major dependencies ([b1b4a4e](https://github.com/DerYeger/d3-graph-controller/commit/b1b4a4ef020a9442fb7b987bd5130ed16316a850)) 189 | 190 | ## [2.3.1](https://github.com/DerYeger/d3-graph-controller/compare/v2.3.0...v2.3.1) (2022-08-19) 191 | 192 | 193 | ### Bug Fixes 194 | 195 | * **deps:** update all non-major dependencies ([1619a03](https://github.com/DerYeger/d3-graph-controller/commit/1619a0390e63ea85196ee01ac969f1e7538aa0cf)) 196 | 197 | # [2.3.0](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.55...v2.3.0) (2022-08-18) 198 | 199 | 200 | ### Bug Fixes 201 | 202 | * **deps:** update all non-major dependencies ([7cdb79f](https://github.com/DerYeger/d3-graph-controller/commit/7cdb79f08f20d7c147866547240a8412368b319f)) 203 | 204 | 205 | ### Features 206 | 207 | * inline `ts-deepmerge` ([606df35](https://github.com/DerYeger/d3-graph-controller/commit/606df358781fdc58b4ffef171e9acc82d780c7b6)) 208 | 209 | ## [2.2.55](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.54...v2.2.55) (2022-08-13) 210 | 211 | 212 | ### Bug Fixes 213 | 214 | * **deps:** update dependency ts-deepmerge to v3 ([58dfb80](https://github.com/DerYeger/d3-graph-controller/commit/58dfb80bd811292819338bfcdbb5106246dcf81b)) 215 | 216 | ## [2.2.54](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.53...v2.2.54) (2022-08-11) 217 | 218 | 219 | ### Bug Fixes 220 | 221 | * **release:** schedule release ([eab13a5](https://github.com/DerYeger/d3-graph-controller/commit/eab13a5ada3d9f58e1bcef87b39bf45ffb208cd7)) 222 | 223 | ## [2.2.53](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.52...v2.2.53) (2022-08-04) 224 | 225 | 226 | ### Bug Fixes 227 | 228 | * **deps:** update dependency @yeger/debounce to v1.0.26 ([6402304](https://github.com/DerYeger/d3-graph-controller/commit/6402304e92b1d7a5bf18e08a6c1f66418ea39a13)) 229 | 230 | ## [2.2.52](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.51...v2.2.52) (2022-08-03) 231 | 232 | 233 | ### Bug Fixes 234 | 235 | * **release:** schedule release ([7b739de](https://github.com/DerYeger/d3-graph-controller/commit/7b739dec5b6413766e15d9d8be993724121cdf30)) 236 | 237 | ## [2.2.51](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.50...v2.2.51) (2022-07-26) 238 | 239 | 240 | ### Bug Fixes 241 | 242 | * **deps:** update all non-major dependencies ([6b512a2](https://github.com/DerYeger/d3-graph-controller/commit/6b512a2b3e04f0d0b6c9abe0912d5cecfaf6f554)) 243 | 244 | ## [2.2.50](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.49...v2.2.50) (2022-07-19) 245 | 246 | 247 | ### Bug Fixes 248 | 249 | * **deps:** update dependency ts-deepmerge to v2.0.4 ([3c42ac0](https://github.com/DerYeger/d3-graph-controller/commit/3c42ac05f6e68b74ca70091f290e12e64f1e2e79)) 250 | 251 | ## [2.2.49](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.48...v2.2.49) (2022-07-18) 252 | 253 | 254 | ### Bug Fixes 255 | 256 | * **release:** schedule release ([b2a5582](https://github.com/DerYeger/d3-graph-controller/commit/b2a558226dcc8a1cb50db8e18033855bbc1472c1)) 257 | 258 | ## [2.2.48](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.47...v2.2.48) (2022-07-11) 259 | 260 | 261 | ### Bug Fixes 262 | 263 | * **deps:** update dependency ts-deepmerge to v2.0.3 ([cdbbd44](https://github.com/DerYeger/d3-graph-controller/commit/cdbbd44a73cdafe8d99a18b5892bfc4af7363e66)) 264 | 265 | ## [2.2.47](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.46...v2.2.47) (2022-07-10) 266 | 267 | 268 | ### Bug Fixes 269 | 270 | * **deps:** update dependency @yeger/debounce to v1.0.23 ([3513049](https://github.com/DerYeger/d3-graph-controller/commit/35130499a26914ae40d41bc6ae4db458afa90cd4)) 271 | 272 | ## [2.2.46](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.45...v2.2.46) (2022-07-09) 273 | 274 | 275 | ### Bug Fixes 276 | 277 | * **deps:** update dependency vecti to v2.0.24 ([b9c5fc0](https://github.com/DerYeger/d3-graph-controller/commit/b9c5fc0412bc0997ea7fbafaa509258bc49e76f0)) 278 | 279 | ## [2.2.45](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.44...v2.2.45) (2022-07-03) 280 | 281 | 282 | ### Bug Fixes 283 | 284 | * **deps:** update all non-major dependencies ([9d581e7](https://github.com/DerYeger/d3-graph-controller/commit/9d581e79f5bb27f6e79f2b9b5509bb0e98d033ff)) 285 | 286 | ## [2.2.44](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.43...v2.2.44) (2022-06-26) 287 | 288 | 289 | ### Bug Fixes 290 | 291 | * **release:** schedule release ([4b36cd6](https://github.com/DerYeger/d3-graph-controller/commit/4b36cd6b025b6743e8d61accc7855f921f978e30)) 292 | 293 | ## [2.2.43](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.42...v2.2.43) (2022-06-18) 294 | 295 | 296 | ### Bug Fixes 297 | 298 | * **deps:** update dependency @yeger/debounce to v1.0.20 ([67a3fc1](https://github.com/DerYeger/d3-graph-controller/commit/67a3fc18ed5276a45fab1b027504347b2b242775)) 299 | 300 | ## [2.2.42](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.41...v2.2.42) (2022-06-17) 301 | 302 | 303 | ### Bug Fixes 304 | 305 | * **deps:** update dependency vecti to v2.0.21 ([a10ab6e](https://github.com/DerYeger/d3-graph-controller/commit/a10ab6e28ea13db7db21577a11d30b53ffa20dc2)) 306 | 307 | ## [2.2.41](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.40...v2.2.41) (2022-06-12) 308 | 309 | 310 | ### Bug Fixes 311 | 312 | * **deps:** update dependency @yeger/debounce to v1.0.19 ([b740d21](https://github.com/DerYeger/d3-graph-controller/commit/b740d21c50e431cdb90ae7b44ff763e0ba17f317)) 313 | 314 | ## [2.2.40](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.39...v2.2.40) (2022-06-09) 315 | 316 | 317 | ### Bug Fixes 318 | 319 | * **deps:** update dependency vecti to v2.0.20 ([5879953](https://github.com/DerYeger/d3-graph-controller/commit/58799532ea3be01ccd11e03ba843d602d454fd8d)) 320 | 321 | ## [2.2.39](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.38...v2.2.39) (2022-06-04) 322 | 323 | 324 | ### Bug Fixes 325 | 326 | * **deps:** update dependency @yeger/debounce to v1.0.18 ([bce5900](https://github.com/DerYeger/d3-graph-controller/commit/bce59000a407af52cb55062c53584128cef64c43)) 327 | 328 | ## [2.2.38](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.37...v2.2.38) (2022-05-31) 329 | 330 | 331 | ### Bug Fixes 332 | 333 | * **deps:** update dependency vecti to v2.0.19 ([a034d97](https://github.com/DerYeger/d3-graph-controller/commit/a034d97520cb6e1007be7e70572ca524f6129961)) 334 | 335 | ## [2.2.37](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.36...v2.2.37) (2022-05-28) 336 | 337 | 338 | ### Bug Fixes 339 | 340 | * **deps:** update dependency @yeger/debounce to v1.0.17 ([a25121d](https://github.com/DerYeger/d3-graph-controller/commit/a25121df8b72a98d8795bd957d5c96911f453704)) 341 | 342 | ## [2.2.36](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.35...v2.2.36) (2022-05-23) 343 | 344 | 345 | ### Bug Fixes 346 | 347 | * **deps:** update dependency vecti to v2.0.18 ([7beb41c](https://github.com/DerYeger/d3-graph-controller/commit/7beb41c008ba19c8ec260c92fb9ffb7146366fd7)) 348 | 349 | ## [2.2.35](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.34...v2.2.35) (2022-05-20) 350 | 351 | 352 | ### Bug Fixes 353 | 354 | * **deps:** update dependency @yeger/debounce to v1.0.16 ([45210c8](https://github.com/DerYeger/d3-graph-controller/commit/45210c87798251b22ee3d2caecf99fd036e21540)) 355 | 356 | ## [2.2.34](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.33...v2.2.34) (2022-05-17) 357 | 358 | 359 | ### Bug Fixes 360 | 361 | * **deps:** update dependency vecti to v2.0.17 ([fee29e0](https://github.com/DerYeger/d3-graph-controller/commit/fee29e08f01c5edb2b8bc68b54baae9f36043462)) 362 | 363 | ## [2.2.33](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.32...v2.2.33) (2022-05-13) 364 | 365 | 366 | ### Bug Fixes 367 | 368 | * **deps:** update dependency @yeger/debounce to v1.0.15 ([8743192](https://github.com/DerYeger/d3-graph-controller/commit/874319201eeea239c027797988619ca703b00940)) 369 | 370 | ## [2.2.32](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.31...v2.2.32) (2022-05-08) 371 | 372 | 373 | ### Bug Fixes 374 | 375 | * **deps:** update dependency vecti to v2.0.16 ([1991032](https://github.com/DerYeger/d3-graph-controller/commit/199103263e66e7c207fcffd300201f6d16c06bb0)) 376 | 377 | ## [2.2.31](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.30...v2.2.31) (2022-05-05) 378 | 379 | 380 | ### Bug Fixes 381 | 382 | * **deps:** update dependency @yeger/debounce to v1.0.14 ([33c1b4c](https://github.com/DerYeger/d3-graph-controller/commit/33c1b4cbcb2f224cbb7ec523dff23bfcaed18dca)) 383 | 384 | ## [2.2.30](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.29...v2.2.30) (2022-04-30) 385 | 386 | 387 | ### Bug Fixes 388 | 389 | * **deps:** update dependency vecti to v2.0.15 ([64f030a](https://github.com/DerYeger/d3-graph-controller/commit/64f030a4a88b6b2ef503f6706f8448ae8c01c376)) 390 | 391 | ## [2.2.29](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.28...v2.2.29) (2022-04-29) 392 | 393 | 394 | ### Bug Fixes 395 | 396 | * **deps:** update dependency @yeger/debounce to v1.0.13 ([a3772ee](https://github.com/DerYeger/d3-graph-controller/commit/a3772eee57de8906afc97386c8810decff861072)) 397 | 398 | ## [2.2.28](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.27...v2.2.28) (2022-04-23) 399 | 400 | 401 | ### Bug Fixes 402 | 403 | * **deps:** update dependency vecti to v2.0.14 ([dab77aa](https://github.com/DerYeger/d3-graph-controller/commit/dab77aab4a530873553d2ef53ae0840c4676e6ab)) 404 | 405 | ## [2.2.27](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.26...v2.2.27) (2022-04-19) 406 | 407 | 408 | ### Bug Fixes 409 | 410 | * **deps:** update dependency @yeger/debounce to v1.0.12 ([dc8145b](https://github.com/DerYeger/d3-graph-controller/commit/dc8145b0c97a833761b37ebd3f9f6c74c4e8d6f3)) 411 | 412 | ## [2.2.26](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.25...v2.2.26) (2022-04-16) 413 | 414 | 415 | ### Bug Fixes 416 | 417 | * **deps:** update dependency vecti to v2.0.13 ([074fbab](https://github.com/DerYeger/d3-graph-controller/commit/074fbabf430939deb08ae153321b36fd4e364a35)) 418 | 419 | ## [2.2.25](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.24...v2.2.25) (2022-04-11) 420 | 421 | 422 | ### Bug Fixes 423 | 424 | * **deps:** update dependency @yeger/debounce to v1.0.11 ([0708a7d](https://github.com/DerYeger/d3-graph-controller/commit/0708a7d200e90210e10083ee7955051600f9a1cb)) 425 | 426 | ## [2.2.24](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.23...v2.2.24) (2022-04-09) 427 | 428 | 429 | ### Bug Fixes 430 | 431 | * **deps:** update dependency vecti to v2.0.12 ([5b9e1dd](https://github.com/DerYeger/d3-graph-controller/commit/5b9e1dd7d0730463e0791b0ca64fa19c681e13f2)) 432 | 433 | ## [2.2.23](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.22...v2.2.23) (2022-04-04) 434 | 435 | 436 | ### Bug Fixes 437 | 438 | * **deps:** update dependency @yeger/debounce to v1.0.10 ([a58fa2a](https://github.com/DerYeger/d3-graph-controller/commit/a58fa2a543280167151369da2b8b395ea13ebd93)) 439 | 440 | ## [2.2.22](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.21...v2.2.22) (2022-04-02) 441 | 442 | 443 | ### Bug Fixes 444 | 445 | * **deps:** update dependency @yeger/debounce to v1.0.9 ([57114f3](https://github.com/DerYeger/d3-graph-controller/commit/57114f32a528db60237f079f3354fe451adc8547)) 446 | 447 | ## [2.2.21](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.20...v2.2.21) (2022-04-01) 448 | 449 | 450 | ### Bug Fixes 451 | 452 | * **deps:** update dependency vecti to v2.0.11 ([d096471](https://github.com/DerYeger/d3-graph-controller/commit/d0964715fcc1d8af17422c5b18a57f0ac461b900)) 453 | 454 | ## [2.2.20](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.19...v2.2.20) (2022-03-27) 455 | 456 | 457 | ### Bug Fixes 458 | 459 | * **deps:** update dependency @yeger/debounce to v1.0.8 ([41e0fde](https://github.com/DerYeger/d3-graph-controller/commit/41e0fde2606d6ac920576cea4cf549a1659b1ca2)) 460 | 461 | ## [2.2.19](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.18...v2.2.19) (2022-03-23) 462 | 463 | 464 | ### Bug Fixes 465 | 466 | * **deps:** update dependency vecti to v2.0.10 ([17a855f](https://github.com/DerYeger/d3-graph-controller/commit/17a855f352eb9ef22e8b60daf752d3e44de28b63)) 467 | 468 | ## [2.2.18](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.17...v2.2.18) (2022-03-19) 469 | 470 | 471 | ### Bug Fixes 472 | 473 | * **deps:** update dependency @yeger/debounce to v1.0.7 ([e808d51](https://github.com/DerYeger/d3-graph-controller/commit/e808d5134382acc25e9f5282da521de73286de16)) 474 | 475 | ## [2.2.17](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.16...v2.2.17) (2022-03-16) 476 | 477 | 478 | ### Bug Fixes 479 | 480 | * **deps:** update dependency vecti to v2.0.9 ([bb97f22](https://github.com/DerYeger/d3-graph-controller/commit/bb97f2274e5be14397110f473cd27b8fb7e701fc)) 481 | 482 | ## [2.2.16](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.15...v2.2.16) (2022-03-12) 483 | 484 | 485 | ### Bug Fixes 486 | 487 | * **deps:** update dependency @yeger/debounce to v1.0.6 ([6cb2844](https://github.com/DerYeger/d3-graph-controller/commit/6cb2844c76f6d31c89d06ea8a451ad5488e6dcd9)) 488 | 489 | ## [2.2.15](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.14...v2.2.15) (2022-03-09) 490 | 491 | 492 | ### Bug Fixes 493 | 494 | * **deps:** update dependency vecti to v2.0.8 ([9c83dd2](https://github.com/DerYeger/d3-graph-controller/commit/9c83dd21111812af4804cee006d6ec83e97e5b03)) 495 | 496 | ## [2.2.14](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.13...v2.2.14) (2022-03-03) 497 | 498 | 499 | ### Bug Fixes 500 | 501 | * **deps:** update dependency @yeger/debounce to v1.0.5 ([9cd1280](https://github.com/DerYeger/d3-graph-controller/commit/9cd128098c69bd7aa014302a082d6d069a6b4d73)) 502 | 503 | ## [2.2.13](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.12...v2.2.13) (2022-03-01) 504 | 505 | 506 | ### Bug Fixes 507 | 508 | * **deps:** update dependency vecti to v2.0.7 ([7d2ae06](https://github.com/DerYeger/d3-graph-controller/commit/7d2ae069e7c969f14c764e2cdb50071e176c7f20)) 509 | 510 | ## [2.2.12](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.11...v2.2.12) (2022-02-24) 511 | 512 | 513 | ### Bug Fixes 514 | 515 | * **deps:** update dependency @yeger/debounce to v1.0.4 ([d10ae73](https://github.com/DerYeger/d3-graph-controller/commit/d10ae732f1538e8ae20304af08ca5b886a43c333)) 516 | 517 | ## [2.2.11](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.10...v2.2.11) (2022-02-20) 518 | 519 | 520 | ### Bug Fixes 521 | 522 | * **deps:** update dependency vecti to v2.0.6 ([b8bcbe2](https://github.com/DerYeger/d3-graph-controller/commit/b8bcbe291cbe582f4079c16f052ab41510333db8)) 523 | 524 | ## [2.2.10](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.9...v2.2.10) (2022-02-16) 525 | 526 | 527 | ### Bug Fixes 528 | 529 | * **deps:** update dependency @yeger/debounce to v1.0.3 ([2dff141](https://github.com/DerYeger/d3-graph-controller/commit/2dff1410ca99866c4100dbab057ba6dc8cfd8991)) 530 | 531 | ## [2.2.9](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.8...v2.2.9) (2022-02-13) 532 | 533 | 534 | ### Bug Fixes 535 | 536 | * **deps:** update dependency vecti to v2.0.5 ([09040a1](https://github.com/DerYeger/d3-graph-controller/commit/09040a1ed3bcde5d8f0f83f44884df258ba43441)) 537 | 538 | ## [2.2.8](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.7...v2.2.8) (2022-02-07) 539 | 540 | 541 | ### Bug Fixes 542 | 543 | * **deps:** update dependency @yeger/debounce to v1.0.2 ([5ff6ada](https://github.com/DerYeger/d3-graph-controller/commit/5ff6ada7275ca0ae12949057991ab45afbe12669)) 544 | 545 | ## [2.2.7](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.6...v2.2.7) (2022-02-04) 546 | 547 | 548 | ### Bug Fixes 549 | 550 | * **deps:** update dependency vecti to v2.0.4 ([b399431](https://github.com/DerYeger/d3-graph-controller/commit/b399431ff8684540bb6d5bdc826fa9482cac6f8d)) 551 | 552 | ## [2.2.6](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.5...v2.2.6) (2022-01-30) 553 | 554 | 555 | ### Bug Fixes 556 | 557 | * **deps:** update dependency @yeger/debounce to v1.0.1 ([7cfb1f0](https://github.com/DerYeger/d3-graph-controller/commit/7cfb1f094109cc5c34eb951e57b58ad66db1dbde)) 558 | 559 | ## [2.2.5](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.4...v2.2.5) (2022-01-28) 560 | 561 | 562 | ### Bug Fixes 563 | 564 | * **deps:** update dependency vecti to v2.0.3 ([98a587d](https://github.com/DerYeger/d3-graph-controller/commit/98a587db0839a6542c683e0427d0639a4cd59cc2)) 565 | 566 | ## [2.2.4](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.3...v2.2.4) (2022-01-22) 567 | 568 | 569 | ### Bug Fixes 570 | 571 | * use `@yeger/debounce` ([413f7fe](https://github.com/DerYeger/d3-graph-controller/commit/413f7fe1adabd6eee58079816b8797960e7a76b2)) 572 | 573 | ## [2.2.3](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.2...v2.2.3) (2022-01-21) 574 | 575 | 576 | ### Bug Fixes 577 | 578 | * **deps:** update dependency ts-deepmerge to v2 ([dc0b82e](https://github.com/DerYeger/d3-graph-controller/commit/dc0b82edafac4573b869d95ff6d252c101cecc15)) 579 | 580 | ## [2.2.2](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.1...v2.2.2) (2022-01-20) 581 | 582 | 583 | ### Bug Fixes 584 | 585 | * **deps:** update dependency vecti to v2.0.2 ([9bc3365](https://github.com/DerYeger/d3-graph-controller/commit/9bc3365aea6c9284f15687dfe61b8f692d64386c)) 586 | 587 | ## [2.2.1](https://github.com/DerYeger/d3-graph-controller/compare/v2.2.0...v2.2.1) (2022-01-15) 588 | 589 | 590 | ### Bug Fixes 591 | 592 | * **deps:** update dependency vecti to v2.0.1 ([c26d56f](https://github.com/DerYeger/d3-graph-controller/commit/c26d56f8647555712157c2104816d79e058e80db)) 593 | 594 | # [2.2.0](https://github.com/DerYeger/d3-graph-controller/compare/v2.1.0...v2.2.0) (2022-01-09) 595 | 596 | 597 | ### Features 598 | 599 | * **config:** expose additional modifiers ([1705dfb](https://github.com/DerYeger/d3-graph-controller/commit/1705dfbb6468f27411c1ad7f763dd4af4d2f17df)) 600 | 601 | # [2.1.0](https://github.com/DerYeger/d3-graph-controller/compare/v2.0.0...v2.1.0) (2022-01-09) 602 | 603 | 604 | ### Features 605 | 606 | * **config:** accept number for `nodeRadius` ([5bd7d31](https://github.com/DerYeger/d3-graph-controller/commit/5bd7d312383e40affe661f8d658a372d5d928daf)) 607 | 608 | # [2.0.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.16.1...v2.0.0) (2022-01-08) 609 | 610 | 611 | ### Code Refactoring 612 | 613 | * **config:** add `simulation` property ([e707ca1](https://github.com/DerYeger/d3-graph-controller/commit/e707ca18025430ad25f313e0476a53f913ec88d2)) 614 | * **config:** make all properties `readonly` ([6aace25](https://github.com/DerYeger/d3-graph-controller/commit/6aace2558ff22947d70bcf513fabc3da1d4f4cda)) 615 | * **config:** make config properties `readonly` ([84d4803](https://github.com/DerYeger/d3-graph-controller/commit/84d4803199aa7dbbc5f2e45532d9c8b7b07a43c2)) 616 | * do not export lib ([4fed1a3](https://github.com/DerYeger/d3-graph-controller/commit/4fed1a35c9482a8f3d7391fb35be3f5b82931f0f)) 617 | * **model:** make most properties `readonly` ([d8fa7be](https://github.com/DerYeger/d3-graph-controller/commit/d8fa7be8904772b2d5487595d15c0bd3f4c2145e)) 618 | * move link length config to its force ([1b7af94](https://github.com/DerYeger/d3-graph-controller/commit/1b7af94dafbcc7f438941931df3431c8c8cd8d0b)) 619 | * rename `MarkerConfig` properties ([2330800](https://github.com/DerYeger/d3-graph-controller/commit/233080017dad1e8bb40955ef244c23c5e466786b)) 620 | 621 | 622 | ### Features 623 | 624 | * **model:** improve label model ([8502d42](https://github.com/DerYeger/d3-graph-controller/commit/8502d420dc97068b6a4d0d84bfb60f909cbba881)) 625 | 626 | 627 | ### BREAKING CHANGES 628 | 629 | * **model:** Merged label properties into single property 630 | * **model:** most model properties are now `readonly` 631 | * **config:** config properties are now `readonly` 632 | * link length config has been moved 633 | * **config:** config properties are now `readonly` 634 | * **config:** Various properties have been moved 635 | * `MarkerConfig` properties have been renamed 636 | * lib is no longer exported 637 | 638 | ## [1.16.1](https://github.com/DerYeger/d3-graph-controller/compare/v1.16.0...v1.16.1) (2022-01-08) 639 | 640 | 641 | ### Bug Fixes 642 | 643 | * add TSDoc to config ([d59d500](https://github.com/DerYeger/d3-graph-controller/commit/d59d5001f34c5be285afa7008e64a82c2294a700)) 644 | * add TSDoc to controller ([ee0d918](https://github.com/DerYeger/d3-graph-controller/commit/ee0d918aa0d7ffb0ad35145c430d6682564de79a)) 645 | * add TSDoc to model ([8c3baf1](https://github.com/DerYeger/d3-graph-controller/commit/8c3baf1acb74000e709b9d9cbaa1a66ea4f7887d)) 646 | * **node:** always set default values for internal properties ([22f4a90](https://github.com/DerYeger/d3-graph-controller/commit/22f4a903e71af0224b63ec27e9393b068d0eca40)) 647 | 648 | # [1.16.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.15.1...v1.16.0) (2022-01-08) 649 | 650 | 651 | ### Features 652 | 653 | * add `defineGraph` helper ([46b2c4b](https://github.com/DerYeger/d3-graph-controller/commit/46b2c4b25cab81c9d090cddcf89f9ffa8a23361e)) 654 | 655 | ## [1.15.1](https://github.com/DerYeger/d3-graph-controller/compare/v1.15.0...v1.15.1) (2022-01-07) 656 | 657 | 658 | ### Bug Fixes 659 | 660 | * apply `graph` class to container automatically ([2f39c30](https://github.com/DerYeger/d3-graph-controller/commit/2f39c30395b20d7ad4d2141cdebe7e3d0fbdd7b1)) 661 | 662 | # [1.15.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.14.1...v1.15.0) (2022-01-07) 663 | 664 | 665 | ### Bug Fixes 666 | 667 | * **simulation:** use effective center ([82cbbd0](https://github.com/DerYeger/d3-graph-controller/commit/82cbbd02ca80a9a564d97479e9e5bf1efd4df288)) 668 | 669 | 670 | ### Features 671 | 672 | * implement automatic resizing ([95cda68](https://github.com/DerYeger/d3-graph-controller/commit/95cda68a186d24ee9f1c56b080b8b25b22a87a81)) 673 | 674 | 675 | ### Performance Improvements 676 | 677 | * do not redraw on resize ([30bb0c3](https://github.com/DerYeger/d3-graph-controller/commit/30bb0c3a42574e266d9acc4859de4a4c877999f4)) 678 | 679 | ## [1.14.1](https://github.com/DerYeger/d3-graph-controller/compare/v1.14.0...v1.14.1) (2022-01-06) 680 | 681 | 682 | ### Bug Fixes 683 | 684 | * consider effective size for simulation ([163f551](https://github.com/DerYeger/d3-graph-controller/commit/163f551941e83cb290a816455ae914ee85600c85)) 685 | * properly calculate graph center ([1ba6d74](https://github.com/DerYeger/d3-graph-controller/commit/1ba6d7474c5ab469e87a49db084284f76e320287)) 686 | * **zoom:** call `zoom.transform` ([519b263](https://github.com/DerYeger/d3-graph-controller/commit/519b2630d09c6e2ed3af28d6090d5fdc213fe0c2)) 687 | 688 | # [1.14.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.13.3...v1.14.0) (2022-01-06) 689 | 690 | 691 | ### Bug Fixes 692 | 693 | * **zoom:** persist state on resize ([cce354d](https://github.com/DerYeger/d3-graph-controller/commit/cce354d4fa63b7cd80287922b5d0f2bc6ef30988)) 694 | 695 | 696 | ### Features 697 | 698 | * **config:** make initial and boundary zoom values configurable ([f2fb7bc](https://github.com/DerYeger/d3-graph-controller/commit/f2fb7bc76dc04ac42bf53dfe318ef12eb07364df)) 699 | 700 | ## [1.13.3](https://github.com/DerYeger/d3-graph-controller/compare/v1.13.2...v1.13.3) (2022-01-06) 701 | 702 | 703 | ### Bug Fixes 704 | 705 | * use named export ([4ee23d7](https://github.com/DerYeger/d3-graph-controller/commit/4ee23d7660af3dca3a998286c2584e7cb6879abf)) 706 | 707 | ## [1.13.2](https://github.com/DerYeger/d3-graph-controller/compare/v1.13.1...v1.13.2) (2022-01-06) 708 | 709 | 710 | ### Bug Fixes 711 | 712 | * **marker:** use `userSpaceOnUse` as `markerUnits` ([bd146d7](https://github.com/DerYeger/d3-graph-controller/commit/bd146d71883ab5258b47943d36864f991e8aa2e9)) 713 | 714 | ## [1.13.1](https://github.com/DerYeger/d3-graph-controller/compare/v1.13.0...v1.13.1) (2022-01-05) 715 | 716 | 717 | ### Bug Fixes 718 | 719 | * **config:** do not use `any` type ([4de3194](https://github.com/DerYeger/d3-graph-controller/commit/4de31949901dd44711493702e73d36e5b8a3268a)) 720 | 721 | # [1.13.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.12.0...v1.13.0) (2022-01-05) 722 | 723 | 724 | ### Bug Fixes 725 | 726 | * **nodes:** only trigger `onNodeSelected` if double-clicked ([68841e3](https://github.com/DerYeger/d3-graph-controller/commit/68841e3e491e7b112efbfe9a71946c9346fa2edf)) 727 | 728 | 729 | ### Features 730 | 731 | * **config:** expose low-level modifier for nodes ([7742d34](https://github.com/DerYeger/d3-graph-controller/commit/7742d34e4259b4cc8da7a2bb54fa5a54951d1cee)) 732 | 733 | # [1.12.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.11.0...v1.12.0) (2022-01-05) 734 | 735 | 736 | ### Features 737 | 738 | * **config:** add configurable `nodeSelected` callback ([c7eb223](https://github.com/DerYeger/d3-graph-controller/commit/c7eb223197c51266d66386b0373a9098985b0139)) 739 | 740 | # [1.11.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.10.1...v1.11.0) (2022-01-05) 741 | 742 | 743 | ### Features 744 | 745 | * **config:** make resize alpha configurable by context ([e940f53](https://github.com/DerYeger/d3-graph-controller/commit/e940f5304a99899b18f3d46590be3f16912e9f3a)) 746 | 747 | ## [1.10.1](https://github.com/DerYeger/d3-graph-controller/compare/v1.10.0...v1.10.1) (2022-01-03) 748 | 749 | 750 | ### Bug Fixes 751 | 752 | * **deps:** update dependency vecti to v2.0.0 ([0cea900](https://github.com/DerYeger/d3-graph-controller/commit/0cea9008642cd37917025eeea8e8c842875eaaae)) 753 | 754 | # [1.10.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.9.0...v1.10.0) (2022-01-01) 755 | 756 | 757 | ### Features 758 | 759 | * **config:** make alpha for resizing configurable ([aaf6010](https://github.com/DerYeger/d3-graph-controller/commit/aaf6010939c93e95f0bdc60caee925cf3bee571e)) 760 | 761 | # [1.9.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.8.1...v1.9.0) (2022-01-01) 762 | 763 | 764 | ### Features 765 | 766 | * **config:** make alpha values configurable ([b5e0d81](https://github.com/DerYeger/d3-graph-controller/commit/b5e0d81a4e270269d539ef13f49d1ca959903ba4)) 767 | 768 | ## [1.8.1](https://github.com/DerYeger/d3-graph-controller/compare/v1.8.0...v1.8.1) (2021-12-30) 769 | 770 | 771 | ### Bug Fixes 772 | 773 | * **deps:** update dependency vecti to v1.0.2 ([4f15890](https://github.com/DerYeger/d3-graph-controller/commit/4f15890c1baf53989eef42b7f87261575b14ab09)) 774 | 775 | # [1.8.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.7.0...v1.8.0) (2021-12-29) 776 | 777 | 778 | ### Features 779 | 780 | * **paths:** replace `ts-matrix` with `vecti` ([b61e394](https://github.com/DerYeger/d3-graph-controller/commit/b61e394feb0430c546a5afcb5299449cde7676a9)) 781 | * **paths:** use custom svg line builder ([5461c1b](https://github.com/DerYeger/d3-graph-controller/commit/5461c1b06acc8ae4120b107f637812469210fdb0)) 782 | 783 | # [1.7.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.6.0...v1.7.0) (2021-12-29) 784 | 785 | 786 | ### Features 787 | 788 | * **paths:** replace `ml-matrix` with `ts-matrix` ([0887b53](https://github.com/DerYeger/d3-graph-controller/commit/0887b53045dc6da1763d7c9da678e115a85e3e44)) 789 | 790 | # [1.6.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.5.0...v1.6.0) (2021-12-28) 791 | 792 | 793 | ### Bug Fixes 794 | 795 | * **config:** merge arrays in config ([d7ebc7f](https://github.com/DerYeger/d3-graph-controller/commit/d7ebc7fea5e659255627aded08648e9a3feffb87)) 796 | 797 | 798 | ### Features 799 | 800 | * **config:** deep-merge config with default config ([ae65c38](https://github.com/DerYeger/d3-graph-controller/commit/ae65c38adddcebfc6aab495113fa51724dbb17ec)) 801 | 802 | 803 | ### Performance Improvements 804 | 805 | * **style:** remove shadow filter from default theme ([3316e44](https://github.com/DerYeger/d3-graph-controller/commit/3316e44c0350d659af89ee4ed7c0a7683311fe31)) 806 | 807 | # [1.5.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.4.0...v1.5.0) (2021-12-28) 808 | 809 | 810 | ### Features 811 | 812 | * make location initialization configurable ([2791ec4](https://github.com/DerYeger/d3-graph-controller/commit/2791ec46a3a11c1339c5eb1724a633566053a45d)) 813 | 814 | # [1.4.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.3.0...v1.4.0) (2021-12-28) 815 | 816 | 817 | ### Features 818 | 819 | * **config:** make `markerConfig` part of `GraphConfig` ([694f4ad](https://github.com/DerYeger/d3-graph-controller/commit/694f4adf33d3dd8c03b86d31c325a833642ef369)), closes [#2](https://github.com/DerYeger/d3-graph-controller/issues/2) 820 | 821 | # [1.3.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.2.0...v1.3.0) (2021-12-27) 822 | 823 | 824 | ### Features 825 | 826 | * make all colors of `default.css` configurable via variables ([19436d3](https://github.com/DerYeger/d3-graph-controller/commit/19436d3237a8bc94addb28dc43927fbc94c44bc7)), closes [#3](https://github.com/DerYeger/d3-graph-controller/issues/3) 827 | 828 | # [1.2.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.1.0...v1.2.0) (2021-12-23) 829 | 830 | 831 | ### Features 832 | 833 | * **simulation:** make collision force radius multiplier configurable ([9f10136](https://github.com/DerYeger/d3-graph-controller/commit/9f10136b063e181bf3fad5d3e7e9a48ee7bc8cea)) 834 | 835 | # [1.1.0](https://github.com/DerYeger/d3-graph-controller/compare/v1.0.0...v1.1.0) (2021-12-23) 836 | 837 | 838 | ### Features 839 | 840 | * **model:** improve helper methods ([9320d78](https://github.com/DerYeger/d3-graph-controller/commit/9320d785c79ac0980bde50dcf5290f802c209670)) 841 | * **simulation:** improve default force configuration ([a7e2fde](https://github.com/DerYeger/d3-graph-controller/commit/a7e2fde48f2db81f42e24b9c27259a53a8b1858c)) 842 | * **style:** disable touch callout for all elements in graph ([2925c80](https://github.com/DerYeger/d3-graph-controller/commit/2925c807f243589ee965f32204eea96377be76f9)) 843 | 844 | # 1.0.0 (2021-12-22) 845 | 846 | 847 | ### Features 848 | 849 | * create project ([9b3c4d4](https://github.com/DerYeger/d3-graph-controller/commit/9b3c4d482ac43e13c63cb67bd1baf50c30fe7e03)) 850 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jan Müller 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 | # d3-graph-controller 2 | 3 | > This repository was moved to [DerYeger/yeger](https://github.com/DerYeger/yeger/tree/main/packages/d3-graph-controller). 4 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 85..100 3 | round: down 4 | precision: 2 5 | status: 6 | project: 7 | default: 8 | target: 85 9 | threshold: 5 10 | base: auto 11 | patch: 12 | default: 13 | target: 50 14 | threshold: 5 15 | base: auto 16 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /default.css: -------------------------------------------------------------------------------- 1 | .graph, 2 | .graph > svg { 3 | display: block; 4 | } 5 | 6 | .graph { 7 | height: 100%; 8 | touch-action: none; 9 | width: 100%; 10 | } 11 | 12 | .graph * { 13 | -webkit-touch-callout: none !important; 14 | -webkit-user-select: none !important; 15 | -moz-user-select: none !important; 16 | -ms-user-select: none !important; 17 | user-select: none !important; 18 | } 19 | 20 | .link { 21 | fill: none; 22 | stroke-width: 4px; 23 | } 24 | 25 | .node { 26 | --color-stroke: var(--color-node-stroke, rgba(0, 0, 0, 0.5)); 27 | 28 | cursor: pointer; 29 | stroke: none; 30 | stroke-width: 2px; 31 | transition: filter 0.25s ease, stroke 0.25s ease, stroke-dasharray 0.25s ease; 32 | } 33 | 34 | .node:hover:not(.focused) { 35 | filter: brightness(80%); 36 | stroke: var(--color-stroke); 37 | stroke-dasharray: 4px; 38 | } 39 | 40 | .node.focused { 41 | stroke: var(--color-stroke); 42 | } 43 | 44 | .link__label, 45 | .node__label { 46 | pointer-events: none; 47 | text-anchor: middle; 48 | } 49 | 50 | .grabbed { 51 | cursor: grabbing !important; 52 | } 53 | -------------------------------------------------------------------------------- /docs/.vitepress/components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/core/pull/3399 4 | import '@vue/runtime-core' 5 | 6 | export {} 7 | 8 | declare module '@vue/runtime-core' { 9 | export interface GlobalComponents { 10 | Demo: typeof import('./components/Demo.vue')['default'] 11 | GraphView: typeof import('./components/GraphView.vue')['default'] 12 | Home: typeof import('./components/Home.vue')['default'] 13 | Status: typeof import('./components/Status.vue')['default'] 14 | UsedIn: typeof import('./components/UsedIn.vue')['default'] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Demo.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 27 | 28 | 49 | 50 | 62 | -------------------------------------------------------------------------------- /docs/.vitepress/components/GraphView.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 83 | 84 | 147 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Home.vue: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Status.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 56 | 57 | 78 | -------------------------------------------------------------------------------- /docs/.vitepress/components/UsedIn.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 35 | 36 | 109 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | import Package from '../../package.json' 4 | 5 | const ogImage = `${Package.homepage}/logo.png` 6 | 7 | export default defineConfig({ 8 | // site config 9 | lang: 'en-US', 10 | title: Package.name, 11 | description: Package.description, 12 | 13 | head: [ 14 | ['meta', { property: 'og:title', content: Package.name }], 15 | [ 16 | 'meta', 17 | { 18 | property: 'og:description', 19 | content: Package.description, 20 | }, 21 | ], 22 | ['meta', { property: 'og:url', content: Package.homepage }], 23 | [ 24 | 'meta', 25 | { 26 | property: 'og:image', 27 | content: ogImage, 28 | }, 29 | ], 30 | ['meta', { name: 'twitter:title', content: Package.name }], 31 | [ 32 | 'meta', 33 | { 34 | name: 'twitter:description', 35 | content: Package.description, 36 | }, 37 | ], 38 | [ 39 | 'meta', 40 | { 41 | name: 'twitter:image', 42 | content: ogImage, 43 | }, 44 | ], 45 | ['meta', { name: 'twitter:card', content: 'summary_large_image' }], 46 | ['link', { rel: 'icon', href: '/logo.svg', type: 'image/svg+xml' }], 47 | ], 48 | 49 | // theme and its config 50 | themeConfig: { 51 | editLink: { 52 | pattern: 53 | 'https://github.com/DerYeger/d3-graph-controller/tree/master/docs/:path', 54 | text: 'Suggest changes to this page', 55 | }, 56 | 57 | logo: '/logo.svg', 58 | 59 | algolia: { 60 | appId: 'P6B0O55SU2', 61 | apiKey: '8191ba63a5c47585bbc996cd8db4f201', 62 | indexName: 'd3-graph-controller', 63 | }, 64 | 65 | nav: [ 66 | { text: 'Home', link: '/' }, 67 | { text: 'Guide', link: '/guide/' }, 68 | { text: 'API', link: '/api/' }, 69 | { text: 'Config', link: '/config/' }, 70 | { text: 'Demo', link: '/demo/' }, 71 | ], 72 | 73 | socialLinks: [ 74 | { icon: 'twitter', link: 'https://twitter.com/DerYeger' }, 75 | { 76 | icon: 'github', 77 | link: 'https://github.com/DerYeger/d3-graph-controller', 78 | }, 79 | ], 80 | 81 | footer: { 82 | message: 'Released under the MIT License.', 83 | copyright: 'Copyright © 2021-PRESENT Jan Müller', 84 | }, 85 | }, 86 | }) 87 | -------------------------------------------------------------------------------- /docs/.vitepress/demo/link.ts: -------------------------------------------------------------------------------- 1 | import type { GraphLink } from 'd3-graph-controller' 2 | import { defineLink } from 'd3-graph-controller' 3 | 4 | import type { DemoType } from './model' 5 | import type { DemoNode } from './node' 6 | import { nodes } from './node' 7 | 8 | export interface DemoLink extends GraphLink { 9 | weight: number 10 | } 11 | 12 | export function defineDemoLink( 13 | source: DemoNode, 14 | target: DemoNode, 15 | weight: number 16 | ): DemoLink { 17 | return defineLink({ 18 | source, 19 | target, 20 | color: `var(--color-secondary)`, 21 | label: { 22 | color: 'var(--text-on-secondary)', 23 | fontSize: '1rem', 24 | text: weight.toString(), 25 | }, 26 | weight, 27 | }) 28 | } 29 | 30 | const aToB: DemoLink = defineDemoLink(nodes.a, nodes.b, 1) 31 | 32 | const bToA: DemoLink = defineDemoLink(nodes.b, nodes.a, 5) 33 | 34 | const bToC: DemoLink = defineDemoLink(nodes.b, nodes.c, 1.5) 35 | 36 | const cToC: DemoLink = defineDemoLink(nodes.c, nodes.c, 1) 37 | 38 | export const links = { 39 | aToB, 40 | bToA, 41 | bToC, 42 | cToC, 43 | } 44 | -------------------------------------------------------------------------------- /docs/.vitepress/demo/model.ts: -------------------------------------------------------------------------------- 1 | import type { Graph, GraphConfig, GraphController } from 'd3-graph-controller' 2 | import { defineGraph, defineGraphConfig } from 'd3-graph-controller' 3 | 4 | import type { DemoLink } from './link' 5 | import { links } from './link' 6 | import type { DemoNode } from './node' 7 | import { nodes } from './node' 8 | 9 | export type DemoType = 'primary' | 'secondary' 10 | 11 | export type DemoGraph = Graph 12 | 13 | export const demoGraph: DemoGraph = defineGraph({ 14 | nodes: Object.values(nodes), 15 | links: Object.values(links), 16 | }) 17 | 18 | export type DemoGraphController = GraphController 19 | 20 | export type DemoGraphConfig = GraphConfig 21 | 22 | export const demoGraphConfig: DemoGraphConfig = defineGraphConfig< 23 | DemoType, 24 | DemoNode, 25 | DemoLink 26 | >({ 27 | autoResize: true, 28 | nodeRadius: (node: DemoNode) => node.radiusMultiplier * 32, 29 | simulation: { 30 | forces: { 31 | collision: { 32 | radiusMultiplier: 4, 33 | }, 34 | link: { 35 | length: (link: DemoLink) => link.weight * 128, 36 | }, 37 | }, 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /docs/.vitepress/demo/node.ts: -------------------------------------------------------------------------------- 1 | import type { GraphNode } from 'd3-graph-controller' 2 | import { defineNode } from 'd3-graph-controller' 3 | 4 | import type { DemoType } from './model' 5 | 6 | export interface DemoNode extends GraphNode { 7 | radiusMultiplier: number 8 | } 9 | 10 | export function defineDemoNode( 11 | id: string, 12 | type: DemoType, 13 | radiusMultiplier: number 14 | ): DemoNode { 15 | return defineNode({ 16 | id, 17 | type, 18 | isFocused: false, 19 | color: `var(--color-${type})`, 20 | label: { 21 | color: 'var(--text-on-node)', 22 | fontSize: '1rem', 23 | text: id.toUpperCase(), 24 | }, 25 | radiusMultiplier, 26 | }) 27 | } 28 | 29 | export const nodes = { 30 | a: defineDemoNode('a', 'primary', 1.25), 31 | b: defineDemoNode('b', 'primary', 1), 32 | c: defineDemoNode('c', 'secondary', 0.8), 33 | d: defineDemoNode('d', 'primary', 1), 34 | } 35 | -------------------------------------------------------------------------------- /docs/.vitepress/demo/random-graph.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PositionInitializers, 3 | defineGraph, 4 | defineGraphConfig, 5 | } from 'd3-graph-controller' 6 | 7 | import type { DemoGraph, DemoGraphConfig } from './model' 8 | import type { DemoNode } from './node' 9 | import { defineDemoNode } from './node' 10 | 11 | export const randomGraphConfig: DemoGraphConfig = defineGraphConfig({ 12 | autoResize: true, 13 | nodeRadius: (node: DemoNode) => 4 * node.radiusMultiplier, 14 | initial: { 15 | showLinkLabels: false, 16 | showNodeLabels: false, 17 | }, 18 | positionInitializer: PositionInitializers.Randomized, 19 | simulation: { 20 | forces: { 21 | charge: { 22 | strength: -50, 23 | }, 24 | }, 25 | }, 26 | }) 27 | 28 | export function generateRandomGraph(): DemoGraph { 29 | const nodeCount = 200 30 | const nodes: DemoNode[] = [...new Array(nodeCount)].map((_, id) => 31 | defineDemoNode( 32 | id.toString(), 33 | id % 4 === 1 ? 'secondary' : 'primary', 34 | 1 + Math.random() * (id % (3 * Math.random())) 35 | ) 36 | ) 37 | // const nodeMap = Object.fromEntries(nodes.map((node) => [node.id, node])) 38 | // const links: DemoLink[] = [...new Array(42)].map(() => { 39 | // const source = nodeMap[randomNodeId(nodeCount)] 40 | // const target = nodeMap[randomNodeId(nodeCount)] 41 | // const weight = Math.random() * 3 42 | // return defineDemoLink(source, target, weight) 43 | // }) 44 | 45 | return defineGraph({ 46 | nodes, 47 | }) 48 | } 49 | 50 | // function randomNodeId(nodeCount: number): string { 51 | // return Math.floor(Math.random() * nodeCount).toString() 52 | // } 53 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | `GraphController` has various methods and properties for manipulating graphs at runtime. 4 | These are described in the following sections. 5 | The following setup is omitted from the samples for brevity. 6 | 7 | <<< @/api/samples/setup.ts 8 | 9 | ## Methods 10 | 11 | ### Filter by Node Type 12 | 13 | Graphs can be filtered by node types. 14 | The filter can be updated at runtime as seen below. 15 | 16 | <<< @/api/samples/node-type-filter.ts#snippet{0} 17 | 18 | ### Resize 19 | 20 | While graphs can be [configured to resize automatically](/config/#resizing), manual resizing is also possible. 21 | 22 | <<< @/api/samples/resize.ts#snippet{0} 23 | 24 | ### Restart 25 | 26 | Simulations are automatically restarted when required. 27 | Should the need arise in some edge cases, simulations can be manually restarted using `GraphController.restart`. 28 | 29 | An alpha value defining the _heat_ of the simulation after restarting must be provided. 30 | 31 | <<< @/api/samples/restart.ts#snippet{0} 32 | 33 | ### Shutdown 34 | 35 | Graphs need to be integrated in framework lifecycles. 36 | In particular, it is necessary to stop the simulation and the (optional) automatic resizing. 37 | 38 | <<< @/api/samples/shutdown.ts#snippet{0} 39 | 40 | ::: danger 41 | Not calling `GraphController.shutdown` when a graph is removed can cause memory leaks. 42 | ::: 43 | 44 | ## Properties 45 | 46 | ### Include Unlinked 47 | 48 | Unlinked nodes, i.e., nodes without incoming or outgoing links, can be included or excluded. 49 | The setting can be changed at runtime using the `includeUnlinked` property. 50 | The property can also be read to get the current state. 51 | 52 | <<< @/api/samples/include-unlinked.ts#snippet{0} 53 | 54 | ### Labels 55 | 56 | Node and link labels can be toggled on and off using the respective property. 57 | Both properties can also be read to get the current state. 58 | 59 | <<< @/api/samples/labels.ts#snippet{0} 60 | 61 | ### Link Filter 62 | 63 | Link filters can be changed at runtime by assigning a new value as seen below. 64 | The property can also be read to get the current filter. 65 | 66 | <<< @/api/samples/link-filter.ts#snippet{0} 67 | 68 | ### Node Types 69 | 70 | An array of available and currently filtered node types can be read using properties seen below. 71 | 72 | <<< @/api/samples/node-types.ts#snippet{0} 73 | -------------------------------------------------------------------------------- /docs/api/samples/include-unlinked.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphController, 3 | defineGraph, 4 | defineGraphConfig, 5 | } from 'd3-graph-controller' 6 | 7 | const container = document.getElementById('graph') as HTMLDivElement 8 | const graph = defineGraph({ 9 | /* ... */ 10 | }) 11 | const config = defineGraphConfig({ 12 | /* ... */ 13 | }) 14 | 15 | // #region snippet 16 | const controller = new GraphController(container, graph, config) 17 | 18 | controller.includeUnlinked = false 19 | // #endregion snippet 20 | -------------------------------------------------------------------------------- /docs/api/samples/labels.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphController, 3 | defineGraph, 4 | defineGraphConfig, 5 | } from 'd3-graph-controller' 6 | 7 | const container = document.getElementById('graph') as HTMLDivElement 8 | const graph = defineGraph({ 9 | /* ... */ 10 | }) 11 | const config = defineGraphConfig({ 12 | /* ... */ 13 | }) 14 | 15 | // #region snippet 16 | const controller = new GraphController(container, graph, config) 17 | 18 | controller.showLinkLabels = true 19 | 20 | controller.showNodeLabels = false 21 | // #endregion snippet 22 | -------------------------------------------------------------------------------- /docs/api/samples/link-filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphController, 3 | GraphLink, 4 | defineGraph, 5 | defineGraphConfig, 6 | } from 'd3-graph-controller' 7 | 8 | const container = document.getElementById('graph') as HTMLDivElement 9 | const graph = defineGraph({ 10 | /* ... */ 11 | }) 12 | const config = defineGraphConfig({ 13 | /* ... */ 14 | }) 15 | 16 | // #region snippet 17 | const controller = new GraphController(container, graph, config) 18 | 19 | // Only include reflexive links 20 | controller.linkFilter = (link: GraphLink) => link.source.id === link.target.id 21 | // #endregion snippet 22 | -------------------------------------------------------------------------------- /docs/api/samples/node-type-filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphController, 3 | defineGraph, 4 | defineGraphConfig, 5 | } from 'd3-graph-controller' 6 | 7 | const container = document.getElementById('graph') as HTMLDivElement 8 | const graph = defineGraph({ 9 | /* ... */ 10 | }) 11 | const config = defineGraphConfig({ 12 | /* ... */ 13 | }) 14 | 15 | // #region snippet 16 | const controller = new GraphController(container, graph, config) 17 | 18 | controller.filterNodesByType(true, 'nodeTypeToInclude') 19 | 20 | controller.filterNodesByType(false, 'nodeTypeToExclude') 21 | // #endregion snippet 22 | -------------------------------------------------------------------------------- /docs/api/samples/node-types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphController, 3 | defineGraph, 4 | defineGraphConfig, 5 | } from 'd3-graph-controller' 6 | 7 | const container = document.getElementById('graph') as HTMLDivElement 8 | const graph = defineGraph({ 9 | /* ... */ 10 | }) 11 | const config = defineGraphConfig({ 12 | /* ... */ 13 | }) 14 | 15 | // #region snippet 16 | const controller = new GraphController(container, graph, config) 17 | 18 | const availableNodeTypes = controller.nodeTypes 19 | 20 | const includedNodeTypes = controller.nodeTypeFilter 21 | // #endregion snippet 22 | -------------------------------------------------------------------------------- /docs/api/samples/resize.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphController, 3 | defineGraph, 4 | defineGraphConfig, 5 | } from 'd3-graph-controller' 6 | 7 | const container = document.getElementById('graph') as HTMLDivElement 8 | const graph = defineGraph({ 9 | /* ... */ 10 | }) 11 | const config = defineGraphConfig({ 12 | /* ... */ 13 | }) 14 | 15 | // #region snippet 16 | const controller = new GraphController(container, graph, config) 17 | 18 | controller.resize() 19 | // #endregion snippet 20 | -------------------------------------------------------------------------------- /docs/api/samples/restart.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphController, 3 | defineGraph, 4 | defineGraphConfig, 5 | } from 'd3-graph-controller' 6 | 7 | const container = document.getElementById('graph') as HTMLDivElement 8 | const graph = defineGraph({ 9 | /* ... */ 10 | }) 11 | const config = defineGraphConfig({ 12 | /* ... */ 13 | }) 14 | 15 | // #region snippet 16 | const controller = new GraphController(container, graph, config) 17 | 18 | const alpha = 0.5 19 | 20 | controller.restart(alpha) 21 | // #endregion snippet 22 | -------------------------------------------------------------------------------- /docs/api/samples/setup.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphController, 3 | defineGraph, 4 | defineGraphConfig, 5 | } from 'd3-graph-controller' 6 | 7 | const container = document.getElementById('graph') as HTMLDivElement 8 | const graph = defineGraph({ 9 | /* ... */ 10 | }) 11 | const config = defineGraphConfig({ 12 | /* ... */ 13 | }) 14 | 15 | const controller = new GraphController(container, graph, config) 16 | -------------------------------------------------------------------------------- /docs/api/samples/shutdown.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphController, 3 | defineGraph, 4 | defineGraphConfig, 5 | } from 'd3-graph-controller' 6 | 7 | const container = document.getElementById('graph') as HTMLDivElement 8 | const graph = defineGraph({ 9 | /* ... */ 10 | }) 11 | const config = defineGraphConfig({ 12 | /* ... */ 13 | }) 14 | 15 | // #region snippet 16 | const controller = new GraphController(container, graph, config) 17 | 18 | controller.shutdown() 19 | // #endregion snippet 20 | -------------------------------------------------------------------------------- /docs/config/index.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Both behavior and visuals of graphs can be customized by passing additional parameters to `defineGraphConfig()`. 4 | 5 | ## Callbacks 6 | 7 | ### nodeClicked 8 | 9 | The `nodeClicked` callback is called whenever a node is double-clicked (using the primary mouse button) or double-tapped in a short time. 10 | If set, the default behavior of focusing a node is disabled. 11 | 12 | <<< @/config/samples/callbacks.ts 13 | 14 | ## Initial Settings 15 | 16 | The `GraphController` settings that can be changed after initialization can have their initial values configured. 17 | The reference below shows the default configuration. 18 | 19 | `linkFilter` receives a link as its parameter. 20 | 21 | `nodeTypeFilter` is an array of type tokens. 22 | Only nodes whose type is included in the array will be shown. 23 | If omitted, the graph will include all nodes. 24 | 25 | <<< @/config/samples/initial.ts 26 | 27 | ## Markers 28 | 29 | Markers are displayed at the end of links. 30 | Because precise marker dimensions are required for path calculations, it is necessary to provide a lot of data. 31 | Hence, it is recommended to only use the default marker `Markers.Arrow` with customizable size as seen below. 32 | 33 | <<< @/config/samples/marker.ts 34 | 35 | ## Modifiers 36 | 37 | If absolute control is required, `modifiers` can be used to customize D3 internals. 38 | 39 | ::: tip 40 | Configuring modifiers is usually not required. 41 | Do not forget to unset predefined callbacks like `pointerdown` and `contextmenu` for `node` if required. 42 | ::: 43 | 44 | ### Drag 45 | 46 | <<< @/config/samples/modifiers/drag.ts 47 | 48 | ### Links 49 | 50 | <<< @/config/samples/modifiers/links.ts 51 | 52 | ### Nodes 53 | 54 | <<< @/config/samples/modifiers/nodes.ts 55 | 56 | ### Simulation 57 | 58 | <<< @/config/samples/modifiers/simulation.ts 59 | 60 | ### Zoom 61 | 62 | <<< @/config/samples/modifiers/zoom.ts 63 | 64 | ## Node Radius 65 | 66 | The radius of nodes is used for their visualization as well as the underlying simulation. 67 | It can be configured using the `nodeRadius` property of the config. 68 | 69 | <<< @/config/samples/static-node-radius.ts 70 | 71 | It is also possible to use a function for dynamic node radii. 72 | 73 | <<< @/config/samples/dynamic-node-radius.ts 74 | 75 | ## Position Initialization 76 | 77 | When a `GraphController` is created, it initializes the positions of nodes that do not have their coordinates set. 78 | The behavior of this initialization can be customized by providing a `PositionInitializer`. 79 | A `PositionInitializer` is a function that receives a `GraphNode` as well as the width and height of a graph and returns two coordinates. 80 | This library provides two `PositionInitializer`s out of the box. 81 | 82 | By default, `PositionInitializers.Centered` is used. 83 | Alternatively, `PositionInitializers.Randomized` or custom implementations can be used. 84 | 85 | <<< @/config/samples/position-initializers.ts 86 | 87 | ## Resizing 88 | 89 | Graphs can be resized to fit their container. 90 | This can either happen manually by calling a `GraphController`'s `resize` method or automatically by setting `autoResize` to `true`. 91 | 92 | <<< @/config/samples/resizing.ts 93 | 94 | ## Simulation 95 | 96 | The interactivity of the graph is driven by a d3 simulation. 97 | Its forces and behavior can be configured for precise control. 98 | 99 | ### Alphas 100 | 101 | Alpha values determine the _heat_ or _activity_ of a simulation. 102 | The higher the value, the stronger the simulation will react. 103 | After certain actions, the simulations needs to be restarted. 104 | The alpha values for those restarts can be configured. 105 | Reference the default configuration below for the available options. 106 | 107 | <<< @/config/samples/alphas.ts 108 | 109 | ::: tip 110 | `simulation.alphas.focus.acquire` and `simulation.alphas.focus.release` receive the (un-)focused node as a parameter. 111 | `simulation.alphas.resize` can either be a static `number` or a function receiving a `ResizeContext` as its parameter. 112 | ::: 113 | 114 | ### Forces 115 | 116 | Forces can be customized or disabled as required. 117 | Some forces provide additional customizability. 118 | Reference the configuration below, which matches the default values. 119 | 120 | ::: tip 121 | Settings `simulation.forces.collision.radiusMultiplier` to a higher value can drastically reduce the number of intersecting edges. 122 | ::: 123 | 124 | All `strength` properties can also be functions that receive the subject of the force as a parameter for individual strength. 125 | Except `forces.link`, the subject is always a `GraphNode` (or the custom type used). 126 | 127 | <<< @/config/samples/forces.ts 128 | 129 | ### Link Length 130 | 131 | Link length is used to determine the length of links for the simulation. 132 | Similar to node radii, link length can be configured on a per-link basis. 133 | Once again, custom link types can be used to provide the required data. 134 | 135 | <<< @/config/samples/link-length.ts 136 | 137 | ## Zoom 138 | 139 | For the zooming functionality, the initial value as well as its boundaries can be configured as seen below. 140 | 141 | ::: warning 142 | Currently, there's no validation of the values. 143 | The `min` value must be larger than 0 and the initial value must be withing the range `[min, max]`. 144 | ::: 145 | 146 | <<< @/config/samples/zoom.ts 147 | -------------------------------------------------------------------------------- /docs/config/samples/alphas.ts: -------------------------------------------------------------------------------- 1 | import { GraphNode, defineGraphConfig } from 'd3-graph-controller' 2 | 3 | const config = defineGraphConfig({ 4 | simulation: { 5 | alphas: { 6 | drag: { 7 | end: 0, 8 | start: 0.1, 9 | }, 10 | filter: { 11 | link: 1, 12 | type: 0.1, 13 | unlinked: { 14 | include: 0.1, 15 | exclude: 0.1, 16 | }, 17 | }, 18 | focus: { 19 | acquire: (node: GraphNode) => 0.1, 20 | release: (node: GraphNode) => 0.1, 21 | }, 22 | initialize: 1, 23 | labels: { 24 | links: { 25 | hide: 0, 26 | show: 0, 27 | }, 28 | nodes: { 29 | hide: 0, 30 | show: 0, 31 | }, 32 | }, 33 | resize: 0.5, 34 | }, 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /docs/config/samples/callbacks.ts: -------------------------------------------------------------------------------- 1 | import { GraphNode, defineGraphConfig } from 'd3-graph-controller' 2 | 3 | const config = defineGraphConfig({ 4 | callbacks: { 5 | nodeClicked: (node: GraphNode) => console.log(node.id), 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /docs/config/samples/dynamic-node-radius.ts: -------------------------------------------------------------------------------- 1 | import { GraphNode, defineGraphConfig } from 'd3-graph-controller' 2 | 3 | type CustomNode = GraphNode & { radius: number } 4 | 5 | const config = defineGraphConfig({ 6 | nodeRadius: (node: CustomNode) => node.radius, 7 | }) 8 | -------------------------------------------------------------------------------- /docs/config/samples/forces.ts: -------------------------------------------------------------------------------- 1 | import { GraphLink, defineGraphConfig } from 'd3-graph-controller' 2 | 3 | const config = defineGraphConfig({ 4 | simulation: { 5 | forces: { 6 | centering: { 7 | enabled: true, 8 | strength: 0.1, 9 | }, 10 | charge: { 11 | enabled: true, 12 | strength: -1, 13 | }, 14 | collision: { 15 | enabled: true, 16 | strength: 1, 17 | radiusMultiplier: 2, 18 | }, 19 | link: { 20 | enabled: true, 21 | length: (link: GraphLink) => 128, 22 | strength: 1, 23 | }, 24 | }, 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /docs/config/samples/initial.ts: -------------------------------------------------------------------------------- 1 | import { defineGraphConfig } from 'd3-graph-controller' 2 | 3 | const config = defineGraphConfig({ 4 | initial: { 5 | includeUnlinked: true, 6 | linkFilter: () => true, 7 | nodeTypeFilter: undefined, 8 | showLinkLabels: true, 9 | showNodeLabels: true, 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /docs/config/samples/link-length.ts: -------------------------------------------------------------------------------- 1 | import { GraphLink, GraphNode, defineGraphConfig } from 'd3-graph-controller' 2 | 3 | type CustomLink = GraphLink & { length: number } 4 | 5 | const config = defineGraphConfig({ 6 | simulation: { 7 | forces: { 8 | link: { 9 | length: (link: CustomLink) => link.length, 10 | }, 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /docs/config/samples/marker.ts: -------------------------------------------------------------------------------- 1 | import { Markers, defineGraphConfig } from 'd3-graph-controller' 2 | 3 | const config = defineGraphConfig({ 4 | marker: Markers.Arrow(4), 5 | }) 6 | -------------------------------------------------------------------------------- /docs/config/samples/modifiers/drag.ts: -------------------------------------------------------------------------------- 1 | import { Drag, GraphNode, defineGraphConfig } from 'd3-graph-controller' 2 | 3 | const config = defineGraphConfig({ 4 | modifiers: { 5 | drag: (drag: Drag) => { 6 | // Customize drag behavior 7 | }, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /docs/config/samples/modifiers/links.ts: -------------------------------------------------------------------------------- 1 | import { GraphLink, defineGraphConfig } from 'd3-graph-controller' 2 | import type { Selection } from 'd3-selection' 3 | 4 | const config = defineGraphConfig({ 5 | modifiers: { 6 | link: ( 7 | selection: Selection 8 | ) => { 9 | // Customize link paths 10 | }, 11 | linkLabel: ( 12 | selection: Selection 13 | ) => { 14 | // Customize link labels 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /docs/config/samples/modifiers/nodes.ts: -------------------------------------------------------------------------------- 1 | import { GraphNode, defineGraphConfig } from 'd3-graph-controller' 2 | import type { Selection } from 'd3-selection' 3 | 4 | const config = defineGraphConfig({ 5 | modifiers: { 6 | node: ( 7 | selection: Selection 8 | ) => { 9 | // Customize node circles 10 | }, 11 | nodeLabel: ( 12 | selection: Selection 13 | ) => { 14 | // Customize node labels 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /docs/config/samples/modifiers/simulation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphLink, 3 | GraphNode, 4 | GraphSimulation, 5 | defineGraphConfig, 6 | } from 'd3-graph-controller' 7 | 8 | const config = defineGraphConfig({ 9 | modifiers: { 10 | simulation: (simulation: GraphSimulation) => { 11 | // Customize simulation 12 | }, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /docs/config/samples/modifiers/zoom.ts: -------------------------------------------------------------------------------- 1 | import { Zoom, defineGraphConfig } from 'd3-graph-controller' 2 | 3 | const config = defineGraphConfig({ 4 | modifiers: { 5 | zoom: (zoom: Zoom) => { 6 | // Customize zoom 7 | }, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /docs/config/samples/position-initializers.ts: -------------------------------------------------------------------------------- 1 | import { PositionInitializers, defineGraphConfig } from 'd3-graph-controller' 2 | 3 | const config = defineGraphConfig({ 4 | positionInitializer: PositionInitializers.Randomized, 5 | }) 6 | -------------------------------------------------------------------------------- /docs/config/samples/resizing.ts: -------------------------------------------------------------------------------- 1 | import { defineGraphConfig } from 'd3-graph-controller' 2 | 3 | const config = defineGraphConfig({ 4 | autoResize: true, 5 | }) 6 | -------------------------------------------------------------------------------- /docs/config/samples/static-node-radius.ts: -------------------------------------------------------------------------------- 1 | import { defineGraphConfig } from 'd3-graph-controller' 2 | 3 | const config = defineGraphConfig({ 4 | nodeRadius: 32, 5 | }) 6 | -------------------------------------------------------------------------------- /docs/config/samples/zoom.ts: -------------------------------------------------------------------------------- 1 | import { defineGraphConfig } from 'd3-graph-controller' 2 | 3 | const config = defineGraphConfig({ 4 | zoom: { 5 | initial: 1, 6 | max: 2, 7 | min: 0.1, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /docs/demo/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: false 3 | editLink: false 4 | --- 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | # Guide 2 | 3 | ## Motivation 4 | 5 | [D3](https://d3js.org/) is powerful but requires some effort to get good results. 6 | Furthermore, managing a robust lifecycle with extensive customization options is downright difficult. 7 | This library aims to make graph building a declarative task and provide an abstraction layer for the complexity of D3. 8 | 9 | It does so by using an [extensive configuration](/config/) as the basis for creating graphs. 10 | Everything should be configurable in a declarative way that is understandable without insight into the inner workings of D3. 11 | 12 | In addition, models of graphs should be type-safe and extensible. 13 | This library allows for custom node and link data types that extend the default model with custom properties. 14 | These custom properties can then be used anywhere in the configuration. 15 | 16 | Lastly, this library is framework-agnostic. 17 | A graph's container element can be retrieved by any means, including [Vue's refs](https://v3.vuejs.org/guide/component-template-refs.html), [React's refs](https://reactjs.org/docs/refs-and-the-dom.html), [Angular's ViewChild](https://angular.io/api/core/ViewChild), or the old and trustworthy `document.gelElementById`. 18 | Just do not forget to [integrate the graph in the framework's lifecycle](/api/#shutdown). 19 | 20 | ## Installation 21 | 22 | ```bash 23 | # with yarn 24 | yarn add d3-graph-controller 25 | 26 | # or with npm 27 | npm install d3-graph-controller 28 | 29 | # or with pnpm 30 | pnpm add d3-graph-controller 31 | ``` 32 | 33 | ## Usage 34 | 35 | The data model of a graph can be customized to fit any need. 36 | The following sections show a model with two node types, `primary` and `secondary`, custom node radius and link length as well as dynamic force strength. 37 | 38 | ### Type Tokens 39 | 40 | First, define the types of nodes the graph may contain. 41 | 42 | <<< @/guide/samples/custom-model.ts#token{0} 43 | 44 | ### Node 45 | 46 | Then you can enhance the `GraphNode` interface with custom properties that can be accessed later on. 47 | 48 | <<< @/guide/samples/custom-model.ts#node{0} 49 | 50 | ### Link 51 | 52 | Analogous to nodes, `GraphLink` can be extended. 53 | While not shown in the example below, `GraphLink` can have specific node types for `source` and `target`. 54 | 55 | <<< @/guide/samples/custom-model.ts#link{0} 56 | 57 | ### Config 58 | 59 | The config can then use the custom types. 60 | 61 | <<< @/guide/samples/custom-model.ts#config{0} 62 | 63 | ### Model 64 | 65 | The actual model can be created using the helper methods seen below. 66 | They are type safe and support custom properties. 67 | 68 | <<< @/guide/samples/custom-model.ts#model{0} 69 | 70 | ### Controller 71 | 72 | The last step is putting it all together and creating the controller. 73 | 74 | <<< @/guide/samples/custom-model.ts#controller{0} 75 | 76 | ::: tip 77 | Do not forget to call `controller.shutdown()` when the graph is no longer required or your component will be destroyed. 78 | ::: 79 | 80 | ## Styling 81 | 82 | The library provides default styles, which need to be imported manually. 83 | 84 | <<< @/guide/samples/style-import.ts 85 | 86 | In addition, the properties `color` and `fontSize` of nodes and links accept any valid CSS value. 87 | This allows you to use dynamic colors with CSS variables. 88 | 89 | <<< @/guide/samples/styling.css 90 | 91 | <<< @/guide/samples/styling.ts 92 | 93 | For customization of the default theme, the custom CSS properties `--color-stroke` and `--color-node-stroke` can be used. 94 | 95 | ### Classes 96 | 97 | Graphs can also be styled using CSS. 98 | For this purpose, various classes are defined. 99 | Reference the table below for a description of all available classes. 100 | 101 | | Class | Element | Description | 102 | | ------------- | ----------------------- | --------------------------------------------------------------------------------------- | 103 | | `graph` | Container of the graph | Added to the graph's container on initialization. | 104 | | `link` | Path of a link | | 105 | | `link__label` | Label of a link | | 106 | | `node` | Circle of a node | | 107 | | `node__label` | Label of a node | | 108 | | `focused` | Focused node | Applied to a focused node. Recommended usage is `.node.focused`. | 109 | | `dragged` | Dragged nodes or canvas | Added to a node or the canvas while it is being dragged. Sets the cursor to `grabbing`. | 110 | 111 | ### Default Stylesheet 112 | 113 | Usually, importing the default stylesheet and configuring variables should be enough to fit all needs. 114 | If a full custom styling is required, the default stylesheet as seen below might act as a template. 115 | 116 | <<< @/../default.css 117 | -------------------------------------------------------------------------------- /docs/guide/samples/custom-model.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-duplicates, import/order */ 2 | // #region token 3 | export type CustomType = 'primary' | 'secondary' 4 | // #endregion token 5 | 6 | // #region node 7 | import { GraphNode } from 'd3-graph-controller' 8 | 9 | export interface CustomNode extends GraphNode { 10 | radius: number 11 | } 12 | // #endregion node 13 | 14 | // #region link 15 | import { GraphLink } from 'd3-graph-controller' 16 | 17 | export interface CustomLink extends GraphLink { 18 | length: number 19 | } 20 | // #endregion link 21 | 22 | // #region config 23 | import { defineGraphConfig } from 'd3-graph-controller' 24 | 25 | const config = defineGraphConfig({ 26 | nodeRadius: (node: CustomNode) => node.radius, 27 | simulation: { 28 | forces: { 29 | centering: { 30 | strength: (node: CustomNode) => (node.type === 'primary' ? 0.5 : 0.1), 31 | }, 32 | link: { 33 | length: (link: CustomLink) => link.length, 34 | }, 35 | }, 36 | }, 37 | }) 38 | // #endregion config 39 | 40 | // #region model 41 | import { defineGraph, defineLink, defineNode } from 'd3-graph-controller' 42 | 43 | const a = defineNode({ 44 | id: 'a', 45 | type: 'primary', 46 | isFocused: false, 47 | color: 'green', 48 | label: { 49 | color: 'black', 50 | fontSize: '1rem', 51 | text: 'A', 52 | }, 53 | radius: 64, 54 | }) 55 | 56 | const b = defineNode({ 57 | id: 'b', 58 | type: 'secondary', 59 | isFocused: false, 60 | color: 'blue', 61 | label: { 62 | color: 'black', 63 | fontSize: '1rem', 64 | text: 'B', 65 | }, 66 | radius: 32, 67 | }) 68 | 69 | const aToB = defineLink({ 70 | source: a, 71 | target: b, 72 | color: 'red', 73 | label: { 74 | color: 'black', 75 | fontSize: '1rem', 76 | text: '128', 77 | }, 78 | length: 128, 79 | }) 80 | 81 | const graph = defineGraph({ 82 | nodes: [a, b], 83 | links: [aToB], 84 | }) 85 | // #endregion model 86 | 87 | // #region controller 88 | import { GraphController } from 'd3-graph-controller' 89 | 90 | // Any HTMLDivElement can be used as the container 91 | const container = document.getElementById('graph') as HTMLDivElement 92 | 93 | const controller = new GraphController(container, graph, config) 94 | // #endregion controller 95 | -------------------------------------------------------------------------------- /docs/guide/samples/style-import.ts: -------------------------------------------------------------------------------- 1 | import 'd3-graph-controller/default.css' 2 | -------------------------------------------------------------------------------- /docs/guide/samples/styling.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-primary: 'red'; 3 | } 4 | -------------------------------------------------------------------------------- /docs/guide/samples/styling.ts: -------------------------------------------------------------------------------- 1 | import { defineNodeWithDefaults } from 'd3-graph-controller' 2 | import 'd3-graph-controller/default.css' 3 | 4 | const a = defineNodeWithDefaults({ 5 | type: 'node', 6 | id: 'a', 7 | label: { 8 | color: 'black', 9 | fontSize: '2rem', 10 | text: 'A', 11 | }, 12 | color: 'var(--color-primary)', 13 | }) 14 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | heroImage: /logo.svg 4 | hero: 5 | name: d3-graph-controller 6 | text: '' 7 | tagline: A TypeScript library for visualizing and simulating directed, interactive graphs. 8 | image: 9 | src: /logo.svg 10 | alt: d3-graph-controller 11 | actions: 12 | - text: Get Started 13 | link: /guide/ 14 | theme: brand 15 | - text: See It In Action 16 | link: /demo/ 17 | theme: alt 18 | - text: View on GitHub 19 | link: https://github.com/DerYeger/d3-graph-controller 20 | theme: alt 21 | features: 22 | - icon: 👉 23 | title: Interactive 24 | details: Dragging, panning, zooming and more. Supports touch input and uses multi-touch. 25 | - icon: 📱 26 | title: Responsive 27 | details: Automatic or manual resizing to fit any screen. 28 | - icon: 🔧 29 | title: Configurable 30 | details: Extensive configuration enables customizable behavior and visuals. 31 | --- 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerYeger/d3-graph-controller/d912a8b690b7f87a5e090d2bb3fe2993a0a77468/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerYeger/d3-graph-controller/d912a8b690b7f87a5e090d2bb3fe2993a0a77468/docs/public/favicon-16x16.png -------------------------------------------------------------------------------- /docs/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerYeger/d3-graph-controller/d912a8b690b7f87a5e090d2bb3fe2993a0a77468/docs/public/favicon-32x32.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerYeger/d3-graph-controller/d912a8b690b7f87a5e090d2bb3fe2993a0a77468/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerYeger/d3-graph-controller/d912a8b690b7f87a5e090d2bb3fe2993a0a77468/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | background 6 | 7 | 8 | 9 | 10 | 11 | 12 | Layer 1 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "lib": ["ESNext", "DOM"], 8 | "moduleResolution": "Node", 9 | "strict": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "noImplicitReturns": true, 16 | "exactOptionalPropertyTypes": true, 17 | "skipLibCheck": true, 18 | "jsx": "preserve" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import Components from 'unplugin-vue-components/vite' 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | root: 'docs', 6 | 7 | plugins: [ 8 | Components({ 9 | include: [/\.vue/, /\.md/], 10 | dirs: '.vitepress/components', 11 | dts: '.vitepress/components.d.ts', 12 | }), 13 | ], 14 | 15 | optimizeDeps: { 16 | include: ['vue'], 17 | }, 18 | 19 | ssr: { 20 | noExternal: ['d3-drag', 'd3-force', 'd3-selection', 'd3-zoom'], 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-graph-controller", 3 | "version": "2.3.28", 4 | "description": "A TypeScript library for visualizing and simulating directed, interactive graphs.", 5 | "author": { 6 | "name": "Jan Müller", 7 | "url": "https://github.com/DerYeger" 8 | }, 9 | "license": "MIT", 10 | "homepage": "https://graph-controller.yeger.eu", 11 | "repository": "github:DerYeger/d3-graph-controller", 12 | "bugs": { 13 | "url": "https://github.com/DerYeger/d3-graph-controller/issues" 14 | }, 15 | "keywords": [ 16 | "d3", 17 | "graph", 18 | "controller" 19 | ], 20 | "sideEffects": false, 21 | "exports": { 22 | ".": { 23 | "require": "./dist/d3-graph-controller.umd.js", 24 | "import": "./dist/d3-graph-controller.es.js" 25 | }, 26 | "./default.css": { 27 | "require": "./default.css", 28 | "import": "./default.css" 29 | } 30 | }, 31 | "main": "./dist/d3-graph-controller.umd.js", 32 | "module": "./dist/d3-graph-controller.es.js", 33 | "types": "dist/types/main.d.ts", 34 | "files": [ 35 | "dist", 36 | "default.css" 37 | ], 38 | "scripts": { 39 | "prepare": "is-ci || husky install", 40 | "prebuild": "rimraf ./dist", 41 | "build": "tsc --noEmit && vite build", 42 | "dev": "vite build --watch", 43 | "docs": "yarn docs:typecheck && yarn docs:build", 44 | "docs:dev": "vitepress dev docs", 45 | "docs:build": "vitepress build docs", 46 | "docs:preview": "vitepress serve docs --port 5173", 47 | "docs:typecheck": "tsc --noEmit -p docs", 48 | "lint": "eslint \"./**/*.{js,json,md,ts,vue,yaml,yml}\"", 49 | "lint:prettier": "prettier --check \"./**/*.{html,js,json,md,scss,ts,vue,yml}\"", 50 | "fix": "yarn lint --fix ", 51 | "test": "yarn test:typecheck && yarn test:ci", 52 | "test:ci": "rimraf ./coverage && vitest --run --coverage", 53 | "test:run": "vitest --run", 54 | "test:typecheck": "tsc --noEmit -p test", 55 | "test:watch": "vitest" 56 | }, 57 | "dependencies": { 58 | "@yeger/debounce": "1.0.60", 59 | "d3-drag": "3.0.0", 60 | "d3-force": "3.0.0", 61 | "d3-selection": "3.0.0", 62 | "d3-zoom": "3.0.0", 63 | "vecti": "2.1.32" 64 | }, 65 | "devDependencies": { 66 | "@commitlint/cli": "17.3.0", 67 | "@commitlint/config-conventional": "17.3.0", 68 | "@types/d3-drag": "3.0.1", 69 | "@types/d3-force": "3.0.3", 70 | "@types/d3-selection": "3.0.3", 71 | "@types/d3-zoom": "3.0.1", 72 | "@types/node": "18.11.9", 73 | "@types/resize-observer-browser": "0.1.7", 74 | "@vitejs/plugin-vue": "3.2.0", 75 | "@vitest/coverage-c8": "0.25.3", 76 | "@vitest/ui": "0.25.3", 77 | "@yeger/eslint-config": "1.4.49", 78 | "c8": "7.12.0", 79 | "d3-graph-controller": "link:.", 80 | "eslint": "8.27.0", 81 | "husky": "8.0.2", 82 | "is-ci": "3.0.1", 83 | "jsdom": "20.0.2", 84 | "lint-staged": "13.0.3", 85 | "rimraf": "3.0.2", 86 | "ts-deepmerge": "4.0.0", 87 | "typescript": "4.8.4", 88 | "unocss": "0.46.4", 89 | "unplugin-vue-components": "0.22.9", 90 | "vite": "3.2.3", 91 | "vite-plugin-dts": "1.7.0", 92 | "vitepress": "1.0.0-alpha.28", 93 | "vitest": "0.25.3", 94 | "vue": "3.2.45" 95 | }, 96 | "lint-staged": { 97 | "*.{js,json,md,ts,vue,yaml,yml}": "eslint --fix" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | '@semantic-release/commit-analyzer', 4 | '@semantic-release/release-notes-generator', 5 | '@semantic-release/changelog', 6 | '@semantic-release/npm', 7 | '@semantic-release/github', 8 | [ 9 | '@semantic-release/git', 10 | { 11 | assets: ['CHANGELOG.md', './package.json'], 12 | }, 13 | ], 14 | ], 15 | } 16 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>DerYeger/renovate-config"] 3 | } 4 | -------------------------------------------------------------------------------- /src/config/alpha.ts: -------------------------------------------------------------------------------- 1 | import type { NodeTypeToken } from 'src/model/graph' 2 | import type { GraphNode } from 'src/model/node' 3 | 4 | /** 5 | * Alpha values when label display changes. 6 | */ 7 | export interface LabelAlphas { 8 | /** 9 | * Alpha value when labels are turned off. 10 | */ 11 | readonly hide: number 12 | /** 13 | * Alpha value when labels are turned on. 14 | */ 15 | readonly show: number 16 | } 17 | 18 | /** 19 | * Context of a resize. 20 | */ 21 | export interface ResizeContext { 22 | /** 23 | * The old height. 24 | */ 25 | readonly oldHeight: number 26 | /** 27 | * The old width. 28 | */ 29 | readonly oldWidth: number 30 | /** 31 | * The new height. 32 | */ 33 | readonly newHeight: number 34 | /** 35 | * The new width. 36 | */ 37 | readonly newWidth: number 38 | } 39 | 40 | /** 41 | * Alpha value configuration for controlling simulation activity. 42 | */ 43 | export interface AlphaConfig< 44 | T extends NodeTypeToken, 45 | Node extends GraphNode 46 | > { 47 | /** 48 | * Target alpha values for dragging. 49 | */ 50 | readonly drag: { 51 | /** 52 | * Target alpha when a drag starts. 53 | * Should be larger than 0. 54 | */ 55 | readonly start: number 56 | /** 57 | * Target alpha when a drag stops. 58 | * Should generally be 0. 59 | */ 60 | readonly end: number 61 | } 62 | /** 63 | * Alpha values for filter changes. 64 | */ 65 | readonly filter: { 66 | /** 67 | * Alpha value when the link filter changes. 68 | */ 69 | readonly link: number 70 | /** 71 | * Alpha value when the node type filter changes. 72 | */ 73 | readonly type: number 74 | /** 75 | * Alpha values when the inclusion of unlinked nodes changes. 76 | */ 77 | readonly unlinked: { 78 | /** 79 | * Alpha value when unlinked nodes are included. 80 | */ 81 | readonly include: number 82 | /** 83 | * Alpha value when unlinked nodes are excluded. 84 | */ 85 | readonly exclude: number 86 | } 87 | } 88 | /** 89 | * Alpha values when node focus changes. 90 | */ 91 | readonly focus: { 92 | /** 93 | * Alpha value when a node is focused. 94 | * @param node - The focused node. 95 | * @returns The alpha value. 96 | */ 97 | readonly acquire: (node: Node) => number 98 | /** 99 | * Alpha value when a node is unfocused. 100 | * @param node - The unfocused node. 101 | * @returns The alpha value. 102 | */ 103 | readonly release: (node: Node) => number 104 | } 105 | /** 106 | * Alpha value when the graph is initialized. 107 | */ 108 | readonly initialize: number 109 | /** 110 | * Alpha values when label display changes. 111 | */ 112 | readonly labels: { 113 | /** 114 | * Alpha values when link label display changes. 115 | */ 116 | readonly links: LabelAlphas 117 | /** 118 | * Alpha values when node label display changes. 119 | */ 120 | readonly nodes: LabelAlphas 121 | } 122 | /** 123 | * Alpha values when the graph is resized. 124 | */ 125 | readonly resize: number | ((context: ResizeContext) => number) 126 | } 127 | 128 | /** 129 | * Create the default alpha configuration. 130 | */ 131 | export function createDefaultAlphaConfig< 132 | T extends NodeTypeToken, 133 | Node extends GraphNode 134 | >(): AlphaConfig { 135 | return { 136 | drag: { 137 | end: 0, 138 | start: 0.1, 139 | }, 140 | filter: { 141 | link: 1, 142 | type: 0.1, 143 | unlinked: { 144 | include: 0.1, 145 | exclude: 0.1, 146 | }, 147 | }, 148 | focus: { 149 | acquire: () => 0.1, 150 | release: () => 0.1, 151 | }, 152 | initialize: 1, 153 | labels: { 154 | links: { 155 | hide: 0, 156 | show: 0, 157 | }, 158 | nodes: { 159 | hide: 0, 160 | show: 0, 161 | }, 162 | }, 163 | resize: 0.5, 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/config/callbacks.ts: -------------------------------------------------------------------------------- 1 | import type { NodeTypeToken } from 'src/model/graph' 2 | import type { GraphNode } from 'src/model/node' 3 | 4 | /** 5 | * Callback configuration. 6 | */ 7 | export interface Callbacks> { 8 | /** 9 | * Callback when a node is double-clicked or double-tapped. 10 | * @param node - The node. 11 | */ 12 | readonly nodeClicked?: (node: Node) => void 13 | } 14 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | import { createDefaultAlphaConfig } from 'src/config/alpha' 2 | import type { Callbacks } from 'src/config/callbacks' 3 | import { createDefaultForceConfig } from 'src/config/forces' 4 | import type { InitialGraphSettings } from 'src/config/initial' 5 | import { createDefaultInitialGraphSettings } from 'src/config/initial' 6 | import type { MarkerConfig } from 'src/config/marker' 7 | import { Markers } from 'src/config/marker' 8 | import type { Modifiers } from 'src/config/modifiers' 9 | import type { PositionInitializer } from 'src/config/position' 10 | import { PositionInitializers } from 'src/config/position' 11 | import type { SimulationConfig } from 'src/config/simulation' 12 | import type { ZoomConfig } from 'src/config/zoom' 13 | import type { NodeTypeToken } from 'src/model/graph' 14 | import type { GraphLink } from 'src/model/link' 15 | import type { GraphNode } from 'src/model/node' 16 | import merge from 'ts-deepmerge' 17 | 18 | export interface GraphConfig< 19 | T extends NodeTypeToken, 20 | Node extends GraphNode, 21 | Link extends GraphLink 22 | > { 23 | /** 24 | * Set to true to enable automatic resizing. 25 | * Warning: Do call shutdown(), once the controller is no longer required. 26 | */ 27 | readonly autoResize: boolean 28 | /** 29 | * Callback configuration. 30 | */ 31 | readonly callbacks: Callbacks 32 | /** 33 | * Initial settings of a controller. 34 | */ 35 | readonly initial: InitialGraphSettings 36 | /** 37 | * Marker configuration. 38 | */ 39 | readonly marker: MarkerConfig 40 | /** 41 | * Low-level callbacks for modifying the underlying d3-selection. 42 | */ 43 | readonly modifiers: Modifiers 44 | /** 45 | * Define the radius of a node for the simulation and visualization. 46 | * Can be a static number or a function receiving a node as its parameter. 47 | */ 48 | readonly nodeRadius: number | ((node: Node) => number) 49 | /** 50 | * Initializes a node's position in context of a graph's width and height. 51 | */ 52 | readonly positionInitializer: PositionInitializer 53 | /** 54 | * Simulation configuration. 55 | */ 56 | readonly simulation: SimulationConfig 57 | /** 58 | * Zoom configuration. 59 | */ 60 | readonly zoom: ZoomConfig 61 | } 62 | 63 | function defaultGraphConfig< 64 | T extends NodeTypeToken, 65 | Node extends GraphNode, 66 | Link extends GraphLink 67 | >(): GraphConfig { 68 | return { 69 | autoResize: false, 70 | callbacks: {}, 71 | initial: createDefaultInitialGraphSettings(), 72 | nodeRadius: 16, 73 | marker: Markers.Arrow(4), 74 | modifiers: {}, 75 | positionInitializer: PositionInitializers.Centered, 76 | simulation: { 77 | alphas: createDefaultAlphaConfig(), 78 | forces: createDefaultForceConfig(), 79 | }, 80 | zoom: { 81 | initial: 1, 82 | min: 0.1, 83 | max: 2, 84 | }, 85 | } 86 | } 87 | 88 | /** 89 | * Utility type for deeply partial objects. 90 | */ 91 | export type DeepPartial = { 92 | readonly [P in keyof T]?: DeepPartial 93 | } 94 | 95 | /** 96 | * Define the configuration of a controller. 97 | * Will be merged with the default configuration. 98 | * @param config - The partial configuration. 99 | * @returns The merged configuration. 100 | */ 101 | export function defineGraphConfig< 102 | T extends NodeTypeToken = NodeTypeToken, 103 | Node extends GraphNode = GraphNode, 104 | Link extends GraphLink = GraphLink 105 | >( 106 | config: DeepPartial> = {} 107 | ): GraphConfig { 108 | return merge.withOptions( 109 | { mergeArrays: false }, 110 | defaultGraphConfig(), 111 | config 112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /src/config/filter.ts: -------------------------------------------------------------------------------- 1 | import type { NodeTypeToken } from 'src/model/graph' 2 | import type { GraphLink } from 'src/model/link' 3 | import type { GraphNode } from 'src/model/node' 4 | 5 | /** 6 | * Link filter. 7 | * Receives a link and returns whether the link should be included or not. 8 | */ 9 | export type LinkFilter< 10 | T extends NodeTypeToken, 11 | Node extends GraphNode, 12 | Link extends GraphLink 13 | > = (link: Link) => boolean 14 | -------------------------------------------------------------------------------- /src/config/forces.ts: -------------------------------------------------------------------------------- 1 | import type { NodeTypeToken } from 'src/model/graph' 2 | import type { GraphLink } from 'src/model/link' 3 | import type { GraphNode } from 'src/model/node' 4 | 5 | /** 6 | * Simulation force. 7 | */ 8 | export interface Force { 9 | /** 10 | * Whether the force is enabled. 11 | */ 12 | readonly enabled: boolean 13 | /** 14 | * The strength of the force. 15 | * Can be a static number or a function receiving the force's subject and returning a number. 16 | */ 17 | readonly strength: number | ((subject: Subject) => number) 18 | } 19 | 20 | /** 21 | * Simulation force applied to nodes. 22 | */ 23 | export type NodeForce< 24 | T extends NodeTypeToken, 25 | Node extends GraphNode 26 | > = Force 27 | 28 | /** 29 | * Collision force applied to nodes. 30 | */ 31 | export interface CollisionForce< 32 | T extends NodeTypeToken, 33 | Node extends GraphNode 34 | > extends NodeForce { 35 | /** 36 | * Multiplier of the node radius. 37 | * Tip: Large values can drastically reduce link intersection. 38 | */ 39 | readonly radiusMultiplier: number 40 | } 41 | 42 | /** 43 | * Simulation force applied to links. 44 | */ 45 | export interface LinkForce< 46 | T extends NodeTypeToken, 47 | Node extends GraphNode, 48 | Link extends GraphLink 49 | > extends Force { 50 | /** 51 | * Define the length of a link for the simulation. 52 | */ 53 | readonly length: number | ((link: Link) => number) 54 | } 55 | 56 | /** 57 | * Simulation force configuration. 58 | */ 59 | export interface SimulationForceConfig< 60 | T extends NodeTypeToken, 61 | Node extends GraphNode, 62 | Link extends GraphLink 63 | > { 64 | /** 65 | * Centering force applied to nodes. 66 | */ 67 | readonly centering: false | NodeForce 68 | /** 69 | * Charge force applied to nodes. 70 | */ 71 | readonly charge: false | NodeForce 72 | /** 73 | * Collision force applied to nodes. 74 | */ 75 | readonly collision: false | CollisionForce 76 | /** 77 | * Link force applied to links. 78 | */ 79 | readonly link: false | LinkForce 80 | } 81 | 82 | /** 83 | * Create the default force configuration. 84 | */ 85 | export function createDefaultForceConfig< 86 | T extends NodeTypeToken, 87 | Node extends GraphNode, 88 | Link extends GraphLink 89 | >(): SimulationForceConfig { 90 | return { 91 | centering: { 92 | enabled: true, 93 | strength: 0.1, 94 | }, 95 | charge: { 96 | enabled: true, 97 | strength: -1, 98 | }, 99 | collision: { 100 | enabled: true, 101 | strength: 1, 102 | radiusMultiplier: 2, 103 | }, 104 | link: { 105 | enabled: true, 106 | strength: 1, 107 | length: 128, 108 | }, 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/config/initial.ts: -------------------------------------------------------------------------------- 1 | import type { LinkFilter } from 'src/config/filter' 2 | import type { NodeTypeToken } from 'src/model/graph' 3 | import type { GraphLink } from 'src/model/link' 4 | import type { GraphNode } from 'src/model/node' 5 | 6 | /** 7 | * Initial settings of a controller. 8 | */ 9 | export interface InitialGraphSettings< 10 | T extends NodeTypeToken, 11 | Node extends GraphNode, 12 | Link extends GraphLink 13 | > { 14 | /** 15 | * Whether nodes without incoming or outgoing links will be shown or not. 16 | */ 17 | readonly includeUnlinked: boolean 18 | /** 19 | * Link filter that decides whether links should be included or not. 20 | */ 21 | readonly linkFilter: LinkFilter 22 | /** 23 | * Node types that should be included. 24 | * If undefined, all node types will be included. 25 | */ 26 | readonly nodeTypeFilter?: T[] | undefined 27 | /** 28 | * Whether link labels are shown or not. 29 | */ 30 | readonly showLinkLabels: boolean 31 | /** 32 | * Whether node labels are shown or not. 33 | */ 34 | readonly showNodeLabels: boolean 35 | } 36 | 37 | /** 38 | * Create default initial settings. 39 | */ 40 | export function createDefaultInitialGraphSettings< 41 | T extends NodeTypeToken, 42 | Node extends GraphNode, 43 | Link extends GraphLink 44 | >(): InitialGraphSettings { 45 | return { 46 | includeUnlinked: true, 47 | linkFilter: () => true, 48 | nodeTypeFilter: undefined, 49 | showLinkLabels: true, 50 | showNodeLabels: true, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/config/marker.ts: -------------------------------------------------------------------------------- 1 | import type { GraphConfig } from 'src/config/config' 2 | import { getNodeRadius } from 'src/lib/utils' 3 | import type { NodeTypeToken } from 'src/model/graph' 4 | import type { GraphLink } from 'src/model/link' 5 | import type { GraphNode } from 'src/model/node' 6 | 7 | /** 8 | * Marker configuration. 9 | */ 10 | export interface MarkerConfig { 11 | /** 12 | * Size of the marker's box. 13 | */ 14 | readonly size: number 15 | /** 16 | * Get padding of the marker for calculating link paths. 17 | * @param node - The node the marker is pointing at. 18 | * @param config - The current config. 19 | * @returns The padding of the marker. 20 | */ 21 | readonly padding: < 22 | T extends NodeTypeToken, 23 | Node extends GraphNode, 24 | Link extends GraphLink 25 | >( 26 | node: Node, 27 | config: GraphConfig 28 | ) => number 29 | /** 30 | * The ref of the marker. 31 | */ 32 | readonly ref: [number, number] 33 | /** 34 | * The path of the marker. 35 | */ 36 | readonly path: [number, number][] 37 | /** 38 | * The ViewBox of the marker. 39 | */ 40 | readonly viewBox: string 41 | } 42 | 43 | function defaultMarkerConfig(size: number): MarkerConfig { 44 | return { 45 | size, 46 | padding: < 47 | T extends NodeTypeToken, 48 | Node extends GraphNode, 49 | Link extends GraphLink 50 | >( 51 | node: Node, 52 | config: GraphConfig 53 | ) => getNodeRadius(config, node) + 2 * size, 54 | ref: [size / 2, size / 2], 55 | path: [ 56 | [0, 0], 57 | [0, size], 58 | [size, size / 2], 59 | ] as [number, number][], 60 | viewBox: [0, 0, size, size].join(','), 61 | } 62 | } 63 | 64 | /** 65 | * Collection of built-in markers. 66 | */ 67 | export const Markers = { 68 | /** 69 | * Create an arrow marker configuration. 70 | * @param size - The size of the arrow 71 | */ 72 | Arrow: (size: number): MarkerConfig => defaultMarkerConfig(size), 73 | } 74 | -------------------------------------------------------------------------------- /src/config/modifiers.ts: -------------------------------------------------------------------------------- 1 | import type { Selection } from 'd3-selection' 2 | import type { Drag, GraphSimulation, Zoom } from 'src/lib/types' 3 | import type { NodeTypeToken } from 'src/model/graph' 4 | import type { GraphLink } from 'src/model/link' 5 | import type { GraphNode } from 'src/model/node' 6 | 7 | /** 8 | * Modifier for the drag. 9 | */ 10 | export type DragModifier> = ( 11 | drag: Drag 12 | ) => void 13 | 14 | /** 15 | * Modifier for node circles. 16 | */ 17 | export type NodeModifier> = ( 18 | selection: Selection 19 | ) => void 20 | 21 | /** 22 | * Modifier for node labels. 23 | */ 24 | export type NodeLabelModifier< 25 | T extends NodeTypeToken, 26 | Node extends GraphNode 27 | > = (selection: Selection) => void 28 | 29 | /** 30 | * Modifier for link paths. 31 | */ 32 | export type LinkModifier< 33 | T extends NodeTypeToken, 34 | Node extends GraphNode, 35 | Link extends GraphLink 36 | > = (selection: Selection) => void 37 | 38 | /** 39 | * Modifier for link labels. 40 | */ 41 | export type LinkLabelModifier< 42 | T extends NodeTypeToken, 43 | Node extends GraphNode, 44 | Link extends GraphLink 45 | > = (selection: Selection) => void 46 | 47 | /** 48 | * Modifier for the simulation. 49 | */ 50 | export type SimulationModifier< 51 | T extends NodeTypeToken, 52 | Node extends GraphNode, 53 | Link extends GraphLink 54 | > = (simulation: GraphSimulation) => void 55 | 56 | /** 57 | * Modifier for the zoom. 58 | */ 59 | export type ZoomModifier = (zoom: Zoom) => void 60 | 61 | /** 62 | * Low-level callbacks for modifying the underlying d3-selection.wd 63 | */ 64 | export interface Modifiers< 65 | T extends NodeTypeToken, 66 | Node extends GraphNode, 67 | Link extends GraphLink 68 | > { 69 | /** 70 | * Modify the drag. 71 | * @param drag - The drag. 72 | */ 73 | readonly drag?: DragModifier 74 | /** 75 | * Modify the node selection. 76 | * @param selection - The selection of nodes. 77 | */ 78 | readonly node?: NodeModifier 79 | /** 80 | * Modify the node label selection. 81 | * @param selection - The selection of node labels. 82 | */ 83 | readonly nodeLabel?: NodeLabelModifier 84 | /** 85 | * Modify the link selection. 86 | * @param selection - The selection of links. 87 | */ 88 | readonly link?: LinkModifier 89 | /** 90 | * Modify the link label selection. 91 | * @param selection - The selection of link labels. 92 | */ 93 | readonly linkLabel?: LinkLabelModifier 94 | /** 95 | * Modify the simulation. 96 | * @param simulation - The simulation. 97 | */ 98 | readonly simulation?: SimulationModifier 99 | /** 100 | * Modify the zoom. 101 | * @param zoom - The zoom. 102 | */ 103 | readonly zoom?: ZoomModifier 104 | } 105 | -------------------------------------------------------------------------------- /src/config/position.ts: -------------------------------------------------------------------------------- 1 | import type { NodeTypeToken } from 'src/model/graph' 2 | import type { GraphNode } from 'src/model/node' 3 | 4 | /** 5 | * Initializes a node's position in context of a graph's width and height. 6 | */ 7 | export type PositionInitializer< 8 | T extends NodeTypeToken, 9 | Node extends GraphNode 10 | > = (node: Node, width: number, height: number) => [number, number] 11 | 12 | const Centered: PositionInitializer = ( 13 | _, 14 | width, 15 | height 16 | ) => [width / 2, height / 2] 17 | 18 | const Randomized: PositionInitializer = ( 19 | _, 20 | width, 21 | height 22 | ) => [randomInRange(0, width), randomInRange(0, height)] 23 | 24 | function randomInRange(min: number, max: number): number { 25 | return Math.random() * (max - min) + min 26 | } 27 | 28 | /** 29 | * Collection of built-in position initializers. 30 | */ 31 | export const PositionInitializers = { 32 | /** 33 | * Initializes node positions to a graph's center. 34 | */ 35 | Centered, 36 | /** 37 | * Randomly initializes node positions within the visible area. 38 | */ 39 | Randomized, 40 | } 41 | -------------------------------------------------------------------------------- /src/config/simulation.ts: -------------------------------------------------------------------------------- 1 | import type { AlphaConfig } from 'src/config/alpha' 2 | import type { SimulationForceConfig } from 'src/config/forces' 3 | import type { NodeTypeToken } from 'src/model/graph' 4 | import type { GraphLink } from 'src/model/link' 5 | import type { GraphNode } from 'src/model/node' 6 | 7 | export interface SimulationConfig< 8 | T extends NodeTypeToken, 9 | Node extends GraphNode, 10 | Link extends GraphLink 11 | > { 12 | /** 13 | * Alpha value configuration. 14 | */ 15 | readonly alphas: AlphaConfig 16 | /** 17 | * Force configuration. 18 | */ 19 | readonly forces: SimulationForceConfig 20 | } 21 | -------------------------------------------------------------------------------- /src/config/zoom.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Zoom configuration. 3 | */ 4 | export interface ZoomConfig { 5 | /** 6 | * The initial zoom. 7 | * Must be in range [min, max]. 8 | */ 9 | readonly initial: number 10 | /** 11 | * Maximum zoom level. 12 | * May not be smaller than initial zoom. 13 | */ 14 | readonly max: number 15 | /** 16 | * Minimum zoom level. 17 | * May not be larger than initial zoom or less than or equal to 0. 18 | */ 19 | readonly min: number 20 | } 21 | -------------------------------------------------------------------------------- /src/controller.ts: -------------------------------------------------------------------------------- 1 | import { debounce } from '@yeger/debounce' 2 | import { select } from 'd3-selection' 3 | import type { D3ZoomEvent } from 'd3-zoom' 4 | import type { GraphConfig } from 'src/config/config' 5 | import type { LinkFilter } from 'src/config/filter' 6 | import { defineCanvas, updateCanvasTransform } from 'src/lib/canvas' 7 | import { defineDrag } from 'src/lib/drag' 8 | import { filterGraph } from 'src/lib/filter' 9 | import { createLinks, defineLinkSelection, updateLinks } from 'src/lib/link' 10 | import { createMarkers, defineMarkerSelection } from 'src/lib/marker' 11 | import { createNodes, defineNodeSelection, updateNodes } from 'src/lib/node' 12 | import { defineSimulation } from 'src/lib/simulation' 13 | import type { 14 | Canvas, 15 | Drag, 16 | GraphSimulation, 17 | LinkSelection, 18 | MarkerSelection, 19 | NodeSelection, 20 | Zoom, 21 | } from 'src/lib/types' 22 | import { isNumber } from 'src/lib/utils' 23 | import { defineZoom } from 'src/lib/zoom' 24 | import type { Graph, NodeTypeToken } from 'src/model/graph' 25 | import type { GraphLink } from 'src/model/link' 26 | import type { GraphNode } from 'src/model/node' 27 | import { Vector } from 'vecti' 28 | 29 | /** 30 | * Controller for a graph view. 31 | */ 32 | export class GraphController< 33 | T extends NodeTypeToken = NodeTypeToken, 34 | Node extends GraphNode = GraphNode, 35 | Link extends GraphLink = GraphLink 36 | > { 37 | /** 38 | * Array of all node types included in the controller's graph. 39 | */ 40 | public readonly nodeTypes: T[] 41 | private _nodeTypeFilter: T[] 42 | private _includeUnlinked = true 43 | 44 | private _linkFilter: LinkFilter = () => true 45 | 46 | private _showLinkLabels = true 47 | private _showNodeLabels = true 48 | 49 | private filteredGraph!: Graph 50 | 51 | private width = 0 52 | private height = 0 53 | 54 | private simulation: GraphSimulation | undefined 55 | 56 | private canvas: Canvas | undefined 57 | private linkSelection: LinkSelection | undefined 58 | private nodeSelection: NodeSelection | undefined 59 | private markerSelection: MarkerSelection | undefined 60 | 61 | private zoom: Zoom | undefined 62 | private drag: Drag | undefined 63 | 64 | private xOffset = 0 65 | private yOffset = 0 66 | private scale: number 67 | 68 | private focusedNode: Node | undefined = undefined 69 | 70 | private resizeObserver?: ResizeObserver 71 | 72 | /** 73 | * Create a new controller and initialize the view. 74 | * @param container - The container the graph will be placed in. 75 | * @param graph - The graph of the controller. 76 | * @param config - The config of the controller. 77 | */ 78 | public constructor( 79 | private readonly container: HTMLDivElement, 80 | private readonly graph: Graph, 81 | private readonly config: GraphConfig 82 | ) { 83 | this.scale = config.zoom.initial 84 | 85 | this.resetView() 86 | 87 | this.graph.nodes.forEach((node) => { 88 | const [x, y] = config.positionInitializer( 89 | node, 90 | this.effectiveWidth, 91 | this.effectiveHeight 92 | ) 93 | node.x = node.x ?? x 94 | node.y = node.y ?? y 95 | }) 96 | 97 | this.nodeTypes = [...new Set(graph.nodes.map((d) => d.type))] 98 | this._nodeTypeFilter = [...this.nodeTypes] 99 | 100 | if (config.initial) { 101 | const { 102 | includeUnlinked, 103 | nodeTypeFilter, 104 | linkFilter, 105 | showLinkLabels, 106 | showNodeLabels, 107 | } = config.initial 108 | this._includeUnlinked = includeUnlinked ?? this._includeUnlinked 109 | this._showLinkLabels = showLinkLabels ?? this._showLinkLabels 110 | this._showNodeLabels = showNodeLabels ?? this._showNodeLabels 111 | this._nodeTypeFilter = nodeTypeFilter ?? this._nodeTypeFilter 112 | this._linkFilter = linkFilter ?? this._linkFilter 113 | } 114 | 115 | this.filterGraph(undefined) 116 | this.initGraph() 117 | this.restart(config.simulation.alphas.initialize) 118 | 119 | if (config.autoResize) { 120 | this.resizeObserver = new ResizeObserver(debounce(() => this.resize())) 121 | this.resizeObserver.observe(this.container) 122 | } 123 | } 124 | 125 | /** 126 | * Get the current node type filter. 127 | * Only nodes whose type is included will be shown. 128 | */ 129 | public get nodeTypeFilter(): T[] { 130 | return this._nodeTypeFilter 131 | } 132 | 133 | /** 134 | * Get whether nodes without incoming or outgoing links will be shown or not. 135 | */ 136 | public get includeUnlinked(): boolean { 137 | return this._includeUnlinked 138 | } 139 | 140 | /** 141 | * Set whether nodes without incoming or outgoing links will be shown or not. 142 | * @param value - The value. 143 | */ 144 | public set includeUnlinked(value: boolean) { 145 | this._includeUnlinked = value 146 | this.filterGraph(this.focusedNode) 147 | const { include, exclude } = this.config.simulation.alphas.filter.unlinked 148 | const alpha = value ? include : exclude 149 | this.restart(alpha) 150 | } 151 | 152 | /** 153 | * Set a new link filter and update the controller's state. 154 | * @param value - The new link filter. 155 | */ 156 | public set linkFilter(value: LinkFilter) { 157 | this._linkFilter = value 158 | this.filterGraph(this.focusedNode) 159 | this.restart(this.config.simulation.alphas.filter.link) 160 | } 161 | 162 | /** 163 | * Get the current link filter. 164 | * @returns - The current link filter. 165 | */ 166 | public get linkFilter(): LinkFilter { 167 | return this._linkFilter 168 | } 169 | 170 | /** 171 | * Get whether node labels are shown or not. 172 | */ 173 | public get showNodeLabels(): boolean { 174 | return this._showNodeLabels 175 | } 176 | 177 | /** 178 | * Set whether node labels will be shown or not. 179 | * @param value - The value. 180 | */ 181 | public set showNodeLabels(value: boolean) { 182 | this._showNodeLabels = value 183 | const { hide, show } = this.config.simulation.alphas.labels.nodes 184 | const alpha = value ? show : hide 185 | this.restart(alpha) 186 | } 187 | 188 | /** 189 | * Get whether link labels are shown or not. 190 | */ 191 | public get showLinkLabels(): boolean { 192 | return this._showLinkLabels 193 | } 194 | 195 | /** 196 | * Set whether link labels will be shown or not. 197 | * @param value - The value. 198 | */ 199 | public set showLinkLabels(value: boolean) { 200 | this._showLinkLabels = value 201 | const { hide, show } = this.config.simulation.alphas.labels.links 202 | const alpha = value ? show : hide 203 | this.restart(alpha) 204 | } 205 | 206 | private get effectiveWidth(): number { 207 | return this.width / this.scale 208 | } 209 | 210 | private get effectiveHeight(): number { 211 | return this.height / this.scale 212 | } 213 | 214 | private get effectiveCenter(): Vector { 215 | return Vector.of([this.width, this.height]) 216 | .divide(2) 217 | .subtract(Vector.of([this.xOffset, this.yOffset])) 218 | .divide(this.scale) 219 | } 220 | 221 | /** 222 | * Resize the graph to fit its container. 223 | */ 224 | public resize(): void { 225 | const oldWidth = this.width 226 | const oldHeight = this.height 227 | const newWidth = this.container.getBoundingClientRect().width 228 | const newHeight = this.container.getBoundingClientRect().height 229 | const widthDiffers = oldWidth.toFixed() !== newWidth.toFixed() 230 | const heightDiffers = oldHeight.toFixed() !== newHeight.toFixed() 231 | 232 | if (!widthDiffers && !heightDiffers) { 233 | return 234 | } 235 | 236 | this.width = this.container.getBoundingClientRect().width 237 | this.height = this.container.getBoundingClientRect().height 238 | 239 | const alpha = this.config.simulation.alphas.resize 240 | 241 | this.restart( 242 | isNumber(alpha) 243 | ? alpha 244 | : alpha({ oldWidth, oldHeight, newWidth, newHeight }) 245 | ) 246 | } 247 | 248 | /** 249 | * Restart the controller. 250 | * @param alpha - The alpha value of the controller's simulation after the restart. 251 | */ 252 | public restart(alpha: number): void { 253 | this.markerSelection = createMarkers({ 254 | config: this.config, 255 | graph: this.filteredGraph, 256 | selection: this.markerSelection, 257 | }) 258 | 259 | this.linkSelection = createLinks({ 260 | config: this.config, 261 | graph: this.filteredGraph, 262 | selection: this.linkSelection, 263 | showLabels: this._showLinkLabels, 264 | }) 265 | 266 | this.nodeSelection = createNodes({ 267 | config: this.config, 268 | drag: this.drag, 269 | graph: this.filteredGraph, 270 | onNodeContext: (d) => this.toggleNodeFocus(d), 271 | onNodeSelected: this.config.callbacks.nodeClicked, 272 | selection: this.nodeSelection, 273 | showLabels: this._showNodeLabels, 274 | }) 275 | 276 | this.simulation?.stop() 277 | this.simulation = defineSimulation({ 278 | center: () => this.effectiveCenter, 279 | config: this.config, 280 | graph: this.filteredGraph, 281 | onTick: () => this.onTick(), 282 | }) 283 | .alpha(alpha) 284 | .restart() 285 | } 286 | 287 | /** 288 | * Update the node type filter by either including or removing the specified type from the filter. 289 | * @param include - Whether the type will be included or removed from the filter. 290 | * @param nodeType - The type to be added or removed from the filter. 291 | */ 292 | public filterNodesByType(include: boolean, nodeType: T) { 293 | if (include) { 294 | this._nodeTypeFilter.push(nodeType) 295 | } else { 296 | this._nodeTypeFilter = this._nodeTypeFilter.filter( 297 | (type) => type !== nodeType 298 | ) 299 | } 300 | this.filterGraph(this.focusedNode) 301 | this.restart(this.config.simulation.alphas.filter.type) 302 | } 303 | 304 | /** 305 | * Shut down the controller's simulation and (optional) automatic resizing. 306 | */ 307 | public shutdown(): void { 308 | if (this.focusedNode !== undefined) { 309 | this.focusedNode.isFocused = false 310 | this.focusedNode = undefined 311 | } 312 | this.resizeObserver?.unobserve(this.container) 313 | this.simulation?.stop() 314 | } 315 | 316 | private initGraph(): void { 317 | this.zoom = defineZoom({ 318 | config: this.config, 319 | canvasContainer: () => select(this.container).select('svg'), 320 | min: this.config.zoom.min, 321 | max: this.config.zoom.max, 322 | onZoom: (event) => this.onZoom(event), 323 | }) 324 | this.canvas = defineCanvas({ 325 | applyZoom: this.scale !== 1, 326 | container: select(this.container), 327 | offset: [this.xOffset, this.yOffset], 328 | scale: this.scale, 329 | zoom: this.zoom, 330 | }) 331 | this.applyZoom() 332 | this.linkSelection = defineLinkSelection(this.canvas) 333 | this.nodeSelection = defineNodeSelection(this.canvas) 334 | this.markerSelection = defineMarkerSelection(this.canvas) 335 | this.drag = defineDrag({ 336 | config: this.config, 337 | onDragStart: () => 338 | this.simulation 339 | ?.alphaTarget(this.config.simulation.alphas.drag.start) 340 | .restart(), 341 | onDragEnd: () => 342 | this.simulation 343 | ?.alphaTarget(this.config.simulation.alphas.drag.end) 344 | .restart(), 345 | }) 346 | } 347 | 348 | private onTick(): void { 349 | updateNodes(this.nodeSelection) 350 | 351 | updateLinks({ 352 | config: this.config, 353 | center: this.effectiveCenter, 354 | graph: this.filteredGraph, 355 | selection: this.linkSelection, 356 | }) 357 | } 358 | 359 | private resetView(): void { 360 | this.simulation?.stop() 361 | select(this.container).selectChildren().remove() 362 | this.zoom = undefined 363 | this.canvas = undefined 364 | this.linkSelection = undefined 365 | this.nodeSelection = undefined 366 | this.markerSelection = undefined 367 | this.simulation = undefined 368 | this.width = this.container.getBoundingClientRect().width 369 | this.height = this.container.getBoundingClientRect().height 370 | } 371 | 372 | private onZoom(event: D3ZoomEvent): void { 373 | this.xOffset = event.transform.x 374 | this.yOffset = event.transform.y 375 | this.scale = event.transform.k 376 | this.applyZoom() 377 | this.simulation?.restart() 378 | } 379 | 380 | private applyZoom() { 381 | updateCanvasTransform({ 382 | canvas: this.canvas, 383 | scale: this.scale, 384 | xOffset: this.xOffset, 385 | yOffset: this.yOffset, 386 | }) 387 | } 388 | 389 | private toggleNodeFocus(node: Node): void { 390 | if (node.isFocused) { 391 | this.filterGraph(undefined) 392 | this.restart(this.config.simulation.alphas.focus.release(node)) 393 | } else { 394 | this.focusNode(node) 395 | } 396 | } 397 | 398 | private focusNode(node: Node): void { 399 | this.filterGraph(node) 400 | this.restart(this.config.simulation.alphas.focus.acquire(node)) 401 | } 402 | 403 | private filterGraph(nodeToFocus?: Node): void { 404 | if (this.focusedNode !== undefined) { 405 | this.focusedNode.isFocused = false 406 | this.focusedNode = undefined 407 | } 408 | 409 | if ( 410 | nodeToFocus !== undefined && 411 | this._nodeTypeFilter.includes(nodeToFocus.type) 412 | ) { 413 | nodeToFocus.isFocused = true 414 | this.focusedNode = nodeToFocus 415 | } 416 | 417 | this.filteredGraph = filterGraph({ 418 | graph: this.graph, 419 | filter: this._nodeTypeFilter, 420 | focusedNode: this.focusedNode, 421 | includeUnlinked: this._includeUnlinked, 422 | linkFilter: this._linkFilter, 423 | }) 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /src/lib/canvas.ts: -------------------------------------------------------------------------------- 1 | import { zoomIdentity } from 'd3-zoom' 2 | import type { Canvas, GraphHost, Zoom } from 'src/lib/types' 3 | import { terminateEvent } from 'src/lib/utils' 4 | 5 | export interface DefineCanvasParams { 6 | readonly applyZoom: boolean 7 | readonly container: GraphHost 8 | readonly offset: [number, number] 9 | readonly onDoubleClick?: (event: PointerEvent) => void 10 | readonly onPointerMoved?: (event: PointerEvent) => void 11 | readonly onPointerUp?: (event: PointerEvent) => void 12 | readonly scale: number 13 | readonly zoom: Zoom 14 | } 15 | 16 | export function defineCanvas({ 17 | applyZoom, 18 | container, 19 | onDoubleClick, 20 | onPointerMoved, 21 | onPointerUp, 22 | offset: [xOffset, yOffset], 23 | scale, 24 | zoom, 25 | }: DefineCanvasParams): Canvas { 26 | const svg = container 27 | .classed('graph', true) 28 | .append('svg') 29 | .attr('height', '100%') 30 | .attr('width', '100%') 31 | .call(zoom) 32 | .on('contextmenu', (event: MouseEvent) => terminateEvent(event)) 33 | .on('dblclick', (event: PointerEvent) => onDoubleClick?.(event)) 34 | .on('dblclick.zoom', null) 35 | .on('pointermove', (event: PointerEvent) => onPointerMoved?.(event)) 36 | .on('pointerup', (event: PointerEvent) => onPointerUp?.(event)) 37 | .style('cursor', 'grab') 38 | 39 | if (applyZoom) { 40 | svg.call( 41 | zoom.transform, 42 | zoomIdentity.translate(xOffset, yOffset).scale(scale) 43 | ) 44 | } 45 | 46 | return svg.append('g') 47 | } 48 | 49 | export interface UpdateCanvasParams { 50 | readonly canvas?: Canvas | undefined 51 | readonly scale: number 52 | readonly xOffset: number 53 | readonly yOffset: number 54 | } 55 | 56 | export function updateCanvasTransform({ 57 | canvas, 58 | scale, 59 | xOffset, 60 | yOffset, 61 | }: UpdateCanvasParams): void { 62 | canvas?.attr('transform', `translate(${xOffset},${yOffset})scale(${scale})`) 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/drag.ts: -------------------------------------------------------------------------------- 1 | import { drag } from 'd3-drag' 2 | import { select } from 'd3-selection' 3 | import type { GraphConfig } from 'src/config/config' 4 | import type { Drag, NodeDragEvent } from 'src/lib/types' 5 | import type { NodeTypeToken } from 'src/model/graph' 6 | import type { GraphLink } from 'src/model/link' 7 | import type { GraphNode } from 'src/model/node' 8 | 9 | export interface DefineDragParams< 10 | T extends NodeTypeToken, 11 | Node extends GraphNode, 12 | Link extends GraphLink 13 | > { 14 | readonly config: GraphConfig 15 | readonly onDragStart: (event: NodeDragEvent, d: Node) => void 16 | readonly onDragEnd: (event: NodeDragEvent, d: Node) => void 17 | } 18 | 19 | export function defineDrag< 20 | T extends NodeTypeToken, 21 | Node extends GraphNode, 22 | Link extends GraphLink 23 | >({ 24 | config, 25 | onDragStart, 26 | onDragEnd, 27 | }: DefineDragParams): Drag { 28 | const drg = drag() 29 | .filter((event: MouseEvent | TouchEvent) => { 30 | if (event.type === 'mousedown') { 31 | return (event as MouseEvent).button === 0 // primary (left) mouse button 32 | } else if (event.type === 'touchstart') { 33 | return (event as TouchEvent).touches.length === 1 34 | } 35 | return false 36 | }) 37 | .on('start', (event: NodeDragEvent, d) => { 38 | if (event.active === 0) { 39 | onDragStart(event, d) 40 | } 41 | select(event.sourceEvent.target).classed('grabbed', true) 42 | d.fx = d.x 43 | d.fy = d.y 44 | }) 45 | .on('drag', (event: NodeDragEvent, d) => { 46 | d.fx = event.x 47 | d.fy = event.y 48 | }) 49 | .on('end', (event: NodeDragEvent, d) => { 50 | if (event.active === 0) { 51 | onDragEnd(event, d) 52 | } 53 | select(event.sourceEvent.target).classed('grabbed', false) 54 | d.fx = undefined 55 | d.fy = undefined 56 | }) 57 | 58 | config.modifiers.drag?.(drg) 59 | 60 | return drg 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/filter.ts: -------------------------------------------------------------------------------- 1 | import type { LinkFilter } from 'src/config/filter' 2 | import type { Graph, NodeTypeToken } from 'src/model/graph' 3 | import type { GraphLink } from 'src/model/link' 4 | import type { GraphNode } from 'src/model/node' 5 | 6 | export interface GraphFilterParams< 7 | T extends NodeTypeToken, 8 | Node extends GraphNode, 9 | Link extends GraphLink 10 | > { 11 | readonly graph: Graph 12 | readonly filter: T[] 13 | readonly focusedNode?: Node | undefined 14 | readonly includeUnlinked: boolean 15 | readonly linkFilter: LinkFilter 16 | } 17 | 18 | export function filterGraph< 19 | T extends NodeTypeToken, 20 | Node extends GraphNode, 21 | Link extends GraphLink 22 | >({ 23 | graph, 24 | filter, 25 | focusedNode, 26 | includeUnlinked, 27 | linkFilter, 28 | }: GraphFilterParams): Graph { 29 | const links = graph.links.filter( 30 | (d) => 31 | filter.includes(d.source.type) && 32 | filter.includes(d.target.type) && 33 | linkFilter(d) 34 | ) 35 | 36 | const isLinked = (node: Node) => 37 | links.find( 38 | (link) => link.source.id === node.id || link.target.id === node.id 39 | ) !== undefined 40 | const nodes = graph.nodes.filter( 41 | (d) => filter.includes(d.type) && (includeUnlinked || isLinked(d)) 42 | ) 43 | 44 | if (focusedNode === undefined || !filter.includes(focusedNode.type)) { 45 | return { 46 | nodes, 47 | links, 48 | } 49 | } 50 | 51 | return getFocusedSubgraph({ nodes, links }, focusedNode) 52 | } 53 | 54 | function getFocusedSubgraph< 55 | T extends NodeTypeToken, 56 | Node extends GraphNode, 57 | Link extends GraphLink 58 | >(graph: Graph, source: Node): Graph { 59 | const links = [ 60 | ...getIncomingLinksTransitively(graph, source), 61 | ...getOutgoingLinksTransitively(graph, source), 62 | ] 63 | 64 | const nodes = links.flatMap((link) => [link.source, link.target]) 65 | 66 | return { 67 | nodes: [...new Set([...nodes, source])], 68 | links: [...new Set(links)], 69 | } 70 | } 71 | 72 | function getIncomingLinksTransitively< 73 | T extends NodeTypeToken, 74 | Node extends GraphNode, 75 | Link extends GraphLink 76 | >(graph: Graph, source: Node): Link[] { 77 | return getLinksInDirectionTransitively( 78 | graph, 79 | source, 80 | (link, node) => link.target.id === node.id 81 | ) 82 | } 83 | 84 | function getOutgoingLinksTransitively< 85 | T extends NodeTypeToken, 86 | Node extends GraphNode, 87 | Link extends GraphLink 88 | >(graph: Graph, source: Node): Link[] { 89 | return getLinksInDirectionTransitively( 90 | graph, 91 | source, 92 | (link, node) => link.source.id === node.id 93 | ) 94 | } 95 | 96 | function getLinksInDirectionTransitively< 97 | T extends NodeTypeToken, 98 | Node extends GraphNode, 99 | Link extends GraphLink 100 | >( 101 | graph: Graph, 102 | source: Node, 103 | directionPredicate: (link: Link, node: Node) => boolean 104 | ): Link[] { 105 | const remainingLinks = new Set(graph.links) 106 | const foundNodes = new Set([source]) 107 | const foundLinks: Link[] = [] 108 | 109 | while (remainingLinks.size > 0) { 110 | const newLinks = [...remainingLinks].filter((link) => 111 | [...foundNodes].some((node) => directionPredicate(link, node)) 112 | ) 113 | 114 | if (newLinks.length === 0) { 115 | return foundLinks 116 | } 117 | 118 | newLinks.forEach((link) => { 119 | foundNodes.add(link.source) 120 | foundNodes.add(link.target) 121 | foundLinks.push(link) 122 | remainingLinks.delete(link) 123 | }) 124 | } 125 | 126 | return foundLinks 127 | } 128 | -------------------------------------------------------------------------------- /src/lib/link.ts: -------------------------------------------------------------------------------- 1 | import type { GraphConfig } from 'src/config/config' 2 | import Paths from 'src/lib/paths' 3 | import type { Canvas, LinkSelection } from 'src/lib/types' 4 | import { getLinkId, getMarkerUrl } from 'src/lib/utils' 5 | import type { Graph, NodeTypeToken } from 'src/model/graph' 6 | import type { GraphLink } from 'src/model/link' 7 | import type { GraphNode } from 'src/model/node' 8 | import type { Vector } from 'vecti' 9 | 10 | export function defineLinkSelection< 11 | T extends NodeTypeToken, 12 | Node extends GraphNode, 13 | Link extends GraphLink 14 | >(canvas: Canvas): LinkSelection { 15 | return canvas.append('g').classed('links', true).selectAll('path') 16 | } 17 | 18 | export interface CreateLinksParams< 19 | T extends NodeTypeToken, 20 | Node extends GraphNode, 21 | Link extends GraphLink 22 | > { 23 | readonly config: GraphConfig 24 | readonly graph: Graph 25 | readonly selection?: LinkSelection | undefined 26 | readonly showLabels: boolean 27 | } 28 | 29 | export function createLinks< 30 | T extends NodeTypeToken, 31 | Node extends GraphNode, 32 | Link extends GraphLink 33 | >({ 34 | config, 35 | graph, 36 | selection, 37 | showLabels, 38 | }: CreateLinksParams): LinkSelection | undefined { 39 | const result = selection 40 | ?.data(graph.links, (d) => getLinkId(d)) 41 | .join((enter) => { 42 | const linkGroup = enter.append('g') 43 | 44 | const linkPath = linkGroup 45 | .append('path') 46 | .classed('link', true) 47 | .style('marker-end', (d) => getMarkerUrl(d)) 48 | .style('stroke', (d) => d.color) 49 | 50 | config.modifiers.link?.(linkPath) 51 | 52 | const linkLabel = linkGroup 53 | .append('text') 54 | .classed('link__label', true) 55 | .style('fill', (d) => (d.label ? d.label.color : null)) 56 | .style('font-size', (d) => (d.label ? d.label.fontSize : null)) 57 | .text((d) => (d.label ? d.label.text : null)) 58 | 59 | config.modifiers.linkLabel?.(linkLabel) 60 | 61 | return linkGroup 62 | }) 63 | 64 | result 65 | ?.select('.link__label') 66 | .attr('opacity', (d) => (d.label && showLabels ? 1 : 0)) 67 | 68 | return result 69 | } 70 | 71 | export interface UpdateLinksParams< 72 | T extends NodeTypeToken, 73 | Node extends GraphNode, 74 | Link extends GraphLink 75 | > { 76 | readonly center: Vector 77 | readonly config: GraphConfig 78 | readonly graph: Graph 79 | readonly selection: LinkSelection | undefined 80 | } 81 | 82 | export function updateLinks< 83 | T extends NodeTypeToken, 84 | Node extends GraphNode, 85 | Link extends GraphLink 86 | >(params: UpdateLinksParams): void { 87 | updateLinkPaths(params) 88 | updateLinkLabels(params) 89 | } 90 | 91 | function updateLinkPaths< 92 | T extends NodeTypeToken, 93 | Node extends GraphNode, 94 | Link extends GraphLink 95 | >({ 96 | center, 97 | config, 98 | graph, 99 | selection, 100 | }: UpdateLinksParams): void { 101 | selection?.selectAll('path').attr('d', (d) => { 102 | if ( 103 | d.source.x === undefined || 104 | d.source.y === undefined || 105 | d.target.x === undefined || 106 | d.target.y === undefined 107 | ) { 108 | return '' 109 | } 110 | if (d.source.id === d.target.id) { 111 | return Paths.reflexive.path({ 112 | config, 113 | node: d.source, 114 | center, 115 | }) 116 | } else if (areBidirectionallyConnected(graph, d.source, d.target)) { 117 | return Paths.arc.path({ config, source: d.source, target: d.target }) 118 | } else { 119 | return Paths.line.path({ config, source: d.source, target: d.target }) 120 | } 121 | }) 122 | } 123 | 124 | function updateLinkLabels< 125 | T extends NodeTypeToken, 126 | Node extends GraphNode, 127 | Link extends GraphLink 128 | >({ 129 | config, 130 | center, 131 | graph, 132 | selection, 133 | }: UpdateLinksParams): void { 134 | selection?.select('.link__label').attr('transform', (d) => { 135 | if ( 136 | d.source.x === undefined || 137 | d.source.y === undefined || 138 | d.target.x === undefined || 139 | d.target.y === undefined 140 | ) { 141 | return 'translate(0, 0)' 142 | } 143 | if (d.source.id === d.target.id) { 144 | return Paths.reflexive.labelTransform({ 145 | config, 146 | node: d.source, 147 | center, 148 | }) 149 | } else if (areBidirectionallyConnected(graph, d.source, d.target)) { 150 | return Paths.arc.labelTransform({ 151 | config, 152 | source: d.source, 153 | target: d.target, 154 | }) 155 | } else { 156 | return Paths.line.labelTransform({ 157 | config, 158 | source: d.source, 159 | target: d.target, 160 | }) 161 | } 162 | }) 163 | } 164 | 165 | function areBidirectionallyConnected< 166 | T extends NodeTypeToken, 167 | Node extends GraphNode, 168 | Link extends GraphLink 169 | >(graph: Graph, source: Node, target: Node): boolean { 170 | return ( 171 | source.id !== target.id && 172 | graph.links.some( 173 | (l) => l.target.id === source.id && l.source.id === target.id 174 | ) && 175 | graph.links.some( 176 | (l) => l.target.id === target.id && l.source.id === source.id 177 | ) 178 | ) 179 | } 180 | -------------------------------------------------------------------------------- /src/lib/marker.ts: -------------------------------------------------------------------------------- 1 | import type { GraphConfig } from 'src/config/config' 2 | import type { Canvas, MarkerSelection } from 'src/lib/types' 3 | import { getMarkerId } from 'src/lib/utils' 4 | import type { Graph, NodeTypeToken } from 'src/model/graph' 5 | import type { GraphLink } from 'src/model/link' 6 | import type { GraphNode } from 'src/model/node' 7 | 8 | export function defineMarkerSelection(canvas: Canvas): MarkerSelection { 9 | return canvas.append('defs').selectAll('marker') 10 | } 11 | 12 | export interface CreateMarkerParams< 13 | T extends NodeTypeToken, 14 | Node extends GraphNode, 15 | Link extends GraphLink 16 | > { 17 | readonly config: GraphConfig 18 | readonly graph: Graph 19 | readonly selection?: MarkerSelection | undefined 20 | } 21 | 22 | export function createMarkers< 23 | T extends NodeTypeToken, 24 | Node extends GraphNode, 25 | Link extends GraphLink 26 | >({ 27 | config, 28 | graph, 29 | selection, 30 | }: CreateMarkerParams): MarkerSelection | undefined { 31 | return selection 32 | ?.data(getUniqueColors(graph), (d) => d) 33 | .join((enter) => { 34 | const marker = enter 35 | .append('marker') 36 | .attr('id', (d) => getMarkerId(d)) 37 | .attr('markerHeight', 4 * config.marker.size) 38 | .attr('markerWidth', 4 * config.marker.size) 39 | .attr('markerUnits', 'userSpaceOnUse') 40 | .attr('orient', 'auto') 41 | .attr('refX', config.marker.ref[0]) 42 | .attr('refY', config.marker.ref[1]) 43 | .attr('viewBox', config.marker.viewBox) 44 | .style('fill', (d) => d) 45 | marker.append('path').attr('d', makeLine(config.marker.path)) 46 | return marker 47 | }) 48 | } 49 | 50 | function getUniqueColors< 51 | T extends NodeTypeToken, 52 | Node extends GraphNode, 53 | Link extends GraphLink 54 | >(graph: Graph): string[] { 55 | return [...new Set(graph.links.map((link) => link.color))] 56 | } 57 | 58 | function makeLine(points: [number, number][]): string { 59 | if (points.length < 1) { 60 | return 'M0,0' 61 | } 62 | const [[startX, startY], ...rest] = points 63 | return rest.reduce( 64 | (line, [x, y]) => `${line}L${x},${y}`, 65 | `M${startX},${startY}` 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/node.ts: -------------------------------------------------------------------------------- 1 | import type { GraphConfig } from 'src/config/config' 2 | import type { Canvas, Drag, NodeSelection } from 'src/lib/types' 3 | import { getNodeRadius, terminateEvent } from 'src/lib/utils' 4 | import type { Graph, NodeTypeToken } from 'src/model/graph' 5 | import type { GraphLink } from 'src/model/link' 6 | import type { GraphNode } from 'src/model/node' 7 | 8 | export function defineNodeSelection< 9 | T extends NodeTypeToken, 10 | Node extends GraphNode 11 | >(canvas: Canvas): NodeSelection { 12 | return canvas.append('g').classed('nodes', true).selectAll('circle') 13 | } 14 | 15 | export interface CreateNodesParams< 16 | T extends NodeTypeToken, 17 | Node extends GraphNode, 18 | Link extends GraphLink 19 | > { 20 | readonly config: GraphConfig 21 | readonly drag?: Drag | undefined 22 | readonly graph: Graph 23 | readonly onNodeSelected: ((node: Node) => void) | undefined 24 | readonly onNodeContext: (node: Node) => void 25 | readonly selection?: NodeSelection | undefined 26 | readonly showLabels: boolean 27 | } 28 | 29 | export function createNodes< 30 | T extends NodeTypeToken, 31 | Node extends GraphNode, 32 | Link extends GraphLink 33 | >({ 34 | config, 35 | drag, 36 | graph, 37 | onNodeContext, 38 | onNodeSelected, 39 | selection, 40 | showLabels, 41 | }: CreateNodesParams): NodeSelection | undefined { 42 | const result = selection 43 | ?.data(graph.nodes, (d) => d.id) 44 | .join((enter) => { 45 | const nodeGroup = enter.append('g') 46 | 47 | if (drag !== undefined) { 48 | nodeGroup.call(drag) 49 | } 50 | 51 | const nodeCircle = nodeGroup 52 | .append('circle') 53 | .classed('node', true) 54 | .attr('aria-label', (d) => (d.label ? d.label.text : d.id)) 55 | .attr('r', (d) => getNodeRadius(config, d)) 56 | .on('contextmenu', (event, d) => { 57 | terminateEvent(event) 58 | onNodeContext(d) 59 | }) 60 | .on('pointerdown', (event: PointerEvent, d) => 61 | onPointerDown(event, d, onNodeSelected ?? onNodeContext) 62 | ) 63 | .style('fill', (d) => d.color) 64 | 65 | config.modifiers.node?.(nodeCircle) 66 | 67 | const nodeLabel = nodeGroup 68 | .append('text') 69 | .classed('node__label', true) 70 | .attr('dy', `0.33em`) 71 | .style('fill', (d) => (d.label ? d.label.color : null)) 72 | .style('font-size', (d) => (d.label ? d.label.fontSize : null)) 73 | .style('stroke', 'none') 74 | .text((d) => (d.label ? d.label.text : null)) 75 | 76 | config.modifiers.nodeLabel?.(nodeLabel) 77 | 78 | return nodeGroup 79 | }) 80 | 81 | result?.select('.node').classed('focused', (d) => d.isFocused) 82 | result?.select('.node__label').attr('opacity', showLabels ? 1 : 0) 83 | 84 | return result 85 | } 86 | 87 | const DOUBLE_CLICK_INTERVAL_MS = 500 88 | 89 | function onPointerDown>( 90 | event: PointerEvent, 91 | node: Node, 92 | onNodeSelected: (node: Node) => void 93 | ): void { 94 | if (event.button !== undefined && event.button !== 0) { 95 | return 96 | } 97 | 98 | const lastInteractionTimestamp = node.lastInteractionTimestamp 99 | const now = Date.now() 100 | if ( 101 | lastInteractionTimestamp === undefined || 102 | now - lastInteractionTimestamp > DOUBLE_CLICK_INTERVAL_MS 103 | ) { 104 | node.lastInteractionTimestamp = now 105 | return 106 | } 107 | node.lastInteractionTimestamp = undefined 108 | onNodeSelected(node) 109 | } 110 | 111 | export function updateNodes>( 112 | selection?: NodeSelection 113 | ): void { 114 | selection?.attr('transform', (d) => `translate(${d.x ?? 0},${d.y ?? 0})`) 115 | } 116 | -------------------------------------------------------------------------------- /src/lib/paths.ts: -------------------------------------------------------------------------------- 1 | import type { GraphConfig } from 'src/config/config' 2 | import { getNodeRadius } from 'src/lib/utils' 3 | import type { NodeTypeToken } from 'src/model/graph' 4 | import type { GraphLink } from 'src/model/link' 5 | import type { GraphNode } from 'src/model/node' 6 | import { Vector } from 'vecti' 7 | 8 | // ################################################## 9 | // COMMON 10 | // ################################################## 11 | 12 | export interface PathParams< 13 | T extends NodeTypeToken, 14 | Node extends GraphNode, 15 | Link extends GraphLink 16 | > { 17 | readonly config: GraphConfig 18 | readonly source: Node 19 | readonly target: Node 20 | } 21 | 22 | export interface ReflexivePathParams< 23 | T extends NodeTypeToken, 24 | Node extends GraphNode, 25 | Link extends GraphLink 26 | > { 27 | readonly config: GraphConfig 28 | readonly node: Node 29 | readonly center: Vector 30 | } 31 | 32 | function getX>( 33 | node: Node 34 | ): number { 35 | return node.x ?? 0 36 | } 37 | 38 | function getY>( 39 | node: Node 40 | ): number { 41 | return node.y ?? 0 42 | } 43 | 44 | interface VectorData { 45 | readonly s: Vector 46 | readonly t: Vector 47 | readonly dist: number 48 | readonly norm: Vector 49 | readonly endNorm: Vector 50 | } 51 | 52 | function calculateVectorData< 53 | T extends NodeTypeToken, 54 | Node extends GraphNode, 55 | Link extends GraphLink 56 | >({ source, target }: PathParams): VectorData { 57 | const s = new Vector(getX(source), getY(source)) 58 | const t = new Vector(getX(target), getY(target)) 59 | const diff = t.subtract(s) 60 | const dist = diff.length() 61 | const norm = diff.normalize() 62 | const endNorm = norm.multiply(-1) 63 | return { 64 | s, 65 | t, 66 | dist, 67 | norm, 68 | endNorm, 69 | } 70 | } 71 | 72 | function calculateCenter< 73 | T extends NodeTypeToken, 74 | Node extends GraphNode, 75 | Link extends GraphLink 76 | >({ center, node }: ReflexivePathParams) { 77 | const n = new Vector(getX(node), getY(node)) 78 | let c = center 79 | if (n.x === c.x && n.y === c.y) { 80 | // Nodes at the exact center of the Graph should have their reflexive edge above them. 81 | c = c.add(new Vector(0, 1)) 82 | } 83 | return { 84 | n, 85 | c, 86 | } 87 | } 88 | 89 | function calculateSourceAndTarget< 90 | T extends NodeTypeToken, 91 | Node extends GraphNode, 92 | Link extends GraphLink 93 | >({ config, source, target }: PathParams) { 94 | const { s, t, norm } = calculateVectorData({ config, source, target }) 95 | const start = s.add(norm.multiply(getNodeRadius(config, source) - 1)) 96 | const end = t.subtract(norm.multiply(config.marker.padding(target, config))) 97 | return { 98 | start, 99 | end, 100 | } 101 | } 102 | 103 | // ################################################## 104 | // LINE 105 | // ################################################## 106 | 107 | function paddedLinePath< 108 | T extends NodeTypeToken, 109 | Node extends GraphNode, 110 | Link extends GraphLink 111 | >(params: PathParams): string { 112 | const { start, end } = calculateSourceAndTarget(params) 113 | return `M${start.x},${start.y} 114 | L${end.x},${end.y}` 115 | } 116 | 117 | function lineLinkTextTransform< 118 | T extends NodeTypeToken, 119 | Node extends GraphNode, 120 | Link extends GraphLink 121 | >(params: PathParams): string { 122 | const { start, end } = calculateSourceAndTarget(params) 123 | 124 | const midpoint = end.subtract(start).multiply(0.5) 125 | const result = start.add(midpoint) 126 | 127 | return `translate(${result.x - 8},${result.y - 4})` 128 | } 129 | 130 | // ################################################## 131 | // ARC 132 | // ################################################## 133 | 134 | function paddedArcPath< 135 | T extends NodeTypeToken, 136 | Node extends GraphNode, 137 | Link extends GraphLink 138 | >({ config, source, target }: PathParams): string { 139 | const { s, t, dist, norm, endNorm } = calculateVectorData({ 140 | config, 141 | source, 142 | target, 143 | }) 144 | const rotation = 10 145 | const start = norm 146 | .rotateByDegrees(-rotation) 147 | .multiply(getNodeRadius(config, source) - 1) 148 | .add(s) 149 | const end = endNorm 150 | .rotateByDegrees(rotation) 151 | .multiply(getNodeRadius(config, target)) 152 | .add(t) 153 | .add(endNorm.rotateByDegrees(rotation).multiply(2 * config.marker.size)) 154 | const arcRadius = 1.2 * dist 155 | return `M${start.x},${start.y} 156 | A${arcRadius},${arcRadius},0,0,1,${end.x},${end.y}` 157 | } 158 | 159 | // ################################################## 160 | // REFLEXIVE 161 | // ################################################## 162 | 163 | function paddedReflexivePath< 164 | T extends NodeTypeToken, 165 | Node extends GraphNode, 166 | Link extends GraphLink 167 | >({ center, config, node }: ReflexivePathParams): string { 168 | const { n, c } = calculateCenter({ center, config, node }) 169 | const radius = getNodeRadius(config, node) 170 | const diff = n.subtract(c) 171 | const norm = diff.multiply(1 / diff.length()) 172 | const rotation = 40 173 | const start = norm 174 | .rotateByDegrees(rotation) 175 | .multiply(radius - 1) 176 | .add(n) 177 | const end = norm 178 | .rotateByDegrees(-rotation) 179 | .multiply(radius) 180 | .add(n) 181 | .add(norm.rotateByDegrees(-rotation).multiply(2 * config.marker.size)) 182 | return `M${start.x},${start.y} 183 | A${radius},${radius},0,1,0,${end.x},${end.y}` 184 | } 185 | 186 | function bidirectionalLinkTextTransform< 187 | T extends NodeTypeToken, 188 | Node extends GraphNode, 189 | Link extends GraphLink 190 | >({ config, source, target }: PathParams): string { 191 | const { t, dist, endNorm } = calculateVectorData({ config, source, target }) 192 | const rotation = 10 193 | const end = endNorm 194 | .rotateByDegrees(rotation) 195 | .multiply(0.5 * dist) 196 | .add(t) 197 | return `translate(${end.x},${end.y})` 198 | } 199 | 200 | function reflexiveLinkTextTransform< 201 | T extends NodeTypeToken, 202 | Node extends GraphNode, 203 | Link extends GraphLink 204 | >({ center, config, node }: ReflexivePathParams): string { 205 | const { n, c } = calculateCenter({ center, config, node }) 206 | const diff = n.subtract(c) 207 | const offset = diff 208 | .multiply(1 / diff.length()) 209 | .multiply(3 * getNodeRadius(config, node) + 8) 210 | .add(n) 211 | return `translate(${offset.x},${offset.y})` 212 | } 213 | 214 | // ################################################## 215 | // EXPORT 216 | // ################################################## 217 | 218 | export default { 219 | line: { 220 | labelTransform: lineLinkTextTransform, 221 | path: paddedLinePath, 222 | }, 223 | arc: { 224 | labelTransform: bidirectionalLinkTextTransform, 225 | path: paddedArcPath, 226 | }, 227 | reflexive: { 228 | labelTransform: reflexiveLinkTextTransform, 229 | path: paddedReflexivePath, 230 | }, 231 | } 232 | -------------------------------------------------------------------------------- /src/lib/simulation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | forceCollide, 3 | forceLink, 4 | forceManyBody, 5 | forceSimulation, 6 | forceX, 7 | forceY, 8 | } from 'd3-force' 9 | import type { GraphConfig } from 'src/config/config' 10 | import type { GraphSimulation } from 'src/lib/types' 11 | import { getNodeRadius } from 'src/lib/utils' 12 | import type { Graph, NodeTypeToken } from 'src/model/graph' 13 | import type { GraphLink } from 'src/model/link' 14 | import type { GraphNode } from 'src/model/node' 15 | import type { Vector } from 'vecti' 16 | 17 | export interface DefineSimulationParams< 18 | T extends NodeTypeToken, 19 | Node extends GraphNode, 20 | Link extends GraphLink 21 | > { 22 | readonly center: () => Vector 23 | readonly config: GraphConfig 24 | readonly graph: Graph 25 | readonly onTick: () => void 26 | } 27 | 28 | export function defineSimulation< 29 | T extends NodeTypeToken, 30 | Node extends GraphNode, 31 | Link extends GraphLink 32 | >({ 33 | center, 34 | config, 35 | graph, 36 | onTick, 37 | }: DefineSimulationParams): GraphSimulation { 38 | const simulation = forceSimulation(graph.nodes) 39 | 40 | const centeringForce = config.simulation.forces.centering 41 | if (centeringForce && centeringForce.enabled) { 42 | const strength = centeringForce.strength 43 | simulation 44 | .force('x', forceX(() => center().x).strength(strength)) 45 | .force('y', forceY(() => center().y).strength(strength)) 46 | } 47 | 48 | const chargeForce = config.simulation.forces.charge 49 | if (chargeForce && chargeForce.enabled) { 50 | simulation.force( 51 | 'charge', 52 | forceManyBody().strength(chargeForce.strength) 53 | ) 54 | } 55 | 56 | const collisionForce = config.simulation.forces.collision 57 | if (collisionForce && collisionForce.enabled) { 58 | simulation.force( 59 | 'collision', 60 | forceCollide().radius( 61 | (d) => collisionForce.radiusMultiplier * getNodeRadius(config, d) 62 | ) 63 | ) 64 | } 65 | 66 | const linkForce = config.simulation.forces.link 67 | if (linkForce && linkForce.enabled) { 68 | simulation.force( 69 | 'link', 70 | forceLink(graph.links) 71 | .id((d) => d.id) 72 | .distance(config.simulation.forces.link.length) 73 | .strength(linkForce.strength) 74 | ) 75 | } 76 | 77 | simulation.on('tick', () => onTick()) 78 | 79 | config.modifiers.simulation?.(simulation) 80 | 81 | return simulation 82 | } 83 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { D3DragEvent, DragBehavior } from 'd3-drag' 2 | import type { Simulation } from 'd3-force' 3 | import type { Selection } from 'd3-selection' 4 | import type { ZoomBehavior } from 'd3-zoom' 5 | import type { NodeTypeToken } from 'src/model/graph' 6 | import type { GraphLink } from 'src/model/link' 7 | import type { GraphNode } from 'src/model/node' 8 | 9 | export type Canvas = Selection 10 | 11 | export type Drag< 12 | T extends NodeTypeToken, 13 | Node extends GraphNode 14 | > = DragBehavior 15 | export type NodeDragEvent< 16 | T extends NodeTypeToken, 17 | Node extends GraphNode 18 | > = D3DragEvent 19 | 20 | export type GraphHost = Selection 21 | 22 | export type GraphSimulation< 23 | T extends NodeTypeToken, 24 | Node extends GraphNode, 25 | Link extends GraphLink 26 | > = Simulation 27 | 28 | export type LinkSelection< 29 | T extends NodeTypeToken, 30 | Node extends GraphNode, 31 | Link extends GraphLink 32 | > = Selection 33 | 34 | export type MarkerSelection = Selection< 35 | SVGMarkerElement, 36 | string, 37 | SVGGElement, 38 | undefined 39 | > 40 | 41 | export type NodeSelection< 42 | T extends NodeTypeToken, 43 | Node extends GraphNode 44 | > = Selection 45 | 46 | export type Zoom = ZoomBehavior 47 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GraphConfig } from 'src/config/config' 2 | import type { NodeTypeToken } from 'src/model/graph' 3 | import type { GraphLink } from 'src/model/link' 4 | import type { GraphNode } from 'src/model/node' 5 | 6 | export function terminateEvent(event: Event): void { 7 | event.preventDefault() 8 | event.stopPropagation() 9 | } 10 | 11 | export function isNumber(value: unknown): value is number { 12 | return typeof value === 'number' 13 | } 14 | 15 | export function getNodeRadius< 16 | T extends NodeTypeToken, 17 | Node extends GraphNode, 18 | Link extends GraphLink 19 | >(config: GraphConfig, node: Node) { 20 | return isNumber(config.nodeRadius) 21 | ? config.nodeRadius 22 | : config.nodeRadius(node) 23 | } 24 | 25 | /** 26 | * Get the id of a link. 27 | * @param link - The link. 28 | */ 29 | export function getLinkId< 30 | T extends NodeTypeToken, 31 | Node extends GraphNode, 32 | Link extends GraphLink 33 | >(link: Link): string { 34 | return `${link.source.id}-${link.target.id}` 35 | } 36 | 37 | /** 38 | * Get the ID of a marker. 39 | * @param color - The color of the link. 40 | */ 41 | export function getMarkerId(color: string): string { 42 | return `link-arrow-${color}`.replace(/[()]/g, '~') 43 | } 44 | 45 | /** 46 | * Get the URL of a marker. 47 | * @param link - The link of the marker. 48 | */ 49 | export function getMarkerUrl< 50 | T extends NodeTypeToken, 51 | Node extends GraphNode, 52 | Link extends GraphLink 53 | >(link: Link): string { 54 | return `url(#${getMarkerId(link.color)})` 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/zoom.ts: -------------------------------------------------------------------------------- 1 | import type { Selection } from 'd3-selection' 2 | import type { D3ZoomEvent } from 'd3-zoom' 3 | import { zoom } from 'd3-zoom' 4 | import type { GraphConfig } from 'src/config/config' 5 | import type { Zoom } from 'src/lib/types' 6 | import type { NodeTypeToken } from 'src/model/graph' 7 | import type { GraphLink } from 'src/model/link' 8 | import type { GraphNode } from 'src/model/node' 9 | 10 | export interface DefineZoomParams< 11 | T extends NodeTypeToken, 12 | Node extends GraphNode, 13 | Link extends GraphLink 14 | > { 15 | readonly canvasContainer: () => Selection< 16 | SVGSVGElement, 17 | unknown, 18 | null, 19 | undefined 20 | > 21 | readonly config: GraphConfig 22 | readonly min: number 23 | readonly max: number 24 | readonly onZoom: (event: D3ZoomEvent) => void 25 | } 26 | 27 | export function defineZoom< 28 | T extends NodeTypeToken, 29 | Node extends GraphNode, 30 | Link extends GraphLink 31 | >({ 32 | canvasContainer, 33 | config, 34 | min, 35 | max, 36 | onZoom, 37 | }: DefineZoomParams): Zoom { 38 | const z = zoom() 39 | .scaleExtent([min, max]) 40 | .filter((event) => event.button === 0 || event.touches?.length >= 2) 41 | .on('start', () => canvasContainer().classed('grabbed', true)) 42 | .on('zoom', (event) => onZoom(event)) 43 | .on('end', () => canvasContainer().classed('grabbed', false)) 44 | 45 | config.modifiers.zoom?.(z) 46 | 47 | return z 48 | } 49 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | export * from 'src/config/alpha' 2 | export * from 'src/config/callbacks' 3 | export * from 'src/config/config' 4 | export * from 'src/config/filter' 5 | export * from 'src/config/forces' 6 | export * from 'src/config/initial' 7 | export * from 'src/config/marker' 8 | export * from 'src/config/modifiers' 9 | export * from 'src/config/position' 10 | export * from 'src/config/zoom' 11 | export * from 'src/controller' 12 | export * from 'src/lib/types' 13 | export * from 'src/model/graph' 14 | export * from 'src/model/link' 15 | export * from 'src/model/node' 16 | export * from 'src/model/shared' 17 | -------------------------------------------------------------------------------- /src/model/graph.ts: -------------------------------------------------------------------------------- 1 | import type { GraphLink } from 'src/model/link' 2 | import type { GraphNode } from 'src/model/node' 3 | 4 | /** 5 | * Type token for nodes. 6 | */ 7 | export type NodeTypeToken = string 8 | 9 | /** 10 | * Graph containing nodes and links. 11 | */ 12 | export interface Graph< 13 | T extends NodeTypeToken = NodeTypeToken, 14 | Node extends GraphNode = GraphNode, 15 | Link extends GraphLink = GraphLink 16 | > { 17 | /** 18 | * The nodes of the graph. 19 | */ 20 | readonly nodes: Node[] 21 | /** 22 | * The links of the graph. 23 | */ 24 | readonly links: Link[] 25 | } 26 | 27 | /** 28 | * Define a graph with type inference. 29 | * @param data - The nodes and links of the graph. If either are omitted, they default to an empty array. 30 | */ 31 | export function defineGraph< 32 | T extends NodeTypeToken = NodeTypeToken, 33 | Node extends GraphNode = GraphNode, 34 | Link extends GraphLink = GraphLink 35 | >({ nodes, links }: Partial>): Graph { 36 | return { 37 | nodes: nodes ?? ([] as Node[]), 38 | links: links ?? ([] as Link[]), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/model/link.ts: -------------------------------------------------------------------------------- 1 | import type { SimulationLinkDatum } from 'd3-force' 2 | import type { NodeTypeToken } from 'src/model/graph' 3 | import type { GraphNode } from 'src/model/node' 4 | import type { Label } from 'src/model/shared' 5 | 6 | /** 7 | * Link defining an edge from one node to another. 8 | */ 9 | export interface GraphLink< 10 | T extends NodeTypeToken = NodeTypeToken, 11 | SourceNode extends GraphNode = GraphNode, 12 | TargetNode extends GraphNode = SourceNode 13 | > extends SimulationLinkDatum { 14 | /** 15 | * The source node of the link. 16 | */ 17 | readonly source: SourceNode 18 | /** 19 | * The target node of the link 20 | */ 21 | readonly target: TargetNode 22 | /** 23 | * The color of the link. 24 | * Can be any valid CSS expression. 25 | */ 26 | readonly color: string 27 | /** 28 | * The label of the node. 29 | * Using false will disable the node's label. 30 | */ 31 | readonly label: false | Label 32 | } 33 | 34 | /** 35 | * Define a link with type inference. 36 | * @param data - The data of the link. 37 | */ 38 | export function defineLink< 39 | T extends NodeTypeToken = NodeTypeToken, 40 | SourceNode extends GraphNode = GraphNode, 41 | TargetNode extends GraphNode = SourceNode, 42 | Link extends GraphLink = GraphLink< 43 | T, 44 | SourceNode, 45 | TargetNode 46 | > 47 | >(data: Link): Link { 48 | return { 49 | ...data, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/model/node.ts: -------------------------------------------------------------------------------- 1 | import type { SimulationNodeDatum } from 'd3-force' 2 | import type { NodeTypeToken } from 'src/model/graph' 3 | import type { Label } from 'src/model/shared' 4 | 5 | /** 6 | * Node representing a datum of a graph. 7 | */ 8 | export interface GraphNode 9 | extends SimulationNodeDatum { 10 | /** 11 | * The type of the node. 12 | */ 13 | readonly type: T 14 | /** 15 | * The ID of the node. 16 | */ 17 | readonly id: string 18 | /** 19 | * The color of the node. 20 | * Can be any valid CSS expression. 21 | */ 22 | readonly color: string 23 | /** 24 | * The label of the node. 25 | * Using false will disable the node's label. 26 | */ 27 | readonly label: false | Label 28 | /** 29 | * The focus state of a node. 30 | * Warning: Used for internal logic. Should not be set manually! 31 | */ 32 | isFocused: boolean 33 | /** 34 | * The x-coordinate of a node. 35 | */ 36 | x?: number | undefined 37 | /** 38 | * The y-coordinate of a node. 39 | */ 40 | y?: number | undefined 41 | /** 42 | * The fixed x-coordinate of a node. 43 | * If set, the node will not be simulated. 44 | */ 45 | fx?: number | undefined 46 | /** 47 | * The fixed y-coordinate of a node. 48 | * If set, the node will not be simulated. 49 | */ 50 | fy?: number | undefined 51 | /** 52 | * Timestamp of the node's last interaction. 53 | * Warning: Used for internal logic. Should not be set manually! 54 | */ 55 | lastInteractionTimestamp?: number | undefined 56 | } 57 | 58 | /** 59 | * Define a node with type inference. 60 | * @param data - The data of the node. 61 | */ 62 | export function defineNode< 63 | T extends NodeTypeToken = NodeTypeToken, 64 | Node extends GraphNode = GraphNode 65 | >(data: Node): Node { 66 | return { 67 | ...data, 68 | isFocused: false, 69 | lastInteractionTimestamp: undefined, 70 | } 71 | } 72 | 73 | const nodeDefaults: Omit = { 74 | color: 'lightgray', 75 | label: { 76 | color: 'black', 77 | fontSize: '1rem', 78 | text: '', 79 | }, 80 | isFocused: false, 81 | } 82 | 83 | /** 84 | * Define a node with type inference and some default values. 85 | * @param data - The data of the node. 86 | */ 87 | export function defineNodeWithDefaults( 88 | data: Partial> & Pick 89 | ): GraphNode { 90 | return defineNode({ 91 | ...nodeDefaults, 92 | ...data, 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /src/model/shared.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Label configuration. 3 | */ 4 | export interface Label { 5 | /** 6 | * The color of the label. 7 | * Can be any valid CSS expression. 8 | */ 9 | readonly color: string 10 | /** 11 | * The font size of the label. 12 | * Can be any valid CSS expression. 13 | */ 14 | readonly fontSize: string 15 | /** 16 | * The text of the label. 17 | */ 18 | readonly text: string 19 | } 20 | -------------------------------------------------------------------------------- /test/__snapshots__/config.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`Config > can be defined > using default values 1`] = ` 4 | { 5 | "autoResize": false, 6 | "callbacks": {}, 7 | "initial": { 8 | "includeUnlinked": true, 9 | "linkFilter": [Function], 10 | "nodeTypeFilter": undefined, 11 | "showLinkLabels": true, 12 | "showNodeLabels": true, 13 | }, 14 | "marker": { 15 | "padding": [Function], 16 | "path": [ 17 | [ 18 | 0, 19 | 0, 20 | ], 21 | [ 22 | 0, 23 | 4, 24 | ], 25 | [ 26 | 4, 27 | 2, 28 | ], 29 | ], 30 | "ref": [ 31 | 2, 32 | 2, 33 | ], 34 | "size": 4, 35 | "viewBox": "0,0,4,4", 36 | }, 37 | "modifiers": {}, 38 | "nodeRadius": 16, 39 | "positionInitializer": [Function], 40 | "simulation": { 41 | "alphas": { 42 | "drag": { 43 | "end": 0, 44 | "start": 0.1, 45 | }, 46 | "filter": { 47 | "link": 1, 48 | "type": 0.1, 49 | "unlinked": { 50 | "exclude": 0.1, 51 | "include": 0.1, 52 | }, 53 | }, 54 | "focus": { 55 | "acquire": [Function], 56 | "release": [Function], 57 | }, 58 | "initialize": 1, 59 | "labels": { 60 | "links": { 61 | "hide": 0, 62 | "show": 0, 63 | }, 64 | "nodes": { 65 | "hide": 0, 66 | "show": 0, 67 | }, 68 | }, 69 | "resize": 0.5, 70 | }, 71 | "forces": { 72 | "centering": { 73 | "enabled": true, 74 | "strength": 0.1, 75 | }, 76 | "charge": { 77 | "enabled": true, 78 | "strength": -1, 79 | }, 80 | "collision": { 81 | "enabled": true, 82 | "radiusMultiplier": 2, 83 | "strength": 1, 84 | }, 85 | "link": { 86 | "enabled": true, 87 | "length": 128, 88 | "strength": 1, 89 | }, 90 | }, 91 | }, 92 | "zoom": { 93 | "initial": 1, 94 | "max": 2, 95 | "min": 0.1, 96 | }, 97 | } 98 | `; 99 | -------------------------------------------------------------------------------- /test/__snapshots__/controller.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`GraphController > matches the snapshot 1`] = ` 4 |
7 | 12 | 15 | 18 | 19 | 23 | 28 | aToB 29 | 30 | 31 | 32 | 36 | 41 | bToA 42 | 43 | 44 | 45 | 49 | 54 | bToC 55 | 56 | 57 | 58 | 62 | 66 | 67 | 68 | 71 | 72 | 78 | 84 | 85 | 86 | 92 | 98 | 99 | 100 | 106 | 112 | 113 | 114 | 120 | 126 | 127 | 128 | 129 | 140 | 143 | 144 | 145 | 146 | 147 |
148 | `; 149 | -------------------------------------------------------------------------------- /test/config.test.ts: -------------------------------------------------------------------------------- 1 | import { defineGraphConfig } from 'src/config/config' 2 | import { describe, expect, it } from 'vitest' 3 | 4 | describe.concurrent('Config', () => { 5 | describe('can be defined', () => { 6 | it('using default values', () => { 7 | const config = defineGraphConfig() 8 | expect(config).toMatchSnapshot() 9 | }) 10 | 11 | it('with deep merging', () => { 12 | const defaultConfig = defineGraphConfig() 13 | const customConfig = defineGraphConfig({ 14 | simulation: { 15 | forces: { 16 | collision: { 17 | radiusMultiplier: 42, 18 | }, 19 | }, 20 | }, 21 | }) 22 | const customCollisionForce = customConfig.simulation.forces.collision 23 | expect(customCollisionForce).not.toBe(false) 24 | // @ts-expect-error It has been asserted that the force is not false 25 | expect(customCollisionForce.radiusMultiplier).toEqual(42) 26 | expect(customConfig).not.toEqual(defaultConfig) 27 | 28 | const customMerge = { 29 | ...defaultConfig, 30 | simulation: { 31 | ...defaultConfig.simulation, 32 | forces: { 33 | ...defaultConfig.simulation.forces, 34 | collision: { 35 | ...defaultConfig.simulation.forces.collision, 36 | radiusMultiplier: 42, 37 | }, 38 | }, 39 | }, 40 | } 41 | expect(customMerge.simulation.forces).toStrictEqual( 42 | customConfig.simulation.forces 43 | ) 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /test/controller.test.ts: -------------------------------------------------------------------------------- 1 | import type { GraphLink } from 'src/main' 2 | import { GraphController, defineGraphConfig } from 'src/main' 3 | import type { TestNodeType } from 'test/test-data' 4 | import TestData from 'test/test-data' 5 | import { afterEach, beforeEach, describe, expect, it } from 'vitest' 6 | 7 | describe('GraphController', () => { 8 | let container: HTMLDivElement 9 | let controller: GraphController 10 | 11 | beforeEach(() => { 12 | container = document.createElement('div') 13 | controller = new GraphController(container, TestData.graph, TestData.config) 14 | }) 15 | 16 | afterEach(() => controller.shutdown()) 17 | 18 | it('matches the snapshot', () => { 19 | expect(container).toMatchSnapshot() 20 | }) 21 | 22 | it('renders nodes', () => { 23 | expect(container.querySelectorAll('.node').length).toEqual( 24 | TestData.graph.nodes.length 25 | ) 26 | }) 27 | 28 | it('renders links', () => { 29 | expect(container.querySelectorAll('.link').length).toEqual( 30 | TestData.graph.links.length 31 | ) 32 | }) 33 | 34 | describe('can be configured', () => { 35 | describe('with initial settings', () => { 36 | it('that set the node type filter', () => { 37 | controller = new GraphController( 38 | container, 39 | TestData.graph, 40 | defineGraphConfig({ initial: { nodeTypeFilter: [] } }) 41 | ) 42 | 43 | expect(controller.nodeTypeFilter).toEqual([]) 44 | expect(container.querySelectorAll('.node').length).toEqual(0) 45 | expect(container.querySelectorAll('.link').length).toEqual(0) 46 | }) 47 | 48 | it('that exclude unlinked nodes', () => { 49 | controller = new GraphController( 50 | container, 51 | TestData.graph, 52 | defineGraphConfig({ 53 | initial: { includeUnlinked: false }, 54 | }) 55 | ) 56 | 57 | expect(controller.includeUnlinked).toEqual(false) 58 | expect(container.querySelectorAll('.node').length).toEqual(3) 59 | }) 60 | 61 | it('that filter links', () => { 62 | controller = new GraphController( 63 | container, 64 | TestData.graph, 65 | defineGraphConfig({ 66 | initial: { 67 | linkFilter: (link: GraphLink) => 68 | link.source.id === link.target.id, 69 | }, 70 | }) 71 | ) 72 | 73 | expect(container.querySelectorAll('.link').length).toEqual(1) 74 | }) 75 | }) 76 | }) 77 | 78 | describe('has settings that', () => { 79 | it('can exclude unlinked nodes', () => { 80 | expect(container.querySelectorAll('.node').length).toEqual( 81 | TestData.graph.nodes.length 82 | ) 83 | 84 | controller.includeUnlinked = false 85 | 86 | expect(container.querySelectorAll('.node').length).toEqual(3) 87 | }) 88 | 89 | it('can filter links', () => { 90 | expect(container.querySelectorAll('.link').length).toEqual( 91 | TestData.graph.links.length 92 | ) 93 | 94 | controller.linkFilter = (link: GraphLink) => 95 | link.source.id === link.target.id 96 | 97 | expect(container.querySelectorAll('.link').length).toEqual(1) 98 | }) 99 | 100 | it('can filter by node type', () => { 101 | const currentlyExcluded: TestNodeType[] = [] 102 | 103 | const checkIncludedNodes = () => { 104 | expect(container.querySelectorAll('.node').length).toEqual( 105 | TestData.graph.nodes.filter( 106 | (node) => !currentlyExcluded.includes(node.type) 107 | ).length 108 | ) 109 | } 110 | 111 | checkIncludedNodes() 112 | 113 | controller.filterNodesByType(false, 'second') 114 | currentlyExcluded.push('second') 115 | checkIncludedNodes() 116 | 117 | controller.filterNodesByType(false, 'first') 118 | currentlyExcluded.push('first') 119 | checkIncludedNodes() 120 | 121 | controller.filterNodesByType(true, 'first') 122 | currentlyExcluded.pop() 123 | checkIncludedNodes() 124 | 125 | controller.filterNodesByType(true, 'second') 126 | currentlyExcluded.pop() 127 | checkIncludedNodes() 128 | }) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /test/lib/filter.test.ts: -------------------------------------------------------------------------------- 1 | import { filterGraph } from 'src/lib/filter' 2 | import type { GraphLink } from 'src/model/link' 3 | import type { TestNodeType } from 'test/test-data' 4 | import TestData from 'test/test-data' 5 | import { describe, expect, it } from 'vitest' 6 | 7 | describe.concurrent('filter', () => { 8 | it('can filter nothing', () => { 9 | const filteredResult = filterGraph({ 10 | filter: ['first', 'second'], 11 | focusedNode: undefined, 12 | includeUnlinked: true, 13 | linkFilter: () => true, 14 | graph: TestData.graph, 15 | }) 16 | expect(filteredResult).toEqual(TestData.graph) 17 | }) 18 | 19 | it('can filter by type', () => { 20 | const filteredResult = filterGraph({ 21 | filter: ['first'], 22 | focusedNode: undefined, 23 | includeUnlinked: false, 24 | linkFilter: () => true, 25 | graph: TestData.graph, 26 | }) 27 | expect(filteredResult.nodes).toEqual( 28 | TestData.graph.nodes.filter((node) => node.type === 'first') 29 | ) 30 | }) 31 | 32 | it('can filter unlinked', () => { 33 | const filteredResult = filterGraph({ 34 | filter: ['first', 'second'], 35 | focusedNode: undefined, 36 | includeUnlinked: false, 37 | linkFilter: () => true, 38 | graph: TestData.graph, 39 | }) 40 | expect(filteredResult.nodes).toEqual( 41 | TestData.graph.nodes.filter((node) => 42 | TestData.graph.links.some( 43 | (link) => link.source.id === node.id || link.target.id === node.id 44 | ) 45 | ) 46 | ) 47 | expect(filteredResult.links).toEqual(TestData.graph.links) 48 | }) 49 | 50 | it('can filter links', () => { 51 | const filteredResult = filterGraph({ 52 | filter: ['first', 'second'], 53 | focusedNode: undefined, 54 | includeUnlinked: true, 55 | linkFilter: (link: GraphLink) => 56 | link.source.id === link.target.id, 57 | graph: TestData.graph, 58 | }) 59 | expect(filteredResult.links.length).toEqual(1) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /test/test-data.ts: -------------------------------------------------------------------------------- 1 | import { defineGraphConfig } from 'src/config/config' 2 | import type { Graph } from 'src/model/graph' 3 | import { defineGraph } from 'src/model/graph' 4 | import { defineLink } from 'src/model/link' 5 | import { defineNodeWithDefaults } from 'src/model/node' 6 | 7 | export type TestNodeType = 'first' | 'second' 8 | 9 | const a = defineNodeWithDefaults({ 10 | type: 'first', 11 | id: 'a', 12 | }) 13 | 14 | const b = defineNodeWithDefaults({ 15 | type: 'first', 16 | id: 'b', 17 | }) 18 | 19 | const c = defineNodeWithDefaults({ 20 | type: 'first', 21 | id: 'c', 22 | label: false, 23 | }) 24 | 25 | const d = defineNodeWithDefaults({ 26 | type: 'second', 27 | id: 'd', 28 | }) 29 | 30 | const aToB = defineLink({ 31 | source: a, 32 | target: b, 33 | color: 'gray', 34 | label: { 35 | color: 'black', 36 | fontSize: '1rem', 37 | text: 'aToB', 38 | }, 39 | }) 40 | 41 | const bToA = defineLink({ 42 | source: b, 43 | target: a, 44 | color: 'gray', 45 | label: { 46 | color: 'black', 47 | fontSize: '1rem', 48 | text: 'bToA', 49 | }, 50 | }) 51 | 52 | const bToC = defineLink({ 53 | source: b, 54 | target: c, 55 | color: 'gray', 56 | label: { 57 | color: 'black', 58 | fontSize: '1rem', 59 | text: 'bToC', 60 | }, 61 | }) 62 | 63 | const cToC = defineLink({ 64 | source: c, 65 | target: c, 66 | color: 'gray', 67 | label: false, 68 | }) 69 | 70 | const graph: Graph = defineGraph({ 71 | nodes: [a, b, c, d], 72 | links: [aToB, bToA, bToC, cToC], 73 | }) 74 | 75 | const config = defineGraphConfig() 76 | 77 | export default { 78 | graph, 79 | config, 80 | } 81 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "lib": ["ESNext", "DOM"], 8 | "moduleResolution": "Node", 9 | "strict": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "exactOptionalPropertyTypes": true, 17 | "skipLibCheck": true, 18 | "paths": { 19 | "src/*": ["../src/*"], 20 | "test/*": ["./*"] 21 | } 22 | }, 23 | "include": ["./**/*.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "lib": ["ESNext", "DOM"], 8 | "moduleResolution": "Node", 9 | "strict": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "exactOptionalPropertyTypes": true, 17 | "paths": { 18 | "src/*": ["src/*"], 19 | "test/*": ["test/*"] 20 | } 21 | }, 22 | "include": ["src"] 23 | } 24 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as path from 'path' 3 | 4 | import { defineConfig } from 'vite' 5 | import dts from 'vite-plugin-dts' 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | dts({ 10 | include: 'src/**', 11 | outputDir: 'dist/types', 12 | staticImport: true, 13 | }), 14 | ], 15 | build: { 16 | lib: { 17 | entry: path.resolve(__dirname, 'src/main.ts'), 18 | formats: ['es', 'umd'], 19 | name: 'd3-graph-controller', 20 | fileName: (format) => `d3-graph-controller.${format}.js`, 21 | }, 22 | rollupOptions: { 23 | external: ['d3-drag', 'd3-force', 'd3-selection', 'd3-zoom', 'vecti'], 24 | output: { 25 | globals: { 26 | 'd3-drag': 'd3', 27 | 'd3-force': 'd3', 28 | 'd3-selection': 'd3', 29 | 'd3-zoom': 'd3', 30 | vecti: 'vecti', 31 | }, 32 | }, 33 | }, 34 | }, 35 | resolve: { 36 | alias: [ 37 | { 38 | find: 'src', 39 | replacement: path.resolve(__dirname, 'src'), 40 | }, 41 | { 42 | find: 'test', 43 | replacement: path.resolve(__dirname, 'test'), 44 | }, 45 | ], 46 | }, 47 | test: { 48 | include: ['test/**/*.test.ts'], 49 | environment: 'jsdom', 50 | coverage: { 51 | all: true, 52 | include: ['src/**/*.ts'], 53 | }, 54 | }, 55 | }) 56 | --------------------------------------------------------------------------------