-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathviz.html
More file actions
228 lines (208 loc) · 23.7 KB
/
viz.html
File metadata and controls
228 lines (208 loc) · 23.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Code2Schema — CQRS Visualizer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',system-ui,sans-serif;background:#0f172a;color:#e2e8f0;height:100vh;display:flex;flex-direction:column}
header{padding:12px 20px;background:#1e293b;border-bottom:1px solid #334155;display:flex;align-items:center;gap:16px;flex-shrink:0}
header h1{font-size:18px;font-weight:700;color:#f8fafc}
header h1 span{color:#818cf8}
.stats{display:flex;gap:10px;margin-left:auto;flex-wrap:wrap}
.stat{background:#0f172a;border:1px solid #334155;border-radius:8px;padding:4px 12px;font-size:12px;display:flex;gap:6px;align-items:center}
.stat b{color:#f8fafc}
.main{display:flex;flex:1;overflow:hidden}
#graph{flex:1;overflow:hidden;cursor:grab}
#graph:active{cursor:grabbing}
#sidebar{width:280px;background:#1e293b;border-left:1px solid #334155;overflow-y:auto;flex-shrink:0;padding:16px;display:flex;flex-direction:column;gap:12px}
.legend h3,.panel h3{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#94a3b8;margin-bottom:8px}
.legend-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;cursor:pointer;padding:4px 6px;border-radius:6px;transition:background .15s}
.legend-row:hover{background:#0f172a}
.legend-row input{cursor:pointer}
.dot{width:14px;height:14px;border-radius:50%;flex-shrink:0}
.legend-row span{font-size:13px}
.panel{background:#0f172a;border-radius:8px;padding:12px}
.panel p{font-size:12px;color:#94a3b8;margin-bottom:8px}
#detail{min-height:120px}
#detail .fname{font-size:15px;font-weight:600;color:#f8fafc;margin-bottom:4px}
#detail .fmodule{font-size:11px;color:#64748b;margin-bottom:10px;word-break:break-all}
#detail .badge{display:inline-block;border-radius:9999px;font-size:11px;padding:2px 10px;font-weight:600;margin-bottom:8px}
.tag{display:inline-block;background:#1e293b;border:1px solid #334155;border-radius:4px;font-size:10px;padding:1px 6px;margin:2px}
.rule-item{font-size:11px;color:#fbbf24;margin:2px 0}
#search{width:100%;background:#0f172a;border:1px solid #334155;border-radius:6px;padding:6px 10px;color:#e2e8f0;font-size:13px;outline:none}
#search:focus{border-color:#818cf8}
#search::placeholder{color:#475569}
.node circle{transition:r .2s,opacity .15s}
.node text{pointer-events:none;user-select:none}
.link{stroke:#334155;stroke-opacity:.7}
.node.dimmed circle{opacity:.2}
.node.dimmed text{opacity:.2}
.link.dimmed{stroke-opacity:.08}
</style>
</head>
<body>
<header>
<h1>Code2<span>Schema</span> — CQRS Visualizer</h1>
<div class="stats" id="stats"></div>
</header>
<div class="main">
<svg id="graph"></svg>
<div id="sidebar">
<div>
<input id="search" type="text" placeholder="🔍 Szukaj funkcji...">
</div>
<div class="legend">
<h3>Role CQRS</h3>
<label class="legend-row"><input type="checkbox" checked data-role="query"><div class="dot" style="background:#4ade80"></div><span>🔍 Query</span></label>
<label class="legend-row"><input type="checkbox" checked data-role="command"><div class="dot" style="background:#fb923c"></div><span>✏️ Command</span></label>
<label class="legend-row"><input type="checkbox" checked data-role="orchestrator"><div class="dot" style="background:#a78bfa"></div><span>🔀 Orchestrator</span></label>
<label class="legend-row"><input type="checkbox" checked data-role="unknown"><div class="dot" style="background:#94a3b8"></div><span>❓ Unknown</span></label>
</div>
<div class="panel">
<h3>Szczegóły węzła</h3>
<div id="detail"><p>Kliknij węzeł aby zobaczyć szczegóły</p></div>
</div>
<div class="panel">
<h3>Reguły jakości</h3>
<div id="rules-panel"><p>Brak naruszeń ✅</p></div>
</div>
</div>
</div>
<script>
const DATA = {"nodes": [{"id": 0, "name": "main", "module": "code2schema.cli", "role": "orchestrator", "color": "#a78bfa", "emoji": "\ud83d\udd00", "fan_out": 21, "lines": 84, "side_effects": ["none"], "is_async": false}, {"id": 1, "name": "sample_file", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 1, "lines": 4, "side_effects": ["none"], "is_async": false}, {"id": 2, "name": "sample_module", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 1, "lines": 2, "side_effects": ["none"], "is_async": false}, {"id": 3, "name": "test_extract_module_returns_module", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 1, "lines": 3, "side_effects": ["none"], "is_async": false}, {"id": 4, "name": "test_function_names", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 0, "lines": 5, "side_effects": ["none"], "is_async": false}, {"id": 5, "name": "test_side_effects_detected", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 1, "lines": 3, "side_effects": ["none"], "is_async": false}, {"id": 6, "name": "test_query_no_side_effects", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 1, "lines": 4, "side_effects": ["none"], "is_async": false}, {"id": 7, "name": "test_role_query", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 3, "side_effects": ["none"], "is_async": false}, {"id": 8, "name": "test_role_command", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 3, "side_effects": ["none"], "is_async": false}, {"id": 9, "name": "test_role_orchestrator", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 5, "side_effects": ["none"], "is_async": false}, {"id": 10, "name": "test_analyze_schema", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 3, "lines": 5, "side_effects": ["none"], "is_async": false}, {"id": 11, "name": "test_analyze_generates_rules", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 5, "side_effects": ["none"], "is_async": false}, {"id": 12, "name": "test_call_graph_has_edges", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 3, "lines": 5, "side_effects": ["none"], "is_async": false}, {"id": 13, "name": "test_to_json", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 6, "side_effects": ["none"], "is_async": false}, {"id": 14, "name": "test_to_proto", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 5, "side_effects": ["none"], "is_async": false}, {"id": 15, "name": "test_to_markdown", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 5, "side_effects": ["none"], "is_async": false}, {"id": 16, "name": "test_empty_project", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 3, "side_effects": ["none"], "is_async": false}, {"id": 17, "name": "test_invalid_syntax_file", "module": "tests.test_code2schema", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 5, "side_effects": ["none"], "is_async": false}, {"id": 18, "name": "_infer_role", "module": "code2schema.analyzer.cqrs", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 0, "lines": 15, "side_effects": ["none"], "is_async": false}, {"id": 19, "name": "build_call_graph", "module": "code2schema.analyzer.cqrs", "role": "orchestrator", "color": "#a78bfa", "emoji": "\ud83d\udd00", "fan_out": 5, "lines": 17, "side_effects": ["none"], "is_async": false}, {"id": 20, "name": "detect_cycles", "module": "code2schema.analyzer.cqrs", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 3, "side_effects": ["none"], "is_async": false}, {"id": 21, "name": "centrality", "module": "code2schema.analyzer.cqrs", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 3, "side_effects": ["none"], "is_async": false}, {"id": 22, "name": "build_workflows", "module": "code2schema.analyzer.cqrs", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 3, "lines": 16, "side_effects": ["none"], "is_async": false}, {"id": 23, "name": "generate_rules", "module": "code2schema.analyzer.cqrs", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 33, "side_effects": ["none"], "is_async": false}, {"id": 24, "name": "analyze", "module": "code2schema.analyzer.cqrs", "role": "orchestrator", "color": "#a78bfa", "emoji": "\ud83d\udd00", "fan_out": 8, "lines": 28, "side_effects": ["none"], "is_async": false}, {"id": 25, "name": "_build_graph_data", "module": "code2schema.codegen.visualizer", "role": "orchestrator", "color": "#a78bfa", "emoji": "\ud83d\udd00", "fan_out": 9, "lines": 47, "side_effects": ["network"], "is_async": false}, {"id": 26, "name": "to_html", "module": "code2schema.codegen.visualizer", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 3, "lines": 4, "side_effects": ["none"], "is_async": false}, {"id": 27, "name": "write_html", "module": "code2schema.codegen.visualizer", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 2, "side_effects": ["none"], "is_async": false}, {"id": 28, "name": "to_json", "module": "code2schema.codegen.__init__", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 3, "side_effects": ["none"], "is_async": false}, {"id": 29, "name": "write_json", "module": "code2schema.codegen.__init__", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 2, "side_effects": ["none"], "is_async": false}, {"id": 30, "name": "to_proto", "module": "code2schema.codegen.__init__", "role": "orchestrator", "color": "#a78bfa", "emoji": "\ud83d\udd00", "fan_out": 8, "lines": 28, "side_effects": ["none"], "is_async": false}, {"id": 31, "name": "write_proto", "module": "code2schema.codegen.__init__", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 2, "side_effects": ["none"], "is_async": false}, {"id": 32, "name": "to_markdown", "module": "code2schema.codegen.__init__", "role": "orchestrator", "color": "#a78bfa", "emoji": "\ud83d\udd00", "fan_out": 7, "lines": 40, "side_effects": ["none"], "is_async": false}, {"id": 33, "name": "write_markdown", "module": "code2schema.codegen.__init__", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 2, "side_effects": ["none"], "is_async": false}, {"id": 34, "name": "_safe_proto_name", "module": "code2schema.codegen.__init__", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 3, "lines": 3, "side_effects": ["none"], "is_async": false}, {"id": 35, "name": "__init__", "module": "code2schema.core.extractor", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 1, "lines": 4, "side_effects": ["none"], "is_async": false}, {"id": 36, "name": "visit_Import", "module": "code2schema.core.extractor", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 3, "lines": 4, "side_effects": ["none"], "is_async": false}, {"id": 37, "name": "visit_ImportFrom", "module": "code2schema.core.extractor", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 3, "lines": 4, "side_effects": ["none"], "is_async": false}, {"id": 38, "name": "visit_FunctionDef", "module": "code2schema.core.extractor", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 3, "side_effects": ["none"], "is_async": false}, {"id": 39, "name": "visit_AsyncFunctionDef", "module": "code2schema.core.extractor", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 3, "side_effects": ["none"], "is_async": false}, {"id": 40, "name": "_process_func", "module": "code2schema.core.extractor", "role": "orchestrator", "color": "#a78bfa", "emoji": "\ud83d\udd00", "fan_out": 10, "lines": 16, "side_effects": ["none"], "is_async": false}, {"id": 41, "name": "_collect_calls", "module": "code2schema.core.extractor", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 4, "lines": 9, "side_effects": ["none"], "is_async": false}, {"id": 42, "name": "_resolve_call_name", "module": "code2schema.core.extractor", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 1, "lines": 6, "side_effects": ["none"], "is_async": false}, {"id": 43, "name": "_detect_side_effects", "module": "code2schema.core.extractor", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 2, "lines": 18, "side_effects": ["none"], "is_async": false}, {"id": 44, "name": "extract_module", "module": "code2schema.core.extractor", "role": "orchestrator", "color": "#a78bfa", "emoji": "\ud83d\udd00", "fan_out": 13, "lines": 25, "side_effects": ["none"], "is_async": false}, {"id": 45, "name": "extract_project", "module": "code2schema.core.extractor", "role": "orchestrator", "color": "#a78bfa", "emoji": "\ud83d\udd00", "fan_out": 5, "lines": 13, "side_effects": ["none"], "is_async": false}, {"id": 46, "name": "_path_to_module", "module": "code2schema.core.extractor", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 3, "lines": 4, "side_effects": ["none"], "is_async": false}, {"id": 47, "name": "qualified_name", "module": "code2schema.core.models", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 0, "lines": 2, "side_effects": ["none"], "is_async": false}, {"id": 48, "name": "all_functions", "module": "code2schema.core.models", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 0, "lines": 2, "side_effects": ["none"], "is_async": false}, {"id": 49, "name": "orchestrators", "module": "code2schema.core.models", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 1, "lines": 2, "side_effects": ["none"], "is_async": false}, {"id": 50, "name": "commands", "module": "code2schema.core.models", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 1, "lines": 2, "side_effects": ["none"], "is_async": false}, {"id": 51, "name": "queries", "module": "code2schema.core.models", "role": "query", "color": "#4ade80", "emoji": "\ud83d\udd0d", "fan_out": 1, "lines": 2, "side_effects": ["none"], "is_async": false}], "links": [{"source": 0, "target": 45}, {"source": 0, "target": 24}, {"source": 0, "target": 29}, {"source": 0, "target": 19}, {"source": 0, "target": 20}, {"source": 0, "target": 31}, {"source": 0, "target": 33}, {"source": 0, "target": 27}, {"source": 0, "target": 48}, {"source": 0, "target": 50}, {"source": 0, "target": 51}, {"source": 0, "target": 49}, {"source": 2, "target": 44}, {"source": 7, "target": 18}, {"source": 8, "target": 18}, {"source": 9, "target": 18}, {"source": 10, "target": 24}, {"source": 10, "target": 48}, {"source": 11, "target": 24}, {"source": 12, "target": 24}, {"source": 12, "target": 19}, {"source": 13, "target": 24}, {"source": 13, "target": 28}, {"source": 14, "target": 24}, {"source": 14, "target": 30}, {"source": 15, "target": 24}, {"source": 15, "target": 32}, {"source": 16, "target": 24}, {"source": 16, "target": 48}, {"source": 17, "target": 44}, {"source": 24, "target": 19}, {"source": 24, "target": 22}, {"source": 24, "target": 23}, {"source": 24, "target": 18}, {"source": 25, "target": 48}, {"source": 25, "target": 50}, {"source": 25, "target": 51}, {"source": 25, "target": 49}, {"source": 26, "target": 25}, {"source": 27, "target": 26}, {"source": 29, "target": 28}, {"source": 30, "target": 48}, {"source": 30, "target": 34}, {"source": 30, "target": 50}, {"source": 30, "target": 51}, {"source": 31, "target": 30}, {"source": 32, "target": 48}, {"source": 32, "target": 50}, {"source": 32, "target": 51}, {"source": 32, "target": 49}, {"source": 33, "target": 32}, {"source": 38, "target": 40}, {"source": 39, "target": 40}, {"source": 40, "target": 41}, {"source": 40, "target": 43}, {"source": 41, "target": 42}, {"source": 44, "target": 46}, {"source": 45, "target": 44}, {"source": 49, "target": 48}, {"source": 50, "target": 48}, {"source": 51, "target": 48}], "rules": {"main": ["HIGH_FAN_OUT"], "_process_func": ["HIGH_FAN_OUT"], "extract_module": ["HIGH_FAN_OUT"]}, "stats": {"modules": 10, "functions": 52, "commands": 0, "queries": 43, "orchestrators": 9, "workflows": 9, "rules": 3}};
// ── Stats bar ─────────────────────────────────────────────────────────────────
const s = DATA.stats;
document.getElementById('stats').innerHTML = [
['📦 Modules', s.modules],
['⚡ Functions', s.functions],
['🔍 Queries', s.queries],
['✏️ Commands', s.commands],
['🔀 Orchestrators', s.orchestrators],
['🔁 Workflows', s.workflows],
['⚠️ Rules', s.rules],
].map(([l,v])=>`<div class="stat">${l} <b>${v}</b></div>`).join('');
// ── Rules panel ───────────────────────────────────────────────────────────────
const rp = document.getElementById('rules-panel');
const rEntries = Object.entries(DATA.rules);
if(rEntries.length){
rp.innerHTML = rEntries.map(([fn,ids])=>
`<div class="rule-item">⚠️ <b>${fn}</b>: ${ids.join(', ')}</div>`
).join('');
}
// ── D3 Force Graph ────────────────────────────────────────────────────────────
const svg = d3.select('#graph');
const container = document.getElementById('graph');
let W = container.clientWidth, H = container.clientHeight;
svg.attr('width', W).attr('height', H);
const g = svg.append('g');
// Zoom
svg.call(d3.zoom().scaleExtent([0.1, 4]).on('zoom', e => g.attr('transform', e.transform)));
// Arrow marker
svg.append('defs').append('marker')
.attr('id','arrow').attr('viewBox','0 -4 8 8').attr('refX',16).attr('refY',0)
.attr('markerWidth',6).attr('markerHeight',6).attr('orient','auto')
.append('path').attr('d','M0,-4L8,0L0,4').attr('fill','#475569');
const simulation = d3.forceSimulation(DATA.nodes)
.force('link', d3.forceLink(DATA.links).id(d=>d.id).distance(80).strength(0.4))
.force('charge', d3.forceManyBody().strength(-220))
.force('center', d3.forceCenter(W/2, H/2))
.force('collision', d3.forceCollide(d => nodeRadius(d) + 6));
function nodeRadius(d){
if(d.role==='orchestrator') return 16 + Math.min(d.fan_out, 20);
if(d.role==='command') return 10;
return 8;
}
const link = g.append('g').selectAll('line')
.data(DATA.links).join('line')
.attr('class','link')
.attr('stroke-width', 1.2)
.attr('marker-end','url(#arrow)');
const node = g.append('g').selectAll('.node')
.data(DATA.nodes).join('g')
.attr('class','node')
.call(d3.drag()
.on('start',(e,d)=>{ if(!e.active) simulation.alphaTarget(0.3).restart(); d.fx=d.x; d.fy=d.y; })
.on('drag', (e,d)=>{ d.fx=e.x; d.fy=e.y; })
.on('end', (e,d)=>{ if(!e.active) simulation.alphaTarget(0); d.fx=null; d.fy=null; })
)
.on('click', onNodeClick)
.on('mouseover', onNodeHover)
.on('mouseout', onNodeOut);
node.append('circle')
.attr('r', nodeRadius)
.attr('fill', d=>d.color)
.attr('stroke', '#0f172a')
.attr('stroke-width', 2);
node.append('text')
.attr('dy', d => nodeRadius(d) + 12)
.attr('text-anchor','middle')
.attr('font-size', 10)
.attr('fill','#94a3b8')
.text(d => d.name.length > 18 ? d.name.slice(0,17)+'…' : d.name);
simulation.on('tick', ()=>{
link
.attr('x1',d=>d.source.x).attr('y1',d=>d.source.y)
.attr('x2',d=>d.target.x).attr('y2',d=>d.target.y);
node.attr('transform',d=>`translate(${d.x},${d.y})`);
});
// ── Hover ─────────────────────────────────────────────────────────────────────
function onNodeHover(e, d){
const connected = new Set([d.id]);
DATA.links.forEach(l=>{
if(l.source.id===d.id||l.target.id===d.id){
connected.add(l.source.id); connected.add(l.target.id);
}
});
node.classed('dimmed', n => !connected.has(n.id));
link.classed('dimmed', l => l.source.id!==d.id && l.target.id!==d.id);
}
function onNodeOut(){
node.classed('dimmed', false);
link.classed('dimmed', false);
}
// ── Click detail ──────────────────────────────────────────────────────────────
function onNodeClick(e, d){
const roleColors={'query':'#4ade80','command':'#fb923c','orchestrator':'#a78bfa','unknown':'#94a3b8'};
const rules = DATA.rules[d.name] || [];
const sideEff = d.side_effects.filter(s=>s!=='none');
document.getElementById('detail').innerHTML = `
<div class="fname">${d.emoji} ${d.name}</div>
<div class="fmodule">${d.module}</div>
<span class="badge" style="background:${roleColors[d.role]}20;color:${roleColors[d.role]};border:1px solid ${roleColors[d.role]}40">${d.role.toUpperCase()}</span>
${d.is_async ? '<span class="badge" style="background:#0ea5e920;color:#38bdf8;border:1px solid #0ea5e940">async</span>' : ''}
<div style="margin-top:8px;font-size:12px;color:#64748b">
<div>Fan-out: <b style="color:#e2e8f0">${d.fan_out}</b></div>
<div>Lines: <b style="color:#e2e8f0">${d.lines}</b></div>
${sideEff.length ? `<div style="margin-top:4px">Side effects:<br>${sideEff.map(s=>`<span class="tag">⚡${s}</span>`).join('')}</div>` : ''}
${rules.length ? `<div style="margin-top:6px;color:#fbbf24">${rules.map(r=>`⚠️ ${r}`).join('<br>')}</div>` : ''}
</div>
`;
}
// ── Search ────────────────────────────────────────────────────────────────────
const hidden = new Set();
document.getElementById('search').addEventListener('input', e=>{
const q = e.target.value.trim().toLowerCase();
if(!q){ node.classed('dimmed',false); link.classed('dimmed',false); return; }
node.classed('dimmed', d => !d.name.toLowerCase().includes(q));
link.classed('dimmed', l => !l.source.name.toLowerCase().includes(q) && !l.target.name.toLowerCase().includes(q));
});
// ── Legend filters ────────────────────────────────────────────────────────────
document.querySelectorAll('[data-role]').forEach(cb=>{
cb.addEventListener('change', ()=>{
const active = new Set([...document.querySelectorAll('[data-role]:checked')].map(c=>c.dataset.role));
node.style('display', d => active.has(d.role) ? null : 'none');
link.style('display', l => active.has(l.source.role) && active.has(l.target.role) ? null : 'none');
});
});
// ── Resize ────────────────────────────────────────────────────────────────────
window.addEventListener('resize', ()=>{
W = container.clientWidth; H = container.clientHeight;
svg.attr('width',W).attr('height',H);
simulation.force('center', d3.forceCenter(W/2,H/2)).alpha(0.3).restart();
});
</script>
</body>
</html>