diff --git a/.gitignore b/.gitignore
index 90df6d4..f549755 100644
--- a/.gitignore
+++ b/.gitignore
@@ -172,6 +172,7 @@ cython_debug/
hyperdb/templates/data.js
hyperdb/templates/g6.min.js
+tests/db/
# UV package manager
.venv/
diff --git a/docs/assets/vis_hg.jpg b/docs/assets/vis_hg.jpg
index 34c4c63..6ebefed 100644
Binary files a/docs/assets/vis_hg.jpg and b/docs/assets/vis_hg.jpg differ
diff --git a/hyperdb/draw.py b/hyperdb/draw.py
index 225063e..c3291e6 100644
--- a/hyperdb/draw.py
+++ b/hyperdb/draw.py
@@ -6,65 +6,171 @@
import webbrowser
from pathlib import Path
from typing import Any, Dict
-from urllib.parse import urlparse
+from urllib.parse import parse_qs, urlparse
from .hypergraph import HypergraphDB
-class HypergraphViewer:
- """Hypergraph visualization tool"""
+class HypergraphAPIHandler(http.server.BaseHTTPRequestHandler):
+ """HTTP request handler with API endpoints"""
- def __init__(self, hypergraph_db: HypergraphDB, port: int = 8080):
+ def __init__(self, hypergraph_db: HypergraphDB, *args, **kwargs):
self.hypergraph_db = hypergraph_db
- self.port = port
- self.html_content = self._generate_html_with_data()
-
- def _generate_html_with_data(self):
- """Generate HTML content with embedded data"""
- # Get all data
- database_info = {
+ super().__init__(*args, **kwargs)
+
+ def log_message(self, format, *args):
+ """Disable default logging"""
+ pass
+
+ def do_GET(self):
+ """Handle GET requests"""
+ parsed_path = urlparse(self.path)
+ path = parsed_path.path
+ query_params = parse_qs(parsed_path.query)
+
+ # CORS headers
+ self.send_response(200)
+ self.send_header("Access-Control-Allow-Origin", "*")
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
+
+ # Route handling
+ if path == "/" or path == "/index.html":
+ self.send_header("Content-type", "text/html; charset=utf-8")
+ self.end_headers()
+ self.wfile.write(self._get_html_template().encode("utf-8"))
+
+ elif path == "/api/database/info":
+ self.send_header("Content-type", "application/json; charset=utf-8")
+ self.end_headers()
+ response = self._get_database_info()
+ self.wfile.write(json.dumps(response, ensure_ascii=False).encode("utf-8"))
+
+ elif path == "/api/vertices":
+ self.send_header("Content-type", "application/json; charset=utf-8")
+ self.end_headers()
+
+ # Parse query parameters
+ page = int(query_params.get("page", ["1"])[0])
+ page_size = int(query_params.get("page_size", ["50"])[0])
+ search = query_params.get("search", [""])[0]
+ sort_by = query_params.get("sort_by", ["degree"])[0]
+ sort_order = query_params.get("sort_order", ["desc"])[0]
+
+ response = self._get_vertices(page, page_size, search, sort_by, sort_order)
+ self.wfile.write(json.dumps(response, ensure_ascii=False).encode("utf-8"))
+
+ elif path == "/api/graph":
+ self.send_header("Content-type", "application/json; charset=utf-8")
+ self.end_headers()
+
+ vertex_id = query_params.get("vertex_id", [""])[0]
+ if vertex_id:
+ response = self._get_graph_data(vertex_id)
+ else:
+ response = {"error": "vertex_id parameter is required"}
+
+ self.wfile.write(json.dumps(response, ensure_ascii=False).encode("utf-8"))
+
+ else:
+ self.send_header("Content-type", "text/plain; charset=utf-8")
+ self.end_headers()
+ self.wfile.write(b"404 Not Found")
+
+ def do_OPTIONS(self):
+ """Handle OPTIONS requests for CORS preflight"""
+ self.send_response(200)
+ self.send_header("Access-Control-Allow-Origin", "*")
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
+ self.end_headers()
+
+ def _get_database_info(self) -> Dict[str, Any]:
+ """Get database information"""
+ return {
"name": "current_hypergraph",
"vertices": self.hypergraph_db.num_v,
"edges": self.hypergraph_db.num_e,
}
- # Get vertex list
- vertices = list(self.hypergraph_db.all_v)[:100]
+ def _get_vertices(self, page: int, page_size: int, search: str, sort_by: str, sort_order: str) -> Dict[str, Any]:
+ """Get vertices with pagination and search"""
+ hg = self.hypergraph_db
+
+ # Get all vertices
+ all_vertices = list(hg.all_v)
+
+ # Prepare vertex data with search scoring
vertex_data = []
+ search_lower = search.lower() if search else ""
+
+ for v_id in all_vertices:
+ v_data = hg.v(v_id, {})
+ degree = hg.degree_v(v_id)
+ entity_type = v_data.get("entity_type", "")
+ description = v_data.get("description", "")
+
+ # Calculate search score
+ score = 0
+ if search_lower:
+ if search_lower in str(v_id).lower():
+ score += 3
+ if search_lower in entity_type.lower():
+ score += 2
+ if search_lower in description.lower():
+ score += 1
+
+ # Skip if no match
+ if score == 0:
+ continue
- for v_id in vertices:
- v_data = self.hypergraph_db.v(v_id, {})
vertex_data.append(
{
"id": v_id,
- "degree": self.hypergraph_db.degree_v(v_id),
- "entity_type": v_data.get("entity_type", ""),
- "description": (
- v_data.get("description", "")[:100] + "..."
- if len(v_data.get("description", "")) > 100
- else v_data.get("description", "")
- ),
+ "degree": degree,
+ "entity_type": entity_type,
+ "description": (description[:100] + "..." if len(description) > 100 else description),
+ "score": score,
}
)
- # Sort by degree
- vertex_data.sort(key=lambda x: x["degree"], reverse=True)
-
- # Get graph data for all vertices
- graph_data = {}
- for vertex in vertex_data:
- vertex_id = vertex["id"]
- graph_data[vertex_id] = self._get_vertex_neighbor_data(self.hypergraph_db, vertex_id)
-
- # Embed data into HTML
- return self._get_html_template(database_info, vertex_data, graph_data)
+ # Sort vertices
+ if search_lower:
+ # Sort by search score if searching (no degree filtering)
+ vertex_data.sort(key=lambda x: x["score"], reverse=True)
+ elif sort_by == "degree":
+ # First, separate by degree threshold (degree > 50 goes to the end)
+ vertex_data.sort(key=lambda x: (x["degree"] > 50, -x["degree"] if sort_order == "desc" else x["degree"]))
+ elif sort_by == "id":
+ # First, separate by degree threshold (degree > 50 goes to the end)
+ vertex_data.sort(key=lambda x: (x["degree"] > 50, str(x["id"])), reverse=(sort_order == "desc"))
+
+ # Remove score from output
+ for v in vertex_data:
+ v.pop("score", None)
+
+ # Pagination
+ total = len(vertex_data)
+ start = (page - 1) * page_size
+ end = start + page_size
+ paginated_data = vertex_data[start:end]
+
+ return {
+ "data": paginated_data,
+ "pagination": {
+ "page": page,
+ "page_size": page_size,
+ "total": total,
+ "total_pages": (total + page_size - 1) // page_size,
+ },
+ }
- def _get_vertex_neighbor_data(self, hypergraph_db: HypergraphDB, vertex_id: str) -> Dict[str, Any]:
- """Get vertex neighbor data"""
- hg = hypergraph_db
+ def _get_graph_data(self, vertex_id: str) -> Dict[str, Any]:
+ """Get graph data for a vertex"""
+ hg = self.hypergraph_db
if not hg.has_v(vertex_id):
- raise ValueError(f"Vertex {vertex_id} not found")
+ return {"error": f"Vertex {vertex_id} not found"}
# Get all neighbor hyperedges of the vertex
neighbor_edges = hg.nbr_e_of_v(vertex_id)
@@ -83,7 +189,8 @@ def _get_vertex_neighbor_data(self, hypergraph_db: HypergraphDB, vertex_id: str)
edges_data[edge_key] = {
"keywords": edge_data.get("keywords", ""),
"summary": edge_data.get("summary", ""),
- "weight": len(edge_tuple), # Hyperedge weight equals the number of vertices it contains
+ "weight": len(edge_tuple),
+ **edge_data,
}
# Get data for all vertices
@@ -99,13 +206,8 @@ def _get_vertex_neighbor_data(self, hypergraph_db: HypergraphDB, vertex_id: str)
return {"vertices": vertices_data, "edges": edges_data}
- def _get_html_template(self, database_info: Dict, vertex_data: list, graph_data: Dict) -> str:
- """Get HTML template with embedded data"""
- # Serialize data to JSON string
- embedded_data = {"database": database_info, "vertices": vertex_data, "graphs": graph_data}
- data_json = json.dumps(embedded_data, ensure_ascii=False)
-
- # Read HTML template file
+ def _get_html_template(self) -> str:
+ """Get HTML template without embedded data"""
template_path = Path(__file__).parent / "templates" / "hypergraph_viewer.html"
try:
@@ -114,31 +216,24 @@ def _get_html_template(self, database_info: Dict, vertex_data: list, graph_data:
except FileNotFoundError:
raise FileNotFoundError(f"HTML template file not found: {template_path}")
- # Replace placeholders in template
- html_content = html_template.replace("{{DATA_JSON}}", data_json)
+ # Replace placeholder with empty object (data will be loaded via API)
+ html_content = html_template.replace("{{DATA_JSON}}", "{}")
return html_content
- def start_server(self, open_browser: bool = True):
- """Start simple HTTP server"""
- class CustomHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
- def __init__(self, html_content, *args, **kwargs):
- self.html_content = html_content
- super().__init__(*args, **kwargs)
+class HypergraphViewer:
+ """Hypergraph visualization tool"""
- def do_GET(self):
- self.send_response(200)
- self.send_header("Content-type", "text/html; charset=utf-8")
- self.end_headers()
- self.wfile.write(self.html_content.encode("utf-8"))
+ def __init__(self, hypergraph_db: HypergraphDB, port: int = 8080):
+ self.hypergraph_db = hypergraph_db
+ self.port = port
- def log_message(self, format, *args):
- # Disable log output
- pass
+ def start_server(self, open_browser: bool = True):
+ """Start HTTP server with API endpoints"""
def run_server():
- handler = lambda *args, **kwargs: CustomHTTPRequestHandler(self.html_content, *args, **kwargs)
+ handler = lambda *args, **kwargs: HypergraphAPIHandler(self.hypergraph_db, *args, **kwargs)
self.httpd = socketserver.TCPServer(("127.0.0.1", self.port), handler)
self.httpd.serve_forever()
diff --git a/hyperdb/templates/hypergraph_viewer.html b/hyperdb/templates/hypergraph_viewer.html
index 988e36e..2d0fc9f 100644
--- a/hyperdb/templates/hypergraph_viewer.html
+++ b/hyperdb/templates/hypergraph_viewer.html
@@ -5,7 +5,7 @@
Hypergraph Visualization
-
+
@@ -29,11 +29,11 @@
const { useState, useEffect, useMemo, useRef } = React;
const { Graph } = window.G6;
- // 嵌入的数据
- const embeddedData = {{DATA_JSON}};
- // const embeddedData = datas;
+ // API base path
+ // const API_BASE = window.location.origin;
+ const API_BASE = "http://localhost:8080";
- // 常量配置
+ // Configuration constants
const COLORS = [
"#F6BD16",
"#00C9C9",
@@ -47,25 +47,30 @@
"#c8ff00",
];
- // 6个预定义颜色,用于循环分配给不同的实体类型
+ // 6 predefined colors, cycled for different entity types
const ENTITY_TYPE_COLORS_PALETTE = [
"#00C9C9",
"#a68fff",
"#F08F56",
"#0d7c4f",
"#004ac9",
- "#f056d1"
+ "#f056d1",
];
const DEFAULT_NODE_COLOR = "#8b5cf6";
- // 动态生成实体类型颜色映射
+ // Dynamically generate entity type color mapping
const generateEntityTypeColors = (vertices) => {
- const entityTypes = [...new Set(vertices.map(v => v.entity_type).filter(Boolean))];
+ const entityTypes = [
+ ...new Set(vertices.map((v) => v.entity_type).filter(Boolean)),
+ ];
const colorMap = {};
entityTypes.forEach((entityType, index) => {
- colorMap[entityType] = ENTITY_TYPE_COLORS_PALETTE[index % ENTITY_TYPE_COLORS_PALETTE.length];
+ colorMap[entityType] =
+ ENTITY_TYPE_COLORS_PALETTE[
+ index % ENTITY_TYPE_COLORS_PALETTE.length
+ ];
});
return colorMap;
@@ -73,16 +78,7 @@
const LAYOUT_THRESHOLD = 100;
const EDGE_SEPARATOR = "|#|";
- // 工具函数
- const calculateMatchScore = (vertex, searchLower) => {
- let score = 0;
- if (vertex.id.toString().toLowerCase().includes(searchLower))
- score += 3;
- if (vertex.entity_type?.toLowerCase().includes(searchLower)) score += 2;
- if (vertex.description?.toLowerCase().includes(searchLower)) score += 1;
- return score;
- };
-
+ // Utility functions
const createBubbleStyle = (baseColor) => ({
fill: baseColor,
stroke: baseColor,
@@ -91,8 +87,8 @@
pixelGroup: 4,
edgeR0: 10,
edgeR1: 60,
- nodeR0: 15,
- nodeR1: 50,
+ nodeR0: 1,
+ nodeR1: 5,
morphBuffer: 10,
threshold: 4,
memberInfluenceFactor: 1,
@@ -111,71 +107,208 @@
return description.split("").join(" ");
};
+ // API call functions
+ const fetchDatabaseInfo = async () => {
+ if (typeof datas !== "undefined") {
+ return Promise.resolve(datas.database);
+ }
+ const response = await fetch(`${API_BASE}/api/database/info`);
+ return await response.json();
+ };
+
+ const fetchVertices = async (
+ page,
+ pageSize,
+ search,
+ sortBy,
+ sortOrder
+ ) => {
+ if (typeof datas !== "undefined") {
+ const toLower = (v) => (v ?? "").toString().toLowerCase();
+ let list = Array.isArray(datas.vertices) ? datas.vertices : [];
+
+ if (search) {
+ const q = toLower(search);
+ list = list.filter(
+ (v) =>
+ toLower(v.id).includes(q) ||
+ toLower(v.entity_type).includes(q) ||
+ toLower(v.description).includes(q)
+ );
+ }
+
+ if (sortBy === "degree") {
+ list.sort((a, b) =>
+ sortOrder === "asc"
+ ? (a.degree || 0) - (b.degree || 0)
+ : (b.degree || 0) - (a.degree || 0)
+ );
+ } else if (sortBy === "id") {
+ list.sort((a, b) => {
+ const cmp = String(a.id).localeCompare(String(b.id));
+ return sortOrder === "asc" ? cmp : -cmp;
+ });
+ }
+
+ const total = list.length;
+ const total_pages = Math.max(1, Math.ceil(total / pageSize));
+ const cur = Math.min(Math.max(1, page), total_pages);
+ const start = (cur - 1) * pageSize;
+ const data = list.slice(start, start + pageSize);
+ return Promise.resolve({
+ data,
+ pagination: { page: cur, page_size: pageSize, total, total_pages },
+ });
+ }
+ const params = new URLSearchParams({
+ page: page.toString(),
+ page_size: pageSize.toString(),
+ search: search || "",
+ sort_by: sortBy,
+ sort_order: sortOrder,
+ });
+ const response = await fetch(`${API_BASE}/api/vertices?${params}`);
+ return await response.json();
+ };
+
+ const fetchGraphData = async (vertexId) => {
+ if (typeof datas !== "undefined") {
+ const entry = datas.graphs[vertexId];
+ if (entry && entry.vertices && entry.edges) {
+ return Promise.resolve({
+ vertices: entry.vertices,
+ edges: entry.edges,
+ });
+ }
+ return Promise.resolve({ vertices: {}, edges: {} });
+ }
+ const params = new URLSearchParams({ vertex_id: vertexId });
+ const response = await fetch(`${API_BASE}/api/graph?${params}`);
+ return await response.json();
+ };
+
function HyperGraphViewer() {
const containerRef = useRef(null);
const graphRef = useRef(null);
- const [databases, setDatabases] = useState([embeddedData.database]);
- const [vertices, setVertices] = useState(embeddedData.vertices);
- const [selectedVertex, setSelectedVertex] = useState(
- embeddedData.vertices.length > 0 ? embeddedData.vertices[0].id : ""
- );
+ const searchTimeoutRef = useRef(null);
+
+ const [databaseInfo, setDatabaseInfo] = useState({
+ vertices: 0,
+ edges: 0,
+ });
+ const [vertices, setVertices] = useState([]);
+ const [pagination, setPagination] = useState({
+ page: 1,
+ page_size: 50,
+ total: 0,
+ total_pages: 0,
+ });
+ const [selectedVertex, setSelectedVertex] = useState("");
const [graphData, setGraphData] = useState(null);
const [loading, setLoading] = useState(false);
+ const [loadingVertices, setLoadingVertices] = useState(false);
const [error, setError] = useState("");
const [searchTerm, setSearchTerm] = useState("");
- const [searchResults, setSearchResults] = useState([]);
- const [filteredVertices, setFilteredVertices] = useState(
- embeddedData.vertices
- );
- const [visualizationMode, setVisualizationMode] = useState("hyper"); // 'hyper' or 'graph'
- const [graphVersion, setGraphVersion] = useState(0);
+ const [sortBy, setSortBy] = useState("degree");
+ const [sortOrder, setSortOrder] = useState("desc");
+ const [visualizationMode, setVisualizationMode] = useState("hyper");
const [hoverHyperedge, setHoverHyperedge] = useState(null);
const [hoverNode, setHoverNode] = useState(null);
- // 生成实体类型颜色映射
+ // Generate entity type color mapping
const entityTypeColors = useMemo(() => {
- return generateEntityTypeColors(embeddedData.vertices);
- }, [embeddedData.vertices]);
- // 搜索功能
+ return generateEntityTypeColors(vertices);
+ }, [vertices]);
+
+ // Load database info
+ useEffect(() => {
+ fetchDatabaseInfo().then(setDatabaseInfo).catch(console.error);
+ }, []);
+
+ // Load vertices list
+ const loadVertices = async (page, search, sortBy, sortOrder) => {
+ setLoadingVertices(true);
+ try {
+ const result = await fetchVertices(
+ page,
+ pagination.page_size,
+ search,
+ sortBy,
+ sortOrder
+ );
+ setVertices(result.data);
+ setPagination(result.pagination);
+
+ // If no vertex selected and data exists, select the first one
+ if (result.data.length > 0) {
+ setSelectedVertex(result.data[0].id);
+ }
+ } catch (err) {
+ console.error("Failed to load vertices:", err);
+ } finally {
+ setLoadingVertices(false);
+ }
+ };
+
+ // Initial load of vertices
+ useEffect(() => {
+ loadVertices(1, "", sortBy, sortOrder);
+ }, []);
+
+ // Search debounce
useEffect(() => {
- if (!searchTerm.trim()) {
- setFilteredVertices(embeddedData.vertices);
- setSearchResults([]);
- return;
+ if (searchTimeoutRef.current) {
+ clearTimeout(searchTimeoutRef.current);
}
- const searchLower = searchTerm.toLowerCase();
- const results = embeddedData.vertices
- .map((vertex) => ({
- ...vertex,
- score: calculateMatchScore(vertex, searchLower),
- }))
- .filter((item) => item.score > 0)
- .sort((a, b) => b.score - a.score)
- .map(({ score, ...vertex }) => vertex);
-
- setFilteredVertices(results);
- setSearchResults(results);
+ searchTimeoutRef.current = setTimeout(() => {
+ loadVertices(1, searchTerm, sortBy, sortOrder);
+ }, 300);
+
+ return () => {
+ if (searchTimeoutRef.current) {
+ clearTimeout(searchTimeoutRef.current);
+ }
+ };
}, [searchTerm]);
- // 加载图数据(从嵌入数据获取)
+ // Reload when sort changes
+ useEffect(() => {
+ if (vertices.length > 0) {
+ loadVertices(pagination.page, searchTerm, sortBy, sortOrder);
+ }
+ }, [sortBy, sortOrder]);
+
+ // Load when page changes
+ const handlePageChange = (newPage) => {
+ loadVertices(newPage, searchTerm, sortBy, sortOrder);
+ };
+
+ // Load graph data
useEffect(() => {
if (selectedVertex) {
setLoading(true);
setError("");
- // 从嵌入数据获取图数据
- const data = embeddedData.graphs[selectedVertex];
- if (data) {
- setGraphData(data);
- } else {
- setError("Graph data not found for this vertex");
- }
- setLoading(false);
+ fetchGraphData(selectedVertex)
+ .then((data) => {
+ if (data.error) {
+ setError(data.error);
+ } else {
+ setGraphData(data);
+ }
+ })
+ .catch((err) => {
+ setError("Failed to load graph data");
+ console.error(err);
+ })
+ .finally(() => {
+ setLoading(false);
+ });
}
}, [selectedVertex]);
- // 转换数据为 G6 Graph 格式,根据可视化模式处理
+ // Convert data to G6 Graph format
const graphDataFormatted = useMemo(() => {
if (!graphData) return null;
@@ -192,7 +325,7 @@
const edgeEntries = Object.entries(graphData.edges);
if (visualizationMode === "graph") {
- // Graph模式:只显示维度为2的球棍图
+ // Graph mode: only show dimension 2 ball-and-stick diagram
const edgeSet = new Set();
edgeEntries.forEach(([key, edge]) => {
@@ -213,7 +346,7 @@
}
});
- // 过滤未连接的节点
+ // Filter unconnected nodes
const connectedNodes = new Set();
hyperData.edges.forEach((edge) => {
connectedNodes.add(edge.source);
@@ -223,10 +356,26 @@
connectedNodes.has(node.id)
);
} else {
- // Hyper模式:使用bubble-sets插件
+ // Hyper mode: render 2-node entries as normal edges, others as bubble-sets
+ const edgeSet = new Set();
edgeEntries.forEach(([key, edge], i) => {
const nodes = key.split(EDGE_SEPARATOR);
+ if (nodes.length === 2) {
+ const [a, b] = nodes;
+ const edgeId = a < b ? `${a}-${b}` : `${b}-${a}`;
+ if (!edgeSet.has(edgeId)) {
+ edgeSet.add(edgeId);
+ hyperData.edges.push({
+ id: edgeId,
+ source: a,
+ target: b,
+ ...edge,
+ });
+ }
+ return;
+ }
+
plugins.push({
key: `bubble-sets-${key}`,
type: "bubble-sets",
@@ -234,6 +383,7 @@
keywords: edge.keywords || "",
summary: edge.summary || "",
weight: edge.weight || nodes.length,
+ description: edge.description || edge.summary || "",
...createBubbleStyle(COLORS[i % COLORS.length]),
});
@@ -243,9 +393,31 @@
members: nodes,
});
});
+
+ // Assign cluster by hyperEdges for layout grouping
+ // Pick the heaviest hyperedge containing the node as its primary cluster
+ const nodeIdToCandidateClusters = new Map();
+ hyperData.hyperEdges.forEach((he) => {
+ const weight =
+ he.weight ||
+ (Array.isArray(he.members) ? he.members.length : 1);
+ (he.members || []).forEach((m) => {
+ const list = nodeIdToCandidateClusters.get(m) || [];
+ list.push({ id: he.id, weight });
+ nodeIdToCandidateClusters.set(m, list);
+ });
+ });
+ hyperData.nodes = hyperData.nodes.map((n) => {
+ const candidates = nodeIdToCandidateClusters.get(n.id) || [];
+ if (candidates.length === 0) return n;
+ const primary = candidates.reduce((best, cur) =>
+ cur.weight > best.weight ? cur : best
+ );
+ return { ...n, cluster: primary.id };
+ });
}
- // 添加tooltip插件
+ // Add tooltip plugin
const excludedKeys = new Set([
"id",
"entity_name",
@@ -270,7 +442,7 @@
item.description
)}`;
}
- // 展示所有剩余属性
+ // Display all remaining properties
Object.entries(item).forEach(([key, value]) => {
if (!excludedKeys.has(key)) {
result += `${key}: ${value}
`;
@@ -290,7 +462,7 @@
node: {
palette: { field: "cluster" },
style: {
- size: isGraph ? 20 : 25,
+ size: hyperData.nodes.length > LAYOUT_THRESHOLD ? 15 : 20,
labelText: (d) => d.id,
fill: (d) => getNodeColor(d, selectedVertex, entityTypeColors),
},
@@ -298,30 +470,28 @@
edge: {
style: {
size: isGraph ? 3 : 2,
- stroke: isGraph ? "#a68fff" : undefined,
- lineWidth: isGraph ? 2 : undefined,
+ stroke: "#a68fff",
+ lineWidth: 1,
},
},
layout: {
type:
- hyperData.nodes.length > LAYOUT_THRESHOLD
- ? "force-atlas2"
- : "force",
+ hyperData.nodes.length > LAYOUT_THRESHOLD ? "force" : "force",
clustering: !isGraph,
preventOverlap: true,
- nodeClusterBy: isGraph ? undefined : "entity_type",
- gravity: 20,
- linkDistance: isGraph ? 100 : 150,
+ nodeClusterBy: isGraph ? undefined : "cluster",
+ gravity: 50,
+ linkDistance: 50,
},
autoFit: "center",
};
}, [graphData, selectedVertex, visualizationMode, entityTypeColors]);
- // 初始化图形
+ // Initialize graph
useEffect(() => {
if (!graphDataFormatted || !containerRef.current) return;
- // 销毁之前的图形实例并清空画布
+ // Destroy previous graph instance and clear canvas
if (graphRef.current && !graphRef.current.destroyed) {
graphRef.current.clear();
if (containerRef.current) {
@@ -348,15 +518,11 @@
graph.on("pointermove", (e) => {
if (e.targetType === "bubble-sets") {
- console.log("bubble-sets", e.target);
const target = e.target.options;
const newHyperedge = {
- keywords: target.keywords || "",
- summary: target.summary || "",
+ ...target,
members: Array.isArray(target.members) ? target.members : [],
- weight: target.weight,
};
- // 只在数据不同时才更新
setHoverHyperedge((prev) => {
if (
!prev ||
@@ -375,7 +541,6 @@
const target = graphDataFormatted.data.nodes.find(
(node) => node.id === e.target.id
);
- // 只在节点不同时才更新
setHoverNode((prev) => {
if (!prev || prev.id !== target?.id) {
return target;
@@ -385,7 +550,7 @@
}
});
- // 添加窗口大小变化监听
+ // Add window resize listener
const handleResize = () => {
if (graphRef.current && containerRef.current) {
graphRef.current.setSize(
@@ -405,12 +570,11 @@
if (containerRef.current) {
containerRef.current.innerHTML = "";
}
- // 清理右侧悬停信息
setHoverHyperedge(null);
};
}, [graphDataFormatted, visualizationMode]);
- // 默认选中"最大的"节点与超边(首次或每次数据/模式变化时)
+ // Select the "largest" node and hyperedges by default
useEffect(() => {
if (!graphDataFormatted) return;
@@ -433,7 +597,8 @@
setHoverHyperedge({
keywords: hyperWithMax.keywords || "",
- summary: hyperWithMax.summary || "",
+ description:
+ hyperWithMax.description || hyperWithMax.summary || "",
members: hyperWithMax.members || [],
weight: getWeight(hyperWithMax),
});
@@ -443,7 +608,7 @@
return (
-
+
Hypergraph-DB
@@ -451,27 +616,19 @@
Database Information
-
+
Vertices:
- {embeddedData.database.vertices}
-
-
-
-
- Hyperedges:
-
-
- {embeddedData.database.edges}
+ {databaseInfo.vertices}
- Displayed Vertices:
+ Hyperedges:
- {vertices.length}
+ {databaseInfo.edges}
@@ -480,20 +637,14 @@
Search & Vertex List
- {/* 搜索框 */}
+ {/* Search and Sort */}
-
+
setSearchTerm(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter" && searchResults.length > 0) {
- setSelectedVertex(searchResults[0].id);
- e.target.blur();
- }
- }}
className="w-full px-4 py-2 pl-10 pr-4 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200"
/>
@@ -532,50 +683,58 @@
)}
- {searchTerm && (
-
- Found {searchResults.length} matching results
- {searchResults.length > 0 && (
-
- (Press Enter to select first result)
-
- )}
-
- )}
+
+ {/* Sort Selection */}
+ {/*
+
+
+
*/}
+
+
+ Found {pagination.total} results
+
-
- {filteredVertices.length === 0 ? (
+ {/* Vertices List */}
+
+ {loadingVertices ? (
- {searchTerm
- ? "No matching vertices found"
- : "No vertex data available"}
+ Loading vertices...
+
+ ) : vertices.length === 0 ? (
+
+ No matching vertices found
) : (
- filteredVertices.map((vertex) => {
+ vertices.map((vertex) => {
const isSelected = selectedVertex === vertex.id;
- const isSearchMatch = searchResults.some(
- (r) => r.id === vertex.id
- );
- const highlightClass = isSelected
- ? "bg-primary-50 border-l-4 border-l-primary-500 shadow-md"
- : isSearchMatch
- ? "bg-yellow-50 border-l-4 border-l-yellow-400 shadow-sm"
- : "";
return (
setSelectedVertex(vertex.id)}
>
{vertex.id}
- {isSearchMatch && (
-
- Match
-
- )}
@@ -603,6 +762,29 @@
})
)}
+
+ {/* Pagination Controls */}
+ {pagination.total_pages > 1 && (
+
+
+
+ {pagination.page} / {pagination.total_pages}
+
+
+
+ )}
@@ -613,7 +795,7 @@
Visualization {selectedVertex && `- ${selectedVertex}`}
- {/* 可视化模式选择器 */}
+ {/* Visualization Mode Selector */}
Mode:
@@ -659,7 +841,7 @@
)}
{loading && (
-
+
🔄
@@ -672,115 +854,112 @@
)}
- {!loading && (
-
-
- {visualizationMode === "hyper" && (
-
-
- HyperGraph Detail
-
- {hoverHyperedge && (
-
-
- HyperEdge
-
- {hoverHyperedge.keywords && (
-
-
Keywords:
-
- {hoverHyperedge.keywords
- .split(/,|,|、|。|/)
- .map((keyword, i) => (
-
- {keyword}
-
- ))}
-
-
- )}
- {hoverHyperedge.summary && (
-
-
Summary:
-
- {hoverHyperedge.summary}
-
+
+
+ {visualizationMode === "hyper" && (
+
+
+ HyperGraph Detail
+
+ {hoverHyperedge && (
+
+
+ HyperEdge
+
+ {hoverHyperedge.description && (
+
+
Description:
+
+ {hoverHyperedge.description}
- )}
- {hoverHyperedge.members?.length > 0 && (
-
-
- Nodes ({hoverHyperedge.members.length}):
-
-
- {hoverHyperedge.members.map((member, i) => (
+
+ )}
+ {hoverHyperedge.keywords && (
+
+
Keywords:
+
+ {hoverHyperedge.keywords
+ .split(/,|,|、|。|/)
+ .map((keyword, i) => (
- {member}
+ {keyword}
))}
-
- )}
-
- )}
- {hoverNode && (
-
-
- Node
- {hoverNode.entity_name && (
-
- Name:
-
- {hoverNode.entity_name}
-
-
- )}
- {hoverNode.entity_type && (
-
- Type:
-
- {hoverNode.entity_type}
-
-
- )}
- {hoverNode.description && (
-
-
- Description:
-
-
- {formatDescription(hoverNode.description)}
-
+ )}
+
+ {hoverHyperedge.members?.length > 0 && (
+
+
+ Nodes ({hoverHyperedge.members.length}):
- )}
- {hoverNode.additional_properties && (
-
-
- Additional Properties:
-
-
- {formatDescription(
- hoverNode.additional_properties
- )}
-
+
+ {hoverHyperedge.members.map((member, i) => (
+
+ {member}
+
+ ))}
- )}
+
+ )}
+
+ )}
+ {hoverNode && (
+
- )}
-
- )}
+ {hoverNode.entity_name && (
+
+ Name:
+
+ {hoverNode.entity_name}
+
+
+ )}
+ {hoverNode.entity_type && (
+
+ Type:
+
+ {hoverNode.entity_type}
+
+
+ )}
+ {hoverNode.description && (
+
+ Description:
+
+ {formatDescription(hoverNode.description)}
+
+
+ )}
+ {hoverNode.additional_properties && (
+
+
+ Additional Properties:
+
+
+ {formatDescription(
+ hoverNode.additional_properties
+ )}
+
+
+ )}
+
+ )}
+
+ )}
+