code-review-graph-codeblackwell 2.3.6.post1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_review_graph/__init__.py +20 -0
- code_review_graph/__main__.py +4 -0
- code_review_graph/analysis.py +410 -0
- code_review_graph/changes.py +409 -0
- code_review_graph/cli.py +1255 -0
- code_review_graph/communities.py +874 -0
- code_review_graph/constants.py +23 -0
- code_review_graph/context_savings.py +317 -0
- code_review_graph/custom_languages.py +322 -0
- code_review_graph/daemon.py +1009 -0
- code_review_graph/daemon_cli.py +320 -0
- code_review_graph/docs/LLM-OPTIMIZED-REFERENCE.md +71 -0
- code_review_graph/embeddings.py +1006 -0
- code_review_graph/enrich.py +303 -0
- code_review_graph/eval/__init__.py +33 -0
- code_review_graph/eval/benchmarks/__init__.py +1 -0
- code_review_graph/eval/benchmarks/agent_baseline.py +193 -0
- code_review_graph/eval/benchmarks/build_performance.py +60 -0
- code_review_graph/eval/benchmarks/flow_completeness.py +36 -0
- code_review_graph/eval/benchmarks/impact_accuracy.py +220 -0
- code_review_graph/eval/benchmarks/multi_hop_retrieval.py +125 -0
- code_review_graph/eval/benchmarks/search_quality.py +59 -0
- code_review_graph/eval/benchmarks/token_efficiency.py +143 -0
- code_review_graph/eval/configs/code-review-graph.yaml +50 -0
- code_review_graph/eval/configs/express.yaml +45 -0
- code_review_graph/eval/configs/fastapi.yaml +48 -0
- code_review_graph/eval/configs/flask.yaml +50 -0
- code_review_graph/eval/configs/gin.yaml +51 -0
- code_review_graph/eval/configs/httpx.yaml +48 -0
- code_review_graph/eval/reporter.py +301 -0
- code_review_graph/eval/runner.py +211 -0
- code_review_graph/eval/scorer.py +85 -0
- code_review_graph/eval/token_benchmark.py +182 -0
- code_review_graph/exports.py +409 -0
- code_review_graph/flows.py +698 -0
- code_review_graph/graph.py +1427 -0
- code_review_graph/graph_diff.py +122 -0
- code_review_graph/hints.py +384 -0
- code_review_graph/incremental.py +1245 -0
- code_review_graph/jedi_resolver.py +303 -0
- code_review_graph/main.py +1079 -0
- code_review_graph/memory.py +142 -0
- code_review_graph/migrations.py +284 -0
- code_review_graph/parser.py +6957 -0
- code_review_graph/postprocessing.py +134 -0
- code_review_graph/prompts.py +159 -0
- code_review_graph/refactor.py +852 -0
- code_review_graph/registry.py +319 -0
- code_review_graph/rescript_resolver.py +206 -0
- code_review_graph/search.py +447 -0
- code_review_graph/skills.py +1481 -0
- code_review_graph/spring_resolver.py +200 -0
- code_review_graph/temporal_resolver.py +199 -0
- code_review_graph/token_benchmark.py +125 -0
- code_review_graph/tools/__init__.py +156 -0
- code_review_graph/tools/_common.py +176 -0
- code_review_graph/tools/analysis_tools.py +184 -0
- code_review_graph/tools/build.py +541 -0
- code_review_graph/tools/community_tools.py +246 -0
- code_review_graph/tools/context.py +152 -0
- code_review_graph/tools/docs.py +274 -0
- code_review_graph/tools/flows_tools.py +176 -0
- code_review_graph/tools/query.py +692 -0
- code_review_graph/tools/refactor_tools.py +168 -0
- code_review_graph/tools/registry_tools.py +125 -0
- code_review_graph/tools/review.py +477 -0
- code_review_graph/tsconfig_resolver.py +257 -0
- code_review_graph/visualization.py +2184 -0
- code_review_graph/wiki.py +305 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/METADATA +718 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/RECORD +74 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/WHEEL +4 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/entry_points.txt +3 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,2184 @@
|
|
|
1
|
+
"""Interactive D3.js graph visualization for code knowledge graphs.
|
|
2
|
+
|
|
3
|
+
Exports graph data to JSON and generates a self-contained HTML file with
|
|
4
|
+
a force-directed D3.js visualization. Dark theme, zoomable, draggable,
|
|
5
|
+
with collapsible file clusters, tooltips, legend, and stats bar.
|
|
6
|
+
|
|
7
|
+
Supports multiple rendering modes for large graphs:
|
|
8
|
+
- ``full`` — render every node (default, current behavior)
|
|
9
|
+
- ``community`` — aggregate by community; double-click to drill down
|
|
10
|
+
- ``file`` — aggregate by file; each file is a node
|
|
11
|
+
- ``auto`` — choose community mode when node count exceeds threshold
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import sqlite3
|
|
19
|
+
from collections import Counter, defaultdict
|
|
20
|
+
from dataclasses import asdict
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from .graph import GraphStore, edge_to_dict, node_to_dict
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _build_name_index(
|
|
29
|
+
nodes: list[dict], seen_qn: set[str]
|
|
30
|
+
) -> dict[str, list[str]]:
|
|
31
|
+
"""Build a mapping from short/module-style names to qualified names.
|
|
32
|
+
|
|
33
|
+
Returns ``{short_name: [qualified_name, ...]}``.
|
|
34
|
+
"""
|
|
35
|
+
index: dict[str, list[str]] = {}
|
|
36
|
+
|
|
37
|
+
def _add(key: str, qn: str) -> None:
|
|
38
|
+
index.setdefault(key, []).append(qn)
|
|
39
|
+
|
|
40
|
+
for n in nodes:
|
|
41
|
+
qn = n["qualified_name"]
|
|
42
|
+
_add(n["name"], qn)
|
|
43
|
+
# Index by "file::name" suffix (e.g. "cli.py::main")
|
|
44
|
+
if "::" in qn:
|
|
45
|
+
_add(qn.rsplit("/", 1)[-1], qn)
|
|
46
|
+
# Index by module-style path (e.g. "merit.cli" or "merit.cli.main")
|
|
47
|
+
fp = n.get("file_path", "")
|
|
48
|
+
if fp:
|
|
49
|
+
mod = fp.replace("/", ".").replace(".py", "")
|
|
50
|
+
if n["kind"] == "File":
|
|
51
|
+
_add(mod, qn)
|
|
52
|
+
# Index by every path suffix so C/C++ bare includes resolve.
|
|
53
|
+
# e.g. "/abs/libs/trading/Foo.hpp" is also indexed as
|
|
54
|
+
# "Foo.hpp", "trading/Foo.hpp", "libs/trading/Foo.hpp", …
|
|
55
|
+
parts = fp.replace("\\", "/").split("/")
|
|
56
|
+
for i in range(len(parts)):
|
|
57
|
+
suffix = "/".join(parts[i:])
|
|
58
|
+
if suffix:
|
|
59
|
+
_add(suffix, qn)
|
|
60
|
+
else:
|
|
61
|
+
_add(mod + "." + n["name"], qn)
|
|
62
|
+
return index
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _resolve_target(
|
|
66
|
+
target: str,
|
|
67
|
+
source: str,
|
|
68
|
+
seen_qn: set[str],
|
|
69
|
+
name_index: dict[str, list[str]],
|
|
70
|
+
) -> str | None:
|
|
71
|
+
"""Try to resolve an unqualified edge target to a full qualified name.
|
|
72
|
+
|
|
73
|
+
Returns the resolved qualified name, or None if unresolvable.
|
|
74
|
+
"""
|
|
75
|
+
# Already fully qualified
|
|
76
|
+
if target in seen_qn:
|
|
77
|
+
return target
|
|
78
|
+
|
|
79
|
+
candidates = name_index.get(target)
|
|
80
|
+
if not candidates:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
if len(candidates) == 1:
|
|
84
|
+
return candidates[0]
|
|
85
|
+
|
|
86
|
+
# Disambiguate: prefer node in the same file as the source
|
|
87
|
+
src_file = source.split("::")[0] if "::" in source else source
|
|
88
|
+
same_file = [c for c in candidates if c.startswith(src_file)]
|
|
89
|
+
if len(same_file) == 1:
|
|
90
|
+
return same_file[0]
|
|
91
|
+
|
|
92
|
+
# Prefer node in the same top-level directory
|
|
93
|
+
src_parts = src_file.rsplit("/", 1)[0] if "/" in src_file else ""
|
|
94
|
+
same_dir = [c for c in candidates if c.startswith(src_parts)]
|
|
95
|
+
if len(same_dir) == 1:
|
|
96
|
+
return same_dir[0]
|
|
97
|
+
|
|
98
|
+
# Ambiguous — pick first match rather than dropping the edge
|
|
99
|
+
return candidates[0]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def export_graph_data(store: GraphStore) -> dict:
|
|
103
|
+
"""Export all graph nodes and edges as a JSON-serializable dict.
|
|
104
|
+
|
|
105
|
+
Returns ``{"nodes": [...], "edges": [...], "stats": {...},
|
|
106
|
+
"flows": [...], "communities": [...]}``.
|
|
107
|
+
"""
|
|
108
|
+
nodes = []
|
|
109
|
+
seen_qn: set[str] = set()
|
|
110
|
+
|
|
111
|
+
# Preload community_id mapping from DB (column may not exist in old schemas)
|
|
112
|
+
community_map = store.get_all_community_ids()
|
|
113
|
+
|
|
114
|
+
for file_path in store.get_all_files():
|
|
115
|
+
for gnode in store.get_nodes_by_file(file_path):
|
|
116
|
+
if gnode.qualified_name in seen_qn:
|
|
117
|
+
continue
|
|
118
|
+
seen_qn.add(gnode.qualified_name)
|
|
119
|
+
d = node_to_dict(gnode)
|
|
120
|
+
d["params"] = gnode.params
|
|
121
|
+
d["return_type"] = gnode.return_type
|
|
122
|
+
d["community_id"] = community_map.get(gnode.qualified_name)
|
|
123
|
+
nodes.append(d)
|
|
124
|
+
|
|
125
|
+
name_index = _build_name_index(nodes, seen_qn)
|
|
126
|
+
|
|
127
|
+
all_edges = [edge_to_dict(e) for e in store.get_all_edges()]
|
|
128
|
+
|
|
129
|
+
# Resolve short/unqualified edge targets to full qualified names,
|
|
130
|
+
# then drop edges that still can't be resolved (external/stdlib calls).
|
|
131
|
+
edges = []
|
|
132
|
+
for e in all_edges:
|
|
133
|
+
src = _resolve_target(e["source"], e["source"], seen_qn, name_index)
|
|
134
|
+
tgt = _resolve_target(e["target"], e["source"], seen_qn, name_index)
|
|
135
|
+
if src and tgt:
|
|
136
|
+
e["source"] = src
|
|
137
|
+
e["target"] = tgt
|
|
138
|
+
edges.append(e)
|
|
139
|
+
|
|
140
|
+
stats = store.get_stats()
|
|
141
|
+
|
|
142
|
+
# Include flows (graceful fallback if table doesn't exist)
|
|
143
|
+
try:
|
|
144
|
+
from code_review_graph.flows import get_flows
|
|
145
|
+
flows = get_flows(store, limit=100)
|
|
146
|
+
except (ImportError, sqlite3.OperationalError) as exc:
|
|
147
|
+
logger.debug("flows unavailable for export: %s", exc)
|
|
148
|
+
flows = []
|
|
149
|
+
|
|
150
|
+
# Include communities (graceful fallback if table doesn't exist)
|
|
151
|
+
try:
|
|
152
|
+
from code_review_graph.communities import get_communities
|
|
153
|
+
communities = get_communities(store)
|
|
154
|
+
except (ImportError, sqlite3.OperationalError) as exc:
|
|
155
|
+
logger.debug("communities unavailable for export: %s", exc)
|
|
156
|
+
communities = []
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
"nodes": nodes,
|
|
160
|
+
"edges": edges,
|
|
161
|
+
"stats": asdict(stats),
|
|
162
|
+
"flows": flows,
|
|
163
|
+
"communities": communities,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _aggregate_community(data: dict) -> dict:
|
|
168
|
+
"""Aggregate full graph data into community-level super-nodes.
|
|
169
|
+
|
|
170
|
+
Each community becomes a single node sized by member count.
|
|
171
|
+
Edges between super-nodes represent the count of cross-community edges.
|
|
172
|
+
Returns a new dict with the same schema as *data* but fewer nodes/edges.
|
|
173
|
+
Also returns per-community detail data for drill-down rendering.
|
|
174
|
+
"""
|
|
175
|
+
communities = data.get("communities") or []
|
|
176
|
+
nodes = data["nodes"]
|
|
177
|
+
edges = data["edges"]
|
|
178
|
+
|
|
179
|
+
# Build mapping: qualified_name -> community_id
|
|
180
|
+
qn_to_cid: dict[str, int] = {}
|
|
181
|
+
for c in communities:
|
|
182
|
+
for qn in c.get("members", []):
|
|
183
|
+
qn_to_cid[qn] = c["id"]
|
|
184
|
+
|
|
185
|
+
# Also use node-level community_id for nodes not in community member lists
|
|
186
|
+
for n in nodes:
|
|
187
|
+
if n.get("community_id") is not None and n["qualified_name"] not in qn_to_cid:
|
|
188
|
+
qn_to_cid[n["qualified_name"]] = n["community_id"]
|
|
189
|
+
|
|
190
|
+
# Assign uncategorized nodes to a synthetic community id = -1
|
|
191
|
+
uncategorized_members: list[str] = []
|
|
192
|
+
for n in nodes:
|
|
193
|
+
if n["qualified_name"] not in qn_to_cid:
|
|
194
|
+
qn_to_cid[n["qualified_name"]] = -1
|
|
195
|
+
uncategorized_members.append(n["qualified_name"])
|
|
196
|
+
|
|
197
|
+
# Build community info map (including the synthetic uncategorized one)
|
|
198
|
+
cid_info: dict[int, dict] = {}
|
|
199
|
+
for c in communities:
|
|
200
|
+
cid_info[c["id"]] = c
|
|
201
|
+
if uncategorized_members:
|
|
202
|
+
cid_info[-1] = {
|
|
203
|
+
"id": -1,
|
|
204
|
+
"name": "Uncategorized",
|
|
205
|
+
"size": len(uncategorized_members),
|
|
206
|
+
"members": uncategorized_members,
|
|
207
|
+
"dominant_language": "",
|
|
208
|
+
"description": "Nodes not assigned to any community",
|
|
209
|
+
"cohesion": 0,
|
|
210
|
+
"level": 0,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Build super-nodes (one per community)
|
|
214
|
+
super_nodes = []
|
|
215
|
+
for cid, info in cid_info.items():
|
|
216
|
+
size = info.get("size", len(info.get("members", [])))
|
|
217
|
+
if size == 0:
|
|
218
|
+
continue
|
|
219
|
+
super_nodes.append({
|
|
220
|
+
"qualified_name": f"__community__{cid}",
|
|
221
|
+
"name": info.get("name", f"Community {cid}"),
|
|
222
|
+
"kind": "Community",
|
|
223
|
+
"file_path": "",
|
|
224
|
+
"line_start": None,
|
|
225
|
+
"line_end": None,
|
|
226
|
+
"language": info.get("dominant_language", ""),
|
|
227
|
+
"community_id": cid,
|
|
228
|
+
"member_count": size,
|
|
229
|
+
"description": info.get("description", ""),
|
|
230
|
+
"id": cid,
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
# Build super-edges: aggregate cross-community edges
|
|
234
|
+
cross_edge_counts: Counter[tuple[int, int]] = Counter()
|
|
235
|
+
for e in edges:
|
|
236
|
+
src_cid = qn_to_cid.get(e["source"])
|
|
237
|
+
tgt_cid = qn_to_cid.get(e["target"])
|
|
238
|
+
if src_cid is not None and tgt_cid is not None and src_cid != tgt_cid:
|
|
239
|
+
pair = (min(src_cid, tgt_cid), max(src_cid, tgt_cid))
|
|
240
|
+
cross_edge_counts[pair] += 1
|
|
241
|
+
|
|
242
|
+
super_edges = []
|
|
243
|
+
for (c1, c2), count in cross_edge_counts.items():
|
|
244
|
+
super_edges.append({
|
|
245
|
+
"source": f"__community__{c1}",
|
|
246
|
+
"target": f"__community__{c2}",
|
|
247
|
+
"kind": "CROSS_COMMUNITY",
|
|
248
|
+
"weight": count,
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
# Build per-community detail data for drill-down
|
|
252
|
+
community_details: dict[int, dict] = {}
|
|
253
|
+
cid_members_set: dict[int, set[str]] = defaultdict(set)
|
|
254
|
+
for qn, cid in qn_to_cid.items():
|
|
255
|
+
cid_members_set[cid].add(qn)
|
|
256
|
+
|
|
257
|
+
for cid, member_qns in cid_members_set.items():
|
|
258
|
+
detail_nodes = [n for n in nodes if n["qualified_name"] in member_qns]
|
|
259
|
+
detail_edges = [
|
|
260
|
+
e for e in edges
|
|
261
|
+
if e["source"] in member_qns and e["target"] in member_qns
|
|
262
|
+
]
|
|
263
|
+
community_details[cid] = {
|
|
264
|
+
"nodes": detail_nodes,
|
|
265
|
+
"edges": detail_edges,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
"nodes": super_nodes,
|
|
270
|
+
"edges": super_edges,
|
|
271
|
+
"stats": data["stats"],
|
|
272
|
+
"flows": data.get("flows", []),
|
|
273
|
+
"communities": communities,
|
|
274
|
+
"mode": "community",
|
|
275
|
+
"community_details": {
|
|
276
|
+
str(k): v for k, v in community_details.items()
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _aggregate_file(data: dict) -> dict:
|
|
282
|
+
"""Aggregate full graph data into file-level nodes.
|
|
283
|
+
|
|
284
|
+
Each file becomes a node sized by symbol count.
|
|
285
|
+
Edges between files represent aggregated cross-file dependencies.
|
|
286
|
+
"""
|
|
287
|
+
nodes = data["nodes"]
|
|
288
|
+
edges = data["edges"]
|
|
289
|
+
|
|
290
|
+
# Count symbols per file
|
|
291
|
+
file_symbol_count: Counter[str] = Counter()
|
|
292
|
+
qn_to_file: dict[str, str] = {}
|
|
293
|
+
file_languages: dict[str, str] = {}
|
|
294
|
+
|
|
295
|
+
for n in nodes:
|
|
296
|
+
fp = n.get("file_path", "")
|
|
297
|
+
if not fp:
|
|
298
|
+
continue
|
|
299
|
+
qn_to_file[n["qualified_name"]] = fp
|
|
300
|
+
if n["kind"] != "File":
|
|
301
|
+
file_symbol_count[fp] += 1
|
|
302
|
+
else:
|
|
303
|
+
file_symbol_count.setdefault(fp, 0)
|
|
304
|
+
if n.get("language"):
|
|
305
|
+
file_languages[fp] = n["language"]
|
|
306
|
+
|
|
307
|
+
# Build file nodes
|
|
308
|
+
file_nodes = []
|
|
309
|
+
for fp, count in file_symbol_count.items():
|
|
310
|
+
parts = fp.replace("\\", "/").split("/")
|
|
311
|
+
short = parts[-1] if parts else fp
|
|
312
|
+
parent = parts[-2] if len(parts) >= 2 else ""
|
|
313
|
+
label = f"{parent}/{short}" if parent else short
|
|
314
|
+
# Recover community_id from the majority of symbols in this file
|
|
315
|
+
cid = None
|
|
316
|
+
for n in nodes:
|
|
317
|
+
if n.get("file_path") == fp and n.get("community_id") is not None:
|
|
318
|
+
cid = n["community_id"]
|
|
319
|
+
break
|
|
320
|
+
file_nodes.append({
|
|
321
|
+
"qualified_name": fp,
|
|
322
|
+
"name": label,
|
|
323
|
+
"kind": "File",
|
|
324
|
+
"file_path": fp,
|
|
325
|
+
"line_start": None,
|
|
326
|
+
"line_end": None,
|
|
327
|
+
"language": file_languages.get(fp, ""),
|
|
328
|
+
"community_id": cid,
|
|
329
|
+
"symbol_count": count,
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
# Aggregate cross-file edges
|
|
333
|
+
cross_file_counts: Counter[tuple[str, str]] = Counter()
|
|
334
|
+
for e in edges:
|
|
335
|
+
src_fp = qn_to_file.get(e["source"])
|
|
336
|
+
tgt_fp = qn_to_file.get(e["target"])
|
|
337
|
+
if src_fp and tgt_fp and src_fp != tgt_fp:
|
|
338
|
+
pair = (src_fp, tgt_fp)
|
|
339
|
+
cross_file_counts[pair] += 1
|
|
340
|
+
|
|
341
|
+
file_edges = []
|
|
342
|
+
for (f1, f2), count in cross_file_counts.items():
|
|
343
|
+
file_edges.append({
|
|
344
|
+
"source": f1,
|
|
345
|
+
"target": f2,
|
|
346
|
+
"kind": "DEPENDS_ON",
|
|
347
|
+
"weight": count,
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
"nodes": file_nodes,
|
|
352
|
+
"edges": file_edges,
|
|
353
|
+
"stats": data["stats"],
|
|
354
|
+
"flows": data.get("flows", []),
|
|
355
|
+
"communities": data.get("communities", []),
|
|
356
|
+
"mode": "file",
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def generate_html(
|
|
361
|
+
store: GraphStore,
|
|
362
|
+
output_path: str | Path,
|
|
363
|
+
mode: str = "auto",
|
|
364
|
+
max_full_nodes: int = 3000,
|
|
365
|
+
) -> Path:
|
|
366
|
+
"""Generate a self-contained interactive HTML visualization.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
store: The GraphStore to read graph data from.
|
|
370
|
+
output_path: Path for the output HTML file.
|
|
371
|
+
mode: Rendering mode — ``"auto"``, ``"full"``, ``"community"``,
|
|
372
|
+
or ``"file"``. ``"auto"`` switches to ``"community"`` when
|
|
373
|
+
the node count exceeds *max_full_nodes*.
|
|
374
|
+
max_full_nodes: Threshold for auto-switching to community mode.
|
|
375
|
+
|
|
376
|
+
Writes the HTML file to *output_path* and returns the resolved Path.
|
|
377
|
+
"""
|
|
378
|
+
output_path = Path(output_path)
|
|
379
|
+
stats = store.get_stats()
|
|
380
|
+
if stats.total_nodes > 50000:
|
|
381
|
+
logger.warning(
|
|
382
|
+
"Graph has %d nodes — visualization may be slow. "
|
|
383
|
+
"Consider filtering by file pattern.", stats.total_nodes,
|
|
384
|
+
)
|
|
385
|
+
data = export_graph_data(store)
|
|
386
|
+
|
|
387
|
+
# Determine effective mode
|
|
388
|
+
effective_mode = mode
|
|
389
|
+
if effective_mode == "auto":
|
|
390
|
+
effective_mode = (
|
|
391
|
+
"community" if stats.total_nodes > max_full_nodes else "full"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
if effective_mode == "community":
|
|
395
|
+
# Keep full data available for drill-down; aggregate for top-level
|
|
396
|
+
agg = _aggregate_community(data)
|
|
397
|
+
# Escape </script> inside JSON to prevent premature tag closure
|
|
398
|
+
data_json = json.dumps(agg, default=str).replace("</", "<\\/")
|
|
399
|
+
html = _AGGREGATED_HTML_TEMPLATE.replace("__GRAPH_DATA__", data_json)
|
|
400
|
+
elif effective_mode == "file":
|
|
401
|
+
agg = _aggregate_file(data)
|
|
402
|
+
data_json = json.dumps(agg, default=str).replace("</", "<\\/")
|
|
403
|
+
html = _AGGREGATED_HTML_TEMPLATE.replace("__GRAPH_DATA__", data_json)
|
|
404
|
+
else:
|
|
405
|
+
# full mode — original behavior
|
|
406
|
+
data_json = json.dumps(data, default=str).replace("</", "<\\/")
|
|
407
|
+
html = _HTML_TEMPLATE.replace("__GRAPH_DATA__", data_json)
|
|
408
|
+
|
|
409
|
+
output_path.write_text(html, encoding="utf-8")
|
|
410
|
+
return output_path
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# ---------------------------------------------------------------------------
|
|
414
|
+
# Full D3.js interactive HTML template
|
|
415
|
+
# ---------------------------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
# Template lives in this file for zero-dependency packaging (no external files
|
|
418
|
+
# to locate at runtime). The E501 suppression for this module is configured via
|
|
419
|
+
# pyproject.toml per-file-ignores for this reason.
|
|
420
|
+
|
|
421
|
+
_HTML_TEMPLATE = r"""<!DOCTYPE html>
|
|
422
|
+
<html lang="en">
|
|
423
|
+
<head>
|
|
424
|
+
<meta charset="utf-8">
|
|
425
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
426
|
+
<title>Code Review Graph</title>
|
|
427
|
+
<script src="https://d3js.org/d3.v7.min.js" integrity="sha384-CjloA8y00+1SDAUkjs099PVfnY2KmDC2BZnws9kh8D/lX1s46w6EPhpXdqMfjK6i" crossorigin="anonymous"></script>
|
|
428
|
+
<style>
|
|
429
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
430
|
+
html, body { width: 100%; height: 100%; overflow: hidden; }
|
|
431
|
+
body {
|
|
432
|
+
background: #0d1117; color: #c9d1d9;
|
|
433
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
434
|
+
font-size: 13px;
|
|
435
|
+
}
|
|
436
|
+
svg { display: block; width: 100%; height: 100%; }
|
|
437
|
+
#legend {
|
|
438
|
+
position: absolute; top: 16px; left: 16px;
|
|
439
|
+
background: rgba(22,27,34,0.95); border: 1px solid #30363d;
|
|
440
|
+
border-radius: 10px; padding: 16px 20px;
|
|
441
|
+
font-size: 12px; line-height: 1.8;
|
|
442
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
|
443
|
+
backdrop-filter: blur(12px); z-index: 10;
|
|
444
|
+
}
|
|
445
|
+
#legend h3 {
|
|
446
|
+
font-size: 11px; font-weight: 700; margin-bottom: 6px;
|
|
447
|
+
color: #9eaab6; text-transform: uppercase; letter-spacing: 1px;
|
|
448
|
+
}
|
|
449
|
+
.legend-section { margin-bottom: 10px; }
|
|
450
|
+
.legend-section:last-child { margin-bottom: 0; }
|
|
451
|
+
.legend-item { display: flex; align-items: center; gap: 10px; padding: 2px 0; cursor: default; }
|
|
452
|
+
.legend-item[data-edge-kind] { cursor: pointer; user-select: none; }
|
|
453
|
+
.legend-item[data-edge-kind].dimmed { opacity: 0.3; }
|
|
454
|
+
.legend-item svg { flex-shrink: 0; }
|
|
455
|
+
.legend-line { width: 24px; height: 0; flex-shrink: 0; border-top-width: 2px; }
|
|
456
|
+
.l-calls { border-top: 2px solid #3fb950; }
|
|
457
|
+
.l-imports { border-top: 2px dashed #f0883e; }
|
|
458
|
+
.l-inherits { border-top: 2.5px dotted #d2a8ff; }
|
|
459
|
+
.l-contains { border-top: 1px solid rgba(139,148,158,0.3); }
|
|
460
|
+
#stats-bar {
|
|
461
|
+
position: absolute; bottom: 0; left: 0; right: 0;
|
|
462
|
+
background: rgba(13,17,23,0.95); border-top: 1px solid #21262d;
|
|
463
|
+
padding: 8px 24px; display: flex; gap: 32px; justify-content: center;
|
|
464
|
+
font-size: 12px; color: #9eaab6; backdrop-filter: blur(12px);
|
|
465
|
+
}
|
|
466
|
+
.stat-item { display: flex; gap: 6px; align-items: center; }
|
|
467
|
+
.stat-value { color: #e6edf3; font-weight: 600; }
|
|
468
|
+
#tooltip {
|
|
469
|
+
position: absolute; pointer-events: none;
|
|
470
|
+
background: rgba(22,27,34,0.97); color: #c9d1d9;
|
|
471
|
+
border: 1px solid #30363d; border-radius: 8px;
|
|
472
|
+
padding: 12px 16px; font-size: 12px;
|
|
473
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
|
474
|
+
max-width: 360px; line-height: 1.7;
|
|
475
|
+
opacity: 0; transition: opacity 0.15s ease;
|
|
476
|
+
z-index: 1000; backdrop-filter: blur(12px);
|
|
477
|
+
}
|
|
478
|
+
#tooltip.visible { opacity: 1; }
|
|
479
|
+
.tt-name { font-weight: 700; font-size: 14px; color: #e6edf3; }
|
|
480
|
+
.tt-kind {
|
|
481
|
+
display: inline-block; font-size: 9px; font-weight: 700;
|
|
482
|
+
padding: 2px 8px; border-radius: 10px; margin-left: 8px;
|
|
483
|
+
text-transform: uppercase; letter-spacing: 0.5px;
|
|
484
|
+
}
|
|
485
|
+
.tt-row { margin-top: 4px; }
|
|
486
|
+
.tt-label { color: #9eaab6; }
|
|
487
|
+
.tt-file { color: #58a6ff; font-size: 11px; }
|
|
488
|
+
#controls {
|
|
489
|
+
position: absolute; top: 16px; right: 16px;
|
|
490
|
+
display: flex; gap: 8px; z-index: 10; flex-wrap: wrap;
|
|
491
|
+
max-width: 650px; justify-content: flex-end;
|
|
492
|
+
}
|
|
493
|
+
#controls button, #controls select {
|
|
494
|
+
background: rgba(22,27,34,0.95); color: #c9d1d9;
|
|
495
|
+
border: 1px solid #30363d; border-radius: 8px;
|
|
496
|
+
padding: 8px 14px; font-size: 12px; cursor: pointer;
|
|
497
|
+
backdrop-filter: blur(12px); transition: all 0.15s;
|
|
498
|
+
}
|
|
499
|
+
#controls button:hover, #controls select:hover { background: #30363d; border-color: #8b949e; }
|
|
500
|
+
#controls button.active { background: #1f6feb; border-color: #58a6ff; color: #fff; }
|
|
501
|
+
#controls select { outline: none; max-width: 200px; }
|
|
502
|
+
#controls select option { background: #161b22; color: #c9d1d9; }
|
|
503
|
+
#search {
|
|
504
|
+
background: rgba(22,27,34,0.95); color: #c9d1d9;
|
|
505
|
+
border: 1px solid #30363d; border-radius: 8px;
|
|
506
|
+
padding: 8px 14px; font-size: 12px; width: 220px;
|
|
507
|
+
outline: none; backdrop-filter: blur(12px);
|
|
508
|
+
}
|
|
509
|
+
#search:focus { border-color: #58a6ff; }
|
|
510
|
+
#search::placeholder { color: #6e7681; }
|
|
511
|
+
#search-results {
|
|
512
|
+
position: absolute; top: 52px; right: 16px;
|
|
513
|
+
background: rgba(22,27,34,0.97); border: 1px solid #30363d;
|
|
514
|
+
border-radius: 8px; max-height: 240px; overflow-y: auto;
|
|
515
|
+
z-index: 15; display: none; min-width: 220px;
|
|
516
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
|
517
|
+
}
|
|
518
|
+
.sr-item {
|
|
519
|
+
padding: 8px 14px; cursor: pointer; font-size: 12px;
|
|
520
|
+
border-bottom: 1px solid #21262d; display: flex; gap: 8px; align-items: center;
|
|
521
|
+
}
|
|
522
|
+
.sr-item:hover { background: #30363d; }
|
|
523
|
+
.sr-item:last-child { border-bottom: none; }
|
|
524
|
+
.sr-kind { font-size: 9px; padding: 2px 6px; border-radius: 8px; text-transform: uppercase; font-weight: 700; }
|
|
525
|
+
#detail-panel {
|
|
526
|
+
position: absolute; top: 16px; left: 16px;
|
|
527
|
+
width: 320px; max-height: calc(100vh - 80px);
|
|
528
|
+
background: rgba(22,27,34,0.97); border: 1px solid #30363d;
|
|
529
|
+
border-radius: 10px; padding: 20px;
|
|
530
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
|
531
|
+
backdrop-filter: blur(12px); z-index: 15;
|
|
532
|
+
overflow-y: auto; display: none; font-size: 12px;
|
|
533
|
+
}
|
|
534
|
+
#detail-panel.visible { display: block; }
|
|
535
|
+
#detail-panel h2 { font-size: 16px; color: #e6edf3; margin-bottom: 4px; word-break: break-all; }
|
|
536
|
+
#detail-panel .dp-close {
|
|
537
|
+
position: absolute; top: 12px; right: 14px;
|
|
538
|
+
cursor: pointer; color: #8b949e; font-size: 14px; line-height: 1;
|
|
539
|
+
border: 1px solid #30363d; background: rgba(22,27,34,0.95);
|
|
540
|
+
border-radius: 6px; width: 28px; height: 28px;
|
|
541
|
+
display: flex; align-items: center; justify-content: center;
|
|
542
|
+
transition: all 0.15s;
|
|
543
|
+
}
|
|
544
|
+
#detail-panel .dp-close:hover { color: #e6edf3; border-color: #8b949e; background: #30363d; }
|
|
545
|
+
.dp-section { margin-top: 14px; }
|
|
546
|
+
.dp-section h4 { color: #9eaab6; font-size: 10px; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 6px; }
|
|
547
|
+
.dp-list { list-style: none; }
|
|
548
|
+
.dp-list li { padding: 3px 0; color: #c9d1d9; cursor: pointer; }
|
|
549
|
+
.dp-list li:hover { color: #58a6ff; text-decoration: underline; }
|
|
550
|
+
.dp-meta { color: #9eaab6; }
|
|
551
|
+
.dp-meta span { color: #e6edf3; font-weight: 600; }
|
|
552
|
+
#filter-panel {
|
|
553
|
+
position: absolute; bottom: 50px; left: 16px;
|
|
554
|
+
background: rgba(22,27,34,0.95); border: 1px solid #30363d;
|
|
555
|
+
border-radius: 10px; padding: 14px 18px;
|
|
556
|
+
font-size: 12px; backdrop-filter: blur(12px); z-index: 10;
|
|
557
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
|
558
|
+
}
|
|
559
|
+
#filter-panel h3 {
|
|
560
|
+
font-size: 11px; font-weight: 700; margin-bottom: 8px;
|
|
561
|
+
color: #9eaab6; text-transform: uppercase; letter-spacing: 1px;
|
|
562
|
+
}
|
|
563
|
+
.filter-item { display: flex; align-items: center; gap: 8px; padding: 3px 0; cursor: pointer; user-select: none; }
|
|
564
|
+
.filter-item input { accent-color: #58a6ff; cursor: pointer; }
|
|
565
|
+
:focus-visible { outline: 2px solid #58a6ff; outline-offset: 2px; }
|
|
566
|
+
.filter-item input:focus-visible { outline: 2px solid #58a6ff; outline-offset: 2px; }
|
|
567
|
+
.dp-close:focus-visible { outline: 2px solid #58a6ff; outline-offset: 2px; }
|
|
568
|
+
.filter-item:focus-within { outline: 2px solid #58a6ff; outline-offset: 2px; border-radius: 4px; }
|
|
569
|
+
#help-overlay {
|
|
570
|
+
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
|
|
571
|
+
display: flex; align-items: center; justify-content: center; z-index: 100;
|
|
572
|
+
backdrop-filter: blur(4px);
|
|
573
|
+
}
|
|
574
|
+
#help-overlay.hidden { display: none; }
|
|
575
|
+
.help-content {
|
|
576
|
+
position: relative; background: #161b22; border: 1px solid #30363d;
|
|
577
|
+
border-radius: 12px; padding: 28px 32px; max-width: 420px; width: 90%;
|
|
578
|
+
box-shadow: 0 16px 48px rgba(0,0,0,0.5);
|
|
579
|
+
}
|
|
580
|
+
.help-content h2 { font-size: 16px; color: #e6edf3; margin-bottom: 16px; }
|
|
581
|
+
.help-content .help-close { position: absolute; top: 12px; right: 14px; }
|
|
582
|
+
.help-content table { width: 100%; border-collapse: collapse; }
|
|
583
|
+
.help-content td { padding: 6px 8px; color: #c9d1d9; font-size: 13px; border-bottom: 1px solid #21262d; }
|
|
584
|
+
.help-content td:first-child { white-space: nowrap; width: 1%; }
|
|
585
|
+
kbd {
|
|
586
|
+
display: inline-block; background: #21262d; border: 1px solid #30363d;
|
|
587
|
+
border-radius: 5px; padding: 2px 7px; font-size: 11px; font-family: inherit;
|
|
588
|
+
color: #e6edf3; box-shadow: inset 0 -1px 0 #0d1117; line-height: 1.6;
|
|
589
|
+
}
|
|
590
|
+
.help-dismiss {
|
|
591
|
+
margin-top: 16px; display: block; text-align: center;
|
|
592
|
+
color: #8b949e; font-size: 12px; cursor: pointer;
|
|
593
|
+
}
|
|
594
|
+
.help-dismiss:hover { color: #e6edf3; }
|
|
595
|
+
#loading-overlay {
|
|
596
|
+
position: fixed; inset: 0; display: flex; align-items: center;
|
|
597
|
+
justify-content: center; z-index: 50; background: rgba(13,17,23,0.85);
|
|
598
|
+
backdrop-filter: blur(6px); transition: opacity 0.4s ease;
|
|
599
|
+
}
|
|
600
|
+
#loading-overlay.hidden { opacity: 0; pointer-events: none; }
|
|
601
|
+
.loading-spinner {
|
|
602
|
+
width: 36px; height: 36px; border: 3px solid #30363d;
|
|
603
|
+
border-top-color: #58a6ff; border-radius: 50%;
|
|
604
|
+
animation: spin 0.8s linear infinite;
|
|
605
|
+
}
|
|
606
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
607
|
+
.loading-text { color: #9eaab6; font-size: 13px; margin-top: 14px; text-align: center; }
|
|
608
|
+
#empty-state {
|
|
609
|
+
position: fixed; inset: 0; display: none; align-items: center;
|
|
610
|
+
justify-content: center; z-index: 50; flex-direction: column; gap: 12px;
|
|
611
|
+
}
|
|
612
|
+
#empty-state.visible { display: flex; }
|
|
613
|
+
.empty-icon { font-size: 48px; opacity: 0.4; }
|
|
614
|
+
.empty-title { color: #e6edf3; font-size: 18px; font-weight: 600; }
|
|
615
|
+
.empty-desc { color: #9eaab6; font-size: 13px; max-width: 320px; text-align: center; line-height: 1.6; }
|
|
616
|
+
#community-legend {
|
|
617
|
+
position: absolute; bottom: 50px; right: 16px;
|
|
618
|
+
background: rgba(22,27,34,0.95);
|
|
619
|
+
border: 1px solid #30363d;
|
|
620
|
+
border-radius: 10px;
|
|
621
|
+
padding: 14px 18px; font-size: 12px;
|
|
622
|
+
backdrop-filter: blur(12px); z-index: 10;
|
|
623
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
|
624
|
+
max-height: 300px; overflow-y: auto;
|
|
625
|
+
display: none;
|
|
626
|
+
}
|
|
627
|
+
#community-legend.visible { display: block; }
|
|
628
|
+
#community-legend h3 {
|
|
629
|
+
font-size: 11px; font-weight: 700;
|
|
630
|
+
margin-bottom: 8px; color: #8b949e;
|
|
631
|
+
text-transform: uppercase; letter-spacing: 1px;
|
|
632
|
+
}
|
|
633
|
+
.cl-item {
|
|
634
|
+
display: flex; align-items: center;
|
|
635
|
+
gap: 8px; padding: 3px 0;
|
|
636
|
+
cursor: pointer; user-select: none;
|
|
637
|
+
}
|
|
638
|
+
.cl-item input {
|
|
639
|
+
accent-color: #58a6ff; cursor: pointer;
|
|
640
|
+
}
|
|
641
|
+
.cl-swatch {
|
|
642
|
+
width: 10px; height: 10px;
|
|
643
|
+
border-radius: 50%; flex-shrink: 0;
|
|
644
|
+
}
|
|
645
|
+
marker { overflow: visible; }
|
|
646
|
+
g.node-g:focus { outline: none; }
|
|
647
|
+
g.node-g:focus-visible .node-shape { stroke: #58a6ff !important; stroke-width: 3 !important; }
|
|
648
|
+
g.node-g:focus-visible .glow-ring { stroke: #58a6ff !important; opacity: 0.6 !important; }
|
|
649
|
+
button.legend-edge { background: none; border: none; color: #c9d1d9; font-size: 12px; font-family: inherit; }
|
|
650
|
+
button.legend-edge:focus-visible { outline: 2px solid #58a6ff; outline-offset: 2px; border-radius: 4px; }
|
|
651
|
+
.sr-item.sr-active { background: #30363d; }
|
|
652
|
+
.skip-link {
|
|
653
|
+
position: absolute; top: -40px; left: 16px; z-index: 100;
|
|
654
|
+
background: #1f6feb; color: #fff; padding: 8px 16px;
|
|
655
|
+
border-radius: 0 0 8px 8px; text-decoration: none; font-weight: 600;
|
|
656
|
+
transition: top 0.2s;
|
|
657
|
+
}
|
|
658
|
+
.skip-link:focus { top: 0; }
|
|
659
|
+
</style>
|
|
660
|
+
</head>
|
|
661
|
+
<body>
|
|
662
|
+
<a href="#graph-svg" class="skip-link">Skip to graph</a>
|
|
663
|
+
<nav id="legend" aria-label="Graph legend">
|
|
664
|
+
<h3>Nodes</h3>
|
|
665
|
+
<div class="legend-section">
|
|
666
|
+
<div class="legend-item"><svg width="16" height="16" viewBox="-8 -8 16 16" aria-hidden="true"><circle r="6" fill="#58a6ff"/></svg> File</div>
|
|
667
|
+
<div class="legend-item"><svg width="16" height="16" viewBox="-8 -8 16 16" aria-hidden="true"><rect x="-5" y="-5" width="10" height="10" fill="#f0883e"/></svg> Class</div>
|
|
668
|
+
<div class="legend-item"><svg width="16" height="16" viewBox="-8 -8 16 16" aria-hidden="true"><polygon points="0,-6 6,5 -6,5" fill="#3fb950"/></svg> Function</div>
|
|
669
|
+
<div class="legend-item"><svg width="16" height="16" viewBox="-8 -8 16 16" aria-hidden="true"><polygon points="0,-6 6,0 0,6 -6,0" fill="#d2a8ff"/></svg> Test</div>
|
|
670
|
+
<div class="legend-item"><svg width="16" height="16" viewBox="-8 -8 16 16" aria-hidden="true"><path d="M-2,-6v4h-4v4h4v4h4v-4h4v-4h-4v-4z" fill="#8b949e"/></svg> Type</div>
|
|
671
|
+
</div>
|
|
672
|
+
<h3>Edges</h3>
|
|
673
|
+
<div class="legend-section">
|
|
674
|
+
<button class="legend-item legend-edge" data-edge-kind="CALLS" aria-pressed="true"><span class="legend-line l-calls"></span> Calls</button>
|
|
675
|
+
<button class="legend-item legend-edge" data-edge-kind="IMPORTS_FROM" aria-pressed="true"><span class="legend-line l-imports"></span> Imports</button>
|
|
676
|
+
<button class="legend-item legend-edge" data-edge-kind="INHERITS" aria-pressed="true"><span class="legend-line l-inherits"></span> Inherits</button>
|
|
677
|
+
<button class="legend-item legend-edge" data-edge-kind="CONTAINS" aria-pressed="true"><span class="legend-line l-contains"></span> Contains</button>
|
|
678
|
+
<button class="legend-item legend-edge" data-edge-kind="IMPLEMENTS" aria-pressed="true"><span class="legend-line" style="border-top:2px dashed #f9e2af"></span> Implements</button>
|
|
679
|
+
<button class="legend-item legend-edge" data-edge-kind="TESTED_BY" aria-pressed="true"><span class="legend-line" style="border-top:2px dotted #f38ba8"></span> Tested By</button>
|
|
680
|
+
<button class="legend-item legend-edge" data-edge-kind="DEPENDS_ON" aria-pressed="true"><span class="legend-line" style="border-top:2px dashed #fab387"></span> Depends On</button>
|
|
681
|
+
</div>
|
|
682
|
+
</nav>
|
|
683
|
+
<div id="filter-panel">
|
|
684
|
+
<h3>Filter by Kind</h3>
|
|
685
|
+
<label class="filter-item"><input type="checkbox" data-kind="File" checked> File</label>
|
|
686
|
+
<label class="filter-item"><input type="checkbox" data-kind="Class" checked> Class</label>
|
|
687
|
+
<label class="filter-item"><input type="checkbox" data-kind="Function" checked> Function</label>
|
|
688
|
+
<label class="filter-item"><input type="checkbox" data-kind="Test" checked> Test</label>
|
|
689
|
+
<label class="filter-item"><input type="checkbox" data-kind="Type" checked> Type</label>
|
|
690
|
+
</div>
|
|
691
|
+
<div id="controls">
|
|
692
|
+
<input id="search" type="text" placeholder="Search nodes…" autocomplete="off" spellcheck="false" aria-label="Search graph nodes by name" aria-controls="search-results" aria-expanded="false">
|
|
693
|
+
<select id="flow-select" aria-label="Select execution flow to highlight"><option value="">Flows</option></select>
|
|
694
|
+
<button id="btn-community" title="Toggle community coloring" aria-label="Toggle community coloring" aria-pressed="false">Communities</button>
|
|
695
|
+
<button id="btn-fit" title="Fit to screen" aria-label="Fit graph to screen">Fit</button>
|
|
696
|
+
<button id="btn-labels" title="Toggle labels" class="active" aria-label="Toggle node labels" aria-pressed="true">Labels</button>
|
|
697
|
+
<button id="btn-help" title="Keyboard shortcuts" aria-label="Show keyboard shortcuts">?</button>
|
|
698
|
+
</div>
|
|
699
|
+
<div id="search-results" role="listbox" aria-label="Search results"></div>
|
|
700
|
+
<div id="detail-panel" role="dialog" aria-label="Node detail" aria-modal="false"><button class="dp-close" aria-label="Close detail panel">×</button><div id="dp-content" tabindex="-1"></div></div>
|
|
701
|
+
<div id="stats-bar" role="status" aria-label="Graph statistics"></div>
|
|
702
|
+
<div id="community-legend" aria-label="Community legend"></div>
|
|
703
|
+
<div id="tooltip" role="tooltip" aria-live="polite"></div>
|
|
704
|
+
<div id="help-overlay" class="hidden" role="dialog" aria-label="Help overlay" aria-modal="true">
|
|
705
|
+
<div class="help-content">
|
|
706
|
+
<h2>Graph Interactions</h2>
|
|
707
|
+
<button class="dp-close help-close" aria-label="Close help">×</button>
|
|
708
|
+
<table>
|
|
709
|
+
<tr><td>Click a file</td><td>Expand/collapse contained symbols</td></tr>
|
|
710
|
+
<tr><td>Click symbol</td><td>Show detail panel with callers/callees</td></tr>
|
|
711
|
+
<tr><td>Shift+click file</td><td>Show detail panel without toggling collapse</td></tr>
|
|
712
|
+
<tr><td>Hover</td><td>Highlight connected nodes and edges</td></tr>
|
|
713
|
+
<tr><td>Drag</td><td>Pin a node in place</td></tr>
|
|
714
|
+
<tr><td>Scroll</td><td>Zoom in/out</td></tr>
|
|
715
|
+
<tr><td>Click+drag background</td><td>Pan the view</td></tr>
|
|
716
|
+
<tr><td>Search</td><td>Type to filter — matching nodes stay bright</td></tr>
|
|
717
|
+
<tr><td>Legend edges</td><td>Click edge types in the legend to toggle visibility</td></tr>
|
|
718
|
+
</table>
|
|
719
|
+
<h2 style="margin-top:16px">Keyboard Shortcuts</h2>
|
|
720
|
+
<table>
|
|
721
|
+
<tr><td><kbd>/</kbd></td><td>Focus search</td></tr>
|
|
722
|
+
<tr><td><kbd>?</kbd></td><td>Toggle this help</td></tr>
|
|
723
|
+
<tr><td><kbd>Esc</kbd></td><td>Close panel / search / help</td></tr>
|
|
724
|
+
<tr><td><kbd>Enter</kbd> / <kbd>Space</kbd></td><td>Activate focused node</td></tr>
|
|
725
|
+
<tr><td><kbd>Arrow keys</kbd></td><td>Navigate between nodes</td></tr>
|
|
726
|
+
</table>
|
|
727
|
+
<span class="help-dismiss">Click anywhere outside to dismiss</span>
|
|
728
|
+
</div>
|
|
729
|
+
</div>
|
|
730
|
+
<div id="loading-overlay" aria-live="polite">
|
|
731
|
+
<div>
|
|
732
|
+
<div class="loading-spinner"></div>
|
|
733
|
+
<div class="loading-text">Laying out graph…</div>
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
<div id="empty-state" role="status">
|
|
737
|
+
<div class="empty-icon">🔍</div>
|
|
738
|
+
<div class="empty-title">No nodes to display</div>
|
|
739
|
+
<div class="empty-desc">The graph is empty. Run <strong>code-review-graph build</strong> to index your codebase, then regenerate the visualization.</div>
|
|
740
|
+
</div>
|
|
741
|
+
<svg id="graph-svg" tabindex="-1" role="img" aria-label="Interactive code knowledge graph visualization. Use search to find nodes, click files to expand."></svg>
|
|
742
|
+
<script>
|
|
743
|
+
"use strict";
|
|
744
|
+
var graphData = __GRAPH_DATA__;
|
|
745
|
+
var KIND_COLOR = { File:"#58a6ff", Class:"#f0883e", Function:"#3fb950", Test:"#d2a8ff", Type:"#8b949e" };
|
|
746
|
+
var KIND_RADIUS = { File:18, Class:12, Function:6, Test:6, Type:5 };
|
|
747
|
+
var KIND_AREA = { File:1018, Class:452, Function:113, Test:113, Type:79 };
|
|
748
|
+
var KIND_SHAPE = { File:d3.symbolCircle, Class:d3.symbolSquare, Function:d3.symbolTriangle, Test:d3.symbolDiamond, Type:d3.symbolCross };
|
|
749
|
+
var EDGE_COLOR = { CALLS:"#3fb950", IMPORTS_FROM:"#f0883e", INHERITS:"#d2a8ff", CONTAINS:"rgba(139,148,158,0.15)", IMPLEMENTS:"#f9e2af", TESTED_BY:"#f38ba8", DEPENDS_ON:"#fab387" };
|
|
750
|
+
var communityColorScale = d3.scaleOrdinal(d3.schemeTableau10);
|
|
751
|
+
var communityColoringOn = false;
|
|
752
|
+
function escH(s) { return !s ? "" : s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/`/g,"`"); }
|
|
753
|
+
function displayName(d) {
|
|
754
|
+
if (d.kind === "File") {
|
|
755
|
+
var fp = d.file_path || d.qualified_name || d.name;
|
|
756
|
+
var parts = fp.replace(/\\/g, "/").split("/");
|
|
757
|
+
var fname = parts.pop();
|
|
758
|
+
var parent = parts.pop() || "";
|
|
759
|
+
return parent ? parent + "/" + fname : fname;
|
|
760
|
+
}
|
|
761
|
+
return d.name;
|
|
762
|
+
}
|
|
763
|
+
var nodes = graphData.nodes.map(function(d) { var o = Object.assign({}, d); o._id = d.qualified_name; o.label = displayName(d); return o; });
|
|
764
|
+
var edges = graphData.edges.map(function(d) { var o = Object.assign({}, d); o._source = d.source; o._target = d.target; return o; });
|
|
765
|
+
var stats = graphData.stats;
|
|
766
|
+
var flows = graphData.flows || [];
|
|
767
|
+
var communities = graphData.communities || [];
|
|
768
|
+
var nodeById = new Map(nodes.map(function(n) { return [n.qualified_name, n]; }));
|
|
769
|
+
var hiddenEdgeKinds = new Set();
|
|
770
|
+
var hiddenNodeKinds = new Set();
|
|
771
|
+
var collapsedFiles = new Set();
|
|
772
|
+
var containsChildren = new Map();
|
|
773
|
+
var childToParent = new Map();
|
|
774
|
+
edges.forEach(function(e) {
|
|
775
|
+
if (e.kind === "CONTAINS") {
|
|
776
|
+
if (!containsChildren.has(e._source)) containsChildren.set(e._source, new Set());
|
|
777
|
+
containsChildren.get(e._source).add(e._target);
|
|
778
|
+
childToParent.set(e._target, e._source);
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
function allDescendants(qn) {
|
|
782
|
+
var result = new Set();
|
|
783
|
+
var stack = [qn];
|
|
784
|
+
while (stack.length) {
|
|
785
|
+
var cur = stack.pop();
|
|
786
|
+
var children = containsChildren.get(cur);
|
|
787
|
+
if (!children) continue;
|
|
788
|
+
children.forEach(function(c) { if (!result.has(c)) { result.add(c); stack.push(c); } });
|
|
789
|
+
}
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
var nodeToCommunity = new Map();
|
|
793
|
+
communities.forEach(function(c) {
|
|
794
|
+
(c.members || []).forEach(function(qn) {
|
|
795
|
+
nodeToCommunity.set(qn, c.id);
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
// Compute node degree (connection count) for size scaling
|
|
799
|
+
var nodeDegree = new Map();
|
|
800
|
+
edges.forEach(function(e) {
|
|
801
|
+
var s = e._source, t = e._target;
|
|
802
|
+
nodeDegree.set(s, (nodeDegree.get(s) || 0) + 1);
|
|
803
|
+
nodeDegree.set(t, (nodeDegree.get(t) || 0) + 1);
|
|
804
|
+
});
|
|
805
|
+
var maxDegree = 1;
|
|
806
|
+
nodeDegree.forEach(function(v) {
|
|
807
|
+
if (v > maxDegree) maxDegree = v;
|
|
808
|
+
});
|
|
809
|
+
function degreeRadius(d) {
|
|
810
|
+
var base = KIND_RADIUS[d.kind] || 6;
|
|
811
|
+
var deg = nodeDegree.get(d.qualified_name) || 0;
|
|
812
|
+
// Scale: base + up to 2x base proportional to degree
|
|
813
|
+
var scale = 1 + (deg / maxDegree);
|
|
814
|
+
return Math.round(base * scale);
|
|
815
|
+
}
|
|
816
|
+
var nodeIdToQn = new Map();
|
|
817
|
+
nodes.forEach(function(n) { nodeIdToQn.set(n.id, n.qualified_name); });
|
|
818
|
+
var flowSelect = document.getElementById("flow-select");
|
|
819
|
+
flows.forEach(function(f, i) {
|
|
820
|
+
var opt = document.createElement("option");
|
|
821
|
+
opt.value = i;
|
|
822
|
+
opt.textContent = f.name + " (" + f.node_count + " nodes)";
|
|
823
|
+
flowSelect.appendChild(opt);
|
|
824
|
+
});
|
|
825
|
+
var statsBar = document.getElementById("stats-bar");
|
|
826
|
+
var langList = (stats.languages || []).join(", ") || "n/a";
|
|
827
|
+
function si(l, v) { return '<div class="stat-item"><span class="tt-label">' + escH(l) + '</span> <span class="stat-value">' + escH(String(v)) + '</span></div>'; }
|
|
828
|
+
statsBar.textContent = "";
|
|
829
|
+
statsBar.insertAdjacentHTML("beforeend", si("Nodes", stats.total_nodes) + si("Edges", stats.total_edges) + si("Files", stats.files_count) + si("Languages", langList));
|
|
830
|
+
var tooltip = document.getElementById("tooltip");
|
|
831
|
+
function showTooltip(ev, d) {
|
|
832
|
+
var bg = communityColoringOn && d.community_id != null ? communityColorScale(d.community_id) : (KIND_COLOR[d.kind] || "#555");
|
|
833
|
+
var relFile = d.file_path ? d.file_path.split("/").slice(-3).join("/") : "";
|
|
834
|
+
var h = '<span class="tt-name">' + escH(d.label) + '</span>';
|
|
835
|
+
h += '<span class="tt-kind" style="background:' + bg + ';color:#0d1117">' + escH(d.kind) + '</span>';
|
|
836
|
+
if (relFile) h += '<div class="tt-row tt-file">' + escH(relFile) + '</div>';
|
|
837
|
+
if (d.line_start != null) h += '<div class="tt-row"><span class="tt-label">Lines: </span>' + d.line_start + ' \u2013 ' + (d.line_end || d.line_start) + '</div>';
|
|
838
|
+
if (d.params) h += '<div class="tt-row"><span class="tt-label">Params: </span>' + escH(d.params) + '</div>';
|
|
839
|
+
if (d.return_type) h += '<div class="tt-row"><span class="tt-label">Returns: </span>' + escH(d.return_type) + '</div>';
|
|
840
|
+
if (d.community_id != null) {
|
|
841
|
+
var comm = communities.find(function(c) { return c.id === d.community_id; });
|
|
842
|
+
if (comm) h += '<div class="tt-row"><span class="tt-label">Community: </span>' + escH(comm.name) + '</div>';
|
|
843
|
+
}
|
|
844
|
+
tooltip.textContent = "";
|
|
845
|
+
tooltip.insertAdjacentHTML("beforeend", h);
|
|
846
|
+
tooltip.classList.add("visible");
|
|
847
|
+
moveTooltip(ev);
|
|
848
|
+
}
|
|
849
|
+
function moveTooltip(ev) {
|
|
850
|
+
var p = 14;
|
|
851
|
+
var x = ev.pageX + p, y = ev.pageY + p;
|
|
852
|
+
var r = tooltip.getBoundingClientRect();
|
|
853
|
+
if (x + r.width > innerWidth - p) x = ev.pageX - r.width - p;
|
|
854
|
+
if (y + r.height > innerHeight - p) y = ev.pageY - r.height - p;
|
|
855
|
+
tooltip.style.left = x + "px"; tooltip.style.top = y + "px";
|
|
856
|
+
}
|
|
857
|
+
function hideTooltip() { tooltip.classList.remove("visible"); }
|
|
858
|
+
var W = innerWidth, H = innerHeight;
|
|
859
|
+
var svg = d3.select("svg").attr("viewBox", [0, 0, W, H]);
|
|
860
|
+
var gRoot = svg.append("g");
|
|
861
|
+
var currentTransform = d3.zoomIdentity;
|
|
862
|
+
var zoomBehavior = d3.zoom()
|
|
863
|
+
.scaleExtent([0.05, 8])
|
|
864
|
+
.on("zoom", function(ev) { currentTransform = ev.transform; gRoot.attr("transform", ev.transform); updateLabelVisibility(); });
|
|
865
|
+
svg.call(zoomBehavior);
|
|
866
|
+
var defs = svg.append("defs");
|
|
867
|
+
var glow = defs.append("filter").attr("id","glow").attr("x","-50%").attr("y","-50%").attr("width","200%").attr("height","200%");
|
|
868
|
+
glow.append("feGaussianBlur").attr("stdDeviation","3").attr("result","blur");
|
|
869
|
+
glow.append("feComposite").attr("in","SourceGraphic").attr("in2","blur").attr("operator","over");
|
|
870
|
+
[{id:"arrow-calls",color:"#3fb950"},{id:"arrow-imports",color:"#f0883e"},{id:"arrow-inherits",color:"#d2a8ff"},{id:"arrow-implements",color:"#f9e2af"},{id:"arrow-tested_by",color:"#f38ba8"},{id:"arrow-depends_on",color:"#fab387"}].forEach(function(mk) {
|
|
871
|
+
defs.append("marker").attr("id", mk.id)
|
|
872
|
+
.attr("viewBox","0 -5 10 10").attr("refX",28).attr("refY",0)
|
|
873
|
+
.attr("markerWidth",8).attr("markerHeight",8).attr("orient","auto")
|
|
874
|
+
.append("path").attr("d","M0,-4L10,0L0,4Z").attr("fill",mk.color);
|
|
875
|
+
});
|
|
876
|
+
var N = nodes.length;
|
|
877
|
+
var isLarge = N > 300;
|
|
878
|
+
var simulation = d3.forceSimulation(nodes)
|
|
879
|
+
.force("link", d3.forceLink(edges).id(function(d) { return d.qualified_name; })
|
|
880
|
+
.distance(function(d) { return d.kind === "CONTAINS" ? 35 : (isLarge ? 80 : 120); })
|
|
881
|
+
.strength(function(d) { return d.kind === "CONTAINS" ? 1.5 : 0.15; }))
|
|
882
|
+
.force("charge", d3.forceManyBody().strength(function(d) { return d.kind === "File" ? (isLarge ? -200 : -400) : (isLarge ? -60 : -120); }).theta(0.85).distanceMax(600))
|
|
883
|
+
.force("collide", d3.forceCollide().radius(function(d) { return degreeRadius(d) + 4; }))
|
|
884
|
+
.force("center", d3.forceCenter(W / 2, H / 2))
|
|
885
|
+
.force("x", d3.forceX(W / 2).strength(0.03))
|
|
886
|
+
.force("y", d3.forceY(H / 2).strength(0.03))
|
|
887
|
+
.alphaDecay(isLarge ? 0.04 : 0.025)
|
|
888
|
+
.velocityDecay(0.4);
|
|
889
|
+
var EDGE_CFG = {
|
|
890
|
+
CONTAINS: { dash:null, width:1, opacity:0.14, marker:"" },
|
|
891
|
+
CALLS: { dash:null, width:2, opacity:0.7, marker:"url(#arrow-calls)" },
|
|
892
|
+
IMPORTS_FROM: { dash:"8,4", width:2, opacity:0.65, marker:"url(#arrow-imports)" },
|
|
893
|
+
INHERITS: { dash:"2,6", width:2.5, opacity:0.7, marker:"url(#arrow-inherits)" },
|
|
894
|
+
IMPLEMENTS: { dash:"4,3", width:1.5, opacity:0.65, marker:"url(#arrow-implements)" },
|
|
895
|
+
TESTED_BY: { dash:"2,4", width:1.5, opacity:0.6, marker:"url(#arrow-tested_by)" },
|
|
896
|
+
DEPENDS_ON: { dash:"8,4", width:1, opacity:0.6, marker:"url(#arrow-depends_on)" },
|
|
897
|
+
};
|
|
898
|
+
function eStyle(d) { return EDGE_CFG[d.kind] || {dash:null,width:1,opacity:0.3,marker:""}; }
|
|
899
|
+
function eColor(d) { return EDGE_COLOR[d.kind] || "#484f58"; }
|
|
900
|
+
function nodeColor(d) {
|
|
901
|
+
if (communityColoringOn && d.community_id != null) return communityColorScale(d.community_id);
|
|
902
|
+
return KIND_COLOR[d.kind] || "#8b949e";
|
|
903
|
+
}
|
|
904
|
+
var linkGroup = gRoot.append("g").attr("class","links");
|
|
905
|
+
var nodeGroup = gRoot.append("g").attr("class","nodes");
|
|
906
|
+
var labelGroup = gRoot.append("g").attr("class","labels");
|
|
907
|
+
var linkSel, labelSel;
|
|
908
|
+
var showLabels = true;
|
|
909
|
+
function updateLinks() {
|
|
910
|
+
var vis = new Set(nodes.filter(function(n) { return !n._hidden; }).map(function(n) { return n.qualified_name; }));
|
|
911
|
+
var visEdges = edges.filter(function(e) {
|
|
912
|
+
if (hiddenEdgeKinds.has(e.kind)) return false;
|
|
913
|
+
var s = typeof e.source === "object" ? e.source.qualified_name : e._source;
|
|
914
|
+
var t = typeof e.target === "object" ? e.target.qualified_name : e._target;
|
|
915
|
+
return vis.has(s) && vis.has(t);
|
|
916
|
+
});
|
|
917
|
+
linkSel = linkGroup.selectAll("line").data(visEdges, function(d) { return d._source+"->"+d._target+":"+d.kind; });
|
|
918
|
+
linkSel.exit().remove();
|
|
919
|
+
var enter = linkSel.enter().append("line");
|
|
920
|
+
linkSel = enter.merge(linkSel);
|
|
921
|
+
linkSel
|
|
922
|
+
.attr("stroke", function(d) { return eColor(d); })
|
|
923
|
+
.attr("stroke-width", function(d) { return eStyle(d).width; })
|
|
924
|
+
.attr("stroke-dasharray", function(d) { return eStyle(d).dash; })
|
|
925
|
+
.attr("opacity", function(d) { return eStyle(d).opacity; })
|
|
926
|
+
.attr("marker-end", function(d) { return eStyle(d).marker; });
|
|
927
|
+
}
|
|
928
|
+
function updateNodes() {
|
|
929
|
+
var hiddenSet = new Set();
|
|
930
|
+
collapsedFiles.forEach(function(fqn) { allDescendants(fqn).forEach(function(c) { hiddenSet.add(c); }); });
|
|
931
|
+
nodes.forEach(function(n) { n._hidden = hiddenSet.has(n.qualified_name) || hiddenNodeKinds.has(n.kind); });
|
|
932
|
+
var vis = nodes.filter(function(n) { return !n._hidden; });
|
|
933
|
+
var nodeSel = nodeGroup.selectAll("g.node-g").data(vis, function(d) { return d.qualified_name; });
|
|
934
|
+
nodeSel.exit().remove();
|
|
935
|
+
var enter = nodeSel.enter().append("g").attr("class","node-g");
|
|
936
|
+
enter.filter(function(d) { return d.kind === "File"; }).append("circle")
|
|
937
|
+
.attr("class","glow-ring")
|
|
938
|
+
.attr("r", function(d) { return degreeRadius(d) + 5; })
|
|
939
|
+
.attr("fill","none")
|
|
940
|
+
.attr("stroke", function(d) { return nodeColor(d); })
|
|
941
|
+
.attr("stroke-width", 1.5).attr("opacity", 0.3).attr("filter","url(#glow)");
|
|
942
|
+
enter.append("path").attr("class","node-shape")
|
|
943
|
+
.attr("d", function(d) { return d3.symbol().type(KIND_SHAPE[d.kind] || d3.symbolCircle).size(KIND_AREA[d.kind] || 113)(); })
|
|
944
|
+
.attr("fill", function(d) { return nodeColor(d); })
|
|
945
|
+
.attr("stroke", function(d) { return d.kind === "File" ? "rgba(88,166,255,0.3)" : "rgba(255,255,255,0.08)"; })
|
|
946
|
+
.attr("stroke-width", function(d) { return d.kind === "File" ? 2 : 1; })
|
|
947
|
+
.attr("cursor", "pointer");
|
|
948
|
+
enter
|
|
949
|
+
.on("mouseover", function(ev, d) { highlightConnected(d, true); showTooltip(ev, d); })
|
|
950
|
+
.on("mousemove", function(ev) { moveTooltip(ev); })
|
|
951
|
+
.on("mouseout", function(ev, d) { highlightConnected(d, false); hideTooltip(); })
|
|
952
|
+
.on("click", function(ev, d) {
|
|
953
|
+
ev.stopPropagation();
|
|
954
|
+
if (d.kind === "File" && !ev.shiftKey) toggleCollapse(d.qualified_name);
|
|
955
|
+
showDetailPanel(d);
|
|
956
|
+
})
|
|
957
|
+
.call(d3.drag().on("start", dragS).on("drag", dragD).on("end", dragE));
|
|
958
|
+
enter.attr("tabindex", 0).attr("role", "button")
|
|
959
|
+
.attr("aria-label", function(d) { return d.kind + ": " + d.label; })
|
|
960
|
+
.on("keydown", function(ev, d) {
|
|
961
|
+
if (ev.key === "Enter" || ev.key === " ") {
|
|
962
|
+
ev.preventDefault();
|
|
963
|
+
if (d.kind === "File" && !ev.shiftKey) toggleCollapse(d.qualified_name);
|
|
964
|
+
showDetailPanel(d);
|
|
965
|
+
} else if (ev.key === "Escape") {
|
|
966
|
+
ev.preventDefault();
|
|
967
|
+
detailPanel.classList.remove("visible");
|
|
968
|
+
hideTooltip();
|
|
969
|
+
} else if (["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].indexOf(ev.key) !== -1) {
|
|
970
|
+
ev.preventDefault();
|
|
971
|
+
var vis = nodes.filter(function(n) { return !n._hidden; });
|
|
972
|
+
var best = null, bestDist = Infinity;
|
|
973
|
+
vis.forEach(function(n) {
|
|
974
|
+
if (n.qualified_name === d.qualified_name) return;
|
|
975
|
+
var dx = n.x - d.x, dy = n.y - d.y;
|
|
976
|
+
var dist = Math.sqrt(dx*dx + dy*dy);
|
|
977
|
+
var ok = false;
|
|
978
|
+
if (ev.key === "ArrowRight" && dx > 0 && Math.abs(dy) < Math.abs(dx)) ok = true;
|
|
979
|
+
if (ev.key === "ArrowLeft" && dx < 0 && Math.abs(dy) < Math.abs(dx)) ok = true;
|
|
980
|
+
if (ev.key === "ArrowDown" && dy > 0 && Math.abs(dx) < Math.abs(dy)) ok = true;
|
|
981
|
+
if (ev.key === "ArrowUp" && dy < 0 && Math.abs(dx) < Math.abs(dy)) ok = true;
|
|
982
|
+
if (ok && dist < bestDist) { best = n; bestDist = dist; }
|
|
983
|
+
});
|
|
984
|
+
if (best) {
|
|
985
|
+
var target = nodeGroup.selectAll("g.node-g").filter(function(n) { return n.qualified_name === best.qualified_name; }).node();
|
|
986
|
+
if (target) target.focus();
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
})
|
|
990
|
+
.on("focus", function(ev, d) { highlightConnected(d, true); showTooltip(ev, d); })
|
|
991
|
+
.on("blur", function(ev, d) { highlightConnected(d, false); hideTooltip(); });
|
|
992
|
+
nodeSel = enter.merge(nodeSel);
|
|
993
|
+
labelSel = labelGroup.selectAll("text.node-label").data(vis, function(d) { return d.qualified_name; });
|
|
994
|
+
labelSel.exit().remove();
|
|
995
|
+
var lEnter = labelSel.enter().append("text").attr("class","node-label")
|
|
996
|
+
.attr("text-anchor","start").attr("dy","0.35em")
|
|
997
|
+
.text(function(d) { return d.label; })
|
|
998
|
+
.attr("fill", function(d) { return d.kind === "File" ? "#e6edf3" : d.kind === "Class" ? "#f0883e" : "#9eaab6"; })
|
|
999
|
+
.attr("font-size", function(d) { return d.kind === "File" ? "12px" : d.kind === "Class" ? "11px" : "10px"; })
|
|
1000
|
+
.attr("font-weight", function(d) { return d.kind === "File" ? 700 : d.kind === "Class" ? 600 : 400; });
|
|
1001
|
+
labelSel = lEnter.merge(labelSel);
|
|
1002
|
+
updateLinks();
|
|
1003
|
+
updateLabelVisibility();
|
|
1004
|
+
}
|
|
1005
|
+
function updateLabelVisibility() {
|
|
1006
|
+
if (!labelSel) return;
|
|
1007
|
+
var s = currentTransform.k;
|
|
1008
|
+
labelSel.attr("display", function(d) {
|
|
1009
|
+
if (!showLabels) return "none";
|
|
1010
|
+
if (d.kind === "File") return null;
|
|
1011
|
+
if (d.kind === "Class") return s > 0.5 ? null : "none";
|
|
1012
|
+
return s > 1.0 ? null : "none";
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
function highlightConnected(d, on) {
|
|
1016
|
+
if (on) {
|
|
1017
|
+
var connected = new Set([d.qualified_name]);
|
|
1018
|
+
edges.forEach(function(e) {
|
|
1019
|
+
var s = typeof e.source === "object" ? e.source.qualified_name : e._source;
|
|
1020
|
+
var t = typeof e.target === "object" ? e.target.qualified_name : e._target;
|
|
1021
|
+
if (s === d.qualified_name) connected.add(t);
|
|
1022
|
+
if (t === d.qualified_name) connected.add(s);
|
|
1023
|
+
});
|
|
1024
|
+
nodeGroup.selectAll("g.node-g").select(".node-shape")
|
|
1025
|
+
.transition().duration(150).attr("opacity", function(n) { return connected.has(n.qualified_name) ? 1 : 0.15; });
|
|
1026
|
+
linkSel.transition().duration(150)
|
|
1027
|
+
.attr("opacity", function(e) {
|
|
1028
|
+
var s = typeof e.source === "object" ? e.source.qualified_name : e._source;
|
|
1029
|
+
var t = typeof e.target === "object" ? e.target.qualified_name : e._target;
|
|
1030
|
+
return (s === d.qualified_name || t === d.qualified_name) ? 0.9 : 0.03;
|
|
1031
|
+
})
|
|
1032
|
+
.attr("stroke-width", function(e) {
|
|
1033
|
+
var s = typeof e.source === "object" ? e.source.qualified_name : e._source;
|
|
1034
|
+
var t = typeof e.target === "object" ? e.target.qualified_name : e._target;
|
|
1035
|
+
return (s === d.qualified_name || t === d.qualified_name) ? 2.5 : eStyle(e).width;
|
|
1036
|
+
});
|
|
1037
|
+
labelSel.transition().duration(150).attr("opacity", function(n) { return connected.has(n.qualified_name) ? 1 : 0.1; });
|
|
1038
|
+
} else {
|
|
1039
|
+
nodeGroup.selectAll("g.node-g").select(".node-shape").transition().duration(300).attr("opacity", 1);
|
|
1040
|
+
linkSel.transition().duration(300)
|
|
1041
|
+
.attr("opacity", function(e) { return eStyle(e).opacity; })
|
|
1042
|
+
.attr("stroke-width", function(e) { return eStyle(e).width; });
|
|
1043
|
+
labelSel.transition().duration(300).attr("opacity", 1);
|
|
1044
|
+
updateLabelVisibility();
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function toggleCollapse(qn) {
|
|
1048
|
+
if (collapsedFiles.has(qn)) collapsedFiles.delete(qn); else collapsedFiles.add(qn);
|
|
1049
|
+
nodeGroup.selectAll("g.node-g").select(".glow-ring")
|
|
1050
|
+
.attr("stroke-dasharray", function(d) { return collapsedFiles.has(d.qualified_name) ? "4,3" : null; })
|
|
1051
|
+
.attr("opacity", function(d) { return collapsedFiles.has(d.qualified_name) ? 0.6 : 0.3; });
|
|
1052
|
+
updateNodes();
|
|
1053
|
+
simulation.alpha(0.3).restart();
|
|
1054
|
+
}
|
|
1055
|
+
function dragS(ev, d) { if (!ev.active) simulation.alphaTarget(0.1).restart(); d.fx = d.x; d.fy = d.y; }
|
|
1056
|
+
function dragD(ev, d) { d.fx = ev.x; d.fy = ev.y; }
|
|
1057
|
+
function dragE(ev, d) { if (!ev.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; }
|
|
1058
|
+
simulation.on("tick", function() {
|
|
1059
|
+
if (linkSel) linkSel
|
|
1060
|
+
.attr("x1", function(d) { return d.source.x; }).attr("y1", function(d) { return d.source.y; })
|
|
1061
|
+
.attr("x2", function(d) { return d.target.x; }).attr("y2", function(d) { return d.target.y; });
|
|
1062
|
+
nodeGroup.selectAll("g.node-g").attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
|
|
1063
|
+
if (labelSel) labelSel
|
|
1064
|
+
.attr("x", function(d) { return d.x + degreeRadius(d) + 5; })
|
|
1065
|
+
.attr("y", function(d) { return d.y; });
|
|
1066
|
+
});
|
|
1067
|
+
// Only auto-collapse File nodes on very large graphs, otherwise all edges
|
|
1068
|
+
// become invisible because they connect to Functions/Classes that are now
|
|
1069
|
+
// hidden beneath collapsed Files. See: #132
|
|
1070
|
+
if (N > 2000) {
|
|
1071
|
+
nodes.forEach(function(n) { if (n.kind === "File") collapsedFiles.add(n.qualified_name); });
|
|
1072
|
+
}
|
|
1073
|
+
updateNodes();
|
|
1074
|
+
function fitGraph() {
|
|
1075
|
+
var b = gRoot.node().getBBox();
|
|
1076
|
+
if (b.width === 0 || b.height === 0) return;
|
|
1077
|
+
var pad = 0.1;
|
|
1078
|
+
var fw = b.width * (1 + 2*pad), fh = b.height * (1 + 2*pad);
|
|
1079
|
+
var s = Math.min(W / fw, H / fh, 2.5);
|
|
1080
|
+
var tx = W/2 - (b.x + b.width/2)*s, ty = H/2 - (b.y + b.height/2)*s;
|
|
1081
|
+
svg.transition().duration(600).call(zoomBehavior.transform, d3.zoomIdentity.translate(tx, ty).scale(s));
|
|
1082
|
+
}
|
|
1083
|
+
var loadingOverlay = document.getElementById("loading-overlay");
|
|
1084
|
+
var emptyState = document.getElementById("empty-state");
|
|
1085
|
+
if (nodes.length === 0) {
|
|
1086
|
+
loadingOverlay.classList.add("hidden");
|
|
1087
|
+
emptyState.classList.add("visible");
|
|
1088
|
+
}
|
|
1089
|
+
simulation.on("end", function() {
|
|
1090
|
+
loadingOverlay.classList.add("hidden");
|
|
1091
|
+
fitGraph();
|
|
1092
|
+
});
|
|
1093
|
+
function zoomToNode(qn) {
|
|
1094
|
+
var nd = nodeById.get(qn);
|
|
1095
|
+
if (!nd || nd.x == null) return;
|
|
1096
|
+
var s = 2.0;
|
|
1097
|
+
var tx = W/2 - nd.x*s, ty = H/2 - nd.y*s;
|
|
1098
|
+
svg.transition().duration(600).call(zoomBehavior.transform, d3.zoomIdentity.translate(tx, ty).scale(s));
|
|
1099
|
+
}
|
|
1100
|
+
document.getElementById("btn-fit").addEventListener("click", fitGraph);
|
|
1101
|
+
document.getElementById("btn-labels").addEventListener("click", function() {
|
|
1102
|
+
showLabels = !showLabels;
|
|
1103
|
+
this.classList.toggle("active");
|
|
1104
|
+
this.setAttribute("aria-pressed", showLabels);
|
|
1105
|
+
updateLabelVisibility();
|
|
1106
|
+
});
|
|
1107
|
+
document.querySelectorAll(".legend-item[data-edge-kind]").forEach(function(el) {
|
|
1108
|
+
el.addEventListener("click", function() {
|
|
1109
|
+
var kind = this.dataset.edgeKind;
|
|
1110
|
+
if (hiddenEdgeKinds.has(kind)) { hiddenEdgeKinds.delete(kind); this.classList.remove("dimmed"); }
|
|
1111
|
+
else { hiddenEdgeKinds.add(kind); this.classList.add("dimmed"); }
|
|
1112
|
+
this.setAttribute("aria-pressed", !hiddenEdgeKinds.has(kind));
|
|
1113
|
+
updateLinks();
|
|
1114
|
+
});
|
|
1115
|
+
});
|
|
1116
|
+
document.querySelectorAll("#filter-panel input[data-kind]").forEach(function(el) {
|
|
1117
|
+
el.addEventListener("change", function() {
|
|
1118
|
+
var kind = this.dataset.kind;
|
|
1119
|
+
if (this.checked) hiddenNodeKinds.delete(kind); else hiddenNodeKinds.add(kind);
|
|
1120
|
+
updateNodes();
|
|
1121
|
+
simulation.alpha(0.15).restart();
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
document.getElementById("btn-community").addEventListener("click", function() {
|
|
1125
|
+
communityColoringOn = !communityColoringOn;
|
|
1126
|
+
this.classList.toggle("active");
|
|
1127
|
+
this.setAttribute("aria-pressed", communityColoringOn);
|
|
1128
|
+
nodeGroup.selectAll("g.node-g").select(".node-shape").transition().duration(300)
|
|
1129
|
+
.attr("fill", function(d) { return nodeColor(d); });
|
|
1130
|
+
nodeGroup.selectAll("g.node-g").select(".glow-ring")
|
|
1131
|
+
.transition().duration(300)
|
|
1132
|
+
.attr("stroke", function(d) { return nodeColor(d); });
|
|
1133
|
+
// Toggle community legend visibility with coloring
|
|
1134
|
+
var cl = document.getElementById("community-legend");
|
|
1135
|
+
if (communityColoringOn) cl.classList.add("visible");
|
|
1136
|
+
else cl.classList.remove("visible");
|
|
1137
|
+
});
|
|
1138
|
+
// Build community legend with toggle checkboxes
|
|
1139
|
+
var hiddenCommunities = new Set();
|
|
1140
|
+
(function buildCommunityLegend() {
|
|
1141
|
+
var cl = document.getElementById("community-legend");
|
|
1142
|
+
if (!communities.length) return;
|
|
1143
|
+
var h = '<h3>Communities</h3>';
|
|
1144
|
+
communities.slice(0, 30).forEach(function(c) {
|
|
1145
|
+
var col = communityColorScale(c.id);
|
|
1146
|
+
var nm = escH(c.name || ("Community " + c.id));
|
|
1147
|
+
h += '<label class="cl-item">'
|
|
1148
|
+
+ '<input type="checkbox" data-cid="'
|
|
1149
|
+
+ c.id + '" checked>'
|
|
1150
|
+
+ '<span class="cl-swatch" style="background:'
|
|
1151
|
+
+ col + '"></span> ' + nm + '</label>';
|
|
1152
|
+
});
|
|
1153
|
+
cl.textContent = "";
|
|
1154
|
+
cl.insertAdjacentHTML("beforeend", h);
|
|
1155
|
+
cl.querySelectorAll("input[data-cid]").forEach(
|
|
1156
|
+
function(el) {
|
|
1157
|
+
el.addEventListener("change", function() {
|
|
1158
|
+
var cid = parseInt(this.dataset.cid);
|
|
1159
|
+
if (this.checked) hiddenCommunities.delete(cid);
|
|
1160
|
+
else hiddenCommunities.add(cid);
|
|
1161
|
+
applyCommunityFilter();
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
);
|
|
1165
|
+
})();
|
|
1166
|
+
function applyCommunityFilter() {
|
|
1167
|
+
if (!communityColoringOn || !hiddenCommunities.size) {
|
|
1168
|
+
nodeGroup.selectAll("g.node-g")
|
|
1169
|
+
.attr("display", function(d) {
|
|
1170
|
+
return d._hidden ? "none" : null;
|
|
1171
|
+
});
|
|
1172
|
+
updateLinks();
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
nodeGroup.selectAll("g.node-g")
|
|
1176
|
+
.attr("display", function(d) {
|
|
1177
|
+
if (d._hidden) return "none";
|
|
1178
|
+
var cid = d.community_id;
|
|
1179
|
+
if (cid == null) {
|
|
1180
|
+
cid = nodeToCommunity.get(d.qualified_name);
|
|
1181
|
+
}
|
|
1182
|
+
if (cid != null && hiddenCommunities.has(cid)) {
|
|
1183
|
+
return "none";
|
|
1184
|
+
}
|
|
1185
|
+
return null;
|
|
1186
|
+
});
|
|
1187
|
+
if (labelSel) labelSel
|
|
1188
|
+
.attr("display", function(d) {
|
|
1189
|
+
var cid = d.community_id;
|
|
1190
|
+
if (cid == null) {
|
|
1191
|
+
cid = nodeToCommunity.get(d.qualified_name);
|
|
1192
|
+
}
|
|
1193
|
+
if (cid != null && hiddenCommunities.has(cid)) {
|
|
1194
|
+
return "none";
|
|
1195
|
+
}
|
|
1196
|
+
return null;
|
|
1197
|
+
});
|
|
1198
|
+
updateLinks();
|
|
1199
|
+
}
|
|
1200
|
+
var activeFlowQns = null;
|
|
1201
|
+
flowSelect.addEventListener("change", function() {
|
|
1202
|
+
var idx = this.value;
|
|
1203
|
+
if (idx === "") { activeFlowQns = null; clearFlowHighlight(); return; }
|
|
1204
|
+
var flow = flows[parseInt(idx)];
|
|
1205
|
+
if (!flow) return;
|
|
1206
|
+
var pathQns = new Set();
|
|
1207
|
+
(flow.path || []).forEach(function(nid) { var qn = nodeIdToQn.get(nid); if (qn) pathQns.add(qn); });
|
|
1208
|
+
activeFlowQns = pathQns;
|
|
1209
|
+
applyFlowHighlight();
|
|
1210
|
+
});
|
|
1211
|
+
function applyFlowHighlight() {
|
|
1212
|
+
if (!activeFlowQns || activeFlowQns.size === 0) { clearFlowHighlight(); return; }
|
|
1213
|
+
nodeGroup.selectAll("g.node-g").select(".node-shape").transition().duration(200)
|
|
1214
|
+
.attr("opacity", function(d) { return activeFlowQns.has(d.qualified_name) ? 1 : 0.2; });
|
|
1215
|
+
if (labelSel) labelSel.transition().duration(200)
|
|
1216
|
+
.attr("opacity", function(d) { return activeFlowQns.has(d.qualified_name) ? 1 : 0.1; });
|
|
1217
|
+
if (linkSel) linkSel.transition().duration(200)
|
|
1218
|
+
.attr("opacity", function(e) {
|
|
1219
|
+
var s = typeof e.source === "object" ? e.source.qualified_name : e._source;
|
|
1220
|
+
var t = typeof e.target === "object" ? e.target.qualified_name : e._target;
|
|
1221
|
+
return (activeFlowQns.has(s) && activeFlowQns.has(t)) ? 0.9 : 0.03;
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
function clearFlowHighlight() {
|
|
1225
|
+
nodeGroup.selectAll("g.node-g").select(".node-shape").transition().duration(300).attr("opacity", 1);
|
|
1226
|
+
if (linkSel) linkSel.transition().duration(300).attr("opacity", function(e) { return eStyle(e).opacity; });
|
|
1227
|
+
if (labelSel) labelSel.transition().duration(300).attr("opacity", 1);
|
|
1228
|
+
updateLabelVisibility();
|
|
1229
|
+
}
|
|
1230
|
+
var detailPanel = document.getElementById("detail-panel");
|
|
1231
|
+
var dpContent = document.getElementById("dp-content");
|
|
1232
|
+
var detailTrigger = null;
|
|
1233
|
+
document.querySelector("#detail-panel .dp-close").addEventListener("click", function() {
|
|
1234
|
+
detailPanel.classList.remove("visible");
|
|
1235
|
+
document.getElementById("legend").style.display = "";
|
|
1236
|
+
if (detailTrigger) detailTrigger.focus();
|
|
1237
|
+
});
|
|
1238
|
+
svg.on("click", function() {
|
|
1239
|
+
detailPanel.classList.remove("visible");
|
|
1240
|
+
document.getElementById("legend").style.display = "";
|
|
1241
|
+
if (detailTrigger) detailTrigger.focus();
|
|
1242
|
+
});
|
|
1243
|
+
function showDetailPanel(d) {
|
|
1244
|
+
detailTrigger = document.activeElement;
|
|
1245
|
+
var callers = [], callees = [];
|
|
1246
|
+
edges.forEach(function(e) {
|
|
1247
|
+
var s = typeof e.source === "object" ? e.source.qualified_name : e._source;
|
|
1248
|
+
var t = typeof e.target === "object" ? e.target.qualified_name : e._target;
|
|
1249
|
+
if (t === d.qualified_name && e.kind === "CALLS") { var sN = nodeById.get(s); if (sN) callers.push(sN); }
|
|
1250
|
+
if (s === d.qualified_name && e.kind === "CALLS") { var tN = nodeById.get(t); if (tN) callees.push(tN); }
|
|
1251
|
+
});
|
|
1252
|
+
var relFile = d.file_path ? d.file_path.split("/").slice(-3).join("/") : "";
|
|
1253
|
+
var bg = communityColoringOn && d.community_id != null ? communityColorScale(d.community_id) : (KIND_COLOR[d.kind] || "#555");
|
|
1254
|
+
var h = '<h2>' + escH(d.label) + '</h2>';
|
|
1255
|
+
h += '<span class="tt-kind" style="background:' + bg + ';color:#0d1117">' + escH(d.kind) + '</span>';
|
|
1256
|
+
if (relFile) h += '<div class="dp-meta" style="margin-top:8px">' + escH(relFile) + (d.line_start != null ? ':' + d.line_start : '') + '</div>';
|
|
1257
|
+
if (d.params) h += '<div class="dp-meta"><span class="tt-label">Params:</span> ' + escH(d.params) + '</div>';
|
|
1258
|
+
if (d.return_type) h += '<div class="dp-meta"><span class="tt-label">Returns:</span> ' + escH(d.return_type) + '</div>';
|
|
1259
|
+
if (d.community_id != null) {
|
|
1260
|
+
var comm = communities.find(function(c) { return c.id === d.community_id; });
|
|
1261
|
+
if (comm) h += '<div class="dp-meta"><span class="tt-label">Community:</span> ' + escH(comm.name) + '</div>';
|
|
1262
|
+
}
|
|
1263
|
+
if (callers.length) {
|
|
1264
|
+
h += '<div class="dp-section"><h4>Callers (' + callers.length + ')</h4><ul class="dp-list">';
|
|
1265
|
+
callers.slice(0, 20).forEach(function(c) { h += '<li data-qn="' + escH(c.qualified_name) + '">' + escH(c.label) + '</li>'; });
|
|
1266
|
+
h += '</ul></div>';
|
|
1267
|
+
}
|
|
1268
|
+
if (callees.length) {
|
|
1269
|
+
h += '<div class="dp-section"><h4>Callees (' + callees.length + ')</h4><ul class="dp-list">';
|
|
1270
|
+
callees.slice(0, 20).forEach(function(c) { h += '<li data-qn="' + escH(c.qualified_name) + '">' + escH(c.label) + '</li>'; });
|
|
1271
|
+
h += '</ul></div>';
|
|
1272
|
+
}
|
|
1273
|
+
dpContent.textContent = "";
|
|
1274
|
+
dpContent.insertAdjacentHTML("beforeend", h);
|
|
1275
|
+
detailPanel.classList.add("visible");
|
|
1276
|
+
document.getElementById("legend").style.display = "none";
|
|
1277
|
+
detailPanel.querySelector(".dp-close").focus();
|
|
1278
|
+
dpContent.querySelectorAll("li[data-qn]").forEach(function(li) {
|
|
1279
|
+
li.addEventListener("click", function() {
|
|
1280
|
+
var qn = li.dataset.qn;
|
|
1281
|
+
zoomToNode(qn);
|
|
1282
|
+
var nd = nodeById.get(qn);
|
|
1283
|
+
if (nd) showDetailPanel(nd);
|
|
1284
|
+
});
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
var searchInput = document.getElementById("search");
|
|
1288
|
+
var searchResults = document.getElementById("search-results");
|
|
1289
|
+
var searchTerm = "";
|
|
1290
|
+
searchInput.addEventListener("input", function() {
|
|
1291
|
+
searchTerm = this.value.trim().toLowerCase();
|
|
1292
|
+
applySearchFilter();
|
|
1293
|
+
showSearchResults();
|
|
1294
|
+
});
|
|
1295
|
+
searchInput.addEventListener("focus", showSearchResults);
|
|
1296
|
+
searchInput.addEventListener("keydown", function(ev) {
|
|
1297
|
+
var items = searchResults.querySelectorAll(".sr-item");
|
|
1298
|
+
if (!items.length) return;
|
|
1299
|
+
var active = searchResults.querySelector(".sr-item.sr-active");
|
|
1300
|
+
var idx = active ? Array.from(items).indexOf(active) : -1;
|
|
1301
|
+
if (ev.key === "ArrowDown") {
|
|
1302
|
+
ev.preventDefault();
|
|
1303
|
+
if (active) active.classList.remove("sr-active");
|
|
1304
|
+
idx = (idx + 1) % items.length;
|
|
1305
|
+
items[idx].classList.add("sr-active");
|
|
1306
|
+
items[idx].scrollIntoView({ block: "nearest" });
|
|
1307
|
+
searchInput.setAttribute("aria-activedescendant", items[idx].id);
|
|
1308
|
+
} else if (ev.key === "ArrowUp") {
|
|
1309
|
+
ev.preventDefault();
|
|
1310
|
+
if (active) active.classList.remove("sr-active");
|
|
1311
|
+
idx = idx <= 0 ? items.length - 1 : idx - 1;
|
|
1312
|
+
items[idx].classList.add("sr-active");
|
|
1313
|
+
items[idx].scrollIntoView({ block: "nearest" });
|
|
1314
|
+
searchInput.setAttribute("aria-activedescendant", items[idx].id);
|
|
1315
|
+
} else if (ev.key === "Enter" && active) {
|
|
1316
|
+
ev.preventDefault();
|
|
1317
|
+
active.click();
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
document.addEventListener("click", function(ev) {
|
|
1321
|
+
if (!searchResults.contains(ev.target) && ev.target !== searchInput) searchResults.style.display = "none";
|
|
1322
|
+
});
|
|
1323
|
+
function showSearchResults() {
|
|
1324
|
+
if (!searchTerm) { searchResults.style.display = "none"; searchInput.setAttribute("aria-expanded", "false"); return; }
|
|
1325
|
+
var matched = [];
|
|
1326
|
+
nodes.forEach(function(n) {
|
|
1327
|
+
if (n._hidden) return;
|
|
1328
|
+
var hay = (n.label + " " + n.qualified_name).toLowerCase();
|
|
1329
|
+
if (hay.indexOf(searchTerm) !== -1) matched.push(n);
|
|
1330
|
+
});
|
|
1331
|
+
if (!matched.length) { searchResults.style.display = "none"; searchInput.setAttribute("aria-expanded", "false"); return; }
|
|
1332
|
+
searchResults.textContent = "";
|
|
1333
|
+
matched.slice(0, 15).forEach(function(n, i) {
|
|
1334
|
+
var bg = KIND_COLOR[n.kind] || "#555";
|
|
1335
|
+
var div = document.createElement("div");
|
|
1336
|
+
div.className = "sr-item";
|
|
1337
|
+
div.setAttribute("role", "option");
|
|
1338
|
+
div.setAttribute("tabindex", "-1");
|
|
1339
|
+
div.id = "sr-" + i;
|
|
1340
|
+
var kindSpan = document.createElement("span");
|
|
1341
|
+
kindSpan.className = "sr-kind";
|
|
1342
|
+
kindSpan.style.background = bg;
|
|
1343
|
+
kindSpan.style.color = "#0d1117";
|
|
1344
|
+
kindSpan.textContent = n.kind;
|
|
1345
|
+
div.appendChild(kindSpan);
|
|
1346
|
+
div.appendChild(document.createTextNode(" " + n.label));
|
|
1347
|
+
div.addEventListener("click", function() {
|
|
1348
|
+
zoomToNode(n.qualified_name);
|
|
1349
|
+
showDetailPanel(n);
|
|
1350
|
+
searchResults.style.display = "none";
|
|
1351
|
+
});
|
|
1352
|
+
searchResults.appendChild(div);
|
|
1353
|
+
});
|
|
1354
|
+
searchResults.style.display = "block";
|
|
1355
|
+
searchInput.setAttribute("aria-expanded", "true");
|
|
1356
|
+
}
|
|
1357
|
+
function applySearchFilter() {
|
|
1358
|
+
if (!searchTerm) {
|
|
1359
|
+
nodeGroup.selectAll("g.node-g").select(".node-shape").attr("opacity", 1);
|
|
1360
|
+
if (labelSel) labelSel.attr("opacity", 1);
|
|
1361
|
+
if (linkSel) linkSel.attr("opacity", function(e) { return eStyle(e).opacity; });
|
|
1362
|
+
updateLabelVisibility();
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
var matched = new Set();
|
|
1366
|
+
nodes.forEach(function(n) {
|
|
1367
|
+
if (n._hidden) return;
|
|
1368
|
+
var hay = (n.label + " " + n.qualified_name).toLowerCase();
|
|
1369
|
+
if (hay.indexOf(searchTerm) !== -1) matched.add(n.qualified_name);
|
|
1370
|
+
});
|
|
1371
|
+
nodeGroup.selectAll("g.node-g").select(".node-shape")
|
|
1372
|
+
.attr("opacity", function(d) { return matched.has(d.qualified_name) ? 1 : 0.08; });
|
|
1373
|
+
if (labelSel) labelSel
|
|
1374
|
+
.attr("opacity", function(d) { return matched.has(d.qualified_name) ? 1 : 0.05; })
|
|
1375
|
+
.attr("display", function(d) { return matched.has(d.qualified_name) ? null : "none"; });
|
|
1376
|
+
if (linkSel) linkSel.attr("opacity", function(e) {
|
|
1377
|
+
var s = typeof e.source === "object" ? e.source.qualified_name : e._source;
|
|
1378
|
+
var t = typeof e.target === "object" ? e.target.qualified_name : e._target;
|
|
1379
|
+
return (matched.has(s) || matched.has(t)) ? eStyle(e).opacity : 0.02;
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
var helpOverlay = document.getElementById("help-overlay");
|
|
1383
|
+
function toggleHelp() {
|
|
1384
|
+
helpOverlay.classList.toggle("hidden");
|
|
1385
|
+
if (!helpOverlay.classList.contains("hidden")) {
|
|
1386
|
+
helpOverlay.querySelector(".help-close").focus();
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
document.getElementById("btn-help").addEventListener("click", toggleHelp);
|
|
1390
|
+
helpOverlay.querySelector(".help-close").addEventListener("click", toggleHelp);
|
|
1391
|
+
helpOverlay.addEventListener("click", function(ev) {
|
|
1392
|
+
if (ev.target === helpOverlay) toggleHelp();
|
|
1393
|
+
});
|
|
1394
|
+
document.addEventListener("keydown", function(ev) {
|
|
1395
|
+
var tag = (ev.target.tagName || "").toLowerCase();
|
|
1396
|
+
var inInput = tag === "input" || tag === "textarea" || tag === "select" || ev.target.isContentEditable;
|
|
1397
|
+
if (ev.key === "Escape") {
|
|
1398
|
+
if (detailPanel.classList.contains("visible")) {
|
|
1399
|
+
detailPanel.classList.remove("visible");
|
|
1400
|
+
if (detailTrigger) detailTrigger.focus();
|
|
1401
|
+
} else if (searchResults.style.display === "block") {
|
|
1402
|
+
searchResults.style.display = "none";
|
|
1403
|
+
searchInput.focus();
|
|
1404
|
+
} else if (!helpOverlay.classList.contains("hidden")) {
|
|
1405
|
+
toggleHelp();
|
|
1406
|
+
}
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
if (inInput) return;
|
|
1410
|
+
if (ev.key === "/") {
|
|
1411
|
+
ev.preventDefault();
|
|
1412
|
+
searchInput.focus();
|
|
1413
|
+
} else if (ev.key === "?") {
|
|
1414
|
+
ev.preventDefault();
|
|
1415
|
+
toggleHelp();
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
</script>
|
|
1419
|
+
</body>
|
|
1420
|
+
</html>
|
|
1421
|
+
"""
|
|
1422
|
+
|
|
1423
|
+
# ---------------------------------------------------------------------------
|
|
1424
|
+
# Aggregated-mode HTML template (community / file)
|
|
1425
|
+
# ---------------------------------------------------------------------------
|
|
1426
|
+
# Supports community super-nodes with drill-down (double-click) and a Back
|
|
1427
|
+
# button to return to the overview.
|
|
1428
|
+
# NOTE: innerHTML / insertAdjacentHTML usage below mirrors the original
|
|
1429
|
+
# _HTML_TEMPLATE and is safe because all interpolated values pass through
|
|
1430
|
+
# escH() which escapes &, <, >, ", ', and backtick characters.
|
|
1431
|
+
|
|
1432
|
+
_AGGREGATED_HTML_TEMPLATE = r"""<!DOCTYPE html>
|
|
1433
|
+
<html lang="en">
|
|
1434
|
+
<head>
|
|
1435
|
+
<meta charset="utf-8">
|
|
1436
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1437
|
+
<title>Code Review Graph (Aggregated)</title>
|
|
1438
|
+
<script src="https://d3js.org/d3.v7.min.js" integrity="sha384-CjloA8y00+1SDAUkjs099PVfnY2KmDC2BZnws9kh8D/lX1s46w6EPhpXdqMfjK6i" crossorigin="anonymous"></script>
|
|
1439
|
+
<style>
|
|
1440
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1441
|
+
html, body { width: 100%; height: 100%; overflow: hidden; }
|
|
1442
|
+
body {
|
|
1443
|
+
background: #0d1117; color: #c9d1d9;
|
|
1444
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
1445
|
+
font-size: 13px;
|
|
1446
|
+
}
|
|
1447
|
+
svg { display: block; width: 100%; height: 100%; }
|
|
1448
|
+
#legend {
|
|
1449
|
+
position: absolute; top: 16px; left: 16px;
|
|
1450
|
+
background: rgba(22,27,34,0.95); border: 1px solid #30363d;
|
|
1451
|
+
border-radius: 10px; padding: 16px 20px;
|
|
1452
|
+
font-size: 12px; line-height: 1.8;
|
|
1453
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
|
1454
|
+
backdrop-filter: blur(12px); z-index: 10;
|
|
1455
|
+
}
|
|
1456
|
+
#legend h3 {
|
|
1457
|
+
font-size: 11px; font-weight: 700; margin-bottom: 6px;
|
|
1458
|
+
color: #8b949e; text-transform: uppercase; letter-spacing: 1px;
|
|
1459
|
+
}
|
|
1460
|
+
.legend-section { margin-bottom: 10px; }
|
|
1461
|
+
.legend-section:last-child { margin-bottom: 0; }
|
|
1462
|
+
.legend-item { display: flex; align-items: center; gap: 10px; padding: 2px 0; cursor: default; }
|
|
1463
|
+
.legend-circle { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
|
1464
|
+
.legend-line { width: 24px; height: 0; flex-shrink: 0; border-top-width: 2px; }
|
|
1465
|
+
.l-cross { border-top: 2px solid #58a6ff; }
|
|
1466
|
+
.l-dep { border-top: 2px dashed #f0883e; }
|
|
1467
|
+
.l-calls { border-top: 2px solid #3fb950; }
|
|
1468
|
+
.l-imports { border-top: 2px dashed #f0883e; }
|
|
1469
|
+
.l-inherits { border-top: 2.5px dotted #d2a8ff; }
|
|
1470
|
+
.l-contains { border-top: 1.5px solid rgba(139,148,158,0.3); }
|
|
1471
|
+
#stats-bar {
|
|
1472
|
+
position: absolute; bottom: 0; left: 0; right: 0;
|
|
1473
|
+
background: rgba(13,17,23,0.95); border-top: 1px solid #21262d;
|
|
1474
|
+
padding: 8px 24px; display: flex; gap: 32px; justify-content: center;
|
|
1475
|
+
font-size: 12px; color: #8b949e; backdrop-filter: blur(12px);
|
|
1476
|
+
}
|
|
1477
|
+
.stat-item { display: flex; gap: 6px; align-items: center; }
|
|
1478
|
+
.stat-value { color: #e6edf3; font-weight: 600; }
|
|
1479
|
+
#tooltip {
|
|
1480
|
+
position: absolute; pointer-events: none;
|
|
1481
|
+
background: rgba(22,27,34,0.97); color: #c9d1d9;
|
|
1482
|
+
border: 1px solid #30363d; border-radius: 8px;
|
|
1483
|
+
padding: 12px 16px; font-size: 12px;
|
|
1484
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
|
1485
|
+
max-width: 360px; line-height: 1.7;
|
|
1486
|
+
opacity: 0; transition: opacity 0.15s ease;
|
|
1487
|
+
z-index: 1000; backdrop-filter: blur(12px);
|
|
1488
|
+
}
|
|
1489
|
+
#tooltip.visible { opacity: 1; }
|
|
1490
|
+
.tt-name { font-weight: 700; font-size: 14px; color: #e6edf3; }
|
|
1491
|
+
.tt-kind {
|
|
1492
|
+
display: inline-block; font-size: 9px; font-weight: 700;
|
|
1493
|
+
padding: 2px 8px; border-radius: 10px; margin-left: 8px;
|
|
1494
|
+
text-transform: uppercase; letter-spacing: 0.5px;
|
|
1495
|
+
}
|
|
1496
|
+
.tt-row { margin-top: 4px; }
|
|
1497
|
+
.tt-label { color: #8b949e; }
|
|
1498
|
+
.tt-file { color: #58a6ff; font-size: 11px; }
|
|
1499
|
+
#controls {
|
|
1500
|
+
position: absolute; top: 16px; right: 16px;
|
|
1501
|
+
display: flex; gap: 8px; z-index: 10; flex-wrap: wrap;
|
|
1502
|
+
max-width: 650px; justify-content: flex-end;
|
|
1503
|
+
}
|
|
1504
|
+
#controls button, #controls select {
|
|
1505
|
+
background: rgba(22,27,34,0.95); color: #c9d1d9;
|
|
1506
|
+
border: 1px solid #30363d; border-radius: 8px;
|
|
1507
|
+
padding: 8px 14px; font-size: 12px; cursor: pointer;
|
|
1508
|
+
backdrop-filter: blur(12px); transition: all 0.15s;
|
|
1509
|
+
}
|
|
1510
|
+
#controls button:hover, #controls select:hover { background: #30363d; border-color: #8b949e; }
|
|
1511
|
+
#controls button.active { background: #1f6feb; border-color: #58a6ff; color: #fff; }
|
|
1512
|
+
#controls select { outline: none; max-width: 200px; }
|
|
1513
|
+
#controls select option { background: #161b22; color: #c9d1d9; }
|
|
1514
|
+
#search {
|
|
1515
|
+
background: rgba(22,27,34,0.95); color: #c9d1d9;
|
|
1516
|
+
border: 1px solid #30363d; border-radius: 8px;
|
|
1517
|
+
padding: 8px 14px; font-size: 12px; width: 220px;
|
|
1518
|
+
outline: none; backdrop-filter: blur(12px);
|
|
1519
|
+
}
|
|
1520
|
+
#search:focus { border-color: #58a6ff; }
|
|
1521
|
+
#search::placeholder { color: #484f58; }
|
|
1522
|
+
#search-results {
|
|
1523
|
+
position: absolute; top: 52px; right: 16px;
|
|
1524
|
+
background: rgba(22,27,34,0.97); border: 1px solid #30363d;
|
|
1525
|
+
border-radius: 8px; max-height: 240px; overflow-y: auto;
|
|
1526
|
+
z-index: 15; display: none; min-width: 220px;
|
|
1527
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
|
1528
|
+
}
|
|
1529
|
+
.sr-item {
|
|
1530
|
+
padding: 8px 14px; cursor: pointer; font-size: 12px;
|
|
1531
|
+
border-bottom: 1px solid #21262d; display: flex; gap: 8px; align-items: center;
|
|
1532
|
+
}
|
|
1533
|
+
.sr-item:hover { background: #30363d; }
|
|
1534
|
+
.sr-item:last-child { border-bottom: none; }
|
|
1535
|
+
.sr-kind { font-size: 9px; padding: 2px 6px; border-radius: 8px; text-transform: uppercase; font-weight: 700; }
|
|
1536
|
+
#detail-panel {
|
|
1537
|
+
position: absolute; top: 16px; right: 16px;
|
|
1538
|
+
width: 320px; max-height: calc(100vh - 80px);
|
|
1539
|
+
background: rgba(22,27,34,0.97); border: 1px solid #30363d;
|
|
1540
|
+
border-radius: 10px; padding: 20px;
|
|
1541
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
|
1542
|
+
backdrop-filter: blur(12px); z-index: 20;
|
|
1543
|
+
overflow-y: auto; display: none; font-size: 12px;
|
|
1544
|
+
}
|
|
1545
|
+
#detail-panel.visible { display: block; }
|
|
1546
|
+
#detail-panel h2 { font-size: 16px; color: #e6edf3; margin-bottom: 4px; word-break: break-all; }
|
|
1547
|
+
#detail-panel .dp-close {
|
|
1548
|
+
position: absolute; top: 12px; right: 14px;
|
|
1549
|
+
cursor: pointer; color: #8b949e; font-size: 18px; line-height: 1;
|
|
1550
|
+
border: none; background: none;
|
|
1551
|
+
}
|
|
1552
|
+
#detail-panel .dp-close:hover { color: #e6edf3; }
|
|
1553
|
+
.dp-section { margin-top: 14px; }
|
|
1554
|
+
.dp-section h4 { color: #8b949e; font-size: 10px; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 6px; }
|
|
1555
|
+
.dp-list { list-style: none; }
|
|
1556
|
+
.dp-list li { padding: 3px 0; color: #c9d1d9; cursor: pointer; }
|
|
1557
|
+
.dp-list li:hover { color: #58a6ff; text-decoration: underline; }
|
|
1558
|
+
.dp-meta { color: #8b949e; }
|
|
1559
|
+
.dp-meta span { color: #e6edf3; font-weight: 600; }
|
|
1560
|
+
#btn-back {
|
|
1561
|
+
display: none;
|
|
1562
|
+
position: absolute; bottom: 50px; right: 16px;
|
|
1563
|
+
background: #1f6feb; color: #fff;
|
|
1564
|
+
border: 1px solid #58a6ff; border-radius: 8px;
|
|
1565
|
+
padding: 10px 18px; font-size: 13px; cursor: pointer;
|
|
1566
|
+
z-index: 10; font-weight: 600;
|
|
1567
|
+
box-shadow: 0 4px 16px rgba(31,111,235,0.4);
|
|
1568
|
+
transition: all 0.15s;
|
|
1569
|
+
}
|
|
1570
|
+
#btn-back:hover { background: #388bfd; }
|
|
1571
|
+
#filter-panel {
|
|
1572
|
+
position: absolute; bottom: 50px; left: 16px;
|
|
1573
|
+
background: rgba(22,27,34,0.95); border: 1px solid #30363d;
|
|
1574
|
+
border-radius: 10px; padding: 14px 18px;
|
|
1575
|
+
font-size: 12px; backdrop-filter: blur(12px); z-index: 10;
|
|
1576
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
|
1577
|
+
}
|
|
1578
|
+
#filter-panel h3 {
|
|
1579
|
+
font-size: 11px; font-weight: 700; margin-bottom: 8px;
|
|
1580
|
+
color: #8b949e; text-transform: uppercase; letter-spacing: 1px;
|
|
1581
|
+
}
|
|
1582
|
+
.filter-item { display: flex; align-items: center; gap: 8px; padding: 3px 0; cursor: pointer; user-select: none; }
|
|
1583
|
+
.filter-item input { accent-color: #58a6ff; cursor: pointer; }
|
|
1584
|
+
marker { overflow: visible; }
|
|
1585
|
+
</style>
|
|
1586
|
+
</head>
|
|
1587
|
+
<body>
|
|
1588
|
+
<div id="legend" role="complementary" aria-label="Graph legend">
|
|
1589
|
+
<h3>Nodes</h3>
|
|
1590
|
+
<div class="legend-section" id="legend-nodes"></div>
|
|
1591
|
+
<h3>Edges</h3>
|
|
1592
|
+
<div class="legend-section" id="legend-edges"></div>
|
|
1593
|
+
</div>
|
|
1594
|
+
<div id="filter-panel">
|
|
1595
|
+
<h3>View Mode</h3>
|
|
1596
|
+
<div id="filter-info" style="color:#8b949e;font-size:11px;"></div>
|
|
1597
|
+
</div>
|
|
1598
|
+
<div id="controls">
|
|
1599
|
+
<input id="search" type="text" placeholder="Search nodes…" autocomplete="off" spellcheck="false" aria-label="Search graph nodes by name">
|
|
1600
|
+
<button id="btn-fit" title="Fit to screen" aria-label="Fit graph to screen">Fit</button>
|
|
1601
|
+
<button id="btn-labels" title="Toggle labels" class="active" aria-label="Toggle node labels" aria-pressed="true">Labels</button>
|
|
1602
|
+
</div>
|
|
1603
|
+
<div id="search-results"></div>
|
|
1604
|
+
<div id="detail-panel"><button class="dp-close" aria-label="Close detail panel">×</button><div id="dp-content"></div></div>
|
|
1605
|
+
<div id="stats-bar" role="status" aria-label="Graph statistics"></div>
|
|
1606
|
+
<div id="tooltip"></div>
|
|
1607
|
+
<button id="btn-back" aria-label="Back to overview">← Back to Overview</button>
|
|
1608
|
+
<svg role="img" aria-label="Interactive code knowledge graph visualization (aggregated view)."></svg>
|
|
1609
|
+
<script>
|
|
1610
|
+
"use strict";
|
|
1611
|
+
var graphData = __GRAPH_DATA__;
|
|
1612
|
+
var dataMode = graphData.mode || "full";
|
|
1613
|
+
var communityDetails = graphData.community_details || {};
|
|
1614
|
+
|
|
1615
|
+
var communityColorScale = d3.scaleOrdinal(d3.schemeTableau10);
|
|
1616
|
+
function escH(s) { return !s ? "" : s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/`/g,"`"); }
|
|
1617
|
+
|
|
1618
|
+
var KIND_COLOR = {
|
|
1619
|
+
Community: "#1f6feb", File: "#58a6ff", Class: "#f0883e",
|
|
1620
|
+
Function: "#3fb950", Test: "#d2a8ff", Type: "#8b949e"
|
|
1621
|
+
};
|
|
1622
|
+
var EDGE_COLOR = {
|
|
1623
|
+
CROSS_COMMUNITY: "#58a6ff", DEPENDS_ON: "#f0883e",
|
|
1624
|
+
CALLS: "#3fb950", IMPORTS_FROM: "#f0883e",
|
|
1625
|
+
INHERITS: "#d2a8ff", CONTAINS: "rgba(139,148,158,0.15)"
|
|
1626
|
+
};
|
|
1627
|
+
var EDGE_CFG = {
|
|
1628
|
+
CROSS_COMMUNITY: { dash: null, width: 2, opacity: 0.6, marker: "" },
|
|
1629
|
+
DEPENDS_ON: { dash: "6,3", width: 1.5, opacity: 0.5, marker: "" },
|
|
1630
|
+
CONTAINS: { dash: null, width: 1, opacity: 0.08, marker: "" },
|
|
1631
|
+
CALLS: { dash: null, width: 1.5, opacity: 0.7, marker: "url(#arrow-calls)" },
|
|
1632
|
+
IMPORTS_FROM: { dash: "6,3", width: 1.5, opacity: 0.65, marker: "url(#arrow-imports)" },
|
|
1633
|
+
INHERITS: { dash: "3,4", width: 2, opacity: 0.7, marker: "url(#arrow-inherits)" },
|
|
1634
|
+
};
|
|
1635
|
+
function eStyle(d) { return EDGE_CFG[d.kind] || { dash: null, width: 1, opacity: 0.3, marker: "" }; }
|
|
1636
|
+
function eColor(d) { return EDGE_COLOR[d.kind] || "#484f58"; }
|
|
1637
|
+
|
|
1638
|
+
/* --- Legend setup --- */
|
|
1639
|
+
var legendNodes = document.getElementById("legend-nodes");
|
|
1640
|
+
var legendEdges = document.getElementById("legend-edges");
|
|
1641
|
+
function buildLegend(nodeKinds, edgeKinds) {
|
|
1642
|
+
legendNodes.textContent = "";
|
|
1643
|
+
legendEdges.textContent = "";
|
|
1644
|
+
nodeKinds.forEach(function(k) {
|
|
1645
|
+
var div = document.createElement("div");
|
|
1646
|
+
div.className = "legend-item";
|
|
1647
|
+
var circle = document.createElement("span");
|
|
1648
|
+
circle.className = "legend-circle";
|
|
1649
|
+
circle.style.background = KIND_COLOR[k] || "#8b949e";
|
|
1650
|
+
div.appendChild(circle);
|
|
1651
|
+
div.appendChild(document.createTextNode(" " + k));
|
|
1652
|
+
legendNodes.appendChild(div);
|
|
1653
|
+
});
|
|
1654
|
+
edgeKinds.forEach(function(k) {
|
|
1655
|
+
var div = document.createElement("div");
|
|
1656
|
+
div.className = "legend-item";
|
|
1657
|
+
var cls = k === "CROSS_COMMUNITY" ? "l-cross" : k === "DEPENDS_ON" ? "l-dep" : k === "CALLS" ? "l-calls" : k === "IMPORTS_FROM" ? "l-imports" : k === "INHERITS" ? "l-inherits" : "l-contains";
|
|
1658
|
+
var line = document.createElement("span");
|
|
1659
|
+
line.className = "legend-line " + cls;
|
|
1660
|
+
div.appendChild(line);
|
|
1661
|
+
var label = k.replace(/_/g, " ").replace(/\b\w/g, function(c) { return c.toUpperCase(); });
|
|
1662
|
+
div.appendChild(document.createTextNode(" " + label));
|
|
1663
|
+
legendEdges.appendChild(div);
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/* --- Stats bar --- */
|
|
1668
|
+
var statsBar = document.getElementById("stats-bar");
|
|
1669
|
+
var stats = graphData.stats;
|
|
1670
|
+
function addStat(label, value) {
|
|
1671
|
+
var div = document.createElement("div");
|
|
1672
|
+
div.className = "stat-item";
|
|
1673
|
+
var lbl = document.createElement("span");
|
|
1674
|
+
lbl.className = "tt-label";
|
|
1675
|
+
lbl.textContent = label;
|
|
1676
|
+
var val = document.createElement("span");
|
|
1677
|
+
val.className = "stat-value";
|
|
1678
|
+
val.textContent = String(value);
|
|
1679
|
+
div.appendChild(lbl);
|
|
1680
|
+
div.appendChild(document.createTextNode(" "));
|
|
1681
|
+
div.appendChild(val);
|
|
1682
|
+
statsBar.appendChild(div);
|
|
1683
|
+
}
|
|
1684
|
+
var langList = (stats.languages || []).join(", ") || "n/a";
|
|
1685
|
+
statsBar.textContent = "";
|
|
1686
|
+
addStat("Nodes", stats.total_nodes);
|
|
1687
|
+
addStat("Edges", stats.total_edges);
|
|
1688
|
+
addStat("Files", stats.files_count);
|
|
1689
|
+
addStat("Languages", langList);
|
|
1690
|
+
addStat("Mode", dataMode);
|
|
1691
|
+
|
|
1692
|
+
/* --- Filter info --- */
|
|
1693
|
+
var filterInfo = document.getElementById("filter-info");
|
|
1694
|
+
if (dataMode === "community") {
|
|
1695
|
+
filterInfo.textContent = "Showing communities. Double-click to drill down.";
|
|
1696
|
+
} else if (dataMode === "file") {
|
|
1697
|
+
filterInfo.textContent = "Showing file-level aggregation.";
|
|
1698
|
+
} else {
|
|
1699
|
+
filterInfo.textContent = "Showing all nodes.";
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
/* --- Tooltip --- */
|
|
1703
|
+
var tooltip = document.getElementById("tooltip");
|
|
1704
|
+
function showTooltip(ev, d) {
|
|
1705
|
+
var bg = KIND_COLOR[d.kind] || "#555";
|
|
1706
|
+
tooltip.textContent = "";
|
|
1707
|
+
var nameSpan = document.createElement("span");
|
|
1708
|
+
nameSpan.className = "tt-name";
|
|
1709
|
+
nameSpan.textContent = d.name || d.label;
|
|
1710
|
+
tooltip.appendChild(nameSpan);
|
|
1711
|
+
var kindSpan = document.createElement("span");
|
|
1712
|
+
kindSpan.className = "tt-kind";
|
|
1713
|
+
kindSpan.style.background = bg;
|
|
1714
|
+
kindSpan.style.color = "#0d1117";
|
|
1715
|
+
kindSpan.textContent = d.kind;
|
|
1716
|
+
tooltip.appendChild(kindSpan);
|
|
1717
|
+
function addRow(label, value) {
|
|
1718
|
+
var row = document.createElement("div");
|
|
1719
|
+
row.className = "tt-row";
|
|
1720
|
+
var lbl = document.createElement("span");
|
|
1721
|
+
lbl.className = "tt-label";
|
|
1722
|
+
lbl.textContent = label + ": ";
|
|
1723
|
+
row.appendChild(lbl);
|
|
1724
|
+
row.appendChild(document.createTextNode(String(value)));
|
|
1725
|
+
tooltip.appendChild(row);
|
|
1726
|
+
}
|
|
1727
|
+
if (d.member_count != null) addRow("Members", d.member_count);
|
|
1728
|
+
if (d.symbol_count != null) addRow("Symbols", d.symbol_count);
|
|
1729
|
+
if (d.description) addRow("Description", d.description);
|
|
1730
|
+
if (d.language) addRow("Language", d.language);
|
|
1731
|
+
if (d.file_path) {
|
|
1732
|
+
var relFile = d.file_path.split("/").slice(-3).join("/");
|
|
1733
|
+
if (relFile) {
|
|
1734
|
+
var fileRow = document.createElement("div");
|
|
1735
|
+
fileRow.className = "tt-row tt-file";
|
|
1736
|
+
fileRow.textContent = relFile;
|
|
1737
|
+
tooltip.appendChild(fileRow);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if (d.line_start != null) addRow("Lines", d.line_start + " \u2013 " + (d.line_end || d.line_start));
|
|
1741
|
+
if (d.params) addRow("Params", d.params);
|
|
1742
|
+
if (d.return_type) addRow("Returns", d.return_type);
|
|
1743
|
+
if (d.weight != null) addRow("Weight", d.weight);
|
|
1744
|
+
tooltip.classList.add("visible");
|
|
1745
|
+
moveTooltip(ev);
|
|
1746
|
+
}
|
|
1747
|
+
function moveTooltip(ev) {
|
|
1748
|
+
var p = 14;
|
|
1749
|
+
var x = ev.pageX + p, y = ev.pageY + p;
|
|
1750
|
+
var r = tooltip.getBoundingClientRect();
|
|
1751
|
+
if (x + r.width > innerWidth - p) x = ev.pageX - r.width - p;
|
|
1752
|
+
if (y + r.height > innerHeight - p) y = ev.pageY - r.height - p;
|
|
1753
|
+
tooltip.style.left = x + "px"; tooltip.style.top = y + "px";
|
|
1754
|
+
}
|
|
1755
|
+
function hideTooltip() { tooltip.classList.remove("visible"); }
|
|
1756
|
+
|
|
1757
|
+
/* --- SVG setup --- */
|
|
1758
|
+
var W = innerWidth, H = innerHeight;
|
|
1759
|
+
var svg = d3.select("svg").attr("viewBox", [0, 0, W, H]);
|
|
1760
|
+
var gRoot = svg.append("g");
|
|
1761
|
+
var currentTransform = d3.zoomIdentity;
|
|
1762
|
+
var zoomBehavior = d3.zoom()
|
|
1763
|
+
.scaleExtent([0.05, 8])
|
|
1764
|
+
.on("zoom", function(ev) { currentTransform = ev.transform; gRoot.attr("transform", ev.transform); updateLabelVisibility(); });
|
|
1765
|
+
svg.call(zoomBehavior);
|
|
1766
|
+
var defs = svg.append("defs");
|
|
1767
|
+
var glow = defs.append("filter").attr("id","glow").attr("x","-50%").attr("y","-50%").attr("width","200%").attr("height","200%");
|
|
1768
|
+
glow.append("feGaussianBlur").attr("stdDeviation","3").attr("result","blur");
|
|
1769
|
+
glow.append("feComposite").attr("in","SourceGraphic").attr("in2","blur").attr("operator","over");
|
|
1770
|
+
[{id:"arrow-calls",color:"#3fb950"},{id:"arrow-imports",color:"#f0883e"},{id:"arrow-inherits",color:"#d2a8ff"}].forEach(function(mk) {
|
|
1771
|
+
defs.append("marker").attr("id", mk.id)
|
|
1772
|
+
.attr("viewBox","0 -5 10 10").attr("refX",28).attr("refY",0)
|
|
1773
|
+
.attr("markerWidth",8).attr("markerHeight",8).attr("orient","auto")
|
|
1774
|
+
.append("path").attr("d","M0,-4L10,0L0,4Z").attr("fill",mk.color);
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
var linkGroup = gRoot.append("g").attr("class","links");
|
|
1778
|
+
var nodeGroup = gRoot.append("g").attr("class","nodes");
|
|
1779
|
+
var labelGroup = gRoot.append("g").attr("class","labels");
|
|
1780
|
+
var linkSel = null, labelSel = null;
|
|
1781
|
+
var showLabels = true;
|
|
1782
|
+
var simulation = null;
|
|
1783
|
+
var currentNodes = [];
|
|
1784
|
+
var currentEdges = [];
|
|
1785
|
+
var isDrilledDown = false;
|
|
1786
|
+
|
|
1787
|
+
function nodeRadius(d) {
|
|
1788
|
+
if (d.kind === "Community") return Math.max(12, Math.min(40, 8 + Math.sqrt(d.member_count || 1) * 3));
|
|
1789
|
+
if (d.kind === "File") {
|
|
1790
|
+
if (d.symbol_count != null) return Math.max(8, Math.min(30, 6 + Math.sqrt(d.symbol_count || 1) * 2));
|
|
1791
|
+
return 18;
|
|
1792
|
+
}
|
|
1793
|
+
return { Class: 12, Function: 6, Test: 6, Type: 5 }[d.kind] || 6;
|
|
1794
|
+
}
|
|
1795
|
+
function nodeColor(d) {
|
|
1796
|
+
if (d.kind === "Community" && d.community_id != null) return communityColorScale(d.community_id);
|
|
1797
|
+
return KIND_COLOR[d.kind] || "#8b949e";
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
function renderGraph(nodesData, edgesData, drillDown) {
|
|
1801
|
+
isDrilledDown = !!drillDown;
|
|
1802
|
+
currentNodes = nodesData.map(function(d) {
|
|
1803
|
+
var o = Object.assign({}, d);
|
|
1804
|
+
o._id = d.qualified_name;
|
|
1805
|
+
o.label = d.name;
|
|
1806
|
+
return o;
|
|
1807
|
+
});
|
|
1808
|
+
currentEdges = edgesData.map(function(d) {
|
|
1809
|
+
var o = Object.assign({}, d);
|
|
1810
|
+
o._source = d.source;
|
|
1811
|
+
o._target = d.target;
|
|
1812
|
+
return o;
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
/* Update legend based on current data */
|
|
1816
|
+
var nodeKindSet = new Set(); var edgeKindSet = new Set();
|
|
1817
|
+
currentNodes.forEach(function(n) { nodeKindSet.add(n.kind); });
|
|
1818
|
+
currentEdges.forEach(function(e) { edgeKindSet.add(e.kind); });
|
|
1819
|
+
buildLegend(Array.from(nodeKindSet), Array.from(edgeKindSet));
|
|
1820
|
+
|
|
1821
|
+
/* Back button */
|
|
1822
|
+
document.getElementById("btn-back").style.display = isDrilledDown ? "block" : "none";
|
|
1823
|
+
|
|
1824
|
+
/* Clear existing */
|
|
1825
|
+
linkGroup.selectAll("*").remove();
|
|
1826
|
+
nodeGroup.selectAll("*").remove();
|
|
1827
|
+
labelGroup.selectAll("*").remove();
|
|
1828
|
+
if (simulation) simulation.stop();
|
|
1829
|
+
|
|
1830
|
+
var N = currentNodes.length;
|
|
1831
|
+
var isLarge = N > 300;
|
|
1832
|
+
var nodeById = new Map(currentNodes.map(function(n) { return [n.qualified_name, n]; }));
|
|
1833
|
+
|
|
1834
|
+
simulation = d3.forceSimulation(currentNodes)
|
|
1835
|
+
.force("link", d3.forceLink(currentEdges).id(function(d) { return d.qualified_name; })
|
|
1836
|
+
.distance(function(d) {
|
|
1837
|
+
if (d.kind === "CONTAINS") return 35;
|
|
1838
|
+
if (d.kind === "CROSS_COMMUNITY" || d.kind === "DEPENDS_ON") return Math.max(100, 200 - (d.weight || 1) * 5);
|
|
1839
|
+
return isLarge ? 80 : 120;
|
|
1840
|
+
})
|
|
1841
|
+
.strength(function(d) {
|
|
1842
|
+
if (d.kind === "CONTAINS") return 1.5;
|
|
1843
|
+
if (d.kind === "CROSS_COMMUNITY" || d.kind === "DEPENDS_ON") return 0.1 + Math.min(0.5, (d.weight || 1) * 0.02);
|
|
1844
|
+
return 0.15;
|
|
1845
|
+
}))
|
|
1846
|
+
.force("charge", d3.forceManyBody().strength(function(d) {
|
|
1847
|
+
if (d.kind === "Community") return -400 - (d.member_count || 0) * 2;
|
|
1848
|
+
return d.kind === "File" ? (isLarge ? -200 : -400) : (isLarge ? -60 : -120);
|
|
1849
|
+
}).theta(0.85).distanceMax(600))
|
|
1850
|
+
.force("collide", d3.forceCollide().radius(function(d) { return nodeRadius(d) + 6; }))
|
|
1851
|
+
.force("center", d3.forceCenter(W / 2, H / 2))
|
|
1852
|
+
.force("x", d3.forceX(W / 2).strength(0.03))
|
|
1853
|
+
.force("y", d3.forceY(H / 2).strength(0.03))
|
|
1854
|
+
.alphaDecay(isLarge ? 0.04 : 0.025)
|
|
1855
|
+
.velocityDecay(0.4);
|
|
1856
|
+
|
|
1857
|
+
/* Draw edges */
|
|
1858
|
+
linkSel = linkGroup.selectAll("line").data(currentEdges, function(d) { return d._source + "->" + d._target + ":" + d.kind; });
|
|
1859
|
+
linkSel.exit().remove();
|
|
1860
|
+
var linkEnter = linkSel.enter().append("line");
|
|
1861
|
+
linkSel = linkEnter.merge(linkSel);
|
|
1862
|
+
linkSel
|
|
1863
|
+
.attr("stroke", function(d) { return eColor(d); })
|
|
1864
|
+
.attr("stroke-width", function(d) {
|
|
1865
|
+
if (d.weight && (d.kind === "CROSS_COMMUNITY" || d.kind === "DEPENDS_ON")) return Math.max(1, Math.min(6, Math.sqrt(d.weight)));
|
|
1866
|
+
return eStyle(d).width;
|
|
1867
|
+
})
|
|
1868
|
+
.attr("stroke-dasharray", function(d) { return eStyle(d).dash; })
|
|
1869
|
+
.attr("opacity", function(d) { return eStyle(d).opacity; })
|
|
1870
|
+
.attr("marker-end", function(d) { return eStyle(d).marker; });
|
|
1871
|
+
|
|
1872
|
+
/* Draw nodes */
|
|
1873
|
+
var nodeSel = nodeGroup.selectAll("g.node-g").data(currentNodes, function(d) { return d.qualified_name; });
|
|
1874
|
+
nodeSel.exit().remove();
|
|
1875
|
+
var enter = nodeSel.enter().append("g").attr("class", "node-g");
|
|
1876
|
+
enter.append("circle")
|
|
1877
|
+
.attr("class", "glow-ring")
|
|
1878
|
+
.attr("r", function(d) { return nodeRadius(d) + 5; })
|
|
1879
|
+
.attr("fill", "none")
|
|
1880
|
+
.attr("stroke", function(d) { return nodeColor(d); })
|
|
1881
|
+
.attr("stroke-width", 1.5).attr("opacity", 0.3).attr("filter", "url(#glow)");
|
|
1882
|
+
enter.append("circle").attr("class", "node-circle")
|
|
1883
|
+
.attr("r", function(d) { return nodeRadius(d); })
|
|
1884
|
+
.attr("fill", function(d) { return nodeColor(d); })
|
|
1885
|
+
.attr("stroke", "rgba(255,255,255,0.15)")
|
|
1886
|
+
.attr("stroke-width", 2)
|
|
1887
|
+
.attr("cursor", "pointer");
|
|
1888
|
+
enter
|
|
1889
|
+
.on("mouseover", function(ev, d) { highlightConnected(d, true); showTooltip(ev, d); })
|
|
1890
|
+
.on("mousemove", function(ev) { moveTooltip(ev); })
|
|
1891
|
+
.on("mouseout", function(ev, d) { highlightConnected(d, false); hideTooltip(); })
|
|
1892
|
+
.on("click", function(ev, d) { ev.stopPropagation(); showDetailPanel(d, nodeById); })
|
|
1893
|
+
.on("dblclick", function(ev, d) {
|
|
1894
|
+
ev.stopPropagation();
|
|
1895
|
+
if (d.kind === "Community" && dataMode === "community") drillIntoCommunity(d);
|
|
1896
|
+
})
|
|
1897
|
+
.call(d3.drag()
|
|
1898
|
+
.on("start", function(ev, d) { if (!ev.active) simulation.alphaTarget(0.1).restart(); d.fx = d.x; d.fy = d.y; })
|
|
1899
|
+
.on("drag", function(ev, d) { d.fx = ev.x; d.fy = ev.y; })
|
|
1900
|
+
.on("end", function(ev, d) { if (!ev.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; })
|
|
1901
|
+
);
|
|
1902
|
+
nodeSel = enter.merge(nodeSel);
|
|
1903
|
+
|
|
1904
|
+
/* Draw labels */
|
|
1905
|
+
labelSel = labelGroup.selectAll("text.node-label").data(currentNodes, function(d) { return d.qualified_name; });
|
|
1906
|
+
labelSel.exit().remove();
|
|
1907
|
+
var lEnter = labelSel.enter().append("text").attr("class", "node-label")
|
|
1908
|
+
.attr("text-anchor", "start").attr("dy", "0.35em")
|
|
1909
|
+
.text(function(d) { return d.label; })
|
|
1910
|
+
.attr("fill", function(d) { return d.kind === "Community" ? "#e6edf3" : d.kind === "File" ? "#e6edf3" : "#8b949e"; })
|
|
1911
|
+
.attr("font-size", function(d) { return d.kind === "Community" ? "13px" : d.kind === "File" ? "12px" : "10px"; })
|
|
1912
|
+
.attr("font-weight", function(d) { return (d.kind === "Community" || d.kind === "File") ? 700 : 400; });
|
|
1913
|
+
labelSel = lEnter.merge(labelSel);
|
|
1914
|
+
|
|
1915
|
+
simulation.on("tick", function() {
|
|
1916
|
+
linkSel
|
|
1917
|
+
.attr("x1", function(d) { return d.source.x; }).attr("y1", function(d) { return d.source.y; })
|
|
1918
|
+
.attr("x2", function(d) { return d.target.x; }).attr("y2", function(d) { return d.target.y; });
|
|
1919
|
+
nodeGroup.selectAll("g.node-g").attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
|
|
1920
|
+
labelSel
|
|
1921
|
+
.attr("x", function(d) { return d.x + nodeRadius(d) + 5; })
|
|
1922
|
+
.attr("y", function(d) { return d.y; });
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
simulation.on("end", fitGraph);
|
|
1926
|
+
updateLabelVisibility();
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function updateLabelVisibility() {
|
|
1930
|
+
if (!labelSel) return;
|
|
1931
|
+
var s = currentTransform.k;
|
|
1932
|
+
labelSel.attr("display", function(d) {
|
|
1933
|
+
if (!showLabels) return "none";
|
|
1934
|
+
if (d.kind === "Community" || d.kind === "File") return null;
|
|
1935
|
+
if (d.kind === "Class") return s > 0.5 ? null : "none";
|
|
1936
|
+
return s > 1.0 ? null : "none";
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
function highlightConnected(d, on) {
|
|
1941
|
+
if (on) {
|
|
1942
|
+
var connected = new Set([d.qualified_name]);
|
|
1943
|
+
currentEdges.forEach(function(e) {
|
|
1944
|
+
var s = typeof e.source === "object" ? e.source.qualified_name : e._source;
|
|
1945
|
+
var t = typeof e.target === "object" ? e.target.qualified_name : e._target;
|
|
1946
|
+
if (s === d.qualified_name) connected.add(t);
|
|
1947
|
+
if (t === d.qualified_name) connected.add(s);
|
|
1948
|
+
});
|
|
1949
|
+
nodeGroup.selectAll("g.node-g").select(".node-circle")
|
|
1950
|
+
.transition().duration(150).attr("opacity", function(n) { return connected.has(n.qualified_name) ? 1 : 0.15; });
|
|
1951
|
+
if (linkSel) linkSel.transition().duration(150)
|
|
1952
|
+
.attr("opacity", function(e) {
|
|
1953
|
+
var s = typeof e.source === "object" ? e.source.qualified_name : e._source;
|
|
1954
|
+
var t = typeof e.target === "object" ? e.target.qualified_name : e._target;
|
|
1955
|
+
return (s === d.qualified_name || t === d.qualified_name) ? 0.9 : 0.03;
|
|
1956
|
+
})
|
|
1957
|
+
.attr("stroke-width", function(e) {
|
|
1958
|
+
var s = typeof e.source === "object" ? e.source.qualified_name : e._source;
|
|
1959
|
+
var t = typeof e.target === "object" ? e.target.qualified_name : e._target;
|
|
1960
|
+
var base = e.weight ? Math.max(1, Math.min(6, Math.sqrt(e.weight))) : eStyle(e).width;
|
|
1961
|
+
return (s === d.qualified_name || t === d.qualified_name) ? base + 1.5 : base;
|
|
1962
|
+
});
|
|
1963
|
+
if (labelSel) labelSel.transition().duration(150).attr("opacity", function(n) { return connected.has(n.qualified_name) ? 1 : 0.1; });
|
|
1964
|
+
} else {
|
|
1965
|
+
nodeGroup.selectAll("g.node-g").select(".node-circle").transition().duration(300).attr("opacity", 1);
|
|
1966
|
+
if (linkSel) linkSel.transition().duration(300)
|
|
1967
|
+
.attr("opacity", function(e) { return eStyle(e).opacity; })
|
|
1968
|
+
.attr("stroke-width", function(e) {
|
|
1969
|
+
if (e.weight && (e.kind === "CROSS_COMMUNITY" || e.kind === "DEPENDS_ON")) return Math.max(1, Math.min(6, Math.sqrt(e.weight)));
|
|
1970
|
+
return eStyle(e).width;
|
|
1971
|
+
});
|
|
1972
|
+
if (labelSel) labelSel.transition().duration(300).attr("opacity", 1);
|
|
1973
|
+
updateLabelVisibility();
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
function fitGraph() {
|
|
1978
|
+
var b = gRoot.node().getBBox();
|
|
1979
|
+
if (b.width === 0 || b.height === 0) return;
|
|
1980
|
+
var pad = 0.1;
|
|
1981
|
+
var fw = b.width * (1 + 2 * pad), fh = b.height * (1 + 2 * pad);
|
|
1982
|
+
var s = Math.min(W / fw, H / fh, 2.5);
|
|
1983
|
+
var tx = W / 2 - (b.x + b.width / 2) * s, ty = H / 2 - (b.y + b.height / 2) * s;
|
|
1984
|
+
svg.transition().duration(600).call(zoomBehavior.transform, d3.zoomIdentity.translate(tx, ty).scale(s));
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
function zoomToNode(qn) {
|
|
1988
|
+
var nd = currentNodes.find(function(n) { return n.qualified_name === qn; });
|
|
1989
|
+
if (!nd || nd.x == null) return;
|
|
1990
|
+
var s = 2.0;
|
|
1991
|
+
var tx = W / 2 - nd.x * s, ty = H / 2 - nd.y * s;
|
|
1992
|
+
svg.transition().duration(600).call(zoomBehavior.transform, d3.zoomIdentity.translate(tx, ty).scale(s));
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
/* --- Detail panel --- */
|
|
1996
|
+
var detailPanel = document.getElementById("detail-panel");
|
|
1997
|
+
var dpContent = document.getElementById("dp-content");
|
|
1998
|
+
document.querySelector("#detail-panel .dp-close").addEventListener("click", function() {
|
|
1999
|
+
detailPanel.classList.remove("visible");
|
|
2000
|
+
});
|
|
2001
|
+
svg.on("click", function() { detailPanel.classList.remove("visible"); });
|
|
2002
|
+
function showDetailPanel(d, nodeById) {
|
|
2003
|
+
dpContent.textContent = "";
|
|
2004
|
+
var h2 = document.createElement("h2");
|
|
2005
|
+
h2.textContent = d.label || d.name;
|
|
2006
|
+
dpContent.appendChild(h2);
|
|
2007
|
+
var kindSpan = document.createElement("span");
|
|
2008
|
+
kindSpan.className = "tt-kind";
|
|
2009
|
+
kindSpan.style.background = KIND_COLOR[d.kind] || "#555";
|
|
2010
|
+
kindSpan.style.color = "#0d1117";
|
|
2011
|
+
kindSpan.textContent = d.kind;
|
|
2012
|
+
dpContent.appendChild(kindSpan);
|
|
2013
|
+
function addMeta(label, value) {
|
|
2014
|
+
var div = document.createElement("div");
|
|
2015
|
+
div.className = "dp-meta";
|
|
2016
|
+
var lbl = document.createElement("span");
|
|
2017
|
+
lbl.className = "tt-label";
|
|
2018
|
+
lbl.textContent = label + ": ";
|
|
2019
|
+
div.appendChild(lbl);
|
|
2020
|
+
var val = document.createElement("span");
|
|
2021
|
+
val.textContent = String(value);
|
|
2022
|
+
div.appendChild(val);
|
|
2023
|
+
dpContent.appendChild(div);
|
|
2024
|
+
}
|
|
2025
|
+
if (d.member_count != null) addMeta("Members", d.member_count);
|
|
2026
|
+
if (d.symbol_count != null) addMeta("Symbols", d.symbol_count);
|
|
2027
|
+
if (d.description) {
|
|
2028
|
+
var desc = document.createElement("div");
|
|
2029
|
+
desc.className = "dp-meta";
|
|
2030
|
+
desc.style.marginTop = "6px";
|
|
2031
|
+
desc.textContent = d.description;
|
|
2032
|
+
dpContent.appendChild(desc);
|
|
2033
|
+
}
|
|
2034
|
+
if (d.language) addMeta("Language", d.language);
|
|
2035
|
+
if (d.file_path) {
|
|
2036
|
+
var relFile = d.file_path.split("/").slice(-3).join("/");
|
|
2037
|
+
if (relFile) {
|
|
2038
|
+
var fDiv = document.createElement("div");
|
|
2039
|
+
fDiv.className = "dp-meta";
|
|
2040
|
+
fDiv.style.marginTop = "8px";
|
|
2041
|
+
fDiv.textContent = relFile + (d.line_start != null ? ":" + d.line_start : "");
|
|
2042
|
+
dpContent.appendChild(fDiv);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
if (d.params) addMeta("Params", d.params);
|
|
2046
|
+
if (d.return_type) addMeta("Returns", d.return_type);
|
|
2047
|
+
/* Show connected super-nodes */
|
|
2048
|
+
var neighbors = [];
|
|
2049
|
+
currentEdges.forEach(function(e) {
|
|
2050
|
+
var s = typeof e.source === "object" ? e.source.qualified_name : e._source;
|
|
2051
|
+
var t = typeof e.target === "object" ? e.target.qualified_name : e._target;
|
|
2052
|
+
if (s === d.qualified_name && nodeById.has(t)) neighbors.push({ node: nodeById.get(t), weight: e.weight || 1 });
|
|
2053
|
+
if (t === d.qualified_name && nodeById.has(s)) neighbors.push({ node: nodeById.get(s), weight: e.weight || 1 });
|
|
2054
|
+
});
|
|
2055
|
+
if (neighbors.length) {
|
|
2056
|
+
var section = document.createElement("div");
|
|
2057
|
+
section.className = "dp-section";
|
|
2058
|
+
var h4 = document.createElement("h4");
|
|
2059
|
+
h4.textContent = "Connected (" + neighbors.length + ")";
|
|
2060
|
+
section.appendChild(h4);
|
|
2061
|
+
var ul = document.createElement("ul");
|
|
2062
|
+
ul.className = "dp-list";
|
|
2063
|
+
neighbors.sort(function(a, b) { return b.weight - a.weight; });
|
|
2064
|
+
neighbors.slice(0, 20).forEach(function(nb) {
|
|
2065
|
+
var li = document.createElement("li");
|
|
2066
|
+
li.dataset.qn = nb.node.qualified_name;
|
|
2067
|
+
li.textContent = (nb.node.label || nb.node.name) + " (" + nb.weight + ")";
|
|
2068
|
+
li.addEventListener("click", function() { zoomToNode(nb.node.qualified_name); });
|
|
2069
|
+
ul.appendChild(li);
|
|
2070
|
+
});
|
|
2071
|
+
section.appendChild(ul);
|
|
2072
|
+
dpContent.appendChild(section);
|
|
2073
|
+
}
|
|
2074
|
+
if (d.kind === "Community" && dataMode === "community") {
|
|
2075
|
+
var drillSection = document.createElement("div");
|
|
2076
|
+
drillSection.className = "dp-section";
|
|
2077
|
+
drillSection.style.marginTop = "12px";
|
|
2078
|
+
var drillHint = document.createElement("em");
|
|
2079
|
+
drillHint.style.color = "#58a6ff";
|
|
2080
|
+
drillHint.textContent = "Double-click node to drill down";
|
|
2081
|
+
drillSection.appendChild(drillHint);
|
|
2082
|
+
dpContent.appendChild(drillSection);
|
|
2083
|
+
}
|
|
2084
|
+
detailPanel.classList.add("visible");
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
/* --- Drill-down (community mode) --- */
|
|
2088
|
+
function drillIntoCommunity(d) {
|
|
2089
|
+
var cid = String(d.community_id != null ? d.community_id : d.id);
|
|
2090
|
+
var detail = communityDetails[cid];
|
|
2091
|
+
if (!detail || !detail.nodes || detail.nodes.length === 0) return;
|
|
2092
|
+
filterInfo.textContent = "Viewing community: " + (d.name || d.label) + ". Click Back to return.";
|
|
2093
|
+
renderGraph(detail.nodes, detail.edges, true);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
/* --- Back button --- */
|
|
2097
|
+
document.getElementById("btn-back").addEventListener("click", function() {
|
|
2098
|
+
filterInfo.textContent = dataMode === "community"
|
|
2099
|
+
? "Showing communities. Double-click to drill down."
|
|
2100
|
+
: "Showing file-level aggregation.";
|
|
2101
|
+
renderGraph(graphData.nodes, graphData.edges, false);
|
|
2102
|
+
});
|
|
2103
|
+
|
|
2104
|
+
/* --- Controls --- */
|
|
2105
|
+
document.getElementById("btn-fit").addEventListener("click", fitGraph);
|
|
2106
|
+
document.getElementById("btn-labels").addEventListener("click", function() {
|
|
2107
|
+
showLabels = !showLabels;
|
|
2108
|
+
this.classList.toggle("active");
|
|
2109
|
+
this.setAttribute("aria-pressed", showLabels);
|
|
2110
|
+
updateLabelVisibility();
|
|
2111
|
+
});
|
|
2112
|
+
|
|
2113
|
+
/* --- Search --- */
|
|
2114
|
+
var searchInput = document.getElementById("search");
|
|
2115
|
+
var searchResults = document.getElementById("search-results");
|
|
2116
|
+
var searchTerm = "";
|
|
2117
|
+
searchInput.addEventListener("input", function() {
|
|
2118
|
+
searchTerm = this.value.trim().toLowerCase();
|
|
2119
|
+
applySearchFilter();
|
|
2120
|
+
showSearchResults();
|
|
2121
|
+
});
|
|
2122
|
+
searchInput.addEventListener("focus", showSearchResults);
|
|
2123
|
+
document.addEventListener("click", function(ev) {
|
|
2124
|
+
if (!searchResults.contains(ev.target) && ev.target !== searchInput) searchResults.style.display = "none";
|
|
2125
|
+
});
|
|
2126
|
+
function showSearchResults() {
|
|
2127
|
+
if (!searchTerm) { searchResults.style.display = "none"; return; }
|
|
2128
|
+
var matched = [];
|
|
2129
|
+
currentNodes.forEach(function(n) {
|
|
2130
|
+
var hay = ((n.label || "") + " " + (n.qualified_name || "") + " " + (n.name || "")).toLowerCase();
|
|
2131
|
+
if (hay.indexOf(searchTerm) !== -1) matched.push(n);
|
|
2132
|
+
});
|
|
2133
|
+
if (!matched.length) { searchResults.style.display = "none"; return; }
|
|
2134
|
+
searchResults.textContent = "";
|
|
2135
|
+
matched.slice(0, 15).forEach(function(n) {
|
|
2136
|
+
var bg = KIND_COLOR[n.kind] || "#555";
|
|
2137
|
+
var div = document.createElement("div");
|
|
2138
|
+
div.className = "sr-item";
|
|
2139
|
+
var kindSpan = document.createElement("span");
|
|
2140
|
+
kindSpan.className = "sr-kind";
|
|
2141
|
+
kindSpan.style.background = bg;
|
|
2142
|
+
kindSpan.style.color = "#0d1117";
|
|
2143
|
+
kindSpan.textContent = n.kind;
|
|
2144
|
+
div.appendChild(kindSpan);
|
|
2145
|
+
div.appendChild(document.createTextNode(" " + (n.label || n.name)));
|
|
2146
|
+
div.addEventListener("click", function() {
|
|
2147
|
+
zoomToNode(n.qualified_name);
|
|
2148
|
+
searchResults.style.display = "none";
|
|
2149
|
+
});
|
|
2150
|
+
searchResults.appendChild(div);
|
|
2151
|
+
});
|
|
2152
|
+
searchResults.style.display = "block";
|
|
2153
|
+
}
|
|
2154
|
+
function applySearchFilter() {
|
|
2155
|
+
if (!searchTerm) {
|
|
2156
|
+
nodeGroup.selectAll("g.node-g").select(".node-circle").attr("opacity", 1);
|
|
2157
|
+
if (labelSel) labelSel.attr("opacity", 1);
|
|
2158
|
+
if (linkSel) linkSel.attr("opacity", function(e) { return eStyle(e).opacity; });
|
|
2159
|
+
updateLabelVisibility();
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
var matched = new Set();
|
|
2163
|
+
currentNodes.forEach(function(n) {
|
|
2164
|
+
var hay = ((n.label || "") + " " + (n.qualified_name || "") + " " + (n.name || "")).toLowerCase();
|
|
2165
|
+
if (hay.indexOf(searchTerm) !== -1) matched.add(n.qualified_name);
|
|
2166
|
+
});
|
|
2167
|
+
nodeGroup.selectAll("g.node-g").select(".node-circle")
|
|
2168
|
+
.attr("opacity", function(d) { return matched.has(d.qualified_name) ? 1 : 0.08; });
|
|
2169
|
+
if (labelSel) labelSel
|
|
2170
|
+
.attr("opacity", function(d) { return matched.has(d.qualified_name) ? 1 : 0.05; })
|
|
2171
|
+
.attr("display", function(d) { return matched.has(d.qualified_name) ? null : "none"; });
|
|
2172
|
+
if (linkSel) linkSel.attr("opacity", function(e) {
|
|
2173
|
+
var s = typeof e.source === "object" ? e.source.qualified_name : e._source;
|
|
2174
|
+
var t = typeof e.target === "object" ? e.target.qualified_name : e._target;
|
|
2175
|
+
return (matched.has(s) || matched.has(t)) ? eStyle(e).opacity : 0.02;
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
/* --- Initial render --- */
|
|
2180
|
+
renderGraph(graphData.nodes, graphData.edges, false);
|
|
2181
|
+
</script>
|
|
2182
|
+
</body>
|
|
2183
|
+
</html>
|
|
2184
|
+
"""
|