depwire-cli 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/chunk-2XOJSBSD.js +3302 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +243 -0
- package/dist/mcpb-entry.d.ts +1 -0
- package/dist/mcpb-entry.js +70 -0
- package/dist/viz/public/arc.js +579 -0
- package/dist/viz/public/index.html +48 -0
- package/dist/viz/public/style.css +326 -0
- package/icon.png +0 -0
- package/package.json +77 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
// Arc Diagram Renderer using D3.js
|
|
2
|
+
let graphData = null;
|
|
3
|
+
let svg = null;
|
|
4
|
+
let g = null;
|
|
5
|
+
let filePositions = new Map();
|
|
6
|
+
let selectedFile = null;
|
|
7
|
+
let selectedArc = null;
|
|
8
|
+
let ws = null;
|
|
9
|
+
|
|
10
|
+
async function init() {
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch('/api/graph');
|
|
13
|
+
graphData = await response.json();
|
|
14
|
+
|
|
15
|
+
// Update header
|
|
16
|
+
document.getElementById('projectName').textContent = graphData.projectName;
|
|
17
|
+
document.getElementById('stats').innerHTML = `
|
|
18
|
+
<div class="stat-item"><span class="stat-label">Files:</span> <span class="stat-value">${graphData.stats.totalFiles}</span></div>
|
|
19
|
+
<div class="stat-item"><span class="stat-label">Symbols:</span> <span class="stat-value">${graphData.stats.totalSymbols}</span></div>
|
|
20
|
+
<div class="stat-item"><span class="stat-label">Edges:</span> <span class="stat-value">${graphData.stats.totalCrossFileEdges}</span></div>
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
// Render diagram
|
|
24
|
+
renderArcDiagram();
|
|
25
|
+
|
|
26
|
+
// Setup interactions
|
|
27
|
+
setupSearch();
|
|
28
|
+
setupExport();
|
|
29
|
+
|
|
30
|
+
// Setup WebSocket for live updates
|
|
31
|
+
setupWebSocket();
|
|
32
|
+
|
|
33
|
+
// Handle window resize
|
|
34
|
+
window.addEventListener('resize', () => {
|
|
35
|
+
renderArcDiagram();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Failed to load graph data:', error);
|
|
40
|
+
document.getElementById('detailPanel').querySelector('.detail-content').innerHTML =
|
|
41
|
+
'<p style="color: #ff4a4a;">Failed to load graph data. Please check the console.</p>';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setupWebSocket() {
|
|
46
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
47
|
+
const wsUrl = `${protocol}//${window.location.host}`;
|
|
48
|
+
|
|
49
|
+
ws = new WebSocket(wsUrl);
|
|
50
|
+
|
|
51
|
+
ws.onopen = () => {
|
|
52
|
+
console.log('WebSocket connected — live updates enabled');
|
|
53
|
+
showNotification('Live updates enabled', 'success');
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
ws.onmessage = async (event) => {
|
|
57
|
+
const message = JSON.parse(event.data);
|
|
58
|
+
|
|
59
|
+
if (message.type === 'refresh') {
|
|
60
|
+
console.log('Graph updated — refreshing visualization...');
|
|
61
|
+
showNotification('Graph updated', 'info');
|
|
62
|
+
|
|
63
|
+
// Re-fetch graph data
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch('/api/graph');
|
|
66
|
+
graphData = await response.json();
|
|
67
|
+
|
|
68
|
+
// Update header stats
|
|
69
|
+
document.getElementById('stats').innerHTML = `
|
|
70
|
+
<div class="stat-item"><span class="stat-label">Files:</span> <span class="stat-value">${graphData.stats.totalFiles}</span></div>
|
|
71
|
+
<div class="stat-item"><span class="stat-label">Symbols:</span> <span class="stat-value">${graphData.stats.totalSymbols}</span></div>
|
|
72
|
+
<div class="stat-item"><span class="stat-label">Edges:</span> <span class="stat-value">${graphData.stats.totalCrossFileEdges}</span></div>
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
// Re-render diagram
|
|
76
|
+
renderArcDiagram();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Failed to refresh graph data:', error);
|
|
79
|
+
showNotification('Failed to refresh', 'error');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
ws.onclose = () => {
|
|
85
|
+
console.log('WebSocket disconnected — attempting reconnect in 3s...');
|
|
86
|
+
showNotification('Connection lost — reconnecting...', 'warning');
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
setupWebSocket();
|
|
89
|
+
}, 3000);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
ws.onerror = (error) => {
|
|
93
|
+
console.error('WebSocket error:', error);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function showNotification(message, type = 'info') {
|
|
98
|
+
const notification = document.createElement('div');
|
|
99
|
+
notification.className = `notification notification-${type}`;
|
|
100
|
+
notification.textContent = message;
|
|
101
|
+
document.body.appendChild(notification);
|
|
102
|
+
|
|
103
|
+
// Fade in
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
notification.classList.add('show');
|
|
106
|
+
}, 10);
|
|
107
|
+
|
|
108
|
+
// Fade out and remove
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
notification.classList.remove('show');
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
notification.remove();
|
|
113
|
+
}, 300);
|
|
114
|
+
}, 3000);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderArcDiagram() {
|
|
118
|
+
const container = document.querySelector('.diagram-container');
|
|
119
|
+
const width = container.clientWidth;
|
|
120
|
+
const height = container.clientHeight;
|
|
121
|
+
|
|
122
|
+
// Reset state
|
|
123
|
+
filePositions.clear();
|
|
124
|
+
selectedFile = null;
|
|
125
|
+
selectedArc = null;
|
|
126
|
+
|
|
127
|
+
// Clear existing SVG
|
|
128
|
+
d3.select('#diagram').selectAll('*').remove();
|
|
129
|
+
|
|
130
|
+
svg = d3.select('#diagram')
|
|
131
|
+
.attr('width', width)
|
|
132
|
+
.attr('height', height);
|
|
133
|
+
|
|
134
|
+
g = svg.append('g');
|
|
135
|
+
|
|
136
|
+
// Add zoom behavior
|
|
137
|
+
const zoom = d3.zoom()
|
|
138
|
+
.scaleExtent([0.5, 4])
|
|
139
|
+
.on('zoom', (event) => {
|
|
140
|
+
g.attr('transform', event.transform);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
svg.call(zoom);
|
|
144
|
+
|
|
145
|
+
// Layout calculations
|
|
146
|
+
const margin = { top: 60, right: 40, bottom: 120, left: 40 };
|
|
147
|
+
const plotWidth = width - margin.left - margin.right;
|
|
148
|
+
const plotHeight = height - margin.top - margin.bottom;
|
|
149
|
+
const baseline = margin.top + plotHeight;
|
|
150
|
+
|
|
151
|
+
// Calculate file bar positions
|
|
152
|
+
const totalSymbols = d3.sum(graphData.files, d => d.symbolCount);
|
|
153
|
+
const minBarWidth = 4;
|
|
154
|
+
const gap = 2;
|
|
155
|
+
|
|
156
|
+
let x = margin.left;
|
|
157
|
+
filePositions.clear();
|
|
158
|
+
|
|
159
|
+
graphData.files.forEach(file => {
|
|
160
|
+
const barWidth = Math.max(minBarWidth, (file.symbolCount / totalSymbols) * plotWidth * 0.8);
|
|
161
|
+
filePositions.set(file.path, {
|
|
162
|
+
x: x + barWidth / 2,
|
|
163
|
+
width: barWidth,
|
|
164
|
+
file: file
|
|
165
|
+
});
|
|
166
|
+
x += barWidth + gap;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Get directory colors
|
|
170
|
+
const directories = [...new Set(graphData.files.map(f => f.directory))];
|
|
171
|
+
const colorScale = d3.scaleOrdinal()
|
|
172
|
+
.domain(directories)
|
|
173
|
+
.range(['#4a9eff', '#7c3aed', '#ec4899', '#f59e0b', '#10b981', '#06b6d4']);
|
|
174
|
+
|
|
175
|
+
// Calculate max distance for color scaling
|
|
176
|
+
const positions = Array.from(filePositions.values());
|
|
177
|
+
const maxDistance = d3.max(graphData.arcs, arc => {
|
|
178
|
+
const sourcePos = filePositions.get(arc.sourceFile);
|
|
179
|
+
const targetPos = filePositions.get(arc.targetFile);
|
|
180
|
+
if (!sourcePos || !targetPos) return 0;
|
|
181
|
+
return Math.abs(targetPos.x - sourcePos.x);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Draw arcs
|
|
185
|
+
const arcs = g.selectAll('.arc')
|
|
186
|
+
.data(graphData.arcs)
|
|
187
|
+
.enter()
|
|
188
|
+
.append('path')
|
|
189
|
+
.attr('class', 'arc')
|
|
190
|
+
.attr('d', d => {
|
|
191
|
+
const sourcePos = filePositions.get(d.sourceFile);
|
|
192
|
+
const targetPos = filePositions.get(d.targetFile);
|
|
193
|
+
if (!sourcePos || !targetPos) return null;
|
|
194
|
+
|
|
195
|
+
const x1 = sourcePos.x;
|
|
196
|
+
const x2 = targetPos.x;
|
|
197
|
+
const distance = Math.abs(x2 - x1);
|
|
198
|
+
const height = distance * 0.4;
|
|
199
|
+
const midX = (x1 + x2) / 2;
|
|
200
|
+
|
|
201
|
+
return `M ${x1} ${baseline} Q ${midX} ${baseline - height} ${x2} ${baseline}`;
|
|
202
|
+
})
|
|
203
|
+
.attr('stroke', d => {
|
|
204
|
+
const sourcePos = filePositions.get(d.sourceFile);
|
|
205
|
+
const targetPos = filePositions.get(d.targetFile);
|
|
206
|
+
if (!sourcePos || !targetPos) return '#4a9eff';
|
|
207
|
+
|
|
208
|
+
const distance = Math.abs(targetPos.x - sourcePos.x);
|
|
209
|
+
const t = distance / maxDistance;
|
|
210
|
+
return d3.interpolateRainbow(t);
|
|
211
|
+
})
|
|
212
|
+
.attr('stroke-width', d => Math.min(4, 1 + Math.log(d.edgeCount)))
|
|
213
|
+
.on('mouseover', handleArcHover)
|
|
214
|
+
.on('mouseout', handleArcOut)
|
|
215
|
+
.on('click', handleArcClick);
|
|
216
|
+
|
|
217
|
+
// Draw file bars
|
|
218
|
+
const bars = g.selectAll('.file-bar')
|
|
219
|
+
.data(graphData.files)
|
|
220
|
+
.enter()
|
|
221
|
+
.append('rect')
|
|
222
|
+
.attr('class', 'file-bar')
|
|
223
|
+
.attr('x', d => {
|
|
224
|
+
const pos = filePositions.get(d.path);
|
|
225
|
+
return pos.x - pos.width / 2;
|
|
226
|
+
})
|
|
227
|
+
.attr('y', baseline)
|
|
228
|
+
.attr('width', d => filePositions.get(d.path).width)
|
|
229
|
+
.attr('height', 8)
|
|
230
|
+
.attr('fill', d => colorScale(d.directory))
|
|
231
|
+
.on('mouseover', handleBarHover)
|
|
232
|
+
.on('mouseout', handleBarOut)
|
|
233
|
+
.on('click', handleBarClick);
|
|
234
|
+
|
|
235
|
+
// Draw file labels
|
|
236
|
+
const labels = g.selectAll('.file-label')
|
|
237
|
+
.data(graphData.files)
|
|
238
|
+
.enter()
|
|
239
|
+
.append('text')
|
|
240
|
+
.attr('class', 'file-label')
|
|
241
|
+
.attr('x', d => filePositions.get(d.path).x)
|
|
242
|
+
.attr('y', baseline + 20)
|
|
243
|
+
.attr('transform', d => `rotate(-45, ${filePositions.get(d.path).x}, ${baseline + 20})`)
|
|
244
|
+
.attr('text-anchor', 'end')
|
|
245
|
+
.text(d => d.path.split('/').pop());
|
|
246
|
+
|
|
247
|
+
// Reset view button
|
|
248
|
+
svg.append('text')
|
|
249
|
+
.attr('x', 10)
|
|
250
|
+
.attr('y', 20)
|
|
251
|
+
.attr('fill', '#4a9eff')
|
|
252
|
+
.attr('font-size', '12px')
|
|
253
|
+
.attr('cursor', 'pointer')
|
|
254
|
+
.text('↺ Reset View')
|
|
255
|
+
.on('click', () => {
|
|
256
|
+
svg.transition()
|
|
257
|
+
.duration(750)
|
|
258
|
+
.call(zoom.transform, d3.zoomIdentity);
|
|
259
|
+
clearSelection();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function handleArcHover(event, d) {
|
|
264
|
+
if (selectedArc) return;
|
|
265
|
+
|
|
266
|
+
// Highlight arc
|
|
267
|
+
d3.select(event.currentTarget).classed('highlighted', true);
|
|
268
|
+
|
|
269
|
+
// Dim other arcs
|
|
270
|
+
d3.selectAll('.arc').filter(arc => arc !== d).classed('dimmed', true);
|
|
271
|
+
|
|
272
|
+
// Highlight connected file bars
|
|
273
|
+
d3.selectAll('.file-bar')
|
|
274
|
+
.filter(f => f.path === d.sourceFile || f.path === d.targetFile)
|
|
275
|
+
.classed('highlighted', true);
|
|
276
|
+
|
|
277
|
+
d3.selectAll('.file-bar')
|
|
278
|
+
.filter(f => f.path !== d.sourceFile && f.path !== d.targetFile)
|
|
279
|
+
.classed('dimmed', true);
|
|
280
|
+
|
|
281
|
+
// Show tooltip
|
|
282
|
+
showTooltip(event, `
|
|
283
|
+
<div class="tooltip-line"><strong>${d.sourceFile}</strong> → <strong>${d.targetFile}</strong></div>
|
|
284
|
+
<div class="tooltip-line"><span class="tooltip-label">Edges:</span> ${d.edgeCount}</div>
|
|
285
|
+
<div class="tooltip-line"><span class="tooltip-label">Types:</span> ${d.edgeKinds.join(', ')}</div>
|
|
286
|
+
`);
|
|
287
|
+
|
|
288
|
+
// Update detail panel
|
|
289
|
+
updateDetailPanel(`
|
|
290
|
+
<p class="detail-title">Arc: ${d.sourceFile} → ${d.targetFile}</p>
|
|
291
|
+
<p class="detail-info"><span class="detail-label">Edge count:</span> ${d.edgeCount}</p>
|
|
292
|
+
<p class="detail-info"><span class="detail-label">Edge types:</span> ${d.edgeKinds.join(', ')}</p>
|
|
293
|
+
`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function handleArcOut(event, d) {
|
|
297
|
+
if (selectedArc) return;
|
|
298
|
+
|
|
299
|
+
d3.select(event.currentTarget).classed('highlighted', false);
|
|
300
|
+
d3.selectAll('.arc').classed('dimmed', false);
|
|
301
|
+
d3.selectAll('.file-bar').classed('highlighted', false).classed('dimmed', false);
|
|
302
|
+
|
|
303
|
+
hideTooltip();
|
|
304
|
+
resetDetailPanel();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function handleArcClick(event, d) {
|
|
308
|
+
event.stopPropagation();
|
|
309
|
+
|
|
310
|
+
if (selectedArc === d) {
|
|
311
|
+
// Deselect
|
|
312
|
+
selectedArc = null;
|
|
313
|
+
handleArcOut(event, d);
|
|
314
|
+
} else {
|
|
315
|
+
// Select
|
|
316
|
+
selectedArc = d;
|
|
317
|
+
selectedFile = null;
|
|
318
|
+
|
|
319
|
+
d3.selectAll('.arc').classed('highlighted', false).classed('dimmed', false);
|
|
320
|
+
d3.select(event.currentTarget).classed('highlighted', true);
|
|
321
|
+
d3.selectAll('.arc').filter(arc => arc !== d).classed('dimmed', true);
|
|
322
|
+
|
|
323
|
+
d3.selectAll('.file-bar')
|
|
324
|
+
.classed('highlighted', false)
|
|
325
|
+
.classed('dimmed', false)
|
|
326
|
+
.filter(f => f.path === d.sourceFile || f.path === d.targetFile)
|
|
327
|
+
.classed('highlighted', true);
|
|
328
|
+
|
|
329
|
+
d3.selectAll('.file-bar')
|
|
330
|
+
.filter(f => f.path !== d.sourceFile && f.path !== d.targetFile)
|
|
331
|
+
.classed('dimmed', true);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function handleBarHover(event, d) {
|
|
336
|
+
if (selectedFile) return;
|
|
337
|
+
|
|
338
|
+
// Highlight bar
|
|
339
|
+
d3.select(event.currentTarget).classed('highlighted', true);
|
|
340
|
+
|
|
341
|
+
// Highlight connected arcs
|
|
342
|
+
const connectedArcs = graphData.arcs.filter(arc =>
|
|
343
|
+
arc.sourceFile === d.path || arc.targetFile === d.path
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
d3.selectAll('.arc')
|
|
347
|
+
.classed('highlighted', arc => connectedArcs.includes(arc))
|
|
348
|
+
.classed('dimmed', arc => !connectedArcs.includes(arc));
|
|
349
|
+
|
|
350
|
+
// Dim other bars
|
|
351
|
+
d3.selectAll('.file-bar').filter(f => f !== d).classed('dimmed', true);
|
|
352
|
+
|
|
353
|
+
// Show tooltip
|
|
354
|
+
showTooltip(event, `
|
|
355
|
+
<div class="tooltip-line"><strong>${d.path}</strong></div>
|
|
356
|
+
<div class="tooltip-line"><span class="tooltip-label">Symbols:</span> ${d.symbolCount}</div>
|
|
357
|
+
<div class="tooltip-line"><span class="tooltip-label">Incoming:</span> ${d.incomingCount} connections</div>
|
|
358
|
+
<div class="tooltip-line"><span class="tooltip-label">Outgoing:</span> ${d.outgoingCount} connections</div>
|
|
359
|
+
`);
|
|
360
|
+
|
|
361
|
+
// Update detail panel
|
|
362
|
+
updateDetailPanel(`
|
|
363
|
+
<p class="detail-title">File: ${d.path}</p>
|
|
364
|
+
<p class="detail-info"><span class="detail-label">Directory:</span> ${d.directory}</p>
|
|
365
|
+
<p class="detail-info"><span class="detail-label">Symbols:</span> ${d.symbolCount}</p>
|
|
366
|
+
<p class="detail-info"><span class="detail-label">Incoming connections:</span> ${d.incomingCount}</p>
|
|
367
|
+
<p class="detail-info"><span class="detail-label">Outgoing connections:</span> ${d.outgoingCount}</p>
|
|
368
|
+
`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function handleBarOut(event, d) {
|
|
372
|
+
if (selectedFile) return;
|
|
373
|
+
|
|
374
|
+
d3.select(event.currentTarget).classed('highlighted', false);
|
|
375
|
+
d3.selectAll('.arc').classed('highlighted', false).classed('dimmed', false);
|
|
376
|
+
d3.selectAll('.file-bar').classed('dimmed', false);
|
|
377
|
+
|
|
378
|
+
hideTooltip();
|
|
379
|
+
resetDetailPanel();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function handleBarClick(event, d) {
|
|
383
|
+
event.stopPropagation();
|
|
384
|
+
|
|
385
|
+
if (selectedFile === d) {
|
|
386
|
+
// Deselect
|
|
387
|
+
selectedFile = null;
|
|
388
|
+
handleBarOut(event, d);
|
|
389
|
+
} else {
|
|
390
|
+
// Select
|
|
391
|
+
selectedFile = d;
|
|
392
|
+
selectedArc = null;
|
|
393
|
+
|
|
394
|
+
const connectedArcs = graphData.arcs.filter(arc =>
|
|
395
|
+
arc.sourceFile === d.path || arc.targetFile === d.path
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
d3.selectAll('.arc')
|
|
399
|
+
.classed('highlighted', arc => connectedArcs.includes(arc))
|
|
400
|
+
.classed('dimmed', arc => !connectedArcs.includes(arc));
|
|
401
|
+
|
|
402
|
+
d3.selectAll('.file-bar')
|
|
403
|
+
.classed('highlighted', f => f === d)
|
|
404
|
+
.classed('dimmed', f => f !== d);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function showTooltip(event, content) {
|
|
409
|
+
const tooltip = document.getElementById('tooltip');
|
|
410
|
+
tooltip.innerHTML = content;
|
|
411
|
+
tooltip.style.left = (event.pageX + 10) + 'px';
|
|
412
|
+
tooltip.style.top = (event.pageY + 10) + 'px';
|
|
413
|
+
tooltip.classList.add('show');
|
|
414
|
+
|
|
415
|
+
// Update position on mouse move
|
|
416
|
+
d3.select('body').on('mousemove.tooltip', (e) => {
|
|
417
|
+
tooltip.style.left = (e.pageX + 10) + 'px';
|
|
418
|
+
tooltip.style.top = (e.pageY + 10) + 'px';
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function hideTooltip() {
|
|
423
|
+
document.getElementById('tooltip').classList.remove('show');
|
|
424
|
+
d3.select('body').on('mousemove.tooltip', null);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function updateDetailPanel(content) {
|
|
428
|
+
document.getElementById('detailPanel').querySelector('.detail-content').innerHTML = content;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function resetDetailPanel() {
|
|
432
|
+
updateDetailPanel('<p class="detail-hint">Hover over arcs or files to see connection details</p>');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function clearSelection() {
|
|
436
|
+
selectedFile = null;
|
|
437
|
+
selectedArc = null;
|
|
438
|
+
d3.selectAll('.arc').classed('highlighted', false).classed('dimmed', false);
|
|
439
|
+
d3.selectAll('.file-bar').classed('highlighted', false).classed('dimmed', false);
|
|
440
|
+
resetDetailPanel();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function setupSearch() {
|
|
444
|
+
const searchInput = document.getElementById('searchInput');
|
|
445
|
+
|
|
446
|
+
searchInput.addEventListener('input', (e) => {
|
|
447
|
+
const query = e.target.value.toLowerCase().trim();
|
|
448
|
+
|
|
449
|
+
if (!query) {
|
|
450
|
+
clearSelection();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Find matching files
|
|
455
|
+
const matchingFiles = graphData.files.filter(f =>
|
|
456
|
+
f.path.toLowerCase().includes(query)
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
if (matchingFiles.length === 0) {
|
|
460
|
+
clearSelection();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Highlight matching files and their arcs
|
|
465
|
+
const matchingPaths = new Set(matchingFiles.map(f => f.path));
|
|
466
|
+
|
|
467
|
+
d3.selectAll('.file-bar')
|
|
468
|
+
.classed('highlighted', f => matchingPaths.has(f.path))
|
|
469
|
+
.classed('dimmed', f => !matchingPaths.has(f.path));
|
|
470
|
+
|
|
471
|
+
const connectedArcs = graphData.arcs.filter(arc =>
|
|
472
|
+
matchingPaths.has(arc.sourceFile) || matchingPaths.has(arc.targetFile)
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
d3.selectAll('.arc')
|
|
476
|
+
.classed('highlighted', arc => connectedArcs.includes(arc))
|
|
477
|
+
.classed('dimmed', arc => !connectedArcs.includes(arc));
|
|
478
|
+
|
|
479
|
+
// Update detail panel
|
|
480
|
+
updateDetailPanel(`
|
|
481
|
+
<p class="detail-title">Search Results: ${matchingFiles.length} file(s)</p>
|
|
482
|
+
${matchingFiles.slice(0, 10).map(f => `<p class="detail-info">${f.path}</p>`).join('')}
|
|
483
|
+
${matchingFiles.length > 10 ? '<p class="detail-info">...</p>' : ''}
|
|
484
|
+
`);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Clear on Escape
|
|
488
|
+
searchInput.addEventListener('keydown', (e) => {
|
|
489
|
+
if (e.key === 'Escape') {
|
|
490
|
+
searchInput.value = '';
|
|
491
|
+
clearSelection();
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function setupExport() {
|
|
497
|
+
const exportButton = document.getElementById('exportButton');
|
|
498
|
+
const exportMenu = document.getElementById('exportMenu');
|
|
499
|
+
const exportSvg = document.getElementById('exportSvg');
|
|
500
|
+
const exportPng = document.getElementById('exportPng');
|
|
501
|
+
|
|
502
|
+
exportButton.addEventListener('click', () => {
|
|
503
|
+
exportMenu.classList.toggle('show');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
document.addEventListener('click', (e) => {
|
|
507
|
+
if (!e.target.closest('.export-dropdown')) {
|
|
508
|
+
exportMenu.classList.remove('show');
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
exportSvg.addEventListener('click', () => {
|
|
513
|
+
exportToSVG();
|
|
514
|
+
exportMenu.classList.remove('show');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
exportPng.addEventListener('click', () => {
|
|
518
|
+
exportToPNG();
|
|
519
|
+
exportMenu.classList.remove('show');
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function exportToSVG() {
|
|
524
|
+
const svgElement = document.getElementById('diagram');
|
|
525
|
+
const serializer = new XMLSerializer();
|
|
526
|
+
const svgString = serializer.serializeToString(svgElement);
|
|
527
|
+
|
|
528
|
+
const blob = new Blob([svgString], { type: 'image/svg+xml' });
|
|
529
|
+
const url = URL.createObjectURL(blob);
|
|
530
|
+
|
|
531
|
+
const link = document.createElement('a');
|
|
532
|
+
link.href = url;
|
|
533
|
+
link.download = `depwire-${graphData.projectName}.svg`;
|
|
534
|
+
link.click();
|
|
535
|
+
|
|
536
|
+
URL.revokeObjectURL(url);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function exportToPNG() {
|
|
540
|
+
const svgElement = document.getElementById('diagram');
|
|
541
|
+
const svgString = new XMLSerializer().serializeToString(svgElement);
|
|
542
|
+
const canvas = document.createElement('canvas');
|
|
543
|
+
const ctx = canvas.getContext('2d');
|
|
544
|
+
|
|
545
|
+
const img = new Image();
|
|
546
|
+
const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|
547
|
+
const url = URL.createObjectURL(svgBlob);
|
|
548
|
+
|
|
549
|
+
img.onload = () => {
|
|
550
|
+
canvas.width = svgElement.clientWidth;
|
|
551
|
+
canvas.height = svgElement.clientHeight;
|
|
552
|
+
ctx.fillStyle = '#1a1a2e';
|
|
553
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
554
|
+
ctx.drawImage(img, 0, 0);
|
|
555
|
+
|
|
556
|
+
canvas.toBlob((blob) => {
|
|
557
|
+
const pngUrl = URL.createObjectURL(blob);
|
|
558
|
+
const link = document.createElement('a');
|
|
559
|
+
link.href = pngUrl;
|
|
560
|
+
link.download = `depwire-${graphData.projectName}.png`;
|
|
561
|
+
link.click();
|
|
562
|
+
|
|
563
|
+
URL.revokeObjectURL(pngUrl);
|
|
564
|
+
URL.revokeObjectURL(url);
|
|
565
|
+
});
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
img.src = url;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Clear selection on clicking background
|
|
572
|
+
d3.select('body').on('click', () => {
|
|
573
|
+
if (!event.target.closest('.arc') && !event.target.closest('.file-bar')) {
|
|
574
|
+
clearSelection();
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// Initialize on page load
|
|
579
|
+
init();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Depwire Visualization</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'%3E%3Crect width='128' height='128' rx='28' fill='%230d1b2a'/%3E%3Crect x='32' y='24' width='6' height='80' rx='3' fill='%2300d4aa' opacity='.9'/%3E%3Cpath d='M35 28Q96 28 96 64Q96 100 35 100' fill='none' stroke='%2300d4aa' stroke-width='3.5' stroke-linecap='round' opacity='.85'/%3E%3Cpath d='M35 38Q82 38 82 64Q82 90 35 90' fill='none' stroke='%2300b4d8' stroke-width='3' stroke-linecap='round' opacity='.7'/%3E%3Cpath d='M35 48Q68 48 68 64Q68 80 35 80' fill='none' stroke='%2348cae4' stroke-width='2.5' stroke-linecap='round' opacity='.6'/%3E%3Ccircle cx='35' cy='28' r='4' fill='%2300d4aa'/%3E%3Ccircle cx='35' cy='100' r='4' fill='%2300d4aa'/%3E%3Ccircle cx='96' cy='64' r='4' fill='%2348cae4'/%3E%3C/svg%3E">
|
|
8
|
+
<link rel="stylesheet" href="style.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="header">
|
|
12
|
+
<div class="header-left">
|
|
13
|
+
<h1 class="title">
|
|
14
|
+
<span class="title-icon">📊</span>
|
|
15
|
+
<span class="title-text">Depwire</span>
|
|
16
|
+
<span class="project-name" id="projectName"></span>
|
|
17
|
+
</h1>
|
|
18
|
+
<div class="stats" id="stats"></div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="header-right">
|
|
21
|
+
<input type="text" id="searchInput" placeholder="Search files..." class="search-input">
|
|
22
|
+
<div class="export-dropdown">
|
|
23
|
+
<button class="export-button" id="exportButton">Export ▼</button>
|
|
24
|
+
<div class="export-menu" id="exportMenu">
|
|
25
|
+
<button class="export-option" id="exportSvg">Download SVG</button>
|
|
26
|
+
<button class="export-option" id="exportPng">Download PNG</button>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="main-container">
|
|
33
|
+
<div class="diagram-container">
|
|
34
|
+
<svg id="diagram"></svg>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="detail-panel" id="detailPanel">
|
|
37
|
+
<div class="detail-content">
|
|
38
|
+
<p class="detail-hint">Hover over arcs or files to see connection details</p>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="tooltip" id="tooltip"></div>
|
|
44
|
+
|
|
45
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
|
46
|
+
<script src="arc.js"></script>
|
|
47
|
+
</body>
|
|
48
|
+
</html>
|