Skip to content

Commit 4291451

Browse files
authored
Merge pull request #5 from yuxizhe/main
feat: enhance hypergraph viewer with hover interactions and integrate custom G6 library
2 parents 6713bff + 470896d commit 4291451

File tree

2 files changed

+202
-24
lines changed

2 files changed

+202
-24
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ cython_debug/
171171
#.idea/
172172

173173
hyperdb/templates/data.js
174+
hyperdb/templates/g6.min.js
174175

175176
# UV package manager
176177
.venv/

hyperdb/templates/hypergraph_viewer.html

Lines changed: 201 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<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> -->
811
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
912
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
1013
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
@@ -70,7 +73,8 @@
7073
);
7174
const [visualizationMode, setVisualizationMode] = useState("hyper"); // 'hyper' or 'graph'
7275
const [graphVersion, setGraphVersion] = useState(0);
73-
76+
const [hoverHyperedge, setHoverHyperedge] = useState(null);
77+
const [hoverNode, setHoverNode] = useState(null);
7478
// 搜索功能
7579
useEffect(() => {
7680
if (!searchTerm.trim()) {
@@ -140,7 +144,7 @@
140144
const graphDataFormatted = useMemo(() => {
141145
if (!graphData) return null;
142146

143-
const hyperData = { nodes: [], edges: [] };
147+
const hyperData = { nodes: [], edges: [], hyperEdges: [] };
144148
const plugins = [];
145149

146150
// 添加顶点
@@ -222,6 +226,12 @@
222226
weight: edge.weight || nodes.length,
223227
...createStyle(colors[i % colors.length]),
224228
});
229+
230+
hyperData.hyperEdges.push({
231+
id: key,
232+
...edge,
233+
members: nodes,
234+
});
225235
}
226236
}
227237

@@ -301,6 +311,7 @@
301311
gravity: 20,
302312
linkDistance: visualizationMode === "graph" ? 100 : 150,
303313
},
314+
autoFit: "center",
304315
};
305316
}, [graphData, selectedVertex, visualizationMode]);
306317

@@ -333,10 +344,23 @@
333344
graphRef.current = graph;
334345
graphRef.current.render();
335346

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+
}
340364
});
341365

342366
// 添加窗口大小变化监听
@@ -359,12 +383,64 @@
359383
if (containerRef.current) {
360384
containerRef.current.innerHTML = "";
361385
}
386+
// 清理右侧悬停信息
387+
setHoverHyperedge(null);
362388
};
363389
}, [graphDataFormatted, visualizationMode]);
364390

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+
365441
return (
366442
<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">
368444
<h2 className="text-2xl font-bold text-gray-800 mb-6 flex items-center">
369445
Hypergraph-DB
370446
</h2>
@@ -501,18 +577,20 @@ <h2 className="text-2xl font-bold text-gray-800 mb-6 flex items-center">
501577
</div>
502578
<div className="text-sm text-gray-600 flex gap-2 items-center">
503579
{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+
)}
516594
<div className="flex items-center">
517595
<span className="font-medium">Degree:</span>
518596
<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">
607685
)}
608686

609687
{!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>
614791
)}
615792
</div>
616793
</div>

0 commit comments

Comments
 (0)