Skip to content

Commit de7244b

Browse files
authored
Merge pull request #7 from yuxizhe/main
feat: refactor draw server into an API interface and visualization of both graph and hypergraph on the same plot
2 parents 88774a9 + e6d5301 commit de7244b

File tree

4 files changed

+577
-302
lines changed

4 files changed

+577
-302
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ cython_debug/
172172

173173
hyperdb/templates/data.js
174174
hyperdb/templates/g6.min.js
175+
tests/db/
175176

176177
# UV package manager
177178
.venv/

docs/assets/vis_hg.jpg

760 KB
Loading

hyperdb/draw.py

Lines changed: 157 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,65 +6,171 @@
66
import webbrowser
77
from pathlib import Path
88
from typing import Any, Dict
9-
from urllib.parse import urlparse
9+
from urllib.parse import parse_qs, urlparse
1010

1111
from .hypergraph import HypergraphDB
1212

1313

14-
class HypergraphViewer:
15-
"""Hypergraph visualization tool"""
14+
class HypergraphAPIHandler(http.server.BaseHTTPRequestHandler):
15+
"""HTTP request handler with API endpoints"""
1616

17-
def __init__(self, hypergraph_db: HypergraphDB, port: int = 8080):
17+
def __init__(self, hypergraph_db: HypergraphDB, *args, **kwargs):
1818
self.hypergraph_db = hypergraph_db
19-
self.port = port
20-
self.html_content = self._generate_html_with_data()
21-
22-
def _generate_html_with_data(self):
23-
"""Generate HTML content with embedded data"""
24-
# Get all data
25-
database_info = {
19+
super().__init__(*args, **kwargs)
20+
21+
def log_message(self, format, *args):
22+
"""Disable default logging"""
23+
pass
24+
25+
def do_GET(self):
26+
"""Handle GET requests"""
27+
parsed_path = urlparse(self.path)
28+
path = parsed_path.path
29+
query_params = parse_qs(parsed_path.query)
30+
31+
# CORS headers
32+
self.send_response(200)
33+
self.send_header("Access-Control-Allow-Origin", "*")
34+
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
35+
self.send_header("Access-Control-Allow-Headers", "Content-Type")
36+
37+
# Route handling
38+
if path == "/" or path == "/index.html":
39+
self.send_header("Content-type", "text/html; charset=utf-8")
40+
self.end_headers()
41+
self.wfile.write(self._get_html_template().encode("utf-8"))
42+
43+
elif path == "/api/database/info":
44+
self.send_header("Content-type", "application/json; charset=utf-8")
45+
self.end_headers()
46+
response = self._get_database_info()
47+
self.wfile.write(json.dumps(response, ensure_ascii=False).encode("utf-8"))
48+
49+
elif path == "/api/vertices":
50+
self.send_header("Content-type", "application/json; charset=utf-8")
51+
self.end_headers()
52+
53+
# Parse query parameters
54+
page = int(query_params.get("page", ["1"])[0])
55+
page_size = int(query_params.get("page_size", ["50"])[0])
56+
search = query_params.get("search", [""])[0]
57+
sort_by = query_params.get("sort_by", ["degree"])[0]
58+
sort_order = query_params.get("sort_order", ["desc"])[0]
59+
60+
response = self._get_vertices(page, page_size, search, sort_by, sort_order)
61+
self.wfile.write(json.dumps(response, ensure_ascii=False).encode("utf-8"))
62+
63+
elif path == "/api/graph":
64+
self.send_header("Content-type", "application/json; charset=utf-8")
65+
self.end_headers()
66+
67+
vertex_id = query_params.get("vertex_id", [""])[0]
68+
if vertex_id:
69+
response = self._get_graph_data(vertex_id)
70+
else:
71+
response = {"error": "vertex_id parameter is required"}
72+
73+
self.wfile.write(json.dumps(response, ensure_ascii=False).encode("utf-8"))
74+
75+
else:
76+
self.send_header("Content-type", "text/plain; charset=utf-8")
77+
self.end_headers()
78+
self.wfile.write(b"404 Not Found")
79+
80+
def do_OPTIONS(self):
81+
"""Handle OPTIONS requests for CORS preflight"""
82+
self.send_response(200)
83+
self.send_header("Access-Control-Allow-Origin", "*")
84+
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
85+
self.send_header("Access-Control-Allow-Headers", "Content-Type")
86+
self.end_headers()
87+
88+
def _get_database_info(self) -> Dict[str, Any]:
89+
"""Get database information"""
90+
return {
2691
"name": "current_hypergraph",
2792
"vertices": self.hypergraph_db.num_v,
2893
"edges": self.hypergraph_db.num_e,
2994
}
3095

31-
# Get vertex list
32-
vertices = list(self.hypergraph_db.all_v)[:100]
96+
def _get_vertices(self, page: int, page_size: int, search: str, sort_by: str, sort_order: str) -> Dict[str, Any]:
97+
"""Get vertices with pagination and search"""
98+
hg = self.hypergraph_db
99+
100+
# Get all vertices
101+
all_vertices = list(hg.all_v)
102+
103+
# Prepare vertex data with search scoring
33104
vertex_data = []
105+
search_lower = search.lower() if search else ""
106+
107+
for v_id in all_vertices:
108+
v_data = hg.v(v_id, {})
109+
degree = hg.degree_v(v_id)
110+
entity_type = v_data.get("entity_type", "")
111+
description = v_data.get("description", "")
112+
113+
# Calculate search score
114+
score = 0
115+
if search_lower:
116+
if search_lower in str(v_id).lower():
117+
score += 3
118+
if search_lower in entity_type.lower():
119+
score += 2
120+
if search_lower in description.lower():
121+
score += 1
122+
123+
# Skip if no match
124+
if score == 0:
125+
continue
34126

35-
for v_id in vertices:
36-
v_data = self.hypergraph_db.v(v_id, {})
37127
vertex_data.append(
38128
{
39129
"id": v_id,
40-
"degree": self.hypergraph_db.degree_v(v_id),
41-
"entity_type": v_data.get("entity_type", ""),
42-
"description": (
43-
v_data.get("description", "")[:100] + "..."
44-
if len(v_data.get("description", "")) > 100
45-
else v_data.get("description", "")
46-
),
130+
"degree": degree,
131+
"entity_type": entity_type,
132+
"description": (description[:100] + "..." if len(description) > 100 else description),
133+
"score": score,
47134
}
48135
)
49136

50-
# Sort by degree
51-
vertex_data.sort(key=lambda x: x["degree"], reverse=True)
52-
53-
# Get graph data for all vertices
54-
graph_data = {}
55-
for vertex in vertex_data:
56-
vertex_id = vertex["id"]
57-
graph_data[vertex_id] = self._get_vertex_neighbor_data(self.hypergraph_db, vertex_id)
58-
59-
# Embed data into HTML
60-
return self._get_html_template(database_info, vertex_data, graph_data)
137+
# Sort vertices
138+
if search_lower:
139+
# Sort by search score if searching (no degree filtering)
140+
vertex_data.sort(key=lambda x: x["score"], reverse=True)
141+
elif sort_by == "degree":
142+
# First, separate by degree threshold (degree > 50 goes to the end)
143+
vertex_data.sort(key=lambda x: (x["degree"] > 50, -x["degree"] if sort_order == "desc" else x["degree"]))
144+
elif sort_by == "id":
145+
# First, separate by degree threshold (degree > 50 goes to the end)
146+
vertex_data.sort(key=lambda x: (x["degree"] > 50, str(x["id"])), reverse=(sort_order == "desc"))
147+
148+
# Remove score from output
149+
for v in vertex_data:
150+
v.pop("score", None)
151+
152+
# Pagination
153+
total = len(vertex_data)
154+
start = (page - 1) * page_size
155+
end = start + page_size
156+
paginated_data = vertex_data[start:end]
157+
158+
return {
159+
"data": paginated_data,
160+
"pagination": {
161+
"page": page,
162+
"page_size": page_size,
163+
"total": total,
164+
"total_pages": (total + page_size - 1) // page_size,
165+
},
166+
}
61167

62-
def _get_vertex_neighbor_data(self, hypergraph_db: HypergraphDB, vertex_id: str) -> Dict[str, Any]:
63-
"""Get vertex neighbor data"""
64-
hg = hypergraph_db
168+
def _get_graph_data(self, vertex_id: str) -> Dict[str, Any]:
169+
"""Get graph data for a vertex"""
170+
hg = self.hypergraph_db
65171

66172
if not hg.has_v(vertex_id):
67-
raise ValueError(f"Vertex {vertex_id} not found")
173+
return {"error": f"Vertex {vertex_id} not found"}
68174

69175
# Get all neighbor hyperedges of the vertex
70176
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)
83189
edges_data[edge_key] = {
84190
"keywords": edge_data.get("keywords", ""),
85191
"summary": edge_data.get("summary", ""),
86-
"weight": len(edge_tuple), # Hyperedge weight equals the number of vertices it contains
192+
"weight": len(edge_tuple),
193+
**edge_data,
87194
}
88195

89196
# Get data for all vertices
@@ -99,13 +206,8 @@ def _get_vertex_neighbor_data(self, hypergraph_db: HypergraphDB, vertex_id: str)
99206

100207
return {"vertices": vertices_data, "edges": edges_data}
101208

102-
def _get_html_template(self, database_info: Dict, vertex_data: list, graph_data: Dict) -> str:
103-
"""Get HTML template with embedded data"""
104-
# Serialize data to JSON string
105-
embedded_data = {"database": database_info, "vertices": vertex_data, "graphs": graph_data}
106-
data_json = json.dumps(embedded_data, ensure_ascii=False)
107-
108-
# Read HTML template file
209+
def _get_html_template(self) -> str:
210+
"""Get HTML template without embedded data"""
109211
template_path = Path(__file__).parent / "templates" / "hypergraph_viewer.html"
110212

111213
try:
@@ -114,31 +216,24 @@ def _get_html_template(self, database_info: Dict, vertex_data: list, graph_data:
114216
except FileNotFoundError:
115217
raise FileNotFoundError(f"HTML template file not found: {template_path}")
116218

117-
# Replace placeholders in template
118-
html_content = html_template.replace("{{DATA_JSON}}", data_json)
219+
# Replace placeholder with empty object (data will be loaded via API)
220+
html_content = html_template.replace("{{DATA_JSON}}", "{}")
119221

120222
return html_content
121223

122-
def start_server(self, open_browser: bool = True):
123-
"""Start simple HTTP server"""
124224

125-
class CustomHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
126-
def __init__(self, html_content, *args, **kwargs):
127-
self.html_content = html_content
128-
super().__init__(*args, **kwargs)
225+
class HypergraphViewer:
226+
"""Hypergraph visualization tool"""
129227

130-
def do_GET(self):
131-
self.send_response(200)
132-
self.send_header("Content-type", "text/html; charset=utf-8")
133-
self.end_headers()
134-
self.wfile.write(self.html_content.encode("utf-8"))
228+
def __init__(self, hypergraph_db: HypergraphDB, port: int = 8080):
229+
self.hypergraph_db = hypergraph_db
230+
self.port = port
135231

136-
def log_message(self, format, *args):
137-
# Disable log output
138-
pass
232+
def start_server(self, open_browser: bool = True):
233+
"""Start HTTP server with API endpoints"""
139234

140235
def run_server():
141-
handler = lambda *args, **kwargs: CustomHTTPRequestHandler(self.html_content, *args, **kwargs)
236+
handler = lambda *args, **kwargs: HypergraphAPIHandler(self.hypergraph_db, *args, **kwargs)
142237
self.httpd = socketserver.TCPServer(("127.0.0.1", self.port), handler)
143238
self.httpd.serve_forever()
144239

0 commit comments

Comments
 (0)