From 470896d8136afb7f2be3c23653504d2b1fd106df Mon Sep 17 00:00:00 2001 From: yuxz Date: Mon, 29 Sep 2025 18:37:01 +0800 Subject: [PATCH] feat: enhance hypergraph viewer with hover interactions integrate custom G6 library --- .gitignore | 1 + hyperdb/templates/hypergraph_viewer.html | 225 ++++++++++++++++++++--- 2 files changed, 202 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index fe0ed00..90df6d4 100644 --- a/.gitignore +++ b/.gitignore @@ -171,6 +171,7 @@ cython_debug/ #.idea/ hyperdb/templates/data.js +hyperdb/templates/g6.min.js # UV package manager .venv/ diff --git a/hyperdb/templates/hypergraph_viewer.html b/hyperdb/templates/hypergraph_viewer.html index 820864d..2b23b89 100644 --- a/hyperdb/templates/hypergraph_viewer.html +++ b/hyperdb/templates/hypergraph_viewer.html @@ -4,7 +4,10 @@ Hypergraph Visualization - + + + + @@ -70,7 +73,8 @@ ); const [visualizationMode, setVisualizationMode] = useState("hyper"); // 'hyper' or 'graph' const [graphVersion, setGraphVersion] = useState(0); - + const [hoverHyperedge, setHoverHyperedge] = useState(null); + const [hoverNode, setHoverNode] = useState(null); // 搜索功能 useEffect(() => { if (!searchTerm.trim()) { @@ -140,7 +144,7 @@ const graphDataFormatted = useMemo(() => { if (!graphData) return null; - const hyperData = { nodes: [], edges: [] }; + const hyperData = { nodes: [], edges: [], hyperEdges: [] }; const plugins = []; // 添加顶点 @@ -220,6 +224,12 @@ weight: edge.weight || nodes.length, ...createStyle(colors[i % colors.length]), }); + + hyperData.hyperEdges.push({ + id: key, + ...edge, + members: nodes, + }); } } @@ -299,6 +309,7 @@ gravity: 20, linkDistance: visualizationMode === "graph" ? 100 : 150, }, + autoFit: "center", }; }, [graphData, selectedVertex, visualizationMode]); @@ -331,10 +342,23 @@ graphRef.current = graph; graphRef.current.render(); - // 添加节点点击事件 - graph.on("node:click", (e) => { - const { itemId } = e; - console.log("Clicked node:", itemId); + graph.on("pointerover", (e) => { + // 如果e.target是hyperEdge,则显示自定义tooltip + if (e.targetType === "bubble-sets") { + const target = e.target.options; + setHoverHyperedge({ + keywords: target.keywords || "", + summary: target.summary || "", + members: Array.isArray(target.members) ? target.members : [], + weight: target.weight, + }); + } + if (e.targetType === "node") { + const target = graphDataFormatted.data.nodes.find( + (node) => node.id === e.target.id + ); + setHoverNode(target); + } }); // 添加窗口大小变化监听 @@ -357,12 +381,64 @@ if (containerRef.current) { containerRef.current.innerHTML = ""; } + // 清理右侧悬停信息 + setHoverHyperedge(null); }; }, [graphDataFormatted, visualizationMode]); + // 默认选中“最大的”节点与超边(首次或每次数据/模式变化时) + useEffect(() => { + if (!graphDataFormatted) return; + + const nodes = graphDataFormatted?.data?.nodes || []; + if (!hoverNode && nodes.length > 0) { + const nodeWithMax = nodes.reduce((best, cur) => { + const bestDeg = typeof best.degree === "number" ? best.degree : 0; + const curDeg = typeof cur.degree === "number" ? cur.degree : 0; + return curDeg > bestDeg ? cur : best; + }, nodes[0]); + setHoverNode(nodeWithMax); + } + + if (visualizationMode === "hyper") { + const hyperEdges = graphDataFormatted?.data?.hyperEdges || []; + if (!hoverHyperedge && hyperEdges.length > 0) { + const hyperWithMax = hyperEdges.reduce((best, cur) => { + const bestVal = + typeof best.weight === "number" + ? best.weight + : Array.isArray(best.members) + ? best.members.length + : 0; + const curVal = + typeof cur.weight === "number" + ? cur.weight + : Array.isArray(cur.members) + ? cur.members.length + : 0; + return curVal > bestVal ? cur : best; + }, hyperEdges[0]); + + setHoverHyperedge({ + keywords: hyperWithMax.keywords || "", + summary: hyperWithMax.summary || "", + members: Array.isArray(hyperWithMax.members) + ? hyperWithMax.members + : [], + weight: + typeof hyperWithMax.weight === "number" + ? hyperWithMax.weight + : Array.isArray(hyperWithMax.members) + ? hyperWithMax.members.length + : undefined, + }); + } + } + }, [graphDataFormatted, visualizationMode, hoverNode, hoverHyperedge]); + return (
-
+

Hypergraph-DB

@@ -499,18 +575,20 @@

{vertex.entity_type ? ( -
- Type: - - {vertex.entity_type} - -
) : ( -
- ID: - - {vertex.id} - -
)} +
+ Type: + + {vertex.entity_type} + +
+ ) : ( +
+ ID: + + {vertex.id} + +
+ )}
Degree: @@ -605,10 +683,109 @@

)} {!loading && ( -
+
+
+ {visualizationMode === "hyper" && ( +
+
+ HyperGraph Detail +
+ {hoverHyperedge ? ( +
+
+ HyperEdge +
+ {hoverHyperedge.keywords && ( +
+ Keywords: +
+ {hoverHyperedge.keywords + .split(",") + .map((keyword) => ( + + {keyword} + + ))} +
+
+ )} + {hoverHyperedge.summary && ( +
+
Summary:
+
+ {hoverHyperedge.summary} +
+
+ )} + {hoverHyperedge.members?.length > 0 && ( +
+
+ Members ({hoverHyperedge.members.length}): +
+
+ {hoverHyperedge.members.map((member) => ( + + {member} + + ))} +
+
+ )} +
+ ) : ( +
+ )} + {hoverNode ? ( +
+
+ Node +
+ {hoverNode.entity_name && ( +
+ Name: + + {hoverNode.entity_name} + +
+ )} + {hoverNode.entity_type && ( +
+ Type: + + {hoverNode.entity_type} + +
+ )} + {hoverNode.description && ( +
+ + Description: + + + {hoverNode.description} + +
+ )} + {hoverNode.additional_properties && ( +
+ + Additional Properties: + + + {hoverNode.additional_properties} + +
+ )} +
+ ) : ( +
+ )} +
+ )} +
)}