Skip to content

Commit 470896d

Browse files
committed
feat: enhance hypergraph viewer with hover interactions integrate custom G6 library
1 parent 20c054d commit 470896d

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
// 添加顶点
@@ -220,6 +224,12 @@
220224
weight: edge.weight || nodes.length,
221225
...createStyle(colors[i % colors.length]),
222226
});
227+
228+
hyperData.hyperEdges.push({
229+
id: key,
230+
...edge,
231+
members: nodes,
232+
});
223233
}
224234
}
225235

@@ -299,6 +309,7 @@
299309
gravity: 20,
300310
linkDistance: visualizationMode === "graph" ? 100 : 150,
301311
},
312+
autoFit: "center",
302313
};
303314
}, [graphData, selectedVertex, visualizationMode]);
304315

@@ -331,10 +342,23 @@
331342
graphRef.current = graph;
332343
graphRef.current.render();
333344

334-
// 添加节点点击事件
335-
graph.on("node:click", (e) => {
336-
const { itemId } = e;
337-
console.log("Clicked node:", itemId);
345+
graph.on("pointerover", (e) => {
346+
// 如果e.target是hyperEdge,则显示自定义tooltip
347+
if (e.targetType === "bubble-sets") {
348+
const target = e.target.options;
349+
setHoverHyperedge({
350+
keywords: target.keywords || "",
351+
summary: target.summary || "",
352+
members: Array.isArray(target.members) ? target.members : [],
353+
weight: target.weight,
354+
});
355+
}
356+
if (e.targetType === "node") {
357+
const target = graphDataFormatted.data.nodes.find(
358+
(node) => node.id === e.target.id
359+
);
360+
setHoverNode(target);
361+
}
338362
});
339363

340364
// 添加窗口大小变化监听
@@ -357,12 +381,64 @@
357381
if (containerRef.current) {
358382
containerRef.current.innerHTML = "";
359383
}
384+
// 清理右侧悬停信息
385+
setHoverHyperedge(null);
360386
};
361387
}, [graphDataFormatted, visualizationMode]);
362388

389+
// 默认选中“最大的”节点与超边(首次或每次数据/模式变化时)
390+
useEffect(() => {
391+
if (!graphDataFormatted) return;
392+
393+
const nodes = graphDataFormatted?.data?.nodes || [];
394+
if (!hoverNode && nodes.length > 0) {
395+
const nodeWithMax = nodes.reduce((best, cur) => {
396+
const bestDeg = typeof best.degree === "number" ? best.degree : 0;
397+
const curDeg = typeof cur.degree === "number" ? cur.degree : 0;
398+
return curDeg > bestDeg ? cur : best;
399+
}, nodes[0]);
400+
setHoverNode(nodeWithMax);
401+
}
402+
403+
if (visualizationMode === "hyper") {
404+
const hyperEdges = graphDataFormatted?.data?.hyperEdges || [];
405+
if (!hoverHyperedge && hyperEdges.length > 0) {
406+
const hyperWithMax = hyperEdges.reduce((best, cur) => {
407+
const bestVal =
408+
typeof best.weight === "number"
409+
? best.weight
410+
: Array.isArray(best.members)
411+
? best.members.length
412+
: 0;
413+
const curVal =
414+
typeof cur.weight === "number"
415+
? cur.weight
416+
: Array.isArray(cur.members)
417+
? cur.members.length
418+
: 0;
419+
return curVal > bestVal ? cur : best;
420+
}, hyperEdges[0]);
421+
422+
setHoverHyperedge({
423+
keywords: hyperWithMax.keywords || "",
424+
summary: hyperWithMax.summary || "",
425+
members: Array.isArray(hyperWithMax.members)
426+
? hyperWithMax.members
427+
: [],
428+
weight:
429+
typeof hyperWithMax.weight === "number"
430+
? hyperWithMax.weight
431+
: Array.isArray(hyperWithMax.members)
432+
? hyperWithMax.members.length
433+
: undefined,
434+
});
435+
}
436+
}
437+
}, [graphDataFormatted, visualizationMode, hoverNode, hoverHyperedge]);
438+
363439
return (
364440
<div className="flex h-screen bg-gradient-to-br from-gray-50 to-gray-100">
365-
<div className="w-80 h-screen overflow-hidden bg-white/95 backdrop-blur-sm border-r border-gray-200/50 p-6 shadow-xl">
441+
<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">
366442
<h2 className="text-2xl font-bold text-gray-800 mb-6 flex items-center">
367443
Hypergraph-DB
368444
</h2>
@@ -499,18 +575,20 @@ <h2 className="text-2xl font-bold text-gray-800 mb-6 flex items-center">
499575
</div>
500576
<div className="text-sm text-gray-600 flex gap-2 items-center">
501577
{vertex.entity_type ? (
502-
<div className="flex items-center">
503-
<span className="font-medium">Type:</span>
504-
<span className="ml-2 px-2 py-1 bg-gray-100 rounded text-xs">
505-
{vertex.entity_type}
506-
</span>
507-
</div>) : (
508-
<div className="flex items-center">
509-
<span className="font-medium">ID:</span>
510-
<span className="ml-2 px-2 py-1 bg-gray-100 rounded text-xs">
511-
{vertex.id}
512-
</span>
513-
</div>)}
578+
<div className="flex items-center">
579+
<span className="font-medium">Type:</span>
580+
<span className="ml-2 px-2 py-1 bg-gray-100 rounded text-xs">
581+
{vertex.entity_type}
582+
</span>
583+
</div>
584+
) : (
585+
<div className="flex items-center">
586+
<span className="font-medium">ID:</span>
587+
<span className="ml-2 px-2 py-1 bg-gray-100 rounded text-xs">
588+
{vertex.id}
589+
</span>
590+
</div>
591+
)}
514592
<div className="flex items-center">
515593
<span className="font-medium">Degree:</span>
516594
<span className="ml-2 px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-semibold">
@@ -605,10 +683,109 @@ <h3 className="text-xl font-semibold text-gray-800 m-0">
605683
)}
606684

607685
{!loading && (
608-
<div
609-
ref={containerRef}
610-
className="w-full h-[calc(100vh-100px)] rounded-xl"
611-
/>
686+
<div className="flex h-[calc(100vh-71px)]">
687+
<div
688+
ref={containerRef}
689+
className="w-full rounded-xl h-full"
690+
/>
691+
{visualizationMode === "hyper" && (
692+
<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">
693+
<div className="text-lg font-bold text-gray-800 mb-3 pb-2 border-b-2 border-primary-500">
694+
HyperGraph Detail
695+
</div>
696+
{hoverHyperedge ? (
697+
<div className="text-sm text-gray-700 space-y-3 border-b-2 border-primary-500 pb-6">
698+
<div className="text-base font-semibold text-gray-900">
699+
HyperEdge
700+
</div>
701+
{hoverHyperedge.keywords && (
702+
<div>
703+
<span className="font-medium">Keywords:</span>
704+
<div className="flex flex-wrap gap-2 mt-2">
705+
{hoverHyperedge.keywords
706+
.split(",")
707+
.map((keyword) => (
708+
<span className=" inline-block p-1 text-xs bg-gray-100 rounded">
709+
{keyword}
710+
</span>
711+
))}
712+
</div>
713+
</div>
714+
)}
715+
{hoverHyperedge.summary && (
716+
<div>
717+
<div className="font-medium">Summary:</div>
718+
<div className="mt-1 text-gray-600 bg-gray-50 p-2 rounded">
719+
{hoverHyperedge.summary}
720+
</div>
721+
</div>
722+
)}
723+
{hoverHyperedge.members?.length > 0 && (
724+
<div>
725+
<div className="font-medium">
726+
Members ({hoverHyperedge.members.length}):
727+
</div>
728+
<div className="flex flex-wrap gap-2 mt-2">
729+
{hoverHyperedge.members.map((member) => (
730+
<span className="p-1 text-xs bg-gray-100 rounded">
731+
{member}
732+
</span>
733+
))}
734+
</div>
735+
</div>
736+
)}
737+
</div>
738+
) : (
739+
<div className="text-sm text-gray-500"></div>
740+
)}
741+
{hoverNode ? (
742+
<div className="text-sm text-gray-700 space-y-3 mt-4">
743+
<div className="text-base font-semibold text-gray-900">
744+
Node
745+
</div>
746+
{hoverNode.entity_name && (
747+
<div>
748+
<span className="font-medium">Name:</span>
749+
<span className="ml-2">
750+
{hoverNode.entity_name}
751+
</span>
752+
</div>
753+
)}
754+
{hoverNode.entity_type && (
755+
<div>
756+
<span className="font-medium">Type:</span>
757+
<span className="ml-2 inline-block px-2 py-0.5 bg-gray-100 rounded">
758+
{hoverNode.entity_type}
759+
</span>
760+
</div>
761+
)}
762+
{hoverNode.description && (
763+
<div>
764+
<span className="font-medium">
765+
Description:
766+
</span>
767+
<span className="inline-block px-2 py-0.5 text-xs">
768+
{hoverNode.description}
769+
</span>
770+
</div>
771+
)}
772+
{hoverNode.additional_properties && (
773+
<div>
774+
<span className="font-medium">
775+
Additional Properties:
776+
</span>
777+
<span className="inline-block px-2 py-0.5 text-xs rounded">
778+
{hoverNode.additional_properties}
779+
</span>
780+
</div>
781+
)}
782+
</div>
783+
) : (
784+
<div className="text-sm text-gray-500"></div>
785+
)}
786+
</div>
787+
)}
788+
</div>
612789
)}
613790
</div>
614791
</div>

0 commit comments

Comments
 (0)