Politur: Gedaechnis-Graph auf react-force-graph-2d umgestellt (elastische D3-Physik, fliessende Datenpartikel, 100% lesbare Text-Pillen)
This commit is contained in:
-312
File diff suppressed because one or more lines are too long
+44
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-r382vnGM.js"></script>
|
<script type="module" crossorigin src="/assets/index-CnQbeKKt.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-B1ESaWd9.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-1USExdfg.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
Generated
+280
@@ -19,6 +19,7 @@
|
|||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.460.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-force-graph-2d": "^1.29.1",
|
||||||
"recharts": "^3.9.0",
|
"recharts": "^3.9.0",
|
||||||
"sigma": "^3.0.3",
|
"sigma": "^3.0.3",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
@@ -2536,6 +2537,15 @@
|
|||||||
"integrity": "sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==",
|
"integrity": "sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/accessor-fn": {
|
||||||
|
"version": "1.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz",
|
||||||
|
"integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/aria-hidden": {
|
"node_modules/aria-hidden": {
|
||||||
"version": "1.2.6",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||||
@@ -2581,6 +2591,16 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bezier-js": {
|
||||||
|
"version": "6.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz",
|
||||||
|
"integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bidi-js": {
|
"node_modules/bidi-js": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||||
@@ -2678,6 +2698,18 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/canvas-color-tracker": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinycolor2": "^1.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/clsx": {
|
"node_modules/clsx": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
@@ -2760,6 +2792,12 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-binarytree": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/d3-color": {
|
"node_modules/d3-color": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
@@ -2769,6 +2807,28 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-dispatch": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-drag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-selection": "3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-ease": {
|
"node_modules/d3-ease": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
@@ -2778,6 +2838,22 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-force-3d": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-binarytree": "1",
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-octree": "1",
|
||||||
|
"d3-quadtree": "1 - 3",
|
||||||
|
"d3-timer": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-format": {
|
"node_modules/d3-format": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
|
||||||
@@ -2799,6 +2875,12 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-octree": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/d3-path": {
|
"node_modules/d3-path": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
@@ -2808,6 +2890,15 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-quadtree": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-scale": {
|
"node_modules/d3-scale": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
@@ -2824,6 +2915,28 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-scale-chromatic": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3",
|
||||||
|
"d3-interpolate": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-selection": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-shape": {
|
"node_modules/d3-shape": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
@@ -2869,6 +2982,41 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-transition": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3",
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-ease": "1 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-timer": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"d3-selection": "2 - 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-zoom": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-drag": "2 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-selection": "2 - 3",
|
||||||
|
"d3-transition": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -3046,6 +3194,46 @@
|
|||||||
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
|
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/float-tooltip": {
|
||||||
|
"version": "1.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz",
|
||||||
|
"integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-selection": "2 - 3",
|
||||||
|
"kapsule": "^1.16",
|
||||||
|
"preact": "10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/force-graph": {
|
||||||
|
"version": "1.51.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.4.tgz",
|
||||||
|
"integrity": "sha512-TdJ2KbkoiDQ7NIRx8IPGD0mAXXpLhamS7c+b7W98b0MHG7lphnda1VOQX/98UDTsttIAdH4TcP0l0MauSnLK8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tweenjs/tween.js": "18 - 25",
|
||||||
|
"accessor-fn": "1",
|
||||||
|
"bezier-js": "3 - 6",
|
||||||
|
"canvas-color-tracker": "^1.3",
|
||||||
|
"d3-array": "1 - 3",
|
||||||
|
"d3-drag": "2 - 3",
|
||||||
|
"d3-force-3d": "2 - 3",
|
||||||
|
"d3-scale": "1 - 4",
|
||||||
|
"d3-scale-chromatic": "1 - 3",
|
||||||
|
"d3-selection": "2 - 3",
|
||||||
|
"d3-zoom": "2 - 3",
|
||||||
|
"float-tooltip": "^1.7",
|
||||||
|
"index-array-by": "1",
|
||||||
|
"kapsule": "^1.16",
|
||||||
|
"lodash-es": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -3175,6 +3363,15 @@
|
|||||||
"url": "https://opencollective.com/immer"
|
"url": "https://opencollective.com/immer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/index-array-by": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/internmap": {
|
"node_modules/internmap": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
@@ -3217,6 +3414,15 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jerrypick": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||||
@@ -3259,6 +3465,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kapsule": {
|
||||||
|
"version": "1.16.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz",
|
||||||
|
"integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash-es": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lie": {
|
"node_modules/lie": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
@@ -3541,6 +3759,12 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash-es": {
|
||||||
|
"version": "4.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
|
||||||
|
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/loose-envify": {
|
"node_modules/loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
@@ -3716,6 +3940,24 @@
|
|||||||
"integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
|
"integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.7.tgz",
|
||||||
|
"integrity": "sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"preact-render-to-string": ">=5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"preact-render-to-string": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/promise-worker-transferable": {
|
"node_modules/promise-worker-transferable": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
|
||||||
@@ -3780,6 +4022,23 @@
|
|||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-force-graph-2d": {
|
||||||
|
"version": "1.29.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-force-graph-2d/-/react-force-graph-2d-1.29.1.tgz",
|
||||||
|
"integrity": "sha512-1Rl/1Z3xy2iTHKj6a0jRXGyiI86xUti81K+jBQZ+Oe46csaMikp47L5AjrzA9hY9fNGD63X8ffrqnvaORukCuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"force-graph": "^1.51",
|
||||||
|
"prop-types": "15",
|
||||||
|
"react-kapsule": "^2.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "19.2.7",
|
"version": "19.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz",
|
||||||
@@ -3787,6 +4046,21 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/react-kapsule": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-kapsule/-/react-kapsule-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-HzLJoYb1n1kfwjXbqFFcRR0EA6oPsJ64tNdDmCSaL/bz2o9wUZRSb0cMe//grLFeF9EVoL4CD/e6ozLyzEv+PQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jerrypick": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-reconciler": {
|
"node_modules/react-reconciler": {
|
||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz",
|
||||||
@@ -4199,6 +4473,12 @@
|
|||||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tinycolor2": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.460.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-force-graph-2d": "^1.29.1",
|
||||||
"recharts": "^3.9.0",
|
"recharts": "^3.9.0",
|
||||||
"sigma": "^3.0.3",
|
"sigma": "^3.0.3",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
|
|||||||
+181
-120
@@ -1,13 +1,8 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react"
|
import { useEffect, useMemo, useRef, useState } from "react"
|
||||||
import Graph from "graphology"
|
import ForceGraph2D from "react-force-graph-2d"
|
||||||
import forceAtlas2 from "graphology-layout-forceatlas2"
|
|
||||||
import Sigma from "sigma"
|
|
||||||
import { Trash2, Sparkles, Share2, RefreshCw } from "lucide-react"
|
import { Trash2, Sparkles, Share2, RefreshCw } from "lucide-react"
|
||||||
import { type MemoryGraph } from "@/lib/api"
|
import { type MemoryGraph } from "@/lib/api"
|
||||||
|
|
||||||
// Sigma.js v3 + graphology: graph-optimiertes WebGL (kein three.js-Ballast) → skaliert auf tausende
|
|
||||||
// Fakten flüssig. Im App-Look gestylt; Hover hebt den Knoten + seine Nachbarn hervor, der Rest dimmt.
|
|
||||||
|
|
||||||
const CAT_COLOR: Record<string, string> = {
|
const CAT_COLOR: Record<string, string> = {
|
||||||
identity: "#00f5ff", knowledge: "#3b82f6", rules: "#d946ef", events: "#fbbf24",
|
identity: "#00f5ff", knowledge: "#3b82f6", rules: "#d946ef", events: "#fbbf24",
|
||||||
}
|
}
|
||||||
@@ -15,129 +10,68 @@ const CAT_LABEL: Record<string, string> = {
|
|||||||
identity: "Identität", knowledge: "Wissen", rules: "Regeln", events: "Ereignisse",
|
identity: "Identität", knowledge: "Wissen", rules: "Regeln", events: "Ereignisse",
|
||||||
}
|
}
|
||||||
const AUTO = new Set(["auto", "agent", "hermes"])
|
const AUTO = new Set(["auto", "agent", "hermes"])
|
||||||
const DIM_NODE = "#1e293b"
|
const DIM_EDGE = "rgba(15, 23, 42, 0.02)"
|
||||||
const DIM_EDGE = "rgba(15, 23, 42, 0.05)"
|
const EDGE = "rgba(255, 255, 255, 0.05)"
|
||||||
const EDGE = "rgba(255, 255, 255, 0.07)"
|
|
||||||
const EDGE_HI = "rgba(99, 102, 241, 0.85)"
|
const EDGE_HI = "rgba(99, 102, 241, 0.85)"
|
||||||
|
|
||||||
export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id: string) => void }) {
|
export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id: string) => void }) {
|
||||||
const [selected, setSelected] = useState<string | null>(null)
|
const [selected, setSelected] = useState<string | null>(null)
|
||||||
|
const [hovered, setHovered] = useState<string | null>(null)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const sigmaRef = useRef<Sigma | null>(null)
|
const fgRef = useRef<any>(null)
|
||||||
const graphRef = useRef<Graph | null>(null)
|
|
||||||
const active = useRef<string | null>(null) // hervorgehobener Knoten (hover oder Auswahl)
|
const [dimensions, setDimensions] = useState({ width: 500, height: 560 })
|
||||||
const selectedRef = useRef<string | null>(null) // aktuelle Auswahl (für leaveNode ohne Effekt-Neulauf)
|
|
||||||
|
// ResizeObserver für präzise Canvas-Dimensionen im Layout
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return
|
||||||
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
for (let entry of entries) {
|
||||||
|
setDimensions({
|
||||||
|
width: Math.floor(entry.contentRect.width),
|
||||||
|
height: Math.floor(entry.contentRect.height),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
resizeObserver.observe(containerRef.current)
|
||||||
|
return () => resizeObserver.disconnect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Graphology-Daten in react-force-graph-2d-kompatibles Format konvertieren
|
||||||
|
const graphData = useMemo(() => {
|
||||||
|
return {
|
||||||
|
nodes: data.nodes.map((n) => ({ ...n })),
|
||||||
|
links: data.edges.map((e) => ({
|
||||||
|
source: e.source,
|
||||||
|
target: e.target,
|
||||||
|
weight: e.weight || 0,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}, [data])
|
||||||
|
|
||||||
const degree = useMemo(() => {
|
const degree = useMemo(() => {
|
||||||
const d: Record<string, number> = {}
|
const d: Record<string, number> = {}
|
||||||
data.edges.forEach((e) => { d[e.source] = (d[e.source] || 0) + 1; d[e.target] = (d[e.target] || 0) + 1 })
|
data.edges.forEach((e) => {
|
||||||
|
d[e.source] = (d[e.source] || 0) + 1
|
||||||
|
d[e.target] = (d[e.target] || 0) + 1
|
||||||
|
})
|
||||||
return d
|
return d
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
// Graph (neu)bauen + Layout + Sigma rendern, wenn sich die Daten ändern.
|
// D3 Force Engine tunen (Luftige Struktur, kein Überlappen)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current || !data.nodes.length) return
|
if (!fgRef.current) return
|
||||||
const graph = new Graph()
|
const fg = fgRef.current
|
||||||
const N = data.nodes.length
|
fg.d3Force("charge").strength(-160)
|
||||||
data.nodes.forEach((n, i) => {
|
fg.d3Force("link").distance(65)
|
||||||
const a = (2 * Math.PI * i) / N
|
}, [graphData])
|
||||||
graph.addNode(n.id, {
|
|
||||||
x: Math.cos(a), y: Math.sin(a), // Startposition (Kreis) für ForceAtlas2
|
|
||||||
size: 5 + Math.min(degree[n.id] || 0, 12) * 1.5,
|
|
||||||
color: CAT_COLOR[n.category] || "#64748b",
|
|
||||||
label: n.content.length > 48 ? n.content.slice(0, 47) + "…" : n.content,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
data.edges.forEach((e) => {
|
|
||||||
if (graph.hasNode(e.source) && graph.hasNode(e.target) && !graph.hasEdge(e.source, e.target))
|
|
||||||
graph.addEdge(e.source, e.target, { size: 0.8 + (e.weight || 0) * 0.5, color: EDGE })
|
|
||||||
})
|
|
||||||
forceAtlas2.assign(graph, { iterations: 220, settings: forceAtlas2.inferSettings(graph) })
|
|
||||||
graphRef.current = graph
|
|
||||||
|
|
||||||
const renderer = new Sigma(graph, containerRef.current, {
|
const activeNode = hovered || selected
|
||||||
renderLabels: true,
|
|
||||||
labelColor: { color: "#e2e8f0" },
|
|
||||||
labelSize: 11,
|
|
||||||
labelWeight: "600",
|
|
||||||
labelDensity: 0.5,
|
|
||||||
labelRenderedSizeThreshold: N > 120 ? 12 : 0, // bei vielen Knoten nur große labeln (entklumpen)
|
|
||||||
defaultEdgeColor: EDGE,
|
|
||||||
minCameraRatio: 0.1,
|
|
||||||
maxCameraRatio: 4,
|
|
||||||
nodeReducer: (id, attrs) => {
|
|
||||||
const a = active.current
|
|
||||||
const isSel = id === selectedRef.current
|
|
||||||
|
|
||||||
if (!a) {
|
|
||||||
if (isSel) return { ...attrs, size: attrs.size * 1.35, highlighted: true }
|
|
||||||
return attrs
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id === a) {
|
|
||||||
return { ...attrs, size: attrs.size * 1.45, highlighted: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (graph.areNeighbors(a, id)) {
|
|
||||||
if (isSel) return { ...attrs, size: attrs.size * 1.35, highlighted: true }
|
|
||||||
return attrs
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...attrs, color: DIM_NODE, label: "" }
|
|
||||||
},
|
|
||||||
edgeReducer: (edge, attrs) => {
|
|
||||||
const a = active.current
|
|
||||||
if (!a) return attrs
|
|
||||||
const ext = graph.extremities(edge)
|
|
||||||
if (ext[0] === a || ext[1] === a) return { ...attrs, color: EDGE_HI, size: (attrs.size || 1) * 2.2 }
|
|
||||||
return { ...attrs, color: DIM_EDGE }
|
|
||||||
},
|
|
||||||
})
|
|
||||||
sigmaRef.current = renderer
|
|
||||||
let dragged: string | null = null // gerade gezogener Knoten (für Hover/Drag-Logik)
|
|
||||||
|
|
||||||
renderer.on("clickNode", ({ node }) => setSelected(node))
|
// Layout zurücksetzen/neu anschubsen
|
||||||
renderer.on("clickStage", () => setSelected(null))
|
|
||||||
renderer.on("enterNode", ({ node }) => { if (!dragged) { active.current = node; renderer.refresh() } containerRef.current!.style.cursor = dragged ? "grabbing" : "grab" })
|
|
||||||
renderer.on("leaveNode", () => { if (!dragged) { active.current = selectedRef.current; renderer.refresh(); containerRef.current!.style.cursor = "default" } })
|
|
||||||
|
|
||||||
// Knoten ziehen (Sigma hat kein eingebautes Dragging → Standard-Muster mit dem Mouse-Captor).
|
|
||||||
renderer.on("downNode", (e) => {
|
|
||||||
dragged = e.node
|
|
||||||
graph.setNodeAttribute(dragged, "highlighted", true)
|
|
||||||
if (!renderer.getCustomBBox()) renderer.setCustomBBox(renderer.getBBox())
|
|
||||||
containerRef.current!.style.cursor = "grabbing"
|
|
||||||
})
|
|
||||||
const mc = renderer.getMouseCaptor()
|
|
||||||
mc.on("mousemovebody", (e) => {
|
|
||||||
if (!dragged) return
|
|
||||||
const pos = renderer.viewportToGraph(e)
|
|
||||||
graph.setNodeAttribute(dragged, "x", pos.x)
|
|
||||||
graph.setNodeAttribute(dragged, "y", pos.y)
|
|
||||||
e.preventSigmaDefault(); e.original.preventDefault(); e.original.stopPropagation()
|
|
||||||
})
|
|
||||||
mc.on("mouseup", () => {
|
|
||||||
if (dragged) graph.removeNodeAttribute(dragged, "highlighted")
|
|
||||||
dragged = null
|
|
||||||
if (containerRef.current) containerRef.current.style.cursor = "default"
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => { renderer.kill(); sigmaRef.current = null; graphRef.current = null }
|
|
||||||
}, [data, degree])
|
|
||||||
|
|
||||||
// Auswahl spiegelt die Hervorhebung (auch ohne Hover).
|
|
||||||
useEffect(() => {
|
|
||||||
selectedRef.current = selected
|
|
||||||
active.current = selected
|
|
||||||
sigmaRef.current?.refresh()
|
|
||||||
}, [selected])
|
|
||||||
|
|
||||||
// Layout neu berechnen (nach manuellem Verschieben „aufräumen").
|
|
||||||
const relayout = () => {
|
const relayout = () => {
|
||||||
const g = graphRef.current, s = sigmaRef.current
|
if (!fgRef.current) return
|
||||||
if (!g || !s) return
|
fgRef.current.zoomToFit(400, 40)
|
||||||
forceAtlas2.assign(g, { iterations: 220, settings: forceAtlas2.inferSettings(g) })
|
|
||||||
s.refresh()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sel = selected ? data.nodes.find((n) => n.id === selected) ?? null : null
|
const sel = selected ? data.nodes.find((n) => n.id === selected) ?? null : null
|
||||||
@@ -162,12 +96,139 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
|
|||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-3">
|
<div className="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-3">
|
||||||
<div className="relative h-[calc(100vh-13rem)] min-h-[560px] rounded-2xl border border-border/60 overflow-hidden"
|
<div className="relative h-[calc(100vh-13rem)] min-h-[560px] rounded-2xl border border-border/60 overflow-hidden"
|
||||||
style={{ background: "radial-gradient(circle at center, rgba(59, 130, 246, 0.06) 0%, #030712 100%)" }}>
|
ref={containerRef}
|
||||||
<div ref={containerRef} className="absolute inset-0" />
|
style={{ background: "radial-gradient(circle at center, rgba(59, 130, 246, 0.08) 0%, #030712 100%)" }}>
|
||||||
|
|
||||||
|
<ForceGraph2D
|
||||||
|
ref={fgRef}
|
||||||
|
width={dimensions.width}
|
||||||
|
height={dimensions.height}
|
||||||
|
graphData={graphData}
|
||||||
|
backgroundColor="rgba(0,0,0,0)"
|
||||||
|
nodeRelSize={4}
|
||||||
|
|
||||||
|
// Hover- und Klick-Logik
|
||||||
|
onNodeClick={(node: any) => setSelected(node.id)}
|
||||||
|
onBackgroundClick={() => setSelected(null)}
|
||||||
|
onNodeHover={(node: any) => {
|
||||||
|
setHovered(node ? node.id : null)
|
||||||
|
containerRef.current!.style.cursor = node ? "pointer" : "default"
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Kanten-Animationen & Partikelfluss
|
||||||
|
linkColor={(link: any) => {
|
||||||
|
const sId = typeof link.source === "object" ? link.source.id : link.source
|
||||||
|
const tId = typeof link.target === "object" ? link.target.id : link.target
|
||||||
|
if (activeNode === sId || activeNode === tId) return EDGE_HI
|
||||||
|
return activeNode ? DIM_EDGE : EDGE
|
||||||
|
}}
|
||||||
|
linkWidth={(link: any) => {
|
||||||
|
const sId = typeof link.source === "object" ? link.source.id : link.source
|
||||||
|
const tId = typeof link.target === "object" ? link.target.id : link.target
|
||||||
|
return (activeNode === sId || activeNode === tId) ? 2.2 : 0.8
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Daten-Partikel fließen auf den aktiven Verbindungen
|
||||||
|
linkDirectionalParticles={(link: any) => {
|
||||||
|
const sId = typeof link.source === "object" ? link.source.id : link.source
|
||||||
|
const tId = typeof link.target === "object" ? link.target.id : link.target
|
||||||
|
return (activeNode === sId || activeNode === tId) ? 4 : 0
|
||||||
|
}}
|
||||||
|
linkDirectionalParticleWidth={2.2}
|
||||||
|
linkDirectionalParticleSpeed={0.006}
|
||||||
|
|
||||||
|
// Custom Canvas Rendering für glühende Knoten und 100% lesbare Text-Pillen
|
||||||
|
nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
|
||||||
|
const size = 5.5 + Math.min(degree[node.id] || 0, 12) * 0.9
|
||||||
|
const color = CAT_COLOR[node.category] || "#64748b"
|
||||||
|
const isHovered = node.id === hovered
|
||||||
|
const isSelected = node.id === selected
|
||||||
|
|
||||||
|
// Abgedunkelter Zustand, wenn ein anderer Knoten aktiv ist
|
||||||
|
const isDimmed = activeNode && node.id !== activeNode && !fgRef.current?.getGraphData()?.links.some((l: any) => {
|
||||||
|
const sId = typeof l.source === "object" ? l.source.id : l.source
|
||||||
|
const tId = typeof l.target === "object" ? l.target.id : l.target
|
||||||
|
return (sId === activeNode && tId === node.id) || (tId === activeNode && sId === node.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
const finalColor = isDimmed ? "#1e293b" : color
|
||||||
|
|
||||||
|
// 1. Zeichne weichen Glow-Schatten
|
||||||
|
if (!isDimmed) {
|
||||||
|
ctx.shadowColor = color
|
||||||
|
ctx.shadowBlur = isHovered || isSelected ? 12 : 6
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Zeichne den Knoten-Kreis
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(node.x, node.y, size, 0, 2 * Math.PI, false)
|
||||||
|
ctx.fillStyle = finalColor
|
||||||
|
ctx.fill()
|
||||||
|
ctx.shadowBlur = 0 // Schatten zurücksetzen
|
||||||
|
|
||||||
|
// 3. Zeichne weißen Außenring bei Interaktivität
|
||||||
|
if (isHovered || isSelected) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(node.x, node.y, size + 2, 0, 2 * Math.PI, false)
|
||||||
|
ctx.strokeStyle = isSelected ? "#ffffff" : "rgba(255, 255, 255, 0.7)"
|
||||||
|
ctx.lineWidth = 1.5
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Typografie (Immer scharf & mitskalierend gezeichnet)
|
||||||
|
const label = node.content.length > 30 ? node.content.slice(0, 29) + "…" : node.content
|
||||||
|
const baseFontSize = isHovered || isSelected ? 10 : 9
|
||||||
|
const fontSize = baseFontSize / Math.max(0.6, globalScale) // Verhindert Riesen-Schriften beim Rauszoomen
|
||||||
|
|
||||||
|
ctx.font = `${isHovered || isSelected ? "bold" : "500"} ${fontSize}px "Space Grotesk", sans-serif`
|
||||||
|
const textWidth = ctx.measureText(label).width
|
||||||
|
const bpad = 4 / Math.max(0.6, globalScale)
|
||||||
|
|
||||||
|
// Render Label-Hintergrund für absolute Lesbarkeit
|
||||||
|
ctx.fillStyle = isDimmed ? "rgba(10, 15, 25, 0.45)" : "rgba(8, 12, 20, 0.85)"
|
||||||
|
ctx.beginPath()
|
||||||
|
|
||||||
|
// Zeichne abgerundete Pille unter dem Knoten
|
||||||
|
const rx = node.x - textWidth / 2 - bpad
|
||||||
|
const ry = node.y + size + (5 / Math.max(0.6, globalScale))
|
||||||
|
const rw = textWidth + bpad * 2
|
||||||
|
const rh = fontSize + bpad * 1.5
|
||||||
|
const rad = 4 / Math.max(0.6, globalScale)
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(rx + rad, ry)
|
||||||
|
ctx.lineTo(rx + rw - rad, ry)
|
||||||
|
ctx.quadraticCurveTo(rx + rw, ry, rx + rw, ry + rad)
|
||||||
|
ctx.lineTo(rx + rw, ry + rh - rad)
|
||||||
|
ctx.quadraticCurveTo(rx + rw, ry + rh, rx + rw - rad, ry + rh)
|
||||||
|
ctx.lineTo(rx + rad, ry + rh)
|
||||||
|
ctx.quadraticCurveTo(rx, ry + rh, rx, ry + rh - rad)
|
||||||
|
ctx.lineTo(rx, ry + rad)
|
||||||
|
ctx.quadraticCurveTo(rx, ry, rx + rad, ry)
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
// Feine Kontur für die Text-Pille
|
||||||
|
ctx.strokeStyle = isHovered || isSelected ? "rgba(255, 255, 255, 0.15)" : "rgba(255, 255, 255, 0.05)"
|
||||||
|
ctx.lineWidth = 0.5 / Math.max(0.6, globalScale)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// Text zeichnen
|
||||||
|
ctx.textAlign = "center"
|
||||||
|
ctx.textBaseline = "top"
|
||||||
|
ctx.fillStyle = isDimmed
|
||||||
|
? "#475569"
|
||||||
|
: (isHovered || isSelected ? "#ffffff" : "#cbd5e1")
|
||||||
|
|
||||||
|
ctx.fillText(label, node.x, node.y + size + (6 / Math.max(0.6, globalScale)))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<button onClick={relayout} title="Knoten neu anordnen"
|
<button onClick={relayout} title="Knoten neu anordnen"
|
||||||
className="absolute right-3 top-3 z-10 flex items-center gap-1.5 rounded-lg border border-border/60 bg-background/50 px-2.5 py-1.5 text-[11px] text-muted-foreground hover:text-foreground backdrop-blur-sm transition-colors">
|
className="absolute right-3 top-3 z-10 flex items-center gap-1.5 rounded-lg border border-border/60 bg-background/50 px-2.5 py-1.5 text-[11px] text-muted-foreground hover:text-foreground backdrop-blur-sm transition-colors cursor-pointer">
|
||||||
<RefreshCw className="h-3.5 w-3.5" /> Neu anordnen
|
<RefreshCw className="h-3.5 w-3.5" /> Zentrieren
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Legende */}
|
{/* Legende */}
|
||||||
<div className="absolute left-3 bottom-3 flex flex-wrap gap-2 rounded-lg bg-background/40 px-2.5 py-1.5 backdrop-blur-sm">
|
<div className="absolute left-3 bottom-3 flex flex-wrap gap-2 rounded-lg bg-background/40 px-2.5 py-1.5 backdrop-blur-sm">
|
||||||
{Object.entries(CAT_LABEL).map(([k, l]) => (
|
{Object.entries(CAT_LABEL).map(([k, l]) => (
|
||||||
@@ -197,14 +258,14 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
|
|||||||
<div className="flex flex-col gap-1.5 mb-4">
|
<div className="flex flex-col gap-1.5 mb-4">
|
||||||
{neighbors.length ? neighbors.map((n) => (
|
{neighbors.length ? neighbors.map((n) => (
|
||||||
<button key={n.id} onClick={() => setSelected(n.id)}
|
<button key={n.id} onClick={() => setSelected(n.id)}
|
||||||
className="flex items-center gap-2 text-[11px] text-muted-foreground hover:text-foreground text-left transition-colors">
|
className="flex items-center gap-2 text-[11px] text-muted-foreground hover:text-foreground text-left transition-colors cursor-pointer">
|
||||||
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ background: CAT_COLOR[n.category] }} />
|
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ background: CAT_COLOR[n.category] }} />
|
||||||
<span className="truncate">{n.content}</span>
|
<span className="truncate">{n.content}</span>
|
||||||
</button>
|
</button>
|
||||||
)) : <span className="text-[11px] text-muted-foreground/60">—</span>}
|
)) : <span className="text-[11px] text-muted-foreground/60">—</span>}
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => { onDelete(sel.id); setSelected(null) }}
|
<button onClick={() => { onDelete(sel.id); setSelected(null) }}
|
||||||
className="flex items-center gap-1.5 text-[11px] text-red-400 border border-red-500/20 rounded-lg px-2.5 py-1.5 hover:bg-red-500/5 transition-all">
|
className="flex items-center gap-1.5 text-[11px] text-red-400 border border-red-500/20 rounded-lg px-2.5 py-1.5 hover:bg-red-500/5 transition-all cursor-pointer">
|
||||||
<Trash2 className="h-3.5 w-3.5" /> vergessen
|
<Trash2 className="h-3.5 w-3.5" /> vergessen
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user