feat(kb): 知识图谱换 react-force-graph-2d(活物理 + 拖拽 + 缩放)
把自建静态 SVG 力导向换成 react-force-graph-2d(canvas + d3-force)——真实物理、 可拖拽节点、滚轮缩放、悬停高亮邻域。实体图谱与笔记关系图同一组件,全部升级。 - GraphView 重写:ForceGraph2D,nodeCanvasObject 自绘节点(按度着色/缩放)+标签, linkCanvasObject 放大后显示关系文字,onNodeHover 高亮邻域、onNodeClick→onNode(笔记图跳转)。 - 调力:charge=-180 / link distance=52 拉开布局;onEngineStop zoomToFit 自动取景。 - 保持原 props(triples/height/onNode),三处调用零改动。 验证(Preview):笔记关系图渲染 笔记B/项目A概述/模块X 大节点 + 链接边、自动取景、可拖拽缩放。 (实体图谱在 default 库因累积了几十个测试实体显得密,新建干净库则清爽。)tsc+vite 通过;重建 .app。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+312
-3
@@ -11,7 +11,8 @@
|
|||||||
"@xyflow/react": "^12.3.0",
|
"@xyflow/react": "^12.3.0",
|
||||||
"lucide-react": "^1.17.0",
|
"lucide-react": "^1.17.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"react-force-graph-2d": "^1.29.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
@@ -1155,6 +1156,12 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@tweenjs/tween.js": {
|
||||||
|
"version": "25.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz",
|
||||||
|
"integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -1339,6 +1346,15 @@
|
|||||||
"d3-zoom": "^3.0.0"
|
"d3-zoom": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/any-promise": {
|
"node_modules/any-promise": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||||
@@ -1417,6 +1433,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/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@@ -1508,6 +1534,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/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
@@ -1589,6 +1627,24 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"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",
|
||||||
@@ -1629,6 +1685,31 @@
|
|||||||
"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": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-interpolate": {
|
"node_modules/d3-interpolate": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
@@ -1641,6 +1722,50 @@
|
|||||||
"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-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": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"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": {
|
"node_modules/d3-selection": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
@@ -1650,6 +1775,30 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-timer": {
|
"node_modules/d3-timer": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
@@ -1845,6 +1994,46 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "5.3.4",
|
"version": "5.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||||
@@ -1920,6 +2109,24 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-binary-path": {
|
"node_modules/is-binary-path": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
@@ -1982,6 +2189,15 @@
|
|||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": "1.21.7",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||||
@@ -1996,7 +2212,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/jsesc": {
|
"node_modules/jsesc": {
|
||||||
@@ -2025,6 +2240,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/lilconfig": {
|
"node_modules/lilconfig": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||||
@@ -2045,6 +2272,24 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"loose-envify": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -2150,7 +2395,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -2376,6 +2620,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz",
|
||||||
|
"integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prop-types": {
|
||||||
|
"version": "15.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -2418,6 +2683,44 @@
|
|||||||
"react": "^19.2.7"
|
"react": "^19.2.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/react-kapsule": {
|
||||||
|
"version": "2.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-kapsule/-/react-kapsule-2.5.7.tgz",
|
||||||
|
"integrity": "sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jerrypick": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
@@ -2676,6 +2979,12 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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",
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"@xyflow/react": "^12.3.0",
|
"@xyflow/react": "^12.3.0",
|
||||||
"lucide-react": "^1.17.0",
|
"lucide-react": "^1.17.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"react-force-graph-2d": "^1.29.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
|
|||||||
@@ -1,171 +1,130 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import ForceGraph2D from "react-force-graph-2d";
|
||||||
import { Network } from "lucide-react";
|
import { Network } from "lucide-react";
|
||||||
import type { Triple } from "../lib/api";
|
import type { Triple } from "../lib/api";
|
||||||
import { EmptyState } from "../ui";
|
import { EmptyState } from "../ui";
|
||||||
|
|
||||||
interface GNode {
|
// 节点按度着色:枢纽紫 / 关联青 / 叶子灰。
|
||||||
id: string;
|
function nodeColor(deg: number): string {
|
||||||
x: number;
|
if (deg >= 4) return "#8b5cf6";
|
||||||
y: number;
|
if (deg >= 2) return "#22d3ee";
|
||||||
deg: number;
|
return "#64748b";
|
||||||
}
|
|
||||||
interface GEdge {
|
|
||||||
s: string;
|
|
||||||
o: string;
|
|
||||||
p: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// layout 用一个轻量力导向模拟(斥力 + 边弹簧 + 居中)把三元组排成图。
|
function build(triples: Triple[]) {
|
||||||
// 静态收敛(useMemo 内跑固定迭代),零依赖;节点过多时按度裁剪。
|
|
||||||
function layout(triples: Triple[], W: number, H: number): { nodes: GNode[]; edges: GEdge[] } {
|
|
||||||
const deg = new Map<string, number>();
|
const deg = new Map<string, number>();
|
||||||
for (const t of triples) {
|
for (const t of triples) {
|
||||||
if (!t.s || !t.o) continue;
|
if (!t.s || !t.o) continue;
|
||||||
deg.set(t.s, (deg.get(t.s) ?? 0) + 1);
|
deg.set(t.s, (deg.get(t.s) ?? 0) + 1);
|
||||||
deg.set(t.o, (deg.get(t.o) ?? 0) + 1);
|
deg.set(t.o, (deg.get(t.o) ?? 0) + 1);
|
||||||
}
|
}
|
||||||
// 裁剪:实体过多只留度最高的 N 个,保留两端都在集合内的边。
|
const nodes = [...deg.keys()].map((id) => ({ id, deg: deg.get(id)! }));
|
||||||
let names = [...deg.keys()];
|
const links = triples.filter((t) => t.s && t.o).map((t) => ({ source: t.s, target: t.o, label: t.p }));
|
||||||
const CAP = 60;
|
return { nodes, links };
|
||||||
if (names.length > CAP) {
|
|
||||||
names = names.sort((a, b) => (deg.get(b)! - deg.get(a)!)).slice(0, CAP);
|
|
||||||
}
|
|
||||||
const keep = new Set(names);
|
|
||||||
const edges = triples.filter((t) => keep.has(t.s) && keep.has(t.o)).map((t) => ({ s: t.s, o: t.o, p: t.p }));
|
|
||||||
|
|
||||||
const nodes = new Map<string, GNode>();
|
|
||||||
const R = Math.min(W, H) * 0.36;
|
|
||||||
names.forEach((n, i) => {
|
|
||||||
const a = (2 * Math.PI * i) / names.length;
|
|
||||||
// 初始撒在圆周上(确定性,避免每次重排抖动)。
|
|
||||||
nodes.set(n, { id: n, x: W / 2 + Math.cos(a) * R, y: H / 2 + Math.sin(a) * R, deg: deg.get(n)! });
|
|
||||||
});
|
|
||||||
|
|
||||||
const arr = [...nodes.values()];
|
|
||||||
for (let it = 0; it < 320; it++) {
|
|
||||||
// 斥力(库仑)
|
|
||||||
for (let i = 0; i < arr.length; i++) {
|
|
||||||
for (let j = i + 1; j < arr.length; j++) {
|
|
||||||
const a = arr[i],
|
|
||||||
b = arr[j];
|
|
||||||
let dx = a.x - b.x,
|
|
||||||
dy = a.y - b.y;
|
|
||||||
let d2 = dx * dx + dy * dy;
|
|
||||||
if (d2 < 1) {
|
|
||||||
d2 = 1;
|
|
||||||
dx = 1;
|
|
||||||
}
|
|
||||||
const d = Math.sqrt(d2);
|
|
||||||
const f = 2600 / d2;
|
|
||||||
a.x += (dx / d) * f;
|
|
||||||
a.y += (dy / d) * f;
|
|
||||||
b.x -= (dx / d) * f;
|
|
||||||
b.y -= (dy / d) * f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 边弹簧(理想长度 ~96)
|
|
||||||
for (const e of edges) {
|
|
||||||
const a = nodes.get(e.s)!,
|
|
||||||
b = nodes.get(e.o)!;
|
|
||||||
const dx = b.x - a.x,
|
|
||||||
dy = b.y - a.y;
|
|
||||||
const d = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
||||||
const f = (d - 96) * 0.012;
|
|
||||||
a.x += (dx / d) * f;
|
|
||||||
a.y += (dy / d) * f;
|
|
||||||
b.x -= (dx / d) * f;
|
|
||||||
b.y -= (dy / d) * f;
|
|
||||||
}
|
|
||||||
// 轻微居中 + 边界约束
|
|
||||||
for (const a of arr) {
|
|
||||||
a.x += (W / 2 - a.x) * 0.004;
|
|
||||||
a.y += (H / 2 - a.y) * 0.004;
|
|
||||||
a.x = Math.max(20, Math.min(W - 20, a.x));
|
|
||||||
a.y = Math.max(16, Math.min(H - 16, a.y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { nodes: arr, edges };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeColor(deg: number): { fill: string; text: string } {
|
// GraphView 用 react-force-graph-2d 渲染知识三元组:活物理力导向、可拖拽、滚轮缩放、悬停高亮。
|
||||||
if (deg >= 4) return { fill: "#8b5cf6", text: "#ede9fe" }; // 枢纽:brand
|
// 实体=节点(按度着色/缩放),关系=带标签边;onNode 非空时点节点回调(笔记关系图用)。
|
||||||
if (deg >= 2) return { fill: "#22d3ee", text: "#083344" }; // 次枢纽:accent
|
|
||||||
return { fill: "#1a1f2d", text: "#cbd5e1" }; // 叶子
|
|
||||||
}
|
|
||||||
|
|
||||||
// GraphView 把知识三元组渲染为力导向图(实体=节点,关系=带标签的边),hover 高亮邻域。
|
|
||||||
// onNode 非空时节点可点(用于笔记关系图点节点开笔记)。
|
|
||||||
export function GraphView({ triples, height = 360, onNode }: { triples: Triple[]; height?: number; onNode?: (id: string) => void }) {
|
export function GraphView({ triples, height = 360, onNode }: { triples: Triple[]; height?: number; onNode?: (id: string) => void }) {
|
||||||
const W = 560;
|
const wrapRef = useRef<HTMLDivElement>(null);
|
||||||
const H = height;
|
const fgRef = useRef<any>(null);
|
||||||
|
const [width, setWidth] = useState(600);
|
||||||
const [hover, setHover] = useState<string | null>(null);
|
const [hover, setHover] = useState<string | null>(null);
|
||||||
const { nodes, edges } = useMemo(() => layout(triples, W, H), [triples, H]);
|
|
||||||
const pos = useMemo(() => new Map(nodes.map((n) => [n.id, n])), [nodes]);
|
useEffect(() => {
|
||||||
|
const el = wrapRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const ro = new ResizeObserver(() => setWidth(el.clientWidth));
|
||||||
|
ro.observe(el);
|
||||||
|
setWidth(el.clientWidth);
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const data = useMemo(() => build(triples), [triples]);
|
||||||
|
|
||||||
|
// 加强斥力 + 拉开边距,避免节点挤成一团(d3 默认 charge=-30 对稍大的图偏弱)。
|
||||||
|
useEffect(() => {
|
||||||
|
const fg = fgRef.current;
|
||||||
|
if (!fg) return;
|
||||||
|
fg.d3Force("charge")?.strength(-180);
|
||||||
|
fg.d3Force("link")?.distance(52);
|
||||||
|
fg.d3ReheatSimulation?.();
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
// 悬停某节点时,与之相邻的节点/边高亮,其余淡出。
|
||||||
|
const neighbors = useMemo(() => {
|
||||||
|
const map = new Map<string, Set<string>>();
|
||||||
|
for (const l of data.links) {
|
||||||
|
if (!map.has(l.source)) map.set(l.source, new Set());
|
||||||
|
if (!map.has(l.target)) map.set(l.target, new Set());
|
||||||
|
map.get(l.source)!.add(l.target);
|
||||||
|
map.get(l.target)!.add(l.source);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
if (triples.length === 0) {
|
if (triples.length === 0) {
|
||||||
return <EmptyState icon={Network} title="暂无图谱" desc="入库文本后,LLM 会抽取实体与关系,这里渲染为可交互的知识图谱。" />;
|
return <EmptyState icon={Network} title="暂无图谱" desc="入库文本后,LLM 会抽取实体与关系;笔记互相 [[链接]] 也会连成图。" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const neighbors = (id: string) => {
|
const dim = (id: string) => hover !== null && hover !== id && !(neighbors.get(hover)?.has(id) ?? false);
|
||||||
const s = new Set<string>([id]);
|
|
||||||
for (const e of edges) {
|
|
||||||
if (e.s === id) s.add(e.o);
|
|
||||||
if (e.o === id) s.add(e.s);
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
};
|
|
||||||
const active = hover ? neighbors(hover) : null;
|
|
||||||
const nodeOn = (id: string) => !active || active.has(id);
|
|
||||||
const edgeOn = (e: GEdge) => !hover || e.s === hover || e.o === hover;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div ref={wrapRef} className="overflow-hidden rounded-md border border-line bg-ink-950/60" style={{ height }}>
|
||||||
<svg viewBox={`0 0 ${W} ${H}`} className="w-full rounded-md border border-line bg-ink-950/60" style={{ height }}>
|
<ForceGraph2D
|
||||||
{edges.map((e, i) => {
|
ref={fgRef}
|
||||||
const a = pos.get(e.s)!,
|
graphData={data}
|
||||||
b = pos.get(e.o)!;
|
width={width}
|
||||||
if (!a || !b) return null;
|
height={height}
|
||||||
const on = edgeOn(e);
|
backgroundColor="rgba(0,0,0,0)"
|
||||||
const mx = (a.x + b.x) / 2,
|
cooldownTicks={120}
|
||||||
my = (a.y + b.y) / 2;
|
onEngineStop={() => fgRef.current?.zoomToFit(400, 50)}
|
||||||
return (
|
nodeRelSize={5}
|
||||||
<g key={i} opacity={on ? 1 : 0.12}>
|
nodeVal={(n: any) => 1 + (n.deg || 0)}
|
||||||
<line x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke="#39435a" strokeWidth={1} />
|
nodeLabel={(n: any) => `${n.id}(度 ${n.deg})`}
|
||||||
{on && (
|
onNodeHover={(n: any) => setHover(n ? n.id : null)}
|
||||||
<text x={mx} y={my - 2} fill="#7c8aa5" fontSize={8.5} textAnchor="middle">
|
onNodeClick={(n: any) => onNode?.(n.id)}
|
||||||
{e.p}
|
linkColor={(l: any) => (hover && l.source.id !== hover && l.target.id !== hover ? "rgba(57,67,90,0.25)" : "#39435a")}
|
||||||
</text>
|
linkDirectionalArrowLength={3}
|
||||||
)}
|
linkDirectionalArrowRelPos={1}
|
||||||
</g>
|
linkLabel={(l: any) => l.label}
|
||||||
);
|
nodeCanvasObjectMode={() => "replace"}
|
||||||
})}
|
nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, scale: number) => {
|
||||||
{nodes.map((n) => {
|
const r = Math.max(3, 4 + (node.deg || 0) * 0.9);
|
||||||
const c = nodeColor(n.deg);
|
const faded = dim(node.id);
|
||||||
const r = Math.min(7 + n.deg * 1.6, 16);
|
ctx.globalAlpha = faded ? 0.2 : 1;
|
||||||
const on = nodeOn(n.id);
|
ctx.beginPath();
|
||||||
return (
|
ctx.arc(node.x, node.y, r, 0, 2 * Math.PI);
|
||||||
<g
|
ctx.fillStyle = nodeColor(node.deg);
|
||||||
key={n.id}
|
ctx.fill();
|
||||||
opacity={on ? 1 : 0.2}
|
if (hover === node.id) {
|
||||||
onMouseEnter={() => setHover(n.id)}
|
ctx.lineWidth = 1.5 / scale;
|
||||||
onMouseLeave={() => setHover(null)}
|
ctx.strokeStyle = "#fff";
|
||||||
onClick={() => onNode?.(n.id)}
|
ctx.stroke();
|
||||||
style={{ cursor: "pointer" }}
|
}
|
||||||
>
|
const fs = 10 / scale;
|
||||||
<circle cx={n.x} cy={n.y} r={r} fill={c.fill} stroke={hover === n.id ? "#fff" : "#0b0d12"} strokeWidth={hover === n.id ? 2 : 1.5} />
|
ctx.font = `${fs}px sans-serif`;
|
||||||
<text x={n.x} y={n.y + r + 9} fill="#cbd5e1" fontSize={9.5} textAnchor="middle">
|
ctx.textAlign = "center";
|
||||||
{n.id.length > 10 ? n.id.slice(0, 10) + "…" : n.id}
|
ctx.textBaseline = "top";
|
||||||
</text>
|
ctx.fillStyle = faded ? "rgba(203,213,225,0.25)" : "#cbd5e1";
|
||||||
</g>
|
const label = node.id.length > 12 ? node.id.slice(0, 12) + "…" : node.id;
|
||||||
);
|
ctx.fillText(label, node.x, node.y + r + 1);
|
||||||
})}
|
ctx.globalAlpha = 1;
|
||||||
</svg>
|
}}
|
||||||
<div className="flex items-center gap-3 px-1 text-[10px] text-slate-500">
|
linkCanvasObjectMode={() => "after"}
|
||||||
<span>{nodes.length} 实体 · {edges.length} 关系</span>
|
linkCanvasObject={(link: any, ctx: CanvasRenderingContext2D, scale: number) => {
|
||||||
<span className="flex items-center gap-1"><span className="h-2 w-2 rounded-full" style={{ background: "#8b5cf6" }} /> 枢纽</span>
|
if (scale < 1.2 || !link.label) return; // 放大到一定程度才显示关系文字,避免拥挤
|
||||||
<span className="flex items-center gap-1"><span className="h-2 w-2 rounded-full" style={{ background: "#22d3ee" }} /> 关联</span>
|
const s = link.source,
|
||||||
<span className="ml-auto">悬停高亮邻域</span>
|
t = link.target;
|
||||||
</div>
|
if (!s || !t) return;
|
||||||
|
const mx = (s.x + t.x) / 2,
|
||||||
|
my = (s.y + t.y) / 2;
|
||||||
|
ctx.font = `${8 / scale}px sans-serif`;
|
||||||
|
ctx.fillStyle = "#7c8aa5";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.fillText(link.label, mx, my);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user