From b7be629a61b7a2c0f3445734f50d29cf1be77af3 Mon Sep 17 00:00:00 2001 From: Austin Dang <122927862+austindang67@users.noreply.github.com> Date: Tue, 9 Sep 2025 22:02:49 -1000 Subject: [PATCH 01/29] 1 Initialize vite-react typescript project Created the vite-react typescript project --- client/.gitignore | 24 + client/README.md | 69 ++ client/eslint.config.js | 23 + client/index.html | 13 + client/package.json | 29 + client/pnpm-lock.yaml | 2082 +++++++++++++++++++++++++++++++++++ client/public/vite.svg | 1 + client/src/App.css | 42 + client/src/App.tsx | 35 + client/src/assets/react.svg | 1 + client/src/index.css | 68 ++ client/src/main.tsx | 10 + client/src/vite-env.d.ts | 1 + client/tsconfig.app.json | 27 + client/tsconfig.json | 7 + client/tsconfig.node.json | 25 + client/vite.config.ts | 7 + 17 files changed, 2464 insertions(+) create mode 100644 client/.gitignore create mode 100644 client/README.md create mode 100644 client/eslint.config.js create mode 100644 client/index.html create mode 100644 client/package.json create mode 100644 client/pnpm-lock.yaml create mode 100644 client/public/vite.svg create mode 100644 client/src/App.css create mode 100644 client/src/App.tsx create mode 100644 client/src/assets/react.svg create mode 100644 client/src/index.css create mode 100644 client/src/main.tsx create mode 100644 client/src/vite-env.d.ts create mode 100644 client/tsconfig.app.json create mode 100644 client/tsconfig.json create mode 100644 client/tsconfig.node.json create mode 100644 client/vite.config.ts diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..7959ce4 --- /dev/null +++ b/client/README.md @@ -0,0 +1,69 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/client/eslint.config.js b/client/eslint.config.js new file mode 100644 index 0000000..d94e7de --- /dev/null +++ b/client/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { globalIgnores } from 'eslint/config' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/client/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..d540c6a --- /dev/null +++ b/client/package.json @@ -0,0 +1,29 @@ +{ + "name": "client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.0", + "eslint": "^9.33.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "typescript": "~5.8.3", + "typescript-eslint": "^8.39.1", + "vite": "^7.1.2" + } +} diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml new file mode 100644 index 0000000..13e65e4 --- /dev/null +++ b/client/pnpm-lock.yaml @@ -0,0 +1,2082 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + react: + specifier: ^19.1.1 + version: 19.1.1 + react-dom: + specifier: ^19.1.1 + version: 19.1.1(react@19.1.1) + devDependencies: + '@eslint/js': + specifier: ^9.33.0 + version: 9.35.0 + '@types/react': + specifier: ^19.1.10 + version: 19.1.12 + '@types/react-dom': + specifier: ^19.1.7 + version: 19.1.9(@types/react@19.1.12) + '@vitejs/plugin-react': + specifier: ^5.0.0 + version: 5.0.2(vite@7.1.5) + eslint: + specifier: ^9.33.0 + version: 9.35.0 + eslint-plugin-react-hooks: + specifier: ^5.2.0 + version: 5.2.0(eslint@9.35.0) + eslint-plugin-react-refresh: + specifier: ^0.4.20 + version: 0.4.20(eslint@9.35.0) + globals: + specifier: ^16.3.0 + version: 16.4.0 + typescript: + specifier: ~5.8.3 + version: 5.8.3 + typescript-eslint: + specifier: ^8.39.1 + version: 8.43.0(eslint@9.35.0)(typescript@5.8.3) + vite: + specifier: ^7.1.2 + version: 7.1.5 + +packages: + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.35.0': + resolution: {integrity: sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@rolldown/pluginutils@1.0.0-beta.34': + resolution: {integrity: sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA==} + + '@rollup/rollup-android-arm-eabi@4.50.1': + resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.50.1': + resolution: {integrity: sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.50.1': + resolution: {integrity: sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.50.1': + resolution: {integrity: sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.50.1': + resolution: {integrity: sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.50.1': + resolution: {integrity: sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': + resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.50.1': + resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.50.1': + resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.50.1': + resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': + resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.50.1': + resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.50.1': + resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.50.1': + resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.50.1': + resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.50.1': + resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.50.1': + resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.50.1': + resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.50.1': + resolution: {integrity: sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.50.1': + resolution: {integrity: sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.50.1': + resolution: {integrity: sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==} + cpu: [x64] + os: [win32] + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/react-dom@19.1.9': + resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.12': + resolution: {integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==} + + '@typescript-eslint/eslint-plugin@8.43.0': + resolution: {integrity: sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.43.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.43.0': + resolution: {integrity: sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.43.0': + resolution: {integrity: sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.43.0': + resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.43.0': + resolution: {integrity: sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.43.0': + resolution: {integrity: sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.43.0': + resolution: {integrity: sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.43.0': + resolution: {integrity: sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.43.0': + resolution: {integrity: sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.43.0': + resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@5.0.2': + resolution: {integrity: sha512-tmyFgixPZCx2+e6VO9TNITWcCQl8+Nl/E8YbAyPVv85QCc7/A3JrdfG2A8gIzvVhWuzMOVrFW1aReaNxrI6tbw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.25.4: + resolution: {integrity: sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001741: + resolution: {integrity: sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + electron-to-chromium@1.5.215: + resolution: {integrity: sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==} + + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.20: + resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==} + peerDependencies: + eslint: '>=8.40' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.35.0: + resolution: {integrity: sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.20: + resolution: {integrity: sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.1.1: + resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} + peerDependencies: + react: ^19.1.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@19.1.1: + resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.50.1: + resolution: {integrity: sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.43.0: + resolution: {integrity: sha512-FyRGJKUGvcFekRRcBKFBlAhnp4Ng8rhe8tuvvkR9OiU0gfd4vyvTRQHEckO6VDlH57jbeUQem2IpqPq9kLJH+w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@7.1.5: + resolution: {integrity: sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@esbuild/aix-ppc64@0.25.9': + optional: true + + '@esbuild/android-arm64@0.25.9': + optional: true + + '@esbuild/android-arm@0.25.9': + optional: true + + '@esbuild/android-x64@0.25.9': + optional: true + + '@esbuild/darwin-arm64@0.25.9': + optional: true + + '@esbuild/darwin-x64@0.25.9': + optional: true + + '@esbuild/freebsd-arm64@0.25.9': + optional: true + + '@esbuild/freebsd-x64@0.25.9': + optional: true + + '@esbuild/linux-arm64@0.25.9': + optional: true + + '@esbuild/linux-arm@0.25.9': + optional: true + + '@esbuild/linux-ia32@0.25.9': + optional: true + + '@esbuild/linux-loong64@0.25.9': + optional: true + + '@esbuild/linux-mips64el@0.25.9': + optional: true + + '@esbuild/linux-ppc64@0.25.9': + optional: true + + '@esbuild/linux-riscv64@0.25.9': + optional: true + + '@esbuild/linux-s390x@0.25.9': + optional: true + + '@esbuild/linux-x64@0.25.9': + optional: true + + '@esbuild/netbsd-arm64@0.25.9': + optional: true + + '@esbuild/netbsd-x64@0.25.9': + optional: true + + '@esbuild/openbsd-arm64@0.25.9': + optional: true + + '@esbuild/openbsd-x64@0.25.9': + optional: true + + '@esbuild/openharmony-arm64@0.25.9': + optional: true + + '@esbuild/sunos-x64@0.25.9': + optional: true + + '@esbuild/win32-arm64@0.25.9': + optional: true + + '@esbuild/win32-ia32@0.25.9': + optional: true + + '@esbuild/win32-x64@0.25.9': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.35.0)': + dependencies: + eslint: 9.35.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.35.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.30': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@rolldown/pluginutils@1.0.0-beta.34': {} + + '@rollup/rollup-android-arm-eabi@4.50.1': + optional: true + + '@rollup/rollup-android-arm64@4.50.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.50.1': + optional: true + + '@rollup/rollup-darwin-x64@4.50.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.50.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.50.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.50.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.50.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.50.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.50.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.50.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.50.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.50.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.50.1': + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/react-dom@19.1.9(@types/react@19.1.12)': + dependencies: + '@types/react': 19.1.12 + + '@types/react@19.1.12': + dependencies: + csstype: 3.1.3 + + '@typescript-eslint/eslint-plugin@8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.8.3))(eslint@9.35.0)(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.43.0(eslint@9.35.0)(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.43.0 + '@typescript-eslint/type-utils': 8.43.0(eslint@9.35.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.43.0 + eslint: 9.35.0 + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.43.0 + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.43.0 + debug: 4.4.1 + eslint: 9.35.0 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.43.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.8.3) + '@typescript-eslint/types': 8.43.0 + debug: 4.4.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.43.0': + dependencies: + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/visitor-keys': 8.43.0 + + '@typescript-eslint/tsconfig-utils@8.43.0(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + + '@typescript-eslint/type-utils@8.43.0(eslint@9.35.0)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0)(typescript@5.8.3) + debug: 4.4.1 + eslint: 9.35.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.43.0': {} + + '@typescript-eslint/typescript-estree@8.43.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/project-service': 8.43.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.8.3) + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/visitor-keys': 8.43.0 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.43.0(eslint@9.35.0)(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0) + '@typescript-eslint/scope-manager': 8.43.0 + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) + eslint: 9.35.0 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.43.0': + dependencies: + '@typescript-eslint/types': 8.43.0 + eslint-visitor-keys: 4.2.1 + + '@vitejs/plugin-react@5.0.2(vite@7.1.5)': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) + '@rolldown/pluginutils': 1.0.0-beta.34 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.1.5 + transitivePeerDependencies: + - supports-color + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.25.4: + dependencies: + caniuse-lite: 1.0.30001741 + electron-to-chromium: 1.5.215 + node-releases: 2.0.20 + update-browserslist-db: 1.1.3(browserslist@4.25.4) + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001741: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.1.3: {} + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + electron-to-chromium@1.5.215: {} + + esbuild@0.25.9: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@5.2.0(eslint@9.35.0): + dependencies: + eslint: 9.35.0 + + eslint-plugin-react-refresh@0.4.20(eslint@9.35.0): + dependencies: + eslint: 9.35.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.35.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.35.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.4.0: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.20: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.1.1(react@19.1.1): + dependencies: + react: 19.1.1 + scheduler: 0.26.0 + + react-refresh@0.17.0: {} + + react@19.1.1: {} + + resolve-from@4.0.0: {} + + reusify@1.1.0: {} + + rollup@4.50.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.50.1 + '@rollup/rollup-android-arm64': 4.50.1 + '@rollup/rollup-darwin-arm64': 4.50.1 + '@rollup/rollup-darwin-x64': 4.50.1 + '@rollup/rollup-freebsd-arm64': 4.50.1 + '@rollup/rollup-freebsd-x64': 4.50.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.50.1 + '@rollup/rollup-linux-arm-musleabihf': 4.50.1 + '@rollup/rollup-linux-arm64-gnu': 4.50.1 + '@rollup/rollup-linux-arm64-musl': 4.50.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.50.1 + '@rollup/rollup-linux-ppc64-gnu': 4.50.1 + '@rollup/rollup-linux-riscv64-gnu': 4.50.1 + '@rollup/rollup-linux-riscv64-musl': 4.50.1 + '@rollup/rollup-linux-s390x-gnu': 4.50.1 + '@rollup/rollup-linux-x64-gnu': 4.50.1 + '@rollup/rollup-linux-x64-musl': 4.50.1 + '@rollup/rollup-openharmony-arm64': 4.50.1 + '@rollup/rollup-win32-arm64-msvc': 4.50.1 + '@rollup/rollup-win32-ia32-msvc': 4.50.1 + '@rollup/rollup-win32-x64-msvc': 4.50.1 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + scheduler@0.26.0: {} + + semver@6.3.1: {} + + semver@7.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.43.0(eslint@9.35.0)(typescript@5.8.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.8.3))(eslint@9.35.0)(typescript@5.8.3) + '@typescript-eslint/parser': 8.43.0(eslint@9.35.0)(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0)(typescript@5.8.3) + eslint: 9.35.0 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + typescript@5.8.3: {} + + update-browserslist-db@1.1.3(browserslist@4.25.4): + dependencies: + browserslist: 4.25.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@7.1.5: + dependencies: + esbuild: 0.25.9 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.50.1 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} diff --git a/client/public/vite.svg b/client/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/App.css b/client/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/client/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..3d7ded3 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +
+ + Vite logo + + + React logo + +
+

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ) +} + +export default App diff --git a/client/src/assets/react.svg b/client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/client/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/client/src/main.tsx b/client/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/client/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/client/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json new file mode 100644 index 0000000..227a6c6 --- /dev/null +++ b/client/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json new file mode 100644 index 0000000..f85a399 --- /dev/null +++ b/client/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/client/vite.config.ts b/client/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/client/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From a58016f384ed83089d3af5b0a0b4a612fd8f4686 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:06:26 -1000 Subject: [PATCH 02/29] Installed d3.js --- .idea/.gitignore | 8 + .idea/kaiaulu_react.iml | 9 ++ .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + client/package.json | 1 + client/pnpm-lock.yaml | 324 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 362 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/kaiaulu_react.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/kaiaulu_react.iml b/.idea/kaiaulu_react.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/kaiaulu_react.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..639900d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..cdedb33 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/client/package.json b/client/package.json index d540c6a..795cd07 100644 --- a/client/package.json +++ b/client/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "d3": "^7.9.0", "react": "^19.1.1", "react-dom": "^19.1.1" }, diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 13e65e4..65bf71d 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + d3: + specifier: ^7.9.0 + version: 7.9.0 react: specifier: ^19.1.1 version: 19.1.1 @@ -627,6 +630,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -640,6 +647,133 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -652,6 +786,9 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + electron-to-chromium@1.5.215: resolution: {integrity: sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==} @@ -797,6 +934,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -813,6 +954,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -971,6 +1116,9 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + rollup@4.50.1: resolution: {integrity: sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -979,6 +1127,12 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -1643,6 +1797,8 @@ snapshots: color-name@1.1.4: {} + commander@7.2.0: {} + concat-map@0.0.1: {} convert-source-map@2.0.0: {} @@ -1655,12 +1811,168 @@ snapshots: csstype@3.1.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.0: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + debug@4.4.1: dependencies: ms: 2.1.3 deep-is@0.1.4: {} + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + electron-to-chromium@1.5.215: {} esbuild@0.25.9: @@ -1834,6 +2146,10 @@ snapshots: has-flag@4.0.0: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -1845,6 +2161,8 @@ snapshots: imurmurhash@0.1.4: {} + internmap@2.0.3: {} + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -1969,6 +2287,8 @@ snapshots: reusify@1.1.0: {} + robust-predicates@3.0.2: {} + rollup@4.50.1: dependencies: '@types/estree': 1.0.8 @@ -2000,6 +2320,10 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rw@1.3.3: {} + + safer-buffer@2.1.2: {} + scheduler@0.26.0: {} semver@6.3.1: {} From 59b3b8aa9e76bef68ebd0081dc396d7dd5d7f5d5 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:43:55 -1000 Subject: [PATCH 03/29] Established page with working network graph. --- client/package.json | 1 + client/pnpm-lock.yaml | 218 ++++++++++++++++++ client/public/sample-data.json | 93 ++++++++ client/src/App.css | 40 ---- client/src/App.tsx | 26 +-- .../components/NetworkGraph/NetworkGraph.css | 3 + .../components/NetworkGraph/NetworkGraph.tsx | 63 +++++ client/src/components/NetworkGraph/index.ts | 1 + client/src/hooks/useDummyData.ts | 25 ++ client/src/index.css | 65 ------ 10 files changed, 406 insertions(+), 129 deletions(-) create mode 100644 client/public/sample-data.json create mode 100644 client/src/components/NetworkGraph/NetworkGraph.css create mode 100644 client/src/components/NetworkGraph/NetworkGraph.tsx create mode 100644 client/src/components/NetworkGraph/index.ts create mode 100644 client/src/hooks/useDummyData.ts diff --git a/client/package.json b/client/package.json index 795cd07..a116546 100644 --- a/client/package.json +++ b/client/package.json @@ -16,6 +16,7 @@ }, "devDependencies": { "@eslint/js": "^9.33.0", + "@types/d3": "^7.4.3", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.0", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 65bf71d..6265c60 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@eslint/js': specifier: ^9.33.0 version: 9.35.0 + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 '@types/react': specifier: ^19.1.10 version: 19.1.12 @@ -495,9 +498,105 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1624,8 +1723,127 @@ snapshots: dependencies: '@babel/types': 7.28.4 + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + '@types/estree@1.0.8': {} + '@types/geojson@7946.0.16': {} + '@types/json-schema@7.0.15': {} '@types/react-dom@19.1.9(@types/react@19.1.12)': diff --git a/client/public/sample-data.json b/client/public/sample-data.json new file mode 100644 index 0000000..5340d4a --- /dev/null +++ b/client/public/sample-data.json @@ -0,0 +1,93 @@ +{ + "nodes": [ + { + "id": 1, + "name": "A" + }, + { + "id": 2, + "name": "B" + }, + { + "id": 3, + "name": "C" + }, + { + "id": 4, + "name": "D" + }, + { + "id": 5, + "name": "E" + }, + { + "id": 6, + "name": "F" + }, + { + "id": 7, + "name": "G" + }, + { + "id": 8, + "name": "H" + }, + { + "id": 9, + "name": "I" + }, + { + "id": 10, + "name": "J" + } + ], + "links": [ + + { + "source": 1, + "target": 2 + }, + { + "source": 1, + "target": 5 + }, + { + "source": 1, + "target": 6 + }, + + { + "source": 2, + "target": 3 + }, + { + "source": 2, + "target": 7 + } + , + + { + "source": 3, + "target": 4 + }, + { + "source": 8, + "target": 3 + } + , + { + "source": 4, + "target": 5 + } + , + + { + "source": 4, + "target": 9 + }, + { + "source": 5, + "target": 10 + } + ] +} \ No newline at end of file diff --git a/client/src/App.css b/client/src/App.css index b9d355d..6ffc97a 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1,42 +1,2 @@ #root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; } diff --git a/client/src/App.tsx b/client/src/App.tsx index 3d7ded3..5ef47eb 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,33 +1,11 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' +import { NetworkGraph } from "./components/NetworkGraph"; import './App.css' function App() { - const [count, setCount] = useState(0) return ( <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

+ ) } diff --git a/client/src/components/NetworkGraph/NetworkGraph.css b/client/src/components/NetworkGraph/NetworkGraph.css new file mode 100644 index 0000000..9edf5ad --- /dev/null +++ b/client/src/components/NetworkGraph/NetworkGraph.css @@ -0,0 +1,3 @@ +.networkGraph { + +} \ No newline at end of file diff --git a/client/src/components/NetworkGraph/NetworkGraph.tsx b/client/src/components/NetworkGraph/NetworkGraph.tsx new file mode 100644 index 0000000..36d1217 --- /dev/null +++ b/client/src/components/NetworkGraph/NetworkGraph.tsx @@ -0,0 +1,63 @@ +import {useEffect, useRef } from 'react'; +import * as d3 from "d3"; +import { useDummyData } from "../../hooks/useDummyData.ts"; +import './NetworkGraph.css'; + +export const NetworkGraph = () => { + const graphContainer = useRef(null); + const { data } = useDummyData(); + + useEffect(() => { + if (data != null) { + if (graphContainer.current) { + const svg = d3.select(graphContainer.current) + .append("svg") + .attr("width", 1500) + .attr("height", 1500) + .append("g") + .attr("transform", + "translate(" + 100 + "," + 100 + ")"); + + + var link = svg.selectAll("line") + .data(data.links) + .enter() + .append("line") + .style("stroke", "#aaa"); + + var node = svg + .selectAll("circle") + .data(data.nodes) + .enter() + .append("circle") + .attr("r", 20) + .style("fill", "#69b3a2"); + + var simulation = d3.forceSimulation(data.nodes).force("link", d3.forceLink() + .id(function(d) {return d.id; }).links(data.links)) + .force("charge", d3.forceManyBody().strength(-400)) + .force("center", d3.forceCenter(750, 750)) + .on("end", ticked); + + function ticked() { + link + .attr("x1", function(d) { return d.source.x; }) + .attr("y1", function(d) { return d.source.y; }) + .attr("x2", function(d) { return d.target.x; }) + .attr("y2", function(d) { return d.target.y; }); + + node + .attr("cx", function (d) { return d.x+6; }) + .attr("cy", function(d) { return d.y-6; }); + } + } + } + }, [data]); + + return ( +
+ +
+ ) + +} \ No newline at end of file diff --git a/client/src/components/NetworkGraph/index.ts b/client/src/components/NetworkGraph/index.ts new file mode 100644 index 0000000..d761ad5 --- /dev/null +++ b/client/src/components/NetworkGraph/index.ts @@ -0,0 +1 @@ +export * from './NetworkGraph'; \ No newline at end of file diff --git a/client/src/hooks/useDummyData.ts b/client/src/hooks/useDummyData.ts new file mode 100644 index 0000000..0d22a3f --- /dev/null +++ b/client/src/hooks/useDummyData.ts @@ -0,0 +1,25 @@ +import * as d3 from "d3"; +import { useEffect, useState } from "react"; + +export function useDummyData() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let ignore = false; + (async () => { + try { + const json = await d3.json("/sample-data.json"); + if (!ignore) setData(json ?? null); + } catch (e: any) { + if (!ignore) setError(e?.message ?? "Failed to load data"); + } finally { + if (!ignore) setLoading(false); + } + })(); + return () => { ignore = true; }; + }, []); + + return { data, loading, error }; +} diff --git a/client/src/index.css b/client/src/index.css index 08a3ac9..5b81229 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,68 +1,3 @@ :root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } } From cb045602a818a3dbd6ccb1c2c3472049494af7db Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:57:41 -1000 Subject: [PATCH 04/29] Slight tweaks for presentation purposes --- client/src/components/NetworkGraph/NetworkGraph.css | 6 ++++-- client/src/components/NetworkGraph/NetworkGraph.tsx | 6 +----- client/src/index.css | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/client/src/components/NetworkGraph/NetworkGraph.css b/client/src/components/NetworkGraph/NetworkGraph.css index 9edf5ad..3c8ef93 100644 --- a/client/src/components/NetworkGraph/NetworkGraph.css +++ b/client/src/components/NetworkGraph/NetworkGraph.css @@ -1,3 +1,5 @@ -.networkGraph { - +#networkGraph { + display: flex; + max-height: 750px; + max-width: 1500px; } \ No newline at end of file diff --git a/client/src/components/NetworkGraph/NetworkGraph.tsx b/client/src/components/NetworkGraph/NetworkGraph.tsx index 36d1217..e8fd556 100644 --- a/client/src/components/NetworkGraph/NetworkGraph.tsx +++ b/client/src/components/NetworkGraph/NetworkGraph.tsx @@ -13,10 +13,7 @@ export const NetworkGraph = () => { const svg = d3.select(graphContainer.current) .append("svg") .attr("width", 1500) - .attr("height", 1500) - .append("g") - .attr("transform", - "translate(" + 100 + "," + 100 + ")"); + .attr("height",1500); var link = svg.selectAll("line") @@ -56,7 +53,6 @@ export const NetworkGraph = () => { return (
-
) diff --git a/client/src/index.css b/client/src/index.css index 5b81229..618efd6 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,3 +1,3 @@ :root { - display: flex; + background-color: #242424; } From 74aa340e9e53cf0d94b998cd15f040900ec8c281 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Fri, 3 Oct 2025 00:45:06 -1000 Subject: [PATCH 05/29] Added more networks. Improved project structure. Created app router. --- client/package.json | 3 +- client/pnpm-lock.yaml | 45 ++++++++ client/src/App.css | 8 +- client/src/App.tsx | 14 ++- client/src/components/Header/Header.css | 7 ++ client/src/components/Header/Header.tsx | 11 ++ client/src/components/Header/index.ts | 1 + .../components/NetworkGraph/NetworkGraph.css | 10 +- .../components/NetworkGraph/NetworkGraph.tsx | 108 ++++++++++-------- client/src/components/NetworkGraph/types.ts | 5 + client/src/data/dummy-data.ts | 42 +++++++ client/src/index.css | 6 +- client/src/layout/AppLayout.css | 9 ++ client/src/layout/AppLayout.tsx | 14 +++ client/src/main.tsx | 12 +- client/src/pages/Landing.tsx | 12 ++ client/src/types/network-graph.types.ts | 14 +++ 17 files changed, 257 insertions(+), 64 deletions(-) create mode 100644 client/src/components/Header/Header.css create mode 100644 client/src/components/Header/Header.tsx create mode 100644 client/src/components/Header/index.ts create mode 100644 client/src/components/NetworkGraph/types.ts create mode 100644 client/src/data/dummy-data.ts create mode 100644 client/src/layout/AppLayout.css create mode 100644 client/src/layout/AppLayout.tsx create mode 100644 client/src/pages/Landing.tsx create mode 100644 client/src/types/network-graph.types.ts diff --git a/client/package.json b/client/package.json index a116546..c7ce3c5 100644 --- a/client/package.json +++ b/client/package.json @@ -12,7 +12,8 @@ "dependencies": { "d3": "^7.9.0", "react": "^19.1.1", - "react-dom": "^19.1.1" + "react-dom": "^19.1.1", + "react-router-dom": "^7.9.3" }, "devDependencies": { "@eslint/js": "^9.33.0", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 6265c60..385a106 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: react-dom: specifier: ^19.1.1 version: 19.1.1(react@19.1.1) + react-router-dom: + specifier: ^7.9.3 + version: 7.9.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) devDependencies: '@eslint/js': specifier: ^9.33.0 @@ -739,6 +742,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1203,6 +1210,23 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} + react-router-dom@7.9.3: + resolution: {integrity: sha512-1QSbA0TGGFKTAc/aWjpfW/zoEukYfU4dc1dLkT/vvf54JoGMkW+fNA+3oyo2gWVW1GM7BxjJVHz5GnPJv40rvg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.9.3: + resolution: {integrity: sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@19.1.1: resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} engines: {node: '>=0.10.0'} @@ -1244,6 +1268,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2021,6 +2048,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.0.2: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2499,6 +2528,20 @@ snapshots: react-refresh@0.17.0: {} + react-router-dom@7.9.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-router: 7.9.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + + react-router@7.9.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + cookie: 1.0.2 + react: 19.1.1 + set-cookie-parser: 2.7.1 + optionalDependencies: + react-dom: 19.1.1(react@19.1.1) + react@19.1.1: {} resolve-from@4.0.0: {} @@ -2548,6 +2591,8 @@ snapshots: semver@7.7.2: {} + set-cookie-parser@2.7.1: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/client/src/App.css b/client/src/App.css index 6ffc97a..639cb8d 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1,2 +1,8 @@ -#root { +html, body, #root { + height: 100%; +} + +html, body { + margin: 0; /* kills the 8px white border */ + padding: 0; } diff --git a/client/src/App.tsx b/client/src/App.tsx index 5ef47eb..14c9967 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,11 +1,21 @@ -import { NetworkGraph } from "./components/NetworkGraph"; +import { lazy, Suspense } from "react"; +import { Route, Routes } from "react-router-dom"; import './App.css' +import AppLayout from "./layout/AppLayout.tsx"; + +const Landing = lazy(() => import("./pages/Landing")); function App() { return ( <> - + + + }> + } /> + + + ) } diff --git a/client/src/components/Header/Header.css b/client/src/components/Header/Header.css new file mode 100644 index 0000000..0ebb239 --- /dev/null +++ b/client/src/components/Header/Header.css @@ -0,0 +1,7 @@ +#header { + font-size: 50px; + justify-content: center; + min-height: 15vh; + display: flex; + align-items: center; +} \ No newline at end of file diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx new file mode 100644 index 0000000..35eeaac --- /dev/null +++ b/client/src/components/Header/Header.tsx @@ -0,0 +1,11 @@ +import "./Header.css"; + +export const Header = () => { + return ( + <> +
+ Kaiaulu +
+ + ) +} \ No newline at end of file diff --git a/client/src/components/Header/index.ts b/client/src/components/Header/index.ts new file mode 100644 index 0000000..64f7c87 --- /dev/null +++ b/client/src/components/Header/index.ts @@ -0,0 +1 @@ +export * from './Header'; \ No newline at end of file diff --git a/client/src/components/NetworkGraph/NetworkGraph.css b/client/src/components/NetworkGraph/NetworkGraph.css index 3c8ef93..09832bd 100644 --- a/client/src/components/NetworkGraph/NetworkGraph.css +++ b/client/src/components/NetworkGraph/NetworkGraph.css @@ -1,5 +1,9 @@ #networkGraph { display: flex; - max-height: 750px; - max-width: 1500px; -} \ No newline at end of file + justify-content: center; + overflow: auto; +} + +#graphCanvas { + display: block; +} diff --git a/client/src/components/NetworkGraph/NetworkGraph.tsx b/client/src/components/NetworkGraph/NetworkGraph.tsx index e8fd556..e93df7b 100644 --- a/client/src/components/NetworkGraph/NetworkGraph.tsx +++ b/client/src/components/NetworkGraph/NetworkGraph.tsx @@ -1,59 +1,67 @@ -import {useEffect, useRef } from 'react'; -import * as d3 from "d3"; -import { useDummyData } from "../../hooks/useDummyData.ts"; +import { useRef, useEffect } from 'react'; import './NetworkGraph.css'; +import * as d3 from "d3"; +import type { Link, Node } from "../../types/network-graph.types.ts"; +import type { NetworkGraphProps } from "./types.ts"; + +export const NetworkGraph = ({ data } : NetworkGraphProps ) => { + const canvasRef = useRef(null); + const links: Link[] = data.links.map((d) => ({ ...d })); + const nodes: Node[] = data.nodes.map((d) => ({ ...d })); -export const NetworkGraph = () => { - const graphContainer = useRef(null); - const { data } = useDummyData(); + const width = 400; + const height = 400; + const radius = 10; useEffect(() => { - if (data != null) { - if (graphContainer.current) { - const svg = d3.select(graphContainer.current) - .append("svg") - .attr("width", 1500) - .attr("height",1500); - - - var link = svg.selectAll("line") - .data(data.links) - .enter() - .append("line") - .style("stroke", "#aaa"); - - var node = svg - .selectAll("circle") - .data(data.nodes) - .enter() - .append("circle") - .attr("r", 20) - .style("fill", "#69b3a2"); - - var simulation = d3.forceSimulation(data.nodes).force("link", d3.forceLink() - .id(function(d) {return d.id; }).links(data.links)) - .force("charge", d3.forceManyBody().strength(-400)) - .force("center", d3.forceCenter(750, 750)) - .on("end", ticked); - - function ticked() { - link - .attr("x1", function(d) { return d.source.x; }) - .attr("y1", function(d) { return d.source.y; }) - .attr("x2", function(d) { return d.target.x; }) - .attr("y2", function(d) { return d.target.y; }); - - node - .attr("cx", function (d) { return d.x+6; }) - .attr("cy", function(d) { return d.y-6; }); - } - } + const canvas = canvasRef.current; + const context = canvas?.getContext("2d"); + + if (!context) { + return; } - }, [data]); + + // run d3-force to find the position of nodes on the canvas + d3.forceSimulation(nodes) + + // list of forces we apply to get node positions + .force( + 'link', + d3.forceLink(links).id((d) => d.id) + ) + .force('collide', d3.forceCollide().radius(radius)) + .force('charge', d3.forceManyBody()) + .force('center', d3.forceCenter(width / 2, height / 2)) + + // at each iteration of the simulation, draw the network diagram with the new node positions + .on('tick', () => { + context.clearRect(0, 0, width, height); + + links.forEach((link) => { + context.beginPath(); + context.moveTo(link.source.x, link.source.y); + context.lineTo(link.target.x, link.target.y); + context.stroke(); + context.strokeStyle = "white"; + }); + + nodes.forEach((node) => { + if (!node.x || !node.y) { + return; + } + + context.beginPath(); + context.moveTo(node.x + radius, node.y); + context.arc(node.x, node.y, radius, 0, 2 * Math.PI); + context.fillStyle = 'white'; + context.fill(); + }); + }); + }, [nodes, links]) return ( -
-
+
+ +
) - } \ No newline at end of file diff --git a/client/src/components/NetworkGraph/types.ts b/client/src/components/NetworkGraph/types.ts new file mode 100644 index 0000000..0208b9f --- /dev/null +++ b/client/src/components/NetworkGraph/types.ts @@ -0,0 +1,5 @@ +import type { NetworkGraphData } from "../../types/network-graph.types.ts"; + +export type NetworkGraphProps = { + data: NetworkGraphData; +}; \ No newline at end of file diff --git a/client/src/data/dummy-data.ts b/client/src/data/dummy-data.ts new file mode 100644 index 0000000..1d03e24 --- /dev/null +++ b/client/src/data/dummy-data.ts @@ -0,0 +1,42 @@ +export const data = { + nodes: [ + { id: 'Myriel', group: 'team1' }, + { id: 'Anne', group: 'team1' }, + { id: 'Gabriel', group: 'team1' }, + { id: 'Mel', group: 'team1' }, + { id: 'Yan', group: 'team2' }, + { id: 'Tom', group: 'team2' }, + { id: 'Cyril', group: 'team2' }, + { id: 'Tuck', group: 'team2' }, + { id: 'Antoine', group: 'team3' }, + { id: 'Rob', group: 'team3' }, + { id: 'Napoleon', group: 'team3' }, + { id: 'Toto', group: 'team4' }, + { id: 'Tutu', group: 'team4' }, + { id: 'Titi', group: 'team4' }, + { id: 'Tata', group: 'team4' }, + { id: 'Turlututu', group: 'team4' }, + { id: 'Tita', group: 'team4' }, + ], + links: [ + { source: 'Anne', target: 'Myriel', value: 1 }, + { source: 'Napoleon', target: 'Myriel', value: 1 }, + { source: 'Gabriel', target: 'Myriel', value: 1 }, + { source: 'Mel', target: 'Myriel', value: 1 }, + { source: 'Yan', target: 'Tom', value: 1 }, + { source: 'Tom', target: 'Cyril', value: 1 }, + { source: 'Tuck', target: 'Myriel', value: 1 }, + { source: 'Tuck', target: 'Mel', value: 1 }, + { source: 'Tuck', target: 'Myriel', value: 1 }, + { source: 'Mel', target: 'Myriel', value: 1 }, + { source: 'Rob', target: 'Antoine', value: 1 }, + { source: 'Tata', target: 'Tutu', value: 1 }, + { source: 'Tata', target: 'Titi', value: 1 }, + { source: 'Tata', target: 'Toto', value: 1 }, + { source: 'Tata', target: 'Tita', value: 1 }, + { source: 'Tita', target: 'Toto', value: 1 }, + { source: 'Tita', target: 'Titi', value: 1 }, + { source: 'Tita', target: 'Turlututu', value: 1 }, + { source: 'Rob', target: 'Turlututu', value: 1 }, + ], +}; \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css index 618efd6..b8252fb 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,3 +1,5 @@ -:root { - background-color: #242424; +#root { + display: flex; + flex-direction: column; + background-color: #242424; } diff --git a/client/src/layout/AppLayout.css b/client/src/layout/AppLayout.css new file mode 100644 index 0000000..1253008 --- /dev/null +++ b/client/src/layout/AppLayout.css @@ -0,0 +1,9 @@ +#appContainer { + min-height: 100dvh; /* use dvh to avoid mobile 100vh bugs */ + width: 100vw; + overflow: hidden; /* optional: hide scrollbars for full-bleed canvases */ + display: flex; /* if you have header/footer layout */ + flex-direction: column; + background-color: black; + color: white; +} \ No newline at end of file diff --git a/client/src/layout/AppLayout.tsx b/client/src/layout/AppLayout.tsx new file mode 100644 index 0000000..3f04eb3 --- /dev/null +++ b/client/src/layout/AppLayout.tsx @@ -0,0 +1,14 @@ +import { Outlet } from "react-router-dom"; +import { Header } from "../components/Header"; +import "./AppLayout.css"; + +const AppLayout = () => { + return ( +
+
+ +
+ ) +} + +export default AppLayout; \ No newline at end of file diff --git a/client/src/main.tsx b/client/src/main.tsx index bef5202..0bc4718 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,10 +1,12 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App.tsx'; createRoot(document.getElementById('root')!).render( - + + + , ) diff --git a/client/src/pages/Landing.tsx b/client/src/pages/Landing.tsx new file mode 100644 index 0000000..e5be076 --- /dev/null +++ b/client/src/pages/Landing.tsx @@ -0,0 +1,12 @@ +import { NetworkGraph} from "../components/NetworkGraph"; +import { data } from "../data/dummy-data.ts"; + +const Landing = () => { + return ( +
+ +
+ ) +} + +export default Landing; \ No newline at end of file diff --git a/client/src/types/network-graph.types.ts b/client/src/types/network-graph.types.ts new file mode 100644 index 0000000..edcbae2 --- /dev/null +++ b/client/src/types/network-graph.types.ts @@ -0,0 +1,14 @@ +import type { SimulationNodeDatum, SimulationLinkDatum } from "d3"; + +export interface Node extends SimulationNodeDatum { + id: string; +} + +export type Link = SimulationLinkDatum & { + value: number; +}; + +export type NetworkGraphData = { + nodes: Node[]; + links: Link[]; +}; \ No newline at end of file From 5e7bb9d71fd9a810ad508ac2d1c78a8c8867f7b8 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:41:46 -1000 Subject: [PATCH 06/29] Implemented drag functionality for nodes Nodes can now be dragged around. --- client/public/kaiaulu_logo.png | Bin 0 -> 116342 bytes client/src/components/Header/Header.css | 2 +- client/src/components/Header/Header.tsx | 2 +- .../components/NetworkGraph/NetworkGraph.tsx | 61 ++++++++++++++++-- client/src/layout/AppLayout.css | 2 +- 5 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 client/public/kaiaulu_logo.png diff --git a/client/public/kaiaulu_logo.png b/client/public/kaiaulu_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d9127272ee6c89007123d0302be6b7544447883e GIT binary patch literal 116342 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

pW7NaW`Ew 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%0ApNc$69T8&}4VoqRLZ0 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 { return ( <>

- Kaiaulu + Kaiāulu
) diff --git a/client/src/components/NetworkGraph/NetworkGraph.tsx b/client/src/components/NetworkGraph/NetworkGraph.tsx index e93df7b..953e6ee 100644 --- a/client/src/components/NetworkGraph/NetworkGraph.tsx +++ b/client/src/components/NetworkGraph/NetworkGraph.tsx @@ -12,6 +12,8 @@ export const NetworkGraph = ({ data } : NetworkGraphProps ) => { const width = 400; const height = 400; const radius = 10; + const forceStrength = -10; + useEffect(() => { const canvas = canvasRef.current; @@ -22,15 +24,15 @@ export const NetworkGraph = ({ data } : NetworkGraphProps ) => { } // run d3-force to find the position of nodes on the canvas - d3.forceSimulation(nodes) + const simulation = d3.forceSimulation(nodes) // list of forces we apply to get node positions .force( 'link', d3.forceLink(links).id((d) => d.id) ) - .force('collide', d3.forceCollide().radius(radius)) - .force('charge', d3.forceManyBody()) + .force('collide', d3.forceCollide().radius(10)) + .force('charge', d3.forceManyBody().strength(forceStrength)) .force('center', d3.forceCenter(width / 2, height / 2)) // at each iteration of the simulation, draw the network diagram with the new node positions @@ -38,9 +40,13 @@ export const NetworkGraph = ({ data } : NetworkGraphProps ) => { context.clearRect(0, 0, width, height); links.forEach((link) => { + if (!isNode(link.source) || !isNode(link.target)) return; + const s = link.source, t = link.target; + if (!hasPos(s) || !hasPos(t)) return; + context.beginPath(); - context.moveTo(link.source.x, link.source.y); - context.lineTo(link.target.x, link.target.y); + context.moveTo(s.x, s.y); + context.lineTo(t.x, t.y); context.stroke(); context.strokeStyle = "white"; }); @@ -50,12 +56,47 @@ export const NetworkGraph = ({ data } : NetworkGraphProps ) => { return; } + context.beginPath(); context.moveTo(node.x + radius, node.y); context.arc(node.x, node.y, radius, 0, 2 * Math.PI); context.fillStyle = 'white'; context.fill(); + + }); + + const drag = d3 + .drag() + .subject((event) => { + const [x, y] = d3.pointer(event, canvas); + + // find nearest node within ~2*radius + const n = simulation.find(x, y, radius * 2) as Node | undefined; + + if (n) { + n.fx = n.x ?? x; + n.fy = n.y ?? y; + } + + return n; + }) + .on('start', (event) => { + if (!event.active) simulation.alphaTarget(0.3).restart(); + }) + .on('drag', (event) => { + const n = event.subject; + n.fx = event.x; + n.fy = event.y; + }) + .on('end', (event) => { + if (!event.active) simulation.alphaTarget(0); + const n = event.subject; + n.fx = null; + n.fy = null; + }); + + d3.select(canvas).call(drag as any); }); }, [nodes, links]) @@ -64,4 +105,12 @@ export const NetworkGraph = ({ data } : NetworkGraphProps ) => { ) -} \ No newline at end of file +} + +function isNode(v: Link["source"]): v is Node { + return typeof v === "object" && v !== null; +} + +function hasPos(n: Node): n is Node & { x: number; y: number } { + return n.x != null && n.y != null; +} diff --git a/client/src/layout/AppLayout.css b/client/src/layout/AppLayout.css index 1253008..51f34da 100644 --- a/client/src/layout/AppLayout.css +++ b/client/src/layout/AppLayout.css @@ -4,6 +4,6 @@ overflow: hidden; /* optional: hide scrollbars for full-bleed canvases */ display: flex; /* if you have header/footer layout */ flex-direction: column; - background-color: black; + background-color: #017371; color: white; } \ No newline at end of file From a5b8436a9f5513fbfbb6bb25e75d3a3b3138a66c Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Sun, 12 Oct 2025 19:14:41 -1000 Subject: [PATCH 07/29] Added more data. Groups now spawn in pre-defined quadrants. Physics simulation runs for a short amount of time to calculate initial positions of the nodes, then it is disabled. Largest node of a group is used as the center node for a group of nodes during simulation. Removed collision from nodes. Nodes now have labels. --- .../{public => src/assets}/kaiaulu_logo.png | Bin client/src/components/Header/Header.css | 4 +- client/src/components/Header/Header.tsx | 1 - .../components/NetworkGraph/NetworkGraph.css | 3 +- .../components/NetworkGraph/NetworkGraph.tsx | 288 ++++++++++----- .../NetworkGraph/NetworkGraphBACKUP.tsx | 339 ++++++++++++++++++ client/src/components/NetworkGraph/types.ts | 3 +- client/src/data/dummy-data.ts | 111 ++++-- client/src/layout/AppLayout.css | 12 +- client/src/layout/AppLayout.tsx | 2 +- client/src/pages/Landing.tsx | 2 - client/src/types/network-graph.types.ts | 6 +- 12 files changed, 633 insertions(+), 138 deletions(-) rename client/{public => src/assets}/kaiaulu_logo.png (100%) create mode 100644 client/src/components/NetworkGraph/NetworkGraphBACKUP.tsx diff --git a/client/public/kaiaulu_logo.png b/client/src/assets/kaiaulu_logo.png similarity index 100% rename from client/public/kaiaulu_logo.png rename to client/src/assets/kaiaulu_logo.png diff --git a/client/src/components/Header/Header.css b/client/src/components/Header/Header.css index eb9738c..9dfbe0f 100644 --- a/client/src/components/Header/Header.css +++ b/client/src/components/Header/Header.css @@ -1,7 +1,7 @@ #header { + flex: 0 0 15%; font-size: 50px; justify-content: center; - min-height: 15dvh; display: flex; align-items: center; -} \ No newline at end of file +} diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx index 3ed34c3..1fa198a 100644 --- a/client/src/components/Header/Header.tsx +++ b/client/src/components/Header/Header.tsx @@ -4,7 +4,6 @@ export const Header = () => { return ( <>
- Kaiāulu
) diff --git a/client/src/components/NetworkGraph/NetworkGraph.css b/client/src/components/NetworkGraph/NetworkGraph.css index 09832bd..f45140f 100644 --- a/client/src/components/NetworkGraph/NetworkGraph.css +++ b/client/src/components/NetworkGraph/NetworkGraph.css @@ -1,9 +1,10 @@ #networkGraph { display: flex; + flex: 1; justify-content: center; overflow: auto; } #graphCanvas { - display: block; + flex: 1; } diff --git a/client/src/components/NetworkGraph/NetworkGraph.tsx b/client/src/components/NetworkGraph/NetworkGraph.tsx index 953e6ee..42c7c62 100644 --- a/client/src/components/NetworkGraph/NetworkGraph.tsx +++ b/client/src/components/NetworkGraph/NetworkGraph.tsx @@ -1,109 +1,217 @@ -import { useRef, useEffect } from 'react'; +import { useRef, useEffect, useCallback } from 'react'; import './NetworkGraph.css'; -import * as d3 from "d3"; -import type { Link, Node } from "../../types/network-graph.types.ts"; +import { + forceX, + forceY, + forceSimulation, + select, + drag, + pointer, + forceCollide, + forceManyBody, + forceLink +} from "d3"; +import type { Simulation } from "d3"; +import type { Link, Node, Group } from "../../types/network-graph.types.ts"; import type { NetworkGraphProps } from "./types.ts"; export const NetworkGraph = ({ data } : NetworkGraphProps ) => { const canvasRef = useRef(null); + const simulationRef = useRef>(null); + const links: Link[] = data.links.map((d) => ({ ...d })); const nodes: Node[] = data.nodes.map((d) => ({ ...d })); - const width = 400; - const height = 400; - const radius = 10; - const forceStrength = -10; + // Hub nodes that serve as the center of each group, determined by size of the node + const hubs: Record = { + people: nodes.filter(n => n.group === 'people').reduce((a,b)=> a.value>b.value?a:b), + mail: nodes.filter(n => n.group === 'mail').reduce((a,b)=> a.value>b.value?a:b), + file: nodes.filter(n => n.group === 'file').reduce((a,b)=> a.value>b.value?a:b), + issue: nodes.filter(n => n.group === 'issue').reduce((a,b)=> a.value>b.value?a:b), + }; + + // Helper for determining hub nodes + const isHub = useCallback((d: Node) => hubs[d.group] === d, []); + + type HubLink = { source: Node; target: Node }; + const hubLinks: HubLink[] = nodes + .filter(n => !isHub(n)) + .map(n => ({ source: hubs[n.group], target: n })); + + const radius = 40; + const forceStrength = -100; + const nodeRadiusMultiplier = 11; + const nodePadding = 6; + + // Helper that takes canvas width and height and returns center coordinate for each group + const centers = (w: number, h: number) => ({ + people: [w * 0.5, h * 0.4], + mail: [w * 0.8, h * 0.3], + file: [w * 0.25, h * 0.4], + issue: [w * 0.8, h * 0.6], + }); + + // Initialize force simulation with nodes and links + if (!simulationRef.current) { + simulationRef.current = forceSimulation(nodes) + .force('link', forceLink(links).id((d) => d.id) + .distance(80) // tighter cluster around hub + .strength(0.05)) // stronger pull to the hub) + .force('hubLinks', forceLink(hubLinks) + .distance(40) // tighter cluster around hub + .strength(0.2) // stronger pull to the hub + ); + } + + // Helper function for clearing forces + function clearForces(sim: Simulation) { + sim + .force('x', null) + .force('y', null) + .force('collide', null) + .force('charge', null) + .force('link', null) + .force('hubLinks', null) + .velocityDecay(1); // stop inertia + } + + // Function for drawing the network graph that is called on each tick of the simulation + const drawGraph = useCallback((context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => { + context.clearRect(0, 0, canvas.width, canvas.height); + if (!simulationRef.current) return; + + links.forEach((link) => { + if (!isNode(link.source) || !isNode(link.target)) return; + console.log("this link has a source and target"); + + const s = link.source, t = link.target; + if (!hasPos(s) || !hasPos(t)) return; + + console.log(s); + console.log(t); + + context.beginPath(); + context.moveTo(s.x, s.y); + context.lineTo(t.x, t.y); + context.stroke(); + context.strokeStyle = "grey"; + }); + + nodes.forEach((node) => { + drawNodeByGroup(node, context); + }) + + }, []); + + // Helper function for drawing a single node based on its group + const drawNodeByGroup = useCallback((node: Node, context: CanvasRenderingContext2D) => { + if (!node.x || !node.y) { + return; + } + + switch (node.group) { + case "people": + context.fillStyle = 'black'; + break; + case "mail": + context.fillStyle = 'LightBlue'; + break; + case 'file': + context.fillStyle = 'yellow'; + break; + case 'issue': + context.fillStyle = 'blue'; + } + + const label = (node as Node).id ?? node.id ?? ""; + if (!label) return; + + context.beginPath(); + context.moveTo(node.x + radius, node.y); + context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); + context.fill(); + + context.textAlign = "center"; + context.textBaseline = "middle"; + context.fillStyle = '#fff'; + context.fillText(label, node.x, node.y); + }, []); + + + // Apply forces for simulation useEffect(() => { + if (!simulationRef.current || !canvasRef.current) return; + const canvas = canvasRef.current; - const context = canvas?.getContext("2d"); + const container = canvas.parentElement; + if (!container) return; + + const context = canvas.getContext("2d"); + if (!context) return; + + const resize = () => { + const rect = container.getBoundingClientRect(); + canvas.width = rect.width; + canvas.height = rect.height; + }; + resize(); + + const c = centers(canvas.width, canvas.height); + + simulationRef.current + .force("x", forceX(d => c[d.group][0]).strength(0.2)) + .force("y", forceY(d => c[d.group][1]).strength(0.2)) + .force('collide', forceCollide().radius(d => d.value * nodeRadiusMultiplier + nodePadding)) + .force('charge', forceManyBody().strength(forceStrength)); + + simulationRef.current.alpha(1); + for (let i = 0; i < 300; i++) simulationRef.current.tick(); + + drawGraph(context, canvas); + clearForces(simulationRef.current); + + simulationRef.current.on("tick", null); + simulationRef.current.stop(); + + + const dragBehavior = drag() + .subject((event) => { + const [x, y] = pointer(event, canvas); + if (!simulationRef.current) return; + + // find nearest node within ~2*radius + const n = simulationRef.current.find(x, y, radius) as Node | undefined; + + if (n) { + n.fx = n.x ?? x; + n.fy = n.y ?? y; + } + + return n; + }) + .on('drag', (event) => { + if (!simulationRef.current) return; + const n = event.subject; + n.x = event.x; + n.y = event.y; + drawGraph(context, canvas); + }); - if (!context) { - return; - } + select(canvas).call(dragBehavior as any); - // run d3-force to find the position of nodes on the canvas - const simulation = d3.forceSimulation(nodes) - - // list of forces we apply to get node positions - .force( - 'link', - d3.forceLink(links).id((d) => d.id) - ) - .force('collide', d3.forceCollide().radius(10)) - .force('charge', d3.forceManyBody().strength(forceStrength)) - .force('center', d3.forceCenter(width / 2, height / 2)) - - // at each iteration of the simulation, draw the network diagram with the new node positions - .on('tick', () => { - context.clearRect(0, 0, width, height); - - links.forEach((link) => { - if (!isNode(link.source) || !isNode(link.target)) return; - const s = link.source, t = link.target; - if (!hasPos(s) || !hasPos(t)) return; - - context.beginPath(); - context.moveTo(s.x, s.y); - context.lineTo(t.x, t.y); - context.stroke(); - context.strokeStyle = "white"; - }); - - nodes.forEach((node) => { - if (!node.x || !node.y) { - return; - } - - - context.beginPath(); - context.moveTo(node.x + radius, node.y); - context.arc(node.x, node.y, radius, 0, 2 * Math.PI); - context.fillStyle = 'white'; - context.fill(); - - - }); - - const drag = d3 - .drag() - .subject((event) => { - const [x, y] = d3.pointer(event, canvas); - - // find nearest node within ~2*radius - const n = simulation.find(x, y, radius * 2) as Node | undefined; - - if (n) { - n.fx = n.x ?? x; - n.fy = n.y ?? y; - } - - return n; - }) - .on('start', (event) => { - if (!event.active) simulation.alphaTarget(0.3).restart(); - }) - .on('drag', (event) => { - const n = event.subject; - n.fx = event.x; - n.fy = event.y; - }) - .on('end', (event) => { - if (!event.active) simulation.alphaTarget(0); - const n = event.subject; - n.fx = null; - n.fy = null; - }); - - d3.select(canvas).call(drag as any); - }); - }, [nodes, links]) + const ro = new ResizeObserver(() => { + resize(); + drawGraph(context, canvas); + }) + + ro.observe(container); + return () => ro.disconnect(); + }, [canvasRef, simulationRef]); return ( -
- -
+ ) } diff --git a/client/src/components/NetworkGraph/NetworkGraphBACKUP.tsx b/client/src/components/NetworkGraph/NetworkGraphBACKUP.tsx new file mode 100644 index 0000000..35388e9 --- /dev/null +++ b/client/src/components/NetworkGraph/NetworkGraphBACKUP.tsx @@ -0,0 +1,339 @@ +// import { useRef, useEffect, useCallback } from 'react'; +// import './NetworkGraph.css'; +// import { +// forceSimulation, +// select, +// drag, +// pointer, +// forceCollide, +// forceManyBody, +// forceLink, +// forceCenter, +// type ForceLink +// } from "d3"; +// import type { Simulation } from "d3"; +// import type { Link, Node } from "../../types/network-graph.types.ts"; +// import type { NetworkGraphProps } from "./types.ts"; +// +// export const NetworkGraph = ({ data } : NetworkGraphProps ) => { +// const canvasRef = useRef(null); +// +// const simulationArrayRef = useRef>>(null); +// const simulationRef = useRef>(null); +// +// const peopleNodes: Node[] = data.nodes.filter(n => n.group === "people").map(n => ({ ...n })); +// const mailNodes: Node[] = data.nodes.filter(n => n.group === "mail").map(n => ({ ...n })); +// const fileNodes: Node[] = data.nodes.filter(n => n.group === "file").map(n => ({ ...n })); +// const issueNodes: Node[] = data.nodes.filter(n => n.group === "issue").map(n => ({ ...n })); +// +// +// const links: Link[] = data.links.map((d) => ({ ...d })); +// const nodes: Node[] = data.nodes.map((d) => ({ ...d })); +// +// const radius = 30; +// const forceStrength = -10; +// const nodeRadiusMultiplier = 10; +// +// if (!simulationArrayRef.current) { +// simulationArrayRef.current = new Array>(); +// simulationArrayRef.current.push(forceSimulation(peopleNodes)); +// simulationArrayRef.current.push(forceSimulation(mailNodes)); +// simulationArrayRef.current.push(forceSimulation(fileNodes)); +// simulationArrayRef.current.push(forceSimulation(issueNodes)); +// } +// +// +// +// // // New draw function called on every tick of a simulation +// // const newDrawGraph = useCallback((context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => { +// // context.clearRect(0, 0, canvas.width, canvas.height); +// // if (!simulationArrayRef.current) return; +// // +// // links.forEach((link) => { +// // if (!isNode(link.source) || !isNode(link.target)) return; +// // +// // const s = link.source, t = link.target; +// // if (!hasPos(s) || !hasPos(t)) return; +// // +// // context.beginPath(); +// // context.moveTo(s.x, s.y); +// // context.lineTo(t.x, t.y); +// // context.stroke(); +// // context.strokeStyle = "green"; +// // }); +// // +// // peopleNodes.forEach((node) => { +// // if (!node.x || !node.y) { +// // return; +// // } +// // +// // context.beginPath(); +// // context.moveTo(node.x + radius, node.y); +// // context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); +// // context.fillStyle = 'black'; +// // context.fill(); +// // }); +// // +// // mailNodes.forEach((node) => { +// // if (!node.x || !node.y) { +// // return; +// // } +// // +// // context.beginPath(); +// // context.moveTo(node.x + radius, node.y); +// // context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); +// // context.fillStyle = 'LightBlue'; +// // context.fill(); +// // }); +// // +// // fileNodes.forEach((node) => { +// // if (!node.x || !node.y) { +// // return; +// // } +// // +// // context.beginPath(); +// // context.moveTo(node.x + radius, node.y); +// // context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); +// // context.fillStyle = 'yellow'; +// // context.fill(); +// // }); +// // +// // issueNodes.forEach((node) => { +// // if (!node.x || !node.y) { +// // return; +// // } +// // +// // context.beginPath(); +// // context.moveTo(node.x + radius, node.y); +// // context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); +// // context.fillStyle = 'blue'; +// // context.fill(); +// // }); +// +// +// +// +// // nodes.forEach((node) => { +// // if (!node.x || !node.y) { +// // return; +// // } +// // +// // context.beginPath(); +// // context.moveTo(node.x + radius, node.y); +// // context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); +// // +// // switch (node.group) { +// // case "mail": +// // context.fillStyle = 'LightBlue'; +// // break; +// // case "issue": +// // context.fillStyle = 'blue'; +// // break; +// // case "people": +// // context.fillStyle = 'black'; +// // break; +// // case "file": +// // context.fillStyle = 'yellow'; +// // } +// // +// // context.fill(); +// // +// // }); +// +// // }, []); +// +// +// // Apply forces for each simulation +// // useEffect(() => { +// // if (!simulationArrayRef.current) return; +// // +// // if (!canvasRef.current) return; +// // const canvas = canvasRef.current; +// // const context = canvas.getContext("2d"); +// // +// // const container = canvas.parentElement; +// // if (!container) return; +// // +// // if (!context) return; +// // +// // simulationArrayRef.current.forEach((simulation) => { +// // simulation +// // .force('collide', forceCollide().radius(radius)) +// // .force('charge', forceManyBody().strength(forceStrength)) +// // .force('center', forceCenter(canvas.width/2, canvas.height/2)) +// // .on('tick', () => { +// // newDrawGraph(context, canvas) +// // if (!simulationRef.current) return; +// // simulationRef.current.force('center', null as any); +// // simulationRef.current.force('link', null as any); +// // }); +// // } +// // ); +// // +// // const resize = () => { +// // const rect = container.getBoundingClientRect(); +// // canvas.width = rect.width; +// // canvas.height = rect.height; +// // }; +// // +// // const ro = new ResizeObserver(resize); +// // ro.observe(container); +// // resize(); +// // +// // return () => ro.disconnect(); +// // }, [canvasRef, simulationArrayRef]); +// +// if (!simulationRef.current) { +// simulationRef.current = forceSimulation(nodes).force( +// 'link', +// forceLink(links).id((d) => d.id) +// ) +// .force('collide', forceCollide().radius(radius)) +// .force('charge', forceManyBody().strength(forceStrength)); +// } +// +// // Helper function called every tick of the simulation, draws the nodes and links at their calculated position on that tick +// const drawGraph = useCallback((context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => { +// context.clearRect(0, 0, canvas.width, canvas.height); +// if (!simulationRef.current) return; +// +// links.forEach((link) => { +// if (!isNode(link.source) || !isNode(link.target)) return; +// +// const s = link.source, t = link.target; +// if (!hasPos(s) || !hasPos(t)) return; +// +// context.beginPath(); +// context.moveTo(s.x, s.y); +// context.lineTo(t.x, t.y); +// context.stroke(); +// context.strokeStyle = "green"; +// }); +// +// nodes.forEach((node) => { +// if (!node.x || !node.y) { +// return; +// } +// +// context.beginPath(); +// context.moveTo(node.x + radius, node.y); +// context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); +// +// switch (node.group) { +// case "mail": +// context.fillStyle = 'LightBlue'; +// break; +// case "issue": +// context.fillStyle = 'blue'; +// break; +// case "people": +// context.fillStyle = 'black'; +// break; +// case "file": +// context.fillStyle = 'yellow'; +// } +// +// context.fill(); +// +// }); +// +// }, []); +// +// useEffect(() => { +// +// const canvas = canvasRef.current; +// if (!canvas) return; +// +// const container = canvas.parentElement; +// if (!container) return; +// +// if (!simulationRef.current) return; +// simulationRef.current.force('center', forceCenter(canvas.width/2, canvas.height/2)); +// +// const resize = () => { +// const rect = container.getBoundingClientRect(); +// canvas.width = rect.width; +// canvas.height = rect.height; +// }; +// +// const ro = new ResizeObserver(resize); +// ro.observe(container); +// resize(); +// +// return () => ro.disconnect(); +// }, [canvasRef]); +// // +// +// useEffect(() => { +// const canvas = canvasRef.current; +// if (!canvas) return; +// +// const context = canvas.getContext("2d"); +// if (!context) return; +// +// if (!simulationRef.current) return; +// +// +// +// simulationRef.current.on('tick', () => { +// drawGraph(context, canvas) +// if (!simulationRef.current) return; +// simulationRef.current.force('center', null as any); +// simulationRef.current.force('link', null as any); +// }); +// +// const dragBehavior = drag() +// .subject((event) => { +// const [x, y] = pointer(event, canvas); +// if (!simulationRef.current) return; +// +// // find nearest node within ~2*radius +// const n = simulationRef.current.find(x, y, radius * 2) as Node | undefined; +// +// if (n) { +// n.fx = n.x ?? x; +// n.fy = n.y ?? y; +// } +// +// return n; +// }) +// .on('drag', (event) => { +// if (!simulationRef.current) return; +// const n = event.subject; +// n.fx = event.x; +// n.fy = event.y; +// +// if (simulationRef.current.alpha() < 0.03) simulationRef.current.alpha(0.03).restart(); +// }) +// .on('end', (event) => { +// if (!simulationRef.current) return; +// +// const n = event.subject; +// n.fx = null; +// n.fy = null; +// +// const linkForce = simulationRef.current.force>('link'); +// if (linkForce) { +// linkForce.strength(0.1); +// } +// +// simulationRef.current.alpha(Math.max(simulationRef.current.alpha(), 0.05)).alphaTarget(0); +// }); +// +// select(canvas).call(dragBehavior as any); +// +// +// }, [canvasRef, nodes, links]); +// +// return ( +// +// ) +// } +// +// function isNode(v: Link["source"]): v is Node { +// return typeof v === "object" && v !== null; +// } +// +// function hasPos(n: Node): n is Node & { x: number; y: number } { +// return n.x != null && n.y != null; +// } diff --git a/client/src/components/NetworkGraph/types.ts b/client/src/components/NetworkGraph/types.ts index 0208b9f..d62d3ac 100644 --- a/client/src/components/NetworkGraph/types.ts +++ b/client/src/components/NetworkGraph/types.ts @@ -2,4 +2,5 @@ import type { NetworkGraphData } from "../../types/network-graph.types.ts"; export type NetworkGraphProps = { data: NetworkGraphData; -}; \ No newline at end of file +}; + diff --git a/client/src/data/dummy-data.ts b/client/src/data/dummy-data.ts index 1d03e24..6093144 100644 --- a/client/src/data/dummy-data.ts +++ b/client/src/data/dummy-data.ts @@ -1,42 +1,81 @@ export const data = { nodes: [ - { id: 'Myriel', group: 'team1' }, - { id: 'Anne', group: 'team1' }, - { id: 'Gabriel', group: 'team1' }, - { id: 'Mel', group: 'team1' }, - { id: 'Yan', group: 'team2' }, - { id: 'Tom', group: 'team2' }, - { id: 'Cyril', group: 'team2' }, - { id: 'Tuck', group: 'team2' }, - { id: 'Antoine', group: 'team3' }, - { id: 'Rob', group: 'team3' }, - { id: 'Napoleon', group: 'team3' }, - { id: 'Toto', group: 'team4' }, - { id: 'Tutu', group: 'team4' }, - { id: 'Titi', group: 'team4' }, - { id: 'Tata', group: 'team4' }, - { id: 'Turlututu', group: 'team4' }, - { id: 'Tita', group: 'team4' }, + { id: 'person_1', group: 'people', value: 2}, + { id: 'person_2', group: 'people', value: 2}, + { id: 'person_3', group: 'people', value: 2}, + { id: 'person_4', group: 'people', value: 2}, + { id: 'mail_1', group: 'mail', value: 2}, + { id: 'mail_2', group: 'mail', value: 2}, + { id: 'mail_3', group: 'mail', value: 2}, + { id: 'mail_4', group: 'mail', value: 2}, + { id: 'mail_5', group: 'mail', value: 2}, + { id: 'mail_6', group: 'mail', value: 2}, + { id: 'mail_7', group: 'mail', value: 2}, + { id: 'mail_8', group: 'mail', value: 2}, + { id: 'issue_1', group: 'issue', value: 4}, + { id: 'issue_2', group: 'issue', value: 2}, + { id: 'issue_3', group: 'issue', value: 2}, + { id: 'file_1', group: 'file', value: 2}, + { id: 'file_2', group: 'file', value: 2}, + { id: 'file_3', group: 'file', value: 2}, + { id: 'file_4', group: 'file', value: 2}, + { id: 'file_5', group: 'file', value: 2}, + { id: 'file_6', group: 'file', value: 2}, + { id: 'file_7', group: 'file', value: 2}, + { id: 'file_8', group: 'file', value: 2}, + { id: 'file_9', group: 'file', value: 2}, + { id: 'file_10', group: 'file', value: 3}, + { id: 'file_11', group: 'file', value: 4}, + { id: 'file_12', group: 'file', value: 4}, + { id: 'file_13', group: 'file', value: 2}, + { id: 'file_14', group: 'file', value: 2}, + { id: 'file_15', group: 'file', value: 2}, + { id: 'file_16', group: 'file', value: 2}, + { id: 'file_17', group: 'file', value: 2}, + { id: 'file_18', group: 'file', value: 2}, + { id: 'file_19', group: 'file', value: 2}, + { id: 'file_20', group: 'file', value: 2}, + { id: 'file_21', group: 'file', value: 2}, + { id: 'file_22', group: 'file', value: 3}, + { id: 'file_23', group: 'file', value: 4}, + { id: 'file_24', group: 'file', value: 8}, + ], links: [ - { source: 'Anne', target: 'Myriel', value: 1 }, - { source: 'Napoleon', target: 'Myriel', value: 1 }, - { source: 'Gabriel', target: 'Myriel', value: 1 }, - { source: 'Mel', target: 'Myriel', value: 1 }, - { source: 'Yan', target: 'Tom', value: 1 }, - { source: 'Tom', target: 'Cyril', value: 1 }, - { source: 'Tuck', target: 'Myriel', value: 1 }, - { source: 'Tuck', target: 'Mel', value: 1 }, - { source: 'Tuck', target: 'Myriel', value: 1 }, - { source: 'Mel', target: 'Myriel', value: 1 }, - { source: 'Rob', target: 'Antoine', value: 1 }, - { source: 'Tata', target: 'Tutu', value: 1 }, - { source: 'Tata', target: 'Titi', value: 1 }, - { source: 'Tata', target: 'Toto', value: 1 }, - { source: 'Tata', target: 'Tita', value: 1 }, - { source: 'Tita', target: 'Toto', value: 1 }, - { source: 'Tita', target: 'Titi', value: 1 }, - { source: 'Tita', target: 'Turlututu', value: 1 }, - { source: 'Rob', target: 'Turlututu', value: 1 }, + { source: 'person_1', target: 'mail_4', value: 1 }, + { source: 'person_1', target: 'file_2', value: 1 }, + { source: 'person_2', target: 'file_5', value: 1 }, + { source: 'person_2', target: 'mail_2', value: 1 }, + { source: 'person_3', target: 'mail_1', value: 1 }, + { source: 'person_3', target: 'issue_1', value: 1 }, + { source: 'person_4', target: 'file_7', value: 1 }, + { source: 'person_4', target: 'file_23', value: 1 }, + { source: 'mail_2', target: 'mail_1', value: 1 }, + { source: 'mail_3', target: 'mail_1', value: 1 }, + { source: 'mail_4', target: 'mail_1', value: 1 }, + { source: 'mail_4', target: 'mail_1', value: 1 }, + { source: 'mail_5', target: 'mail_1', value: 1 }, + { source: 'mail_5', target: 'mail_4', value: 1 }, + { source: 'mail_5', target: 'mail_1', value: 1 }, + { source: 'mail_6', target: 'mail_7', value: 1 }, + { source: 'mail_7', target: 'mail_8', value: 1 }, + { source: 'issue_1', target: 'issue_2', value: 1 }, + { source: 'issue_2', target: 'issue_3', value: 1 }, + { source: 'file_4', target: 'file_2', value: 1 }, + { source: 'file_4', target: 'file_3', value: 1 }, + { source: 'file_4', target: 'file_1', value: 1 }, + { source: 'file_4', target: 'file_6', value: 1 }, + { source: 'file_4', target: 'issue_2', value: 1 }, + { source: 'file_6', target: 'file_1', value: 1 }, + { source: 'file_6', target: 'file_3', value: 1 }, + { source: 'file_6', target: 'file_5', value: 1 }, + { source: 'file_7', target: 'file_8', value: 2}, + { source: 'file_8', target: 'file_9', value: 2}, + { source: 'file_9', target: 'file_10', value: 2}, + { source: 'file_9', target: 'file_22', value: 2}, + { source: 'file_10', target: 'file_11', value: 3}, + { source: 'file_11', target: 'file_12', value: 4}, + { source: 'file_23', target: 'issue_1', value: 4}, + { source: 'file_23', target: 'file_24', value: 4}, ], }; \ No newline at end of file diff --git a/client/src/layout/AppLayout.css b/client/src/layout/AppLayout.css index 51f34da..da65b17 100644 --- a/client/src/layout/AppLayout.css +++ b/client/src/layout/AppLayout.css @@ -1,9 +1,15 @@ #appContainer { - min-height: 100dvh; /* use dvh to avoid mobile 100vh bugs */ - width: 100vw; + height: 100dvh; /* use dvh to avoid mobile 100vh bugs */ + width: 100dvw; overflow: hidden; /* optional: hide scrollbars for full-bleed canvases */ display: flex; /* if you have header/footer layout */ flex-direction: column; - background-color: #017371; color: white; + flex: 1; + background: + radial-gradient(closest-side at 50% 50%, + rgba(0,0,0,0) 65%, + rgba(0,0,0,0.08) 100%), #fff; + + } \ No newline at end of file diff --git a/client/src/layout/AppLayout.tsx b/client/src/layout/AppLayout.tsx index 3f04eb3..468e709 100644 --- a/client/src/layout/AppLayout.tsx +++ b/client/src/layout/AppLayout.tsx @@ -6,7 +6,7 @@ const AppLayout = () => { return (
- +
) } diff --git a/client/src/pages/Landing.tsx b/client/src/pages/Landing.tsx index e5be076..84632bf 100644 --- a/client/src/pages/Landing.tsx +++ b/client/src/pages/Landing.tsx @@ -3,9 +3,7 @@ import { data } from "../data/dummy-data.ts"; const Landing = () => { return ( -
-
) } diff --git a/client/src/types/network-graph.types.ts b/client/src/types/network-graph.types.ts index edcbae2..b9583ab 100644 --- a/client/src/types/network-graph.types.ts +++ b/client/src/types/network-graph.types.ts @@ -2,6 +2,8 @@ import type { SimulationNodeDatum, SimulationLinkDatum } from "d3"; export interface Node extends SimulationNodeDatum { id: string; + group: Group; + value: number; } export type Link = SimulationLinkDatum & { @@ -11,4 +13,6 @@ export type Link = SimulationLinkDatum & { export type NetworkGraphData = { nodes: Node[]; links: Link[]; -}; \ No newline at end of file +}; + +export type Group = 'people' | 'mail' | 'file' | 'issue'; \ No newline at end of file From 8ee9935ddb77812ce18c03364f94993a27d7bc78 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Sun, 12 Oct 2025 19:25:20 -1000 Subject: [PATCH 08/29] Black nodes now have white text, while other nodes have black text. --- client/src/components/NetworkGraph/NetworkGraph.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/src/components/NetworkGraph/NetworkGraph.tsx b/client/src/components/NetworkGraph/NetworkGraph.tsx index 42c7c62..f030568 100644 --- a/client/src/components/NetworkGraph/NetworkGraph.tsx +++ b/client/src/components/NetworkGraph/NetworkGraph.tsx @@ -132,9 +132,17 @@ export const NetworkGraph = ({ data } : NetworkGraphProps ) => { context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); context.fill(); + const px = Math.round(Math.max(10, Math.min(24, node.value * nodeRadiusMultiplier * 0.6))); + context.font = `${px}px Segoe UI, Roboto, sans-serif`; context.textAlign = "center"; context.textBaseline = "middle"; - context.fillStyle = '#fff'; + + + context.fillStyle = 'black'; + if (node.group == 'people' as Group) { + context.fillStyle = 'white'; + } + context.fillText(label, node.x, node.y); }, []); From 1364eb4182a8173c0a390859e9ae9ebf103a8eb1 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Wed, 15 Oct 2025 15:13:51 -1000 Subject: [PATCH 09/29] Added IDE files to .gitignore IDE files will no longer be versioned. --- client/.gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/.gitignore b/client/.gitignore index a547bf3..e729307 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -1,3 +1,7 @@ +# --- JetBrains / IntelliJ --- +.idea/ +.idea/* + # Logs logs *.log @@ -21,4 +25,4 @@ dist-ssr *.ntvs* *.njsproj *.sln -*.sw? +*.sw? \ No newline at end of file From 6a602df09c307a19cda011ab11d57b25a35565ea Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Wed, 15 Oct 2025 18:15:33 -1000 Subject: [PATCH 10/29] Refactored project structure to better follow the guidelines of Bulletproof React App directory created. Moved routing into a dedicated router file, created an app provider, and created an index. App directory now has a 'routes' directory which contains the pages of the application, rather than being located in the 'pages' directory. Pages directory has been removed. Config directory created with path config. Moved network graph component out of 'components' directory and into the new 'features' directory as it has become a very complicated component. Began using react's Query Client for managing data fetching and caching. Will likely be unused for now, but will be very helpful in the future when a back-end has been established and HTTP requests can be made to it. Components directory now has subdirectories 'ui' and 'layouts.' Any layouts and primitive components have been migrated here. --- client/package.json | 1 + client/pnpm-lock.yaml | 18 + client/src/App.css | 8 - client/src/App.tsx | 23 -- client/src/app/index.tsx | 12 + client/src/app/provider.tsx | 24 ++ client/src/app/router.tsx | 30 ++ .../Landing.tsx => app/routes/landing.tsx} | 4 +- client/src/components/Header/index.ts | 1 - .../NetworkGraph/NetworkGraphBACKUP.tsx | 339 ------------------ client/src/components/NetworkGraph/index.ts | 1 - .../layouts}/AppLayout.css | 0 .../layouts}/AppLayout.tsx | 2 +- .../src/components/{ => ui}/Header/Header.css | 0 .../src/components/{ => ui}/Header/Header.tsx | 0 client/src/components/ui/Header/index.ts | 1 + client/src/config/paths.ts | 13 + client/src/data/dummy-data.ts | 4 +- .../NetworkGraph/NetworkGraph.css | 0 .../NetworkGraph/NetworkGraph.tsx | 0 client/src/features/NetworkGraph/index.ts | 1 + .../NetworkGraph/types.ts | 0 client/src/index.css | 11 +- client/src/lib/react-query.ts | 10 + client/src/main.tsx | 9 +- 25 files changed, 127 insertions(+), 385 deletions(-) delete mode 100644 client/src/App.css delete mode 100644 client/src/App.tsx create mode 100644 client/src/app/index.tsx create mode 100644 client/src/app/provider.tsx create mode 100644 client/src/app/router.tsx rename client/src/{pages/Landing.tsx => app/routes/landing.tsx} (50%) delete mode 100644 client/src/components/Header/index.ts delete mode 100644 client/src/components/NetworkGraph/NetworkGraphBACKUP.tsx delete mode 100644 client/src/components/NetworkGraph/index.ts rename client/src/{layout => components/layouts}/AppLayout.css (100%) rename client/src/{layout => components/layouts}/AppLayout.tsx (83%) rename client/src/components/{ => ui}/Header/Header.css (100%) rename client/src/components/{ => ui}/Header/Header.tsx (100%) create mode 100644 client/src/components/ui/Header/index.ts create mode 100644 client/src/config/paths.ts rename client/src/{components => features}/NetworkGraph/NetworkGraph.css (100%) rename client/src/{components => features}/NetworkGraph/NetworkGraph.tsx (100%) create mode 100644 client/src/features/NetworkGraph/index.ts rename client/src/{components => features}/NetworkGraph/types.ts (100%) create mode 100644 client/src/lib/react-query.ts diff --git a/client/package.json b/client/package.json index c7ce3c5..436985f 100644 --- a/client/package.json +++ b/client/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@tanstack/react-query": "^5.90.3", "d3": "^7.9.0", "react": "^19.1.1", "react-dom": "^19.1.1", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 385a106..9dbab9c 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@tanstack/react-query': + specifier: ^5.90.3 + version: 5.90.3(react@19.1.1) d3: specifier: ^7.9.0 version: 7.9.0 @@ -489,6 +492,14 @@ packages: cpu: [x64] os: [win32] + '@tanstack/query-core@5.90.3': + resolution: {integrity: sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA==} + + '@tanstack/react-query@5.90.3': + resolution: {integrity: sha512-i/LRL6DtuhG6bjGzavIMIVuKKPWx2AnEBIsBfuMm3YoHne0a20nWmsatOCBcVSaT0/8/5YFjNkebHAPLVUSi0Q==} + peerDependencies: + react: ^18 || ^19 + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1729,6 +1740,13 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.50.1': optional: true + '@tanstack/query-core@5.90.3': {} + + '@tanstack/react-query@5.90.3(react@19.1.1)': + dependencies: + '@tanstack/query-core': 5.90.3 + react: 19.1.1 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.4 diff --git a/client/src/App.css b/client/src/App.css deleted file mode 100644 index 639cb8d..0000000 --- a/client/src/App.css +++ /dev/null @@ -1,8 +0,0 @@ -html, body, #root { - height: 100%; -} - -html, body { - margin: 0; /* kills the 8px white border */ - padding: 0; -} diff --git a/client/src/App.tsx b/client/src/App.tsx deleted file mode 100644 index 14c9967..0000000 --- a/client/src/App.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { lazy, Suspense } from "react"; -import { Route, Routes } from "react-router-dom"; -import './App.css' -import AppLayout from "./layout/AppLayout.tsx"; - -const Landing = lazy(() => import("./pages/Landing")); - -function App() { - - return ( - <> - - - }> - } /> - - - - - ) -} - -export default App diff --git a/client/src/app/index.tsx b/client/src/app/index.tsx new file mode 100644 index 0000000..8f834b3 --- /dev/null +++ b/client/src/app/index.tsx @@ -0,0 +1,12 @@ +import { AppProvider} from "./provider.tsx"; +import { AppRouter } from './router.tsx'; + +export const App = ()=> { + return ( + <> + + + + + ) +} diff --git a/client/src/app/provider.tsx b/client/src/app/provider.tsx new file mode 100644 index 0000000..7f6030f --- /dev/null +++ b/client/src/app/provider.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { queryConfig } from '../lib/react-query.ts'; + +type AppProviderProps = { + children: React.ReactNode; +}; + +export const AppProvider = ({ children }: AppProviderProps) => { + const [queryClient] = React.useState( + () => + new QueryClient({ + defaultOptions: queryConfig, + }), + ); + + return ( + + + {children} + + + ); +} \ No newline at end of file diff --git a/client/src/app/router.tsx b/client/src/app/router.tsx new file mode 100644 index 0000000..c3d212d --- /dev/null +++ b/client/src/app/router.tsx @@ -0,0 +1,30 @@ +import { QueryClient, useQueryClient } from '@tanstack/react-query'; +import {createBrowserRouter, RouterProvider } from "react-router-dom"; +import { paths } from '../config/paths.ts'; +import { useMemo } from "react"; + +const convert = (queryClient: QueryClient) => (m: any) => { + const { clientLoader, clientAction, default: Component, ...rest } = m; + return { + ...rest, + loader: clientLoader?.(queryClient), + action: clientAction?.(queryClient), + Component, + }; +}; + +export const createAppRouter = (queryClient: QueryClient) => + createBrowserRouter([ + { + path: paths.home.path, + lazy: () => import('./routes/landing').then(convert(queryClient)), + } + ]); + +export const AppRouter = () => { + const queryClient = useQueryClient(); + + const router = useMemo(() => createAppRouter(queryClient), [queryClient]); + + return ; +}; \ No newline at end of file diff --git a/client/src/pages/Landing.tsx b/client/src/app/routes/landing.tsx similarity index 50% rename from client/src/pages/Landing.tsx rename to client/src/app/routes/landing.tsx index 84632bf..38ac62b 100644 --- a/client/src/pages/Landing.tsx +++ b/client/src/app/routes/landing.tsx @@ -1,5 +1,5 @@ -import { NetworkGraph} from "../components/NetworkGraph"; -import { data } from "../data/dummy-data.ts"; +import { NetworkGraph } from "../../features/NetworkGraph"; +import { data } from "../../data/dummy-data.ts"; const Landing = () => { return ( diff --git a/client/src/components/Header/index.ts b/client/src/components/Header/index.ts deleted file mode 100644 index 64f7c87..0000000 --- a/client/src/components/Header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Header'; \ No newline at end of file diff --git a/client/src/components/NetworkGraph/NetworkGraphBACKUP.tsx b/client/src/components/NetworkGraph/NetworkGraphBACKUP.tsx deleted file mode 100644 index 35388e9..0000000 --- a/client/src/components/NetworkGraph/NetworkGraphBACKUP.tsx +++ /dev/null @@ -1,339 +0,0 @@ -// import { useRef, useEffect, useCallback } from 'react'; -// import './NetworkGraph.css'; -// import { -// forceSimulation, -// select, -// drag, -// pointer, -// forceCollide, -// forceManyBody, -// forceLink, -// forceCenter, -// type ForceLink -// } from "d3"; -// import type { Simulation } from "d3"; -// import type { Link, Node } from "../../types/network-graph.types.ts"; -// import type { NetworkGraphProps } from "./types.ts"; -// -// export const NetworkGraph = ({ data } : NetworkGraphProps ) => { -// const canvasRef = useRef(null); -// -// const simulationArrayRef = useRef>>(null); -// const simulationRef = useRef>(null); -// -// const peopleNodes: Node[] = data.nodes.filter(n => n.group === "people").map(n => ({ ...n })); -// const mailNodes: Node[] = data.nodes.filter(n => n.group === "mail").map(n => ({ ...n })); -// const fileNodes: Node[] = data.nodes.filter(n => n.group === "file").map(n => ({ ...n })); -// const issueNodes: Node[] = data.nodes.filter(n => n.group === "issue").map(n => ({ ...n })); -// -// -// const links: Link[] = data.links.map((d) => ({ ...d })); -// const nodes: Node[] = data.nodes.map((d) => ({ ...d })); -// -// const radius = 30; -// const forceStrength = -10; -// const nodeRadiusMultiplier = 10; -// -// if (!simulationArrayRef.current) { -// simulationArrayRef.current = new Array>(); -// simulationArrayRef.current.push(forceSimulation(peopleNodes)); -// simulationArrayRef.current.push(forceSimulation(mailNodes)); -// simulationArrayRef.current.push(forceSimulation(fileNodes)); -// simulationArrayRef.current.push(forceSimulation(issueNodes)); -// } -// -// -// -// // // New draw function called on every tick of a simulation -// // const newDrawGraph = useCallback((context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => { -// // context.clearRect(0, 0, canvas.width, canvas.height); -// // if (!simulationArrayRef.current) return; -// // -// // links.forEach((link) => { -// // if (!isNode(link.source) || !isNode(link.target)) return; -// // -// // const s = link.source, t = link.target; -// // if (!hasPos(s) || !hasPos(t)) return; -// // -// // context.beginPath(); -// // context.moveTo(s.x, s.y); -// // context.lineTo(t.x, t.y); -// // context.stroke(); -// // context.strokeStyle = "green"; -// // }); -// // -// // peopleNodes.forEach((node) => { -// // if (!node.x || !node.y) { -// // return; -// // } -// // -// // context.beginPath(); -// // context.moveTo(node.x + radius, node.y); -// // context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); -// // context.fillStyle = 'black'; -// // context.fill(); -// // }); -// // -// // mailNodes.forEach((node) => { -// // if (!node.x || !node.y) { -// // return; -// // } -// // -// // context.beginPath(); -// // context.moveTo(node.x + radius, node.y); -// // context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); -// // context.fillStyle = 'LightBlue'; -// // context.fill(); -// // }); -// // -// // fileNodes.forEach((node) => { -// // if (!node.x || !node.y) { -// // return; -// // } -// // -// // context.beginPath(); -// // context.moveTo(node.x + radius, node.y); -// // context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); -// // context.fillStyle = 'yellow'; -// // context.fill(); -// // }); -// // -// // issueNodes.forEach((node) => { -// // if (!node.x || !node.y) { -// // return; -// // } -// // -// // context.beginPath(); -// // context.moveTo(node.x + radius, node.y); -// // context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); -// // context.fillStyle = 'blue'; -// // context.fill(); -// // }); -// -// -// -// -// // nodes.forEach((node) => { -// // if (!node.x || !node.y) { -// // return; -// // } -// // -// // context.beginPath(); -// // context.moveTo(node.x + radius, node.y); -// // context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); -// // -// // switch (node.group) { -// // case "mail": -// // context.fillStyle = 'LightBlue'; -// // break; -// // case "issue": -// // context.fillStyle = 'blue'; -// // break; -// // case "people": -// // context.fillStyle = 'black'; -// // break; -// // case "file": -// // context.fillStyle = 'yellow'; -// // } -// // -// // context.fill(); -// // -// // }); -// -// // }, []); -// -// -// // Apply forces for each simulation -// // useEffect(() => { -// // if (!simulationArrayRef.current) return; -// // -// // if (!canvasRef.current) return; -// // const canvas = canvasRef.current; -// // const context = canvas.getContext("2d"); -// // -// // const container = canvas.parentElement; -// // if (!container) return; -// // -// // if (!context) return; -// // -// // simulationArrayRef.current.forEach((simulation) => { -// // simulation -// // .force('collide', forceCollide().radius(radius)) -// // .force('charge', forceManyBody().strength(forceStrength)) -// // .force('center', forceCenter(canvas.width/2, canvas.height/2)) -// // .on('tick', () => { -// // newDrawGraph(context, canvas) -// // if (!simulationRef.current) return; -// // simulationRef.current.force('center', null as any); -// // simulationRef.current.force('link', null as any); -// // }); -// // } -// // ); -// // -// // const resize = () => { -// // const rect = container.getBoundingClientRect(); -// // canvas.width = rect.width; -// // canvas.height = rect.height; -// // }; -// // -// // const ro = new ResizeObserver(resize); -// // ro.observe(container); -// // resize(); -// // -// // return () => ro.disconnect(); -// // }, [canvasRef, simulationArrayRef]); -// -// if (!simulationRef.current) { -// simulationRef.current = forceSimulation(nodes).force( -// 'link', -// forceLink(links).id((d) => d.id) -// ) -// .force('collide', forceCollide().radius(radius)) -// .force('charge', forceManyBody().strength(forceStrength)); -// } -// -// // Helper function called every tick of the simulation, draws the nodes and links at their calculated position on that tick -// const drawGraph = useCallback((context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => { -// context.clearRect(0, 0, canvas.width, canvas.height); -// if (!simulationRef.current) return; -// -// links.forEach((link) => { -// if (!isNode(link.source) || !isNode(link.target)) return; -// -// const s = link.source, t = link.target; -// if (!hasPos(s) || !hasPos(t)) return; -// -// context.beginPath(); -// context.moveTo(s.x, s.y); -// context.lineTo(t.x, t.y); -// context.stroke(); -// context.strokeStyle = "green"; -// }); -// -// nodes.forEach((node) => { -// if (!node.x || !node.y) { -// return; -// } -// -// context.beginPath(); -// context.moveTo(node.x + radius, node.y); -// context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); -// -// switch (node.group) { -// case "mail": -// context.fillStyle = 'LightBlue'; -// break; -// case "issue": -// context.fillStyle = 'blue'; -// break; -// case "people": -// context.fillStyle = 'black'; -// break; -// case "file": -// context.fillStyle = 'yellow'; -// } -// -// context.fill(); -// -// }); -// -// }, []); -// -// useEffect(() => { -// -// const canvas = canvasRef.current; -// if (!canvas) return; -// -// const container = canvas.parentElement; -// if (!container) return; -// -// if (!simulationRef.current) return; -// simulationRef.current.force('center', forceCenter(canvas.width/2, canvas.height/2)); -// -// const resize = () => { -// const rect = container.getBoundingClientRect(); -// canvas.width = rect.width; -// canvas.height = rect.height; -// }; -// -// const ro = new ResizeObserver(resize); -// ro.observe(container); -// resize(); -// -// return () => ro.disconnect(); -// }, [canvasRef]); -// // -// -// useEffect(() => { -// const canvas = canvasRef.current; -// if (!canvas) return; -// -// const context = canvas.getContext("2d"); -// if (!context) return; -// -// if (!simulationRef.current) return; -// -// -// -// simulationRef.current.on('tick', () => { -// drawGraph(context, canvas) -// if (!simulationRef.current) return; -// simulationRef.current.force('center', null as any); -// simulationRef.current.force('link', null as any); -// }); -// -// const dragBehavior = drag() -// .subject((event) => { -// const [x, y] = pointer(event, canvas); -// if (!simulationRef.current) return; -// -// // find nearest node within ~2*radius -// const n = simulationRef.current.find(x, y, radius * 2) as Node | undefined; -// -// if (n) { -// n.fx = n.x ?? x; -// n.fy = n.y ?? y; -// } -// -// return n; -// }) -// .on('drag', (event) => { -// if (!simulationRef.current) return; -// const n = event.subject; -// n.fx = event.x; -// n.fy = event.y; -// -// if (simulationRef.current.alpha() < 0.03) simulationRef.current.alpha(0.03).restart(); -// }) -// .on('end', (event) => { -// if (!simulationRef.current) return; -// -// const n = event.subject; -// n.fx = null; -// n.fy = null; -// -// const linkForce = simulationRef.current.force>('link'); -// if (linkForce) { -// linkForce.strength(0.1); -// } -// -// simulationRef.current.alpha(Math.max(simulationRef.current.alpha(), 0.05)).alphaTarget(0); -// }); -// -// select(canvas).call(dragBehavior as any); -// -// -// }, [canvasRef, nodes, links]); -// -// return ( -// -// ) -// } -// -// function isNode(v: Link["source"]): v is Node { -// return typeof v === "object" && v !== null; -// } -// -// function hasPos(n: Node): n is Node & { x: number; y: number } { -// return n.x != null && n.y != null; -// } diff --git a/client/src/components/NetworkGraph/index.ts b/client/src/components/NetworkGraph/index.ts deleted file mode 100644 index d761ad5..0000000 --- a/client/src/components/NetworkGraph/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './NetworkGraph'; \ No newline at end of file diff --git a/client/src/layout/AppLayout.css b/client/src/components/layouts/AppLayout.css similarity index 100% rename from client/src/layout/AppLayout.css rename to client/src/components/layouts/AppLayout.css diff --git a/client/src/layout/AppLayout.tsx b/client/src/components/layouts/AppLayout.tsx similarity index 83% rename from client/src/layout/AppLayout.tsx rename to client/src/components/layouts/AppLayout.tsx index 468e709..ad1cc2c 100644 --- a/client/src/layout/AppLayout.tsx +++ b/client/src/components/layouts/AppLayout.tsx @@ -1,5 +1,5 @@ import { Outlet } from "react-router-dom"; -import { Header } from "../components/Header"; +import { Header } from "../ui/Header"; import "./AppLayout.css"; const AppLayout = () => { diff --git a/client/src/components/Header/Header.css b/client/src/components/ui/Header/Header.css similarity index 100% rename from client/src/components/Header/Header.css rename to client/src/components/ui/Header/Header.css diff --git a/client/src/components/Header/Header.tsx b/client/src/components/ui/Header/Header.tsx similarity index 100% rename from client/src/components/Header/Header.tsx rename to client/src/components/ui/Header/Header.tsx diff --git a/client/src/components/ui/Header/index.ts b/client/src/components/ui/Header/index.ts new file mode 100644 index 0000000..07df1bf --- /dev/null +++ b/client/src/components/ui/Header/index.ts @@ -0,0 +1 @@ +export * from './Header.tsx'; \ No newline at end of file diff --git a/client/src/config/paths.ts b/client/src/config/paths.ts new file mode 100644 index 0000000..7ef3c7f --- /dev/null +++ b/client/src/config/paths.ts @@ -0,0 +1,13 @@ +export const paths = { + home: { + path: '/', + getHref: () => '/', + }, + + app: { + root: { + path: '/app', + getHref: () => '/app', + }, + } +} \ No newline at end of file diff --git a/client/src/data/dummy-data.ts b/client/src/data/dummy-data.ts index 6093144..d1e6550 100644 --- a/client/src/data/dummy-data.ts +++ b/client/src/data/dummy-data.ts @@ -1,3 +1,5 @@ +import type {NetworkGraphData} from "../types/network-graph.types.ts"; + export const data = { nodes: [ { id: 'person_1', group: 'people', value: 2}, @@ -78,4 +80,4 @@ export const data = { { source: 'file_23', target: 'issue_1', value: 4}, { source: 'file_23', target: 'file_24', value: 4}, ], -}; \ No newline at end of file +} as NetworkGraphData; \ No newline at end of file diff --git a/client/src/components/NetworkGraph/NetworkGraph.css b/client/src/features/NetworkGraph/NetworkGraph.css similarity index 100% rename from client/src/components/NetworkGraph/NetworkGraph.css rename to client/src/features/NetworkGraph/NetworkGraph.css diff --git a/client/src/components/NetworkGraph/NetworkGraph.tsx b/client/src/features/NetworkGraph/NetworkGraph.tsx similarity index 100% rename from client/src/components/NetworkGraph/NetworkGraph.tsx rename to client/src/features/NetworkGraph/NetworkGraph.tsx diff --git a/client/src/features/NetworkGraph/index.ts b/client/src/features/NetworkGraph/index.ts new file mode 100644 index 0000000..43375e5 --- /dev/null +++ b/client/src/features/NetworkGraph/index.ts @@ -0,0 +1 @@ +export * from './NetworkGraph.tsx'; \ No newline at end of file diff --git a/client/src/components/NetworkGraph/types.ts b/client/src/features/NetworkGraph/types.ts similarity index 100% rename from client/src/components/NetworkGraph/types.ts rename to client/src/features/NetworkGraph/types.ts diff --git a/client/src/index.css b/client/src/index.css index b8252fb..00bb2b2 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,5 +1,8 @@ -#root { - display: flex; - flex-direction: column; - background-color: #242424; +html, body, #root { + height: 100%; } + +html, body { + margin: 0; /* kills the 8px white border */ + padding: 0; +} \ No newline at end of file diff --git a/client/src/lib/react-query.ts b/client/src/lib/react-query.ts new file mode 100644 index 0000000..0ef7cf8 --- /dev/null +++ b/client/src/lib/react-query.ts @@ -0,0 +1,10 @@ +import type { DefaultOptions } from '@tanstack/react-query'; + +export const queryConfig = { + queries: { + // throwOnError: true, + refetchOnWindowFocus: false, + retry: false, + staleTime: 1000 * 60, + }, +} satisfies DefaultOptions; \ No newline at end of file diff --git a/client/src/main.tsx b/client/src/main.tsx index 0bc4718..dad8c88 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,12 +1,11 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import { BrowserRouter } from 'react-router-dom'; -import App from './App.tsx'; + +import './index.css'; +import { App } from './app'; createRoot(document.getElementById('root')!).render( - - - + , ) From 90bf5d3dcab8b2df259af77952b01ddedab4651d Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Wed, 15 Oct 2025 18:28:00 -1000 Subject: [PATCH 11/29] Moved .gitignore into root, deleted .idea folder .gitignore should now apply to the entire repo instead of just client. .idea folder has been deleted. --- client/.gitignore => .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename client/.gitignore => .gitignore (93%) diff --git a/client/.gitignore b/.gitignore similarity index 93% rename from client/.gitignore rename to .gitignore index e729307..5c3ff2b 100644 --- a/client/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* -node_modules +client/node_modules dist dist-ssr *.local From 8b33f97c47e24e67ee933d45a3b513170bed2cd6 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Wed, 15 Oct 2025 18:28:37 -1000 Subject: [PATCH 12/29] Moved .gitignore into root, deleted .idea folder .gitignore should now apply to the entire repo instead of just client. .idea folder has been deleted. --- .idea/.gitignore | 8 -------- .idea/kaiaulu_react.iml | 9 --------- .idea/misc.xml | 6 ------ .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ 5 files changed, 37 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/kaiaulu_react.iml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/kaiaulu_react.iml b/.idea/kaiaulu_react.iml deleted file mode 100644 index d6ebd48..0000000 --- a/.idea/kaiaulu_react.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 639900d..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index cdedb33..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From b4d776f463ad7d0c78e77c3c005e56d957db7cf9 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Wed, 22 Oct 2025 12:11:13 -1000 Subject: [PATCH 13/29] Massive refactor of Network Graph feature Moved all logic into useEffect with only dependency being the data Dummy data now comes from a hook rather than being passed as a prop Network Graph will not properly re-render if data source changes (like if user views different repo) --- .../features/NetworkGraph/NetworkGraph.tsx | 265 ++++++++---------- client/src/features/NetworkGraph/types.ts | 6 - client/src/hooks/useDummyData.ts | 26 +- client/src/types/network-graph.types.ts | 10 +- 4 files changed, 136 insertions(+), 171 deletions(-) delete mode 100644 client/src/features/NetworkGraph/types.ts diff --git a/client/src/features/NetworkGraph/NetworkGraph.tsx b/client/src/features/NetworkGraph/NetworkGraph.tsx index f030568..1a47b11 100644 --- a/client/src/features/NetworkGraph/NetworkGraph.tsx +++ b/client/src/features/NetworkGraph/NetworkGraph.tsx @@ -1,50 +1,19 @@ -import { useRef, useEffect, useCallback } from 'react'; +import { useRef, useEffect } from 'react'; +import { useDummyData } from "../../hooks/useDummyData.ts"; import './NetworkGraph.css'; -import { - forceX, - forceY, - forceSimulation, - select, - drag, - pointer, - forceCollide, - forceManyBody, - forceLink + +import { forceX, forceY, forceSimulation, select, drag, pointer, forceCollide, forceManyBody, + forceLink, + type DragBehavior } from "d3"; + import type { Simulation } from "d3"; -import type { Link, Node, Group } from "../../types/network-graph.types.ts"; -import type { NetworkGraphProps } from "./types.ts"; +import type { Link, Node, Group, HubLink } from "../../types/network-graph.types.ts"; -export const NetworkGraph = ({ data } : NetworkGraphProps ) => { +export const NetworkGraph = () => { + const { data } = useDummyData(); const canvasRef = useRef(null); - const simulationRef = useRef>(null); - - const links: Link[] = data.links.map((d) => ({ ...d })); - const nodes: Node[] = data.nodes.map((d) => ({ ...d })); - - - // Hub nodes that serve as the center of each group, determined by size of the node - const hubs: Record = { - people: nodes.filter(n => n.group === 'people').reduce((a,b)=> a.value>b.value?a:b), - mail: nodes.filter(n => n.group === 'mail').reduce((a,b)=> a.value>b.value?a:b), - file: nodes.filter(n => n.group === 'file').reduce((a,b)=> a.value>b.value?a:b), - issue: nodes.filter(n => n.group === 'issue').reduce((a,b)=> a.value>b.value?a:b), - }; - // Helper for determining hub nodes - const isHub = useCallback((d: Node) => hubs[d.group] === d, []); - - type HubLink = { source: Node; target: Node }; - const hubLinks: HubLink[] = nodes - .filter(n => !isHub(n)) - .map(n => ({ source: hubs[n.group], target: n })); - - const radius = 40; - const forceStrength = -100; - const nodeRadiusMultiplier = 11; - const nodePadding = 6; - - // Helper that takes canvas width and height and returns center coordinate for each group const centers = (w: number, h: number) => ({ people: [w * 0.5, h * 0.4], mail: [w * 0.8, h * 0.3], @@ -52,18 +21,6 @@ export const NetworkGraph = ({ data } : NetworkGraphProps ) => { issue: [w * 0.8, h * 0.6], }); - // Initialize force simulation with nodes and links - if (!simulationRef.current) { - simulationRef.current = forceSimulation(nodes) - .force('link', forceLink(links).id((d) => d.id) - .distance(80) // tighter cluster around hub - .strength(0.05)) // stronger pull to the hub) - .force('hubLinks', forceLink(hubLinks) - .distance(40) // tighter cluster around hub - .strength(0.2) // stronger pull to the hub - ); - } - // Helper function for clearing forces function clearForces(sim: Simulation) { sim @@ -76,121 +33,146 @@ export const NetworkGraph = ({ data } : NetworkGraphProps ) => { .velocityDecay(1); // stop inertia } - // Function for drawing the network graph that is called on each tick of the simulation - const drawGraph = useCallback((context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => { - context.clearRect(0, 0, canvas.width, canvas.height); - if (!simulationRef.current) return; - - links.forEach((link) => { - if (!isNode(link.source) || !isNode(link.target)) return; - console.log("this link has a source and target"); - - const s = link.source, t = link.target; - if (!hasPos(s) || !hasPos(t)) return; + // Apply forces for simulation + useEffect(() => { + const radius = 40; + const forceStrength = -100; + const nodeRadiusMultiplier = 11; + const nodePadding = 6; - console.log(s); - console.log(t); + if (!canvasRef.current) return; - context.beginPath(); - context.moveTo(s.x, s.y); - context.lineTo(t.x, t.y); - context.stroke(); - context.strokeStyle = "grey"; - }); - - nodes.forEach((node) => { - drawNodeByGroup(node, context); - }) + const canvas = canvasRef.current; + const container = canvas.parentElement; + const c = centers(canvas.width, canvas.height); - }, []); + const links: Link[] = data.links.map((d) => ({ ...d })); + const nodes: Node[] = data.nodes.map((d) => ({ ...d })); - // Helper function for drawing a single node based on its group - const drawNodeByGroup = useCallback((node: Node, context: CanvasRenderingContext2D) => { - if (!node.x || !node.y) { - return; - } - switch (node.group) { - case "people": - context.fillStyle = 'black'; - break; - case "mail": - context.fillStyle = 'LightBlue'; - break; - case 'file': - context.fillStyle = 'yellow'; - break; - case 'issue': - context.fillStyle = 'blue'; - } + const resize = () => { + if (!container) return; + const rect = container.getBoundingClientRect(); + canvas.width = rect.width; + canvas.height = rect.height; + }; + resize(); + + // Hub nodes that serve as the center of each group, determined by size of the node + const hubs: Record = { + people: nodes.filter(n => n.group === 'people').reduce((a,b)=> a.value>b.value?a:b), + mail: nodes.filter(n => n.group === 'mail').reduce((a,b)=> a.value>b.value?a:b), + file: nodes.filter(n => n.group === 'file').reduce((a,b)=> a.value>b.value?a:b), + issue: nodes.filter(n => n.group === 'issue').reduce((a,b)=> a.value>b.value?a:b), + }; - const label = (node as Node).id ?? node.id ?? ""; - if (!label) return; + // Helper for determining hub nodes + const isHub = (d: Node) => hubs[d.group] === d; - context.beginPath(); - context.moveTo(node.x + radius, node.y); - context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); - context.fill(); + const hubLinks: HubLink[] = nodes + .filter(n => !isHub(n)) + .map(n => ({ source: hubs[n.group], target: n })); + + const physicsSimulation = forceSimulation(nodes) + .force('link', forceLink(links).id((d) => d.id) + .distance(80) // tighter cluster around hub + .strength(0.05)) // stronger pull to the hub) + .force('hubLinks', forceLink(hubLinks) + .distance(40) // tighter cluster around hub + .strength(0.2) // stronger pull to the hub + ) + .force("x", forceX(d => c[d.group][0]).strength(0.2)) + .force("y", forceY(d => c[d.group][1]).strength(0.2)) + .force('collide', forceCollide().radius(d => d.value * nodeRadiusMultiplier + nodePadding)) + .force('charge', forceManyBody().strength(forceStrength)); - const px = Math.round(Math.max(10, Math.min(24, node.value * nodeRadiusMultiplier * 0.6))); - context.font = `${px}px Segoe UI, Roboto, sans-serif`; - context.textAlign = "center"; - context.textBaseline = "middle"; + console.log("Simulation established"); + physicsSimulation.alpha(1); + for (let i = 0; i < 300; i++) physicsSimulation.tick(); + clearForces(physicsSimulation); + physicsSimulation.stop(); - context.fillStyle = 'black'; - if (node.group == 'people' as Group) { - context.fillStyle = 'white'; - } + const context = canvas.getContext("2d"); + if (!context) return; - context.fillText(label, node.x, node.y); + // Helper function for drawing a single node based on its group + const drawNodeByGroup = (node: Node, context: CanvasRenderingContext2D) => { + if (!node.x || !node.y) { + return; + } + + switch (node.group) { + case "people": + context.fillStyle = 'black'; + break; + case "mail": + context.fillStyle = 'LightBlue'; + break; + case 'file': + context.fillStyle = 'yellow'; + break; + case 'issue': + context.fillStyle = 'blue'; + } + + const label = (node as Node).id ?? node.id ?? ""; + if (!label) return; - }, []); + context.beginPath(); + context.moveTo(node.x + radius, node.y); + context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); + context.fill(); + const px = Math.round(Math.max(10, Math.min(24, node.value * nodeRadiusMultiplier * 0.6))); + context.font = `${px}px Segoe UI, Roboto, sans-serif`; + context.textAlign = "center"; + context.textBaseline = "middle"; - // Apply forces for simulation - useEffect(() => { - if (!simulationRef.current || !canvasRef.current) return; - const canvas = canvasRef.current; - const container = canvas.parentElement; - if (!container) return; + context.fillStyle = 'black'; + if (node.group == 'people' as Group) { + context.fillStyle = 'white'; + } - const context = canvas.getContext("2d"); - if (!context) return; + context.fillText(label, node.x, node.y); - const resize = () => { - const rect = container.getBoundingClientRect(); - canvas.width = rect.width; - canvas.height = rect.height; }; - resize(); - const c = centers(canvas.width, canvas.height); + // Function for drawing the network graph that is called on each tick of the simulation + const drawGraph = (context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => { + context.clearRect(0, 0, canvas.width, canvas.height); - simulationRef.current - .force("x", forceX(d => c[d.group][0]).strength(0.2)) - .force("y", forceY(d => c[d.group][1]).strength(0.2)) - .force('collide', forceCollide().radius(d => d.value * nodeRadiusMultiplier + nodePadding)) - .force('charge', forceManyBody().strength(forceStrength)); + links.forEach((link) => { + if (!isNode(link.source) || !isNode(link.target)) return; + console.log("this link has a source and target"); - simulationRef.current.alpha(1); - for (let i = 0; i < 300; i++) simulationRef.current.tick(); + const s = link.source, t = link.target; + if (!hasPos(s) || !hasPos(t)) return; - drawGraph(context, canvas); - clearForces(simulationRef.current); + console.log(s); + console.log(t); + + context.beginPath(); + context.moveTo(s.x, s.y); + context.lineTo(t.x, t.y); + context.stroke(); + context.strokeStyle = "grey"; + }); - simulationRef.current.on("tick", null); - simulationRef.current.stop(); + nodes.forEach((node) => { + drawNodeByGroup(node, context); + }) + }; + drawGraph(context, canvas); const dragBehavior = drag() .subject((event) => { const [x, y] = pointer(event, canvas); - if (!simulationRef.current) return; // find nearest node within ~2*radius - const n = simulationRef.current.find(x, y, radius) as Node | undefined; + const n = physicsSimulation.find(x, y, radius) as Node | undefined; if (n) { n.fx = n.x ?? x; @@ -200,23 +182,24 @@ export const NetworkGraph = ({ data } : NetworkGraphProps ) => { return n; }) .on('drag', (event) => { - if (!simulationRef.current) return; const n = event.subject; n.x = event.x; n.y = event.y; drawGraph(context, canvas); }); - select(canvas).call(dragBehavior as any); + select(canvas).call(dragBehavior as DragBehavior); const ro = new ResizeObserver(() => { resize(); drawGraph(context, canvas); }) + if (!container) return; ro.observe(container); return () => ro.disconnect(); - }, [canvasRef, simulationRef]); + + }, [data]); return ( diff --git a/client/src/features/NetworkGraph/types.ts b/client/src/features/NetworkGraph/types.ts deleted file mode 100644 index d62d3ac..0000000 --- a/client/src/features/NetworkGraph/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { NetworkGraphData } from "../../types/network-graph.types.ts"; - -export type NetworkGraphProps = { - data: NetworkGraphData; -}; - diff --git a/client/src/hooks/useDummyData.ts b/client/src/hooks/useDummyData.ts index 0d22a3f..a81073c 100644 --- a/client/src/hooks/useDummyData.ts +++ b/client/src/hooks/useDummyData.ts @@ -1,25 +1,5 @@ -import * as d3 from "d3"; -import { useEffect, useState } from "react"; +import { data } from '../data/dummy-data.ts'; -export function useDummyData() { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - let ignore = false; - (async () => { - try { - const json = await d3.json("/sample-data.json"); - if (!ignore) setData(json ?? null); - } catch (e: any) { - if (!ignore) setError(e?.message ?? "Failed to load data"); - } finally { - if (!ignore) setLoading(false); - } - })(); - return () => { ignore = true; }; - }, []); - - return { data, loading, error }; +export function useDummyData() { + return { data }; } diff --git a/client/src/types/network-graph.types.ts b/client/src/types/network-graph.types.ts index b9583ab..04dded1 100644 --- a/client/src/types/network-graph.types.ts +++ b/client/src/types/network-graph.types.ts @@ -15,4 +15,12 @@ export type NetworkGraphData = { links: Link[]; }; -export type Group = 'people' | 'mail' | 'file' | 'issue'; \ No newline at end of file +export type Group = 'people' | 'mail' | 'file' | 'issue'; + +export type NetworkGraphProps = { + data: NetworkGraphData; +}; + +export type HubLink = { + source: Node; target: Node +}; \ No newline at end of file From efe50d1e41e6f233cb5ec98942f18615e141a8c9 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Wed, 22 Oct 2025 12:44:02 -1000 Subject: [PATCH 14/29] Matched node colors to kaiaulu API examples Used color picker to grab hex values of corresponding nodes on API webpage and applied them to the nodes in the Network Graph. --- client/src/features/NetworkGraph/NetworkGraph.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/src/features/NetworkGraph/NetworkGraph.tsx b/client/src/features/NetworkGraph/NetworkGraph.tsx index 1a47b11..d9fb259 100644 --- a/client/src/features/NetworkGraph/NetworkGraph.tsx +++ b/client/src/features/NetworkGraph/NetworkGraph.tsx @@ -49,7 +49,6 @@ export const NetworkGraph = () => { const links: Link[] = data.links.map((d) => ({ ...d })); const nodes: Node[] = data.nodes.map((d) => ({ ...d })); - const resize = () => { if (!container) return; const rect = container.getBoundingClientRect(); @@ -107,13 +106,13 @@ export const NetworkGraph = () => { context.fillStyle = 'black'; break; case "mail": - context.fillStyle = 'LightBlue'; + context.fillStyle = '#add8e6'; break; case 'file': - context.fillStyle = 'yellow'; + context.fillStyle = '#fafad2'; break; case 'issue': - context.fillStyle = 'blue'; + context.fillStyle = '#0052cc'; } const label = (node as Node).id ?? node.id ?? ""; From 7f7a2b544fb5745f6b8eb0a5372d8ba36387d15f Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:22:06 -1000 Subject: [PATCH 15/29] Added double-click interaction for nodes Implemented double-click feature for nodes. Working on highlighting. --- .../features/NetworkGraph/NetworkGraph.tsx | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/client/src/features/NetworkGraph/NetworkGraph.tsx b/client/src/features/NetworkGraph/NetworkGraph.tsx index d9fb259..3cfa621 100644 --- a/client/src/features/NetworkGraph/NetworkGraph.tsx +++ b/client/src/features/NetworkGraph/NetworkGraph.tsx @@ -56,7 +56,7 @@ export const NetworkGraph = () => { canvas.height = rect.height; }; resize(); - + // Hub nodes that serve as the center of each group, determined by size of the node const hubs: Record = { people: nodes.filter(n => n.group === 'people').reduce((a,b)=> a.value>b.value?a:b), @@ -71,11 +71,11 @@ export const NetworkGraph = () => { const hubLinks: HubLink[] = nodes .filter(n => !isHub(n)) .map(n => ({ source: hubs[n.group], target: n })); - + const physicsSimulation = forceSimulation(nodes) .force('link', forceLink(links).id((d) => d.id) .distance(80) // tighter cluster around hub - .strength(0.05)) // stronger pull to the hub) + .strength(0.05)) // stronger pull to the hub .force('hubLinks', forceLink(hubLinks) .distance(40) // tighter cluster around hub .strength(0.2) // stronger pull to the hub @@ -85,7 +85,6 @@ export const NetworkGraph = () => { .force('collide', forceCollide().radius(d => d.value * nodeRadiusMultiplier + nodePadding)) .force('charge', forceManyBody().strength(forceStrength)); - console.log("Simulation established"); physicsSimulation.alpha(1); for (let i = 0; i < 300; i++) physicsSimulation.tick(); @@ -124,7 +123,7 @@ export const NetworkGraph = () => { context.fill(); const px = Math.round(Math.max(10, Math.min(24, node.value * nodeRadiusMultiplier * 0.6))); - context.font = `${px}px Segoe UI, Roboto, sans-serif`; + context.font = `${px}px Roboto, sans-serif`; context.textAlign = "center"; context.textBaseline = "middle"; @@ -138,20 +137,16 @@ export const NetworkGraph = () => { }; - // Function for drawing the network graph that is called on each tick of the simulation + // Function for drawing the network graph const drawGraph = (context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => { context.clearRect(0, 0, canvas.width, canvas.height); links.forEach((link) => { if (!isNode(link.source) || !isNode(link.target)) return; - console.log("this link has a source and target"); const s = link.source, t = link.target; if (!hasPos(s) || !hasPos(t)) return; - console.log(s); - console.log(t); - context.beginPath(); context.moveTo(s.x, s.y); context.lineTo(t.x, t.y); @@ -164,12 +159,12 @@ export const NetworkGraph = () => { }) }; + drawGraph(context, canvas); const dragBehavior = drag() .subject((event) => { const [x, y] = pointer(event, canvas); - // find nearest node within ~2*radius const n = physicsSimulation.find(x, y, radius) as Node | undefined; @@ -189,6 +184,42 @@ export const NetworkGraph = () => { select(canvas).call(dragBehavior as DragBehavior); + + // Finds the topmost node under (x,y) + const findNodeAt = (x: number, y: number): Node | undefined => { + // iterate in reverse draw order so on top wins + for (let i = nodes.length - 1; i >= 0; i--) { + const n = nodes[i]; + if (!hasPos(n)) continue; + const r = n.value * nodeRadiusMultiplier; + const dx = x - n.x!; + const dy = y - n.y!; + if (dx*dx + dy*dy <= r*r) return n; + } + return undefined; + }; + + // Double-click handler + const onNodeDoubleClick = (node: Node) => { + if (!hasPos(node)) return; + context.fillStyle = 'green'; + context.beginPath(); + context.moveTo(node.x + radius, node.y); + context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); + context.fill(); + }; + + // Double-click listener + const handleDblClick = (event: MouseEvent) => { + event.preventDefault(); + const [x, y] = pointer(event, canvas); + const hit = findNodeAt(x, y); + if (hit) onNodeDoubleClick(hit); + }; + + // Attach double-click listener to canvas + select(canvas).on('dblclick', handleDblClick); + const ro = new ResizeObserver(() => { resize(); drawGraph(context, canvas); From 7194e53cd101a793787c9fd31da4e206d59ed2f5 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:40:59 -1000 Subject: [PATCH 16/29] Updated README.md Updated README.md with instructions for running the application. Changed code style of an import in dummy-data.ts --- README.md | 12 ++++++++++++ client/src/data/dummy-data.ts | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e69de29..95a1fe7 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,12 @@ +## Requires PNPM +https://pnpm.io/installation + +## Starting the Application: + + 1. Navigate into 'client' directory via terminal. + + 2. Once inside, run the following command: + pnpm install + + 3. After installation has completed run the following command: + pnpm run dev diff --git a/client/src/data/dummy-data.ts b/client/src/data/dummy-data.ts index d1e6550..ceeceff 100644 --- a/client/src/data/dummy-data.ts +++ b/client/src/data/dummy-data.ts @@ -1,4 +1,4 @@ -import type {NetworkGraphData} from "../types/network-graph.types.ts"; +import type { NetworkGraphData } from "../types/network-graph.types.ts"; export const data = { nodes: [ From 3288b7c3d07fac615aa7d0abaa3826c9bd7fecda Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Wed, 22 Oct 2025 18:05:49 -1000 Subject: [PATCH 17/29] Added a simple highlight functionality Double-clicking a node will highlight it and grey out other nodes. Double-clicking it again will exit highlight mode. Highlighting of related nodes not yet implemented. --- .../features/NetworkGraph/NetworkGraph.tsx | 94 +++++++++++++------ 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/client/src/features/NetworkGraph/NetworkGraph.tsx b/client/src/features/NetworkGraph/NetworkGraph.tsx index 3cfa621..55e4590 100644 --- a/client/src/features/NetworkGraph/NetworkGraph.tsx +++ b/client/src/features/NetworkGraph/NetworkGraph.tsx @@ -49,6 +49,12 @@ export const NetworkGraph = () => { const links: Link[] = data.links.map((d) => ({ ...d })); const nodes: Node[] = data.nodes.map((d) => ({ ...d })); + const nodeHighlightMap = new Map(); + + data.nodes.forEach((p) => { + nodeHighlightMap.set(p.id, { highlightStatus: 0 }); + }); + const resize = () => { if (!container) return; const rect = container.getBoundingClientRect(); @@ -65,13 +71,14 @@ export const NetworkGraph = () => { issue: nodes.filter(n => n.group === 'issue').reduce((a,b)=> a.value>b.value?a:b), }; - // Helper for determining hub nodes + // Helper for determining if a node is a hub node const isHub = (d: Node) => hubs[d.group] === d; const hubLinks: HubLink[] = nodes .filter(n => !isHub(n)) .map(n => ({ source: hubs[n.group], target: n })); + // Initialize forces for physics simulation const physicsSimulation = forceSimulation(nodes) .force('link', forceLink(links).id((d) => d.id) .distance(80) // tighter cluster around hub @@ -85,21 +92,23 @@ export const NetworkGraph = () => { .force('collide', forceCollide().radius(d => d.value * nodeRadiusMultiplier + nodePadding)) .force('charge', forceManyBody().strength(forceStrength)); - + // Reset the simulation physicsSimulation.alpha(1); + + // Run the simulation for some time and then stop it for (let i = 0; i < 300; i++) physicsSimulation.tick(); clearForces(physicsSimulation); physicsSimulation.stop(); + // Grab context (used for drawing) from canvas const context = canvas.getContext("2d"); if (!context) return; // Helper function for drawing a single node based on its group const drawNodeByGroup = (node: Node, context: CanvasRenderingContext2D) => { - if (!node.x || !node.y) { - return; - } + if (!hasPos(node)) return; + // Sets color of node according to group switch (node.group) { case "people": context.fillStyle = 'black'; @@ -114,27 +123,38 @@ export const NetworkGraph = () => { context.fillStyle = '#0052cc'; } + + // Creates a text label for the node const label = (node as Node).id ?? node.id ?? ""; if (!label) return; + // Draws the node + context.save(); + if (nodeHighlightMap.get(node.id).highlightStatus == -1) { + context.globalAlpha = 0.2; + } + context.beginPath(); context.moveTo(node.x + radius, node.y); context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); context.fill(); + + // Set text attributes const px = Math.round(Math.max(10, Math.min(24, node.value * nodeRadiusMultiplier * 0.6))); context.font = `${px}px Roboto, sans-serif`; context.textAlign = "center"; context.textBaseline = "middle"; - - context.fillStyle = 'black'; + + // If the node is of group people, then set text color to white (to contrast the black node) if (node.group == 'people' as Group) { context.fillStyle = 'white'; } + // Draws the text context.fillText(label, node.x, node.y); - + context.restore(); }; // Function for drawing the network graph @@ -162,11 +182,26 @@ export const NetworkGraph = () => { drawGraph(context, canvas); + // Finds the topmost node under (x,y) + const findNodeAt = (x: number, y: number): Node | undefined => { + // iterate in reverse draw order so on top wins + for (let i = nodes.length - 1; i >= 0; i--) { + const n = nodes[i]; + if (!hasPos(n)) continue; + const r = n.value * nodeRadiusMultiplier; + const dx = x - n.x!; + const dy = y - n.y!; + if (dx*dx + dy*dy <= r*r) return n; + } + return undefined; + }; + const dragBehavior = drag() .subject((event) => { const [x, y] = pointer(event, canvas); - // find nearest node within ~2*radius - const n = physicsSimulation.find(x, y, radius) as Node | undefined; + + // find nearest node within radius + const n = findNodeAt(x, y); if (n) { n.fx = n.x ?? x; @@ -184,31 +219,30 @@ export const NetworkGraph = () => { select(canvas).call(dragBehavior as DragBehavior); - - // Finds the topmost node under (x,y) - const findNodeAt = (x: number, y: number): Node | undefined => { - // iterate in reverse draw order so on top wins - for (let i = nodes.length - 1; i >= 0; i--) { - const n = nodes[i]; - if (!hasPos(n)) continue; - const r = n.value * nodeRadiusMultiplier; - const dx = x - n.x!; - const dy = y - n.y!; - if (dx*dx + dy*dy <= r*r) return n; - } - return undefined; - }; - // Double-click handler const onNodeDoubleClick = (node: Node) => { if (!hasPos(node)) return; - context.fillStyle = 'green'; - context.beginPath(); - context.moveTo(node.x + radius, node.y); - context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); - context.fill(); + const selectedNode = nodeHighlightMap.get(node.id); + if (selectedNode.highlightStatus == 0) { + selectedNode.highlightStatus = 1; + for (const [key, value] of nodeHighlightMap) { + if (key != node.id) { + value.highlightStatus = -1; + } + } + } else if (selectedNode.highlightStatus == 1) { + selectedNode.highlightStatus = 0; + for (const [key, value] of nodeHighlightMap) { + if (key != node.id) { + value.highlightStatus = 0; + } + } + } + + drawGraph(context, canvas); }; + // Double-click listener const handleDblClick = (event: MouseEvent) => { event.preventDefault(); From 03e03242af86cbc25ef13e330278e00e9d78a8b2 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:47:25 -1000 Subject: [PATCH 18/29] Split data into separate jsons by group, implemented related highlighting Dummy data has been split into four different jsons. Data from jsons are combined before initial render to perform physics simulation. Double-clicking a node will now highlight it along with immediately related nodes (nodes that are one link away). Double-clicking a node previously would highlight only the double-clicked node. --- client/src/data/file-dummy-data.ts | 49 +++++++++++ client/src/data/issue-dummy-data.ts | 13 +++ client/src/data/mail-dummy-data.ts | 25 ++++++ client/src/data/person-dummy-data.ts | 20 +++++ .../features/NetworkGraph/NetworkGraph.tsx | 84 ++++++++++++------- client/src/hooks/useDummyData.ts | 7 +- client/src/types/network-graph.types.ts | 2 + 7 files changed, 170 insertions(+), 30 deletions(-) create mode 100644 client/src/data/file-dummy-data.ts create mode 100644 client/src/data/issue-dummy-data.ts create mode 100644 client/src/data/mail-dummy-data.ts create mode 100644 client/src/data/person-dummy-data.ts diff --git a/client/src/data/file-dummy-data.ts b/client/src/data/file-dummy-data.ts new file mode 100644 index 0000000..f04916c --- /dev/null +++ b/client/src/data/file-dummy-data.ts @@ -0,0 +1,49 @@ +import type { NetworkGraphData } from "../types/network-graph.types.ts"; + +export const fileDummyData = { + nodes: [ + { id: 'file_1', group: 'file', value: 2}, + { id: 'file_2', group: 'file', value: 2}, + { id: 'file_3', group: 'file', value: 2}, + { id: 'file_4', group: 'file', value: 2}, + { id: 'file_5', group: 'file', value: 2}, + { id: 'file_6', group: 'file', value: 2}, + { id: 'file_7', group: 'file', value: 2}, + { id: 'file_8', group: 'file', value: 2}, + { id: 'file_9', group: 'file', value: 2}, + { id: 'file_10', group: 'file', value: 3}, + { id: 'file_11', group: 'file', value: 4}, + { id: 'file_12', group: 'file', value: 4}, + { id: 'file_13', group: 'file', value: 2}, + { id: 'file_14', group: 'file', value: 2}, + { id: 'file_15', group: 'file', value: 2}, + { id: 'file_16', group: 'file', value: 2}, + { id: 'file_17', group: 'file', value: 2}, + { id: 'file_18', group: 'file', value: 2}, + { id: 'file_19', group: 'file', value: 2}, + { id: 'file_20', group: 'file', value: 2}, + { id: 'file_21', group: 'file', value: 2}, + { id: 'file_22', group: 'file', value: 3}, + { id: 'file_23', group: 'file', value: 4}, + { id: 'file_24', group: 'file', value: 8}, + + ], + links: [ + { source: 'file_4', target: 'file_2', value: 1 }, + { source: 'file_4', target: 'file_3', value: 1 }, + { source: 'file_4', target: 'file_1', value: 1 }, + { source: 'file_4', target: 'file_6', value: 1 }, + { source: 'file_4', target: 'issue_2', value: 1 }, + { source: 'file_6', target: 'file_1', value: 1 }, + { source: 'file_6', target: 'file_3', value: 1 }, + { source: 'file_6', target: 'file_5', value: 1 }, + { source: 'file_7', target: 'file_8', value: 2}, + { source: 'file_8', target: 'file_9', value: 2}, + { source: 'file_9', target: 'file_10', value: 2}, + { source: 'file_9', target: 'file_22', value: 2}, + { source: 'file_10', target: 'file_11', value: 3}, + { source: 'file_11', target: 'file_12', value: 4}, + { source: 'file_23', target: 'issue_1', value: 4}, + { source: 'file_23', target: 'file_24', value: 4}, + ], +} as NetworkGraphData; \ No newline at end of file diff --git a/client/src/data/issue-dummy-data.ts b/client/src/data/issue-dummy-data.ts new file mode 100644 index 0000000..04dcb47 --- /dev/null +++ b/client/src/data/issue-dummy-data.ts @@ -0,0 +1,13 @@ +import type { NetworkGraphData } from "../types/network-graph.types.ts"; + +export const issueDummyData = { + nodes: [ + { id: 'issue_1', group: 'issue', value: 4}, + { id: 'issue_2', group: 'issue', value: 2}, + { id: 'issue_3', group: 'issue', value: 2}, + ], + links: [ + { source: 'issue_1', target: 'issue_2', value: 1 }, + { source: 'issue_2', target: 'issue_3', value: 1 }, + ], +} as NetworkGraphData; \ No newline at end of file diff --git a/client/src/data/mail-dummy-data.ts b/client/src/data/mail-dummy-data.ts new file mode 100644 index 0000000..b95e91a --- /dev/null +++ b/client/src/data/mail-dummy-data.ts @@ -0,0 +1,25 @@ +import type { NetworkGraphData } from "../types/network-graph.types.ts"; + +export const mailDummyData = { + nodes: [ + { id: 'mail_1', group: 'mail', value: 2}, + { id: 'mail_2', group: 'mail', value: 2}, + { id: 'mail_3', group: 'mail', value: 2}, + { id: 'mail_4', group: 'mail', value: 2}, + { id: 'mail_5', group: 'mail', value: 2}, + { id: 'mail_6', group: 'mail', value: 2}, + { id: 'mail_7', group: 'mail', value: 2}, + { id: 'mail_8', group: 'mail', value: 2}, + ], + links: [ + { source: 'mail_2', target: 'mail_1', value: 1 }, + { source: 'mail_3', target: 'mail_1', value: 1 }, + { source: 'mail_4', target: 'mail_1', value: 1 }, + { source: 'mail_4', target: 'mail_1', value: 1 }, + { source: 'mail_5', target: 'mail_1', value: 1 }, + { source: 'mail_5', target: 'mail_4', value: 1 }, + { source: 'mail_5', target: 'mail_1', value: 1 }, + { source: 'mail_6', target: 'mail_7', value: 1 }, + { source: 'mail_7', target: 'mail_8', value: 1 }, + ], +} as NetworkGraphData; \ No newline at end of file diff --git a/client/src/data/person-dummy-data.ts b/client/src/data/person-dummy-data.ts new file mode 100644 index 0000000..21e7e56 --- /dev/null +++ b/client/src/data/person-dummy-data.ts @@ -0,0 +1,20 @@ +import type { NetworkGraphData } from "../types/network-graph.types.ts"; + +export const personDummyData = { + nodes: [ + {id: 'person_1', group: 'people', value: 2}, + {id: 'person_2', group: 'people', value: 2}, + {id: 'person_3', group: 'people', value: 2}, + {id: 'person_4', group: 'people', value: 2}, + ], + links: [ + {source: 'person_1', target: 'mail_4', value: 1}, + {source: 'person_1', target: 'file_2', value: 1}, + {source: 'person_2', target: 'file_5', value: 1}, + {source: 'person_2', target: 'mail_2', value: 1}, + {source: 'person_3', target: 'mail_1', value: 1}, + {source: 'person_3', target: 'issue_1', value: 1}, + {source: 'person_4', target: 'file_7', value: 1}, + {source: 'person_4', target: 'file_23', value: 1} + ], +} as NetworkGraphData; \ No newline at end of file diff --git a/client/src/features/NetworkGraph/NetworkGraph.tsx b/client/src/features/NetworkGraph/NetworkGraph.tsx index 55e4590..d833c33 100644 --- a/client/src/features/NetworkGraph/NetworkGraph.tsx +++ b/client/src/features/NetworkGraph/NetworkGraph.tsx @@ -11,10 +11,11 @@ import type { Simulation } from "d3"; import type { Link, Node, Group, HubLink } from "../../types/network-graph.types.ts"; export const NetworkGraph = () => { - const { data } = useDummyData(); + const data = useDummyData(); + const canvasRef = useRef(null); - const centers = (w: number, h: number) => ({ + const nodeGroupCenters = (w: number, h: number) => ({ people: [w * 0.5, h * 0.4], mail: [w * 0.8, h * 0.3], file: [w * 0.25, h * 0.4], @@ -44,15 +45,38 @@ export const NetworkGraph = () => { const canvas = canvasRef.current; const container = canvas.parentElement; - const c = centers(canvas.width, canvas.height); + const c = nodeGroupCenters(canvas.width, canvas.height); + + const links : Link[] = [] as Link[]; + const nodes: Node[] = [] as Node[]; + + for (const [key, value] of Object.entries(data)) { + links.push(... value.links); + nodes.push(... value.nodes); + } - const links: Link[] = data.links.map((d) => ({ ...d })); - const nodes: Node[] = data.nodes.map((d) => ({ ...d })); + const nodeRelationshipMap = new Map>() + nodes.forEach((node) => { + nodeRelationshipMap.set(node, new Set); + links.forEach((link) => { + if (link.source == node) { + const relationshipSet = nodeRelationshipMap.get(node); + if (relationshipSet) { + relationshipSet.add(link.target); + } + } else if (link.target == node) { + const relationshipSet = nodeRelationshipMap.get(node); + if (relationshipSet) { + relationshipSet.add(link.source); + } + } + }) + }); - const nodeHighlightMap = new Map(); + const transparentNodeMap = new Map(); - data.nodes.forEach((p) => { - nodeHighlightMap.set(p.id, { highlightStatus: 0 }); + nodes.forEach((node) => { + transparentNodeMap.set(node, 0); }); const resize = () => { @@ -81,8 +105,8 @@ export const NetworkGraph = () => { // Initialize forces for physics simulation const physicsSimulation = forceSimulation(nodes) .force('link', forceLink(links).id((d) => d.id) - .distance(80) // tighter cluster around hub - .strength(0.05)) // stronger pull to the hub + .distance(80) + .strength(0.05)) .force('hubLinks', forceLink(hubLinks) .distance(40) // tighter cluster around hub .strength(0.2) // stronger pull to the hub @@ -105,7 +129,7 @@ export const NetworkGraph = () => { if (!context) return; // Helper function for drawing a single node based on its group - const drawNodeByGroup = (node: Node, context: CanvasRenderingContext2D) => { + const drawNodeByGroup = (node: Node, context: CanvasRenderingContext2D) => { if (!hasPos(node)) return; // Sets color of node according to group @@ -130,7 +154,7 @@ export const NetworkGraph = () => { // Draws the node context.save(); - if (nodeHighlightMap.get(node.id).highlightStatus == -1) { + if (transparentNodeMap.get(node) == 1) { context.globalAlpha = 0.2; } @@ -219,26 +243,30 @@ export const NetworkGraph = () => { select(canvas).call(dragBehavior as DragBehavior); - // Double-click handler - const onNodeDoubleClick = (node: Node) => { + const setNodeTransparentValue = (node: Node) => { if (!hasPos(node)) return; - const selectedNode = nodeHighlightMap.get(node.id); - if (selectedNode.highlightStatus == 0) { - selectedNode.highlightStatus = 1; - for (const [key, value] of nodeHighlightMap) { - if (key != node.id) { - value.highlightStatus = -1; - } - } - } else if (selectedNode.highlightStatus == 1) { - selectedNode.highlightStatus = 0; - for (const [key, value] of nodeHighlightMap) { - if (key != node.id) { - value.highlightStatus = 0; - } + + console.log(node.id); + + const isTransparent = transparentNodeMap.get(node); + + if (isTransparent == 0) { + for (const key of transparentNodeMap.keys()) transparentNodeMap.set(key, 1); + transparentNodeMap.set(node, 0); + const relatedNodeSet = nodeRelationshipMap.get(node); + if (relatedNodeSet) { + relatedNodeSet.forEach((node) => { + transparentNodeMap.set(node, 0); + }); } } + } + + // Double-click handler + const onNodeDoubleClick = (node: Node) => { + console.log(nodeRelationshipMap.get(node)); + setNodeTransparentValue(node); drawGraph(context, canvas); }; diff --git a/client/src/hooks/useDummyData.ts b/client/src/hooks/useDummyData.ts index a81073c..30090a7 100644 --- a/client/src/hooks/useDummyData.ts +++ b/client/src/hooks/useDummyData.ts @@ -1,5 +1,8 @@ -import { data } from '../data/dummy-data.ts'; +import { personDummyData } from "../data/person-dummy-data.ts"; +import { mailDummyData } from "../data/mail-dummy-data.ts"; +import { issueDummyData } from "../data/issue-dummy-data.ts"; +import { fileDummyData } from "../data/file-dummy-data.ts"; export function useDummyData() { - return { data }; + return { personData: personDummyData, mailData: mailDummyData, issueData: issueDummyData, fileData: fileDummyData }; } diff --git a/client/src/types/network-graph.types.ts b/client/src/types/network-graph.types.ts index 04dded1..d10ca40 100644 --- a/client/src/types/network-graph.types.ts +++ b/client/src/types/network-graph.types.ts @@ -7,6 +7,8 @@ export interface Node extends SimulationNodeDatum { } export type Link = SimulationLinkDatum & { + source: Node; + target: Node; value: number; }; From 52f7c5d2bf2d4efcfea12503309d98b0a1a951e6 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:01:24 -1000 Subject: [PATCH 19/29] Added file picker and loading json from local Landing page now has a file selector that allows the user to load JSONS from local storage. --- client/src/app/router.tsx | 14 ++++- client/src/app/routes/landing.tsx | 12 +++- client/src/components/ui/Header/Header.css | 8 +-- client/src/components/ui/Header/Header.tsx | 2 - .../features/NetworkGraph/NetworkGraph.css | 1 + .../features/NetworkGraph/NetworkGraph.tsx | 32 +++++----- .../NetworkGraph/NetworkGraphProvider.tsx | 59 +++++++++++++++++++ .../NetworkGraph/NetworkGraphToolbar.css | 8 +++ .../NetworkGraph/NetworkGraphToolbar.tsx | 17 ++++++ client/src/hooks/useDummyData.ts | 7 ++- client/src/types/network-graph.types.ts | 6 +- 11 files changed, 134 insertions(+), 32 deletions(-) create mode 100644 client/src/features/NetworkGraph/NetworkGraphProvider.tsx create mode 100644 client/src/features/NetworkGraph/NetworkGraphToolbar.css create mode 100644 client/src/features/NetworkGraph/NetworkGraphToolbar.tsx diff --git a/client/src/app/router.tsx b/client/src/app/router.tsx index c3d212d..4c9513e 100644 --- a/client/src/app/router.tsx +++ b/client/src/app/router.tsx @@ -2,6 +2,7 @@ import { QueryClient, useQueryClient } from '@tanstack/react-query'; import {createBrowserRouter, RouterProvider } from "react-router-dom"; import { paths } from '../config/paths.ts'; import { useMemo } from "react"; +import AppLayout from "../components/layouts/AppLayout.tsx"; const convert = (queryClient: QueryClient) => (m: any) => { const { clientLoader, clientAction, default: Component, ...rest } = m; @@ -16,9 +17,16 @@ const convert = (queryClient: QueryClient) => (m: any) => { export const createAppRouter = (queryClient: QueryClient) => createBrowserRouter([ { - path: paths.home.path, - lazy: () => import('./routes/landing').then(convert(queryClient)), - } + // Layout route + element: , + children: [ + { + path: paths.home.path, + lazy: () => import('./routes/landing').then(convert(queryClient)), + }, + // add more child routes here later + ], + }, ]); export const AppRouter = () => { diff --git a/client/src/app/routes/landing.tsx b/client/src/app/routes/landing.tsx index 38ac62b..3112a02 100644 --- a/client/src/app/routes/landing.tsx +++ b/client/src/app/routes/landing.tsx @@ -1,9 +1,15 @@ -import { NetworkGraph } from "../../features/NetworkGraph"; -import { data } from "../../data/dummy-data.ts"; +import { NetworkGraphProvider } from "../../features/NetworkGraph/NetworkGraphProvider.tsx"; +import { NetworkGraph } from "../../features/NetworkGraph/NetworkGraph.tsx"; +import { NetworkGraphToolbar } from "../../features/NetworkGraph/NetworkGraphToolbar.tsx"; const Landing = () => { return ( - + <> + + + + + ) } diff --git a/client/src/components/ui/Header/Header.css b/client/src/components/ui/Header/Header.css index 9dfbe0f..0cf37a8 100644 --- a/client/src/components/ui/Header/Header.css +++ b/client/src/components/ui/Header/Header.css @@ -1,7 +1,7 @@ #header { - flex: 0 0 15%; - font-size: 50px; - justify-content: center; + color: black; display: flex; - align-items: center; + flex: 0 0 5%; + justify-content: center; + overflow: auto; } diff --git a/client/src/components/ui/Header/Header.tsx b/client/src/components/ui/Header/Header.tsx index 1fa198a..c6a346f 100644 --- a/client/src/components/ui/Header/Header.tsx +++ b/client/src/components/ui/Header/Header.tsx @@ -2,9 +2,7 @@ import "./Header.css"; export const Header = () => { return ( - <>
- ) } \ No newline at end of file diff --git a/client/src/features/NetworkGraph/NetworkGraph.css b/client/src/features/NetworkGraph/NetworkGraph.css index f45140f..0dc81bb 100644 --- a/client/src/features/NetworkGraph/NetworkGraph.css +++ b/client/src/features/NetworkGraph/NetworkGraph.css @@ -8,3 +8,4 @@ #graphCanvas { flex: 1; } + diff --git a/client/src/features/NetworkGraph/NetworkGraph.tsx b/client/src/features/NetworkGraph/NetworkGraph.tsx index d833c33..c622b6c 100644 --- a/client/src/features/NetworkGraph/NetworkGraph.tsx +++ b/client/src/features/NetworkGraph/NetworkGraph.tsx @@ -1,5 +1,4 @@ import { useRef, useEffect } from 'react'; -import { useDummyData } from "../../hooks/useDummyData.ts"; import './NetworkGraph.css'; import { forceX, forceY, forceSimulation, select, drag, pointer, forceCollide, forceManyBody, @@ -9,9 +8,10 @@ import { forceX, forceY, forceSimulation, select, drag, pointer, forceCollide, f import type { Simulation } from "d3"; import type { Link, Node, Group, HubLink } from "../../types/network-graph.types.ts"; +import {useNetworkGraph} from "./NetworkGraphProvider.tsx"; export const NetworkGraph = () => { - const data = useDummyData(); + const { data } = useNetworkGraph(); const canvasRef = useRef(null); @@ -36,6 +36,8 @@ export const NetworkGraph = () => { // Apply forces for simulation useEffect(() => { + if (!data) return; + const radius = 40; const forceStrength = -100; const nodeRadiusMultiplier = 11; @@ -45,15 +47,19 @@ export const NetworkGraph = () => { const canvas = canvasRef.current; const container = canvas.parentElement; - const c = nodeGroupCenters(canvas.width, canvas.height); - const links : Link[] = [] as Link[]; - const nodes: Node[] = [] as Node[]; + const resize = () => { + if (!container) return; + const rect = container.getBoundingClientRect(); + canvas.width = rect.width; + canvas.height = rect.height; - for (const [key, value] of Object.entries(data)) { - links.push(... value.links); - nodes.push(... value.nodes); - } + }; + resize(); + + const c = nodeGroupCenters(canvas.width, canvas.height); + const links : Link[] = data.links; + const nodes: Node[] = data.nodes; const nodeRelationshipMap = new Map>() nodes.forEach((node) => { @@ -79,13 +85,7 @@ export const NetworkGraph = () => { transparentNodeMap.set(node, 0); }); - const resize = () => { - if (!container) return; - const rect = container.getBoundingClientRect(); - canvas.width = rect.width; - canvas.height = rect.height; - }; - resize(); + // Hub nodes that serve as the center of each group, determined by size of the node const hubs: Record = { diff --git a/client/src/features/NetworkGraph/NetworkGraphProvider.tsx b/client/src/features/NetworkGraph/NetworkGraphProvider.tsx new file mode 100644 index 0000000..11f13a8 --- /dev/null +++ b/client/src/features/NetworkGraph/NetworkGraphProvider.tsx @@ -0,0 +1,59 @@ +import { type ReactNode, createContext, useContext, useCallback, useState } from 'react'; +import { type NetworkGraphData } from '../../types/network-graph.types.ts'; + +const NetworkGraphContext = createContext(null); + +interface NetworkGraphContextValue { + data: NetworkGraphData | null; + loadFromFiles: (files: FileList | File[]) => Promise; +} + +export function useNetworkGraph() { + const ctx = useContext(NetworkGraphContext); + + if (!ctx) { + throw new Error("useNetworkGraph must be used inside NetworkGraphProvider"); + } + return ctx; +} + +export function NetworkGraphProvider({ children }: { children: ReactNode }) { + const [data, setGraph] = useState(null); + + const loadFromFiles = useCallback(async (filesLike : FileList | File[]) => { + try { + const files = Array.from(filesLike); + + const allNodes: NetworkGraphData["nodes"] = []; + const allLinks: NetworkGraphData["links"] = []; + + for (const file of files) { + if (!file.name.toLowerCase().endsWith(".json")) continue; + + const text = await file.text(); + const json = JSON.parse(text) as NetworkGraphData; // or a smaller "chunk" type + + allNodes.push(...json.nodes); + allLinks.push(...json.links); + } + + console.log(allNodes); + console.log(allLinks); + + setGraph({ nodes: allNodes, links: allLinks }); + } catch (err) { + console.error(err); + } + }, []); + + const value: NetworkGraphContextValue = { + data, + loadFromFiles, + }; + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/client/src/features/NetworkGraph/NetworkGraphToolbar.css b/client/src/features/NetworkGraph/NetworkGraphToolbar.css new file mode 100644 index 0000000..a4c0fb7 --- /dev/null +++ b/client/src/features/NetworkGraph/NetworkGraphToolbar.css @@ -0,0 +1,8 @@ +#networkGraphToolbar { + color: black; + display: flex; + flex-direction: row; + flex: 0 0 10%; + justify-content: center; + overflow: auto; +} diff --git a/client/src/features/NetworkGraph/NetworkGraphToolbar.tsx b/client/src/features/NetworkGraph/NetworkGraphToolbar.tsx new file mode 100644 index 0000000..6a1674b --- /dev/null +++ b/client/src/features/NetworkGraph/NetworkGraphToolbar.tsx @@ -0,0 +1,17 @@ +import "./NetworkGraphToolbar.css"; +import { useNetworkGraph } from "./NetworkGraphProvider.tsx"; + +export const NetworkGraphToolbar = () => { + const { loadFromFiles } = useNetworkGraph(); + + const handleChange: React.ChangeEventHandler = async (e) => { + if (!e.target.files) return; + await loadFromFiles(e.target.files); + } + + return ( +
+ +
+ ) +}; \ No newline at end of file diff --git a/client/src/hooks/useDummyData.ts b/client/src/hooks/useDummyData.ts index 30090a7..3e0fc10 100644 --- a/client/src/hooks/useDummyData.ts +++ b/client/src/hooks/useDummyData.ts @@ -4,5 +4,10 @@ import { issueDummyData } from "../data/issue-dummy-data.ts"; import { fileDummyData } from "../data/file-dummy-data.ts"; export function useDummyData() { - return { personData: personDummyData, mailData: mailDummyData, issueData: issueDummyData, fileData: fileDummyData }; + return { + personData: personDummyData, + mailData: mailDummyData, + issueData: issueDummyData, + fileData: fileDummyData + }; } diff --git a/client/src/types/network-graph.types.ts b/client/src/types/network-graph.types.ts index d10ca40..ec26081 100644 --- a/client/src/types/network-graph.types.ts +++ b/client/src/types/network-graph.types.ts @@ -2,14 +2,14 @@ import type { SimulationNodeDatum, SimulationLinkDatum } from "d3"; export interface Node extends SimulationNodeDatum { id: string; - group: Group; - value: number; + group: Group; // added property + value: number; // added property } export type Link = SimulationLinkDatum & { source: Node; target: Node; - value: number; + value: number; // added property }; export type NetworkGraphData = { From 27bbf7e92ef626d418e2489efe0eaeca6c5ac5dd Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:03:04 -1000 Subject: [PATCH 20/29] Added json files to data Json files are now in data directory for dev usage --- client/src/data/file-dummy-data.json | 46 ++++++++++++++++++++++++++ client/src/data/issue-dummy-data.json | 11 ++++++ client/src/data/mail-dummy-data.json | 23 +++++++++++++ client/src/data/person-dummy-data.json | 18 ++++++++++ 4 files changed, 98 insertions(+) create mode 100644 client/src/data/file-dummy-data.json create mode 100644 client/src/data/issue-dummy-data.json create mode 100644 client/src/data/mail-dummy-data.json create mode 100644 client/src/data/person-dummy-data.json diff --git a/client/src/data/file-dummy-data.json b/client/src/data/file-dummy-data.json new file mode 100644 index 0000000..fb3e743 --- /dev/null +++ b/client/src/data/file-dummy-data.json @@ -0,0 +1,46 @@ +{ + "nodes": [ + { "id": "file_1", "group": "file", "value": 2 }, + { "id": "file_2", "group": "file", "value": 2 }, + { "id": "file_3", "group": "file", "value": 2 }, + { "id": "file_4", "group": "file", "value": 2 }, + { "id": "file_5", "group": "file", "value": 2 }, + { "id": "file_6", "group": "file", "value": 2 }, + { "id": "file_7", "group": "file", "value": 2 }, + { "id": "file_8", "group": "file", "value": 2 }, + { "id": "file_9", "group": "file", "value": 2 }, + { "id": "file_10", "group": "file", "value": 3 }, + { "id": "file_11", "group": "file", "value": 4 }, + { "id": "file_12", "group": "file", "value": 4 }, + { "id": "file_13", "group": "file", "value": 2 }, + { "id": "file_14", "group": "file", "value": 2 }, + { "id": "file_15", "group": "file", "value": 2 }, + { "id": "file_16", "group": "file", "value": 2 }, + { "id": "file_17", "group": "file", "value": 2 }, + { "id": "file_18", "group": "file", "value": 2 }, + { "id": "file_19", "group": "file", "value": 2 }, + { "id": "file_20", "group": "file", "value": 2 }, + { "id": "file_21", "group": "file", "value": 2 }, + { "id": "file_22", "group": "file", "value": 3 }, + { "id": "file_23", "group": "file", "value": 4 }, + { "id": "file_24", "group": "file", "value": 8 } + ], + "links": [ + { "source": "file_4", "target": "file_2", "value": 1 }, + { "source": "file_4", "target": "file_3", "value": 1 }, + { "source": "file_4", "target": "file_1", "value": 1 }, + { "source": "file_4", "target": "file_6", "value": 1 }, + { "source": "file_4", "target": "issue_2", "value": 1 }, + { "source": "file_6", "target": "file_1", "value": 1 }, + { "source": "file_6", "target": "file_3", "value": 1 }, + { "source": "file_6", "target": "file_5", "value": 1 }, + { "source": "file_7", "target": "file_8", "value": 2 }, + { "source": "file_8", "target": "file_9", "value": 2 }, + { "source": "file_9", "target": "file_10", "value": 2 }, + { "source": "file_9", "target": "file_22", "value": 2 }, + { "source": "file_10", "target": "file_11", "value": 3 }, + { "source": "file_11", "target": "file_12", "value": 4 }, + { "source": "file_23", "target": "issue_1", "value": 4 }, + { "source": "file_23", "target": "file_24", "value": 4 } + ] +} diff --git a/client/src/data/issue-dummy-data.json b/client/src/data/issue-dummy-data.json new file mode 100644 index 0000000..9ca6cbe --- /dev/null +++ b/client/src/data/issue-dummy-data.json @@ -0,0 +1,11 @@ +{ + "nodes": [ + { "id": "issue_1", "group": "issue", "value": 4 }, + { "id": "issue_2", "group": "issue", "value": 2 }, + { "id": "issue_3", "group": "issue", "value": 2 } + ], + "links": [ + { "source": "issue_1", "target": "issue_2", "value": 1 }, + { "source": "issue_2", "target": "issue_3", "value": 1 } + ] +} diff --git a/client/src/data/mail-dummy-data.json b/client/src/data/mail-dummy-data.json new file mode 100644 index 0000000..00f716e --- /dev/null +++ b/client/src/data/mail-dummy-data.json @@ -0,0 +1,23 @@ +{ + "nodes": [ + { "id": "mail_1", "group": "mail", "value": 2 }, + { "id": "mail_2", "group": "mail", "value": 2 }, + { "id": "mail_3", "group": "mail", "value": 2 }, + { "id": "mail_4", "group": "mail", "value": 2 }, + { "id": "mail_5", "group": "mail", "value": 2 }, + { "id": "mail_6", "group": "mail", "value": 2 }, + { "id": "mail_7", "group": "mail", "value": 2 }, + { "id": "mail_8", "group": "mail", "value": 2 } + ], + "links": [ + { "source": "mail_2", "target": "mail_1", "value": 1 }, + { "source": "mail_3", "target": "mail_1", "value": 1 }, + { "source": "mail_4", "target": "mail_1", "value": 1 }, + { "source": "mail_4", "target": "mail_1", "value": 1 }, + { "source": "mail_5", "target": "mail_1", "value": 1 }, + { "source": "mail_5", "target": "mail_4", "value": 1 }, + { "source": "mail_5", "target": "mail_1", "value": 1 }, + { "source": "mail_6", "target": "mail_7", "value": 1 }, + { "source": "mail_7", "target": "mail_8", "value": 1 } + ] +} diff --git a/client/src/data/person-dummy-data.json b/client/src/data/person-dummy-data.json new file mode 100644 index 0000000..50a24a3 --- /dev/null +++ b/client/src/data/person-dummy-data.json @@ -0,0 +1,18 @@ +{ + "nodes": [ + { "id": "person_1", "group": "people", "value": 2 }, + { "id": "person_2", "group": "people", "value": 2 }, + { "id": "person_3", "group": "people", "value": 2 }, + { "id": "person_4", "group": "people", "value": 2 } + ], + "links": [ + { "source": "person_1", "target": "mail_4", "value": 1 }, + { "source": "person_1", "target": "file_2", "value": 1 }, + { "source": "person_2", "target": "file_5", "value": 1 }, + { "source": "person_2", "target": "mail_2", "value": 1 }, + { "source": "person_3", "target": "mail_1", "value": 1 }, + { "source": "person_3", "target": "issue_1", "value": 1 }, + { "source": "person_4", "target": "file_7", "value": 1 }, + { "source": "person_4", "target": "file_23", "value": 1 } + ] +} From 1ab69322be52bb5a3f8cc48678e37ba143241020 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Tue, 18 Nov 2025 19:58:58 -1000 Subject: [PATCH 21/29] Ungodded the NetworkGraph component! NetworkGraph component now contains just hooks and the canvas Created two new custom hooks for running NetworkGraph logic Refactored all helper functions for NetworkGraph Moved helper functions into new utils file of the NetworkGraph feature --- client/eslint.config.js | 33 +- client/package.json | 1 + client/pnpm-lock.yaml | 26 +- client/src/app/routes/landing.tsx | 6 +- .../features/NetworkGraph/NetworkGraph.tsx | 323 ++---------------- ...phProvider.tsx => NetworkGraphContext.tsx} | 45 ++- .../NetworkGraph/NetworkGraphToolbar.tsx | 2 +- .../NetworkGraph/lib/networkGraphUtils.ts | 232 +++++++++++++ client/src/hooks/NetworkGraph/useDummyData.ts | 13 + .../useNetworkGraphInteractions.ts | 62 ++++ .../NetworkGraph/useNetworkGraphSimulation.ts | 47 +++ client/src/hooks/useDummyData.ts | 13 - client/tsconfig.app.json | 4 + client/vite.config.ts | 9 +- 14 files changed, 476 insertions(+), 340 deletions(-) rename client/src/features/NetworkGraph/{NetworkGraphProvider.tsx => NetworkGraphContext.tsx} (56%) create mode 100644 client/src/features/NetworkGraph/lib/networkGraphUtils.ts create mode 100644 client/src/hooks/NetworkGraph/useDummyData.ts create mode 100644 client/src/hooks/NetworkGraph/useNetworkGraphInteractions.ts create mode 100644 client/src/hooks/NetworkGraph/useNetworkGraphSimulation.ts delete mode 100644 client/src/hooks/useDummyData.ts diff --git a/client/eslint.config.js b/client/eslint.config.js index d94e7de..9b4e9a5 100644 --- a/client/eslint.config.js +++ b/client/eslint.config.js @@ -6,18 +6,25 @@ import tseslint from 'typescript-eslint' import { globalIgnores } from 'eslint/config' export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowExportNames: ['useNetworkGraph'] }, + ], + }, }, - }, + ]) diff --git a/client/package.json b/client/package.json index 436985f..99e4e69 100644 --- a/client/package.json +++ b/client/package.json @@ -19,6 +19,7 @@ "devDependencies": { "@eslint/js": "^9.33.0", "@types/d3": "^7.4.3", + "@types/node": "^24.10.1", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.0", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 9dbab9c..6668b6a 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: '@types/d3': specifier: ^7.4.3 version: 7.4.3 + '@types/node': + specifier: ^24.10.1 + version: 24.10.1 '@types/react': specifier: ^19.1.10 version: 19.1.12 @@ -38,7 +41,7 @@ importers: version: 19.1.9(@types/react@19.1.12) '@vitejs/plugin-react': specifier: ^5.0.0 - version: 5.0.2(vite@7.1.5) + version: 5.0.2(vite@7.1.5(@types/node@24.10.1)) eslint: specifier: ^9.33.0 version: 9.35.0 @@ -59,7 +62,7 @@ importers: version: 8.43.0(eslint@9.35.0)(typescript@5.8.3) vite: specifier: ^7.1.2 - version: 7.1.5 + version: 7.1.5(@types/node@24.10.1) packages: @@ -614,6 +617,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + '@types/react-dom@19.1.9': resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} peerDependencies: @@ -1332,6 +1338,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true @@ -1891,6 +1900,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + '@types/react-dom@19.1.9(@types/react@19.1.12)': dependencies: '@types/react': 19.1.12 @@ -1992,7 +2005,7 @@ snapshots: '@typescript-eslint/types': 8.43.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@5.0.2(vite@7.1.5)': + '@vitejs/plugin-react@5.0.2(vite@7.1.5(@types/node@24.10.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -2000,7 +2013,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.34 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.5 + vite: 7.1.5(@types/node@24.10.1) transitivePeerDependencies: - supports-color @@ -2655,6 +2668,8 @@ snapshots: typescript@5.8.3: {} + undici-types@7.16.0: {} + update-browserslist-db@1.1.3(browserslist@4.25.4): dependencies: browserslist: 4.25.4 @@ -2665,7 +2680,7 @@ snapshots: dependencies: punycode: 2.3.1 - vite@7.1.5: + vite@7.1.5(@types/node@24.10.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -2674,6 +2689,7 @@ snapshots: rollup: 4.50.1 tinyglobby: 0.2.15 optionalDependencies: + '@types/node': 24.10.1 fsevents: 2.3.3 which@2.0.2: diff --git a/client/src/app/routes/landing.tsx b/client/src/app/routes/landing.tsx index 3112a02..74fa32f 100644 --- a/client/src/app/routes/landing.tsx +++ b/client/src/app/routes/landing.tsx @@ -1,6 +1,6 @@ -import { NetworkGraphProvider } from "../../features/NetworkGraph/NetworkGraphProvider.tsx"; -import { NetworkGraph } from "../../features/NetworkGraph/NetworkGraph.tsx"; -import { NetworkGraphToolbar } from "../../features/NetworkGraph/NetworkGraphToolbar.tsx"; +import { NetworkGraphProvider } from "@/features/NetworkGraph/NetworkGraphContext.tsx"; +import { NetworkGraph } from "@/features/NetworkGraph"; +import { NetworkGraphToolbar } from "@/features/NetworkGraph/NetworkGraphToolbar.tsx"; const Landing = () => { return ( diff --git a/client/src/features/NetworkGraph/NetworkGraph.tsx b/client/src/features/NetworkGraph/NetworkGraph.tsx index c622b6c..a7ce281 100644 --- a/client/src/features/NetworkGraph/NetworkGraph.tsx +++ b/client/src/features/NetworkGraph/NetworkGraph.tsx @@ -1,307 +1,38 @@ -import { useRef, useEffect } from 'react'; +import { useRef } from 'react'; import './NetworkGraph.css'; - -import { forceX, forceY, forceSimulation, select, drag, pointer, forceCollide, forceManyBody, - forceLink, - type DragBehavior -} from "d3"; - -import type { Simulation } from "d3"; -import type { Link, Node, Group, HubLink } from "../../types/network-graph.types.ts"; -import {useNetworkGraph} from "./NetworkGraphProvider.tsx"; +import { useNetworkGraph } from "./NetworkGraphContext.tsx"; +import {useNetworkGraphSimulation} from "@/hooks/NetworkGraph/useNetworkGraphSimulation.ts"; +import {useNetworkGraphInteractions} from "@/hooks/NetworkGraph/useNetworkGraphInteractions.ts"; export const NetworkGraph = () => { const { data } = useNetworkGraph(); + const { nodes, links } = data ?? { nodes: [], links: [] }; const canvasRef = useRef(null); - const nodeGroupCenters = (w: number, h: number) => ({ - people: [w * 0.5, h * 0.4], - mail: [w * 0.8, h * 0.3], - file: [w * 0.25, h * 0.4], - issue: [w * 0.8, h * 0.6], - }); - - // Helper function for clearing forces - function clearForces(sim: Simulation) { - sim - .force('x', null) - .force('y', null) - .force('collide', null) - .force('charge', null) - .force('link', null) - .force('hubLinks', null) - .velocityDecay(1); // stop inertia - } - - // Apply forces for simulation - useEffect(() => { - if (!data) return; - - const radius = 40; - const forceStrength = -100; - const nodeRadiusMultiplier = 11; - const nodePadding = 6; - - if (!canvasRef.current) return; - - const canvas = canvasRef.current; - const container = canvas.parentElement; - - const resize = () => { - if (!container) return; - const rect = container.getBoundingClientRect(); - canvas.width = rect.width; - canvas.height = rect.height; - - }; - resize(); - - const c = nodeGroupCenters(canvas.width, canvas.height); - const links : Link[] = data.links; - const nodes: Node[] = data.nodes; - - const nodeRelationshipMap = new Map>() - nodes.forEach((node) => { - nodeRelationshipMap.set(node, new Set); - links.forEach((link) => { - if (link.source == node) { - const relationshipSet = nodeRelationshipMap.get(node); - if (relationshipSet) { - relationshipSet.add(link.target); - } - } else if (link.target == node) { - const relationshipSet = nodeRelationshipMap.get(node); - if (relationshipSet) { - relationshipSet.add(link.source); - } - } - }) - }); - - const transparentNodeMap = new Map(); - - nodes.forEach((node) => { - transparentNodeMap.set(node, 0); - }); - - - - // Hub nodes that serve as the center of each group, determined by size of the node - const hubs: Record = { - people: nodes.filter(n => n.group === 'people').reduce((a,b)=> a.value>b.value?a:b), - mail: nodes.filter(n => n.group === 'mail').reduce((a,b)=> a.value>b.value?a:b), - file: nodes.filter(n => n.group === 'file').reduce((a,b)=> a.value>b.value?a:b), - issue: nodes.filter(n => n.group === 'issue').reduce((a,b)=> a.value>b.value?a:b), - }; - - // Helper for determining if a node is a hub node - const isHub = (d: Node) => hubs[d.group] === d; - - const hubLinks: HubLink[] = nodes - .filter(n => !isHub(n)) - .map(n => ({ source: hubs[n.group], target: n })); - - // Initialize forces for physics simulation - const physicsSimulation = forceSimulation(nodes) - .force('link', forceLink(links).id((d) => d.id) - .distance(80) - .strength(0.05)) - .force('hubLinks', forceLink(hubLinks) - .distance(40) // tighter cluster around hub - .strength(0.2) // stronger pull to the hub - ) - .force("x", forceX(d => c[d.group][0]).strength(0.2)) - .force("y", forceY(d => c[d.group][1]).strength(0.2)) - .force('collide', forceCollide().radius(d => d.value * nodeRadiusMultiplier + nodePadding)) - .force('charge', forceManyBody().strength(forceStrength)); - - // Reset the simulation - physicsSimulation.alpha(1); - - // Run the simulation for some time and then stop it - for (let i = 0; i < 300; i++) physicsSimulation.tick(); - clearForces(physicsSimulation); - physicsSimulation.stop(); - - // Grab context (used for drawing) from canvas - const context = canvas.getContext("2d"); - if (!context) return; - - // Helper function for drawing a single node based on its group - const drawNodeByGroup = (node: Node, context: CanvasRenderingContext2D) => { - if (!hasPos(node)) return; - - // Sets color of node according to group - switch (node.group) { - case "people": - context.fillStyle = 'black'; - break; - case "mail": - context.fillStyle = '#add8e6'; - break; - case 'file': - context.fillStyle = '#fafad2'; - break; - case 'issue': - context.fillStyle = '#0052cc'; - } - - - // Creates a text label for the node - const label = (node as Node).id ?? node.id ?? ""; - if (!label) return; - - // Draws the node - context.save(); - if (transparentNodeMap.get(node) == 1) { - context.globalAlpha = 0.2; - } - - context.beginPath(); - context.moveTo(node.x + radius, node.y); - context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); - context.fill(); - - - // Set text attributes - const px = Math.round(Math.max(10, Math.min(24, node.value * nodeRadiusMultiplier * 0.6))); - context.font = `${px}px Roboto, sans-serif`; - context.textAlign = "center"; - context.textBaseline = "middle"; - context.fillStyle = 'black'; - - // If the node is of group people, then set text color to white (to contrast the black node) - if (node.group == 'people' as Group) { - context.fillStyle = 'white'; - } - - // Draws the text - context.fillText(label, node.x, node.y); - context.restore(); - }; - - // Function for drawing the network graph - const drawGraph = (context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => { - context.clearRect(0, 0, canvas.width, canvas.height); - - links.forEach((link) => { - if (!isNode(link.source) || !isNode(link.target)) return; - - const s = link.source, t = link.target; - if (!hasPos(s) || !hasPos(t)) return; - - context.beginPath(); - context.moveTo(s.x, s.y); - context.lineTo(t.x, t.y); - context.stroke(); - context.strokeStyle = "grey"; - }); - - nodes.forEach((node) => { - drawNodeByGroup(node, context); - }) - - }; - - drawGraph(context, canvas); - - // Finds the topmost node under (x,y) - const findNodeAt = (x: number, y: number): Node | undefined => { - // iterate in reverse draw order so on top wins - for (let i = nodes.length - 1; i >= 0; i--) { - const n = nodes[i]; - if (!hasPos(n)) continue; - const r = n.value * nodeRadiusMultiplier; - const dx = x - n.x!; - const dy = y - n.y!; - if (dx*dx + dy*dy <= r*r) return n; - } - return undefined; - }; - - const dragBehavior = drag() - .subject((event) => { - const [x, y] = pointer(event, canvas); - - // find nearest node within radius - const n = findNodeAt(x, y); - - if (n) { - n.fx = n.x ?? x; - n.fy = n.y ?? y; - } - - return n; - }) - .on('drag', (event) => { - const n = event.subject; - n.x = event.x; - n.y = event.y; - drawGraph(context, canvas); - }); - - select(canvas).call(dragBehavior as DragBehavior); - - const setNodeTransparentValue = (node: Node) => { - if (!hasPos(node)) return; - - console.log(node.id); - - const isTransparent = transparentNodeMap.get(node); - - if (isTransparent == 0) { - for (const key of transparentNodeMap.keys()) transparentNodeMap.set(key, 1); - transparentNodeMap.set(node, 0); - const relatedNodeSet = nodeRelationshipMap.get(node); - if (relatedNodeSet) { - relatedNodeSet.forEach((node) => { - transparentNodeMap.set(node, 0); - }); - } - } - - } - - // Double-click handler - const onNodeDoubleClick = (node: Node) => { - console.log(nodeRelationshipMap.get(node)); - setNodeTransparentValue(node); - drawGraph(context, canvas); - }; - - - // Double-click listener - const handleDblClick = (event: MouseEvent) => { - event.preventDefault(); - const [x, y] = pointer(event, canvas); - const hit = findNodeAt(x, y); - if (hit) onNodeDoubleClick(hit); - }; - - // Attach double-click listener to canvas - select(canvas).on('dblclick', handleDblClick); - - const ro = new ResizeObserver(() => { - resize(); - drawGraph(context, canvas); - }) - - if (!container) return; - ro.observe(container); - return () => ro.disconnect(); - - }, [data]); + useNetworkGraphSimulation({ nodes, links, canvasRef }); + useNetworkGraphInteractions({ nodes, links, canvasRef }); return ( - +
+ + + {/*{popup && (
Heres a popup
)}*/} + +
) } - -function isNode(v: Link["source"]): v is Node { - return typeof v === "object" && v !== null; -} - -function hasPos(n: Node): n is Node & { x: number; y: number } { - return n.x != null && n.y != null; -} diff --git a/client/src/features/NetworkGraph/NetworkGraphProvider.tsx b/client/src/features/NetworkGraph/NetworkGraphContext.tsx similarity index 56% rename from client/src/features/NetworkGraph/NetworkGraphProvider.tsx rename to client/src/features/NetworkGraph/NetworkGraphContext.tsx index 11f13a8..a52cb41 100644 --- a/client/src/features/NetworkGraph/NetworkGraphProvider.tsx +++ b/client/src/features/NetworkGraph/NetworkGraphContext.tsx @@ -1,24 +1,54 @@ -import { type ReactNode, createContext, useContext, useCallback, useState } from 'react'; -import { type NetworkGraphData } from '../../types/network-graph.types.ts'; +import { type ReactNode, createContext, useContext, useCallback, useState, useEffect } from 'react'; +import { type NetworkGraphData } from '@/types/network-graph.types.ts'; const NetworkGraphContext = createContext(null); interface NetworkGraphContextValue { - data: NetworkGraphData | null; + data: NetworkGraphData; loadFromFiles: (files: FileList | File[]) => Promise; } +const STORAGE_KEY = "networkGraphData"; + +function saveGraphToStorage(data: NetworkGraphData) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + } catch (err) { + console.error("Failed to save graph to localStorage", err); + } +} + +function loadGraphFromStorage(): NetworkGraphData | null { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + return JSON.parse(raw) as NetworkGraphData; + } catch (err) { + console.error("Failed to load graph from localStorage", err); + return null; + } +} + export function useNetworkGraph() { const ctx = useContext(NetworkGraphContext); if (!ctx) { throw new Error("useNetworkGraph must be used inside NetworkGraphProvider"); } + return ctx; } export function NetworkGraphProvider({ children }: { children: ReactNode }) { - const [data, setGraph] = useState(null); + const initialData: NetworkGraphData = { nodes: [], links: [] }; + const [data, setGraph] = useState(initialData); + + useEffect(() => { + const stored = loadGraphFromStorage(); + if (stored) { + setGraph(stored); + } + }, []); const loadFromFiles = useCallback(async (filesLike : FileList | File[]) => { try { @@ -37,10 +67,11 @@ export function NetworkGraphProvider({ children }: { children: ReactNode }) { allLinks.push(...json.links); } - console.log(allNodes); - console.log(allLinks); + const graph: NetworkGraphData = { nodes: allNodes, links: allLinks }; + + setGraph(graph); + saveGraphToStorage(graph); - setGraph({ nodes: allNodes, links: allLinks }); } catch (err) { console.error(err); } diff --git a/client/src/features/NetworkGraph/NetworkGraphToolbar.tsx b/client/src/features/NetworkGraph/NetworkGraphToolbar.tsx index 6a1674b..855e00c 100644 --- a/client/src/features/NetworkGraph/NetworkGraphToolbar.tsx +++ b/client/src/features/NetworkGraph/NetworkGraphToolbar.tsx @@ -1,5 +1,5 @@ import "./NetworkGraphToolbar.css"; -import { useNetworkGraph } from "./NetworkGraphProvider.tsx"; +import { useNetworkGraph } from "./NetworkGraphContext.tsx"; export const NetworkGraphToolbar = () => { const { loadFromFiles } = useNetworkGraph(); diff --git a/client/src/features/NetworkGraph/lib/networkGraphUtils.ts b/client/src/features/NetworkGraph/lib/networkGraphUtils.ts new file mode 100644 index 0000000..108acc6 --- /dev/null +++ b/client/src/features/NetworkGraph/lib/networkGraphUtils.ts @@ -0,0 +1,232 @@ +import type {Group, Link, Node} from "@/types/network-graph.types.ts"; +import {drag, pointer, type Simulation} from "d3"; + +export const nodeGroupCenters = (w: number, h: number) => ({ + people: [w * 0.5, h * 0.4], + mail: [w * 0.8, h * 0.3], + file: [w * 0.25, h * 0.4], + issue: [w * 0.8, h * 0.6], +}); + +export function buildInitialTransparencyMap(nodes: Node[]) { + const map = new Map(); + nodes.forEach(n => map.set(n.id, 0)); + return map; +} + +export function updateTransparency( + hitNode: Node, + transparentNodeMap: Map, + nodeRelationshipMap: Map> +){ + if (!hasPos(hitNode)) return; + + const isTransparent = transparentNodeMap.get(hitNode.id); + + if (isTransparent === 0) { + for (const key of transparentNodeMap.keys()) { + transparentNodeMap.set(key, 1); + } + transparentNodeMap.set(hitNode.id, 0); + + const relatedNodeSet = nodeRelationshipMap.get(hitNode.id); + relatedNodeSet?.forEach(nodeId => { + transparentNodeMap.set(nodeId, 0); + }); + } +} + +export function buildHubs(nodes: Node[]) { + const hubs: Partial> = {}; + + for (const n of nodes) { + const existing = hubs[n.group]; + if (!existing || n.value > existing.value) { + hubs[n.group] = n; + } + } + + return hubs; +} + +export function buildHubLinks(nodes: Node[], hubs: Partial>) { + return nodes + .filter(n => hubs[n.group] && !isHub(n, hubs as Record)) + .map(n => ({ source: hubs[n.group]!, target: n })); +} + +function isHub (d: Node, hubs: Record) { + return hubs[d.group] === d; +} + +export function createDragBehavior( + context: CanvasRenderingContext2D, + canvas: HTMLCanvasElement, + nodes: Node[], + nodeRadiusMultiplier: number, + links: Link[], + transparentNodeMap: Map) { + + return drag() + .subject((event) => { + const [x, y] = pointer(event, canvas); + + // find nearest node within radius + const n = findNodeAt(nodes, nodeRadiusMultiplier, x, y); + + if (n) { + n.fx = n.x ?? x; + n.fy = n.y ?? y; + } + + return n; + }) + .on('drag', (event) => { + const n = event.subject; + n.x = event.x; + n.y = event.y; + drawGraph(context, canvas, nodes, links, transparentNodeMap, nodeRadiusMultiplier); + }); +} + +export function buildRelationshipMap(nodes: Node[], links: Link[]) { + const map = new Map>(); + + nodes.forEach((node) => { + const set = new Set(); + + links.forEach((link) => { + const sourceId = isNode(link.source) ? link.source.id : link.source; + const targetId = isNode(link.target) ? link.target.id : link.target; + + if (sourceId === node.id) set.add(targetId); + else if (targetId === node.id) set.add(sourceId); + }); + + map.set(node.id, set); + }); + + return map; +} + +const drawNodeByGroup = ( + node: Node, + context: CanvasRenderingContext2D, + transparentNodeMap: Map, + nodeRadiusMultiplier: number) => { + + if (!hasPos(node)) return; + + // Sets color of node according to group + switch (node.group) { + case "people": + context.fillStyle = 'black'; + break; + case "mail": + context.fillStyle = '#add8e6'; + break; + case 'file': + context.fillStyle = '#fafad2'; + break; + case 'issue': + context.fillStyle = '#0052cc'; + } + + + const label = node.id; + if (!label) return; + + // Draws the node + context.save(); + if (transparentNodeMap.get(node.id) == 1) { + context.globalAlpha = 0.2; + } + + context.beginPath(); + context.moveTo(node.x, node.y); + context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); + context.fill(); + + + // Set text attributes + const px = Math.round(Math.max(10, Math.min(24, node.value * nodeRadiusMultiplier * 0.6))); + context.font = `${px}px Roboto, sans-serif`; + context.textAlign = "center"; + context.textBaseline = "middle"; + context.fillStyle = 'black'; + + // If the node is of group people, then set text color to white (to contrast the black node) + if (node.group === 'people') { + context.fillStyle = 'white'; + } + + // Draws the text + context.fillText(label, node.x, node.y); + context.restore(); +}; + +function isNode(v: Link["source"]): v is Node { + return typeof v === "object" && v !== null; +} + +function hasPos(n: Node): n is Node & { x: number; y: number } { + return n.x != null && n.y != null; +} + + +export function drawGraph ( + context: CanvasRenderingContext2D, + canvas: HTMLCanvasElement, + nodes: Node[], + links: Link[], + transparentNodeMap: Map, + nodeRadiusMultiplier: number){ + + context.clearRect(0, 0, canvas.width, canvas.height); + + context.strokeStyle = "grey"; + + links.forEach((link) => { + if (!isNode(link.source) || !isNode(link.target)) return; + + const s = link.source; + const t = link.target; + if (!hasPos(s) || !hasPos(t)) return; + + context.beginPath(); + context.moveTo(s.x, s.y); + context.lineTo(t.x, t.y); + context.stroke(); + }); + + nodes.forEach((node) => { + drawNodeByGroup(node, context, transparentNodeMap, nodeRadiusMultiplier); + }) + +} + +// Finds the topmost node under (x,y) +export function findNodeAt(nodes: Node[], nodeRadiusMultiplier: number, x: number, y: number) { + for (let i = nodes.length - 1; i >= 0; i--) { + const n = nodes[i]; + if (!hasPos(n)) continue; + + const r = n.value * nodeRadiusMultiplier; + const dx = x - n.x!; + const dy = y - n.y!; + + if (dx*dx + dy*dy <= r*r) return n; + } + return undefined; +} + +export function clearForces(sim: Simulation) { + sim + .force('x', null) + .force('y', null) + .force('collide', null) + .force('charge', null) + .force('link', null) + .force('hubLinks', null) + .velocityDecay(1); // stop inertia +} \ No newline at end of file diff --git a/client/src/hooks/NetworkGraph/useDummyData.ts b/client/src/hooks/NetworkGraph/useDummyData.ts new file mode 100644 index 0000000..5c70e85 --- /dev/null +++ b/client/src/hooks/NetworkGraph/useDummyData.ts @@ -0,0 +1,13 @@ +import { personDummyData } from "@/data/person-dummy-data.ts"; +import { mailDummyData } from "@/data/mail-dummy-data.ts"; +import { issueDummyData } from "@/data/issue-dummy-data.ts"; +import { fileDummyData } from "@/data/file-dummy-data.ts"; + +export function useDummyData() { + return { + personData: personDummyData, + mailData: mailDummyData, + issueData: issueDummyData, + fileData: fileDummyData + }; +} diff --git a/client/src/hooks/NetworkGraph/useNetworkGraphInteractions.ts b/client/src/hooks/NetworkGraph/useNetworkGraphInteractions.ts new file mode 100644 index 0000000..b838e4c --- /dev/null +++ b/client/src/hooks/NetworkGraph/useNetworkGraphInteractions.ts @@ -0,0 +1,62 @@ +import { useEffect, type RefObject } from "react"; +import { type Link, type Node } from "@/types/network-graph.types.ts" +import { + buildInitialTransparencyMap, + buildRelationshipMap, + createDragBehavior, + drawGraph, findNodeAt, updateTransparency +} from "@/features/NetworkGraph/lib/networkGraphUtils.ts"; +import {type DragBehavior, pointer, select} from "d3"; + +export function useNetworkGraphInteractions( + { nodes, links, canvasRef }: { nodes: Node[]; links: Link[]; canvasRef: RefObject} +) { + + useEffect(() => { + if (!canvasRef.current) return; + + const canvas = canvasRef.current; + if (!canvas) return; + const container = canvas.parentElement; + if (!container) return; + + const context = canvas.getContext("2d"); + if (!context) return; + + const nodeRadiusMultiplier = 11; + const nodeRelationshipMap = buildRelationshipMap(nodes, links); + const transparentNodeMap = buildInitialTransparencyMap(nodes); + + const resize = () => { + const rect = container.getBoundingClientRect(); + canvas.width = rect.width; + canvas.height = rect.height; + drawGraph(context, canvas, nodes, links, transparentNodeMap, nodeRadiusMultiplier); + }; + + resize(); + + const dragBehavior = createDragBehavior(context, canvas, nodes, nodeRadiusMultiplier, links, transparentNodeMap,); + select(canvas).call(dragBehavior as DragBehavior); + + const handleDblClick = (event: MouseEvent) => { + const [x, y] = pointer(event, canvas); + const hit = findNodeAt(nodes, nodeRadiusMultiplier, x, y,); + if (hit) { + updateTransparency(hit, transparentNodeMap, nodeRelationshipMap); + drawGraph(context, canvas, nodes, links, transparentNodeMap, nodeRadiusMultiplier); + } + }; + select(canvas).on("dblclick", handleDblClick); + + const ro = new ResizeObserver(() => { + resize(); + }); + ro.observe(container); + + return () => { + ro.disconnect(); + select(canvas).on(".drag", null).on("dblclick", null); + }; + }, [nodes, links, canvasRef]); +} \ No newline at end of file diff --git a/client/src/hooks/NetworkGraph/useNetworkGraphSimulation.ts b/client/src/hooks/NetworkGraph/useNetworkGraphSimulation.ts new file mode 100644 index 0000000..1e97361 --- /dev/null +++ b/client/src/hooks/NetworkGraph/useNetworkGraphSimulation.ts @@ -0,0 +1,47 @@ +import { useEffect, type RefObject } from "react"; +import { forceSimulation, forceLink, forceX, forceY, forceCollide, forceManyBody } from "d3"; +import type { Simulation } from "d3"; +import type { Node, Link, HubLink } from "@/types/network-graph.types.ts" +import { nodeGroupCenters, buildHubs, buildHubLinks, clearForces } from "@/features/NetworkGraph/lib/networkGraphUtils.ts"; + +interface UseNetworkGraphSimulationArgs { + nodes: Node[]; + links: Link[]; + canvasRef: RefObject; +} + +export function useNetworkGraphSimulation({ nodes, links, canvasRef }: UseNetworkGraphSimulationArgs) { + useEffect(() => { + if (!canvasRef.current) return; + if (!nodes.length || !links.length) return; + + const canvas = canvasRef.current; + if (!canvas) return; + + const forceStrength = -100; + const nodeRadiusMultiplier = 11; + const nodePadding = 6; + + const c = nodeGroupCenters(canvas.width, canvas.height); + const hubs = buildHubs(nodes); + const hubLinks: HubLink[] = buildHubLinks(nodes, hubs); + + const sim = forceSimulation(nodes) + .force("link", forceLink(links).id(d => d.id).distance(80).strength(0.05)) + .force("hubLinks", forceLink(hubLinks).distance(40).strength(0.2)) + .force("x", forceX(d => c[d.group][0]).strength(0.2)) + .force("y", forceY(d => c[d.group][1]).strength(0.2)) + .force("collide", forceCollide().radius(d => d.value * nodeRadiusMultiplier + nodePadding)) + .force("charge", forceManyBody().strength(forceStrength)); + + sim.alpha(1); + for (let i = 0; i < 300; i++) sim.tick(); + clearForces(sim as Simulation); + sim.stop(); + + return () => { + clearForces(sim as Simulation); + sim.stop(); + }; + }, [nodes, links, canvasRef]); +} diff --git a/client/src/hooks/useDummyData.ts b/client/src/hooks/useDummyData.ts deleted file mode 100644 index 3e0fc10..0000000 --- a/client/src/hooks/useDummyData.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { personDummyData } from "../data/person-dummy-data.ts"; -import { mailDummyData } from "../data/mail-dummy-data.ts"; -import { issueDummyData } from "../data/issue-dummy-data.ts"; -import { fileDummyData } from "../data/file-dummy-data.ts"; - -export function useDummyData() { - return { - personData: personDummyData, - mailData: mailDummyData, - issueData: issueDummyData, - fileData: fileDummyData - }; -} diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json index 227a6c6..b50f7ca 100644 --- a/client/tsconfig.app.json +++ b/client/tsconfig.app.json @@ -1,5 +1,9 @@ { "compilerOptions": { + "baseUrl": "./src", + "paths": { + "@/*": ["*"] + }, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2022", "useDefineForClassFields": true, diff --git a/client/vite.config.ts b/client/vite.config.ts index 8b0f57b..34f510a 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,7 +1,12 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import path from "path"; -// https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, }) From f2d08f203339783e45d267e4760c98175eed3715 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:01:19 -1000 Subject: [PATCH 22/29] JSON loading hook added to NetworkGraphContext NetworkGraphContext now uses a hook that loads JSON data from directory Manual file selector removed Hook loads from directory specified by user in new user.config.yaml --- client/package.json | 3 +- client/pnpm-lock.yaml | 21 ++++- .../DummyProject/file-graph-data.json | 46 +++++++++ .../DummyProject/issue-graph-data.json | 11 +++ .../DummyProject/mail-graph-data.json | 23 +++++ .../DummyProject/person-graph-data.json | 18 ++++ client/public/sample-data.json | 93 ------------------- client/src/app/routes/landing.tsx | 2 +- client/src/config/app.ts | 13 +++ client/src/config/user.config.yaml | 3 + .../NetworkGraph/NetworkGraphContext.tsx | 63 +------------ .../{ => components}/NetworkGraph.css | 0 .../{ => components}/NetworkGraph.tsx | 8 +- .../{ => components}/NetworkGraphToolbar.css | 0 .../{ => components}/NetworkGraphToolbar.tsx | 2 +- .../NetworkGraph/hooks}/useDummyData.ts | 0 .../hooks}/useNetworkGraphInteractions.ts | 0 .../hooks/useNetworkGraphJsonData.ts | 47 ++++++++++ .../hooks}/useNetworkGraphSimulation.ts | 0 client/src/features/NetworkGraph/index.ts | 2 +- client/src/types/network-graph.types.ts | 4 +- 21 files changed, 192 insertions(+), 167 deletions(-) create mode 100644 client/public/Projects/DummyProject/file-graph-data.json create mode 100644 client/public/Projects/DummyProject/issue-graph-data.json create mode 100644 client/public/Projects/DummyProject/mail-graph-data.json create mode 100644 client/public/Projects/DummyProject/person-graph-data.json delete mode 100644 client/public/sample-data.json create mode 100644 client/src/config/app.ts create mode 100644 client/src/config/user.config.yaml rename client/src/features/NetworkGraph/{ => components}/NetworkGraph.css (100%) rename client/src/features/NetworkGraph/{ => components}/NetworkGraph.tsx (80%) rename client/src/features/NetworkGraph/{ => components}/NetworkGraphToolbar.css (100%) rename client/src/features/NetworkGraph/{ => components}/NetworkGraphToolbar.tsx (87%) rename client/src/{hooks/NetworkGraph => features/NetworkGraph/hooks}/useDummyData.ts (100%) rename client/src/{hooks/NetworkGraph => features/NetworkGraph/hooks}/useNetworkGraphInteractions.ts (100%) create mode 100644 client/src/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts rename client/src/{hooks/NetworkGraph => features/NetworkGraph/hooks}/useNetworkGraphSimulation.ts (100%) diff --git a/client/package.json b/client/package.json index 99e4e69..ca7f238 100644 --- a/client/package.json +++ b/client/package.json @@ -14,7 +14,8 @@ "d3": "^7.9.0", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-router-dom": "^7.9.3" + "react-router-dom": "^7.9.3", + "yaml": "^2.8.1" }, "devDependencies": { "@eslint/js": "^9.33.0", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 6668b6a..6ee01c7 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: react-router-dom: specifier: ^7.9.3 version: 7.9.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + yaml: + specifier: ^2.8.1 + version: 2.8.1 devDependencies: '@eslint/js': specifier: ^9.33.0 @@ -41,7 +44,7 @@ importers: version: 19.1.9(@types/react@19.1.12) '@vitejs/plugin-react': specifier: ^5.0.0 - version: 5.0.2(vite@7.1.5(@types/node@24.10.1)) + version: 5.0.2(vite@7.1.5(@types/node@24.10.1)(yaml@2.8.1)) eslint: specifier: ^9.33.0 version: 9.35.0 @@ -62,7 +65,7 @@ importers: version: 8.43.0(eslint@9.35.0)(typescript@5.8.3) vite: specifier: ^7.1.2 - version: 7.1.5(@types/node@24.10.1) + version: 7.1.5(@types/node@24.10.1)(yaml@2.8.1) packages: @@ -1402,6 +1405,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2005,7 +2013,7 @@ snapshots: '@typescript-eslint/types': 8.43.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@5.0.2(vite@7.1.5(@types/node@24.10.1))': + '@vitejs/plugin-react@5.0.2(vite@7.1.5(@types/node@24.10.1)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -2013,7 +2021,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.34 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.5(@types/node@24.10.1) + vite: 7.1.5(@types/node@24.10.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -2680,7 +2688,7 @@ snapshots: dependencies: punycode: 2.3.1 - vite@7.1.5(@types/node@24.10.1): + vite@7.1.5(@types/node@24.10.1)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -2691,6 +2699,7 @@ snapshots: optionalDependencies: '@types/node': 24.10.1 fsevents: 2.3.3 + yaml: 2.8.1 which@2.0.2: dependencies: @@ -2700,4 +2709,6 @@ snapshots: yallist@3.1.1: {} + yaml@2.8.1: {} + yocto-queue@0.1.0: {} diff --git a/client/public/Projects/DummyProject/file-graph-data.json b/client/public/Projects/DummyProject/file-graph-data.json new file mode 100644 index 0000000..fb3e743 --- /dev/null +++ b/client/public/Projects/DummyProject/file-graph-data.json @@ -0,0 +1,46 @@ +{ + "nodes": [ + { "id": "file_1", "group": "file", "value": 2 }, + { "id": "file_2", "group": "file", "value": 2 }, + { "id": "file_3", "group": "file", "value": 2 }, + { "id": "file_4", "group": "file", "value": 2 }, + { "id": "file_5", "group": "file", "value": 2 }, + { "id": "file_6", "group": "file", "value": 2 }, + { "id": "file_7", "group": "file", "value": 2 }, + { "id": "file_8", "group": "file", "value": 2 }, + { "id": "file_9", "group": "file", "value": 2 }, + { "id": "file_10", "group": "file", "value": 3 }, + { "id": "file_11", "group": "file", "value": 4 }, + { "id": "file_12", "group": "file", "value": 4 }, + { "id": "file_13", "group": "file", "value": 2 }, + { "id": "file_14", "group": "file", "value": 2 }, + { "id": "file_15", "group": "file", "value": 2 }, + { "id": "file_16", "group": "file", "value": 2 }, + { "id": "file_17", "group": "file", "value": 2 }, + { "id": "file_18", "group": "file", "value": 2 }, + { "id": "file_19", "group": "file", "value": 2 }, + { "id": "file_20", "group": "file", "value": 2 }, + { "id": "file_21", "group": "file", "value": 2 }, + { "id": "file_22", "group": "file", "value": 3 }, + { "id": "file_23", "group": "file", "value": 4 }, + { "id": "file_24", "group": "file", "value": 8 } + ], + "links": [ + { "source": "file_4", "target": "file_2", "value": 1 }, + { "source": "file_4", "target": "file_3", "value": 1 }, + { "source": "file_4", "target": "file_1", "value": 1 }, + { "source": "file_4", "target": "file_6", "value": 1 }, + { "source": "file_4", "target": "issue_2", "value": 1 }, + { "source": "file_6", "target": "file_1", "value": 1 }, + { "source": "file_6", "target": "file_3", "value": 1 }, + { "source": "file_6", "target": "file_5", "value": 1 }, + { "source": "file_7", "target": "file_8", "value": 2 }, + { "source": "file_8", "target": "file_9", "value": 2 }, + { "source": "file_9", "target": "file_10", "value": 2 }, + { "source": "file_9", "target": "file_22", "value": 2 }, + { "source": "file_10", "target": "file_11", "value": 3 }, + { "source": "file_11", "target": "file_12", "value": 4 }, + { "source": "file_23", "target": "issue_1", "value": 4 }, + { "source": "file_23", "target": "file_24", "value": 4 } + ] +} diff --git a/client/public/Projects/DummyProject/issue-graph-data.json b/client/public/Projects/DummyProject/issue-graph-data.json new file mode 100644 index 0000000..9ca6cbe --- /dev/null +++ b/client/public/Projects/DummyProject/issue-graph-data.json @@ -0,0 +1,11 @@ +{ + "nodes": [ + { "id": "issue_1", "group": "issue", "value": 4 }, + { "id": "issue_2", "group": "issue", "value": 2 }, + { "id": "issue_3", "group": "issue", "value": 2 } + ], + "links": [ + { "source": "issue_1", "target": "issue_2", "value": 1 }, + { "source": "issue_2", "target": "issue_3", "value": 1 } + ] +} diff --git a/client/public/Projects/DummyProject/mail-graph-data.json b/client/public/Projects/DummyProject/mail-graph-data.json new file mode 100644 index 0000000..00f716e --- /dev/null +++ b/client/public/Projects/DummyProject/mail-graph-data.json @@ -0,0 +1,23 @@ +{ + "nodes": [ + { "id": "mail_1", "group": "mail", "value": 2 }, + { "id": "mail_2", "group": "mail", "value": 2 }, + { "id": "mail_3", "group": "mail", "value": 2 }, + { "id": "mail_4", "group": "mail", "value": 2 }, + { "id": "mail_5", "group": "mail", "value": 2 }, + { "id": "mail_6", "group": "mail", "value": 2 }, + { "id": "mail_7", "group": "mail", "value": 2 }, + { "id": "mail_8", "group": "mail", "value": 2 } + ], + "links": [ + { "source": "mail_2", "target": "mail_1", "value": 1 }, + { "source": "mail_3", "target": "mail_1", "value": 1 }, + { "source": "mail_4", "target": "mail_1", "value": 1 }, + { "source": "mail_4", "target": "mail_1", "value": 1 }, + { "source": "mail_5", "target": "mail_1", "value": 1 }, + { "source": "mail_5", "target": "mail_4", "value": 1 }, + { "source": "mail_5", "target": "mail_1", "value": 1 }, + { "source": "mail_6", "target": "mail_7", "value": 1 }, + { "source": "mail_7", "target": "mail_8", "value": 1 } + ] +} diff --git a/client/public/Projects/DummyProject/person-graph-data.json b/client/public/Projects/DummyProject/person-graph-data.json new file mode 100644 index 0000000..50a24a3 --- /dev/null +++ b/client/public/Projects/DummyProject/person-graph-data.json @@ -0,0 +1,18 @@ +{ + "nodes": [ + { "id": "person_1", "group": "people", "value": 2 }, + { "id": "person_2", "group": "people", "value": 2 }, + { "id": "person_3", "group": "people", "value": 2 }, + { "id": "person_4", "group": "people", "value": 2 } + ], + "links": [ + { "source": "person_1", "target": "mail_4", "value": 1 }, + { "source": "person_1", "target": "file_2", "value": 1 }, + { "source": "person_2", "target": "file_5", "value": 1 }, + { "source": "person_2", "target": "mail_2", "value": 1 }, + { "source": "person_3", "target": "mail_1", "value": 1 }, + { "source": "person_3", "target": "issue_1", "value": 1 }, + { "source": "person_4", "target": "file_7", "value": 1 }, + { "source": "person_4", "target": "file_23", "value": 1 } + ] +} diff --git a/client/public/sample-data.json b/client/public/sample-data.json deleted file mode 100644 index 5340d4a..0000000 --- a/client/public/sample-data.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "nodes": [ - { - "id": 1, - "name": "A" - }, - { - "id": 2, - "name": "B" - }, - { - "id": 3, - "name": "C" - }, - { - "id": 4, - "name": "D" - }, - { - "id": 5, - "name": "E" - }, - { - "id": 6, - "name": "F" - }, - { - "id": 7, - "name": "G" - }, - { - "id": 8, - "name": "H" - }, - { - "id": 9, - "name": "I" - }, - { - "id": 10, - "name": "J" - } - ], - "links": [ - - { - "source": 1, - "target": 2 - }, - { - "source": 1, - "target": 5 - }, - { - "source": 1, - "target": 6 - }, - - { - "source": 2, - "target": 3 - }, - { - "source": 2, - "target": 7 - } - , - - { - "source": 3, - "target": 4 - }, - { - "source": 8, - "target": 3 - } - , - { - "source": 4, - "target": 5 - } - , - - { - "source": 4, - "target": 9 - }, - { - "source": 5, - "target": 10 - } - ] -} \ No newline at end of file diff --git a/client/src/app/routes/landing.tsx b/client/src/app/routes/landing.tsx index 74fa32f..a1e8126 100644 --- a/client/src/app/routes/landing.tsx +++ b/client/src/app/routes/landing.tsx @@ -1,6 +1,6 @@ import { NetworkGraphProvider } from "@/features/NetworkGraph/NetworkGraphContext.tsx"; import { NetworkGraph } from "@/features/NetworkGraph"; -import { NetworkGraphToolbar } from "@/features/NetworkGraph/NetworkGraphToolbar.tsx"; +import { NetworkGraphToolbar } from "@/features/NetworkGraph/components/NetworkGraphToolbar.tsx"; const Landing = () => { return ( diff --git a/client/src/config/app.ts b/client/src/config/app.ts new file mode 100644 index 0000000..7d02693 --- /dev/null +++ b/client/src/config/app.ts @@ -0,0 +1,13 @@ +import { parse } from 'yaml'; +import rawConfig from './user.config.yaml?raw'; + +const config = parse(rawConfig) as { + networkGraph: { + projectsDirectory: string, + targetProject: string; + }; +}; + +export const TARGET_PROJECT = config.networkGraph.targetProject; + +export const PROJECTS_DIRECTORY = config.networkGraph.projectsDirectory; \ No newline at end of file diff --git a/client/src/config/user.config.yaml b/client/src/config/user.config.yaml new file mode 100644 index 0000000..6b1cb70 --- /dev/null +++ b/client/src/config/user.config.yaml @@ -0,0 +1,3 @@ +networkGraph: + projectsDirectory: "Projects" + targetProject: "DummyProject" \ No newline at end of file diff --git a/client/src/features/NetworkGraph/NetworkGraphContext.tsx b/client/src/features/NetworkGraph/NetworkGraphContext.tsx index a52cb41..840e72b 100644 --- a/client/src/features/NetworkGraph/NetworkGraphContext.tsx +++ b/client/src/features/NetworkGraph/NetworkGraphContext.tsx @@ -1,32 +1,11 @@ -import { type ReactNode, createContext, useContext, useCallback, useState, useEffect } from 'react'; +import { type ReactNode, createContext, useContext } from 'react'; import { type NetworkGraphData } from '@/types/network-graph.types.ts'; +import {useJsonFiles} from "@/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts"; const NetworkGraphContext = createContext(null); interface NetworkGraphContextValue { data: NetworkGraphData; - loadFromFiles: (files: FileList | File[]) => Promise; -} - -const STORAGE_KEY = "networkGraphData"; - -function saveGraphToStorage(data: NetworkGraphData) { - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); - } catch (err) { - console.error("Failed to save graph to localStorage", err); - } -} - -function loadGraphFromStorage(): NetworkGraphData | null { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return null; - return JSON.parse(raw) as NetworkGraphData; - } catch (err) { - console.error("Failed to load graph from localStorage", err); - return null; - } } export function useNetworkGraph() { @@ -40,46 +19,12 @@ export function useNetworkGraph() { } export function NetworkGraphProvider({ children }: { children: ReactNode }) { - const initialData: NetworkGraphData = { nodes: [], links: [] }; - const [data, setGraph] = useState(initialData); - - useEffect(() => { - const stored = loadGraphFromStorage(); - if (stored) { - setGraph(stored); - } - }, []); - - const loadFromFiles = useCallback(async (filesLike : FileList | File[]) => { - try { - const files = Array.from(filesLike); - - const allNodes: NetworkGraphData["nodes"] = []; - const allLinks: NetworkGraphData["links"] = []; - - for (const file of files) { - if (!file.name.toLowerCase().endsWith(".json")) continue; - - const text = await file.text(); - const json = JSON.parse(text) as NetworkGraphData; // or a smaller "chunk" type - - allNodes.push(...json.nodes); - allLinks.push(...json.links); - } - - const graph: NetworkGraphData = { nodes: allNodes, links: allLinks }; + const data = useJsonFiles(); - setGraph(graph); - saveGraphToStorage(graph); - } catch (err) { - console.error(err); - } - }, []); const value: NetworkGraphContextValue = { - data, - loadFromFiles, + data }; return ( diff --git a/client/src/features/NetworkGraph/NetworkGraph.css b/client/src/features/NetworkGraph/components/NetworkGraph.css similarity index 100% rename from client/src/features/NetworkGraph/NetworkGraph.css rename to client/src/features/NetworkGraph/components/NetworkGraph.css diff --git a/client/src/features/NetworkGraph/NetworkGraph.tsx b/client/src/features/NetworkGraph/components/NetworkGraph.tsx similarity index 80% rename from client/src/features/NetworkGraph/NetworkGraph.tsx rename to client/src/features/NetworkGraph/components/NetworkGraph.tsx index a7ce281..6801282 100644 --- a/client/src/features/NetworkGraph/NetworkGraph.tsx +++ b/client/src/features/NetworkGraph/components/NetworkGraph.tsx @@ -1,15 +1,15 @@ import { useRef } from 'react'; import './NetworkGraph.css'; -import { useNetworkGraph } from "./NetworkGraphContext.tsx"; -import {useNetworkGraphSimulation} from "@/hooks/NetworkGraph/useNetworkGraphSimulation.ts"; -import {useNetworkGraphInteractions} from "@/hooks/NetworkGraph/useNetworkGraphInteractions.ts"; +import { useNetworkGraph } from "../NetworkGraphContext.tsx"; +import { useNetworkGraphSimulation } from "@/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts"; +import { useNetworkGraphInteractions } from "@/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts"; export const NetworkGraph = () => { const { data } = useNetworkGraph(); + const { nodes, links } = data ?? { nodes: [], links: [] }; const canvasRef = useRef(null); - useNetworkGraphSimulation({ nodes, links, canvasRef }); useNetworkGraphInteractions({ nodes, links, canvasRef }); diff --git a/client/src/features/NetworkGraph/NetworkGraphToolbar.css b/client/src/features/NetworkGraph/components/NetworkGraphToolbar.css similarity index 100% rename from client/src/features/NetworkGraph/NetworkGraphToolbar.css rename to client/src/features/NetworkGraph/components/NetworkGraphToolbar.css diff --git a/client/src/features/NetworkGraph/NetworkGraphToolbar.tsx b/client/src/features/NetworkGraph/components/NetworkGraphToolbar.tsx similarity index 87% rename from client/src/features/NetworkGraph/NetworkGraphToolbar.tsx rename to client/src/features/NetworkGraph/components/NetworkGraphToolbar.tsx index 855e00c..a1861a8 100644 --- a/client/src/features/NetworkGraph/NetworkGraphToolbar.tsx +++ b/client/src/features/NetworkGraph/components/NetworkGraphToolbar.tsx @@ -1,5 +1,5 @@ import "./NetworkGraphToolbar.css"; -import { useNetworkGraph } from "./NetworkGraphContext.tsx"; +import { useNetworkGraph } from "../NetworkGraphContext.tsx"; export const NetworkGraphToolbar = () => { const { loadFromFiles } = useNetworkGraph(); diff --git a/client/src/hooks/NetworkGraph/useDummyData.ts b/client/src/features/NetworkGraph/hooks/useDummyData.ts similarity index 100% rename from client/src/hooks/NetworkGraph/useDummyData.ts rename to client/src/features/NetworkGraph/hooks/useDummyData.ts diff --git a/client/src/hooks/NetworkGraph/useNetworkGraphInteractions.ts b/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts similarity index 100% rename from client/src/hooks/NetworkGraph/useNetworkGraphInteractions.ts rename to client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts diff --git a/client/src/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts b/client/src/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts new file mode 100644 index 0000000..a864d50 --- /dev/null +++ b/client/src/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; +import { PROJECTS_DIRECTORY, TARGET_PROJECT } from "@/config/app.ts"; +import type {NetworkGraphData} from "@/types/network-graph.types.ts"; + +const GRAPH_FILES = ['file-graph-data.json', 'issue-graph-data.json', 'mail-graph-data.json', 'person-graph-data.json']; + +export function useJsonFiles(){ + const [graphData, setGraphData] = useState({ nodes: [], links: [] }); + + useEffect(() => { + async function load() { + const result = await readAllJsonsFromDirectory(); + + const compiledGraphData : NetworkGraphData = { nodes: [], links: [] }; + + for (const value of Object.values(result)) { + compiledGraphData.nodes.push(... value.nodes); + compiledGraphData.links.push(... value.links); + } + + setGraphData(compiledGraphData); + } + load(); + }, []); + + return graphData; +} + +async function readAllJsonsFromDirectory() { + const BASE_PATH = `/${PROJECTS_DIRECTORY}/${TARGET_PROJECT}`; + + const entries = await Promise.all( + GRAPH_FILES.map(async (fileName) => { + const res = await fetch(`${BASE_PATH}/${fileName}`); + + if (!res.ok) { + throw new Error(`Failed to load ${fileName}: ${res.status} ${res.statusText}`); + } + + const json = await res.json(); + return [fileName, json] as const; + }), + ); + + // object keyed by file name: + return Object.fromEntries(entries); +} \ No newline at end of file diff --git a/client/src/hooks/NetworkGraph/useNetworkGraphSimulation.ts b/client/src/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts similarity index 100% rename from client/src/hooks/NetworkGraph/useNetworkGraphSimulation.ts rename to client/src/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts diff --git a/client/src/features/NetworkGraph/index.ts b/client/src/features/NetworkGraph/index.ts index 43375e5..9f18d84 100644 --- a/client/src/features/NetworkGraph/index.ts +++ b/client/src/features/NetworkGraph/index.ts @@ -1 +1 @@ -export * from './NetworkGraph.tsx'; \ No newline at end of file +export { NetworkGraph } from './components/NetworkGraph.tsx'; \ No newline at end of file diff --git a/client/src/types/network-graph.types.ts b/client/src/types/network-graph.types.ts index ec26081..9a7ece0 100644 --- a/client/src/types/network-graph.types.ts +++ b/client/src/types/network-graph.types.ts @@ -7,8 +7,8 @@ export interface Node extends SimulationNodeDatum { } export type Link = SimulationLinkDatum & { - source: Node; - target: Node; + source: string; + target: string; value: number; // added property }; From d2be7179bff3fe101289c57ea275ab431c3f0851 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:25:56 -1000 Subject: [PATCH 23/29] Added unhighlight feature for all subgraph nodes Highlight mode will toggle off upon double-clicking ANY subgraph node. Whereas before, only double-clicking the original node would toggle it. --- .../NetworkGraph/components/NetworkGraph.tsx | 13 +---- .../components/NetworkGraphToolbar.tsx | 8 --- .../hooks/useNetworkGraphInteractions.ts | 1 + .../NetworkGraph/lib/networkGraphUtils.ts | 50 ++++++++++++++----- client/src/types/network-graph.types.ts | 15 +++--- 5 files changed, 47 insertions(+), 40 deletions(-) diff --git a/client/src/features/NetworkGraph/components/NetworkGraph.tsx b/client/src/features/NetworkGraph/components/NetworkGraph.tsx index 6801282..abcff66 100644 --- a/client/src/features/NetworkGraph/components/NetworkGraph.tsx +++ b/client/src/features/NetworkGraph/components/NetworkGraph.tsx @@ -10,6 +10,7 @@ export const NetworkGraph = () => { const { nodes, links } = data ?? { nodes: [], links: [] }; const canvasRef = useRef(null); + useNetworkGraphSimulation({ nodes, links, canvasRef }); useNetworkGraphInteractions({ nodes, links, canvasRef }); @@ -21,18 +22,6 @@ export const NetworkGraph = () => { }}> - {/*{popup && (
Heres a popup
)}*/} - ) } diff --git a/client/src/features/NetworkGraph/components/NetworkGraphToolbar.tsx b/client/src/features/NetworkGraph/components/NetworkGraphToolbar.tsx index a1861a8..93418f9 100644 --- a/client/src/features/NetworkGraph/components/NetworkGraphToolbar.tsx +++ b/client/src/features/NetworkGraph/components/NetworkGraphToolbar.tsx @@ -1,17 +1,9 @@ import "./NetworkGraphToolbar.css"; -import { useNetworkGraph } from "../NetworkGraphContext.tsx"; export const NetworkGraphToolbar = () => { - const { loadFromFiles } = useNetworkGraph(); - - const handleChange: React.ChangeEventHandler = async (e) => { - if (!e.target.files) return; - await loadFromFiles(e.target.files); - } return (
-
) }; \ No newline at end of file diff --git a/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts b/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts index b838e4c..363b826 100644 --- a/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts +++ b/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts @@ -42,6 +42,7 @@ export function useNetworkGraphInteractions( const handleDblClick = (event: MouseEvent) => { const [x, y] = pointer(event, canvas); const hit = findNodeAt(nodes, nodeRadiusMultiplier, x, y,); + if (hit) { updateTransparency(hit, transparentNodeMap, nodeRelationshipMap); drawGraph(context, canvas, nodes, links, transparentNodeMap, nodeRadiusMultiplier); diff --git a/client/src/features/NetworkGraph/lib/networkGraphUtils.ts b/client/src/features/NetworkGraph/lib/networkGraphUtils.ts index 108acc6..a2237fe 100644 --- a/client/src/features/NetworkGraph/lib/networkGraphUtils.ts +++ b/client/src/features/NetworkGraph/lib/networkGraphUtils.ts @@ -21,18 +21,45 @@ export function updateTransparency( ){ if (!hasPos(hitNode)) return; - const isTransparent = transparentNodeMap.get(hitNode.id); + const current = transparentNodeMap.get(hitNode.id) ?? 0; - if (isTransparent === 0) { + // Are we currently in "normal" mode? (everything opaque) + const allOpaque = Array.from(transparentNodeMap.values()).every(v => v === 0); + + const highlightNeighborhood = () => { + // fade everything for (const key of transparentNodeMap.keys()) { transparentNodeMap.set(key, 1); } - transparentNodeMap.set(hitNode.id, 0); + // make hit node + its related nodes opaque const relatedNodeSet = nodeRelationshipMap.get(hitNode.id); + transparentNodeMap.set(hitNode.id, 0); relatedNodeSet?.forEach(nodeId => { transparentNodeMap.set(nodeId, 0); }); + }; + + const clearAll = () => { + for (const key of transparentNodeMap.keys()) { + transparentNodeMap.set(key, 0); + } + }; + + if (allOpaque) { + // First time: go into "highlight" mode + highlightNeighborhood(); + return; + } + + // We're already in highlight mode (some nodes have value 1) + + if (current === 0) { + // Clicked *inside* the highlighted group → reset to fully opaque + clearAll(); + } else { + // Clicked on a faded node → switch highlight to this node's group instead + highlightNeighborhood(); } } @@ -96,8 +123,8 @@ export function buildRelationshipMap(nodes: Node[], links: Link[]) { const set = new Set(); links.forEach((link) => { - const sourceId = isNode(link.source) ? link.source.id : link.source; - const targetId = isNode(link.target) ? link.target.id : link.target; + const sourceId = link.source.id; + const targetId = link.target.id; if (sourceId === node.id) set.add(targetId); else if (targetId === node.id) set.add(sourceId); @@ -165,10 +192,6 @@ const drawNodeByGroup = ( context.restore(); }; -function isNode(v: Link["source"]): v is Node { - return typeof v === "object" && v !== null; -} - function hasPos(n: Node): n is Node & { x: number; y: number } { return n.x != null && n.y != null; } @@ -187,13 +210,12 @@ export function drawGraph ( context.strokeStyle = "grey"; links.forEach((link) => { - if (!isNode(link.source) || !isNode(link.target)) return; - const s = link.source; const t = link.target; - if (!hasPos(s) || !hasPos(t)) return; context.beginPath(); + if (isNullOrUndefined(s.x) && isNullOrUndefined(s.y)) return; + context.moveTo(s.x, s.y); context.lineTo(t.x, t.y); context.stroke(); @@ -220,6 +242,10 @@ export function findNodeAt(nodes: Node[], nodeRadiusMultiplier: number, x: numbe return undefined; } +function isNullOrUndefined(value: T | null | undefined): value is null | undefined { + return value == null; +} + export function clearForces(sim: Simulation) { sim .force('x', null) diff --git a/client/src/types/network-graph.types.ts b/client/src/types/network-graph.types.ts index 9a7ece0..45f118c 100644 --- a/client/src/types/network-graph.types.ts +++ b/client/src/types/network-graph.types.ts @@ -2,15 +2,17 @@ import type { SimulationNodeDatum, SimulationLinkDatum } from "d3"; export interface Node extends SimulationNodeDatum { id: string; + x: number; + y: number; group: Group; // added property value: number; // added property } -export type Link = SimulationLinkDatum & { - source: string; - target: string; - value: number; // added property -}; +export interface Link extends SimulationLinkDatum { + source: Node; + target: Node; + value: number; +} export type NetworkGraphData = { nodes: Node[]; @@ -19,9 +21,6 @@ export type NetworkGraphData = { export type Group = 'people' | 'mail' | 'file' | 'issue'; -export type NetworkGraphProps = { - data: NetworkGraphData; -}; export type HubLink = { source: Node; target: Node From 66b0418d2e7d6bc5a73892ef8bd8960c31e49580 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:06:11 -1000 Subject: [PATCH 24/29] Implemented moveable Subgraph Overlay Installed dnd-kit, a library that allows components to be draggable New draggable overlay component created. Network Graph uses modular component "FloatingPanel" to render overlay. --- .gitignore | 3 +- client/package.json | 7 +- client/pnpm-lock.yaml | 596 +++++++++++++++++- .../ui/FloatingOverlay/FloatingOverlay.tsx | 66 ++ .../NetworkGraph/components/NetworkGraph.tsx | 14 +- 5 files changed, 673 insertions(+), 13 deletions(-) create mode 100644 client/src/components/ui/FloatingOverlay/FloatingOverlay.tsx diff --git a/.gitignore b/.gitignore index 5c3ff2b..03fd5b4 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ dist-ssr *.ntvs* *.njsproj *.sln -*.sw? \ No newline at end of file +*.sw? +/kaiaulu_react.iml diff --git a/client/package.json b/client/package.json index ca7f238..1d20ec1 100644 --- a/client/package.json +++ b/client/package.json @@ -10,6 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/utilities": "^3.2.2", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "^7.3.5", "@tanstack/react-query": "^5.90.3", "d3": "^7.9.0", "react": "^19.1.1", @@ -30,6 +35,6 @@ "globals": "^16.3.0", "typescript": "~5.8.3", "typescript-eslint": "^8.39.1", - "vite": "^7.1.2" + "vite": "7.1.11" } } diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 6ee01c7..c6b165c 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -8,6 +8,21 @@ importers: .: dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.1.1) + '@emotion/react': + specifier: ^11.14.0 + version: 11.14.0(@types/react@19.1.12)(react@19.1.1) + '@emotion/styled': + specifier: ^11.14.1 + version: 11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) + '@mui/material': + specifier: ^7.3.5 + version: 7.3.5(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@tanstack/react-query': specifier: ^5.90.3 version: 5.90.3(react@19.1.1) @@ -44,7 +59,7 @@ importers: version: 19.1.9(@types/react@19.1.12) '@vitejs/plugin-react': specifier: ^5.0.0 - version: 5.0.2(vite@7.1.5(@types/node@24.10.1)(yaml@2.8.1)) + version: 5.0.2(vite@7.1.11(@types/node@24.10.1)(yaml@2.8.1)) eslint: specifier: ^9.33.0 version: 9.35.0 @@ -64,8 +79,8 @@ importers: specifier: ^8.39.1 version: 8.43.0(eslint@9.35.0)(typescript@5.8.3) vite: - specifier: ^7.1.2 - version: 7.1.5(@types/node@24.10.1)(yaml@2.8.1) + specifier: 7.1.11 + version: 7.1.11(@types/node@24.10.1)(yaml@2.8.1) packages: @@ -140,6 +155,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -152,6 +171,76 @@ packages: resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/styled@11.14.1': + resolution: {integrity: sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + '@esbuild/aix-ppc64@0.25.9': resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} engines: {node: '>=18'} @@ -378,6 +467,86 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@mui/core-downloads-tracker@7.3.5': + resolution: {integrity: sha512-kOLwlcDPnVz2QMhiBv0OQ8le8hTCqKM9cRXlfVPL91l3RGeOsxrIhNRsUt3Xb8wb+pTVUolW+JXKym93vRKxCw==} + + '@mui/material@7.3.5': + resolution: {integrity: sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@mui/material-pigment-css': ^7.3.5 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@mui/material-pigment-css': + optional: true + '@types/react': + optional: true + + '@mui/private-theming@7.3.5': + resolution: {integrity: sha512-cTx584W2qrLonwhZLbEN7P5pAUu0nZblg8cLBlTrZQ4sIiw8Fbvg7GvuphQaSHxPxrCpa7FDwJKtXdbl2TSmrA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/styled-engine@7.3.5': + resolution: {integrity: sha512-zbsZ0uYYPndFCCPp2+V3RLcAN6+fv4C8pdwRx6OS3BwDkRCN8WBehqks7hWyF3vj1kdQLIWrpdv/5Y0jHRxYXQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + + '@mui/system@7.3.5': + resolution: {integrity: sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + + '@mui/types@7.4.8': + resolution: {integrity: sha512-ZNXLBjkPV6ftLCmmRCafak3XmSn8YV0tKE/ZOhzKys7TZXUiE0mZxlH8zKDo6j6TTUaDnuij68gIG+0Ucm7Xhw==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/utils@7.3.5': + resolution: {integrity: sha512-jisvFsEC3sgjUjcPnR4mYfhzjCDIudttSGSbe1o/IXFNu0kZuR+7vqQI0jg8qtcVZBHWrwTfvAZj9MNMumcq1g==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -390,6 +559,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@rolldown/pluginutils@1.0.0-beta.34': resolution: {integrity: sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA==} @@ -623,11 +795,22 @@ packages: '@types/node@24.10.1': resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + '@types/react-dom@19.1.9': resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} peerDependencies: '@types/react': ^19.0.0 + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + '@types/react@19.1.12': resolution: {integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==} @@ -716,6 +899,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -745,6 +932,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -759,6 +950,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -766,6 +960,10 @@ packages: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -915,9 +1113,15 @@ packages: delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + electron-to-chromium@1.5.215: resolution: {integrity: sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==} + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + esbuild@0.25.9: resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} engines: {node: '>=18'} @@ -1017,6 +1221,9 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1033,6 +1240,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1060,6 +1270,13 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -1084,6 +1301,13 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1114,6 +1338,9 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -1132,6 +1359,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1139,6 +1369,10 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1171,6 +1405,10 @@ packages: node-releases@2.0.20: resolution: {integrity: sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1187,6 +1425,10 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1195,6 +1437,13 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1214,6 +1463,9 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1226,6 +1478,12 @@ packages: peerDependencies: react: ^19.1.1 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@19.2.0: + resolution: {integrity: sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==} + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -1247,6 +1505,12 @@ packages: react-dom: optional: true + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + react@19.1.1: resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} engines: {node: '>=0.10.0'} @@ -1255,6 +1519,11 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1303,14 +1572,25 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -1325,6 +1605,9 @@ packages: peerDependencies: typescript: '>=4.8.4' + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1353,8 +1636,8 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - vite@7.1.5: - resolution: {integrity: sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==} + vite@7.1.11: + resolution: {integrity: sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -1405,6 +1688,10 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + yaml@2.8.1: resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} engines: {node: '>= 14.6'} @@ -1505,6 +1792,8 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 + '@babel/runtime@7.28.4': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -1528,6 +1817,107 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@dnd-kit/accessibility@3.1.1(react@19.1.1)': + dependencies: + react: 19.1.1 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.1.1) + '@dnd-kit/utilities': 3.2.2(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.1.1)': + dependencies: + react: 19.1.1 + tslib: 2.8.1 + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/runtime': 7.28.4 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/is-prop-valid@1.4.0': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.1) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.12 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.1.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@emotion/babel-plugin': 11.13.5 + '@emotion/is-prop-valid': 1.4.0 + '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.1) + '@emotion/utils': 1.4.2 + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.12 + transitivePeerDependencies: + - supports-color + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.1.1)': + dependencies: + react: 19.1.1 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + '@esbuild/aix-ppc64@0.25.9': optional: true @@ -1680,6 +2070,85 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mui/core-downloads-tracker@7.3.5': {} + + '@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/core-downloads-tracker': 7.3.5 + '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) + '@mui/types': 7.4.8(@types/react@19.1.12) + '@mui/utils': 7.3.5(@types/react@19.1.12)(react@19.1.1) + '@popperjs/core': 2.11.8 + '@types/react-transition-group': 4.4.12(@types/react@19.1.12) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-is: 19.2.0 + react-transition-group: 4.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) + '@types/react': 19.1.12 + + '@mui/private-theming@7.3.5(@types/react@19.1.12)(react@19.1.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/utils': 7.3.5(@types/react@19.1.12)(react@19.1.1) + prop-types: 15.8.1 + react: 19.1.1 + optionalDependencies: + '@types/react': 19.1.12 + + '@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/sheet': 1.4.0 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.1.1 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) + + '@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/private-theming': 7.3.5(@types/react@19.1.12)(react@19.1.1) + '@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(react@19.1.1) + '@mui/types': 7.4.8(@types/react@19.1.12) + '@mui/utils': 7.3.5(@types/react@19.1.12)(react@19.1.1) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.1.1 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) + '@types/react': 19.1.12 + + '@mui/types@7.4.8(@types/react@19.1.12)': + dependencies: + '@babel/runtime': 7.28.4 + optionalDependencies: + '@types/react': 19.1.12 + + '@mui/utils@7.3.5(@types/react@19.1.12)(react@19.1.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/types': 7.4.8(@types/react@19.1.12) + '@types/prop-types': 15.7.15 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.1.1 + react-is: 19.2.0 + optionalDependencies: + '@types/react': 19.1.12 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -1692,6 +2161,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@popperjs/core@2.11.8': {} + '@rolldown/pluginutils@1.0.0-beta.34': {} '@rollup/rollup-android-arm-eabi@4.50.1': @@ -1912,10 +2383,18 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/parse-json@4.0.2': {} + + '@types/prop-types@15.7.15': {} + '@types/react-dom@19.1.9(@types/react@19.1.12)': dependencies: '@types/react': 19.1.12 + '@types/react-transition-group@4.4.12(@types/react@19.1.12)': + dependencies: + '@types/react': 19.1.12 + '@types/react@19.1.12': dependencies: csstype: 3.1.3 @@ -2013,7 +2492,7 @@ snapshots: '@typescript-eslint/types': 8.43.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@5.0.2(vite@7.1.5(@types/node@24.10.1)(yaml@2.8.1))': + '@vitejs/plugin-react@5.0.2(vite@7.1.11(@types/node@24.10.1)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -2021,7 +2500,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.34 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.5(@types/node@24.10.1)(yaml@2.8.1) + vite: 7.1.11(@types/node@24.10.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -2044,6 +2523,12 @@ snapshots: argparse@2.0.1: {} + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.28.4 + cosmiconfig: 7.1.0 + resolve: 1.22.11 + balanced-match@1.0.2: {} brace-expansion@1.1.12: @@ -2075,6 +2560,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2085,10 +2572,20 @@ snapshots: concat-map@0.0.1: {} + convert-source-map@1.9.0: {} + convert-source-map@2.0.0: {} cookie@1.0.2: {} + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2259,8 +2756,17 @@ snapshots: dependencies: robust-predicates: 3.0.2 + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.28.4 + csstype: 3.1.3 + electron-to-chromium@1.5.215: {} + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + esbuild@0.25.9: optionalDependencies: '@esbuild/aix-ppc64': 0.25.9 @@ -2399,6 +2905,8 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-root@1.1.0: {} + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -2414,6 +2922,8 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} glob-parent@5.1.2: @@ -2432,6 +2942,14 @@ snapshots: has-flag@4.0.0: {} + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -2449,6 +2967,12 @@ snapshots: internmap@2.0.3: {} + is-arrayish@0.2.1: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -2469,6 +2993,8 @@ snapshots: json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -2484,12 +3010,18 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lines-and-columns@1.2.4: {} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 lodash.merge@4.6.2: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -2517,6 +3049,8 @@ snapshots: node-releases@2.0.20: {} + object-assign@4.1.1: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2538,10 +3072,21 @@ snapshots: dependencies: callsites: 3.1.0 + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + path-exists@4.0.0: {} path-key@3.1.1: {} + path-parse@1.0.7: {} + + path-type@4.0.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -2556,6 +3101,12 @@ snapshots: prelude-ls@1.2.1: {} + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -2565,6 +3116,10 @@ snapshots: react: 19.1.1 scheduler: 0.26.0 + react-is@16.13.1: {} + + react-is@19.2.0: {} + react-refresh@0.17.0: {} react-router-dom@7.9.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1): @@ -2581,10 +3136,25 @@ snapshots: optionalDependencies: react-dom: 19.1.1(react@19.1.1) + react-transition-group@4.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + '@babel/runtime': 7.28.4 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react@19.1.1: {} resolve-from@4.0.0: {} + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + reusify@1.1.0: {} robust-predicates@3.0.2: {} @@ -2640,12 +3210,18 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.5.7: {} + strip-json-comments@3.1.1: {} + stylis@4.2.0: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -2659,6 +3235,8 @@ snapshots: dependencies: typescript: 5.8.3 + tslib@2.8.1: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -2688,7 +3266,7 @@ snapshots: dependencies: punycode: 2.3.1 - vite@7.1.5(@types/node@24.10.1)(yaml@2.8.1): + vite@7.1.11(@types/node@24.10.1)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -2709,6 +3287,8 @@ snapshots: yallist@3.1.1: {} + yaml@1.10.2: {} + yaml@2.8.1: {} yocto-queue@0.1.0: {} diff --git a/client/src/components/ui/FloatingOverlay/FloatingOverlay.tsx b/client/src/components/ui/FloatingOverlay/FloatingOverlay.tsx new file mode 100644 index 0000000..8ef0f25 --- /dev/null +++ b/client/src/components/ui/FloatingOverlay/FloatingOverlay.tsx @@ -0,0 +1,66 @@ +import Paper from "@mui/material/Paper"; +import { CSS } from "@dnd-kit/utilities"; +import { useDraggable, useDndMonitor } from '@dnd-kit/core'; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; +import {useState} from "react"; + +type FloatingPanelProps = { + id: string; + title?: React.ReactNode; + children?: React.ReactNode; +}; + +export const FloatingPanel: React.FC = ({ title, children, id }) => { + const [position, setPosition] = useState({ x: 16, y: 16 }); + + const {attributes, listeners, setNodeRef, transform } = useDraggable({ id }); + + + useDndMonitor({ + onDragEnd(event) { + if (event.active.id !== id) return; + const { delta } = event; + + setPosition((prev) => ({ + x: prev.x + delta.x, + y: prev.y + delta.y, + })); + }, + }); + + const dragTransform = transform ?? { x: 0, y: 0 }; + + const style: React.CSSProperties = { + transform: CSS.Translate.toString({ + scaleX: 1, scaleY: 1, + x: position.x + dragTransform.x, + y: position.y + dragTransform.y + }), + }; + + return ( + + {title && ( + + {title} + + )} + {children} + + + ); +}; \ No newline at end of file diff --git a/client/src/features/NetworkGraph/components/NetworkGraph.tsx b/client/src/features/NetworkGraph/components/NetworkGraph.tsx index abcff66..1eb7c4c 100644 --- a/client/src/features/NetworkGraph/components/NetworkGraph.tsx +++ b/client/src/features/NetworkGraph/components/NetworkGraph.tsx @@ -1,8 +1,10 @@ import { useRef } from 'react'; import './NetworkGraph.css'; +import { DndContext } from '@dnd-kit/core'; import { useNetworkGraph } from "../NetworkGraphContext.tsx"; import { useNetworkGraphSimulation } from "@/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts"; import { useNetworkGraphInteractions } from "@/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts"; +import {FloatingPanel} from "@/components/ui/FloatingOverlay/FloatingOverlay.tsx"; export const NetworkGraph = () => { const { data } = useNetworkGraph(); @@ -15,13 +17,19 @@ export const NetworkGraph = () => { useNetworkGraphInteractions({ nodes, links, canvasRef }); return ( -
+
- + -
+ {} + + +
+ ) } From 8687bc69ea89f8d78d78d4fb13adcd94e098eac8 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Tue, 2 Dec 2025 20:05:24 -1000 Subject: [PATCH 25/29] Updated readme and made some structure changes Readme now includes instructions for installing node.js Changed several files to kebab case for consistency Re-organized file structure for better adherence to Bulletproof react. Simplified app router. Removed unused query client --- README.md | 12 +-- client/index.html | 2 +- client/public/vite.svg | 1 - client/src/{ => app}/index.css | 2 +- client/src/app/index.tsx | 1 + client/src/app/router.tsx | 44 ++++------ client/src/app/routes/landing.tsx | 4 +- client/src/assets/react.svg | 1 - .../layouts/{AppLayout.css => app-layout.css} | 0 .../layouts/{AppLayout.tsx => app-layout.tsx} | 4 +- client/src/components/ui/Header/index.ts | 1 - .../floating-overlay.tsx} | 0 .../{Header/Header.css => header/header.css} | 0 .../{Header/Header.tsx => header/header.tsx} | 2 +- client/src/components/ui/header/index.ts | 1 + client/src/data/dummy-data.ts | 83 ------------------- client/src/data/file-dummy-data.json | 46 ---------- client/src/data/file-dummy-data.ts | 49 ----------- client/src/data/issue-dummy-data.json | 11 --- client/src/data/issue-dummy-data.ts | 13 --- client/src/data/mail-dummy-data.json | 23 ----- client/src/data/mail-dummy-data.ts | 25 ------ client/src/data/person-dummy-data.json | 18 ---- client/src/data/person-dummy-data.ts | 20 ----- ...hToolbar.css => network-graph-toolbar.css} | 0 ...hToolbar.tsx => network-graph-toolbar.tsx} | 2 +- .../{NetworkGraph.css => network-graph.css} | 0 .../{NetworkGraph.tsx => network-graph.tsx} | 26 ++++-- .../hooks/useNetworkGraphInteractions.ts | 12 +-- .../hooks/useNetworkGraphJsonData.ts | 2 +- .../hooks/useNetworkGraphSimulation.ts | 2 +- client/src/features/NetworkGraph/index.ts | 2 +- ...rkGraphUtils.ts => network-graph-utils.ts} | 67 +++++++++++++-- .../network-graph-context.tsx} | 4 +- client/src/main.tsx | 2 - client/src/types/network-graph.types.ts | 1 + 36 files changed, 122 insertions(+), 361 deletions(-) delete mode 100644 client/public/vite.svg rename client/src/{ => app}/index.css (55%) delete mode 100644 client/src/assets/react.svg rename client/src/components/layouts/{AppLayout.css => app-layout.css} (100%) rename client/src/components/layouts/{AppLayout.tsx => app-layout.tsx} (76%) delete mode 100644 client/src/components/ui/Header/index.ts rename client/src/components/ui/{FloatingOverlay/FloatingOverlay.tsx => floating-overlay/floating-overlay.tsx} (100%) rename client/src/components/ui/{Header/Header.css => header/header.css} (100%) rename client/src/components/ui/{Header/Header.tsx => header/header.tsx} (81%) create mode 100644 client/src/components/ui/header/index.ts delete mode 100644 client/src/data/dummy-data.ts delete mode 100644 client/src/data/file-dummy-data.json delete mode 100644 client/src/data/file-dummy-data.ts delete mode 100644 client/src/data/issue-dummy-data.json delete mode 100644 client/src/data/issue-dummy-data.ts delete mode 100644 client/src/data/mail-dummy-data.json delete mode 100644 client/src/data/mail-dummy-data.ts delete mode 100644 client/src/data/person-dummy-data.json delete mode 100644 client/src/data/person-dummy-data.ts rename client/src/features/NetworkGraph/components/{NetworkGraphToolbar.css => network-graph-toolbar.css} (100%) rename client/src/features/NetworkGraph/components/{NetworkGraphToolbar.tsx => network-graph-toolbar.tsx} (76%) rename client/src/features/NetworkGraph/components/{NetworkGraph.css => network-graph.css} (100%) rename client/src/features/NetworkGraph/components/{NetworkGraph.tsx => network-graph.tsx} (53%) rename client/src/features/NetworkGraph/lib/{networkGraphUtils.ts => network-graph-utils.ts} (79%) rename client/src/features/NetworkGraph/{NetworkGraphContext.tsx => stores/network-graph-context.tsx} (85%) diff --git a/README.md b/README.md index 95a1fe7..2924ea7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ +## Requires Node.js and NPM +https://docs.npmjs.com/downloading-and-installing-node-js-and-npm + ## Requires PNPM https://pnpm.io/installation + ## Starting the Application: - 1. Navigate into 'client' directory via terminal. +1. Navigate into 'client' directory via terminal. - 2. Once inside, run the following command: - pnpm install +2. Once inside, run the following command: pnpm install - 3. After installation has completed run the following command: - pnpm run dev +3. After installation has completed run the following command: pnpm run dev diff --git a/client/index.html b/client/index.html index e4b78ea..36fc265 100644 --- a/client/index.html +++ b/client/index.html @@ -2,12 +2,12 @@ - Vite + React + TS
+ diff --git a/client/public/vite.svg b/client/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/client/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/index.css b/client/src/app/index.css similarity index 55% rename from client/src/index.css rename to client/src/app/index.css index 00bb2b2..f321f8e 100644 --- a/client/src/index.css +++ b/client/src/app/index.css @@ -3,6 +3,6 @@ html, body, #root { } html, body { - margin: 0; /* kills the 8px white border */ + margin: 0; padding: 0; } \ No newline at end of file diff --git a/client/src/app/index.tsx b/client/src/app/index.tsx index 8f834b3..50199c6 100644 --- a/client/src/app/index.tsx +++ b/client/src/app/index.tsx @@ -1,5 +1,6 @@ import { AppProvider} from "./provider.tsx"; import { AppRouter } from './router.tsx'; +import './index.css'; export const App = ()=> { return ( diff --git a/client/src/app/router.tsx b/client/src/app/router.tsx index 4c9513e..74e090c 100644 --- a/client/src/app/router.tsx +++ b/client/src/app/router.tsx @@ -1,38 +1,24 @@ -import { QueryClient, useQueryClient } from '@tanstack/react-query'; -import {createBrowserRouter, RouterProvider } from "react-router-dom"; +import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { paths } from '../config/paths.ts'; -import { useMemo } from "react"; -import AppLayout from "../components/layouts/AppLayout.tsx"; +import AppLayout from "../components/layouts/app-layout.tsx"; -const convert = (queryClient: QueryClient) => (m: any) => { - const { clientLoader, clientAction, default: Component, ...rest } = m; - return { - ...rest, - loader: clientLoader?.(queryClient), - action: clientAction?.(queryClient), - Component, - }; -}; -export const createAppRouter = (queryClient: QueryClient) => - createBrowserRouter([ - { - // Layout route - element: , - children: [ - { - path: paths.home.path, - lazy: () => import('./routes/landing').then(convert(queryClient)), +const router = createBrowserRouter([ + { + element: , + children: [ + { + path: paths.home.path, + lazy: async () => { + const { default: Component } = await import("./routes/landing"); + return { Component }; }, - // add more child routes here later - ], - }, - ]); + }, + ], + }, +]); export const AppRouter = () => { - const queryClient = useQueryClient(); - - const router = useMemo(() => createAppRouter(queryClient), [queryClient]); return ; }; \ No newline at end of file diff --git a/client/src/app/routes/landing.tsx b/client/src/app/routes/landing.tsx index a1e8126..af27afb 100644 --- a/client/src/app/routes/landing.tsx +++ b/client/src/app/routes/landing.tsx @@ -1,6 +1,6 @@ -import { NetworkGraphProvider } from "@/features/NetworkGraph/NetworkGraphContext.tsx"; +import { NetworkGraphProvider } from "@/features/NetworkGraph/stores/network-graph-context.tsx"; import { NetworkGraph } from "@/features/NetworkGraph"; -import { NetworkGraphToolbar } from "@/features/NetworkGraph/components/NetworkGraphToolbar.tsx"; +import { NetworkGraphToolbar } from "@/features/NetworkGraph/components/network-graph-toolbar.tsx"; const Landing = () => { return ( diff --git a/client/src/assets/react.svg b/client/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/client/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/components/layouts/AppLayout.css b/client/src/components/layouts/app-layout.css similarity index 100% rename from client/src/components/layouts/AppLayout.css rename to client/src/components/layouts/app-layout.css diff --git a/client/src/components/layouts/AppLayout.tsx b/client/src/components/layouts/app-layout.tsx similarity index 76% rename from client/src/components/layouts/AppLayout.tsx rename to client/src/components/layouts/app-layout.tsx index ad1cc2c..3c3559d 100644 --- a/client/src/components/layouts/AppLayout.tsx +++ b/client/src/components/layouts/app-layout.tsx @@ -1,6 +1,6 @@ import { Outlet } from "react-router-dom"; -import { Header } from "../ui/Header"; -import "./AppLayout.css"; +import { Header } from "../ui/header"; +import "./app-layout.css"; const AppLayout = () => { return ( diff --git a/client/src/components/ui/Header/index.ts b/client/src/components/ui/Header/index.ts deleted file mode 100644 index 07df1bf..0000000 --- a/client/src/components/ui/Header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Header.tsx'; \ No newline at end of file diff --git a/client/src/components/ui/FloatingOverlay/FloatingOverlay.tsx b/client/src/components/ui/floating-overlay/floating-overlay.tsx similarity index 100% rename from client/src/components/ui/FloatingOverlay/FloatingOverlay.tsx rename to client/src/components/ui/floating-overlay/floating-overlay.tsx diff --git a/client/src/components/ui/Header/Header.css b/client/src/components/ui/header/header.css similarity index 100% rename from client/src/components/ui/Header/Header.css rename to client/src/components/ui/header/header.css diff --git a/client/src/components/ui/Header/Header.tsx b/client/src/components/ui/header/header.tsx similarity index 81% rename from client/src/components/ui/Header/Header.tsx rename to client/src/components/ui/header/header.tsx index c6a346f..7d62921 100644 --- a/client/src/components/ui/Header/Header.tsx +++ b/client/src/components/ui/header/header.tsx @@ -1,4 +1,4 @@ -import "./Header.css"; +import "./header.css"; export const Header = () => { return ( diff --git a/client/src/components/ui/header/index.ts b/client/src/components/ui/header/index.ts new file mode 100644 index 0000000..d83c5da --- /dev/null +++ b/client/src/components/ui/header/index.ts @@ -0,0 +1 @@ +export * from './header.tsx'; \ No newline at end of file diff --git a/client/src/data/dummy-data.ts b/client/src/data/dummy-data.ts deleted file mode 100644 index ceeceff..0000000 --- a/client/src/data/dummy-data.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { NetworkGraphData } from "../types/network-graph.types.ts"; - -export const data = { - nodes: [ - { id: 'person_1', group: 'people', value: 2}, - { id: 'person_2', group: 'people', value: 2}, - { id: 'person_3', group: 'people', value: 2}, - { id: 'person_4', group: 'people', value: 2}, - { id: 'mail_1', group: 'mail', value: 2}, - { id: 'mail_2', group: 'mail', value: 2}, - { id: 'mail_3', group: 'mail', value: 2}, - { id: 'mail_4', group: 'mail', value: 2}, - { id: 'mail_5', group: 'mail', value: 2}, - { id: 'mail_6', group: 'mail', value: 2}, - { id: 'mail_7', group: 'mail', value: 2}, - { id: 'mail_8', group: 'mail', value: 2}, - { id: 'issue_1', group: 'issue', value: 4}, - { id: 'issue_2', group: 'issue', value: 2}, - { id: 'issue_3', group: 'issue', value: 2}, - { id: 'file_1', group: 'file', value: 2}, - { id: 'file_2', group: 'file', value: 2}, - { id: 'file_3', group: 'file', value: 2}, - { id: 'file_4', group: 'file', value: 2}, - { id: 'file_5', group: 'file', value: 2}, - { id: 'file_6', group: 'file', value: 2}, - { id: 'file_7', group: 'file', value: 2}, - { id: 'file_8', group: 'file', value: 2}, - { id: 'file_9', group: 'file', value: 2}, - { id: 'file_10', group: 'file', value: 3}, - { id: 'file_11', group: 'file', value: 4}, - { id: 'file_12', group: 'file', value: 4}, - { id: 'file_13', group: 'file', value: 2}, - { id: 'file_14', group: 'file', value: 2}, - { id: 'file_15', group: 'file', value: 2}, - { id: 'file_16', group: 'file', value: 2}, - { id: 'file_17', group: 'file', value: 2}, - { id: 'file_18', group: 'file', value: 2}, - { id: 'file_19', group: 'file', value: 2}, - { id: 'file_20', group: 'file', value: 2}, - { id: 'file_21', group: 'file', value: 2}, - { id: 'file_22', group: 'file', value: 3}, - { id: 'file_23', group: 'file', value: 4}, - { id: 'file_24', group: 'file', value: 8}, - - ], - links: [ - { source: 'person_1', target: 'mail_4', value: 1 }, - { source: 'person_1', target: 'file_2', value: 1 }, - { source: 'person_2', target: 'file_5', value: 1 }, - { source: 'person_2', target: 'mail_2', value: 1 }, - { source: 'person_3', target: 'mail_1', value: 1 }, - { source: 'person_3', target: 'issue_1', value: 1 }, - { source: 'person_4', target: 'file_7', value: 1 }, - { source: 'person_4', target: 'file_23', value: 1 }, - { source: 'mail_2', target: 'mail_1', value: 1 }, - { source: 'mail_3', target: 'mail_1', value: 1 }, - { source: 'mail_4', target: 'mail_1', value: 1 }, - { source: 'mail_4', target: 'mail_1', value: 1 }, - { source: 'mail_5', target: 'mail_1', value: 1 }, - { source: 'mail_5', target: 'mail_4', value: 1 }, - { source: 'mail_5', target: 'mail_1', value: 1 }, - { source: 'mail_6', target: 'mail_7', value: 1 }, - { source: 'mail_7', target: 'mail_8', value: 1 }, - { source: 'issue_1', target: 'issue_2', value: 1 }, - { source: 'issue_2', target: 'issue_3', value: 1 }, - { source: 'file_4', target: 'file_2', value: 1 }, - { source: 'file_4', target: 'file_3', value: 1 }, - { source: 'file_4', target: 'file_1', value: 1 }, - { source: 'file_4', target: 'file_6', value: 1 }, - { source: 'file_4', target: 'issue_2', value: 1 }, - { source: 'file_6', target: 'file_1', value: 1 }, - { source: 'file_6', target: 'file_3', value: 1 }, - { source: 'file_6', target: 'file_5', value: 1 }, - { source: 'file_7', target: 'file_8', value: 2}, - { source: 'file_8', target: 'file_9', value: 2}, - { source: 'file_9', target: 'file_10', value: 2}, - { source: 'file_9', target: 'file_22', value: 2}, - { source: 'file_10', target: 'file_11', value: 3}, - { source: 'file_11', target: 'file_12', value: 4}, - { source: 'file_23', target: 'issue_1', value: 4}, - { source: 'file_23', target: 'file_24', value: 4}, - ], -} as NetworkGraphData; \ No newline at end of file diff --git a/client/src/data/file-dummy-data.json b/client/src/data/file-dummy-data.json deleted file mode 100644 index fb3e743..0000000 --- a/client/src/data/file-dummy-data.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "nodes": [ - { "id": "file_1", "group": "file", "value": 2 }, - { "id": "file_2", "group": "file", "value": 2 }, - { "id": "file_3", "group": "file", "value": 2 }, - { "id": "file_4", "group": "file", "value": 2 }, - { "id": "file_5", "group": "file", "value": 2 }, - { "id": "file_6", "group": "file", "value": 2 }, - { "id": "file_7", "group": "file", "value": 2 }, - { "id": "file_8", "group": "file", "value": 2 }, - { "id": "file_9", "group": "file", "value": 2 }, - { "id": "file_10", "group": "file", "value": 3 }, - { "id": "file_11", "group": "file", "value": 4 }, - { "id": "file_12", "group": "file", "value": 4 }, - { "id": "file_13", "group": "file", "value": 2 }, - { "id": "file_14", "group": "file", "value": 2 }, - { "id": "file_15", "group": "file", "value": 2 }, - { "id": "file_16", "group": "file", "value": 2 }, - { "id": "file_17", "group": "file", "value": 2 }, - { "id": "file_18", "group": "file", "value": 2 }, - { "id": "file_19", "group": "file", "value": 2 }, - { "id": "file_20", "group": "file", "value": 2 }, - { "id": "file_21", "group": "file", "value": 2 }, - { "id": "file_22", "group": "file", "value": 3 }, - { "id": "file_23", "group": "file", "value": 4 }, - { "id": "file_24", "group": "file", "value": 8 } - ], - "links": [ - { "source": "file_4", "target": "file_2", "value": 1 }, - { "source": "file_4", "target": "file_3", "value": 1 }, - { "source": "file_4", "target": "file_1", "value": 1 }, - { "source": "file_4", "target": "file_6", "value": 1 }, - { "source": "file_4", "target": "issue_2", "value": 1 }, - { "source": "file_6", "target": "file_1", "value": 1 }, - { "source": "file_6", "target": "file_3", "value": 1 }, - { "source": "file_6", "target": "file_5", "value": 1 }, - { "source": "file_7", "target": "file_8", "value": 2 }, - { "source": "file_8", "target": "file_9", "value": 2 }, - { "source": "file_9", "target": "file_10", "value": 2 }, - { "source": "file_9", "target": "file_22", "value": 2 }, - { "source": "file_10", "target": "file_11", "value": 3 }, - { "source": "file_11", "target": "file_12", "value": 4 }, - { "source": "file_23", "target": "issue_1", "value": 4 }, - { "source": "file_23", "target": "file_24", "value": 4 } - ] -} diff --git a/client/src/data/file-dummy-data.ts b/client/src/data/file-dummy-data.ts deleted file mode 100644 index f04916c..0000000 --- a/client/src/data/file-dummy-data.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { NetworkGraphData } from "../types/network-graph.types.ts"; - -export const fileDummyData = { - nodes: [ - { id: 'file_1', group: 'file', value: 2}, - { id: 'file_2', group: 'file', value: 2}, - { id: 'file_3', group: 'file', value: 2}, - { id: 'file_4', group: 'file', value: 2}, - { id: 'file_5', group: 'file', value: 2}, - { id: 'file_6', group: 'file', value: 2}, - { id: 'file_7', group: 'file', value: 2}, - { id: 'file_8', group: 'file', value: 2}, - { id: 'file_9', group: 'file', value: 2}, - { id: 'file_10', group: 'file', value: 3}, - { id: 'file_11', group: 'file', value: 4}, - { id: 'file_12', group: 'file', value: 4}, - { id: 'file_13', group: 'file', value: 2}, - { id: 'file_14', group: 'file', value: 2}, - { id: 'file_15', group: 'file', value: 2}, - { id: 'file_16', group: 'file', value: 2}, - { id: 'file_17', group: 'file', value: 2}, - { id: 'file_18', group: 'file', value: 2}, - { id: 'file_19', group: 'file', value: 2}, - { id: 'file_20', group: 'file', value: 2}, - { id: 'file_21', group: 'file', value: 2}, - { id: 'file_22', group: 'file', value: 3}, - { id: 'file_23', group: 'file', value: 4}, - { id: 'file_24', group: 'file', value: 8}, - - ], - links: [ - { source: 'file_4', target: 'file_2', value: 1 }, - { source: 'file_4', target: 'file_3', value: 1 }, - { source: 'file_4', target: 'file_1', value: 1 }, - { source: 'file_4', target: 'file_6', value: 1 }, - { source: 'file_4', target: 'issue_2', value: 1 }, - { source: 'file_6', target: 'file_1', value: 1 }, - { source: 'file_6', target: 'file_3', value: 1 }, - { source: 'file_6', target: 'file_5', value: 1 }, - { source: 'file_7', target: 'file_8', value: 2}, - { source: 'file_8', target: 'file_9', value: 2}, - { source: 'file_9', target: 'file_10', value: 2}, - { source: 'file_9', target: 'file_22', value: 2}, - { source: 'file_10', target: 'file_11', value: 3}, - { source: 'file_11', target: 'file_12', value: 4}, - { source: 'file_23', target: 'issue_1', value: 4}, - { source: 'file_23', target: 'file_24', value: 4}, - ], -} as NetworkGraphData; \ No newline at end of file diff --git a/client/src/data/issue-dummy-data.json b/client/src/data/issue-dummy-data.json deleted file mode 100644 index 9ca6cbe..0000000 --- a/client/src/data/issue-dummy-data.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "nodes": [ - { "id": "issue_1", "group": "issue", "value": 4 }, - { "id": "issue_2", "group": "issue", "value": 2 }, - { "id": "issue_3", "group": "issue", "value": 2 } - ], - "links": [ - { "source": "issue_1", "target": "issue_2", "value": 1 }, - { "source": "issue_2", "target": "issue_3", "value": 1 } - ] -} diff --git a/client/src/data/issue-dummy-data.ts b/client/src/data/issue-dummy-data.ts deleted file mode 100644 index 04dcb47..0000000 --- a/client/src/data/issue-dummy-data.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { NetworkGraphData } from "../types/network-graph.types.ts"; - -export const issueDummyData = { - nodes: [ - { id: 'issue_1', group: 'issue', value: 4}, - { id: 'issue_2', group: 'issue', value: 2}, - { id: 'issue_3', group: 'issue', value: 2}, - ], - links: [ - { source: 'issue_1', target: 'issue_2', value: 1 }, - { source: 'issue_2', target: 'issue_3', value: 1 }, - ], -} as NetworkGraphData; \ No newline at end of file diff --git a/client/src/data/mail-dummy-data.json b/client/src/data/mail-dummy-data.json deleted file mode 100644 index 00f716e..0000000 --- a/client/src/data/mail-dummy-data.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "nodes": [ - { "id": "mail_1", "group": "mail", "value": 2 }, - { "id": "mail_2", "group": "mail", "value": 2 }, - { "id": "mail_3", "group": "mail", "value": 2 }, - { "id": "mail_4", "group": "mail", "value": 2 }, - { "id": "mail_5", "group": "mail", "value": 2 }, - { "id": "mail_6", "group": "mail", "value": 2 }, - { "id": "mail_7", "group": "mail", "value": 2 }, - { "id": "mail_8", "group": "mail", "value": 2 } - ], - "links": [ - { "source": "mail_2", "target": "mail_1", "value": 1 }, - { "source": "mail_3", "target": "mail_1", "value": 1 }, - { "source": "mail_4", "target": "mail_1", "value": 1 }, - { "source": "mail_4", "target": "mail_1", "value": 1 }, - { "source": "mail_5", "target": "mail_1", "value": 1 }, - { "source": "mail_5", "target": "mail_4", "value": 1 }, - { "source": "mail_5", "target": "mail_1", "value": 1 }, - { "source": "mail_6", "target": "mail_7", "value": 1 }, - { "source": "mail_7", "target": "mail_8", "value": 1 } - ] -} diff --git a/client/src/data/mail-dummy-data.ts b/client/src/data/mail-dummy-data.ts deleted file mode 100644 index b95e91a..0000000 --- a/client/src/data/mail-dummy-data.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { NetworkGraphData } from "../types/network-graph.types.ts"; - -export const mailDummyData = { - nodes: [ - { id: 'mail_1', group: 'mail', value: 2}, - { id: 'mail_2', group: 'mail', value: 2}, - { id: 'mail_3', group: 'mail', value: 2}, - { id: 'mail_4', group: 'mail', value: 2}, - { id: 'mail_5', group: 'mail', value: 2}, - { id: 'mail_6', group: 'mail', value: 2}, - { id: 'mail_7', group: 'mail', value: 2}, - { id: 'mail_8', group: 'mail', value: 2}, - ], - links: [ - { source: 'mail_2', target: 'mail_1', value: 1 }, - { source: 'mail_3', target: 'mail_1', value: 1 }, - { source: 'mail_4', target: 'mail_1', value: 1 }, - { source: 'mail_4', target: 'mail_1', value: 1 }, - { source: 'mail_5', target: 'mail_1', value: 1 }, - { source: 'mail_5', target: 'mail_4', value: 1 }, - { source: 'mail_5', target: 'mail_1', value: 1 }, - { source: 'mail_6', target: 'mail_7', value: 1 }, - { source: 'mail_7', target: 'mail_8', value: 1 }, - ], -} as NetworkGraphData; \ No newline at end of file diff --git a/client/src/data/person-dummy-data.json b/client/src/data/person-dummy-data.json deleted file mode 100644 index 50a24a3..0000000 --- a/client/src/data/person-dummy-data.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "nodes": [ - { "id": "person_1", "group": "people", "value": 2 }, - { "id": "person_2", "group": "people", "value": 2 }, - { "id": "person_3", "group": "people", "value": 2 }, - { "id": "person_4", "group": "people", "value": 2 } - ], - "links": [ - { "source": "person_1", "target": "mail_4", "value": 1 }, - { "source": "person_1", "target": "file_2", "value": 1 }, - { "source": "person_2", "target": "file_5", "value": 1 }, - { "source": "person_2", "target": "mail_2", "value": 1 }, - { "source": "person_3", "target": "mail_1", "value": 1 }, - { "source": "person_3", "target": "issue_1", "value": 1 }, - { "source": "person_4", "target": "file_7", "value": 1 }, - { "source": "person_4", "target": "file_23", "value": 1 } - ] -} diff --git a/client/src/data/person-dummy-data.ts b/client/src/data/person-dummy-data.ts deleted file mode 100644 index 21e7e56..0000000 --- a/client/src/data/person-dummy-data.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { NetworkGraphData } from "../types/network-graph.types.ts"; - -export const personDummyData = { - nodes: [ - {id: 'person_1', group: 'people', value: 2}, - {id: 'person_2', group: 'people', value: 2}, - {id: 'person_3', group: 'people', value: 2}, - {id: 'person_4', group: 'people', value: 2}, - ], - links: [ - {source: 'person_1', target: 'mail_4', value: 1}, - {source: 'person_1', target: 'file_2', value: 1}, - {source: 'person_2', target: 'file_5', value: 1}, - {source: 'person_2', target: 'mail_2', value: 1}, - {source: 'person_3', target: 'mail_1', value: 1}, - {source: 'person_3', target: 'issue_1', value: 1}, - {source: 'person_4', target: 'file_7', value: 1}, - {source: 'person_4', target: 'file_23', value: 1} - ], -} as NetworkGraphData; \ No newline at end of file diff --git a/client/src/features/NetworkGraph/components/NetworkGraphToolbar.css b/client/src/features/NetworkGraph/components/network-graph-toolbar.css similarity index 100% rename from client/src/features/NetworkGraph/components/NetworkGraphToolbar.css rename to client/src/features/NetworkGraph/components/network-graph-toolbar.css diff --git a/client/src/features/NetworkGraph/components/NetworkGraphToolbar.tsx b/client/src/features/NetworkGraph/components/network-graph-toolbar.tsx similarity index 76% rename from client/src/features/NetworkGraph/components/NetworkGraphToolbar.tsx rename to client/src/features/NetworkGraph/components/network-graph-toolbar.tsx index 93418f9..001fd31 100644 --- a/client/src/features/NetworkGraph/components/NetworkGraphToolbar.tsx +++ b/client/src/features/NetworkGraph/components/network-graph-toolbar.tsx @@ -1,4 +1,4 @@ -import "./NetworkGraphToolbar.css"; +import "./network-graph-toolbar.css"; export const NetworkGraphToolbar = () => { diff --git a/client/src/features/NetworkGraph/components/NetworkGraph.css b/client/src/features/NetworkGraph/components/network-graph.css similarity index 100% rename from client/src/features/NetworkGraph/components/NetworkGraph.css rename to client/src/features/NetworkGraph/components/network-graph.css diff --git a/client/src/features/NetworkGraph/components/NetworkGraph.tsx b/client/src/features/NetworkGraph/components/network-graph.tsx similarity index 53% rename from client/src/features/NetworkGraph/components/NetworkGraph.tsx rename to client/src/features/NetworkGraph/components/network-graph.tsx index 1eb7c4c..4e93a4c 100644 --- a/client/src/features/NetworkGraph/components/NetworkGraph.tsx +++ b/client/src/features/NetworkGraph/components/network-graph.tsx @@ -1,12 +1,14 @@ -import { useRef } from 'react'; -import './NetworkGraph.css'; +import { useRef, useState } from 'react'; +import './network-graph.css'; import { DndContext } from '@dnd-kit/core'; -import { useNetworkGraph } from "../NetworkGraphContext.tsx"; +import { useNetworkGraph } from "../stores/network-graph-context.tsx"; import { useNetworkGraphSimulation } from "@/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts"; import { useNetworkGraphInteractions } from "@/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts"; -import {FloatingPanel} from "@/components/ui/FloatingOverlay/FloatingOverlay.tsx"; +import {FloatingPanel} from "@/components/ui/floating-overlay/floating-overlay.tsx"; export const NetworkGraph = () => { + const [ overlayOn, setOverlayOn ] = useState(false); + const { data } = useNetworkGraph(); const { nodes, links } = data ?? { nodes: [], links: [] }; @@ -14,7 +16,7 @@ export const NetworkGraph = () => { const canvasRef = useRef(null); useNetworkGraphSimulation({ nodes, links, canvasRef }); - useNetworkGraphInteractions({ nodes, links, canvasRef }); + useNetworkGraphInteractions({ nodes, links, canvasRef, setOverlayOn }); return ( @@ -26,9 +28,17 @@ export const NetworkGraph = () => { }}> - {} - - + {overlayOn ? ( + +
+ Total nodes: +
+
+ Show More +
+
+ ) : null} +
) diff --git a/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts b/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts index 363b826..bc1a3a1 100644 --- a/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts +++ b/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts @@ -1,15 +1,15 @@ -import { useEffect, type RefObject } from "react"; +import {useEffect, type RefObject, type SetStateAction, type Dispatch } from "react"; import { type Link, type Node } from "@/types/network-graph.types.ts" import { buildInitialTransparencyMap, buildRelationshipMap, createDragBehavior, - drawGraph, findNodeAt, updateTransparency -} from "@/features/NetworkGraph/lib/networkGraphUtils.ts"; + drawGraph, findNodeAt, highlightSubgraph +} from "@/features/NetworkGraph/lib/network-graph-utils.ts"; import {type DragBehavior, pointer, select} from "d3"; export function useNetworkGraphInteractions( - { nodes, links, canvasRef }: { nodes: Node[]; links: Link[]; canvasRef: RefObject} + { nodes, links, canvasRef, setOverlayOn }: { nodes: Node[]; links: Link[]; canvasRef: RefObject; setOverlayOn: Dispatch> } ) { useEffect(() => { @@ -44,7 +44,7 @@ export function useNetworkGraphInteractions( const hit = findNodeAt(nodes, nodeRadiusMultiplier, x, y,); if (hit) { - updateTransparency(hit, transparentNodeMap, nodeRelationshipMap); + highlightSubgraph(hit, transparentNodeMap, nodeRelationshipMap, setOverlayOn); drawGraph(context, canvas, nodes, links, transparentNodeMap, nodeRadiusMultiplier); } }; @@ -59,5 +59,5 @@ export function useNetworkGraphInteractions( ro.disconnect(); select(canvas).on(".drag", null).on("dblclick", null); }; - }, [nodes, links, canvasRef]); + }, [nodes, links, canvasRef, setOverlayOn]); } \ No newline at end of file diff --git a/client/src/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts b/client/src/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts index a864d50..5dff811 100644 --- a/client/src/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts +++ b/client/src/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts @@ -4,7 +4,7 @@ import type {NetworkGraphData} from "@/types/network-graph.types.ts"; const GRAPH_FILES = ['file-graph-data.json', 'issue-graph-data.json', 'mail-graph-data.json', 'person-graph-data.json']; -export function useJsonFiles(){ +export function useGraphJsonFiles(){ const [graphData, setGraphData] = useState({ nodes: [], links: [] }); useEffect(() => { diff --git a/client/src/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts b/client/src/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts index 1e97361..4f11ab1 100644 --- a/client/src/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts +++ b/client/src/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts @@ -2,7 +2,7 @@ import { useEffect, type RefObject } from "react"; import { forceSimulation, forceLink, forceX, forceY, forceCollide, forceManyBody } from "d3"; import type { Simulation } from "d3"; import type { Node, Link, HubLink } from "@/types/network-graph.types.ts" -import { nodeGroupCenters, buildHubs, buildHubLinks, clearForces } from "@/features/NetworkGraph/lib/networkGraphUtils.ts"; +import { nodeGroupCenters, buildHubs, buildHubLinks, clearForces } from "@/features/NetworkGraph/lib/network-graph-utils.ts"; interface UseNetworkGraphSimulationArgs { nodes: Node[]; diff --git a/client/src/features/NetworkGraph/index.ts b/client/src/features/NetworkGraph/index.ts index 9f18d84..1daae81 100644 --- a/client/src/features/NetworkGraph/index.ts +++ b/client/src/features/NetworkGraph/index.ts @@ -1 +1 @@ -export { NetworkGraph } from './components/NetworkGraph.tsx'; \ No newline at end of file +export { NetworkGraph } from './components/network-graph.tsx'; \ No newline at end of file diff --git a/client/src/features/NetworkGraph/lib/networkGraphUtils.ts b/client/src/features/NetworkGraph/lib/network-graph-utils.ts similarity index 79% rename from client/src/features/NetworkGraph/lib/networkGraphUtils.ts rename to client/src/features/NetworkGraph/lib/network-graph-utils.ts index a2237fe..f9ade62 100644 --- a/client/src/features/NetworkGraph/lib/networkGraphUtils.ts +++ b/client/src/features/NetworkGraph/lib/network-graph-utils.ts @@ -1,5 +1,6 @@ import type {Group, Link, Node} from "@/types/network-graph.types.ts"; import {drag, pointer, type Simulation} from "d3"; +import type {Dispatch, SetStateAction} from "react"; export const nodeGroupCenters = (w: number, h: number) => ({ people: [w * 0.5, h * 0.4], @@ -14,10 +15,11 @@ export function buildInitialTransparencyMap(nodes: Node[]) { return map; } -export function updateTransparency( +export function highlightSubgraph( hitNode: Node, transparentNodeMap: Map, - nodeRelationshipMap: Map> + nodeRelationshipMap: Map>, + setOverlayOn: Dispatch> ){ if (!hasPos(hitNode)) return; @@ -38,6 +40,8 @@ export function updateTransparency( relatedNodeSet?.forEach(nodeId => { transparentNodeMap.set(nodeId, 0); }); + + setOverlayOn(true); }; const clearAll = () => { @@ -57,6 +61,7 @@ export function updateTransparency( if (current === 0) { // Clicked *inside* the highlighted group → reset to fully opaque clearAll(); + setOverlayOn(false); } else { // Clicked on a faded node → switch highlight to this node's group instead highlightNeighborhood(); @@ -170,10 +175,13 @@ const drawNodeByGroup = ( } context.beginPath(); - context.moveTo(node.x, node.y); context.arc(node.x, node.y, node.value * nodeRadiusMultiplier, 0, 2 * Math.PI); context.fill(); + // Outline + context.lineWidth = 1; + context.strokeStyle = "#222"; // or per-group if you want + context.stroke(); // Set text attributes const px = Math.round(Math.max(10, Math.min(24, node.value * nodeRadiusMultiplier * 0.6))); @@ -196,6 +204,37 @@ function hasPos(n: Node): n is Node & { x: number; y: number } { return n.x != null && n.y != null; } +function drawArrow( + ctx: CanvasRenderingContext2D, + fromX: number, + fromY: number, + toX: number, + toY: number, + headLength = 10 +) { + const dx = toX - fromX; + const dy = toY - fromY; + const angle = Math.atan2(dy, dx); + + ctx.beginPath(); + ctx.moveTo(fromX, fromY); + ctx.lineTo(toX, toY); + ctx.stroke(); + + // Arrowhead + ctx.beginPath(); + ctx.moveTo(toX, toY); + ctx.lineTo( + toX - headLength * Math.cos(angle - Math.PI / 6), + toY - headLength * Math.sin(angle - Math.PI / 6) + ); + ctx.lineTo( + toX - headLength * Math.cos(angle + Math.PI / 6), + toY - headLength * Math.sin(angle + Math.PI / 6) + ); + ctx.closePath(); + ctx.fill(); +} export function drawGraph ( context: CanvasRenderingContext2D, @@ -213,12 +252,26 @@ export function drawGraph ( const s = link.source; const t = link.target; - context.beginPath(); if (isNullOrUndefined(s.x) && isNullOrUndefined(s.y)) return; - context.moveTo(s.x, s.y); - context.lineTo(t.x, t.y); - context.stroke(); + const dx = t.x - s.x; + const dy = t.y - s.y; + const len = Math.hypot(dx, dy); + if (len === 0) return; + + const ux = dx / len; + const uy = dy / len; + + const rSource = s.value * nodeRadiusMultiplier; + const rTarget = t.value * nodeRadiusMultiplier; + const headLength = 10; + + const fromX = s.x + ux * rSource; + const fromY = s.y + uy * rSource; + const toX = t.x - ux * (rTarget + headLength * 0.5); + const toY = t.y - uy * (rTarget + headLength * 0.5); + + drawArrow(context, fromX, fromY, toX, toY, headLength); }); nodes.forEach((node) => { diff --git a/client/src/features/NetworkGraph/NetworkGraphContext.tsx b/client/src/features/NetworkGraph/stores/network-graph-context.tsx similarity index 85% rename from client/src/features/NetworkGraph/NetworkGraphContext.tsx rename to client/src/features/NetworkGraph/stores/network-graph-context.tsx index 840e72b..4a4ae67 100644 --- a/client/src/features/NetworkGraph/NetworkGraphContext.tsx +++ b/client/src/features/NetworkGraph/stores/network-graph-context.tsx @@ -1,6 +1,6 @@ import { type ReactNode, createContext, useContext } from 'react'; import { type NetworkGraphData } from '@/types/network-graph.types.ts'; -import {useJsonFiles} from "@/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts"; +import {useGraphJsonFiles} from "@/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts"; const NetworkGraphContext = createContext(null); @@ -19,7 +19,7 @@ export function useNetworkGraph() { } export function NetworkGraphProvider({ children }: { children: ReactNode }) { - const data = useJsonFiles(); + const data = useGraphJsonFiles(); diff --git a/client/src/main.tsx b/client/src/main.tsx index dad8c88..a4fc9c5 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,7 +1,5 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; - -import './index.css'; import { App } from './app'; createRoot(document.getElementById('root')!).render( diff --git a/client/src/types/network-graph.types.ts b/client/src/types/network-graph.types.ts index 45f118c..fa93afe 100644 --- a/client/src/types/network-graph.types.ts +++ b/client/src/types/network-graph.types.ts @@ -1,3 +1,4 @@ + import type { SimulationNodeDatum, SimulationLinkDatum } from "d3"; export interface Node extends SimulationNodeDatum { From 1955d6c5eba6cc0dd56f0819e0836e7aa7471e36 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Wed, 3 Dec 2025 00:47:46 -1000 Subject: [PATCH 26/29] Documentation, separation of concerns, and more Created documentation for app entry points (layout, provider, router) Created documentation for floating overlay. Created documentation for network-graph-context. Created documentation for all hooks in the network-graph feature. Deprecated 'network-graph-utils' and moved helper logic into hooks. --- client/src/app/index.tsx | 4 + client/src/app/provider.tsx | 23 +- client/src/app/router.tsx | 4 +- client/src/app/routes/landing.tsx | 5 + client/src/components/layouts/app-layout.tsx | 3 + .../ui/floating-overlay/floating-overlay.tsx | 75 +++-- .../src/features/NetworkGraph/config/paths.ts | 3 + .../NetworkGraph/hooks/useDummyData.ts | 13 - ...raphJsonData.ts => useNetworkGraphData.ts} | 31 +- .../hooks/useNetworkGraphInteractions.ts | 130 +++++++- .../hooks/useNetworkGraphSimulation.ts | 62 +++- ...k-graph-utils.ts => draw-network-graph.ts} | 278 +++++------------- .../stores/network-graph-context.tsx | 14 +- client/src/utils/type-guards.ts | 3 + 14 files changed, 368 insertions(+), 280 deletions(-) create mode 100644 client/src/features/NetworkGraph/config/paths.ts delete mode 100644 client/src/features/NetworkGraph/hooks/useDummyData.ts rename client/src/features/NetworkGraph/hooks/{useNetworkGraphJsonData.ts => useNetworkGraphData.ts} (56%) rename client/src/features/NetworkGraph/lib/{network-graph-utils.ts => draw-network-graph.ts} (57%) create mode 100644 client/src/utils/type-guards.ts diff --git a/client/src/app/index.tsx b/client/src/app/index.tsx index 50199c6..a02b70c 100644 --- a/client/src/app/index.tsx +++ b/client/src/app/index.tsx @@ -2,6 +2,10 @@ import { AppProvider} from "./provider.tsx"; import { AppRouter } from './router.tsx'; import './index.css'; +/** + * Root of the app + */ + export const App = ()=> { return ( <> diff --git a/client/src/app/provider.tsx b/client/src/app/provider.tsx index 7f6030f..f4c9d91 100644 --- a/client/src/app/provider.tsx +++ b/client/src/app/provider.tsx @@ -1,24 +1,17 @@ -import * as React from 'react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { queryConfig } from '../lib/react-query.ts'; +import { Suspense, type ReactNode } from 'react'; type AppProviderProps = { - children: React.ReactNode; + children: ReactNode; }; -export const AppProvider = ({ children }: AppProviderProps) => { - const [queryClient] = React.useState( - () => - new QueryClient({ - defaultOptions: queryConfig, - }), - ); +/** + * Provider for entire application. +*/ +export const AppProvider = ({ children }: AppProviderProps) => { return ( - - + {children} - - + ); } \ No newline at end of file diff --git a/client/src/app/router.tsx b/client/src/app/router.tsx index 74e090c..c724046 100644 --- a/client/src/app/router.tsx +++ b/client/src/app/router.tsx @@ -2,7 +2,9 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { paths } from '../config/paths.ts'; import AppLayout from "../components/layouts/app-layout.tsx"; - +/** + * Holds the routes (url + page) of the app + */ const router = createBrowserRouter([ { element: , diff --git a/client/src/app/routes/landing.tsx b/client/src/app/routes/landing.tsx index af27afb..872901e 100644 --- a/client/src/app/routes/landing.tsx +++ b/client/src/app/routes/landing.tsx @@ -2,6 +2,11 @@ import { NetworkGraphProvider } from "@/features/NetworkGraph/stores/network-gra import { NetworkGraph } from "@/features/NetworkGraph"; import { NetworkGraphToolbar } from "@/features/NetworkGraph/components/network-graph-toolbar.tsx"; +/** + * Landing page component. + * + * Holds the Network Graph feature currently. + */ const Landing = () => { return ( <> diff --git a/client/src/components/layouts/app-layout.tsx b/client/src/components/layouts/app-layout.tsx index 3c3559d..f2a3cad 100644 --- a/client/src/components/layouts/app-layout.tsx +++ b/client/src/components/layouts/app-layout.tsx @@ -2,6 +2,9 @@ import { Outlet } from "react-router-dom"; import { Header } from "../ui/header"; import "./app-layout.css"; +/** + * Provides current layout for the application. Renders the header above any child components passed to it. + */ const AppLayout = () => { return (
diff --git a/client/src/components/ui/floating-overlay/floating-overlay.tsx b/client/src/components/ui/floating-overlay/floating-overlay.tsx index 8ef0f25..ba75724 100644 --- a/client/src/components/ui/floating-overlay/floating-overlay.tsx +++ b/client/src/components/ui/floating-overlay/floating-overlay.tsx @@ -3,20 +3,36 @@ import { CSS } from "@dnd-kit/utilities"; import { useDraggable, useDndMonitor } from '@dnd-kit/core'; import DialogTitle from "@mui/material/DialogTitle"; import DialogContent from "@mui/material/DialogContent"; -import {useState} from "react"; +import { useState, type ReactNode, type FC, type CSSProperties } from "react"; type FloatingPanelProps = { + /** + * Passed to the useDraggable hook from dnd-kit to establish the component as 'draggable' and distinguish it + */ id: string; - title?: React.ReactNode; - children?: React.ReactNode; + + /** + * Optional title text displayed on the panel + */ + title?: ReactNode; + + /** + * Optional components to be rendered within the panel + */ + children?: ReactNode; }; -export const FloatingPanel: React.FC = ({ title, children, id }) => { +/** + * A draggable panel component. + */ +export const FloatingPanel: FC = ({ title, children, id }) => { + // Initial position of the panel const [position, setPosition] = useState({ x: 16, y: 16 }); - const {attributes, listeners, setNodeRef, transform } = useDraggable({ id }); - + // Hook that exposes constants necessary for dragging the component + const { attributes, listeners, setNodeRef, transform } = useDraggable({ id }); + // Listener for drag and drop events useDndMonitor({ onDragEnd(event) { if (event.active.id !== id) return; @@ -29,9 +45,11 @@ export const FloatingPanel: React.FC = ({ title, children, i }, }); + // Transform applied to the style of the component const dragTransform = transform ?? { x: 0, y: 0 }; - const style: React.CSSProperties = { + // Style of the draggable component + const style: CSSProperties = { transform: CSS.Translate.toString({ scaleX: 1, scaleY: 1, x: position.x + dragTransform.x, @@ -40,27 +58,28 @@ export const FloatingPanel: React.FC = ({ title, children, i }; return ( - - {title && ( - - {title} - - )} - {children} - + + {title && ( + + {title} + + )} + + {children} + ); }; \ No newline at end of file diff --git a/client/src/features/NetworkGraph/config/paths.ts b/client/src/features/NetworkGraph/config/paths.ts new file mode 100644 index 0000000..503a90c --- /dev/null +++ b/client/src/features/NetworkGraph/config/paths.ts @@ -0,0 +1,3 @@ +import { PROJECTS_DIRECTORY, TARGET_PROJECT } from "@/config/app.ts"; + +export const BASE_PATH = `/${PROJECTS_DIRECTORY}/${TARGET_PROJECT}`; \ No newline at end of file diff --git a/client/src/features/NetworkGraph/hooks/useDummyData.ts b/client/src/features/NetworkGraph/hooks/useDummyData.ts deleted file mode 100644 index 5c70e85..0000000 --- a/client/src/features/NetworkGraph/hooks/useDummyData.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { personDummyData } from "@/data/person-dummy-data.ts"; -import { mailDummyData } from "@/data/mail-dummy-data.ts"; -import { issueDummyData } from "@/data/issue-dummy-data.ts"; -import { fileDummyData } from "@/data/file-dummy-data.ts"; - -export function useDummyData() { - return { - personData: personDummyData, - mailData: mailDummyData, - issueData: issueDummyData, - fileData: fileDummyData - }; -} diff --git a/client/src/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts b/client/src/features/NetworkGraph/hooks/useNetworkGraphData.ts similarity index 56% rename from client/src/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts rename to client/src/features/NetworkGraph/hooks/useNetworkGraphData.ts index 5dff811..5518aca 100644 --- a/client/src/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts +++ b/client/src/features/NetworkGraph/hooks/useNetworkGraphData.ts @@ -1,37 +1,47 @@ import { useEffect, useState } from 'react'; -import { PROJECTS_DIRECTORY, TARGET_PROJECT } from "@/config/app.ts"; -import type {NetworkGraphData} from "@/types/network-graph.types.ts"; +import { BASE_PATH } from "@/features/NetworkGraph/config/paths.ts"; + +import type { NetworkGraphData } from "@/types/network-graph.types.ts"; const GRAPH_FILES = ['file-graph-data.json', 'issue-graph-data.json', 'mail-graph-data.json', 'person-graph-data.json']; -export function useGraphJsonFiles(){ +type NetworkGraphJsonData = {[filename: string]: NetworkGraphData}; + +/** + * Loads all network graph data for project specified in user.config.yaml + */ +export function useNetworkGraphData(){ const [graphData, setGraphData] = useState({ nodes: [], links: [] }); useEffect(() => { async function load() { - const result = await readAllJsonsFromDirectory(); + const jsonData: NetworkGraphJsonData = await readAllJsonsFromDirectory(BASE_PATH); + // Initialize an empty NetworkGraphData object const compiledGraphData : NetworkGraphData = { nodes: [], links: [] }; - for (const value of Object.values(result)) { + // For each file, extract the nodes + for (const value of Object.values(jsonData)) { compiledGraphData.nodes.push(... value.nodes); compiledGraphData.links.push(... value.links); } setGraphData(compiledGraphData); } + load(); }, []); return graphData; } -async function readAllJsonsFromDirectory() { - const BASE_PATH = `/${PROJECTS_DIRECTORY}/${TARGET_PROJECT}`; - +/** + * Loads all .json files from project specified in user.config.yaml + */ +async function readAllJsonsFromDirectory(filePath: string): Promise { const entries = await Promise.all( GRAPH_FILES.map(async (fileName) => { - const res = await fetch(`${BASE_PATH}/${fileName}`); + const res = await fetch(filePath + '/' + fileName); if (!res.ok) { throw new Error(`Failed to load ${fileName}: ${res.status} ${res.statusText}`); @@ -44,4 +54,5 @@ async function readAllJsonsFromDirectory() { // object keyed by file name: return Object.fromEntries(entries); -} \ No newline at end of file +} + diff --git a/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts b/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts index bc1a3a1..28eb099 100644 --- a/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts +++ b/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts @@ -1,13 +1,17 @@ -import {useEffect, type RefObject, type SetStateAction, type Dispatch } from "react"; +import { useEffect, type RefObject, type SetStateAction, type Dispatch } from "react"; +import { highlightSubgraph } from "../lib/draw-network-graph.ts"; import { type Link, type Node } from "@/types/network-graph.types.ts" -import { - buildInitialTransparencyMap, - buildRelationshipMap, - createDragBehavior, - drawGraph, findNodeAt, highlightSubgraph -} from "@/features/NetworkGraph/lib/network-graph-utils.ts"; -import {type DragBehavior, pointer, select} from "d3"; +import { drawGraph } from '../lib/draw-network-graph.ts' +import { drag, type DragBehavior, pointer, select } from "d3"; +/** + * Allows a network graph that is rendered to a canvas to become interactable to the user. + * + * Contains logic for the following user interactions: + * - Double-click detection + * - Drag detection + * - Redraws network graph whenever canvas size changes (user resizes window, user opens browser console, etc.) + */ export function useNetworkGraphInteractions( { nodes, links, canvasRef, setOverlayOn }: { nodes: Node[]; links: Link[]; canvasRef: RefObject; setOverlayOn: Dispatch> } ) { @@ -17,16 +21,31 @@ export function useNetworkGraphInteractions( const canvas = canvasRef.current; if (!canvas) return; + + // Canvas parent container is stored, necessary for resizing 'drawing space' later const container = canvas.parentElement; if (!container) return; const context = canvas.getContext("2d"); if (!context) return; + // Can be adjusted but is arbitrarily set to 11 const nodeRadiusMultiplier = 11; + + // Map that stores every node's related nodes const nodeRelationshipMap = buildRelationshipMap(nodes, links); + + // Map that stores transparency status of every node const transparentNodeMap = buildInitialTransparencyMap(nodes); + /** + * Helper function that resizes the drawing space for a canvas. + * + * Note: + * - A canvas has size as an HTML element, but also has virtual size for its drawing space. + * - The virtual size is being adjusted by this function, not the element size. + * - If the virtual size of a canvas does not match its element size then drawing on the canvas will look 'wrong' + */ const resize = () => { const rect = container.getBoundingClientRect(); canvas.width = rect.width; @@ -36,9 +55,20 @@ export function useNetworkGraphInteractions( resize(); + // Creates drag behavior const dragBehavior = createDragBehavior(context, canvas, nodes, nodeRadiusMultiplier, links, transparentNodeMap,); + + // Attaches drag behavior to the canvas select(canvas).call(dragBehavior as DragBehavior); + /** + * Double-click handler that captures the location of the mouse pointer and triggers the highlight interaction. + * + * Flow: + * 1. Whenever the canvas is double-clicked, check the position of the mouse pointer. + * 2. If the mouse pointer is over a node, then trigger the highlight logic. + * 3. After running the highlight logic, redraw the network graph. + */ const handleDblClick = (event: MouseEvent) => { const [x, y] = pointer(event, canvas); const hit = findNodeAt(nodes, nodeRadiusMultiplier, x, y,); @@ -48,16 +78,98 @@ export function useNetworkGraphInteractions( drawGraph(context, canvas, nodes, links, transparentNodeMap, nodeRadiusMultiplier); } }; + + // Creates double-click listener that triggers double-click handler and attaches it to the canvas select(canvas).on("dblclick", handleDblClick); + /** + * Observes for when an element is resized and triggers the resize helper function. + * + * Note: + * - By attaching this observer to the parent container of the canvas, we ensure that its drawing space is adjusted + * whenever its element size changes. + */ const ro = new ResizeObserver(() => { resize(); }); + + // Observes the parent container of the canvas ro.observe(container); + // Clean-up function that disconnects observer and listeners when the hook is no longer in use. return () => { ro.disconnect(); select(canvas).on(".drag", null).on("dblclick", null); }; }, [nodes, links, canvasRef, setOverlayOn]); -} \ No newline at end of file +} + +function createDragBehavior( + context: CanvasRenderingContext2D, + canvas: HTMLCanvasElement, + nodes: Node[], + nodeRadiusMultiplier: number, + links: Link[], + transparentNodeMap: Map) { + + return drag() + .subject((event) => { + const [x, y] = pointer(event, canvas); + + // find nearest node within radius + const n = findNodeAt(nodes, nodeRadiusMultiplier, x, y); + + if (n) { + n.fx = n.x ?? x; + n.fy = n.y ?? y; + } + + return n; + }) + .on('drag', (event) => { + const n = event.subject; + n.x = event.x; + n.y = event.y; + drawGraph(context, canvas, nodes, links, transparentNodeMap, nodeRadiusMultiplier); + }); +} + +function buildInitialTransparencyMap(nodes: Node[]) { + const map = new Map(); + nodes.forEach(n => map.set(n.id, 0)); + return map; +} + +function buildRelationshipMap(nodes: Node[], links: Link[]) { + const map = new Map>(); + + nodes.forEach((node) => { + const set = new Set(); + + links.forEach((link) => { + const sourceId = link.source.id; + const targetId = link.target.id; + + if (sourceId === node.id) set.add(targetId); + else if (targetId === node.id) set.add(sourceId); + }); + + map.set(node.id, set); + }); + + return map; +} + +// Finds the topmost node under (x,y) +function findNodeAt(nodes: Node[], nodeRadiusMultiplier: number, x: number, y: number) { + for (let i = nodes.length - 1; i >= 0; i--) { + const n = nodes[i]; + + const r = n.value * nodeRadiusMultiplier; + const dx = x - n.x!; + const dy = y - n.y!; + + if (dx*dx + dy*dy <= r*r) return n; + } + return undefined; +} diff --git a/client/src/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts b/client/src/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts index 4f11ab1..dd9f270 100644 --- a/client/src/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts +++ b/client/src/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts @@ -1,8 +1,7 @@ import { useEffect, type RefObject } from "react"; import { forceSimulation, forceLink, forceX, forceY, forceCollide, forceManyBody } from "d3"; import type { Simulation } from "d3"; -import type { Node, Link, HubLink } from "@/types/network-graph.types.ts" -import { nodeGroupCenters, buildHubs, buildHubLinks, clearForces } from "@/features/NetworkGraph/lib/network-graph-utils.ts"; +import type { Node, Link, HubLink, Group } from "@/types/network-graph.types.ts" interface UseNetworkGraphSimulationArgs { nodes: Node[]; @@ -10,6 +9,20 @@ interface UseNetworkGraphSimulationArgs { canvasRef: RefObject; } +type NodeGroupCenters = { + people: [number, number]; + mail: [number, number]; + file: [number, number]; + issue: [number, number]; +} + +/** + * Runs physics simulation to determine initial position of nodes and links in the Network Graph + * + * Note: + * - D3.js force simulation is used to calculate the positions of all nodes and links + * - Nodes and link objects are mutated by D3.js force simulation to contain position values x and y + */ export function useNetworkGraphSimulation({ nodes, links, canvasRef }: UseNetworkGraphSimulationArgs) { useEffect(() => { if (!canvasRef.current) return; @@ -22,7 +35,7 @@ export function useNetworkGraphSimulation({ nodes, links, canvasRef }: UseNetwor const nodeRadiusMultiplier = 11; const nodePadding = 6; - const c = nodeGroupCenters(canvas.width, canvas.height); + const c = calculateNodeGroupCenters(canvas.width, canvas.height); const hubs = buildHubs(nodes); const hubLinks: HubLink[] = buildHubLinks(nodes, hubs); @@ -45,3 +58,46 @@ export function useNetworkGraphSimulation({ nodes, links, canvasRef }: UseNetwor }; }, [nodes, links, canvasRef]); } + +function calculateNodeGroupCenters(canvasWidth: number, canvasHeight: number): NodeGroupCenters { + return { + people: [canvasWidth * 0.5, canvasHeight * 0.4], + mail: [canvasWidth * 0.8, canvasHeight * 0.3], + file: [canvasWidth * 0.25, canvasHeight * 0.4], + issue: [canvasWidth * 0.8, canvasHeight * 0.6], + } +} + +function buildHubs(nodes: Node[]) { + const hubs: Partial> = {}; + + for (const n of nodes) { + const existing = hubs[n.group]; + if (!existing || n.value > existing.value) { + hubs[n.group] = n; + } + } + + return hubs; +} + +function buildHubLinks(nodes: Node[], hubs: Partial>) { + return nodes + .filter(n => hubs[n.group] && !isHub(n, hubs as Record)) + .map(n => ({ source: hubs[n.group]!, target: n })); +} + +function isHub (d: Node, hubs: Record) { + return hubs[d.group] === d; +} + +function clearForces(sim: Simulation) { + sim + .force('x', null) + .force('y', null) + .force('collide', null) + .force('charge', null) + .force('link', null) + .force('hubLinks', null) + .velocityDecay(1); // stop inertia +} \ No newline at end of file diff --git a/client/src/features/NetworkGraph/lib/network-graph-utils.ts b/client/src/features/NetworkGraph/lib/draw-network-graph.ts similarity index 57% rename from client/src/features/NetworkGraph/lib/network-graph-utils.ts rename to client/src/features/NetworkGraph/lib/draw-network-graph.ts index f9ade62..12c1c21 100644 --- a/client/src/features/NetworkGraph/lib/network-graph-utils.ts +++ b/client/src/features/NetworkGraph/lib/draw-network-graph.ts @@ -1,153 +1,60 @@ -import type {Group, Link, Node} from "@/types/network-graph.types.ts"; -import {drag, pointer, type Simulation} from "d3"; +import type { Link, Node } from "@/types/network-graph.types.ts"; +import { isNullOrUndefined } from "@/utils/type-guards.ts"; import type {Dispatch, SetStateAction} from "react"; -export const nodeGroupCenters = (w: number, h: number) => ({ - people: [w * 0.5, h * 0.4], - mail: [w * 0.8, h * 0.3], - file: [w * 0.25, h * 0.4], - issue: [w * 0.8, h * 0.6], -}); - -export function buildInitialTransparencyMap(nodes: Node[]) { - const map = new Map(); - nodes.forEach(n => map.set(n.id, 0)); - return map; -} - -export function highlightSubgraph( - hitNode: Node, - transparentNodeMap: Map, - nodeRelationshipMap: Map>, - setOverlayOn: Dispatch> -){ - if (!hasPos(hitNode)) return; - - const current = transparentNodeMap.get(hitNode.id) ?? 0; - - // Are we currently in "normal" mode? (everything opaque) - const allOpaque = Array.from(transparentNodeMap.values()).every(v => v === 0); - - const highlightNeighborhood = () => { - // fade everything - for (const key of transparentNodeMap.keys()) { - transparentNodeMap.set(key, 1); - } - - // make hit node + its related nodes opaque - const relatedNodeSet = nodeRelationshipMap.get(hitNode.id); - transparentNodeMap.set(hitNode.id, 0); - relatedNodeSet?.forEach(nodeId => { - transparentNodeMap.set(nodeId, 0); - }); - - setOverlayOn(true); - }; - - const clearAll = () => { - for (const key of transparentNodeMap.keys()) { - transparentNodeMap.set(key, 0); - } - }; - - if (allOpaque) { - // First time: go into "highlight" mode - highlightNeighborhood(); - return; - } +export function drawGraph( + context: CanvasRenderingContext2D, + canvas: HTMLCanvasElement, + nodes: Node[], + links: Link[], + transparentNodeMap: Map, + nodeRadiusMultiplier: number) +{ - // We're already in highlight mode (some nodes have value 1) + context.clearRect(0, 0, canvas.width, canvas.height); - if (current === 0) { - // Clicked *inside* the highlighted group → reset to fully opaque - clearAll(); - setOverlayOn(false); - } else { - // Clicked on a faded node → switch highlight to this node's group instead - highlightNeighborhood(); - } -} + context.strokeStyle = "grey"; -export function buildHubs(nodes: Node[]) { - const hubs: Partial> = {}; + links.forEach((link) => { + const s = link.source; + const t = link.target; - for (const n of nodes) { - const existing = hubs[n.group]; - if (!existing || n.value > existing.value) { - hubs[n.group] = n; - } - } + if (isNullOrUndefined(s.x) && isNullOrUndefined(s.y)) return; - return hubs; -} + const dx = t.x - s.x; + const dy = t.y - s.y; + const len = Math.hypot(dx, dy); + if (len === 0) return; -export function buildHubLinks(nodes: Node[], hubs: Partial>) { - return nodes - .filter(n => hubs[n.group] && !isHub(n, hubs as Record)) - .map(n => ({ source: hubs[n.group]!, target: n })); -} + const ux = dx / len; + const uy = dy / len; -function isHub (d: Node, hubs: Record) { - return hubs[d.group] === d; -} + const rSource = s.value * nodeRadiusMultiplier; + const rTarget = t.value * nodeRadiusMultiplier; + const headLength = 10; -export function createDragBehavior( - context: CanvasRenderingContext2D, - canvas: HTMLCanvasElement, - nodes: Node[], - nodeRadiusMultiplier: number, - links: Link[], - transparentNodeMap: Map) { - - return drag() - .subject((event) => { - const [x, y] = pointer(event, canvas); - - // find nearest node within radius - const n = findNodeAt(nodes, nodeRadiusMultiplier, x, y); - - if (n) { - n.fx = n.x ?? x; - n.fy = n.y ?? y; - } - - return n; - }) - .on('drag', (event) => { - const n = event.subject; - n.x = event.x; - n.y = event.y; - drawGraph(context, canvas, nodes, links, transparentNodeMap, nodeRadiusMultiplier); - }); -} + const fromX = s.x + ux * rSource; + const fromY = s.y + uy * rSource; + const toX = t.x - ux * (rTarget + headLength * 0.5); + const toY = t.y - uy * (rTarget + headLength * 0.5); -export function buildRelationshipMap(nodes: Node[], links: Link[]) { - const map = new Map>(); + drawArrow(context, fromX, fromY, toX, toY, headLength); + }); nodes.forEach((node) => { - const set = new Set(); - - links.forEach((link) => { - const sourceId = link.source.id; - const targetId = link.target.id; - - if (sourceId === node.id) set.add(targetId); - else if (targetId === node.id) set.add(sourceId); - }); - - map.set(node.id, set); - }); + drawNodeByGroup(node, context, transparentNodeMap, nodeRadiusMultiplier); + }) - return map; } -const drawNodeByGroup = ( +/** + * Helper function for drawing nodes + */ +function drawNodeByGroup( node: Node, context: CanvasRenderingContext2D, transparentNodeMap: Map, - nodeRadiusMultiplier: number) => { - - if (!hasPos(node)) return; + nodeRadiusMultiplier: number) { // Sets color of node according to group switch (node.group) { @@ -198,10 +105,6 @@ const drawNodeByGroup = ( // Draws the text context.fillText(label, node.x, node.y); context.restore(); -}; - -function hasPos(n: Node): n is Node & { x: number; y: number } { - return n.x != null && n.y != null; } function drawArrow( @@ -236,76 +139,57 @@ function drawArrow( ctx.fill(); } -export function drawGraph ( - context: CanvasRenderingContext2D, - canvas: HTMLCanvasElement, - nodes: Node[], - links: Link[], - transparentNodeMap: Map, - nodeRadiusMultiplier: number){ - - context.clearRect(0, 0, canvas.width, canvas.height); - - context.strokeStyle = "grey"; - - links.forEach((link) => { - const s = link.source; - const t = link.target; - - if (isNullOrUndefined(s.x) && isNullOrUndefined(s.y)) return; - - const dx = t.x - s.x; - const dy = t.y - s.y; - const len = Math.hypot(dx, dy); - if (len === 0) return; - - const ux = dx / len; - const uy = dy / len; - - const rSource = s.value * nodeRadiusMultiplier; - const rTarget = t.value * nodeRadiusMultiplier; - const headLength = 10; +/** + * Helper function for highlighting subgraph + */ +export function highlightSubgraph( + hitNode: Node, + transparentNodeMap: Map, + nodeRelationshipMap: Map>, + setOverlayOn: Dispatch> +){ - const fromX = s.x + ux * rSource; - const fromY = s.y + uy * rSource; - const toX = t.x - ux * (rTarget + headLength * 0.5); - const toY = t.y - uy * (rTarget + headLength * 0.5); + const current = transparentNodeMap.get(hitNode.id) ?? 0; - drawArrow(context, fromX, fromY, toX, toY, headLength); - }); + // Are we currently in "normal" mode? (everything opaque) + const allOpaque = Array.from(transparentNodeMap.values()).every(v => v === 0); - nodes.forEach((node) => { - drawNodeByGroup(node, context, transparentNodeMap, nodeRadiusMultiplier); - }) + const highlightNeighborhood = () => { + // fade everything + for (const key of transparentNodeMap.keys()) { + transparentNodeMap.set(key, 1); + } -} + // make hit node + its related nodes opaque + const relatedNodeSet = nodeRelationshipMap.get(hitNode.id); + transparentNodeMap.set(hitNode.id, 0); + relatedNodeSet?.forEach(nodeId => { + transparentNodeMap.set(nodeId, 0); + }); -// Finds the topmost node under (x,y) -export function findNodeAt(nodes: Node[], nodeRadiusMultiplier: number, x: number, y: number) { - for (let i = nodes.length - 1; i >= 0; i--) { - const n = nodes[i]; - if (!hasPos(n)) continue; + setOverlayOn(true); + }; - const r = n.value * nodeRadiusMultiplier; - const dx = x - n.x!; - const dy = y - n.y!; + const clearAll = () => { + for (const key of transparentNodeMap.keys()) { + transparentNodeMap.set(key, 0); + } + }; - if (dx*dx + dy*dy <= r*r) return n; + if (allOpaque) { + // First time: go into "highlight" mode + highlightNeighborhood(); + return; } - return undefined; -} -function isNullOrUndefined(value: T | null | undefined): value is null | undefined { - return value == null; -} + // We're already in highlight mode (some nodes have value 1) -export function clearForces(sim: Simulation) { - sim - .force('x', null) - .force('y', null) - .force('collide', null) - .force('charge', null) - .force('link', null) - .force('hubLinks', null) - .velocityDecay(1); // stop inertia + if (current === 0) { + // Clicked *inside* the highlighted group → reset to fully opaque + clearAll(); + setOverlayOn(false); + } else { + // Clicked on a faded node → switch highlight to this node's group instead + highlightNeighborhood(); + } } \ No newline at end of file diff --git a/client/src/features/NetworkGraph/stores/network-graph-context.tsx b/client/src/features/NetworkGraph/stores/network-graph-context.tsx index 4a4ae67..f0ce732 100644 --- a/client/src/features/NetworkGraph/stores/network-graph-context.tsx +++ b/client/src/features/NetworkGraph/stores/network-graph-context.tsx @@ -1,6 +1,6 @@ import { type ReactNode, createContext, useContext } from 'react'; import { type NetworkGraphData } from '@/types/network-graph.types.ts'; -import {useGraphJsonFiles} from "@/features/NetworkGraph/hooks/useNetworkGraphJsonData.ts"; +import {useNetworkGraphData} from "@/features/NetworkGraph/hooks/useNetworkGraphData.ts"; const NetworkGraphContext = createContext(null); @@ -8,6 +8,9 @@ interface NetworkGraphContextValue { data: NetworkGraphData; } +/** + * Allows a child component of the NetworkGraphProvider to use the context. + */ export function useNetworkGraph() { const ctx = useContext(NetworkGraphContext); @@ -18,10 +21,13 @@ export function useNetworkGraph() { return ctx; } +/** + * Provider of context for the Network Graph + * + * Calls useGraphJsonFiles to load and provide the .json data for the Network Graph. + */ export function NetworkGraphProvider({ children }: { children: ReactNode }) { - const data = useGraphJsonFiles(); - - + const data = useNetworkGraphData(); const value: NetworkGraphContextValue = { data diff --git a/client/src/utils/type-guards.ts b/client/src/utils/type-guards.ts new file mode 100644 index 0000000..be74413 --- /dev/null +++ b/client/src/utils/type-guards.ts @@ -0,0 +1,3 @@ +export function isNullOrUndefined(value: T | null | undefined): value is null | undefined { + return value == null; +} \ No newline at end of file From 2efff6467e3c6f8273ec14e8b991c6c5de447fa7 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Sat, 13 Dec 2025 00:11:47 -1000 Subject: [PATCH 27/29] Implemented search highlight capability. Modularized drawGraph function to accept colors for link and outline. Created drawToCanvas function in NetworkGraph. Exposed drawToCanvas to hooks used in NetworkGraph. Hooks use drawToCanvas after mutating nodes and links. Created MUI theme with dark mode. Resized navbar and changed color scheme of application. Removed in-line tailwind styling and replaced with MUI. --- client/index.html | 3 +- client/package.json | 2 + client/pnpm-lock.yaml | 401 ++++++++++++++++-- client/src/app/index.css | 14 +- client/src/app/provider.tsx | 4 + client/src/app/routes/landing.tsx | 33 +- client/src/components/layouts/app-layout.css | 12 +- client/src/components/layouts/app-layout.tsx | 27 +- client/src/components/ui/header/header.css | 7 - client/src/components/ui/header/header.tsx | 6 +- client/src/components/ui/navbar/navbar.tsx | 45 ++ .../components/network-graph-canvas.tsx | 76 ++++ .../components/network-graph-search-bar.tsx | 58 +++ .../components/network-graph-toolbar.css | 8 - .../components/network-graph-toolbar.tsx | 9 - .../NetworkGraph/components/network-graph.css | 11 - .../NetworkGraph/components/network-graph.tsx | 45 -- ...tion.ts => useInitialPhysicsSimulation.ts} | 16 +- .../hooks/useNetworkGraphInteractions.ts | 102 +---- client/src/features/NetworkGraph/index.ts | 2 +- .../NetworkGraph/lib/draw-network-graph.ts | 55 ++- .../stores/network-graph-context.tsx | 46 +- .../utils/transparent-node-map.ts | 26 ++ client/src/styles/theme.ts | 19 + client/src/types/network-graph.types.ts | 7 +- client/vite.config.ts | 9 +- 26 files changed, 782 insertions(+), 261 deletions(-) delete mode 100644 client/src/components/ui/header/header.css create mode 100644 client/src/components/ui/navbar/navbar.tsx create mode 100644 client/src/features/NetworkGraph/components/network-graph-canvas.tsx create mode 100644 client/src/features/NetworkGraph/components/network-graph-search-bar.tsx delete mode 100644 client/src/features/NetworkGraph/components/network-graph-toolbar.css delete mode 100644 client/src/features/NetworkGraph/components/network-graph-toolbar.tsx delete mode 100644 client/src/features/NetworkGraph/components/network-graph.css delete mode 100644 client/src/features/NetworkGraph/components/network-graph.tsx rename client/src/features/NetworkGraph/hooks/{useNetworkGraphSimulation.ts => useInitialPhysicsSimulation.ts} (87%) create mode 100644 client/src/features/NetworkGraph/utils/transparent-node-map.ts create mode 100644 client/src/styles/theme.ts diff --git a/client/index.html b/client/index.html index 36fc265..5070f50 100644 --- a/client/index.html +++ b/client/index.html @@ -3,10 +3,11 @@ - Vite + React + TS + Kaiaulu React
+ diff --git a/client/package.json b/client/package.json index 1d20ec1..8ed28f0 100644 --- a/client/package.json +++ b/client/package.json @@ -15,11 +15,13 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/material": "^7.3.5", + "@tailwindcss/vite": "^4.1.17", "@tanstack/react-query": "^5.90.3", "d3": "^7.9.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.9.3", + "tailwindcss": "^4.1.17", "yaml": "^2.8.1" }, "devDependencies": { diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index c6b165c..46eed0d 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@mui/material': specifier: ^7.3.5 version: 7.3.5(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@tailwindcss/vite': + specifier: ^4.1.17 + version: 4.1.17(vite@7.1.11(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) '@tanstack/react-query': specifier: ^5.90.3 version: 5.90.3(react@19.1.1) @@ -38,6 +41,9 @@ importers: react-router-dom: specifier: ^7.9.3 version: 7.9.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + tailwindcss: + specifier: ^4.1.17 + version: 4.1.17 yaml: specifier: ^2.8.1 version: 2.8.1 @@ -59,16 +65,16 @@ importers: version: 19.1.9(@types/react@19.1.12) '@vitejs/plugin-react': specifier: ^5.0.0 - version: 5.0.2(vite@7.1.11(@types/node@24.10.1)(yaml@2.8.1)) + version: 5.0.2(vite@7.1.11(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) eslint: specifier: ^9.33.0 - version: 9.35.0 + version: 9.35.0(jiti@2.6.1) eslint-plugin-react-hooks: specifier: ^5.2.0 - version: 5.2.0(eslint@9.35.0) + version: 5.2.0(eslint@9.35.0(jiti@2.6.1)) eslint-plugin-react-refresh: specifier: ^0.4.20 - version: 0.4.20(eslint@9.35.0) + version: 0.4.20(eslint@9.35.0(jiti@2.6.1)) globals: specifier: ^16.3.0 version: 16.4.0 @@ -77,10 +83,10 @@ importers: version: 5.8.3 typescript-eslint: specifier: ^8.39.1 - version: 8.43.0(eslint@9.35.0)(typescript@5.8.3) + version: 8.43.0(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3) vite: specifier: 7.1.11 - version: 7.1.11(@types/node@24.10.1)(yaml@2.8.1) + version: 7.1.11(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1) packages: @@ -670,6 +676,96 @@ packages: cpu: [x64] os: [win32] + '@tailwindcss/node@4.1.17': + resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==} + + '@tailwindcss/oxide-android-arm64@4.1.17': + resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.17': + resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.17': + resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.17': + resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': + resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': + resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.17': + resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.17': + resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.17': + resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.17': + resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': + resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.17': + resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.17': + resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.17': + resolution: {integrity: sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + '@tanstack/query-core@5.90.3': resolution: {integrity: sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA==} @@ -1113,12 +1209,20 @@ packages: delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} electron-to-chromium@1.5.215: resolution: {integrity: sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -1263,6 +1367,9 @@ packages: resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} engines: {node: '>=18'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -1323,6 +1430,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1359,6 +1470,76 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -1376,6 +1557,9 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1591,6 +1775,13 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tailwindcss@4.1.17: + resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -1996,9 +2187,9 @@ snapshots: '@esbuild/win32-x64@0.25.9': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.35.0)': + '@eslint-community/eslint-utils@4.9.0(eslint@9.35.0(jiti@2.6.1))': dependencies: - eslint: 9.35.0 + eslint: 9.35.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -2228,6 +2419,74 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.50.1': optional: true + '@tailwindcss/node@4.1.17': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.17 + + '@tailwindcss/oxide-android-arm64@4.1.17': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.17': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.17': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.17': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.17': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.17': + optional: true + + '@tailwindcss/oxide@4.1.17': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.17 + '@tailwindcss/oxide-darwin-arm64': 4.1.17 + '@tailwindcss/oxide-darwin-x64': 4.1.17 + '@tailwindcss/oxide-freebsd-x64': 4.1.17 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.17 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.17 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.17 + '@tailwindcss/oxide-linux-x64-musl': 4.1.17 + '@tailwindcss/oxide-wasm32-wasi': 4.1.17 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 + + '@tailwindcss/vite@4.1.17(vite@7.1.11(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1))': + dependencies: + '@tailwindcss/node': 4.1.17 + '@tailwindcss/oxide': 4.1.17 + tailwindcss: 4.1.17 + vite: 7.1.11(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1) + '@tanstack/query-core@5.90.3': {} '@tanstack/react-query@5.90.3(react@19.1.1)': @@ -2399,15 +2658,15 @@ snapshots: dependencies: csstype: 3.1.3 - '@typescript-eslint/eslint-plugin@8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.8.3))(eslint@9.35.0)(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.43.0(eslint@9.35.0)(typescript@5.8.3) + '@typescript-eslint/parser': 8.43.0(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.43.0 - '@typescript-eslint/type-utils': 8.43.0(eslint@9.35.0)(typescript@5.8.3) - '@typescript-eslint/utils': 8.43.0(eslint@9.35.0)(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.43.0(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3) + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.43.0 - eslint: 9.35.0 + eslint: 9.35.0(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -2416,14 +2675,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.8.3)': + '@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.43.0 '@typescript-eslint/types': 8.43.0 '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.43.0 debug: 4.4.1 - eslint: 9.35.0 + eslint: 9.35.0(jiti@2.6.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -2446,13 +2705,13 @@ snapshots: dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.43.0(eslint@9.35.0)(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.43.0(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3)': dependencies: '@typescript-eslint/types': 8.43.0 '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.43.0(eslint@9.35.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3) debug: 4.4.1 - eslint: 9.35.0 + eslint: 9.35.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -2476,13 +2735,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.43.0(eslint@9.35.0)(typescript@5.8.3)': + '@typescript-eslint/utils@8.43.0(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.43.0 '@typescript-eslint/types': 8.43.0 '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) - eslint: 9.35.0 + eslint: 9.35.0(jiti@2.6.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -2492,7 +2751,7 @@ snapshots: '@typescript-eslint/types': 8.43.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@5.0.2(vite@7.1.11(@types/node@24.10.1)(yaml@2.8.1))': + '@vitejs/plugin-react@5.0.2(vite@7.1.11(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -2500,7 +2759,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.34 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.11(@types/node@24.10.1)(yaml@2.8.1) + vite: 7.1.11(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -2756,6 +3015,8 @@ snapshots: dependencies: robust-predicates: 3.0.2 + detect-libc@2.1.2: {} + dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.28.4 @@ -2763,6 +3024,11 @@ snapshots: electron-to-chromium@1.5.215: {} + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -2800,13 +3066,13 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-react-hooks@5.2.0(eslint@9.35.0): + eslint-plugin-react-hooks@5.2.0(eslint@9.35.0(jiti@2.6.1)): dependencies: - eslint: 9.35.0 + eslint: 9.35.0(jiti@2.6.1) - eslint-plugin-react-refresh@0.4.20(eslint@9.35.0): + eslint-plugin-react-refresh@0.4.20(eslint@9.35.0(jiti@2.6.1)): dependencies: - eslint: 9.35.0 + eslint: 9.35.0(jiti@2.6.1) eslint-scope@8.4.0: dependencies: @@ -2817,9 +3083,9 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.35.0: + eslint@9.35.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.0 '@eslint/config-helpers': 0.3.1 @@ -2854,6 +3120,8 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color @@ -2938,6 +3206,8 @@ snapshots: globals@16.4.0: {} + graceful-fs@4.2.11: {} + graphemer@1.4.0: {} has-flag@4.0.0: {} @@ -2983,6 +3253,8 @@ snapshots: isexe@2.0.0: {} + jiti@2.6.1: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -3010,6 +3282,55 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + lines-and-columns@1.2.4: {} locate-path@6.0.0: @@ -3026,6 +3347,10 @@ snapshots: dependencies: yallist: 3.1.1 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + merge2@1.4.1: {} micromatch@4.0.8: @@ -3222,6 +3547,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tailwindcss@4.1.17: {} + + tapable@2.3.0: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -3241,13 +3570,13 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.43.0(eslint@9.35.0)(typescript@5.8.3): + typescript-eslint@8.43.0(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.8.3))(eslint@9.35.0)(typescript@5.8.3) - '@typescript-eslint/parser': 8.43.0(eslint@9.35.0)(typescript@5.8.3) + '@typescript-eslint/eslint-plugin': 8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3) + '@typescript-eslint/parser': 8.43.0(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3) '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.43.0(eslint@9.35.0)(typescript@5.8.3) - eslint: 9.35.0 + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0(jiti@2.6.1))(typescript@5.8.3) + eslint: 9.35.0(jiti@2.6.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -3266,7 +3595,7 @@ snapshots: dependencies: punycode: 2.3.1 - vite@7.1.11(@types/node@24.10.1)(yaml@2.8.1): + vite@7.1.11(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -3277,6 +3606,8 @@ snapshots: optionalDependencies: '@types/node': 24.10.1 fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 yaml: 2.8.1 which@2.0.2: diff --git a/client/src/app/index.css b/client/src/app/index.css index f321f8e..c06ae84 100644 --- a/client/src/app/index.css +++ b/client/src/app/index.css @@ -1,8 +1,10 @@ -html, body, #root { - height: 100%; -} +@import "tailwindcss"; -html, body { - margin: 0; - padding: 0; +@layer base { + html, + body, + #root { + height: 100%; + margin: 0; + } } \ No newline at end of file diff --git a/client/src/app/provider.tsx b/client/src/app/provider.tsx index f4c9d91..d070138 100644 --- a/client/src/app/provider.tsx +++ b/client/src/app/provider.tsx @@ -1,4 +1,6 @@ import { Suspense, type ReactNode } from 'react'; +import { ThemeProvider } from "@mui/material"; +import { theme } from "@/styles/theme.ts"; type AppProviderProps = { children: ReactNode; @@ -11,7 +13,9 @@ type AppProviderProps = { export const AppProvider = ({ children }: AppProviderProps) => { return ( + {children} + ); } \ No newline at end of file diff --git a/client/src/app/routes/landing.tsx b/client/src/app/routes/landing.tsx index 872901e..b25d39e 100644 --- a/client/src/app/routes/landing.tsx +++ b/client/src/app/routes/landing.tsx @@ -1,6 +1,7 @@ import { NetworkGraphProvider } from "@/features/NetworkGraph/stores/network-graph-context.tsx"; -import { NetworkGraph } from "@/features/NetworkGraph"; -import { NetworkGraphToolbar } from "@/features/NetworkGraph/components/network-graph-toolbar.tsx"; +import { NetworkGraphCanvas } from "@/features/NetworkGraph"; +import { Navbar } from "@/components/ui/navbar/navbar.tsx"; +import Box from "@mui/material/Box"; /** * Landing page component. @@ -9,12 +10,28 @@ import { NetworkGraphToolbar } from "@/features/NetworkGraph/components/network- */ const Landing = () => { return ( - <> - - - - - + + + + + + + + + ) } diff --git a/client/src/components/layouts/app-layout.css b/client/src/components/layouts/app-layout.css index da65b17..fe64e4d 100644 --- a/client/src/components/layouts/app-layout.css +++ b/client/src/components/layouts/app-layout.css @@ -1,15 +1,9 @@ #appContainer { - height: 100dvh; /* use dvh to avoid mobile 100vh bugs */ + height: 100dvh; width: 100dvw; - overflow: hidden; /* optional: hide scrollbars for full-bleed canvases */ - display: flex; /* if you have header/footer layout */ + overflow: hidden; + display: flex; flex-direction: column; color: white; flex: 1; - background: - radial-gradient(closest-side at 50% 50%, - rgba(0,0,0,0) 65%, - rgba(0,0,0,0.08) 100%), #fff; - - } \ No newline at end of file diff --git a/client/src/components/layouts/app-layout.tsx b/client/src/components/layouts/app-layout.tsx index f2a3cad..324186c 100644 --- a/client/src/components/layouts/app-layout.tsx +++ b/client/src/components/layouts/app-layout.tsx @@ -1,16 +1,31 @@ import { Outlet } from "react-router-dom"; -import { Header } from "../ui/header"; -import "./app-layout.css"; +import Box from "@mui/material/Box"; /** * Provides current layout for the application. Renders the header above any child components passed to it. */ const AppLayout = () => { return ( -
-
- -
+ + + + + ) } diff --git a/client/src/components/ui/header/header.css b/client/src/components/ui/header/header.css deleted file mode 100644 index 0cf37a8..0000000 --- a/client/src/components/ui/header/header.css +++ /dev/null @@ -1,7 +0,0 @@ -#header { - color: black; - display: flex; - flex: 0 0 5%; - justify-content: center; - overflow: auto; -} diff --git a/client/src/components/ui/header/header.tsx b/client/src/components/ui/header/header.tsx index 7d62921..dbc9db7 100644 --- a/client/src/components/ui/header/header.tsx +++ b/client/src/components/ui/header/header.tsx @@ -1,8 +1,8 @@ -import "./header.css"; export const Header = () => { return ( -
-
+
+ +
) } \ No newline at end of file diff --git a/client/src/components/ui/navbar/navbar.tsx b/client/src/components/ui/navbar/navbar.tsx new file mode 100644 index 0000000..8b7d8a9 --- /dev/null +++ b/client/src/components/ui/navbar/navbar.tsx @@ -0,0 +1,45 @@ +import AppBar from '@mui/material/AppBar'; +import Box from '@mui/material/Box'; +import Toolbar from '@mui/material/Toolbar'; +import IconButton from '@mui/material/IconButton'; +import logo from '@/assets/kaiaulu_logo.png'; +import { Typography } from "@mui/material"; +import { NetworkGraphSearchBar } from "@/features/NetworkGraph/components/network-graph-search-bar.tsx"; + +export const Navbar = () => { + + + return ( + + + + + + + + Projects + + + + + + ) +} \ No newline at end of file diff --git a/client/src/features/NetworkGraph/components/network-graph-canvas.tsx b/client/src/features/NetworkGraph/components/network-graph-canvas.tsx new file mode 100644 index 0000000..4b1fd18 --- /dev/null +++ b/client/src/features/NetworkGraph/components/network-graph-canvas.tsx @@ -0,0 +1,76 @@ +import { DndContext } from '@dnd-kit/core'; +import { useNetworkGraph } from "../stores/network-graph-context.tsx"; +import { useInitialPhysicsSimulation } from "@/features/NetworkGraph/hooks/useInitialPhysicsSimulation.ts"; +import { useNetworkGraphInteractions } from "@/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts"; +import { FloatingPanel } from "@/components/ui/floating-overlay/floating-overlay.tsx"; +import Box from "@mui/material/Box"; +import { useCallback, useEffect } from "react"; +import { drawGraph } from "@/features/NetworkGraph/lib/draw-network-graph.ts"; + +export const NetworkGraphCanvas = () => { + + const { + nodes, + links, + canvasRef, + overlayOn, + setOverlayOn, + nodeRelationshipMapRef, + transparentNodeMapRef, + } = useNetworkGraph(); + + const drawToCanvas = useCallback(() => { + if (!canvasRef.current) return; + const canvas = canvasRef.current; + + const container = canvas.parentElement; + if (!container) return; + + const rect = container.getBoundingClientRect(); + const nextW = Math.max(1, Math.floor(rect.width)); + const nextH = Math.max(1, Math.floor(rect.height)); + + if (canvas.width !== nextW) canvas.width = nextW; + if (canvas.height !== nextH) canvas.height = nextH; + + drawGraph(canvas, nodes, links, transparentNodeMapRef.current, + 11, + 'gray', + 'gray'); + + }, [canvasRef, links, nodes, transparentNodeMapRef]); + + useInitialPhysicsSimulation({ nodes, links, canvasRef, drawToCanvas: drawToCanvas }); + + useNetworkGraphInteractions({ + nodes, links, canvasRef, overlayOn, setOverlayOn, + nodeRelationshipMap: nodeRelationshipMapRef.current, + transparentNodeMap: transparentNodeMapRef.current, + drawToCanvas: drawToCanvas }); + + useEffect(() => { + drawToCanvas(); + }, [drawToCanvas, overlayOn]); + + return ( + + + + + + {overlayOn ? ( + +
+ Total nodes: +
+
+ Show More +
+
+ ) : null} +
+
+
+ ) +} + diff --git a/client/src/features/NetworkGraph/components/network-graph-search-bar.tsx b/client/src/features/NetworkGraph/components/network-graph-search-bar.tsx new file mode 100644 index 0000000..ccf8976 --- /dev/null +++ b/client/src/features/NetworkGraph/components/network-graph-search-bar.tsx @@ -0,0 +1,58 @@ +import { alpha, InputBase, styled } from "@mui/material"; +import { useNetworkGraph } from "@/features/NetworkGraph/stores/network-graph-context.tsx"; +import { useState, type FormEvent } from "react"; +import { + setAllNodesToBeTransparent, + setNodeNeighborhoodToBeOpaque +} from "@/features/NetworkGraph/utils/transparent-node-map.ts"; + + +const Search = styled("form")(({ theme }) => ({ + position: "relative", + borderRadius: theme.shape.borderRadius, + backgroundColor: theme.palette.background.default, + "&:hover": { + backgroundColor: alpha(theme.palette.common.white, 0.25), + }, + marginLeft: 0, + width: "100%", + [theme.breakpoints.up("sm")]: { + marginLeft: theme.spacing(1), + width: "auto", + }, + paddingLeft: 10, +})); + + +export const NetworkGraphSearchBar = () => { + const { nodeRelationshipMapRef, transparentNodeMapRef, overlayOn, setOverlayOn } = useNetworkGraph(); + + const [searchInput, setSearchInput] = useState(""); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + const query = searchInput.trim(); + if (!query) return; + + if (!overlayOn) { + setAllNodesToBeTransparent(transparentNodeMapRef.current); + setNodeNeighborhoodToBeOpaque(query, transparentNodeMapRef.current, nodeRelationshipMapRef.current); + setOverlayOn(true); + } + + }; + + return ( + + setSearchInput(e.target.value)} + placeholder="Search…" + inputProps={{ "aria-label": "search" }} + sx={{ + color: 'white', + }}> + + + ); +} \ No newline at end of file diff --git a/client/src/features/NetworkGraph/components/network-graph-toolbar.css b/client/src/features/NetworkGraph/components/network-graph-toolbar.css deleted file mode 100644 index a4c0fb7..0000000 --- a/client/src/features/NetworkGraph/components/network-graph-toolbar.css +++ /dev/null @@ -1,8 +0,0 @@ -#networkGraphToolbar { - color: black; - display: flex; - flex-direction: row; - flex: 0 0 10%; - justify-content: center; - overflow: auto; -} diff --git a/client/src/features/NetworkGraph/components/network-graph-toolbar.tsx b/client/src/features/NetworkGraph/components/network-graph-toolbar.tsx deleted file mode 100644 index 001fd31..0000000 --- a/client/src/features/NetworkGraph/components/network-graph-toolbar.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import "./network-graph-toolbar.css"; - -export const NetworkGraphToolbar = () => { - - return ( -
-
- ) -}; \ No newline at end of file diff --git a/client/src/features/NetworkGraph/components/network-graph.css b/client/src/features/NetworkGraph/components/network-graph.css deleted file mode 100644 index 0dc81bb..0000000 --- a/client/src/features/NetworkGraph/components/network-graph.css +++ /dev/null @@ -1,11 +0,0 @@ -#networkGraph { - display: flex; - flex: 1; - justify-content: center; - overflow: auto; -} - -#graphCanvas { - flex: 1; -} - diff --git a/client/src/features/NetworkGraph/components/network-graph.tsx b/client/src/features/NetworkGraph/components/network-graph.tsx deleted file mode 100644 index 4e93a4c..0000000 --- a/client/src/features/NetworkGraph/components/network-graph.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useRef, useState } from 'react'; -import './network-graph.css'; -import { DndContext } from '@dnd-kit/core'; -import { useNetworkGraph } from "../stores/network-graph-context.tsx"; -import { useNetworkGraphSimulation } from "@/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts"; -import { useNetworkGraphInteractions } from "@/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts"; -import {FloatingPanel} from "@/components/ui/floating-overlay/floating-overlay.tsx"; - -export const NetworkGraph = () => { - const [ overlayOn, setOverlayOn ] = useState(false); - - const { data } = useNetworkGraph(); - - const { nodes, links } = data ?? { nodes: [], links: [] }; - - const canvasRef = useRef(null); - - useNetworkGraphSimulation({ nodes, links, canvasRef }); - useNetworkGraphInteractions({ nodes, links, canvasRef, setOverlayOn }); - - return ( - -
- - - {overlayOn ? ( - -
- Total nodes: -
-
- Show More -
-
- ) : null} - -
-
- ) -} diff --git a/client/src/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts b/client/src/features/NetworkGraph/hooks/useInitialPhysicsSimulation.ts similarity index 87% rename from client/src/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts rename to client/src/features/NetworkGraph/hooks/useInitialPhysicsSimulation.ts index dd9f270..1e51ba4 100644 --- a/client/src/features/NetworkGraph/hooks/useNetworkGraphSimulation.ts +++ b/client/src/features/NetworkGraph/hooks/useInitialPhysicsSimulation.ts @@ -7,6 +7,7 @@ interface UseNetworkGraphSimulationArgs { nodes: Node[]; links: Link[]; canvasRef: RefObject; + drawToCanvas: () => void } type NodeGroupCenters = { @@ -23,7 +24,7 @@ type NodeGroupCenters = { * - D3.js force simulation is used to calculate the positions of all nodes and links * - Nodes and link objects are mutated by D3.js force simulation to contain position values x and y */ -export function useNetworkGraphSimulation({ nodes, links, canvasRef }: UseNetworkGraphSimulationArgs) { +export function useInitialPhysicsSimulation({ nodes, links, canvasRef, drawToCanvas }: UseNetworkGraphSimulationArgs) { useEffect(() => { if (!canvasRef.current) return; if (!nodes.length || !links.length) return; @@ -52,18 +53,15 @@ export function useNetworkGraphSimulation({ nodes, links, canvasRef }: UseNetwor clearForces(sim as Simulation); sim.stop(); - return () => { - clearForces(sim as Simulation); - sim.stop(); - }; - }, [nodes, links, canvasRef]); + drawToCanvas(); + }, [nodes, links, canvasRef, drawToCanvas]); } function calculateNodeGroupCenters(canvasWidth: number, canvasHeight: number): NodeGroupCenters { return { - people: [canvasWidth * 0.5, canvasHeight * 0.4], - mail: [canvasWidth * 0.8, canvasHeight * 0.3], - file: [canvasWidth * 0.25, canvasHeight * 0.4], + people: [canvasWidth * 0.5, canvasHeight * 0.5], + mail: [canvasWidth * 0.8, canvasHeight * 0.4], + file: [canvasWidth * 0.25, canvasHeight * 0.5], issue: [canvasWidth * 0.8, canvasHeight * 0.6], } } diff --git a/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts b/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts index 28eb099..c47ad85 100644 --- a/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts +++ b/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts @@ -1,9 +1,19 @@ import { useEffect, type RefObject, type SetStateAction, type Dispatch } from "react"; import { highlightSubgraph } from "../lib/draw-network-graph.ts"; -import { type Link, type Node } from "@/types/network-graph.types.ts" -import { drawGraph } from '../lib/draw-network-graph.ts' +import { type Link, type Node , type NodeRelationshipMap, type TransparentNodeMap} from "@/types/network-graph.types.ts" import { drag, type DragBehavior, pointer, select } from "d3"; +interface UseNetworkGraphInteractionsArgs { + nodes: Node[]; + links: Link[]; + canvasRef: RefObject; + overlayOn: boolean; + setOverlayOn: Dispatch>; + nodeRelationshipMap: NodeRelationshipMap; + transparentNodeMap: TransparentNodeMap; + drawToCanvas: () => void; +} + /** * Allows a network graph that is rendered to a canvas to become interactable to the user. * @@ -13,7 +23,8 @@ import { drag, type DragBehavior, pointer, select } from "d3"; * - Redraws network graph whenever canvas size changes (user resizes window, user opens browser console, etc.) */ export function useNetworkGraphInteractions( - { nodes, links, canvasRef, setOverlayOn }: { nodes: Node[]; links: Link[]; canvasRef: RefObject; setOverlayOn: Dispatch> } + { nodes, links, canvasRef, overlayOn, setOverlayOn, nodeRelationshipMap, transparentNodeMap, drawToCanvas } + : UseNetworkGraphInteractionsArgs ) { useEffect(() => { @@ -22,41 +33,11 @@ export function useNetworkGraphInteractions( const canvas = canvasRef.current; if (!canvas) return; - // Canvas parent container is stored, necessary for resizing 'drawing space' later - const container = canvas.parentElement; - if (!container) return; - - const context = canvas.getContext("2d"); - if (!context) return; - // Can be adjusted but is arbitrarily set to 11 const nodeRadiusMultiplier = 11; - // Map that stores every node's related nodes - const nodeRelationshipMap = buildRelationshipMap(nodes, links); - - // Map that stores transparency status of every node - const transparentNodeMap = buildInitialTransparencyMap(nodes); - - /** - * Helper function that resizes the drawing space for a canvas. - * - * Note: - * - A canvas has size as an HTML element, but also has virtual size for its drawing space. - * - The virtual size is being adjusted by this function, not the element size. - * - If the virtual size of a canvas does not match its element size then drawing on the canvas will look 'wrong' - */ - const resize = () => { - const rect = container.getBoundingClientRect(); - canvas.width = rect.width; - canvas.height = rect.height; - drawGraph(context, canvas, nodes, links, transparentNodeMap, nodeRadiusMultiplier); - }; - - resize(); - // Creates drag behavior - const dragBehavior = createDragBehavior(context, canvas, nodes, nodeRadiusMultiplier, links, transparentNodeMap,); + const dragBehavior = createDragBehavior(canvas, nodes, nodeRadiusMultiplier, drawToCanvas); // Attaches drag behavior to the canvas select(canvas).call(dragBehavior as DragBehavior); @@ -74,43 +55,26 @@ export function useNetworkGraphInteractions( const hit = findNodeAt(nodes, nodeRadiusMultiplier, x, y,); if (hit) { - highlightSubgraph(hit, transparentNodeMap, nodeRelationshipMap, setOverlayOn); - drawGraph(context, canvas, nodes, links, transparentNodeMap, nodeRadiusMultiplier); + highlightSubgraph(hit.id, transparentNodeMap, nodeRelationshipMap, setOverlayOn); + drawToCanvas(); } }; // Creates double-click listener that triggers double-click handler and attaches it to the canvas select(canvas).on("dblclick", handleDblClick); - /** - * Observes for when an element is resized and triggers the resize helper function. - * - * Note: - * - By attaching this observer to the parent container of the canvas, we ensure that its drawing space is adjusted - * whenever its element size changes. - */ - const ro = new ResizeObserver(() => { - resize(); - }); - - // Observes the parent container of the canvas - ro.observe(container); - - // Clean-up function that disconnects observer and listeners when the hook is no longer in use. + // Clean-up function that disconnects listener when the hook is no longer in use. return () => { - ro.disconnect(); select(canvas).on(".drag", null).on("dblclick", null); }; - }, [nodes, links, canvasRef, setOverlayOn]); + }, [nodes, links, canvasRef, setOverlayOn, transparentNodeMap, nodeRelationshipMap, drawToCanvas, overlayOn]); } function createDragBehavior( - context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, nodes: Node[], nodeRadiusMultiplier: number, - links: Link[], - transparentNodeMap: Map) { + requestDraw: () => void) { return drag() .subject((event) => { @@ -130,35 +94,11 @@ function createDragBehavior( const n = event.subject; n.x = event.x; n.y = event.y; - drawGraph(context, canvas, nodes, links, transparentNodeMap, nodeRadiusMultiplier); + requestDraw(); }); } -function buildInitialTransparencyMap(nodes: Node[]) { - const map = new Map(); - nodes.forEach(n => map.set(n.id, 0)); - return map; -} - -function buildRelationshipMap(nodes: Node[], links: Link[]) { - const map = new Map>(); - nodes.forEach((node) => { - const set = new Set(); - - links.forEach((link) => { - const sourceId = link.source.id; - const targetId = link.target.id; - - if (sourceId === node.id) set.add(targetId); - else if (targetId === node.id) set.add(sourceId); - }); - - map.set(node.id, set); - }); - - return map; -} // Finds the topmost node under (x,y) function findNodeAt(nodes: Node[], nodeRadiusMultiplier: number, x: number, y: number) { diff --git a/client/src/features/NetworkGraph/index.ts b/client/src/features/NetworkGraph/index.ts index 1daae81..359b235 100644 --- a/client/src/features/NetworkGraph/index.ts +++ b/client/src/features/NetworkGraph/index.ts @@ -1 +1 @@ -export { NetworkGraph } from './components/network-graph.tsx'; \ No newline at end of file +export { NetworkGraphCanvas } from './components/network-graph-canvas.tsx'; \ No newline at end of file diff --git a/client/src/features/NetworkGraph/lib/draw-network-graph.ts b/client/src/features/NetworkGraph/lib/draw-network-graph.ts index 12c1c21..434005c 100644 --- a/client/src/features/NetworkGraph/lib/draw-network-graph.ts +++ b/client/src/features/NetworkGraph/lib/draw-network-graph.ts @@ -3,17 +3,20 @@ import { isNullOrUndefined } from "@/utils/type-guards.ts"; import type {Dispatch, SetStateAction} from "react"; export function drawGraph( - context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, nodes: Node[], links: Link[], transparentNodeMap: Map, - nodeRadiusMultiplier: number) + nodeRadiusMultiplier: number, + linkColor: string, + nodeOutlineColor: string,) { + const context = canvas.getContext("2d"); + if (!context) return; context.clearRect(0, 0, canvas.width, canvas.height); - - context.strokeStyle = "grey"; + context.strokeStyle = linkColor; + context.fillStyle = linkColor; links.forEach((link) => { const s = link.source; @@ -42,7 +45,7 @@ export function drawGraph( }); nodes.forEach((node) => { - drawNodeByGroup(node, context, transparentNodeMap, nodeRadiusMultiplier); + drawNodeByGroup(node, context, transparentNodeMap, nodeRadiusMultiplier, nodeOutlineColor); }) } @@ -54,7 +57,8 @@ function drawNodeByGroup( node: Node, context: CanvasRenderingContext2D, transparentNodeMap: Map, - nodeRadiusMultiplier: number) { + nodeRadiusMultiplier: number, + nodeOutlineColor: string) { // Sets color of node according to group switch (node.group) { @@ -87,7 +91,7 @@ function drawNodeByGroup( // Outline context.lineWidth = 1; - context.strokeStyle = "#222"; // or per-group if you want + context.strokeStyle = nodeOutlineColor; context.stroke(); // Set text attributes @@ -143,13 +147,13 @@ function drawArrow( * Helper function for highlighting subgraph */ export function highlightSubgraph( - hitNode: Node, + hitNodeId: string, transparentNodeMap: Map, nodeRelationshipMap: Map>, - setOverlayOn: Dispatch> + setOverlayOn: Dispatch>, ){ - const current = transparentNodeMap.get(hitNode.id) ?? 0; + const current = transparentNodeMap.get(hitNodeId) ?? 0; // Are we currently in "normal" mode? (everything opaque) const allOpaque = Array.from(transparentNodeMap.values()).every(v => v === 0); @@ -161,8 +165,8 @@ export function highlightSubgraph( } // make hit node + its related nodes opaque - const relatedNodeSet = nodeRelationshipMap.get(hitNode.id); - transparentNodeMap.set(hitNode.id, 0); + const relatedNodeSet = nodeRelationshipMap.get(hitNodeId); + transparentNodeMap.set(hitNodeId, 0); relatedNodeSet?.forEach(nodeId => { transparentNodeMap.set(nodeId, 0); }); @@ -192,4 +196,31 @@ export function highlightSubgraph( // Clicked on a faded node → switch highlight to this node's group instead highlightNeighborhood(); } +} + + +export function buildInitialTransparencyMap(nodes: Node[]) { + const map = new Map(); + nodes.forEach(n => map.set(n.id, 0)); + return map; +} + +export function buildRelationshipMap(nodes: Node[], links: Link[]) { + const map = new Map>(); + + nodes.forEach((node) => { + const set = new Set(); + + links.forEach((link) => { + const sourceId = link.source.id; + const targetId = link.target.id; + + if (sourceId === node.id) set.add(targetId); + else if (targetId === node.id) set.add(sourceId); + }); + + map.set(node.id, set); + }); + + return map; } \ No newline at end of file diff --git a/client/src/features/NetworkGraph/stores/network-graph-context.tsx b/client/src/features/NetworkGraph/stores/network-graph-context.tsx index f0ce732..01ead70 100644 --- a/client/src/features/NetworkGraph/stores/network-graph-context.tsx +++ b/client/src/features/NetworkGraph/stores/network-graph-context.tsx @@ -1,11 +1,27 @@ -import { type ReactNode, createContext, useContext } from 'react'; -import { type NetworkGraphData } from '@/types/network-graph.types.ts'; -import {useNetworkGraphData} from "@/features/NetworkGraph/hooks/useNetworkGraphData.ts"; +import { + type ReactNode, + createContext, + useContext, + useState, + type Dispatch, + type SetStateAction, + useRef, + type RefObject, useEffect +} from 'react'; +import type { Node, Link } from "@/types/network-graph.types.ts" +import { useNetworkGraphData } from "@/features/NetworkGraph/hooks/useNetworkGraphData.ts"; +import { buildInitialTransparencyMap, buildRelationshipMap } from "@/features/NetworkGraph/lib/draw-network-graph.ts"; const NetworkGraphContext = createContext(null); interface NetworkGraphContextValue { - data: NetworkGraphData; + nodes: Node[]; + links: Link[]; + canvasRef: RefObject + overlayOn: boolean; + setOverlayOn: Dispatch>; + nodeRelationshipMapRef: RefObject>>; + transparentNodeMapRef: RefObject>; } /** @@ -27,10 +43,28 @@ export function useNetworkGraph() { * Calls useGraphJsonFiles to load and provide the .json data for the Network Graph. */ export function NetworkGraphProvider({ children }: { children: ReactNode }) { - const data = useNetworkGraphData(); + const { nodes, links } = useNetworkGraphData(); + + useEffect(() => { + nodeRelationshipMapRef.current = buildRelationshipMap(nodes, links); + transparentNodeMapRef.current = buildInitialTransparencyMap(nodes); + }, [nodes, links]); + + const transparentNodeMapRef = useRef>(new Map()); + const nodeRelationshipMapRef = useRef>>(new Map()); + + const [ overlayOn, setOverlayOn ] = useState(false); + + const canvasRef = useRef(null); const value: NetworkGraphContextValue = { - data + nodes, + links, + canvasRef, + overlayOn, + setOverlayOn, + nodeRelationshipMapRef, + transparentNodeMapRef }; return ( diff --git a/client/src/features/NetworkGraph/utils/transparent-node-map.ts b/client/src/features/NetworkGraph/utils/transparent-node-map.ts new file mode 100644 index 0000000..650383e --- /dev/null +++ b/client/src/features/NetworkGraph/utils/transparent-node-map.ts @@ -0,0 +1,26 @@ +/** + * Helper function that sets a target node and its related nodes transparency values to be opaque + */ +export function setNodeNeighborhoodToBeOpaque(hitNodeId: string, + transparentNodeMap: Map, + nodeRelationshipMap: Map>) { + + // make hit node + its related nodes opaque + const relatedNodeSet = nodeRelationshipMap.get(hitNodeId); + + transparentNodeMap.set(hitNodeId, 0); + + relatedNodeSet?.forEach(nodeId => { + transparentNodeMap.set(nodeId, 0); + }); + +} + +/** + * Helper function that sets all node transparency values to be transparent + */ +export function setAllNodesToBeTransparent(transparentNodeMap: Map) { + for (const key of transparentNodeMap.keys()) { + transparentNodeMap.set(key, 1); + } +} \ No newline at end of file diff --git a/client/src/styles/theme.ts b/client/src/styles/theme.ts new file mode 100644 index 0000000..7803c34 --- /dev/null +++ b/client/src/styles/theme.ts @@ -0,0 +1,19 @@ +import { createTheme } from '@mui/material/styles'; + +export const theme = createTheme({ + palette: { + mode: 'dark', + primary: { + main: '#2c2c2c', + }, + secondary: { + main: '#3c3c3c', + }, + background: { + default: '#444444', + paper: '#111827', + }, + }, + components: { + }, +}); diff --git a/client/src/types/network-graph.types.ts b/client/src/types/network-graph.types.ts index fa93afe..9c497ec 100644 --- a/client/src/types/network-graph.types.ts +++ b/client/src/types/network-graph.types.ts @@ -1,4 +1,3 @@ - import type { SimulationNodeDatum, SimulationLinkDatum } from "d3"; export interface Node extends SimulationNodeDatum { @@ -25,4 +24,8 @@ export type Group = 'people' | 'mail' | 'file' | 'issue'; export type HubLink = { source: Node; target: Node -}; \ No newline at end of file +}; + +export type NodeRelationshipMap = Map>; + +export type TransparentNodeMap = Map; \ No newline at end of file diff --git a/client/vite.config.ts b/client/vite.config.ts index 34f510a..ac87f27 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,9 +1,14 @@ -import { defineConfig } from 'vite' +import { defineConfig, type PluginOption } from 'vite' +import tailwindcss from '@tailwindcss/vite' import react from '@vitejs/plugin-react' import path from "path"; + export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + tailwindcss(), + ] as PluginOption[], resolve: { alias: { "@": path.resolve(__dirname, "src"), From 1d6f84088f01fb889994918d5ebfaa5c80ffd045 Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Sun, 14 Dec 2025 18:37:43 -1000 Subject: [PATCH 28/29] Changes to highlight, structure, style, and more. Modularization of drawGraph, now accepts colors as params Organized common helper functions by putting them in library. Simplified Network Graph hooks to require just one parameter Network Graph hooks accept canvas draw function as parameter. Separated further concerns, NetworkGraphCanvas owns draw function. Separated further concerns, SubGraphOverlay moved out of canvas. Removed remaining tailwind styling, just MUI now. Fixed search bar padding. Replaced divs with Box component from MUI. Added large test data. --- client/package.json | 1 + client/pnpm-lock.yaml | 8 + .../file-graph-data-test-large.json | 340 ++++++++++++++++++ .../mail-graph-data-test-large.json | 320 +++++++++++++++++ client/src/app/provider.tsx | 2 +- client/src/app/routes/landing.tsx | 28 +- .../ui/floating-overlay/floating-overlay.tsx | 28 +- client/src/components/ui/navbar/navbar.tsx | 7 +- .../components/network-graph-canvas.tsx | 76 ---- .../network-graph-canvas.tsx | 51 +++ .../network-graph-search-bar.tsx | 2 +- .../network-graph-sub-graph-overlay.tsx | 37 ++ .../hooks/useInitialPhysicsSimulation.ts | 25 +- .../NetworkGraph/hooks/useNetworkGraphData.ts | 2 +- .../hooks/useNetworkGraphInteractions.ts | 51 +-- client/src/features/NetworkGraph/index.ts | 1 - client/src/features/NetworkGraph/index.tsx | 31 ++ .../NetworkGraph/lib/draw-network-graph.ts | 3 +- .../stores/network-graph-context.tsx | 12 +- .../types/network-graph.types.ts | 6 +- .../utils/transparent-node-map.ts | 9 + client/src/styles/{ => mui}/theme.ts | 2 +- 22 files changed, 869 insertions(+), 173 deletions(-) create mode 100644 client/public/Projects/DummyProject/file-graph-data-test-large.json create mode 100644 client/public/Projects/DummyProject/mail-graph-data-test-large.json delete mode 100644 client/src/features/NetworkGraph/components/network-graph-canvas.tsx create mode 100644 client/src/features/NetworkGraph/components/network-graph-canvas/network-graph-canvas.tsx rename client/src/features/NetworkGraph/components/{ => network-graph-search-bar}/network-graph-search-bar.tsx (96%) create mode 100644 client/src/features/NetworkGraph/components/network-graph-sub-graph-overlay/network-graph-sub-graph-overlay.tsx delete mode 100644 client/src/features/NetworkGraph/index.ts create mode 100644 client/src/features/NetworkGraph/index.tsx rename client/src/{ => features/NetworkGraph}/types/network-graph.types.ts (82%) rename client/src/styles/{ => mui}/theme.ts (90%) diff --git a/client/package.json b/client/package.json index 8ed28f0..56f651a 100644 --- a/client/package.json +++ b/client/package.json @@ -14,6 +14,7 @@ "@dnd-kit/utilities": "^3.2.2", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@fontsource/roboto": "^5.2.9", "@mui/material": "^7.3.5", "@tailwindcss/vite": "^4.1.17", "@tanstack/react-query": "^5.90.3", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 46eed0d..442f685 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@emotion/styled': specifier: ^11.14.1 version: 11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1) + '@fontsource/roboto': + specifier: ^5.2.9 + version: 5.2.9 '@mui/material': specifier: ^7.3.5 version: 7.3.5(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react@19.1.1))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -441,6 +444,9 @@ packages: resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fontsource/roboto@5.2.9': + resolution: {integrity: sha512-ZTkyHiPk74B/aj8BZWbsxD5Yu+Lq+nR64eV4wirlrac2qXR7jYk2h6JlLYuOuoruTkGQWNw2fMuKNavw7/rg0w==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2231,6 +2237,8 @@ snapshots: '@eslint/core': 0.15.2 levn: 0.4.1 + '@fontsource/roboto@5.2.9': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': diff --git a/client/public/Projects/DummyProject/file-graph-data-test-large.json b/client/public/Projects/DummyProject/file-graph-data-test-large.json new file mode 100644 index 0000000..717c7a3 --- /dev/null +++ b/client/public/Projects/DummyProject/file-graph-data-test-large.json @@ -0,0 +1,340 @@ +{ + "nodes": [ + {"id": "file_1", "group": "file", "value": 2}, + {"id": "file_2", "group": "file", "value": 2}, + {"id": "file_3", "group": "file", "value": 2}, + {"id": "file_4", "group": "file", "value": 2}, + {"id": "file_5", "group": "file", "value": 2}, + {"id": "file_6", "group": "file", "value": 2}, + {"id": "file_7", "group": "file", "value": 2}, + {"id": "file_8", "group": "file", "value": 2}, + {"id": "file_9", "group": "file", "value": 2}, + {"id": "file_10", "group": "file", "value": 3}, + {"id": "file_11", "group": "file", "value": 4}, + {"id": "file_12", "group": "file", "value": 4}, + {"id": "file_13", "group": "file", "value": 2}, + {"id": "file_14", "group": "file", "value": 2}, + {"id": "file_15", "group": "file", "value": 2}, + {"id": "file_16", "group": "file", "value": 2}, + {"id": "file_17", "group": "file", "value": 2}, + {"id": "file_18", "group": "file", "value": 2}, + {"id": "file_19", "group": "file", "value": 2}, + {"id": "file_20", "group": "file", "value": 2}, + {"id": "file_21", "group": "file", "value": 2}, + {"id": "file_22", "group": "file", "value": 3}, + {"id": "file_23", "group": "file", "value": 4}, + {"id": "file_24", "group": "file", "value": 8}, + {"id": "file_25", "group": "file", "value": 2}, + {"id": "file_26", "group": "file", "value": 2}, + {"id": "file_27", "group": "file", "value": 2}, + {"id": "file_28", "group": "file", "value": 3}, + {"id": "file_29", "group": "file", "value": 2}, + {"id": "file_30", "group": "file", "value": 2}, + {"id": "file_31", "group": "file", "value": 2}, + {"id": "file_32", "group": "file", "value": 2}, + {"id": "file_33", "group": "file", "value": 2}, + {"id": "file_34", "group": "file", "value": 3}, + {"id": "file_35", "group": "file", "value": 5}, + {"id": "file_36", "group": "file", "value": 2}, + {"id": "file_37", "group": "file", "value": 2}, + {"id": "file_38", "group": "file", "value": 2}, + {"id": "file_39", "group": "file", "value": 2}, + {"id": "file_40", "group": "file", "value": 2}, + {"id": "file_41", "group": "file", "value": 2}, + {"id": "file_42", "group": "file", "value": 4}, + {"id": "file_43", "group": "file", "value": 2}, + {"id": "file_44", "group": "file", "value": 2}, + {"id": "file_45", "group": "file", "value": 2}, + {"id": "file_46", "group": "file", "value": 2}, + {"id": "file_47", "group": "file", "value": 2}, + {"id": "file_48", "group": "file", "value": 2}, + {"id": "file_49", "group": "file", "value": 3}, + {"id": "file_50", "group": "file", "value": 7}, + {"id": "file_51", "group": "file", "value": 2}, + {"id": "file_52", "group": "file", "value": 2}, + {"id": "file_53", "group": "file", "value": 2}, + {"id": "file_54", "group": "file", "value": 2}, + {"id": "file_55", "group": "file", "value": 5}, + {"id": "file_56", "group": "file", "value": 4}, + {"id": "file_57", "group": "file", "value": 2}, + {"id": "file_58", "group": "file", "value": 2}, + {"id": "file_59", "group": "file", "value": 2}, + {"id": "file_60", "group": "file", "value": 2}, + {"id": "file_61", "group": "file", "value": 2}, + {"id": "file_62", "group": "file", "value": 2}, + {"id": "file_63", "group": "file", "value": 3}, + {"id": "file_64", "group": "file", "value": 2}, + {"id": "file_65", "group": "file", "value": 2}, + {"id": "file_66", "group": "file", "value": 2}, + {"id": "file_67", "group": "file", "value": 2}, + {"id": "file_68", "group": "file", "value": 2}, + {"id": "file_69", "group": "file", "value": 2}, + {"id": "file_70", "group": "file", "value": 4}, + {"id": "file_71", "group": "file", "value": 2}, + {"id": "file_72", "group": "file", "value": 6}, + {"id": "file_73", "group": "file", "value": 2}, + {"id": "file_74", "group": "file", "value": 2}, + {"id": "file_75", "group": "file", "value": 2}, + {"id": "file_76", "group": "file", "value": 2}, + {"id": "file_77", "group": "file", "value": 2}, + {"id": "file_78", "group": "file", "value": 5}, + {"id": "file_79", "group": "file", "value": 2}, + {"id": "file_80", "group": "file", "value": 2}, + {"id": "file_81", "group": "file", "value": 2}, + {"id": "file_82", "group": "file", "value": 2}, + {"id": "file_83", "group": "file", "value": 2}, + {"id": "file_84", "group": "file", "value": 4}, + {"id": "file_85", "group": "file", "value": 2}, + {"id": "file_86", "group": "file", "value": 2}, + {"id": "file_87", "group": "file", "value": 2}, + {"id": "file_88", "group": "file", "value": 7}, + {"id": "file_89", "group": "file", "value": 2}, + {"id": "file_90", "group": "file", "value": 2}, + {"id": "file_91", "group": "file", "value": 3}, + {"id": "file_92", "group": "file", "value": 2}, + {"id": "file_93", "group": "file", "value": 2}, + {"id": "file_94", "group": "file", "value": 2}, + {"id": "file_95", "group": "file", "value": 2}, + {"id": "file_96", "group": "file", "value": 2}, + {"id": "file_97", "group": "file", "value": 2}, + {"id": "file_98", "group": "file", "value": 4}, + {"id": "file_99", "group": "file", "value": 2}, + {"id": "file_100", "group": "file", "value": 8} + ], + "links": [ + {"source": "file_4", "target": "file_2", "value": 1}, + {"source": "file_4", "target": "file_3", "value": 1}, + {"source": "file_4", "target": "file_1", "value": 1}, + {"source": "file_4", "target": "file_6", "value": 1}, + {"source": "file_4", "target": "issue_2", "value": 1}, + {"source": "file_6", "target": "file_1", "value": 1}, + {"source": "file_6", "target": "file_3", "value": 1}, + {"source": "file_6", "target": "file_5", "value": 1}, + {"source": "file_7", "target": "file_8", "value": 2}, + {"source": "file_8", "target": "file_9", "value": 2}, + {"source": "file_9", "target": "file_10", "value": 2}, + {"source": "file_9", "target": "file_22", "value": 2}, + {"source": "file_10", "target": "file_11", "value": 3}, + {"source": "file_11", "target": "file_12", "value": 4}, + {"source": "file_23", "target": "issue_1", "value": 4}, + {"source": "file_23", "target": "file_24", "value": 4}, + {"source": "file_12", "target": "file_13", "value": 1}, + {"source": "file_13", "target": "file_14", "value": 1}, + {"source": "file_14", "target": "file_15", "value": 1}, + {"source": "file_15", "target": "file_16", "value": 1}, + {"source": "file_16", "target": "file_17", "value": 1}, + {"source": "file_17", "target": "file_18", "value": 1}, + {"source": "file_18", "target": "file_19", "value": 1}, + {"source": "file_19", "target": "file_20", "value": 1}, + {"source": "file_20", "target": "file_21", "value": 2}, + {"source": "file_21", "target": "file_22", "value": 2}, + {"source": "file_22", "target": "file_23", "value": 2}, + {"source": "file_23", "target": "file_24", "value": 2}, + {"source": "file_24", "target": "file_25", "value": 2}, + {"source": "file_25", "target": "file_26", "value": 2}, + {"source": "file_26", "target": "file_27", "value": 2}, + {"source": "file_27", "target": "file_28", "value": 2}, + {"source": "file_28", "target": "file_29", "value": 2}, + {"source": "file_29", "target": "file_30", "value": 2}, + {"source": "file_30", "target": "file_31", "value": 2}, + {"source": "file_31", "target": "file_32", "value": 2}, + {"source": "file_32", "target": "file_33", "value": 2}, + {"source": "file_33", "target": "file_34", "value": 2}, + {"source": "file_34", "target": "file_35", "value": 2}, + {"source": "file_35", "target": "file_36", "value": 2}, + {"source": "file_36", "target": "file_37", "value": 2}, + {"source": "file_37", "target": "file_38", "value": 2}, + {"source": "file_38", "target": "file_39", "value": 2}, + {"source": "file_39", "target": "file_40", "value": 2}, + {"source": "file_24", "target": "file_13", "value": 2}, + {"source": "file_24", "target": "file_14", "value": 2}, + {"source": "file_24", "target": "file_15", "value": 2}, + {"source": "file_24", "target": "file_16", "value": 2}, + {"source": "file_24", "target": "file_17", "value": 2}, + {"source": "file_24", "target": "file_18", "value": 2}, + {"source": "file_24", "target": "file_19", "value": 2}, + {"source": "file_24", "target": "file_20", "value": 2}, + {"source": "file_24", "target": "file_21", "value": 2}, + {"source": "file_24", "target": "file_22", "value": 2}, + {"source": "file_24", "target": "file_25", "value": 3}, + {"source": "file_24", "target": "file_26", "value": 3}, + {"source": "file_24", "target": "file_27", "value": 3}, + {"source": "file_24", "target": "file_28", "value": 3}, + {"source": "file_24", "target": "file_29", "value": 3}, + {"source": "file_24", "target": "file_30", "value": 3}, + {"source": "file_24", "target": "file_31", "value": 3}, + {"source": "file_24", "target": "file_32", "value": 3}, + {"source": "file_50", "target": "file_33", "value": 2}, + {"source": "file_50", "target": "file_34", "value": 2}, + {"source": "file_50", "target": "file_35", "value": 2}, + {"source": "file_50", "target": "file_36", "value": 2}, + {"source": "file_50", "target": "file_37", "value": 2}, + {"source": "file_50", "target": "file_38", "value": 2}, + {"source": "file_50", "target": "file_39", "value": 2}, + {"source": "file_50", "target": "file_40", "value": 2}, + {"source": "file_50", "target": "file_41", "value": 2}, + {"source": "file_50", "target": "file_42", "value": 2}, + {"source": "file_50", "target": "file_43", "value": 2}, + {"source": "file_50", "target": "file_44", "value": 2}, + {"source": "file_50", "target": "file_45", "value": 3}, + {"source": "file_50", "target": "file_46", "value": 3}, + {"source": "file_50", "target": "file_47", "value": 3}, + {"source": "file_50", "target": "file_48", "value": 3}, + {"source": "file_50", "target": "file_49", "value": 3}, + {"source": "file_50", "target": "file_51", "value": 3}, + {"source": "file_50", "target": "file_52", "value": 3}, + {"source": "file_50", "target": "file_53", "value": 3}, + {"source": "file_50", "target": "file_54", "value": 3}, + {"source": "file_50", "target": "file_55", "value": 3}, + {"source": "file_40", "target": "file_42", "value": 2}, + {"source": "file_41", "target": "file_43", "value": 1}, + {"source": "file_42", "target": "file_44", "value": 2}, + {"source": "file_43", "target": "file_45", "value": 1}, + {"source": "file_44", "target": "file_46", "value": 2}, + {"source": "file_45", "target": "file_47", "value": 1}, + {"source": "file_46", "target": "file_48", "value": 2}, + {"source": "file_47", "target": "file_49", "value": 1}, + {"source": "file_48", "target": "file_50", "value": 2}, + {"source": "file_49", "target": "file_51", "value": 1}, + {"source": "file_50", "target": "file_52", "value": 2}, + {"source": "file_51", "target": "file_53", "value": 1}, + {"source": "file_52", "target": "file_54", "value": 2}, + {"source": "file_53", "target": "file_55", "value": 1}, + {"source": "file_54", "target": "file_56", "value": 2}, + {"source": "file_55", "target": "file_57", "value": 1}, + {"source": "file_56", "target": "file_58", "value": 2}, + {"source": "file_57", "target": "file_59", "value": 1}, + {"source": "file_58", "target": "file_60", "value": 2}, + {"source": "file_59", "target": "file_61", "value": 1}, + {"source": "file_24", "target": "issue_3", "value": 3}, + {"source": "file_37", "target": "issue_3", "value": 3}, + {"source": "file_50", "target": "issue_3", "value": 3}, + {"source": "file_58", "target": "issue_3", "value": 3}, + {"source": "file_63", "target": "issue_3", "value": 3}, + {"source": "file_72", "target": "issue_3", "value": 3}, + {"source": "file_88", "target": "issue_3", "value": 3}, + {"source": "file_100", "target": "issue_3", "value": 3}, + {"source": "file_4", "target": "issue_2", "value": 2}, + {"source": "file_6", "target": "issue_2", "value": 2}, + {"source": "file_18", "target": "issue_2", "value": 2}, + {"source": "file_33", "target": "issue_2", "value": 2}, + {"source": "file_41", "target": "issue_2", "value": 2}, + {"source": "file_57", "target": "issue_2", "value": 2}, + {"source": "file_75", "target": "issue_2", "value": 2}, + {"source": "file_23", "target": "issue_1", "value": 3}, + {"source": "file_24", "target": "issue_1", "value": 3}, + {"source": "file_45", "target": "issue_1", "value": 3}, + {"source": "file_50", "target": "issue_1", "value": 3}, + {"source": "file_67", "target": "issue_1", "value": 3}, + {"source": "file_88", "target": "issue_1", "value": 3}, + {"source": "file_60", "target": "file_61", "value": 1}, + {"source": "file_60", "target": "file_65", "value": 2}, + {"source": "file_61", "target": "file_62", "value": 1}, + {"source": "file_61", "target": "file_66", "value": 2}, + {"source": "file_62", "target": "file_63", "value": 1}, + {"source": "file_62", "target": "file_67", "value": 2}, + {"source": "file_63", "target": "file_64", "value": 1}, + {"source": "file_63", "target": "file_68", "value": 2}, + {"source": "file_64", "target": "file_65", "value": 1}, + {"source": "file_64", "target": "file_69", "value": 2}, + {"source": "file_65", "target": "file_66", "value": 1}, + {"source": "file_65", "target": "file_70", "value": 2}, + {"source": "file_66", "target": "file_67", "value": 1}, + {"source": "file_66", "target": "file_71", "value": 2}, + {"source": "file_67", "target": "file_68", "value": 1}, + {"source": "file_67", "target": "file_72", "value": 2}, + {"source": "file_68", "target": "file_69", "value": 1}, + {"source": "file_68", "target": "file_73", "value": 2}, + {"source": "file_69", "target": "file_70", "value": 1}, + {"source": "file_69", "target": "file_74", "value": 2}, + {"source": "file_70", "target": "file_71", "value": 1}, + {"source": "file_70", "target": "file_75", "value": 2}, + {"source": "file_71", "target": "file_72", "value": 1}, + {"source": "file_71", "target": "file_76", "value": 2}, + {"source": "file_72", "target": "file_73", "value": 1}, + {"source": "file_72", "target": "file_77", "value": 2}, + {"source": "file_73", "target": "file_74", "value": 1}, + {"source": "file_73", "target": "file_78", "value": 2}, + {"source": "file_74", "target": "file_75", "value": 1}, + {"source": "file_74", "target": "file_79", "value": 2}, + {"source": "file_75", "target": "file_76", "value": 1}, + {"source": "file_75", "target": "file_80", "value": 2}, + {"source": "file_76", "target": "file_77", "value": 1}, + {"source": "file_77", "target": "file_78", "value": 1}, + {"source": "file_78", "target": "file_79", "value": 1}, + {"source": "file_79", "target": "file_80", "value": 1}, + {"source": "file_72", "target": "file_61", "value": 2}, + {"source": "file_72", "target": "file_62", "value": 2}, + {"source": "file_72", "target": "file_63", "value": 2}, + {"source": "file_72", "target": "file_64", "value": 2}, + {"source": "file_72", "target": "file_65", "value": 2}, + {"source": "file_72", "target": "file_66", "value": 2}, + {"source": "file_72", "target": "file_67", "value": 2}, + {"source": "file_72", "target": "file_68", "value": 2}, + {"source": "file_72", "target": "file_69", "value": 2}, + {"source": "file_72", "target": "file_70", "value": 2}, + {"source": "file_72", "target": "file_71", "value": 2}, + {"source": "file_72", "target": "file_73", "value": 2}, + {"source": "file_72", "target": "file_74", "value": 2}, + {"source": "file_72", "target": "file_75", "value": 2}, + {"source": "file_72", "target": "file_76", "value": 2}, + {"source": "file_72", "target": "file_77", "value": 2}, + {"source": "file_72", "target": "file_78", "value": 2}, + {"source": "file_72", "target": "file_79", "value": 2}, + {"source": "file_72", "target": "file_80", "value": 2}, + {"source": "file_72", "target": "file_81", "value": 2}, + {"source": "file_72", "target": "file_82", "value": 2}, + {"source": "file_80", "target": "file_81", "value": 2}, + {"source": "file_81", "target": "file_82", "value": 2}, + {"source": "file_82", "target": "file_83", "value": 2}, + {"source": "file_83", "target": "file_84", "value": 2}, + {"source": "file_84", "target": "file_85", "value": 2}, + {"source": "file_85", "target": "file_86", "value": 2}, + {"source": "file_86", "target": "file_87", "value": 2}, + {"source": "file_87", "target": "file_88", "value": 2}, + {"source": "file_88", "target": "file_89", "value": 2}, + {"source": "file_89", "target": "file_90", "value": 2}, + {"source": "file_90", "target": "file_91", "value": 3}, + {"source": "file_91", "target": "file_92", "value": 3}, + {"source": "file_92", "target": "file_93", "value": 3}, + {"source": "file_93", "target": "file_94", "value": 3}, + {"source": "file_94", "target": "file_95", "value": 3}, + {"source": "file_95", "target": "file_96", "value": 3}, + {"source": "file_96", "target": "file_97", "value": 3}, + {"source": "file_97", "target": "file_98", "value": 3}, + {"source": "file_98", "target": "file_99", "value": 3}, + {"source": "file_99", "target": "file_100", "value": 3}, + {"source": "file_82", "target": "file_84", "value": 2}, + {"source": "file_84", "target": "file_86", "value": 2}, + {"source": "file_82", "target": "file_86", "value": 1}, + {"source": "file_85", "target": "file_87", "value": 2}, + {"source": "file_87", "target": "file_89", "value": 2}, + {"source": "file_85", "target": "file_89", "value": 1}, + {"source": "file_90", "target": "file_92", "value": 2}, + {"source": "file_92", "target": "file_94", "value": 2}, + {"source": "file_90", "target": "file_94", "value": 1}, + {"source": "file_93", "target": "file_95", "value": 2}, + {"source": "file_95", "target": "file_97", "value": 2}, + {"source": "file_93", "target": "file_97", "value": 1}, + {"source": "file_96", "target": "file_98", "value": 2}, + {"source": "file_98", "target": "file_100", "value": 2}, + {"source": "file_96", "target": "file_100", "value": 1}, + {"source": "file_88", "target": "file_83", "value": 3}, + {"source": "file_88", "target": "file_84", "value": 3}, + {"source": "file_88", "target": "file_85", "value": 3}, + {"source": "file_88", "target": "file_86", "value": 3}, + {"source": "file_88", "target": "file_87", "value": 3}, + {"source": "file_88", "target": "file_89", "value": 3}, + {"source": "file_88", "target": "file_90", "value": 3}, + {"source": "file_88", "target": "file_91", "value": 3}, + {"source": "file_88", "target": "file_92", "value": 3}, + {"source": "file_88", "target": "file_93", "value": 3}, + {"source": "file_88", "target": "file_94", "value": 3}, + {"source": "file_88", "target": "file_95", "value": 3}, + {"source": "file_88", "target": "file_96", "value": 3}, + {"source": "file_88", "target": "file_97", "value": 3}, + {"source": "file_88", "target": "file_98", "value": 3}, + {"source": "file_88", "target": "file_99", "value": 3}, + {"source": "file_88", "target": "file_100", "value": 3} + ] +} \ No newline at end of file diff --git a/client/public/Projects/DummyProject/mail-graph-data-test-large.json b/client/public/Projects/DummyProject/mail-graph-data-test-large.json new file mode 100644 index 0000000..8da2d4b --- /dev/null +++ b/client/public/Projects/DummyProject/mail-graph-data-test-large.json @@ -0,0 +1,320 @@ +{ + "nodes": [ + { "id": "mail_1", "group": "mail", "value": 2 }, + { "id": "mail_2", "group": "mail", "value": 2 }, + { "id": "mail_3", "group": "mail", "value": 2 }, + { "id": "mail_4", "group": "mail", "value": 2 }, + { "id": "mail_5", "group": "mail", "value": 2 }, + { "id": "mail_6", "group": "mail", "value": 2 }, + { "id": "mail_7", "group": "mail", "value": 2 }, + { "id": "mail_8", "group": "mail", "value": 2 }, + + { "id": "mail_9", "group": "mail", "value": 2 }, + { "id": "mail_10", "group": "mail", "value": 3 }, + { "id": "mail_11", "group": "mail", "value": 2 }, + { "id": "mail_12", "group": "mail", "value": 2 }, + { "id": "mail_13", "group": "mail", "value": 2 }, + { "id": "mail_14", "group": "mail", "value": 3 }, + { "id": "mail_15", "group": "mail", "value": 2 }, + { "id": "mail_16", "group": "mail", "value": 2 }, + { "id": "mail_17", "group": "mail", "value": 2 }, + { "id": "mail_18", "group": "mail", "value": 2 }, + { "id": "mail_19", "group": "mail", "value": 2 }, + { "id": "mail_20", "group": "mail", "value": 3 }, + { "id": "mail_21", "group": "mail", "value": 2 }, + { "id": "mail_22", "group": "mail", "value": 4 }, + { "id": "mail_23", "group": "mail", "value": 2 }, + { "id": "mail_24", "group": "mail", "value": 6 }, + { "id": "mail_25", "group": "mail", "value": 2 }, + { "id": "mail_26", "group": "mail", "value": 2 }, + { "id": "mail_27", "group": "mail", "value": 2 }, + { "id": "mail_28", "group": "mail", "value": 3 }, + { "id": "mail_29", "group": "mail", "value": 2 }, + { "id": "mail_30", "group": "mail", "value": 2 }, + { "id": "mail_31", "group": "mail", "value": 2 }, + { "id": "mail_32", "group": "mail", "value": 2 }, + { "id": "mail_33", "group": "mail", "value": 2 }, + { "id": "mail_34", "group": "mail", "value": 3 }, + { "id": "mail_35", "group": "mail", "value": 5 }, + { "id": "mail_36", "group": "mail", "value": 2 }, + { "id": "mail_37", "group": "mail", "value": 2 }, + { "id": "mail_38", "group": "mail", "value": 2 }, + { "id": "mail_39", "group": "mail", "value": 2 }, + { "id": "mail_40", "group": "mail", "value": 2 }, + { "id": "mail_41", "group": "mail", "value": 2 }, + { "id": "mail_42", "group": "mail", "value": 4 }, + { "id": "mail_43", "group": "mail", "value": 2 }, + { "id": "mail_44", "group": "mail", "value": 2 }, + { "id": "mail_45", "group": "mail", "value": 2 }, + { "id": "mail_46", "group": "mail", "value": 2 }, + { "id": "mail_47", "group": "mail", "value": 2 }, + { "id": "mail_48", "group": "mail", "value": 2 }, + { "id": "mail_49", "group": "mail", "value": 3 }, + { "id": "mail_50", "group": "mail", "value": 7 }, + + { "id": "mail_51", "group": "mail", "value": 2 }, + { "id": "mail_52", "group": "mail", "value": 2 }, + { "id": "mail_53", "group": "mail", "value": 2 }, + { "id": "mail_54", "group": "mail", "value": 2 }, + { "id": "mail_55", "group": "mail", "value": 5 }, + { "id": "mail_56", "group": "mail", "value": 4 }, + { "id": "mail_57", "group": "mail", "value": 2 }, + { "id": "mail_58", "group": "mail", "value": 2 }, + { "id": "mail_59", "group": "mail", "value": 2 }, + { "id": "mail_60", "group": "mail", "value": 2 }, + { "id": "mail_61", "group": "mail", "value": 2 }, + { "id": "mail_62", "group": "mail", "value": 2 }, + { "id": "mail_63", "group": "mail", "value": 3 }, + { "id": "mail_64", "group": "mail", "value": 2 }, + { "id": "mail_65", "group": "mail", "value": 2 }, + { "id": "mail_66", "group": "mail", "value": 2 }, + { "id": "mail_67", "group": "mail", "value": 2 }, + { "id": "mail_68", "group": "mail", "value": 2 }, + { "id": "mail_69", "group": "mail", "value": 2 }, + { "id": "mail_70", "group": "mail", "value": 4 }, + { "id": "mail_71", "group": "mail", "value": 2 }, + { "id": "mail_72", "group": "mail", "value": 6 }, + { "id": "mail_73", "group": "mail", "value": 2 }, + { "id": "mail_74", "group": "mail", "value": 2 }, + { "id": "mail_75", "group": "mail", "value": 2 }, + { "id": "mail_76", "group": "mail", "value": 2 }, + { "id": "mail_77", "group": "mail", "value": 2 }, + { "id": "mail_78", "group": "mail", "value": 5 }, + { "id": "mail_79", "group": "mail", "value": 2 }, + { "id": "mail_80", "group": "mail", "value": 2 }, + { "id": "mail_81", "group": "mail", "value": 2 }, + { "id": "mail_82", "group": "mail", "value": 2 }, + { "id": "mail_83", "group": "mail", "value": 2 }, + { "id": "mail_84", "group": "mail", "value": 4 }, + { "id": "mail_85", "group": "mail", "value": 2 }, + { "id": "mail_86", "group": "mail", "value": 2 }, + { "id": "mail_87", "group": "mail", "value": 2 }, + { "id": "mail_88", "group": "mail", "value": 7 }, + { "id": "mail_89", "group": "mail", "value": 2 }, + { "id": "mail_90", "group": "mail", "value": 2 }, + { "id": "mail_91", "group": "mail", "value": 3 }, + { "id": "mail_92", "group": "mail", "value": 2 }, + { "id": "mail_93", "group": "mail", "value": 2 }, + { "id": "mail_94", "group": "mail", "value": 2 }, + { "id": "mail_95", "group": "mail", "value": 2 }, + { "id": "mail_96", "group": "mail", "value": 2 }, + { "id": "mail_97", "group": "mail", "value": 2 }, + { "id": "mail_98", "group": "mail", "value": 4 }, + { "id": "mail_99", "group": "mail", "value": 2 }, + { "id": "mail_100", "group": "mail", "value": 8 } + ], + "links": [ + { "source": "mail_2", "target": "mail_1", "value": 1 }, + { "source": "mail_3", "target": "mail_1", "value": 1 }, + { "source": "mail_4", "target": "mail_1", "value": 1 }, + { "source": "mail_4", "target": "mail_1", "value": 1 }, + { "source": "mail_5", "target": "mail_1", "value": 1 }, + { "source": "mail_5", "target": "mail_4", "value": 1 }, + { "source": "mail_5", "target": "mail_1", "value": 1 }, + { "source": "mail_6", "target": "mail_7", "value": 1 }, + { "source": "mail_7", "target": "mail_8", "value": 1 }, + + { "source": "mail_8", "target": "mail_9", "value": 2 }, + { "source": "mail_9", "target": "mail_10", "value": 2 }, + { "source": "mail_10", "target": "mail_11", "value": 2 }, + { "source": "mail_10", "target": "mail_14", "value": 2 }, + { "source": "mail_11", "target": "mail_12", "value": 2 }, + { "source": "mail_12", "target": "mail_13", "value": 2 }, + { "source": "mail_14", "target": "mail_15", "value": 3 }, + { "source": "mail_15", "target": "mail_16", "value": 2 }, + { "source": "mail_16", "target": "mail_17", "value": 2 }, + { "source": "mail_17", "target": "mail_18", "value": 2 }, + { "source": "mail_18", "target": "mail_19", "value": 2 }, + { "source": "mail_19", "target": "mail_20", "value": 2 }, + { "source": "mail_20", "target": "mail_21", "value": 2 }, + { "source": "mail_21", "target": "mail_22", "value": 3 }, + { "source": "mail_22", "target": "mail_23", "value": 3 }, + { "source": "mail_23", "target": "mail_24", "value": 4 }, + + { "source": "mail_24", "target": "mail_25", "value": 2 }, + { "source": "mail_25", "target": "mail_26", "value": 2 }, + { "source": "mail_26", "target": "mail_27", "value": 2 }, + { "source": "mail_27", "target": "mail_28", "value": 2 }, + { "source": "mail_28", "target": "mail_29", "value": 2 }, + { "source": "mail_29", "target": "mail_30", "value": 2 }, + { "source": "mail_30", "target": "mail_31", "value": 2 }, + { "source": "mail_31", "target": "mail_32", "value": 2 }, + { "source": "mail_32", "target": "mail_33", "value": 2 }, + { "source": "mail_33", "target": "mail_34", "value": 2 }, + { "source": "mail_34", "target": "mail_35", "value": 2 }, + { "source": "mail_35", "target": "mail_36", "value": 2 }, + { "source": "mail_36", "target": "mail_37", "value": 2 }, + { "source": "mail_37", "target": "mail_38", "value": 2 }, + { "source": "mail_38", "target": "mail_39", "value": 2 }, + { "source": "mail_39", "target": "mail_40", "value": 2 }, + + { "source": "mail_24", "target": "mail_13", "value": 2 }, + { "source": "mail_24", "target": "mail_14", "value": 2 }, + { "source": "mail_24", "target": "mail_15", "value": 2 }, + { "source": "mail_24", "target": "mail_16", "value": 2 }, + { "source": "mail_24", "target": "mail_17", "value": 2 }, + { "source": "mail_24", "target": "mail_18", "value": 2 }, + { "source": "mail_24", "target": "mail_19", "value": 2 }, + { "source": "mail_24", "target": "mail_20", "value": 2 }, + { "source": "mail_24", "target": "mail_21", "value": 2 }, + { "source": "mail_24", "target": "mail_22", "value": 2 }, + + { "source": "mail_50", "target": "mail_33", "value": 2 }, + { "source": "mail_50", "target": "mail_34", "value": 2 }, + { "source": "mail_50", "target": "mail_35", "value": 2 }, + { "source": "mail_50", "target": "mail_36", "value": 2 }, + { "source": "mail_50", "target": "mail_37", "value": 2 }, + { "source": "mail_50", "target": "mail_38", "value": 2 }, + { "source": "mail_50", "target": "mail_39", "value": 2 }, + { "source": "mail_50", "target": "mail_40", "value": 2 }, + { "source": "mail_50", "target": "mail_41", "value": 2 }, + { "source": "mail_50", "target": "mail_42", "value": 2 }, + { "source": "mail_50", "target": "mail_43", "value": 2 }, + { "source": "mail_50", "target": "mail_44", "value": 2 }, + { "source": "mail_50", "target": "mail_45", "value": 3 }, + { "source": "mail_50", "target": "mail_46", "value": 3 }, + { "source": "mail_50", "target": "mail_47", "value": 3 }, + { "source": "mail_50", "target": "mail_48", "value": 3 }, + { "source": "mail_50", "target": "mail_49", "value": 3 }, + { "source": "mail_50", "target": "mail_51", "value": 3 }, + { "source": "mail_50", "target": "mail_52", "value": 3 }, + { "source": "mail_50", "target": "mail_53", "value": 3 }, + { "source": "mail_50", "target": "mail_54", "value": 3 }, + { "source": "mail_50", "target": "mail_55", "value": 3 }, + + { "source": "mail_40", "target": "mail_42", "value": 2 }, + { "source": "mail_41", "target": "mail_43", "value": 1 }, + { "source": "mail_42", "target": "mail_44", "value": 2 }, + { "source": "mail_43", "target": "mail_45", "value": 1 }, + { "source": "mail_44", "target": "mail_46", "value": 2 }, + { "source": "mail_45", "target": "mail_47", "value": 1 }, + { "source": "mail_46", "target": "mail_48", "value": 2 }, + { "source": "mail_47", "target": "mail_49", "value": 1 }, + { "source": "mail_48", "target": "mail_50", "value": 2 }, + { "source": "mail_49", "target": "mail_51", "value": 1 }, + { "source": "mail_50", "target": "mail_52", "value": 2 }, + { "source": "mail_51", "target": "mail_53", "value": 1 }, + { "source": "mail_52", "target": "mail_54", "value": 2 }, + { "source": "mail_53", "target": "mail_55", "value": 1 }, + { "source": "mail_54", "target": "mail_56", "value": 2 }, + { "source": "mail_55", "target": "mail_57", "value": 1 }, + { "source": "mail_56", "target": "mail_58", "value": 2 }, + { "source": "mail_57", "target": "mail_59", "value": 1 }, + { "source": "mail_58", "target": "mail_60", "value": 2 }, + { "source": "mail_59", "target": "mail_61", "value": 1 }, + + { "source": "mail_60", "target": "mail_61", "value": 1 }, + { "source": "mail_60", "target": "mail_65", "value": 2 }, + { "source": "mail_61", "target": "mail_62", "value": 1 }, + { "source": "mail_61", "target": "mail_66", "value": 2 }, + { "source": "mail_62", "target": "mail_63", "value": 1 }, + { "source": "mail_62", "target": "mail_67", "value": 2 }, + { "source": "mail_63", "target": "mail_64", "value": 1 }, + { "source": "mail_63", "target": "mail_68", "value": 2 }, + { "source": "mail_64", "target": "mail_65", "value": 1 }, + { "source": "mail_64", "target": "mail_69", "value": 2 }, + { "source": "mail_65", "target": "mail_66", "value": 1 }, + { "source": "mail_65", "target": "mail_70", "value": 2 }, + { "source": "mail_66", "target": "mail_67", "value": 1 }, + { "source": "mail_66", "target": "mail_71", "value": 2 }, + { "source": "mail_67", "target": "mail_68", "value": 1 }, + { "source": "mail_67", "target": "mail_72", "value": 2 }, + { "source": "mail_68", "target": "mail_69", "value": 1 }, + { "source": "mail_68", "target": "mail_73", "value": 2 }, + { "source": "mail_69", "target": "mail_70", "value": 1 }, + { "source": "mail_69", "target": "mail_74", "value": 2 }, + { "source": "mail_70", "target": "mail_71", "value": 1 }, + { "source": "mail_70", "target": "mail_75", "value": 2 }, + { "source": "mail_71", "target": "mail_72", "value": 1 }, + { "source": "mail_71", "target": "mail_76", "value": 2 }, + { "source": "mail_72", "target": "mail_73", "value": 1 }, + { "source": "mail_72", "target": "mail_77", "value": 2 }, + { "source": "mail_73", "target": "mail_74", "value": 1 }, + { "source": "mail_73", "target": "mail_78", "value": 2 }, + { "source": "mail_74", "target": "mail_75", "value": 1 }, + { "source": "mail_74", "target": "mail_79", "value": 2 }, + { "source": "mail_75", "target": "mail_76", "value": 1 }, + { "source": "mail_75", "target": "mail_80", "value": 2 }, + { "source": "mail_76", "target": "mail_77", "value": 1 }, + { "source": "mail_77", "target": "mail_78", "value": 1 }, + { "source": "mail_78", "target": "mail_79", "value": 1 }, + { "source": "mail_79", "target": "mail_80", "value": 1 }, + + { "source": "mail_72", "target": "mail_61", "value": 2 }, + { "source": "mail_72", "target": "mail_62", "value": 2 }, + { "source": "mail_72", "target": "mail_63", "value": 2 }, + { "source": "mail_72", "target": "mail_64", "value": 2 }, + { "source": "mail_72", "target": "mail_65", "value": 2 }, + { "source": "mail_72", "target": "mail_66", "value": 2 }, + { "source": "mail_72", "target": "mail_67", "value": 2 }, + { "source": "mail_72", "target": "mail_68", "value": 2 }, + { "source": "mail_72", "target": "mail_69", "value": 2 }, + { "source": "mail_72", "target": "mail_70", "value": 2 }, + { "source": "mail_72", "target": "mail_71", "value": 2 }, + { "source": "mail_72", "target": "mail_73", "value": 2 }, + { "source": "mail_72", "target": "mail_74", "value": 2 }, + { "source": "mail_72", "target": "mail_75", "value": 2 }, + { "source": "mail_72", "target": "mail_76", "value": 2 }, + { "source": "mail_72", "target": "mail_77", "value": 2 }, + { "source": "mail_72", "target": "mail_78", "value": 2 }, + { "source": "mail_72", "target": "mail_79", "value": 2 }, + { "source": "mail_72", "target": "mail_80", "value": 2 }, + { "source": "mail_72", "target": "mail_81", "value": 2 }, + { "source": "mail_72", "target": "mail_82", "value": 2 }, + + { "source": "mail_80", "target": "mail_81", "value": 2 }, + { "source": "mail_81", "target": "mail_82", "value": 2 }, + { "source": "mail_82", "target": "mail_83", "value": 2 }, + { "source": "mail_83", "target": "mail_84", "value": 2 }, + { "source": "mail_84", "target": "mail_85", "value": 2 }, + { "source": "mail_85", "target": "mail_86", "value": 2 }, + { "source": "mail_86", "target": "mail_87", "value": 2 }, + { "source": "mail_87", "target": "mail_88", "value": 2 }, + { "source": "mail_88", "target": "mail_89", "value": 2 }, + { "source": "mail_89", "target": "mail_90", "value": 2 }, + { "source": "mail_90", "target": "mail_91", "value": 3 }, + { "source": "mail_91", "target": "mail_92", "value": 3 }, + { "source": "mail_92", "target": "mail_93", "value": 3 }, + { "source": "mail_93", "target": "mail_94", "value": 3 }, + { "source": "mail_94", "target": "mail_95", "value": 3 }, + { "source": "mail_95", "target": "mail_96", "value": 3 }, + { "source": "mail_96", "target": "mail_97", "value": 3 }, + { "source": "mail_97", "target": "mail_98", "value": 3 }, + { "source": "mail_98", "target": "mail_99", "value": 3 }, + { "source": "mail_99", "target": "mail_100", "value": 3 }, + + { "source": "mail_82", "target": "mail_84", "value": 2 }, + { "source": "mail_84", "target": "mail_86", "value": 2 }, + { "source": "mail_82", "target": "mail_86", "value": 1 }, + { "source": "mail_85", "target": "mail_87", "value": 2 }, + { "source": "mail_87", "target": "mail_89", "value": 2 }, + { "source": "mail_85", "target": "mail_89", "value": 1 }, + { "source": "mail_90", "target": "mail_92", "value": 2 }, + { "source": "mail_92", "target": "mail_94", "value": 2 }, + { "source": "mail_90", "target": "mail_94", "value": 1 }, + { "source": "mail_93", "target": "mail_95", "value": 2 }, + { "source": "mail_95", "target": "mail_97", "value": 2 }, + { "source": "mail_93", "target": "mail_97", "value": 1 }, + { "source": "mail_96", "target": "mail_98", "value": 2 }, + { "source": "mail_98", "target": "mail_100", "value": 2 }, + { "source": "mail_96", "target": "mail_100", "value": 1 }, + + { "source": "mail_88", "target": "mail_83", "value": 3 }, + { "source": "mail_88", "target": "mail_84", "value": 3 }, + { "source": "mail_88", "target": "mail_85", "value": 3 }, + { "source": "mail_88", "target": "mail_86", "value": 3 }, + { "source": "mail_88", "target": "mail_87", "value": 3 }, + { "source": "mail_88", "target": "mail_89", "value": 3 }, + { "source": "mail_88", "target": "mail_90", "value": 3 }, + { "source": "mail_88", "target": "mail_91", "value": 3 }, + { "source": "mail_88", "target": "mail_92", "value": 3 }, + { "source": "mail_88", "target": "mail_93", "value": 3 }, + { "source": "mail_88", "target": "mail_94", "value": 3 }, + { "source": "mail_88", "target": "mail_95", "value": 3 }, + { "source": "mail_88", "target": "mail_96", "value": 3 }, + { "source": "mail_88", "target": "mail_97", "value": 3 }, + { "source": "mail_88", "target": "mail_98", "value": 3 }, + { "source": "mail_88", "target": "mail_99", "value": 3 }, + { "source": "mail_88", "target": "mail_100", "value": 3 } + ] +} diff --git a/client/src/app/provider.tsx b/client/src/app/provider.tsx index d070138..6ce176c 100644 --- a/client/src/app/provider.tsx +++ b/client/src/app/provider.tsx @@ -1,6 +1,6 @@ import { Suspense, type ReactNode } from 'react'; import { ThemeProvider } from "@mui/material"; -import { theme } from "@/styles/theme.ts"; +import { theme } from "@/styles/mui/theme.ts"; type AppProviderProps = { children: ReactNode; diff --git a/client/src/app/routes/landing.tsx b/client/src/app/routes/landing.tsx index b25d39e..3ff20a4 100644 --- a/client/src/app/routes/landing.tsx +++ b/client/src/app/routes/landing.tsx @@ -1,7 +1,4 @@ -import { NetworkGraphProvider } from "@/features/NetworkGraph/stores/network-graph-context.tsx"; -import { NetworkGraphCanvas } from "@/features/NetworkGraph"; -import { Navbar } from "@/components/ui/navbar/navbar.tsx"; -import Box from "@mui/material/Box"; +import { NetworkGraph } from "@/features/NetworkGraph"; /** * Landing page component. @@ -10,28 +7,7 @@ import Box from "@mui/material/Box"; */ const Landing = () => { return ( - - - - - - - - - + ) } diff --git a/client/src/components/ui/floating-overlay/floating-overlay.tsx b/client/src/components/ui/floating-overlay/floating-overlay.tsx index ba75724..d3307ee 100644 --- a/client/src/components/ui/floating-overlay/floating-overlay.tsx +++ b/client/src/components/ui/floating-overlay/floating-overlay.tsx @@ -1,9 +1,8 @@ -import Paper from "@mui/material/Paper"; import { CSS } from "@dnd-kit/utilities"; import { useDraggable, useDndMonitor } from '@dnd-kit/core'; -import DialogTitle from "@mui/material/DialogTitle"; -import DialogContent from "@mui/material/DialogContent"; import { useState, type ReactNode, type FC, type CSSProperties } from "react"; +import Box from "@mui/material/Box"; +import { Paper, Typography } from "@mui/material"; type FloatingPanelProps = { /** @@ -58,9 +57,13 @@ export const FloatingPanel: FC = ({ title, children, id }) = }; return ( - = ({ title, children, id }) = }} > {title && ( - - {title} - + + + {title} + + )} - {children} - + + {children} + + ); }; \ No newline at end of file diff --git a/client/src/components/ui/navbar/navbar.tsx b/client/src/components/ui/navbar/navbar.tsx index 8b7d8a9..a2c7ba8 100644 --- a/client/src/components/ui/navbar/navbar.tsx +++ b/client/src/components/ui/navbar/navbar.tsx @@ -4,10 +4,9 @@ import Toolbar from '@mui/material/Toolbar'; import IconButton from '@mui/material/IconButton'; import logo from '@/assets/kaiaulu_logo.png'; import { Typography } from "@mui/material"; -import { NetworkGraphSearchBar } from "@/features/NetworkGraph/components/network-graph-search-bar.tsx"; - -export const Navbar = () => { +import type { ReactNode } from "react"; +export const Navbar = ({ children }: { children: ReactNode }) => { return ( @@ -38,7 +37,7 @@ export const Navbar = () => { Projects - + { children } ) diff --git a/client/src/features/NetworkGraph/components/network-graph-canvas.tsx b/client/src/features/NetworkGraph/components/network-graph-canvas.tsx deleted file mode 100644 index 4b1fd18..0000000 --- a/client/src/features/NetworkGraph/components/network-graph-canvas.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { DndContext } from '@dnd-kit/core'; -import { useNetworkGraph } from "../stores/network-graph-context.tsx"; -import { useInitialPhysicsSimulation } from "@/features/NetworkGraph/hooks/useInitialPhysicsSimulation.ts"; -import { useNetworkGraphInteractions } from "@/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts"; -import { FloatingPanel } from "@/components/ui/floating-overlay/floating-overlay.tsx"; -import Box from "@mui/material/Box"; -import { useCallback, useEffect } from "react"; -import { drawGraph } from "@/features/NetworkGraph/lib/draw-network-graph.ts"; - -export const NetworkGraphCanvas = () => { - - const { - nodes, - links, - canvasRef, - overlayOn, - setOverlayOn, - nodeRelationshipMapRef, - transparentNodeMapRef, - } = useNetworkGraph(); - - const drawToCanvas = useCallback(() => { - if (!canvasRef.current) return; - const canvas = canvasRef.current; - - const container = canvas.parentElement; - if (!container) return; - - const rect = container.getBoundingClientRect(); - const nextW = Math.max(1, Math.floor(rect.width)); - const nextH = Math.max(1, Math.floor(rect.height)); - - if (canvas.width !== nextW) canvas.width = nextW; - if (canvas.height !== nextH) canvas.height = nextH; - - drawGraph(canvas, nodes, links, transparentNodeMapRef.current, - 11, - 'gray', - 'gray'); - - }, [canvasRef, links, nodes, transparentNodeMapRef]); - - useInitialPhysicsSimulation({ nodes, links, canvasRef, drawToCanvas: drawToCanvas }); - - useNetworkGraphInteractions({ - nodes, links, canvasRef, overlayOn, setOverlayOn, - nodeRelationshipMap: nodeRelationshipMapRef.current, - transparentNodeMap: transparentNodeMapRef.current, - drawToCanvas: drawToCanvas }); - - useEffect(() => { - drawToCanvas(); - }, [drawToCanvas, overlayOn]); - - return ( - - - - - - {overlayOn ? ( - -
- Total nodes: -
-
- Show More -
-
- ) : null} -
-
-
- ) -} - diff --git a/client/src/features/NetworkGraph/components/network-graph-canvas/network-graph-canvas.tsx b/client/src/features/NetworkGraph/components/network-graph-canvas/network-graph-canvas.tsx new file mode 100644 index 0000000..40ab930 --- /dev/null +++ b/client/src/features/NetworkGraph/components/network-graph-canvas/network-graph-canvas.tsx @@ -0,0 +1,51 @@ +import { useCallback, useEffect } from "react"; + +import Box from "@mui/material/Box"; + +import { useNetworkGraph } from "../../stores/network-graph-context.tsx"; +import { useInitialPhysicsSimulation } from "@/features/NetworkGraph/hooks/useInitialPhysicsSimulation.ts"; +import { useNetworkGraphInteractions } from "@/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts"; + +import { drawGraph } from "@/features/NetworkGraph/lib/draw-network-graph.ts"; + + +export const NetworkGraphCanvas = () => { + const { nodes, links, canvasRef, overlayOn, transparentNodeMapRef } = useNetworkGraph(); + + const renderNetworkGraph = useCallback(() => { + if (!canvasRef.current) return; + const canvas = canvasRef.current; + + const container = canvas.parentElement; + if (!container) return; + + const rect = container.getBoundingClientRect(); + const nextW = Math.max(1, Math.floor(rect.width)); + const nextH = Math.max(1, Math.floor(rect.height)); + + if (canvas.width !== nextW) canvas.width = nextW; + if (canvas.height !== nextH) canvas.height = nextH; + + drawGraph(canvas, nodes, links, transparentNodeMapRef.current, + 11, + 'gray', + 'gray'); + + }, [canvasRef, links, nodes, transparentNodeMapRef]); + + useInitialPhysicsSimulation(renderNetworkGraph); + useNetworkGraphInteractions(renderNetworkGraph); + + useEffect(() => { + renderNetworkGraph(); + }, [renderNetworkGraph, overlayOn]); + + return ( + + + + + + ) +} + diff --git a/client/src/features/NetworkGraph/components/network-graph-search-bar.tsx b/client/src/features/NetworkGraph/components/network-graph-search-bar/network-graph-search-bar.tsx similarity index 96% rename from client/src/features/NetworkGraph/components/network-graph-search-bar.tsx rename to client/src/features/NetworkGraph/components/network-graph-search-bar/network-graph-search-bar.tsx index ccf8976..ee44c1b 100644 --- a/client/src/features/NetworkGraph/components/network-graph-search-bar.tsx +++ b/client/src/features/NetworkGraph/components/network-graph-search-bar/network-graph-search-bar.tsx @@ -10,7 +10,7 @@ import { const Search = styled("form")(({ theme }) => ({ position: "relative", borderRadius: theme.shape.borderRadius, - backgroundColor: theme.palette.background.default, + backgroundColor: theme.palette.secondary.main, "&:hover": { backgroundColor: alpha(theme.palette.common.white, 0.25), }, diff --git a/client/src/features/NetworkGraph/components/network-graph-sub-graph-overlay/network-graph-sub-graph-overlay.tsx b/client/src/features/NetworkGraph/components/network-graph-sub-graph-overlay/network-graph-sub-graph-overlay.tsx new file mode 100644 index 0000000..980ab8c --- /dev/null +++ b/client/src/features/NetworkGraph/components/network-graph-sub-graph-overlay/network-graph-sub-graph-overlay.tsx @@ -0,0 +1,37 @@ +import { DndContext } from "@dnd-kit/core"; +import { FloatingPanel } from "@/components/ui/floating-overlay/floating-overlay.tsx"; + +import { useNetworkGraph } from "@/features/NetworkGraph/stores/network-graph-context.tsx"; +import {Button, Container, Typography} from "@mui/material"; + +/** + * Overlay that appears upon entering "subgraph highlight mode" that displays information about the subgraph + * + * Note: + * - Uses custom FloatingPanel component + */ +export const NetworkGraphSubGraphOverlay = () => { + const { overlayOn } = useNetworkGraph(); + + if (!overlayOn) return null; + + return ( + // Any draggable components created with dnd-kit must be rendered within a DndContext + + + + + Total nodes: + + + + + + + + ); +}; \ No newline at end of file diff --git a/client/src/features/NetworkGraph/hooks/useInitialPhysicsSimulation.ts b/client/src/features/NetworkGraph/hooks/useInitialPhysicsSimulation.ts index 1e51ba4..8c1791f 100644 --- a/client/src/features/NetworkGraph/hooks/useInitialPhysicsSimulation.ts +++ b/client/src/features/NetworkGraph/hooks/useInitialPhysicsSimulation.ts @@ -1,14 +1,8 @@ -import { useEffect, type RefObject } from "react"; +import { useEffect } from "react"; import { forceSimulation, forceLink, forceX, forceY, forceCollide, forceManyBody } from "d3"; import type { Simulation } from "d3"; -import type { Node, Link, HubLink, Group } from "@/types/network-graph.types.ts" - -interface UseNetworkGraphSimulationArgs { - nodes: Node[]; - links: Link[]; - canvasRef: RefObject; - drawToCanvas: () => void -} +import type { Node, Link, HubLink, Group } from "@/features/NetworkGraph/types/network-graph.types.ts" +import {useNetworkGraph} from "@/features/NetworkGraph/stores/network-graph-context.tsx"; type NodeGroupCenters = { people: [number, number]; @@ -24,7 +18,14 @@ type NodeGroupCenters = { * - D3.js force simulation is used to calculate the positions of all nodes and links * - Nodes and link objects are mutated by D3.js force simulation to contain position values x and y */ -export function useInitialPhysicsSimulation({ nodes, links, canvasRef, drawToCanvas }: UseNetworkGraphSimulationArgs) { +export function useInitialPhysicsSimulation(renderNetworkGraph: () => void) { + + const { + nodes, + links, + canvasRef + } = useNetworkGraph(); + useEffect(() => { if (!canvasRef.current) return; if (!nodes.length || !links.length) return; @@ -53,8 +54,8 @@ export function useInitialPhysicsSimulation({ nodes, links, canvasRef, drawToCan clearForces(sim as Simulation); sim.stop(); - drawToCanvas(); - }, [nodes, links, canvasRef, drawToCanvas]); + renderNetworkGraph(); + }, [nodes, links, canvasRef, renderNetworkGraph]); } function calculateNodeGroupCenters(canvasWidth: number, canvasHeight: number): NodeGroupCenters { diff --git a/client/src/features/NetworkGraph/hooks/useNetworkGraphData.ts b/client/src/features/NetworkGraph/hooks/useNetworkGraphData.ts index 5518aca..df38fff 100644 --- a/client/src/features/NetworkGraph/hooks/useNetworkGraphData.ts +++ b/client/src/features/NetworkGraph/hooks/useNetworkGraphData.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { BASE_PATH } from "@/features/NetworkGraph/config/paths.ts"; -import type { NetworkGraphData } from "@/types/network-graph.types.ts"; +import type { NetworkGraphData } from "@/features/NetworkGraph/types/network-graph.types.ts"; const GRAPH_FILES = ['file-graph-data.json', 'issue-graph-data.json', 'mail-graph-data.json', 'person-graph-data.json']; diff --git a/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts b/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts index c47ad85..81a4fb2 100644 --- a/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts +++ b/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts @@ -1,18 +1,12 @@ -import { useEffect, type RefObject, type SetStateAction, type Dispatch } from "react"; -import { highlightSubgraph } from "../lib/draw-network-graph.ts"; -import { type Link, type Node , type NodeRelationshipMap, type TransparentNodeMap} from "@/types/network-graph.types.ts" +import { useEffect } from "react"; +import { type Node } from "@/features/NetworkGraph/types/network-graph.types.ts" import { drag, type DragBehavior, pointer, select } from "d3"; - -interface UseNetworkGraphInteractionsArgs { - nodes: Node[]; - links: Link[]; - canvasRef: RefObject; - overlayOn: boolean; - setOverlayOn: Dispatch>; - nodeRelationshipMap: NodeRelationshipMap; - transparentNodeMap: TransparentNodeMap; - drawToCanvas: () => void; -} +import { + setAllNodesToBeOpaque, + setAllNodesToBeTransparent, + setNodeNeighborhoodToBeOpaque +} from "@/features/NetworkGraph/utils/transparent-node-map.ts"; +import {useNetworkGraph} from "@/features/NetworkGraph/stores/network-graph-context.tsx"; /** * Allows a network graph that is rendered to a canvas to become interactable to the user. @@ -22,10 +16,17 @@ interface UseNetworkGraphInteractionsArgs { * - Drag detection * - Redraws network graph whenever canvas size changes (user resizes window, user opens browser console, etc.) */ -export function useNetworkGraphInteractions( - { nodes, links, canvasRef, overlayOn, setOverlayOn, nodeRelationshipMap, transparentNodeMap, drawToCanvas } - : UseNetworkGraphInteractionsArgs -) { +export function useNetworkGraphInteractions(renderNetworkGraph: () => void) { + + const { + nodes, + links, + canvasRef, + overlayOn, + setOverlayOn, + nodeRelationshipMapRef, + transparentNodeMapRef, + } = useNetworkGraph(); useEffect(() => { if (!canvasRef.current) return; @@ -37,7 +38,7 @@ export function useNetworkGraphInteractions( const nodeRadiusMultiplier = 11; // Creates drag behavior - const dragBehavior = createDragBehavior(canvas, nodes, nodeRadiusMultiplier, drawToCanvas); + const dragBehavior = createDragBehavior(canvas, nodes, nodeRadiusMultiplier, renderNetworkGraph); // Attaches drag behavior to the canvas select(canvas).call(dragBehavior as DragBehavior); @@ -55,8 +56,14 @@ export function useNetworkGraphInteractions( const hit = findNodeAt(nodes, nodeRadiusMultiplier, x, y,); if (hit) { - highlightSubgraph(hit.id, transparentNodeMap, nodeRelationshipMap, setOverlayOn); - drawToCanvas(); + if (!overlayOn) { + setAllNodesToBeTransparent(transparentNodeMapRef.current); + setNodeNeighborhoodToBeOpaque(hit.id, transparentNodeMapRef.current, nodeRelationshipMapRef.current); + setOverlayOn(true); + } else { + setAllNodesToBeOpaque(transparentNodeMapRef.current) + setOverlayOn(false); + } } }; @@ -67,7 +74,7 @@ export function useNetworkGraphInteractions( return () => { select(canvas).on(".drag", null).on("dblclick", null); }; - }, [nodes, links, canvasRef, setOverlayOn, transparentNodeMap, nodeRelationshipMap, drawToCanvas, overlayOn]); + }, [nodes, links, canvasRef, setOverlayOn, transparentNodeMapRef, nodeRelationshipMapRef, overlayOn, renderNetworkGraph ]); } function createDragBehavior( diff --git a/client/src/features/NetworkGraph/index.ts b/client/src/features/NetworkGraph/index.ts deleted file mode 100644 index 359b235..0000000 --- a/client/src/features/NetworkGraph/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { NetworkGraphCanvas } from './components/network-graph-canvas.tsx'; \ No newline at end of file diff --git a/client/src/features/NetworkGraph/index.tsx b/client/src/features/NetworkGraph/index.tsx new file mode 100644 index 0000000..bff5c8c --- /dev/null +++ b/client/src/features/NetworkGraph/index.tsx @@ -0,0 +1,31 @@ +import Box from "@mui/material/Box"; +import { Navbar } from "@/components/ui/navbar/navbar.tsx"; + +import { NetworkGraphProvider } from "@/features/NetworkGraph/stores/network-graph-context.tsx"; +import { NetworkGraphSearchBar } from "@/features/NetworkGraph/components/network-graph-search-bar/network-graph-search-bar.tsx"; +import { NetworkGraphCanvas } from "@/features/NetworkGraph/components/network-graph-canvas/network-graph-canvas.tsx"; +import { NetworkGraphSubGraphOverlay } from "@/features/NetworkGraph/components/network-graph-sub-graph-overlay/network-graph-sub-graph-overlay.tsx"; + +/** + * Layout of the Network Graph + * + * Usage: + * - This is to be rendered in a route (a page of the application) + */ +export const NetworkGraph = () => { + return ( + + + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/client/src/features/NetworkGraph/lib/draw-network-graph.ts b/client/src/features/NetworkGraph/lib/draw-network-graph.ts index 434005c..337ab7a 100644 --- a/client/src/features/NetworkGraph/lib/draw-network-graph.ts +++ b/client/src/features/NetworkGraph/lib/draw-network-graph.ts @@ -1,4 +1,4 @@ -import type { Link, Node } from "@/types/network-graph.types.ts"; +import type { Link, Node } from "@/features/NetworkGraph/types/network-graph.types.ts"; import { isNullOrUndefined } from "@/utils/type-guards.ts"; import type {Dispatch, SetStateAction} from "react"; @@ -60,7 +60,6 @@ function drawNodeByGroup( nodeRadiusMultiplier: number, nodeOutlineColor: string) { - // Sets color of node according to group switch (node.group) { case "people": context.fillStyle = 'black'; diff --git a/client/src/features/NetworkGraph/stores/network-graph-context.tsx b/client/src/features/NetworkGraph/stores/network-graph-context.tsx index 01ead70..8241a68 100644 --- a/client/src/features/NetworkGraph/stores/network-graph-context.tsx +++ b/client/src/features/NetworkGraph/stores/network-graph-context.tsx @@ -1,14 +1,8 @@ -import { - type ReactNode, - createContext, - useContext, - useState, - type Dispatch, - type SetStateAction, - useRef, +import { type ReactNode, createContext, useContext, useState, type Dispatch, type SetStateAction, useRef, type RefObject, useEffect } from 'react'; -import type { Node, Link } from "@/types/network-graph.types.ts" + +import type { Node, Link } from "@/features/NetworkGraph/types/network-graph.types.ts" import { useNetworkGraphData } from "@/features/NetworkGraph/hooks/useNetworkGraphData.ts"; import { buildInitialTransparencyMap, buildRelationshipMap } from "@/features/NetworkGraph/lib/draw-network-graph.ts"; diff --git a/client/src/types/network-graph.types.ts b/client/src/features/NetworkGraph/types/network-graph.types.ts similarity index 82% rename from client/src/types/network-graph.types.ts rename to client/src/features/NetworkGraph/types/network-graph.types.ts index 9c497ec..45f118c 100644 --- a/client/src/types/network-graph.types.ts +++ b/client/src/features/NetworkGraph/types/network-graph.types.ts @@ -24,8 +24,4 @@ export type Group = 'people' | 'mail' | 'file' | 'issue'; export type HubLink = { source: Node; target: Node -}; - -export type NodeRelationshipMap = Map>; - -export type TransparentNodeMap = Map; \ No newline at end of file +}; \ No newline at end of file diff --git a/client/src/features/NetworkGraph/utils/transparent-node-map.ts b/client/src/features/NetworkGraph/utils/transparent-node-map.ts index 650383e..2c5507e 100644 --- a/client/src/features/NetworkGraph/utils/transparent-node-map.ts +++ b/client/src/features/NetworkGraph/utils/transparent-node-map.ts @@ -23,4 +23,13 @@ export function setAllNodesToBeTransparent(transparentNodeMap: Map) { + for (const key of transparentNodeMap.keys()) { + transparentNodeMap.set(key, 0); + } } \ No newline at end of file diff --git a/client/src/styles/theme.ts b/client/src/styles/mui/theme.ts similarity index 90% rename from client/src/styles/theme.ts rename to client/src/styles/mui/theme.ts index 7803c34..ac5480e 100644 --- a/client/src/styles/theme.ts +++ b/client/src/styles/mui/theme.ts @@ -1,4 +1,5 @@ import { createTheme } from '@mui/material/styles'; +import '@fontsource/roboto/300.css'; export const theme = createTheme({ palette: { @@ -11,7 +12,6 @@ export const theme = createTheme({ }, background: { default: '#444444', - paper: '#111827', }, }, components: { From e304fe99ae155edfce2b1387aeca4728ee7c392c Mon Sep 17 00:00:00 2001 From: austindang67 <122927862+austindang67@users.noreply.github.com> Date: Sun, 14 Dec 2025 19:49:37 -1000 Subject: [PATCH 29/29] Documented everything, changed navbar. Added documentation to all major parts of the Network Graph. 'Projects' has been replaced with 'Kaiaulu' in NavBar. --- .../DummyProject/file-graph-data.json | 374 ++++++++++++++++-- .../DummyProject/mail-graph-data.json | 301 +++++++++++++- client/src/components/layouts/app-layout.css | 9 - client/src/components/ui/header/index.ts | 1 - client/src/components/ui/navbar/navbar.tsx | 2 +- client/src/config/app.ts | 5 + .../network-graph-canvas.tsx | 21 + .../network-graph-search-bar.tsx | 8 +- .../network-graph-sub-graph-overlay.tsx | 2 +- .../src/features/NetworkGraph/config/paths.ts | 4 + .../hooks/useInitialPhysicsSimulation.ts | 55 ++- .../hooks/useNetworkGraphInteractions.ts | 22 +- .../stores/network-graph-context.tsx | 17 + .../NetworkGraph/types/network-graph.types.ts | 20 +- .../utils/transparent-node-map.ts | 7 + client/src/styles/mui/theme.ts | 3 + 16 files changed, 775 insertions(+), 76 deletions(-) delete mode 100644 client/src/components/layouts/app-layout.css delete mode 100644 client/src/components/ui/header/index.ts diff --git a/client/public/Projects/DummyProject/file-graph-data.json b/client/public/Projects/DummyProject/file-graph-data.json index fb3e743..71a4c6a 100644 --- a/client/public/Projects/DummyProject/file-graph-data.json +++ b/client/public/Projects/DummyProject/file-graph-data.json @@ -1,46 +1,340 @@ { "nodes": [ - { "id": "file_1", "group": "file", "value": 2 }, - { "id": "file_2", "group": "file", "value": 2 }, - { "id": "file_3", "group": "file", "value": 2 }, - { "id": "file_4", "group": "file", "value": 2 }, - { "id": "file_5", "group": "file", "value": 2 }, - { "id": "file_6", "group": "file", "value": 2 }, - { "id": "file_7", "group": "file", "value": 2 }, - { "id": "file_8", "group": "file", "value": 2 }, - { "id": "file_9", "group": "file", "value": 2 }, - { "id": "file_10", "group": "file", "value": 3 }, - { "id": "file_11", "group": "file", "value": 4 }, - { "id": "file_12", "group": "file", "value": 4 }, - { "id": "file_13", "group": "file", "value": 2 }, - { "id": "file_14", "group": "file", "value": 2 }, - { "id": "file_15", "group": "file", "value": 2 }, - { "id": "file_16", "group": "file", "value": 2 }, - { "id": "file_17", "group": "file", "value": 2 }, - { "id": "file_18", "group": "file", "value": 2 }, - { "id": "file_19", "group": "file", "value": 2 }, - { "id": "file_20", "group": "file", "value": 2 }, - { "id": "file_21", "group": "file", "value": 2 }, - { "id": "file_22", "group": "file", "value": 3 }, - { "id": "file_23", "group": "file", "value": 4 }, - { "id": "file_24", "group": "file", "value": 8 } + {"id": "file_1", "group": "file", "value": 2}, + {"id": "file_2", "group": "file", "value": 2}, + {"id": "file_3", "group": "file", "value": 2}, + {"id": "file_4", "group": "file", "value": 2}, + {"id": "file_5", "group": "file", "value": 2}, + {"id": "file_6", "group": "file", "value": 2}, + {"id": "file_7", "group": "file", "value": 2}, + {"id": "file_8", "group": "file", "value": 2}, + {"id": "file_9", "group": "file", "value": 2}, + {"id": "file_10", "group": "file", "value": 3}, + {"id": "file_11", "group": "file", "value": 4}, + {"id": "file_12", "group": "file", "value": 4}, + {"id": "file_13", "group": "file", "value": 2}, + {"id": "file_14", "group": "file", "value": 2}, + {"id": "file_15", "group": "file", "value": 2}, + {"id": "file_16", "group": "file", "value": 2}, + {"id": "file_17", "group": "file", "value": 2}, + {"id": "file_18", "group": "file", "value": 2}, + {"id": "file_19", "group": "file", "value": 2}, + {"id": "file_20", "group": "file", "value": 2}, + {"id": "file_21", "group": "file", "value": 2}, + {"id": "file_22", "group": "file", "value": 3}, + {"id": "file_23", "group": "file", "value": 4}, + {"id": "file_24", "group": "file", "value": 8}, + {"id": "file_25", "group": "file", "value": 2}, + {"id": "file_26", "group": "file", "value": 2}, + {"id": "file_27", "group": "file", "value": 2}, + {"id": "file_28", "group": "file", "value": 3}, + {"id": "file_29", "group": "file", "value": 2}, + {"id": "file_30", "group": "file", "value": 2}, + {"id": "file_31", "group": "file", "value": 2}, + {"id": "file_32", "group": "file", "value": 2}, + {"id": "file_33", "group": "file", "value": 2}, + {"id": "file_34", "group": "file", "value": 3}, + {"id": "file_35", "group": "file", "value": 5}, + {"id": "file_36", "group": "file", "value": 2}, + {"id": "file_37", "group": "file", "value": 2}, + {"id": "file_38", "group": "file", "value": 2}, + {"id": "file_39", "group": "file", "value": 2}, + {"id": "file_40", "group": "file", "value": 2}, + {"id": "file_41", "group": "file", "value": 2}, + {"id": "file_42", "group": "file", "value": 4}, + {"id": "file_43", "group": "file", "value": 2}, + {"id": "file_44", "group": "file", "value": 2}, + {"id": "file_45", "group": "file", "value": 2}, + {"id": "file_46", "group": "file", "value": 2}, + {"id": "file_47", "group": "file", "value": 2}, + {"id": "file_48", "group": "file", "value": 2}, + {"id": "file_49", "group": "file", "value": 3}, + {"id": "file_50", "group": "file", "value": 7}, + {"id": "file_51", "group": "file", "value": 2}, + {"id": "file_52", "group": "file", "value": 2}, + {"id": "file_53", "group": "file", "value": 2}, + {"id": "file_54", "group": "file", "value": 2}, + {"id": "file_55", "group": "file", "value": 5}, + {"id": "file_56", "group": "file", "value": 4}, + {"id": "file_57", "group": "file", "value": 2}, + {"id": "file_58", "group": "file", "value": 2}, + {"id": "file_59", "group": "file", "value": 2}, + {"id": "file_60", "group": "file", "value": 2}, + {"id": "file_61", "group": "file", "value": 2}, + {"id": "file_62", "group": "file", "value": 2}, + {"id": "file_63", "group": "file", "value": 3}, + {"id": "file_64", "group": "file", "value": 2}, + {"id": "file_65", "group": "file", "value": 2}, + {"id": "file_66", "group": "file", "value": 2}, + {"id": "file_67", "group": "file", "value": 2}, + {"id": "file_68", "group": "file", "value": 2}, + {"id": "file_69", "group": "file", "value": 2}, + {"id": "file_70", "group": "file", "value": 4}, + {"id": "file_71", "group": "file", "value": 2}, + {"id": "file_72", "group": "file", "value": 6}, + {"id": "file_73", "group": "file", "value": 2}, + {"id": "file_74", "group": "file", "value": 2}, + {"id": "file_75", "group": "file", "value": 2}, + {"id": "file_76", "group": "file", "value": 2}, + {"id": "file_77", "group": "file", "value": 2}, + {"id": "file_78", "group": "file", "value": 5}, + {"id": "file_79", "group": "file", "value": 2}, + {"id": "file_80", "group": "file", "value": 2}, + {"id": "file_81", "group": "file", "value": 2}, + {"id": "file_82", "group": "file", "value": 2}, + {"id": "file_83", "group": "file", "value": 2}, + {"id": "file_84", "group": "file", "value": 4}, + {"id": "file_85", "group": "file", "value": 2}, + {"id": "file_86", "group": "file", "value": 2}, + {"id": "file_87", "group": "file", "value": 2}, + {"id": "file_88", "group": "file", "value": 7}, + {"id": "file_89", "group": "file", "value": 2}, + {"id": "file_90", "group": "file", "value": 2}, + {"id": "file_91", "group": "file", "value": 3}, + {"id": "file_92", "group": "file", "value": 2}, + {"id": "file_93", "group": "file", "value": 2}, + {"id": "file_94", "group": "file", "value": 2}, + {"id": "file_95", "group": "file", "value": 2}, + {"id": "file_96", "group": "file", "value": 2}, + {"id": "file_97", "group": "file", "value": 2}, + {"id": "file_98", "group": "file", "value": 4}, + {"id": "file_99", "group": "file", "value": 2}, + {"id": "file_100", "group": "file", "value": 8} ], "links": [ - { "source": "file_4", "target": "file_2", "value": 1 }, - { "source": "file_4", "target": "file_3", "value": 1 }, - { "source": "file_4", "target": "file_1", "value": 1 }, - { "source": "file_4", "target": "file_6", "value": 1 }, - { "source": "file_4", "target": "issue_2", "value": 1 }, - { "source": "file_6", "target": "file_1", "value": 1 }, - { "source": "file_6", "target": "file_3", "value": 1 }, - { "source": "file_6", "target": "file_5", "value": 1 }, - { "source": "file_7", "target": "file_8", "value": 2 }, - { "source": "file_8", "target": "file_9", "value": 2 }, - { "source": "file_9", "target": "file_10", "value": 2 }, - { "source": "file_9", "target": "file_22", "value": 2 }, - { "source": "file_10", "target": "file_11", "value": 3 }, - { "source": "file_11", "target": "file_12", "value": 4 }, - { "source": "file_23", "target": "issue_1", "value": 4 }, - { "source": "file_23", "target": "file_24", "value": 4 } + {"source": "file_4", "target": "file_2", "value": 1}, + {"source": "file_4", "target": "file_3", "value": 1}, + {"source": "file_4", "target": "file_1", "value": 1}, + {"source": "file_4", "target": "file_6", "value": 1}, + {"source": "file_4", "target": "issue_2", "value": 1}, + {"source": "file_6", "target": "file_1", "value": 1}, + {"source": "file_6", "target": "file_3", "value": 1}, + {"source": "file_6", "target": "file_5", "value": 1}, + {"source": "file_7", "target": "file_8", "value": 2}, + {"source": "file_8", "target": "file_9", "value": 2}, + {"source": "file_9", "target": "file_10", "value": 2}, + {"source": "file_9", "target": "file_22", "value": 2}, + {"source": "file_10", "target": "file_11", "value": 3}, + {"source": "file_11", "target": "file_12", "value": 4}, + {"source": "file_23", "target": "issue_1", "value": 4}, + {"source": "file_23", "target": "file_24", "value": 4}, + {"source": "file_12", "target": "file_13", "value": 1}, + {"source": "file_13", "target": "file_14", "value": 1}, + {"source": "file_14", "target": "file_15", "value": 1}, + {"source": "file_15", "target": "file_16", "value": 1}, + {"source": "file_16", "target": "file_17", "value": 1}, + {"source": "file_17", "target": "file_18", "value": 1}, + {"source": "file_18", "target": "file_19", "value": 1}, + {"source": "file_19", "target": "file_20", "value": 1}, + {"source": "file_20", "target": "file_21", "value": 2}, + {"source": "file_21", "target": "file_22", "value": 2}, + {"source": "file_22", "target": "file_23", "value": 2}, + {"source": "file_23", "target": "file_24", "value": 2}, + {"source": "file_24", "target": "file_25", "value": 2}, + {"source": "file_25", "target": "file_26", "value": 2}, + {"source": "file_26", "target": "file_27", "value": 2}, + {"source": "file_27", "target": "file_28", "value": 2}, + {"source": "file_28", "target": "file_29", "value": 2}, + {"source": "file_29", "target": "file_30", "value": 2}, + {"source": "file_30", "target": "file_31", "value": 2}, + {"source": "file_31", "target": "file_32", "value": 2}, + {"source": "file_32", "target": "file_33", "value": 2}, + {"source": "file_33", "target": "file_34", "value": 2}, + {"source": "file_34", "target": "file_35", "value": 2}, + {"source": "file_35", "target": "file_36", "value": 2}, + {"source": "file_36", "target": "file_37", "value": 2}, + {"source": "file_37", "target": "file_38", "value": 2}, + {"source": "file_38", "target": "file_39", "value": 2}, + {"source": "file_39", "target": "file_40", "value": 2}, + {"source": "file_24", "target": "file_13", "value": 2}, + {"source": "file_24", "target": "file_14", "value": 2}, + {"source": "file_24", "target": "file_15", "value": 2}, + {"source": "file_24", "target": "file_16", "value": 2}, + {"source": "file_24", "target": "file_17", "value": 2}, + {"source": "file_24", "target": "file_18", "value": 2}, + {"source": "file_24", "target": "file_19", "value": 2}, + {"source": "file_24", "target": "file_20", "value": 2}, + {"source": "file_24", "target": "file_21", "value": 2}, + {"source": "file_24", "target": "file_22", "value": 2}, + {"source": "file_24", "target": "file_25", "value": 3}, + {"source": "file_24", "target": "file_26", "value": 3}, + {"source": "file_24", "target": "file_27", "value": 3}, + {"source": "file_24", "target": "file_28", "value": 3}, + {"source": "file_24", "target": "file_29", "value": 3}, + {"source": "file_24", "target": "file_30", "value": 3}, + {"source": "file_24", "target": "file_31", "value": 3}, + {"source": "file_24", "target": "file_32", "value": 3}, + {"source": "file_50", "target": "file_33", "value": 2}, + {"source": "file_50", "target": "file_34", "value": 2}, + {"source": "file_50", "target": "file_35", "value": 2}, + {"source": "file_50", "target": "file_36", "value": 2}, + {"source": "file_50", "target": "file_37", "value": 2}, + {"source": "file_50", "target": "file_38", "value": 2}, + {"source": "file_50", "target": "file_39", "value": 2}, + {"source": "file_50", "target": "file_40", "value": 2}, + {"source": "file_50", "target": "file_41", "value": 2}, + {"source": "file_50", "target": "file_42", "value": 2}, + {"source": "file_50", "target": "file_43", "value": 2}, + {"source": "file_50", "target": "file_44", "value": 2}, + {"source": "file_50", "target": "file_45", "value": 3}, + {"source": "file_50", "target": "file_46", "value": 3}, + {"source": "file_50", "target": "file_47", "value": 3}, + {"source": "file_50", "target": "file_48", "value": 3}, + {"source": "file_50", "target": "file_49", "value": 3}, + {"source": "file_50", "target": "file_51", "value": 3}, + {"source": "file_50", "target": "file_52", "value": 3}, + {"source": "file_50", "target": "file_53", "value": 3}, + {"source": "file_50", "target": "file_54", "value": 3}, + {"source": "file_50", "target": "file_55", "value": 3}, + {"source": "file_40", "target": "file_42", "value": 2}, + {"source": "file_41", "target": "file_43", "value": 1}, + {"source": "file_42", "target": "file_44", "value": 2}, + {"source": "file_43", "target": "file_45", "value": 1}, + {"source": "file_44", "target": "file_46", "value": 2}, + {"source": "file_45", "target": "file_47", "value": 1}, + {"source": "file_46", "target": "file_48", "value": 2}, + {"source": "file_47", "target": "file_49", "value": 1}, + {"source": "file_48", "target": "file_50", "value": 2}, + {"source": "file_49", "target": "file_51", "value": 1}, + {"source": "file_50", "target": "file_52", "value": 2}, + {"source": "file_51", "target": "file_53", "value": 1}, + {"source": "file_52", "target": "file_54", "value": 2}, + {"source": "file_53", "target": "file_55", "value": 1}, + {"source": "file_54", "target": "file_56", "value": 2}, + {"source": "file_55", "target": "file_57", "value": 1}, + {"source": "file_56", "target": "file_58", "value": 2}, + {"source": "file_57", "target": "file_59", "value": 1}, + {"source": "file_58", "target": "file_60", "value": 2}, + {"source": "file_59", "target": "file_61", "value": 1}, + {"source": "file_24", "target": "issue_3", "value": 3}, + {"source": "file_37", "target": "issue_3", "value": 3}, + {"source": "file_50", "target": "issue_3", "value": 3}, + {"source": "file_58", "target": "issue_3", "value": 3}, + {"source": "file_63", "target": "issue_3", "value": 3}, + {"source": "file_72", "target": "issue_3", "value": 3}, + {"source": "file_88", "target": "issue_3", "value": 3}, + {"source": "file_100", "target": "issue_3", "value": 3}, + {"source": "file_4", "target": "issue_2", "value": 2}, + {"source": "file_6", "target": "issue_2", "value": 2}, + {"source": "file_18", "target": "issue_2", "value": 2}, + {"source": "file_33", "target": "issue_2", "value": 2}, + {"source": "file_41", "target": "issue_2", "value": 2}, + {"source": "file_57", "target": "issue_2", "value": 2}, + {"source": "file_75", "target": "issue_2", "value": 2}, + {"source": "file_23", "target": "issue_1", "value": 3}, + {"source": "file_24", "target": "issue_1", "value": 3}, + {"source": "file_45", "target": "issue_1", "value": 3}, + {"source": "file_50", "target": "issue_1", "value": 3}, + {"source": "file_67", "target": "issue_1", "value": 3}, + {"source": "file_88", "target": "issue_1", "value": 3}, + {"source": "file_60", "target": "file_61", "value": 1}, + {"source": "file_60", "target": "file_65", "value": 2}, + {"source": "file_61", "target": "file_62", "value": 1}, + {"source": "file_61", "target": "file_66", "value": 2}, + {"source": "file_62", "target": "file_63", "value": 1}, + {"source": "file_62", "target": "file_67", "value": 2}, + {"source": "file_63", "target": "file_64", "value": 1}, + {"source": "file_63", "target": "file_68", "value": 2}, + {"source": "file_64", "target": "file_65", "value": 1}, + {"source": "file_64", "target": "file_69", "value": 2}, + {"source": "file_65", "target": "file_66", "value": 1}, + {"source": "file_65", "target": "file_70", "value": 2}, + {"source": "file_66", "target": "file_67", "value": 1}, + {"source": "file_66", "target": "file_71", "value": 2}, + {"source": "file_67", "target": "file_68", "value": 1}, + {"source": "file_67", "target": "file_72", "value": 2}, + {"source": "file_68", "target": "file_69", "value": 1}, + {"source": "file_68", "target": "file_73", "value": 2}, + {"source": "file_69", "target": "file_70", "value": 1}, + {"source": "file_69", "target": "file_74", "value": 2}, + {"source": "file_70", "target": "file_71", "value": 1}, + {"source": "file_70", "target": "file_75", "value": 2}, + {"source": "file_71", "target": "file_72", "value": 1}, + {"source": "file_71", "target": "file_76", "value": 2}, + {"source": "file_72", "target": "file_73", "value": 1}, + {"source": "file_72", "target": "file_77", "value": 2}, + {"source": "file_73", "target": "file_74", "value": 1}, + {"source": "file_73", "target": "file_78", "value": 2}, + {"source": "file_74", "target": "file_75", "value": 1}, + {"source": "file_74", "target": "file_79", "value": 2}, + {"source": "file_75", "target": "file_76", "value": 1}, + {"source": "file_75", "target": "file_80", "value": 2}, + {"source": "file_76", "target": "file_77", "value": 1}, + {"source": "file_77", "target": "file_78", "value": 1}, + {"source": "file_78", "target": "file_79", "value": 1}, + {"source": "file_79", "target": "file_80", "value": 1}, + {"source": "file_72", "target": "file_61", "value": 2}, + {"source": "file_72", "target": "file_62", "value": 2}, + {"source": "file_72", "target": "file_63", "value": 2}, + {"source": "file_72", "target": "file_64", "value": 2}, + {"source": "file_72", "target": "file_65", "value": 2}, + {"source": "file_72", "target": "file_66", "value": 2}, + {"source": "file_72", "target": "file_67", "value": 2}, + {"source": "file_72", "target": "file_68", "value": 2}, + {"source": "file_72", "target": "file_69", "value": 2}, + {"source": "file_72", "target": "file_70", "value": 2}, + {"source": "file_72", "target": "file_71", "value": 2}, + {"source": "file_72", "target": "file_73", "value": 2}, + {"source": "file_72", "target": "file_74", "value": 2}, + {"source": "file_72", "target": "file_75", "value": 2}, + {"source": "file_72", "target": "file_76", "value": 2}, + {"source": "file_72", "target": "file_77", "value": 2}, + {"source": "file_72", "target": "file_78", "value": 2}, + {"source": "file_72", "target": "file_79", "value": 2}, + {"source": "file_72", "target": "file_80", "value": 2}, + {"source": "file_72", "target": "file_81", "value": 2}, + {"source": "file_72", "target": "file_82", "value": 2}, + {"source": "file_80", "target": "file_81", "value": 2}, + {"source": "file_81", "target": "file_82", "value": 2}, + {"source": "file_82", "target": "file_83", "value": 2}, + {"source": "file_83", "target": "file_84", "value": 2}, + {"source": "file_84", "target": "file_85", "value": 2}, + {"source": "file_85", "target": "file_86", "value": 2}, + {"source": "file_86", "target": "file_87", "value": 2}, + {"source": "file_87", "target": "file_88", "value": 2}, + {"source": "file_88", "target": "file_89", "value": 2}, + {"source": "file_89", "target": "file_90", "value": 2}, + {"source": "file_90", "target": "file_91", "value": 3}, + {"source": "file_91", "target": "file_92", "value": 3}, + {"source": "file_92", "target": "file_93", "value": 3}, + {"source": "file_93", "target": "file_94", "value": 3}, + {"source": "file_94", "target": "file_95", "value": 3}, + {"source": "file_95", "target": "file_96", "value": 3}, + {"source": "file_96", "target": "file_97", "value": 3}, + {"source": "file_97", "target": "file_98", "value": 3}, + {"source": "file_98", "target": "file_99", "value": 3}, + {"source": "file_99", "target": "file_100", "value": 3}, + {"source": "file_82", "target": "file_84", "value": 2}, + {"source": "file_84", "target": "file_86", "value": 2}, + {"source": "file_82", "target": "file_86", "value": 1}, + {"source": "file_85", "target": "file_87", "value": 2}, + {"source": "file_87", "target": "file_89", "value": 2}, + {"source": "file_85", "target": "file_89", "value": 1}, + {"source": "file_90", "target": "file_92", "value": 2}, + {"source": "file_92", "target": "file_94", "value": 2}, + {"source": "file_90", "target": "file_94", "value": 1}, + {"source": "file_93", "target": "file_95", "value": 2}, + {"source": "file_95", "target": "file_97", "value": 2}, + {"source": "file_93", "target": "file_97", "value": 1}, + {"source": "file_96", "target": "file_98", "value": 2}, + {"source": "file_98", "target": "file_100", "value": 2}, + {"source": "file_96", "target": "file_100", "value": 1}, + {"source": "file_88", "target": "file_83", "value": 3}, + {"source": "file_88", "target": "file_84", "value": 3}, + {"source": "file_88", "target": "file_85", "value": 3}, + {"source": "file_88", "target": "file_86", "value": 3}, + {"source": "file_88", "target": "file_87", "value": 3}, + {"source": "file_88", "target": "file_89", "value": 3}, + {"source": "file_88", "target": "file_90", "value": 3}, + {"source": "file_88", "target": "file_91", "value": 3}, + {"source": "file_88", "target": "file_92", "value": 3}, + {"source": "file_88", "target": "file_93", "value": 3}, + {"source": "file_88", "target": "file_94", "value": 3}, + {"source": "file_88", "target": "file_95", "value": 3}, + {"source": "file_88", "target": "file_96", "value": 3}, + {"source": "file_88", "target": "file_97", "value": 3}, + {"source": "file_88", "target": "file_98", "value": 3}, + {"source": "file_88", "target": "file_99", "value": 3}, + {"source": "file_88", "target": "file_100", "value": 3} ] } diff --git a/client/public/Projects/DummyProject/mail-graph-data.json b/client/public/Projects/DummyProject/mail-graph-data.json index 00f716e..8da2d4b 100644 --- a/client/public/Projects/DummyProject/mail-graph-data.json +++ b/client/public/Projects/DummyProject/mail-graph-data.json @@ -7,7 +7,101 @@ { "id": "mail_5", "group": "mail", "value": 2 }, { "id": "mail_6", "group": "mail", "value": 2 }, { "id": "mail_7", "group": "mail", "value": 2 }, - { "id": "mail_8", "group": "mail", "value": 2 } + { "id": "mail_8", "group": "mail", "value": 2 }, + + { "id": "mail_9", "group": "mail", "value": 2 }, + { "id": "mail_10", "group": "mail", "value": 3 }, + { "id": "mail_11", "group": "mail", "value": 2 }, + { "id": "mail_12", "group": "mail", "value": 2 }, + { "id": "mail_13", "group": "mail", "value": 2 }, + { "id": "mail_14", "group": "mail", "value": 3 }, + { "id": "mail_15", "group": "mail", "value": 2 }, + { "id": "mail_16", "group": "mail", "value": 2 }, + { "id": "mail_17", "group": "mail", "value": 2 }, + { "id": "mail_18", "group": "mail", "value": 2 }, + { "id": "mail_19", "group": "mail", "value": 2 }, + { "id": "mail_20", "group": "mail", "value": 3 }, + { "id": "mail_21", "group": "mail", "value": 2 }, + { "id": "mail_22", "group": "mail", "value": 4 }, + { "id": "mail_23", "group": "mail", "value": 2 }, + { "id": "mail_24", "group": "mail", "value": 6 }, + { "id": "mail_25", "group": "mail", "value": 2 }, + { "id": "mail_26", "group": "mail", "value": 2 }, + { "id": "mail_27", "group": "mail", "value": 2 }, + { "id": "mail_28", "group": "mail", "value": 3 }, + { "id": "mail_29", "group": "mail", "value": 2 }, + { "id": "mail_30", "group": "mail", "value": 2 }, + { "id": "mail_31", "group": "mail", "value": 2 }, + { "id": "mail_32", "group": "mail", "value": 2 }, + { "id": "mail_33", "group": "mail", "value": 2 }, + { "id": "mail_34", "group": "mail", "value": 3 }, + { "id": "mail_35", "group": "mail", "value": 5 }, + { "id": "mail_36", "group": "mail", "value": 2 }, + { "id": "mail_37", "group": "mail", "value": 2 }, + { "id": "mail_38", "group": "mail", "value": 2 }, + { "id": "mail_39", "group": "mail", "value": 2 }, + { "id": "mail_40", "group": "mail", "value": 2 }, + { "id": "mail_41", "group": "mail", "value": 2 }, + { "id": "mail_42", "group": "mail", "value": 4 }, + { "id": "mail_43", "group": "mail", "value": 2 }, + { "id": "mail_44", "group": "mail", "value": 2 }, + { "id": "mail_45", "group": "mail", "value": 2 }, + { "id": "mail_46", "group": "mail", "value": 2 }, + { "id": "mail_47", "group": "mail", "value": 2 }, + { "id": "mail_48", "group": "mail", "value": 2 }, + { "id": "mail_49", "group": "mail", "value": 3 }, + { "id": "mail_50", "group": "mail", "value": 7 }, + + { "id": "mail_51", "group": "mail", "value": 2 }, + { "id": "mail_52", "group": "mail", "value": 2 }, + { "id": "mail_53", "group": "mail", "value": 2 }, + { "id": "mail_54", "group": "mail", "value": 2 }, + { "id": "mail_55", "group": "mail", "value": 5 }, + { "id": "mail_56", "group": "mail", "value": 4 }, + { "id": "mail_57", "group": "mail", "value": 2 }, + { "id": "mail_58", "group": "mail", "value": 2 }, + { "id": "mail_59", "group": "mail", "value": 2 }, + { "id": "mail_60", "group": "mail", "value": 2 }, + { "id": "mail_61", "group": "mail", "value": 2 }, + { "id": "mail_62", "group": "mail", "value": 2 }, + { "id": "mail_63", "group": "mail", "value": 3 }, + { "id": "mail_64", "group": "mail", "value": 2 }, + { "id": "mail_65", "group": "mail", "value": 2 }, + { "id": "mail_66", "group": "mail", "value": 2 }, + { "id": "mail_67", "group": "mail", "value": 2 }, + { "id": "mail_68", "group": "mail", "value": 2 }, + { "id": "mail_69", "group": "mail", "value": 2 }, + { "id": "mail_70", "group": "mail", "value": 4 }, + { "id": "mail_71", "group": "mail", "value": 2 }, + { "id": "mail_72", "group": "mail", "value": 6 }, + { "id": "mail_73", "group": "mail", "value": 2 }, + { "id": "mail_74", "group": "mail", "value": 2 }, + { "id": "mail_75", "group": "mail", "value": 2 }, + { "id": "mail_76", "group": "mail", "value": 2 }, + { "id": "mail_77", "group": "mail", "value": 2 }, + { "id": "mail_78", "group": "mail", "value": 5 }, + { "id": "mail_79", "group": "mail", "value": 2 }, + { "id": "mail_80", "group": "mail", "value": 2 }, + { "id": "mail_81", "group": "mail", "value": 2 }, + { "id": "mail_82", "group": "mail", "value": 2 }, + { "id": "mail_83", "group": "mail", "value": 2 }, + { "id": "mail_84", "group": "mail", "value": 4 }, + { "id": "mail_85", "group": "mail", "value": 2 }, + { "id": "mail_86", "group": "mail", "value": 2 }, + { "id": "mail_87", "group": "mail", "value": 2 }, + { "id": "mail_88", "group": "mail", "value": 7 }, + { "id": "mail_89", "group": "mail", "value": 2 }, + { "id": "mail_90", "group": "mail", "value": 2 }, + { "id": "mail_91", "group": "mail", "value": 3 }, + { "id": "mail_92", "group": "mail", "value": 2 }, + { "id": "mail_93", "group": "mail", "value": 2 }, + { "id": "mail_94", "group": "mail", "value": 2 }, + { "id": "mail_95", "group": "mail", "value": 2 }, + { "id": "mail_96", "group": "mail", "value": 2 }, + { "id": "mail_97", "group": "mail", "value": 2 }, + { "id": "mail_98", "group": "mail", "value": 4 }, + { "id": "mail_99", "group": "mail", "value": 2 }, + { "id": "mail_100", "group": "mail", "value": 8 } ], "links": [ { "source": "mail_2", "target": "mail_1", "value": 1 }, @@ -18,6 +112,209 @@ { "source": "mail_5", "target": "mail_4", "value": 1 }, { "source": "mail_5", "target": "mail_1", "value": 1 }, { "source": "mail_6", "target": "mail_7", "value": 1 }, - { "source": "mail_7", "target": "mail_8", "value": 1 } + { "source": "mail_7", "target": "mail_8", "value": 1 }, + + { "source": "mail_8", "target": "mail_9", "value": 2 }, + { "source": "mail_9", "target": "mail_10", "value": 2 }, + { "source": "mail_10", "target": "mail_11", "value": 2 }, + { "source": "mail_10", "target": "mail_14", "value": 2 }, + { "source": "mail_11", "target": "mail_12", "value": 2 }, + { "source": "mail_12", "target": "mail_13", "value": 2 }, + { "source": "mail_14", "target": "mail_15", "value": 3 }, + { "source": "mail_15", "target": "mail_16", "value": 2 }, + { "source": "mail_16", "target": "mail_17", "value": 2 }, + { "source": "mail_17", "target": "mail_18", "value": 2 }, + { "source": "mail_18", "target": "mail_19", "value": 2 }, + { "source": "mail_19", "target": "mail_20", "value": 2 }, + { "source": "mail_20", "target": "mail_21", "value": 2 }, + { "source": "mail_21", "target": "mail_22", "value": 3 }, + { "source": "mail_22", "target": "mail_23", "value": 3 }, + { "source": "mail_23", "target": "mail_24", "value": 4 }, + + { "source": "mail_24", "target": "mail_25", "value": 2 }, + { "source": "mail_25", "target": "mail_26", "value": 2 }, + { "source": "mail_26", "target": "mail_27", "value": 2 }, + { "source": "mail_27", "target": "mail_28", "value": 2 }, + { "source": "mail_28", "target": "mail_29", "value": 2 }, + { "source": "mail_29", "target": "mail_30", "value": 2 }, + { "source": "mail_30", "target": "mail_31", "value": 2 }, + { "source": "mail_31", "target": "mail_32", "value": 2 }, + { "source": "mail_32", "target": "mail_33", "value": 2 }, + { "source": "mail_33", "target": "mail_34", "value": 2 }, + { "source": "mail_34", "target": "mail_35", "value": 2 }, + { "source": "mail_35", "target": "mail_36", "value": 2 }, + { "source": "mail_36", "target": "mail_37", "value": 2 }, + { "source": "mail_37", "target": "mail_38", "value": 2 }, + { "source": "mail_38", "target": "mail_39", "value": 2 }, + { "source": "mail_39", "target": "mail_40", "value": 2 }, + + { "source": "mail_24", "target": "mail_13", "value": 2 }, + { "source": "mail_24", "target": "mail_14", "value": 2 }, + { "source": "mail_24", "target": "mail_15", "value": 2 }, + { "source": "mail_24", "target": "mail_16", "value": 2 }, + { "source": "mail_24", "target": "mail_17", "value": 2 }, + { "source": "mail_24", "target": "mail_18", "value": 2 }, + { "source": "mail_24", "target": "mail_19", "value": 2 }, + { "source": "mail_24", "target": "mail_20", "value": 2 }, + { "source": "mail_24", "target": "mail_21", "value": 2 }, + { "source": "mail_24", "target": "mail_22", "value": 2 }, + + { "source": "mail_50", "target": "mail_33", "value": 2 }, + { "source": "mail_50", "target": "mail_34", "value": 2 }, + { "source": "mail_50", "target": "mail_35", "value": 2 }, + { "source": "mail_50", "target": "mail_36", "value": 2 }, + { "source": "mail_50", "target": "mail_37", "value": 2 }, + { "source": "mail_50", "target": "mail_38", "value": 2 }, + { "source": "mail_50", "target": "mail_39", "value": 2 }, + { "source": "mail_50", "target": "mail_40", "value": 2 }, + { "source": "mail_50", "target": "mail_41", "value": 2 }, + { "source": "mail_50", "target": "mail_42", "value": 2 }, + { "source": "mail_50", "target": "mail_43", "value": 2 }, + { "source": "mail_50", "target": "mail_44", "value": 2 }, + { "source": "mail_50", "target": "mail_45", "value": 3 }, + { "source": "mail_50", "target": "mail_46", "value": 3 }, + { "source": "mail_50", "target": "mail_47", "value": 3 }, + { "source": "mail_50", "target": "mail_48", "value": 3 }, + { "source": "mail_50", "target": "mail_49", "value": 3 }, + { "source": "mail_50", "target": "mail_51", "value": 3 }, + { "source": "mail_50", "target": "mail_52", "value": 3 }, + { "source": "mail_50", "target": "mail_53", "value": 3 }, + { "source": "mail_50", "target": "mail_54", "value": 3 }, + { "source": "mail_50", "target": "mail_55", "value": 3 }, + + { "source": "mail_40", "target": "mail_42", "value": 2 }, + { "source": "mail_41", "target": "mail_43", "value": 1 }, + { "source": "mail_42", "target": "mail_44", "value": 2 }, + { "source": "mail_43", "target": "mail_45", "value": 1 }, + { "source": "mail_44", "target": "mail_46", "value": 2 }, + { "source": "mail_45", "target": "mail_47", "value": 1 }, + { "source": "mail_46", "target": "mail_48", "value": 2 }, + { "source": "mail_47", "target": "mail_49", "value": 1 }, + { "source": "mail_48", "target": "mail_50", "value": 2 }, + { "source": "mail_49", "target": "mail_51", "value": 1 }, + { "source": "mail_50", "target": "mail_52", "value": 2 }, + { "source": "mail_51", "target": "mail_53", "value": 1 }, + { "source": "mail_52", "target": "mail_54", "value": 2 }, + { "source": "mail_53", "target": "mail_55", "value": 1 }, + { "source": "mail_54", "target": "mail_56", "value": 2 }, + { "source": "mail_55", "target": "mail_57", "value": 1 }, + { "source": "mail_56", "target": "mail_58", "value": 2 }, + { "source": "mail_57", "target": "mail_59", "value": 1 }, + { "source": "mail_58", "target": "mail_60", "value": 2 }, + { "source": "mail_59", "target": "mail_61", "value": 1 }, + + { "source": "mail_60", "target": "mail_61", "value": 1 }, + { "source": "mail_60", "target": "mail_65", "value": 2 }, + { "source": "mail_61", "target": "mail_62", "value": 1 }, + { "source": "mail_61", "target": "mail_66", "value": 2 }, + { "source": "mail_62", "target": "mail_63", "value": 1 }, + { "source": "mail_62", "target": "mail_67", "value": 2 }, + { "source": "mail_63", "target": "mail_64", "value": 1 }, + { "source": "mail_63", "target": "mail_68", "value": 2 }, + { "source": "mail_64", "target": "mail_65", "value": 1 }, + { "source": "mail_64", "target": "mail_69", "value": 2 }, + { "source": "mail_65", "target": "mail_66", "value": 1 }, + { "source": "mail_65", "target": "mail_70", "value": 2 }, + { "source": "mail_66", "target": "mail_67", "value": 1 }, + { "source": "mail_66", "target": "mail_71", "value": 2 }, + { "source": "mail_67", "target": "mail_68", "value": 1 }, + { "source": "mail_67", "target": "mail_72", "value": 2 }, + { "source": "mail_68", "target": "mail_69", "value": 1 }, + { "source": "mail_68", "target": "mail_73", "value": 2 }, + { "source": "mail_69", "target": "mail_70", "value": 1 }, + { "source": "mail_69", "target": "mail_74", "value": 2 }, + { "source": "mail_70", "target": "mail_71", "value": 1 }, + { "source": "mail_70", "target": "mail_75", "value": 2 }, + { "source": "mail_71", "target": "mail_72", "value": 1 }, + { "source": "mail_71", "target": "mail_76", "value": 2 }, + { "source": "mail_72", "target": "mail_73", "value": 1 }, + { "source": "mail_72", "target": "mail_77", "value": 2 }, + { "source": "mail_73", "target": "mail_74", "value": 1 }, + { "source": "mail_73", "target": "mail_78", "value": 2 }, + { "source": "mail_74", "target": "mail_75", "value": 1 }, + { "source": "mail_74", "target": "mail_79", "value": 2 }, + { "source": "mail_75", "target": "mail_76", "value": 1 }, + { "source": "mail_75", "target": "mail_80", "value": 2 }, + { "source": "mail_76", "target": "mail_77", "value": 1 }, + { "source": "mail_77", "target": "mail_78", "value": 1 }, + { "source": "mail_78", "target": "mail_79", "value": 1 }, + { "source": "mail_79", "target": "mail_80", "value": 1 }, + + { "source": "mail_72", "target": "mail_61", "value": 2 }, + { "source": "mail_72", "target": "mail_62", "value": 2 }, + { "source": "mail_72", "target": "mail_63", "value": 2 }, + { "source": "mail_72", "target": "mail_64", "value": 2 }, + { "source": "mail_72", "target": "mail_65", "value": 2 }, + { "source": "mail_72", "target": "mail_66", "value": 2 }, + { "source": "mail_72", "target": "mail_67", "value": 2 }, + { "source": "mail_72", "target": "mail_68", "value": 2 }, + { "source": "mail_72", "target": "mail_69", "value": 2 }, + { "source": "mail_72", "target": "mail_70", "value": 2 }, + { "source": "mail_72", "target": "mail_71", "value": 2 }, + { "source": "mail_72", "target": "mail_73", "value": 2 }, + { "source": "mail_72", "target": "mail_74", "value": 2 }, + { "source": "mail_72", "target": "mail_75", "value": 2 }, + { "source": "mail_72", "target": "mail_76", "value": 2 }, + { "source": "mail_72", "target": "mail_77", "value": 2 }, + { "source": "mail_72", "target": "mail_78", "value": 2 }, + { "source": "mail_72", "target": "mail_79", "value": 2 }, + { "source": "mail_72", "target": "mail_80", "value": 2 }, + { "source": "mail_72", "target": "mail_81", "value": 2 }, + { "source": "mail_72", "target": "mail_82", "value": 2 }, + + { "source": "mail_80", "target": "mail_81", "value": 2 }, + { "source": "mail_81", "target": "mail_82", "value": 2 }, + { "source": "mail_82", "target": "mail_83", "value": 2 }, + { "source": "mail_83", "target": "mail_84", "value": 2 }, + { "source": "mail_84", "target": "mail_85", "value": 2 }, + { "source": "mail_85", "target": "mail_86", "value": 2 }, + { "source": "mail_86", "target": "mail_87", "value": 2 }, + { "source": "mail_87", "target": "mail_88", "value": 2 }, + { "source": "mail_88", "target": "mail_89", "value": 2 }, + { "source": "mail_89", "target": "mail_90", "value": 2 }, + { "source": "mail_90", "target": "mail_91", "value": 3 }, + { "source": "mail_91", "target": "mail_92", "value": 3 }, + { "source": "mail_92", "target": "mail_93", "value": 3 }, + { "source": "mail_93", "target": "mail_94", "value": 3 }, + { "source": "mail_94", "target": "mail_95", "value": 3 }, + { "source": "mail_95", "target": "mail_96", "value": 3 }, + { "source": "mail_96", "target": "mail_97", "value": 3 }, + { "source": "mail_97", "target": "mail_98", "value": 3 }, + { "source": "mail_98", "target": "mail_99", "value": 3 }, + { "source": "mail_99", "target": "mail_100", "value": 3 }, + + { "source": "mail_82", "target": "mail_84", "value": 2 }, + { "source": "mail_84", "target": "mail_86", "value": 2 }, + { "source": "mail_82", "target": "mail_86", "value": 1 }, + { "source": "mail_85", "target": "mail_87", "value": 2 }, + { "source": "mail_87", "target": "mail_89", "value": 2 }, + { "source": "mail_85", "target": "mail_89", "value": 1 }, + { "source": "mail_90", "target": "mail_92", "value": 2 }, + { "source": "mail_92", "target": "mail_94", "value": 2 }, + { "source": "mail_90", "target": "mail_94", "value": 1 }, + { "source": "mail_93", "target": "mail_95", "value": 2 }, + { "source": "mail_95", "target": "mail_97", "value": 2 }, + { "source": "mail_93", "target": "mail_97", "value": 1 }, + { "source": "mail_96", "target": "mail_98", "value": 2 }, + { "source": "mail_98", "target": "mail_100", "value": 2 }, + { "source": "mail_96", "target": "mail_100", "value": 1 }, + + { "source": "mail_88", "target": "mail_83", "value": 3 }, + { "source": "mail_88", "target": "mail_84", "value": 3 }, + { "source": "mail_88", "target": "mail_85", "value": 3 }, + { "source": "mail_88", "target": "mail_86", "value": 3 }, + { "source": "mail_88", "target": "mail_87", "value": 3 }, + { "source": "mail_88", "target": "mail_89", "value": 3 }, + { "source": "mail_88", "target": "mail_90", "value": 3 }, + { "source": "mail_88", "target": "mail_91", "value": 3 }, + { "source": "mail_88", "target": "mail_92", "value": 3 }, + { "source": "mail_88", "target": "mail_93", "value": 3 }, + { "source": "mail_88", "target": "mail_94", "value": 3 }, + { "source": "mail_88", "target": "mail_95", "value": 3 }, + { "source": "mail_88", "target": "mail_96", "value": 3 }, + { "source": "mail_88", "target": "mail_97", "value": 3 }, + { "source": "mail_88", "target": "mail_98", "value": 3 }, + { "source": "mail_88", "target": "mail_99", "value": 3 }, + { "source": "mail_88", "target": "mail_100", "value": 3 } ] } diff --git a/client/src/components/layouts/app-layout.css b/client/src/components/layouts/app-layout.css deleted file mode 100644 index fe64e4d..0000000 --- a/client/src/components/layouts/app-layout.css +++ /dev/null @@ -1,9 +0,0 @@ -#appContainer { - height: 100dvh; - width: 100dvw; - overflow: hidden; - display: flex; - flex-direction: column; - color: white; - flex: 1; -} \ No newline at end of file diff --git a/client/src/components/ui/header/index.ts b/client/src/components/ui/header/index.ts deleted file mode 100644 index d83c5da..0000000 --- a/client/src/components/ui/header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './header.tsx'; \ No newline at end of file diff --git a/client/src/components/ui/navbar/navbar.tsx b/client/src/components/ui/navbar/navbar.tsx index a2c7ba8..f6a9a2b 100644 --- a/client/src/components/ui/navbar/navbar.tsx +++ b/client/src/components/ui/navbar/navbar.tsx @@ -34,7 +34,7 @@ export const Navbar = ({ children }: { children: ReactNode }) => { fontSize: '1.25rem', }} > - Projects + Kaiāulu { children } diff --git a/client/src/config/app.ts b/client/src/config/app.ts index 7d02693..1fdf28e 100644 --- a/client/src/config/app.ts +++ b/client/src/config/app.ts @@ -1,6 +1,11 @@ import { parse } from 'yaml'; import rawConfig from './user.config.yaml?raw'; +/** + * THIS FILE CONVERTS THE USER DEFINEd PATHS IN THE KAIAULU CONFIG INTO CONSTANTS USED BY THE APPLICATION + */ + + const config = parse(rawConfig) as { networkGraph: { projectsDirectory: string, diff --git a/client/src/features/NetworkGraph/components/network-graph-canvas/network-graph-canvas.tsx b/client/src/features/NetworkGraph/components/network-graph-canvas/network-graph-canvas.tsx index 40ab930..ef8a51f 100644 --- a/client/src/features/NetworkGraph/components/network-graph-canvas/network-graph-canvas.tsx +++ b/client/src/features/NetworkGraph/components/network-graph-canvas/network-graph-canvas.tsx @@ -9,9 +9,16 @@ import { useNetworkGraphInteractions } from "@/features/NetworkGraph/hooks/useNe import { drawGraph } from "@/features/NetworkGraph/lib/draw-network-graph.ts"; +/** + * Provides an HTML canvas for the Network Graph feature and exposes a function that draws a network graph to that canvas + */ export const NetworkGraphCanvas = () => { const { nodes, links, canvasRef, overlayOn, transparentNodeMapRef } = useNetworkGraph(); + /** + * Function called by network graph hooks whenever they have mutated relevant data (node position changed by user, highlight mode, etc.) + * that draws the current state of the network graph to the canvas. + */ const renderNetworkGraph = useCallback(() => { if (!canvasRef.current) return; const canvas = canvasRef.current; @@ -19,6 +26,7 @@ export const NetworkGraphCanvas = () => { const container = canvas.parentElement; if (!container) return; + //Before every draw, canvas 'draw space' needs to be the same as its parent container, else the drawing will appear distorted const rect = container.getBoundingClientRect(); const nextW = Math.max(1, Math.floor(rect.width)); const nextH = Math.max(1, Math.floor(rect.height)); @@ -26,6 +34,7 @@ export const NetworkGraphCanvas = () => { if (canvas.width !== nextW) canvas.width = nextW; if (canvas.height !== nextH) canvas.height = nextH; + // Helper function from draw-network-graph library, runs the canvas + context logic for drawing drawGraph(canvas, nodes, links, transparentNodeMapRef.current, 11, 'gray', @@ -33,9 +42,21 @@ export const NetworkGraphCanvas = () => { }, [canvasRef, links, nodes, transparentNodeMapRef]); + /** + * Calculates the initial positions of nodes and links in the Network Graph by running a d3.js simulation. + * Renders the Network Graph after simulation has completed. + */ useInitialPhysicsSimulation(renderNetworkGraph); + + /** + * Allows user to interface with the network graph and also applies interaction specific logic + * Renders the Network Graph whenever user interaction has changed anything about the graph visually (dragged nodes, highlight mode). + */ useNetworkGraphInteractions(renderNetworkGraph); + /** + * If a DOM re-render happens and the subgraph overlay state has changed, then draw the network graph to the canvas + */ useEffect(() => { renderNetworkGraph(); }, [renderNetworkGraph, overlayOn]); diff --git a/client/src/features/NetworkGraph/components/network-graph-search-bar/network-graph-search-bar.tsx b/client/src/features/NetworkGraph/components/network-graph-search-bar/network-graph-search-bar.tsx index ee44c1b..33b9fe2 100644 --- a/client/src/features/NetworkGraph/components/network-graph-search-bar/network-graph-search-bar.tsx +++ b/client/src/features/NetworkGraph/components/network-graph-search-bar/network-graph-search-bar.tsx @@ -6,7 +6,9 @@ import { setNodeNeighborhoodToBeOpaque } from "@/features/NetworkGraph/utils/transparent-node-map.ts"; - +/** + * Custom styled Search form that acts as container for the Input + */ const Search = styled("form")(({ theme }) => ({ position: "relative", borderRadius: theme.shape.borderRadius, @@ -23,7 +25,9 @@ const Search = styled("form")(({ theme }) => ({ paddingLeft: 10, })); - +/** + * Search bar that allows the user to target a node by name and highlight a subgraph + */ export const NetworkGraphSearchBar = () => { const { nodeRelationshipMapRef, transparentNodeMapRef, overlayOn, setOverlayOn } = useNetworkGraph(); diff --git a/client/src/features/NetworkGraph/components/network-graph-sub-graph-overlay/network-graph-sub-graph-overlay.tsx b/client/src/features/NetworkGraph/components/network-graph-sub-graph-overlay/network-graph-sub-graph-overlay.tsx index 980ab8c..ba74907 100644 --- a/client/src/features/NetworkGraph/components/network-graph-sub-graph-overlay/network-graph-sub-graph-overlay.tsx +++ b/client/src/features/NetworkGraph/components/network-graph-sub-graph-overlay/network-graph-sub-graph-overlay.tsx @@ -9,6 +9,7 @@ import {Button, Container, Typography} from "@mui/material"; * * Note: * - Uses custom FloatingPanel component + * - Uses DndContext, necessary for components created with dnd-kit to have drag functionality */ export const NetworkGraphSubGraphOverlay = () => { const { overlayOn } = useNetworkGraph(); @@ -16,7 +17,6 @@ export const NetworkGraphSubGraphOverlay = () => { if (!overlayOn) return null; return ( - // Any draggable components created with dnd-kit must be rendered within a DndContext diff --git a/client/src/features/NetworkGraph/config/paths.ts b/client/src/features/NetworkGraph/config/paths.ts index 503a90c..23fea10 100644 --- a/client/src/features/NetworkGraph/config/paths.ts +++ b/client/src/features/NetworkGraph/config/paths.ts @@ -1,3 +1,7 @@ +/** + * This file exposes the base path utilized by a hook to load the relevant Network Graph data for a user-specified project + */ + import { PROJECTS_DIRECTORY, TARGET_PROJECT } from "@/config/app.ts"; export const BASE_PATH = `/${PROJECTS_DIRECTORY}/${TARGET_PROJECT}`; \ No newline at end of file diff --git a/client/src/features/NetworkGraph/hooks/useInitialPhysicsSimulation.ts b/client/src/features/NetworkGraph/hooks/useInitialPhysicsSimulation.ts index 8c1791f..4d2a220 100644 --- a/client/src/features/NetworkGraph/hooks/useInitialPhysicsSimulation.ts +++ b/client/src/features/NetworkGraph/hooks/useInitialPhysicsSimulation.ts @@ -12,11 +12,16 @@ type NodeGroupCenters = { } /** - * Runs physics simulation to determine initial position of nodes and links in the Network Graph + * Runs physics simulation to determine initial position of nodes and links in the Network Graph. + * Accepts a function as a parameter which it will execute once the simulation finishes. + * Mutates node and link objects to have x and y coordinates, sets them according to simulation results. * * Note: * - D3.js force simulation is used to calculate the positions of all nodes and links * - Nodes and link objects are mutated by D3.js force simulation to contain position values x and y + * + * Usage: + * - Call it in a NetworkGraphCanvas component */ export function useInitialPhysicsSimulation(renderNetworkGraph: () => void) { @@ -33,14 +38,29 @@ export function useInitialPhysicsSimulation(renderNetworkGraph: () => void) { const canvas = canvasRef.current; if (!canvas) return; + // Strength of node gravity const forceStrength = -100; + + // Arbitrary scalar that changes the size of the nodes, 11 seemed to be the nicest const nodeRadiusMultiplier = 11; + + // Padding between nodes (so they don't cling tightly to each other and instead are separated by a small amount of space) const nodePadding = 6; + // Calculates the positions of the hubs, the hub is the center of a network, each node type has one hub which the other nodes are gravitated to const c = calculateNodeGroupCenters(canvas.width, canvas.height); + + // Decides which node in each type network will be the hub of its network. const hubs = buildHubs(nodes); + + // Create links for the physics simulation between hub nodes and their surrounding nodes const hubLinks: HubLink[] = buildHubLinks(nodes, hubs); + /** + * Creates a D3.js force simulation with the nodes and links provided. + * + * Look into the documentation for D3.js forceSimulation for more detail. + */ const sim = forceSimulation(nodes) .force("link", forceLink(links).id(d => d.id).distance(80).strength(0.05)) .force("hubLinks", forceLink(hubLinks).distance(40).strength(0.2)) @@ -49,15 +69,36 @@ export function useInitialPhysicsSimulation(renderNetworkGraph: () => void) { .force("collide", forceCollide().radius(d => d.value * nodeRadiusMultiplier + nodePadding)) .force("charge", forceManyBody().strength(forceStrength)); + /** + * Set the simulation alpha to 1 (this is like the start of the simulation where nothing has happened) + */ sim.alpha(1); + + /** + * Run the simulation for an arbitrary amount of ticks, 300 seemed to be the best. + */ for (let i = 0; i < 300; i++) sim.tick(); + + /** + * Zeroes out any remaining forces + */ clearForces(sim as Simulation); + + /** + * Stops the simulation + */ sim.stop(); + // Finally, draw the Network Graph with its updated state renderNetworkGraph(); + }, [nodes, links, canvasRef, renderNetworkGraph]); } +/** + * Helper function for calculating node group centers, the values inside are arbitrary, this can be modularized more in the future, + * for now there are four node types, and four positions. + */ function calculateNodeGroupCenters(canvasWidth: number, canvasHeight: number): NodeGroupCenters { return { people: [canvasWidth * 0.5, canvasHeight * 0.5], @@ -67,6 +108,9 @@ function calculateNodeGroupCenters(canvasWidth: number, canvasHeight: number): N } } +/** + * Helper function for determining the hub node for each node network + */ function buildHubs(nodes: Node[]) { const hubs: Partial> = {}; @@ -80,16 +124,25 @@ function buildHubs(nodes: Node[]) { return hubs; } +/** + * Helper function for creating the links between each hub node and its surrounding nodes (part of the simulation) + */ function buildHubLinks(nodes: Node[], hubs: Partial>) { return nodes .filter(n => hubs[n.group] && !isHub(n, hubs as Record)) .map(n => ({ source: hubs[n.group]!, target: n })); } +/** + * Helper function for determining if a node is the hub + */ function isHub (d: Node, hubs: Record) { return hubs[d.group] === d; } +/** + * Clears all forces used in simulation. + */ function clearForces(sim: Simulation) { sim .force('x', null) diff --git a/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts b/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts index 81a4fb2..848ef5a 100644 --- a/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts +++ b/client/src/features/NetworkGraph/hooks/useNetworkGraphInteractions.ts @@ -10,11 +10,11 @@ import {useNetworkGraph} from "@/features/NetworkGraph/stores/network-graph-cont /** * Allows a network graph that is rendered to a canvas to become interactable to the user. + * It creates the listeners for user interaction and also runs the logic for each listener. * - * Contains logic for the following user interactions: + * Currently supported user interactions * - Double-click detection * - Drag detection - * - Redraws network graph whenever canvas size changes (user resizes window, user opens browser console, etc.) */ export function useNetworkGraphInteractions(renderNetworkGraph: () => void) { @@ -48,8 +48,8 @@ export function useNetworkGraphInteractions(renderNetworkGraph: () => void) { * * Flow: * 1. Whenever the canvas is double-clicked, check the position of the mouse pointer. - * 2. If the mouse pointer is over a node, then trigger the highlight logic. - * 3. After running the highlight logic, redraw the network graph. + * 2. If the mouse pointer is over a node, then target that node and its related nodes according to the relationship map + * 3. Update transparency values depending on current highlight state (is the overlay enabled? or is it disabled) */ const handleDblClick = (event: MouseEvent) => { const [x, y] = pointer(event, canvas); @@ -77,11 +77,16 @@ export function useNetworkGraphInteractions(renderNetworkGraph: () => void) { }, [nodes, links, canvasRef, setOverlayOn, transparentNodeMapRef, nodeRelationshipMapRef, overlayOn, renderNetworkGraph ]); } +/** + * Helper function that creates a D3.js drag behavior + * - Updates node position to position of pointer while the node is being dragged + * - Renders the graph with each tick of a drag + */ function createDragBehavior( canvas: HTMLCanvasElement, nodes: Node[], nodeRadiusMultiplier: number, - requestDraw: () => void) { + renderNetworkGraph: () => void) { return drag() .subject((event) => { @@ -101,13 +106,14 @@ function createDragBehavior( const n = event.subject; n.x = event.x; n.y = event.y; - requestDraw(); + renderNetworkGraph(); }); } - -// Finds the topmost node under (x,y) +/** + * Helper function that finds the topmost node under (x,y) + */ function findNodeAt(nodes: Node[], nodeRadiusMultiplier: number, x: number, y: number) { for (let i = nodes.length - 1; i >= 0; i--) { const n = nodes[i]; diff --git a/client/src/features/NetworkGraph/stores/network-graph-context.tsx b/client/src/features/NetworkGraph/stores/network-graph-context.tsx index 8241a68..a951fe2 100644 --- a/client/src/features/NetworkGraph/stores/network-graph-context.tsx +++ b/client/src/features/NetworkGraph/stores/network-graph-context.tsx @@ -8,13 +8,30 @@ import { buildInitialTransparencyMap, buildRelationshipMap } from "@/features/Ne const NetworkGraphContext = createContext(null); + +/** + * This data is exposed to the children of the NetworkGraphProvider. It acts as a central source of truth for the Network Graph components. + */ interface NetworkGraphContextValue { + // Array of d3.js node objects that have been extended with additional properties nodes: Node[]; + + // Array of d3.js link objects that have been extended with additional properties links: Link[]; + + // Reference object to HTML Canvas upon which the Network Graph will be drawn, survives React DOM re-renders canvasRef: RefObject + + // Current highlight state, is determined by whether or whether not the subgraph overlay is active overlayOn: boolean; + + // Function to set the current highlight state setOverlayOn: Dispatch>; + + // Reference object to a map with each node's id as a key, and a set of their related nodes as values, survives React DOM re-renders nodeRelationshipMapRef: RefObject>>; + + // Reference object to a map with each node's id as a key, and a number representing their current transparency (1 for transparent, 0 for opaque), survives React DOM re-renders transparentNodeMapRef: RefObject>; } diff --git a/client/src/features/NetworkGraph/types/network-graph.types.ts b/client/src/features/NetworkGraph/types/network-graph.types.ts index 45f118c..332f90e 100644 --- a/client/src/features/NetworkGraph/types/network-graph.types.ts +++ b/client/src/features/NetworkGraph/types/network-graph.types.ts @@ -1,5 +1,9 @@ import type { SimulationNodeDatum, SimulationLinkDatum } from "d3"; +/** + * Extended D3.js node object interface from D3.js. + */ + export interface Node extends SimulationNodeDatum { id: string; x: number; @@ -8,20 +12,14 @@ export interface Node extends SimulationNodeDatum { value: number; // added property } +/** + * Extended D3.js link object interface from D3.js. + */ + export interface Link extends SimulationLinkDatum { source: Node; target: Node; value: number; } -export type NetworkGraphData = { - nodes: Node[]; - links: Link[]; -}; - -export type Group = 'people' | 'mail' | 'file' | 'issue'; - - -export type HubLink = { - source: Node; target: Node -}; \ No newline at end of file +type Group = 'people' | 'mail' | 'file' | 'issue'; diff --git a/client/src/features/NetworkGraph/utils/transparent-node-map.ts b/client/src/features/NetworkGraph/utils/transparent-node-map.ts index 2c5507e..885311b 100644 --- a/client/src/features/NetworkGraph/utils/transparent-node-map.ts +++ b/client/src/features/NetworkGraph/utils/transparent-node-map.ts @@ -1,3 +1,10 @@ +/** + * THIS FILE CONTAINS HELPER FUNCTIONS FOR MANIPULATING THE TRANSPARENT NODE MAP. + */ + + + + /** * Helper function that sets a target node and its related nodes transparency values to be opaque */ diff --git a/client/src/styles/mui/theme.ts b/client/src/styles/mui/theme.ts index ac5480e..92149db 100644 --- a/client/src/styles/mui/theme.ts +++ b/client/src/styles/mui/theme.ts @@ -1,6 +1,9 @@ import { createTheme } from '@mui/material/styles'; import '@fontsource/roboto/300.css'; +/** + * Custom MUI theme that follows the color scheme of the control panel show in Carlos' UAMC_summer25_auna.pdf + */ export const theme = createTheme({ palette: { mode: 'dark',