|
4 | 4 | <meta charset="UTF-8" /> |
5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
6 | 6 | <title>Hypergraph Visualization</title> |
7 | | - <script src="https://unpkg.com/@antv/g6@5/dist/g6.min.js"></script> |
| 7 | + <!-- <script src="https://unpkg.com/@antv/g6@5/dist/g6.min.js"></script> --> |
| 8 | + <!-- 使用修改过的g6 --> |
| 9 | + <script src="http://hub.dappwind.com/static/g6.min.js"></script> |
| 10 | + <!-- <script src="./g6.min.js"></script> --> |
8 | 11 | <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script> |
9 | 12 | <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> |
10 | 13 | <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> |
|
70 | 73 | ); |
71 | 74 | const [visualizationMode, setVisualizationMode] = useState("hyper"); // 'hyper' or 'graph' |
72 | 75 | const [graphVersion, setGraphVersion] = useState(0); |
73 | | - |
| 76 | + const [hoverHyperedge, setHoverHyperedge] = useState(null); |
| 77 | + const [hoverNode, setHoverNode] = useState(null); |
74 | 78 | // 搜索功能 |
75 | 79 | useEffect(() => { |
76 | 80 | if (!searchTerm.trim()) { |
|
140 | 144 | const graphDataFormatted = useMemo(() => { |
141 | 145 | if (!graphData) return null; |
142 | 146 |
|
143 | | - const hyperData = { nodes: [], edges: [] }; |
| 147 | + const hyperData = { nodes: [], edges: [], hyperEdges: [] }; |
144 | 148 | const plugins = []; |
145 | 149 |
|
146 | 150 | // 添加顶点 |
|
222 | 226 | weight: edge.weight || nodes.length, |
223 | 227 | ...createStyle(colors[i % colors.length]), |
224 | 228 | }); |
| 229 | + |
| 230 | + hyperData.hyperEdges.push({ |
| 231 | + id: key, |
| 232 | + ...edge, |
| 233 | + members: nodes, |
| 234 | + }); |
225 | 235 | } |
226 | 236 | } |
227 | 237 |
|
|
301 | 311 | gravity: 20, |
302 | 312 | linkDistance: visualizationMode === "graph" ? 100 : 150, |
303 | 313 | }, |
| 314 | + autoFit: "center", |
304 | 315 | }; |
305 | 316 | }, [graphData, selectedVertex, visualizationMode]); |
306 | 317 |
|
|
333 | 344 | graphRef.current = graph; |
334 | 345 | graphRef.current.render(); |
335 | 346 |
|
336 | | - // 添加节点点击事件 |
337 | | - graph.on("node:click", (e) => { |
338 | | - const { itemId } = e; |
339 | | - console.log("Clicked node:", itemId); |
| 347 | + graph.on("pointerover", (e) => { |
| 348 | + // 如果e.target是hyperEdge,则显示自定义tooltip |
| 349 | + if (e.targetType === "bubble-sets") { |
| 350 | + const target = e.target.options; |
| 351 | + setHoverHyperedge({ |
| 352 | + keywords: target.keywords || "", |
| 353 | + summary: target.summary || "", |
| 354 | + members: Array.isArray(target.members) ? target.members : [], |
| 355 | + weight: target.weight, |
| 356 | + }); |
| 357 | + } |
| 358 | + if (e.targetType === "node") { |
| 359 | + const target = graphDataFormatted.data.nodes.find( |
| 360 | + (node) => node.id === e.target.id |
| 361 | + ); |
| 362 | + setHoverNode(target); |
| 363 | + } |
340 | 364 | }); |
341 | 365 |
|
342 | 366 | // 添加窗口大小变化监听 |
|
359 | 383 | if (containerRef.current) { |
360 | 384 | containerRef.current.innerHTML = ""; |
361 | 385 | } |
| 386 | + // 清理右侧悬停信息 |
| 387 | + setHoverHyperedge(null); |
362 | 388 | }; |
363 | 389 | }, [graphDataFormatted, visualizationMode]); |
364 | 390 |
|
| 391 | + // 默认选中“最大的”节点与超边(首次或每次数据/模式变化时) |
| 392 | + useEffect(() => { |
| 393 | + if (!graphDataFormatted) return; |
| 394 | + |
| 395 | + const nodes = graphDataFormatted?.data?.nodes || []; |
| 396 | + if (!hoverNode && nodes.length > 0) { |
| 397 | + const nodeWithMax = nodes.reduce((best, cur) => { |
| 398 | + const bestDeg = typeof best.degree === "number" ? best.degree : 0; |
| 399 | + const curDeg = typeof cur.degree === "number" ? cur.degree : 0; |
| 400 | + return curDeg > bestDeg ? cur : best; |
| 401 | + }, nodes[0]); |
| 402 | + setHoverNode(nodeWithMax); |
| 403 | + } |
| 404 | + |
| 405 | + if (visualizationMode === "hyper") { |
| 406 | + const hyperEdges = graphDataFormatted?.data?.hyperEdges || []; |
| 407 | + if (!hoverHyperedge && hyperEdges.length > 0) { |
| 408 | + const hyperWithMax = hyperEdges.reduce((best, cur) => { |
| 409 | + const bestVal = |
| 410 | + typeof best.weight === "number" |
| 411 | + ? best.weight |
| 412 | + : Array.isArray(best.members) |
| 413 | + ? best.members.length |
| 414 | + : 0; |
| 415 | + const curVal = |
| 416 | + typeof cur.weight === "number" |
| 417 | + ? cur.weight |
| 418 | + : Array.isArray(cur.members) |
| 419 | + ? cur.members.length |
| 420 | + : 0; |
| 421 | + return curVal > bestVal ? cur : best; |
| 422 | + }, hyperEdges[0]); |
| 423 | + |
| 424 | + setHoverHyperedge({ |
| 425 | + keywords: hyperWithMax.keywords || "", |
| 426 | + summary: hyperWithMax.summary || "", |
| 427 | + members: Array.isArray(hyperWithMax.members) |
| 428 | + ? hyperWithMax.members |
| 429 | + : [], |
| 430 | + weight: |
| 431 | + typeof hyperWithMax.weight === "number" |
| 432 | + ? hyperWithMax.weight |
| 433 | + : Array.isArray(hyperWithMax.members) |
| 434 | + ? hyperWithMax.members.length |
| 435 | + : undefined, |
| 436 | + }); |
| 437 | + } |
| 438 | + } |
| 439 | + }, [graphDataFormatted, visualizationMode, hoverNode, hoverHyperedge]); |
| 440 | + |
365 | 441 | return ( |
366 | 442 | <div className="flex h-screen bg-gradient-to-br from-gray-50 to-gray-100"> |
367 | | - <div className="w-80 h-screen overflow-hidden bg-white/95 backdrop-blur-sm border-r border-gray-200/50 p-6 shadow-xl"> |
| 443 | + <div className="w-80 h-screen overflow-hidden bg-white/95 backdrop-blur-sm border-r border-gray-200/50 p-6 shadow-xl shrink-0"> |
368 | 444 | <h2 className="text-2xl font-bold text-gray-800 mb-6 flex items-center"> |
369 | 445 | Hypergraph-DB |
370 | 446 | </h2> |
@@ -501,18 +577,20 @@ <h2 className="text-2xl font-bold text-gray-800 mb-6 flex items-center"> |
501 | 577 | </div> |
502 | 578 | <div className="text-sm text-gray-600 flex gap-2 items-center"> |
503 | 579 | {vertex.entity_type ? ( |
504 | | - <div className="flex items-center"> |
505 | | - <span className="font-medium">Type:</span> |
506 | | - <span className="ml-2 px-2 py-1 bg-gray-100 rounded text-xs"> |
507 | | - {vertex.entity_type} |
508 | | - </span> |
509 | | - </div>) : ( |
510 | | - <div className="flex items-center"> |
511 | | - <span className="font-medium">ID:</span> |
512 | | - <span className="ml-2 px-2 py-1 bg-gray-100 rounded text-xs"> |
513 | | - {vertex.id} |
514 | | - </span> |
515 | | - </div>)} |
| 580 | + <div className="flex items-center"> |
| 581 | + <span className="font-medium">Type:</span> |
| 582 | + <span className="ml-2 px-2 py-1 bg-gray-100 rounded text-xs"> |
| 583 | + {vertex.entity_type} |
| 584 | + </span> |
| 585 | + </div> |
| 586 | + ) : ( |
| 587 | + <div className="flex items-center"> |
| 588 | + <span className="font-medium">ID:</span> |
| 589 | + <span className="ml-2 px-2 py-1 bg-gray-100 rounded text-xs"> |
| 590 | + {vertex.id} |
| 591 | + </span> |
| 592 | + </div> |
| 593 | + )} |
516 | 594 | <div className="flex items-center"> |
517 | 595 | <span className="font-medium">Degree:</span> |
518 | 596 | <span className="ml-2 px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-semibold"> |
@@ -607,10 +685,109 @@ <h3 className="text-xl font-semibold text-gray-800 m-0"> |
607 | 685 | )} |
608 | 686 |
|
609 | 687 | {!loading && ( |
610 | | - <div |
611 | | - ref={containerRef} |
612 | | - className="w-full h-[calc(100vh-100px)] rounded-xl" |
613 | | - /> |
| 688 | + <div className="flex h-[calc(100vh-71px)]"> |
| 689 | + <div |
| 690 | + ref={containerRef} |
| 691 | + className="w-full rounded-xl h-full" |
| 692 | + /> |
| 693 | + {visualizationMode === "hyper" && ( |
| 694 | + <div className="shrink-0 w-72 h-full overflow-y-auto bg-white/95 backdrop-blur-sm border-l border-gray-200/50 p-3 shadow-xl overflow-y-auto"> |
| 695 | + <div className="text-lg font-bold text-gray-800 mb-3 pb-2 border-b-2 border-primary-500"> |
| 696 | + HyperGraph Detail |
| 697 | + </div> |
| 698 | + {hoverHyperedge ? ( |
| 699 | + <div className="text-sm text-gray-700 space-y-3 border-b-2 border-primary-500 pb-6"> |
| 700 | + <div className="text-base font-semibold text-gray-900"> |
| 701 | + HyperEdge |
| 702 | + </div> |
| 703 | + {hoverHyperedge.keywords && ( |
| 704 | + <div> |
| 705 | + <span className="font-medium">Keywords:</span> |
| 706 | + <div className="flex flex-wrap gap-2 mt-2"> |
| 707 | + {hoverHyperedge.keywords |
| 708 | + .split(",") |
| 709 | + .map((keyword) => ( |
| 710 | + <span className=" inline-block p-1 text-xs bg-gray-100 rounded"> |
| 711 | + {keyword} |
| 712 | + </span> |
| 713 | + ))} |
| 714 | + </div> |
| 715 | + </div> |
| 716 | + )} |
| 717 | + {hoverHyperedge.summary && ( |
| 718 | + <div> |
| 719 | + <div className="font-medium">Summary:</div> |
| 720 | + <div className="mt-1 text-gray-600 bg-gray-50 p-2 rounded"> |
| 721 | + {hoverHyperedge.summary} |
| 722 | + </div> |
| 723 | + </div> |
| 724 | + )} |
| 725 | + {hoverHyperedge.members?.length > 0 && ( |
| 726 | + <div> |
| 727 | + <div className="font-medium"> |
| 728 | + Members ({hoverHyperedge.members.length}): |
| 729 | + </div> |
| 730 | + <div className="flex flex-wrap gap-2 mt-2"> |
| 731 | + {hoverHyperedge.members.map((member) => ( |
| 732 | + <span className="p-1 text-xs bg-gray-100 rounded"> |
| 733 | + {member} |
| 734 | + </span> |
| 735 | + ))} |
| 736 | + </div> |
| 737 | + </div> |
| 738 | + )} |
| 739 | + </div> |
| 740 | + ) : ( |
| 741 | + <div className="text-sm text-gray-500"></div> |
| 742 | + )} |
| 743 | + {hoverNode ? ( |
| 744 | + <div className="text-sm text-gray-700 space-y-3 mt-4"> |
| 745 | + <div className="text-base font-semibold text-gray-900"> |
| 746 | + Node |
| 747 | + </div> |
| 748 | + {hoverNode.entity_name && ( |
| 749 | + <div> |
| 750 | + <span className="font-medium">Name:</span> |
| 751 | + <span className="ml-2"> |
| 752 | + {hoverNode.entity_name} |
| 753 | + </span> |
| 754 | + </div> |
| 755 | + )} |
| 756 | + {hoverNode.entity_type && ( |
| 757 | + <div> |
| 758 | + <span className="font-medium">Type:</span> |
| 759 | + <span className="ml-2 inline-block px-2 py-0.5 bg-gray-100 rounded"> |
| 760 | + {hoverNode.entity_type} |
| 761 | + </span> |
| 762 | + </div> |
| 763 | + )} |
| 764 | + {hoverNode.description && ( |
| 765 | + <div> |
| 766 | + <span className="font-medium"> |
| 767 | + Description: |
| 768 | + </span> |
| 769 | + <span className="inline-block px-2 py-0.5 text-xs"> |
| 770 | + {hoverNode.description} |
| 771 | + </span> |
| 772 | + </div> |
| 773 | + )} |
| 774 | + {hoverNode.additional_properties && ( |
| 775 | + <div> |
| 776 | + <span className="font-medium"> |
| 777 | + Additional Properties: |
| 778 | + </span> |
| 779 | + <span className="inline-block px-2 py-0.5 text-xs rounded"> |
| 780 | + {hoverNode.additional_properties} |
| 781 | + </span> |
| 782 | + </div> |
| 783 | + )} |
| 784 | + </div> |
| 785 | + ) : ( |
| 786 | + <div className="text-sm text-gray-500"></div> |
| 787 | + )} |
| 788 | + </div> |
| 789 | + )} |
| 790 | + </div> |
614 | 791 | )} |
615 | 792 | </div> |
616 | 793 | </div> |
|
0 commit comments