EchaRz commited on
Commit
cac5404
·
verified ·
1 Parent(s): b588fff

Change API_BASE

Browse files
Files changed (1) hide show
  1. explorepage.html +773 -773
explorepage.html CHANGED
@@ -1,774 +1,774 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <link href="https://fonts.googleapis.com/css2?family=Mandali&display=swap" rel="stylesheet">
5
- <link href="https://fonts.googleapis.com/css2?family=Varta:wght@400;500;600;700&display=swap" rel="stylesheet">
6
- <meta charset="UTF-8">
7
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
- <title>Knowledge Graph Explorer</title>
9
- <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
10
- <style>
11
- * {
12
- margin: 0;
13
- padding: 0;
14
- box-sizing: border-box;
15
- }
16
-
17
- body {
18
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
19
- background: linear-gradient(135deg, #4a5568 0%, #2d3748 50%, #1a202c 100%);
20
- color: white;
21
- height: 100vh;
22
- overflow: hidden;
23
- }
24
-
25
- .container {
26
- display: flex;
27
- height: 100vh;
28
- }
29
-
30
- /* Sidebar Styles */
31
- .sidebar {
32
- width: 400px;
33
- background: rgba(0 0 0 0.25);
34
- display: flex;
35
- flex-direction: column;
36
- transition: margin-left 0.3s ease;
37
- position: relative;
38
- z-index: 100;
39
- }
40
-
41
- .sidebar.collapsed {
42
- margin-left: -400px;
43
- }
44
-
45
- /* Header */
46
- .sidebar-header {
47
- padding: 1.5rem;
48
- }
49
-
50
- .header-controls {
51
- display: flex;
52
- justify-content: space-between;
53
- align-items: center;
54
- margin-bottom: 1rem;
55
- }
56
-
57
- /* Menu Toggle - Always Visible */
58
- .menu-toggle {
59
- position: fixed;
60
- top: 1.5rem;
61
- left: 0.7rem;
62
- z-index: 300;
63
- background: none;
64
- border: none;
65
- color: white;
66
- font-size: 1.5rem;
67
- cursor: pointer;
68
- padding: 0.75rem;
69
- border-radius: 8px;
70
- backdrop-filter: blur(10px);
71
- }
72
-
73
- .menu-toggle:hover {
74
- background: rgba(0, 0, 0, 0.8);
75
- }
76
-
77
- .home-btn {
78
- position: fixed;
79
- background: none;
80
- border: none;
81
- color: white;
82
- font-size: 1.5rem;
83
- cursor: pointer;
84
- padding: 0.5rem;
85
- border-radius: 4px;
86
- transition: background-color 0.3s ease;
87
- }
88
-
89
- .home-btn:hover {
90
- background-color: rgba(0, 0, 0, 0.8);
91
- }
92
-
93
- .sidebar-title {
94
- font-size: 2.5rem;
95
- font-weight: 700;
96
- color: #F8F3E7;
97
- line-height: 1.2;
98
- margin-top : 50px;
99
- }
100
-
101
- /* Search Section */
102
- .search-section {
103
- padding: 0 1.5rem 1.5rem;
104
- }
105
-
106
- .search-input {
107
- width: 100%;
108
- padding: 0.75rem 1rem;
109
- background: rgb(248 243 231);
110
- border-width: 2px;
111
- border-style: solid;
112
- border-color: #F3E7DD;
113
- border-radius: 15px;
114
- font-size: 0.9rem;
115
- color: #797979;
116
- margin-bottom: 1rem;
117
- }
118
-
119
- .search-input::placeholder {
120
- color: #a0aec0;
121
- }
122
-
123
- .search-input:focus {
124
- outline: none;
125
- background: rgba(255, 255, 255, 1);
126
- }
127
-
128
- .reset-btn {
129
- display: block; /* make it a block so margin works */
130
- margin: 0 auto; /* this centers it horizontally */
131
- background: rgb(110 131 131);
132
- border: none;
133
- color: white;
134
- padding: 0.75rem 1.5rem;
135
- border-radius: 20px;
136
- font-size: 0.9rem;
137
- font-weight: 500;
138
- cursor: pointer;
139
- max-width: 150px;
140
- width: 100%;
141
- box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2);
142
- transition: all 0.3s ease;
143
- }
144
-
145
- .reset-btn:hover {
146
- background: rgba(74, 85, 104, 1);
147
- }
148
-
149
- /* Instructions Panel */
150
- .instructions-panel {
151
- margin: 1rem;
152
- background: rgb(248 243 231);
153
- border-radius: 15px;
154
- padding: 1.5rem;
155
- color: #4a5568;
156
- flex: 1;
157
- margin-bottom: 7rem;
158
- box-shadow: inset 0 4px 4px rgba(0,0,0,0.25);
159
- }
160
-
161
- .instructions-title {
162
- font-size: 1.5rem;
163
- font-weight: 800;
164
- font-family: 'Varta', sans-serif;
165
- color: #485656;
166
- margin-bottom: 1rem;
167
- text-align: center;
168
- }
169
-
170
- .instruction-item {
171
- margin-bottom: 1rem;
172
- font-family: 'Varta', sans-serif;
173
- font-size: 1rem;
174
- color: #485656;
175
- line-height: 1.5;
176
- font-weight: 300;
177
- }
178
-
179
- .instruction-item:last-child {
180
- margin-bottom: 0;
181
- }
182
-
183
- .instruction-action {
184
- font-weight: 700;
185
- color: #485656;
186
- }
187
-
188
- /* Main Graph Area */
189
- .main-content {
190
- flex: 1;
191
- position: relative;
192
- background: rgb(77 83 109);
193
- }
194
-
195
- /* Home Button in Top-Right Corner */
196
- .main-home-btn {
197
- position: absolute;
198
- top: 1.5rem;
199
- right: 1.5rem;
200
- z-index: 200;
201
- background: rgba(0, 0, 0, 0.6);
202
- border: none;
203
- color: white;
204
- font-size: 1.5rem;
205
- cursor: pointer;
206
- padding: 0.75rem;
207
- border-radius: 8px;
208
- transition: background-color 0.3s ease;
209
- backdrop-filter: blur(10px);
210
- }
211
-
212
- .main-home-btn:hover {
213
- background: rgba(0, 0, 0, 0.8);
214
- }
215
-
216
- /* Remove floating home - not needed anymore */
217
- .floating-home {
218
- display: none;
219
- }
220
-
221
- /* Graph Styles */
222
- #graph {
223
- width: 100%;
224
- height: 100%;
225
- }
226
-
227
- .node {
228
- cursor: pointer;
229
- transition: all 0.3s ease;
230
- filter: drop-shadow(0 0 6px rgba(76, 175, 80, 0.3));
231
- }
232
-
233
- .node:hover {
234
- stroke-width: 3px;
235
- filter: drop-shadow(0 0 12px rgba(76, 175, 80, 0.6));
236
- }
237
-
238
- .node.highlighted {
239
- stroke: #4CAF50 !important;
240
- stroke-width: 3px !important;
241
- filter: drop-shadow(0 0 15px rgba(76, 175, 80, 0.8));
242
- }
243
-
244
- .node.selected {
245
- stroke: #FFD700 !important;
246
- stroke-width: 4px !important;
247
- filter: drop-shadow(0 0 20px rgba(255, 215, 0, 0.8));
248
- }
249
-
250
- .node.dimmed {
251
- opacity: 0.2;
252
- filter: none;
253
- }
254
-
255
- .link {
256
- stroke: rgba(255, 255, 255, 0.4);
257
- stroke-width: 2px;
258
- cursor: pointer;
259
- transition: all 0.3s ease;
260
- }
261
-
262
- .link:hover {
263
- stroke: #4CAF50;
264
- stroke-width: 3px;
265
- filter: drop-shadow(0 0 6px rgba(76, 175, 80, 0.5));
266
- }
267
-
268
- .link.highlighted {
269
- stroke: #4CAF50 !important;
270
- stroke-width: 3px !important;
271
- filter: drop-shadow(0 0 8px rgba(76, 175, 80, 0.6));
272
- }
273
-
274
- .link.dimmed {
275
- opacity: 0.1;
276
- }
277
-
278
- .node-label {
279
- font-size: 11px;
280
- font-weight: 600;
281
- fill: white;
282
- text-anchor: middle;
283
- pointer-events: none;
284
- text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
285
- transition: all 0.3s ease;
286
- }
287
-
288
- .node-label.dimmed {
289
- opacity: 0.2;
290
- }
291
-
292
- .node-label.highlighted {
293
- fill: #4CAF50;
294
- font-size: 13px;
295
- text-shadow: 0 0 8px rgba(76, 175, 80, 0.8);
296
- }
297
-
298
- .tooltip {
299
- position: absolute;
300
- text-align: left;
301
- padding: 1rem;
302
- font-size: 0.9rem;
303
- background: rgba(0, 0, 0, 0.9);
304
- color: white;
305
- border-radius: 8px;
306
- pointer-events: none;
307
- opacity: 0;
308
- transition: opacity 0.3s;
309
- max-width: 300px;
310
- line-height: 1.5;
311
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
312
- z-index: 1000;
313
- }
314
-
315
- .tooltip h4 {
316
- margin: 0 0 0.5rem 0;
317
- color: #4CAF50;
318
- font-weight: 700;
319
- }
320
-
321
- .loading {
322
- position: absolute;
323
- top: 50%;
324
- left: 50%;
325
- transform: translate(-50%, -50%);
326
- font-size: 1.1rem;
327
- color: white;
328
- text-align: center;
329
- }
330
-
331
- .loading-spinner {
332
- border: 3px solid rgba(255, 255, 255, 0.3);
333
- border-radius: 50%;
334
- border-top: 3px solid white;
335
- width: 40px;
336
- height: 40px;
337
- animation: spin 1s linear infinite;
338
- margin: 0 auto 1rem;
339
- }
340
-
341
- @keyframes spin {
342
- 0% { transform: rotate(0deg); }
343
- 100% { transform: rotate(360deg); }
344
- }
345
-
346
- .error {
347
- color: #ff6b6b;
348
- background: rgba(255, 107, 107, 0.1);
349
- padding: 1rem;
350
- border-radius: 8px;
351
- margin: 1rem;
352
- border: 1px solid rgba(255, 107, 107, 0.3);
353
- }
354
-
355
- /* Responsive Design */
356
- @media (max-width: 768px) {
357
- .sidebar {
358
- width: 100%;
359
- position: absolute;
360
- height: 100%;
361
- z-index: 1000;
362
- }
363
-
364
- .sidebar.collapsed {
365
- margin-left: -100%;
366
- }
367
- }
368
- </style>
369
- </head>
370
- <body>
371
- <div class="container">
372
- <!-- Fixed Menu Toggle Button -->
373
- <button class="menu-toggle" id="menuToggle">☰</button>
374
-
375
- <!-- Sidebar -->
376
- <div class="sidebar" id="sidebar">
377
- <div class="sidebar-header">
378
- <h1 class="sidebar-title">KNOWLEDGE<br>GRAPH</h1>
379
- </div>
380
-
381
- <div class="search-section">
382
- <input
383
- type="text"
384
- class="search-input"
385
- id="searchInput"
386
- placeholder="Search nodes and relations..."
387
- >
388
- <button class="reset-btn" id="resetBtn">Reset Highlight</button>
389
- </div>
390
-
391
- <div class="instructions-panel">
392
- <h3 class="instructions-title">HOW TO USE</h3>
393
- <div class="instruction-item">
394
- <span class="instruction-action">Click a node</span> to highlight its connections
395
- </div>
396
- <div class="instruction-item">
397
- <span class="instruction-action">Hover over nodes and edges</span> for details
398
- </div>
399
- <div class="instruction-item">
400
- <span class="instruction-action">Drag nodes</span> to reposition them
401
- </div>
402
- <div class="instruction-item">
403
- <span class="instruction-action">Zoom and pan</span> to explore the graph
404
- </div>
405
- <div class="instruction-item">
406
- <span class="instruction-action">Search</span> to filter nodes and relations
407
- </div>
408
- </div>
409
- </div>
410
-
411
- <!-- Main Graph Area -->
412
- <div class="main-content">
413
- <!-- Home Button in Top-Right -->
414
- <button class="main-home-btn" id="mainHomeBtn">
415
- <img src="/static/Home.png" alt="Home" style="width: 20px; height: 20px;">
416
- </button>
417
- <div id="loading" class="loading">
418
- <div class="loading-spinner"></div>
419
- Loading knowledge graph...
420
- </div>
421
- <svg id="graph"></svg>
422
- </div>
423
- </div>
424
-
425
- <div class="tooltip" id="tooltip"></div>
426
-
427
- <script>
428
- // Configuration
429
- const API_BASE = 'http://0.0.0.0:7860/api';
430
-
431
- // Global variables
432
- let graphData = { nodes: [], edges: [] };
433
- let simulation;
434
- let svg, g;
435
- let currentSearch = '';
436
- let selectedNode = null;
437
- let highlightedElements = { nodes: new Set(), edges: new Set() };
438
- let sidebarCollapsed = false;
439
-
440
- // Initialize the application
441
- async function init() {
442
- setupEventListeners();
443
- setupVisualizationSVG();
444
- await loadGraphData();
445
- hideLoading();
446
- }
447
-
448
- function setupEventListeners() {
449
- // Sidebar toggle
450
- document.getElementById('menuToggle').addEventListener('click', toggleSidebar);
451
- document.getElementById('mainHomeBtn').addEventListener('click', goHome);
452
-
453
- // Search and reset
454
- document.getElementById('searchInput').addEventListener('input', debounce(handleSearch, 300));
455
- document.getElementById('resetBtn').addEventListener('click', resetHighlighting);
456
- }
457
-
458
- function toggleSidebar() {
459
- const sidebar = document.getElementById('sidebar');
460
- sidebarCollapsed = !sidebarCollapsed;
461
-
462
- if (sidebarCollapsed) {
463
- sidebar.classList.add('collapsed');
464
- } else {
465
- sidebar.classList.remove('collapsed');
466
- }
467
- }
468
-
469
- function goHome() {
470
- // Navigate back to main page
471
- window.location.href = 'http://0.0.0.0:7860/'; // Adjust path as needed
472
- }
473
-
474
- function setupVisualizationSVG() {
475
- const container = document.querySelector('.main-content');
476
- const containerRect = container.getBoundingClientRect();
477
-
478
- svg = d3.select('#graph')
479
- .attr('width', containerRect.width)
480
- .attr('height', containerRect.height);
481
-
482
- g = svg.append('g');
483
-
484
- // Add zoom behavior
485
- const zoom = d3.zoom()
486
- .scaleExtent([0.1, 4])
487
- .on('zoom', (event) => {
488
- g.attr('transform', event.transform);
489
- });
490
-
491
- svg.call(zoom);
492
-
493
- // Click on empty space to reset highlighting
494
- svg.on('click', (event) => {
495
- if (event.target === event.currentTarget) {
496
- resetHighlighting();
497
- }
498
- });
499
- }
500
-
501
- async function loadGraphData(search = '') {
502
- try {
503
- showLoading();
504
- const url = search
505
- ? `${API_BASE}/graph?search=${encodeURIComponent(search)}`
506
- : `${API_BASE}/graph`;
507
-
508
- const response = await fetch(url);
509
- if (!response.ok) throw new Error('Failed to fetch graph data');
510
-
511
- graphData = await response.json();
512
- renderGraph();
513
- } catch (error) {
514
- showError('Failed to load graph data: ' + error.message);
515
- } finally {
516
- hideLoading();
517
- }
518
- }
519
-
520
- function renderGraph() {
521
- if (!graphData.nodes || graphData.nodes.length === 0) {
522
- showError('No data to display');
523
- return;
524
- }
525
-
526
- // Clear existing elements
527
- g.selectAll('*').remove();
528
- resetHighlighting();
529
-
530
- // Get container dimensions
531
- const width = +svg.attr('width');
532
- const height = +svg.attr('height');
533
-
534
- // Create simulation
535
- simulation = d3.forceSimulation(graphData.nodes)
536
- .force('link', d3.forceLink(graphData.edges).id(d => d.id).distance(100))
537
- .force('charge', d3.forceManyBody().strength(-300))
538
- .force('center', d3.forceCenter(width / 2, height / 2))
539
- .force('collision', d3.forceCollide().radius(30));
540
-
541
- // Create links
542
- const link = g.append('g')
543
- .selectAll('line')
544
- .data(graphData.edges)
545
- .join('line')
546
- .attr('class', 'link')
547
- .on('mouseover', showEdgeTooltip)
548
- .on('mouseout', hideTooltip);
549
-
550
- // Create nodes
551
- const node = g.append('g')
552
- .selectAll('circle')
553
- .data(graphData.nodes)
554
- .join('circle')
555
- .attr('class', 'node')
556
- .attr('r', 12)
557
- .attr('fill', d => getNodeColor(d))
558
- .attr('stroke', '#fff')
559
- .attr('stroke-width', 2)
560
- .on('mouseover', showNodeTooltip)
561
- .on('mouseout', hideTooltip)
562
- .on('click', handleNodeClick)
563
- .call(d3.drag()
564
- .on('start', dragStarted)
565
- .on('drag', dragged)
566
- .on('end', dragEnded));
567
-
568
- // Create labels
569
- const labels = g.append('g')
570
- .selectAll('text')
571
- .data(graphData.nodes)
572
- .join('text')
573
- .attr('class', 'node-label')
574
- .text(d => d.label.length > 12 ? d.label.substring(0, 12) + '...' : d.label);
575
-
576
- // Update positions on simulation tick
577
- simulation.on('tick', () => {
578
- link
579
- .attr('x1', d => d.source.x)
580
- .attr('y1', d => d.source.y)
581
- .attr('x2', d => d.target.x)
582
- .attr('y2', d => d.target.y);
583
-
584
- node
585
- .attr('cx', d => d.x)
586
- .attr('cy', d => d.y);
587
-
588
- labels
589
- .attr('x', d => d.x)
590
- .attr('y', d => d.y + 20);
591
- });
592
- }
593
-
594
- function getNodeColor(node) {
595
- const colors = {
596
- 'concept': '#4CAF50',
597
- 'disease': '#f44336',
598
- 'treatment': '#2196F3',
599
- 'attribute': '#FF9800',
600
- 'method': '#9C27B0',
601
- 'default': '#607D8B'
602
- };
603
- return colors[node.type] || colors.default;
604
- }
605
-
606
- function showNodeTooltip(event, d) {
607
- const tooltip = d3.select('#tooltip');
608
- tooltip.transition().duration(200).style('opacity', 1);
609
-
610
- const connectionCount = graphData.edges.filter(edge =>
611
- edge.source.id === d.id || edge.target.id === d.id
612
- ).length;
613
-
614
- tooltip.html(`
615
- <h4>${d.label}</h4>
616
- <p><strong>Type:</strong> ${d.type || 'Node'}</p>
617
- <p><strong>Connections:</strong> ${connectionCount}</p>
618
- <p>Click to highlight connections</p>
619
- `)
620
- .style('left', (event.pageX + 10) + 'px')
621
- .style('top', (event.pageY - 28) + 'px');
622
- }
623
-
624
- function showEdgeTooltip(event, d) {
625
- const tooltip = d3.select('#tooltip');
626
- tooltip.transition().duration(200).style('opacity', 1);
627
- tooltip.html(`
628
- <h4>${d.relation}</h4>
629
- <p><strong>From:</strong> ${d.source.label || d.source.id}</p>
630
- <p><strong>To:</strong> ${d.target.label || d.target.id}</p>
631
- `)
632
- .style('left', (event.pageX + 10) + 'px')
633
- .style('top', (event.pageY - 28) + 'px');
634
- }
635
-
636
- function hideTooltip() {
637
- d3.select('#tooltip').transition().duration(500).style('opacity', 0);
638
- }
639
-
640
- function handleNodeClick(event, d) {
641
- event.stopPropagation();
642
-
643
- if (selectedNode && selectedNode.id === d.id) {
644
- resetHighlighting();
645
- return;
646
- }
647
-
648
- selectedNode = d;
649
- highlightConnections(d);
650
- }
651
-
652
- function highlightConnections(selectedNode) {
653
- highlightedElements.nodes.clear();
654
- highlightedElements.edges.clear();
655
-
656
- graphData.edges.forEach(edge => {
657
- if (edge.source.id === selectedNode.id || edge.target.id === selectedNode.id) {
658
- highlightedElements.edges.add(edge);
659
- highlightedElements.nodes.add(edge.source.id);
660
- highlightedElements.nodes.add(edge.target.id);
661
- }
662
- });
663
-
664
- applyHighlighting();
665
- }
666
-
667
- function applyHighlighting() {
668
- g.selectAll('.node')
669
- .classed('highlighted', d => highlightedElements.nodes.has(d.id) && (!selectedNode || d.id !== selectedNode.id))
670
- .classed('selected', d => selectedNode && d.id === selectedNode.id)
671
- .classed('dimmed', d => selectedNode && !highlightedElements.nodes.has(d.id));
672
-
673
- g.selectAll('.link')
674
- .classed('highlighted', d => highlightedElements.edges.has(d))
675
- .classed('dimmed', d => selectedNode && !highlightedElements.edges.has(d));
676
-
677
- g.selectAll('.node-label')
678
- .classed('highlighted', d => highlightedElements.nodes.has(d.id))
679
- .classed('dimmed', d => selectedNode && !highlightedElements.nodes.has(d.id));
680
- }
681
-
682
- function resetHighlighting() {
683
- selectedNode = null;
684
- highlightedElements.nodes.clear();
685
- highlightedElements.edges.clear();
686
-
687
- g.selectAll('.node')
688
- .classed('highlighted', false)
689
- .classed('selected', false)
690
- .classed('dimmed', false);
691
-
692
- g.selectAll('.link')
693
- .classed('highlighted', false)
694
- .classed('dimmed', false);
695
-
696
- g.selectAll('.node-label')
697
- .classed('highlighted', false)
698
- .classed('dimmed', false);
699
- }
700
-
701
- async function handleSearch() {
702
- const query = document.getElementById('searchInput').value.trim();
703
- if (query === currentSearch) return;
704
-
705
- currentSearch = query;
706
- await loadGraphData(query);
707
- }
708
-
709
- function showError(message) {
710
- const mainContent = document.querySelector('.main-content');
711
- const errorDiv = document.createElement('div');
712
- errorDiv.className = 'error';
713
- errorDiv.textContent = message;
714
- mainContent.appendChild(errorDiv);
715
- setTimeout(() => errorDiv.remove(), 5000);
716
- }
717
-
718
- function showLoading() {
719
- document.getElementById('loading').style.display = 'block';
720
- }
721
-
722
- function hideLoading() {
723
- document.getElementById('loading').style.display = 'none';
724
- }
725
-
726
- function debounce(func, wait) {
727
- let timeout;
728
- return function executedFunction(...args) {
729
- const later = () => {
730
- clearTimeout(timeout);
731
- func(...args);
732
- };
733
- clearTimeout(timeout);
734
- timeout = setTimeout(later, wait);
735
- };
736
- }
737
-
738
- // Drag functions
739
- function dragStarted(event, d) {
740
- if (!event.active) simulation.alphaTarget(0.3).restart();
741
- d.fx = d.x;
742
- d.fy = d.y;
743
- }
744
-
745
- function dragged(event, d) {
746
- d.fx = event.x;
747
- d.fy = event.y;
748
- }
749
-
750
- function dragEnded(event, d) {
751
- if (!event.active) simulation.alphaTarget(0);
752
- d.fx = null;
753
- d.fy = null;
754
- }
755
-
756
- // Window resize handler
757
- window.addEventListener('resize', () => {
758
- const container = document.querySelector('.main-content');
759
- const containerRect = container.getBoundingClientRect();
760
-
761
- svg.attr('width', containerRect.width)
762
- .attr('height', containerRect.height);
763
-
764
- if (simulation) {
765
- simulation.force('center', d3.forceCenter(containerRect.width / 2, containerRect.height / 2));
766
- simulation.alpha(0.3).restart();
767
- }
768
- });
769
-
770
- // Initialize when DOM is loaded
771
- document.addEventListener('DOMContentLoaded', init);
772
- </script>
773
- </body>
774
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <link href="https://fonts.googleapis.com/css2?family=Mandali&display=swap" rel="stylesheet">
5
+ <link href="https://fonts.googleapis.com/css2?family=Varta:wght@400;500;600;700&display=swap" rel="stylesheet">
6
+ <meta charset="UTF-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <title>Knowledge Graph Explorer</title>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
10
+ <style>
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ body {
18
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
19
+ background: linear-gradient(135deg, #4a5568 0%, #2d3748 50%, #1a202c 100%);
20
+ color: white;
21
+ height: 100vh;
22
+ overflow: hidden;
23
+ }
24
+
25
+ .container {
26
+ display: flex;
27
+ height: 100vh;
28
+ }
29
+
30
+ /* Sidebar Styles */
31
+ .sidebar {
32
+ width: 400px;
33
+ background: rgba(0 0 0 0.25);
34
+ display: flex;
35
+ flex-direction: column;
36
+ transition: margin-left 0.3s ease;
37
+ position: relative;
38
+ z-index: 100;
39
+ }
40
+
41
+ .sidebar.collapsed {
42
+ margin-left: -400px;
43
+ }
44
+
45
+ /* Header */
46
+ .sidebar-header {
47
+ padding: 1.5rem;
48
+ }
49
+
50
+ .header-controls {
51
+ display: flex;
52
+ justify-content: space-between;
53
+ align-items: center;
54
+ margin-bottom: 1rem;
55
+ }
56
+
57
+ /* Menu Toggle - Always Visible */
58
+ .menu-toggle {
59
+ position: fixed;
60
+ top: 1.5rem;
61
+ left: 0.7rem;
62
+ z-index: 300;
63
+ background: none;
64
+ border: none;
65
+ color: white;
66
+ font-size: 1.5rem;
67
+ cursor: pointer;
68
+ padding: 0.75rem;
69
+ border-radius: 8px;
70
+ backdrop-filter: blur(10px);
71
+ }
72
+
73
+ .menu-toggle:hover {
74
+ background: rgba(0, 0, 0, 0.8);
75
+ }
76
+
77
+ .home-btn {
78
+ position: fixed;
79
+ background: none;
80
+ border: none;
81
+ color: white;
82
+ font-size: 1.5rem;
83
+ cursor: pointer;
84
+ padding: 0.5rem;
85
+ border-radius: 4px;
86
+ transition: background-color 0.3s ease;
87
+ }
88
+
89
+ .home-btn:hover {
90
+ background-color: rgba(0, 0, 0, 0.8);
91
+ }
92
+
93
+ .sidebar-title {
94
+ font-size: 2.5rem;
95
+ font-weight: 700;
96
+ color: #F8F3E7;
97
+ line-height: 1.2;
98
+ margin-top : 50px;
99
+ }
100
+
101
+ /* Search Section */
102
+ .search-section {
103
+ padding: 0 1.5rem 1.5rem;
104
+ }
105
+
106
+ .search-input {
107
+ width: 100%;
108
+ padding: 0.75rem 1rem;
109
+ background: rgb(248 243 231);
110
+ border-width: 2px;
111
+ border-style: solid;
112
+ border-color: #F3E7DD;
113
+ border-radius: 15px;
114
+ font-size: 0.9rem;
115
+ color: #797979;
116
+ margin-bottom: 1rem;
117
+ }
118
+
119
+ .search-input::placeholder {
120
+ color: #a0aec0;
121
+ }
122
+
123
+ .search-input:focus {
124
+ outline: none;
125
+ background: rgba(255, 255, 255, 1);
126
+ }
127
+
128
+ .reset-btn {
129
+ display: block; /* make it a block so margin works */
130
+ margin: 0 auto; /* this centers it horizontally */
131
+ background: rgb(110 131 131);
132
+ border: none;
133
+ color: white;
134
+ padding: 0.75rem 1.5rem;
135
+ border-radius: 20px;
136
+ font-size: 0.9rem;
137
+ font-weight: 500;
138
+ cursor: pointer;
139
+ max-width: 150px;
140
+ width: 100%;
141
+ box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2);
142
+ transition: all 0.3s ease;
143
+ }
144
+
145
+ .reset-btn:hover {
146
+ background: rgba(74, 85, 104, 1);
147
+ }
148
+
149
+ /* Instructions Panel */
150
+ .instructions-panel {
151
+ margin: 1rem;
152
+ background: rgb(248 243 231);
153
+ border-radius: 15px;
154
+ padding: 1.5rem;
155
+ color: #4a5568;
156
+ flex: 1;
157
+ margin-bottom: 7rem;
158
+ box-shadow: inset 0 4px 4px rgba(0,0,0,0.25);
159
+ }
160
+
161
+ .instructions-title {
162
+ font-size: 1.5rem;
163
+ font-weight: 800;
164
+ font-family: 'Varta', sans-serif;
165
+ color: #485656;
166
+ margin-bottom: 1rem;
167
+ text-align: center;
168
+ }
169
+
170
+ .instruction-item {
171
+ margin-bottom: 1rem;
172
+ font-family: 'Varta', sans-serif;
173
+ font-size: 1rem;
174
+ color: #485656;
175
+ line-height: 1.5;
176
+ font-weight: 300;
177
+ }
178
+
179
+ .instruction-item:last-child {
180
+ margin-bottom: 0;
181
+ }
182
+
183
+ .instruction-action {
184
+ font-weight: 700;
185
+ color: #485656;
186
+ }
187
+
188
+ /* Main Graph Area */
189
+ .main-content {
190
+ flex: 1;
191
+ position: relative;
192
+ background: rgb(77 83 109);
193
+ }
194
+
195
+ /* Home Button in Top-Right Corner */
196
+ .main-home-btn {
197
+ position: absolute;
198
+ top: 1.5rem;
199
+ right: 1.5rem;
200
+ z-index: 200;
201
+ background: rgba(0, 0, 0, 0.6);
202
+ border: none;
203
+ color: white;
204
+ font-size: 1.5rem;
205
+ cursor: pointer;
206
+ padding: 0.75rem;
207
+ border-radius: 8px;
208
+ transition: background-color 0.3s ease;
209
+ backdrop-filter: blur(10px);
210
+ }
211
+
212
+ .main-home-btn:hover {
213
+ background: rgba(0, 0, 0, 0.8);
214
+ }
215
+
216
+ /* Remove floating home - not needed anymore */
217
+ .floating-home {
218
+ display: none;
219
+ }
220
+
221
+ /* Graph Styles */
222
+ #graph {
223
+ width: 100%;
224
+ height: 100%;
225
+ }
226
+
227
+ .node {
228
+ cursor: pointer;
229
+ transition: all 0.3s ease;
230
+ filter: drop-shadow(0 0 6px rgba(76, 175, 80, 0.3));
231
+ }
232
+
233
+ .node:hover {
234
+ stroke-width: 3px;
235
+ filter: drop-shadow(0 0 12px rgba(76, 175, 80, 0.6));
236
+ }
237
+
238
+ .node.highlighted {
239
+ stroke: #4CAF50 !important;
240
+ stroke-width: 3px !important;
241
+ filter: drop-shadow(0 0 15px rgba(76, 175, 80, 0.8));
242
+ }
243
+
244
+ .node.selected {
245
+ stroke: #FFD700 !important;
246
+ stroke-width: 4px !important;
247
+ filter: drop-shadow(0 0 20px rgba(255, 215, 0, 0.8));
248
+ }
249
+
250
+ .node.dimmed {
251
+ opacity: 0.2;
252
+ filter: none;
253
+ }
254
+
255
+ .link {
256
+ stroke: rgba(255, 255, 255, 0.4);
257
+ stroke-width: 2px;
258
+ cursor: pointer;
259
+ transition: all 0.3s ease;
260
+ }
261
+
262
+ .link:hover {
263
+ stroke: #4CAF50;
264
+ stroke-width: 3px;
265
+ filter: drop-shadow(0 0 6px rgba(76, 175, 80, 0.5));
266
+ }
267
+
268
+ .link.highlighted {
269
+ stroke: #4CAF50 !important;
270
+ stroke-width: 3px !important;
271
+ filter: drop-shadow(0 0 8px rgba(76, 175, 80, 0.6));
272
+ }
273
+
274
+ .link.dimmed {
275
+ opacity: 0.1;
276
+ }
277
+
278
+ .node-label {
279
+ font-size: 11px;
280
+ font-weight: 600;
281
+ fill: white;
282
+ text-anchor: middle;
283
+ pointer-events: none;
284
+ text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
285
+ transition: all 0.3s ease;
286
+ }
287
+
288
+ .node-label.dimmed {
289
+ opacity: 0.2;
290
+ }
291
+
292
+ .node-label.highlighted {
293
+ fill: #4CAF50;
294
+ font-size: 13px;
295
+ text-shadow: 0 0 8px rgba(76, 175, 80, 0.8);
296
+ }
297
+
298
+ .tooltip {
299
+ position: absolute;
300
+ text-align: left;
301
+ padding: 1rem;
302
+ font-size: 0.9rem;
303
+ background: rgba(0, 0, 0, 0.9);
304
+ color: white;
305
+ border-radius: 8px;
306
+ pointer-events: none;
307
+ opacity: 0;
308
+ transition: opacity 0.3s;
309
+ max-width: 300px;
310
+ line-height: 1.5;
311
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
312
+ z-index: 1000;
313
+ }
314
+
315
+ .tooltip h4 {
316
+ margin: 0 0 0.5rem 0;
317
+ color: #4CAF50;
318
+ font-weight: 700;
319
+ }
320
+
321
+ .loading {
322
+ position: absolute;
323
+ top: 50%;
324
+ left: 50%;
325
+ transform: translate(-50%, -50%);
326
+ font-size: 1.1rem;
327
+ color: white;
328
+ text-align: center;
329
+ }
330
+
331
+ .loading-spinner {
332
+ border: 3px solid rgba(255, 255, 255, 0.3);
333
+ border-radius: 50%;
334
+ border-top: 3px solid white;
335
+ width: 40px;
336
+ height: 40px;
337
+ animation: spin 1s linear infinite;
338
+ margin: 0 auto 1rem;
339
+ }
340
+
341
+ @keyframes spin {
342
+ 0% { transform: rotate(0deg); }
343
+ 100% { transform: rotate(360deg); }
344
+ }
345
+
346
+ .error {
347
+ color: #ff6b6b;
348
+ background: rgba(255, 107, 107, 0.1);
349
+ padding: 1rem;
350
+ border-radius: 8px;
351
+ margin: 1rem;
352
+ border: 1px solid rgba(255, 107, 107, 0.3);
353
+ }
354
+
355
+ /* Responsive Design */
356
+ @media (max-width: 768px) {
357
+ .sidebar {
358
+ width: 100%;
359
+ position: absolute;
360
+ height: 100%;
361
+ z-index: 1000;
362
+ }
363
+
364
+ .sidebar.collapsed {
365
+ margin-left: -100%;
366
+ }
367
+ }
368
+ </style>
369
+ </head>
370
+ <body>
371
+ <div class="container">
372
+ <!-- Fixed Menu Toggle Button -->
373
+ <button class="menu-toggle" id="menuToggle">☰</button>
374
+
375
+ <!-- Sidebar -->
376
+ <div class="sidebar" id="sidebar">
377
+ <div class="sidebar-header">
378
+ <h1 class="sidebar-title">KNOWLEDGE<br>GRAPH</h1>
379
+ </div>
380
+
381
+ <div class="search-section">
382
+ <input
383
+ type="text"
384
+ class="search-input"
385
+ id="searchInput"
386
+ placeholder="Search nodes and relations..."
387
+ >
388
+ <button class="reset-btn" id="resetBtn">Reset Highlight</button>
389
+ </div>
390
+
391
+ <div class="instructions-panel">
392
+ <h3 class="instructions-title">HOW TO USE</h3>
393
+ <div class="instruction-item">
394
+ <span class="instruction-action">Click a node</span> to highlight its connections
395
+ </div>
396
+ <div class="instruction-item">
397
+ <span class="instruction-action">Hover over nodes and edges</span> for details
398
+ </div>
399
+ <div class="instruction-item">
400
+ <span class="instruction-action">Drag nodes</span> to reposition them
401
+ </div>
402
+ <div class="instruction-item">
403
+ <span class="instruction-action">Zoom and pan</span> to explore the graph
404
+ </div>
405
+ <div class="instruction-item">
406
+ <span class="instruction-action">Search</span> to filter nodes and relations
407
+ </div>
408
+ </div>
409
+ </div>
410
+
411
+ <!-- Main Graph Area -->
412
+ <div class="main-content">
413
+ <!-- Home Button in Top-Right -->
414
+ <button class="main-home-btn" id="mainHomeBtn">
415
+ <img src="/static/Home.png" alt="Home" style="width: 20px; height: 20px;">
416
+ </button>
417
+ <div id="loading" class="loading">
418
+ <div class="loading-spinner"></div>
419
+ Loading knowledge graph...
420
+ </div>
421
+ <svg id="graph"></svg>
422
+ </div>
423
+ </div>
424
+
425
+ <div class="tooltip" id="tooltip"></div>
426
+
427
+ <script>
428
+ // Configuration
429
+ const API_BASE = '/api';
430
+
431
+ // Global variables
432
+ let graphData = { nodes: [], edges: [] };
433
+ let simulation;
434
+ let svg, g;
435
+ let currentSearch = '';
436
+ let selectedNode = null;
437
+ let highlightedElements = { nodes: new Set(), edges: new Set() };
438
+ let sidebarCollapsed = false;
439
+
440
+ // Initialize the application
441
+ async function init() {
442
+ setupEventListeners();
443
+ setupVisualizationSVG();
444
+ await loadGraphData();
445
+ hideLoading();
446
+ }
447
+
448
+ function setupEventListeners() {
449
+ // Sidebar toggle
450
+ document.getElementById('menuToggle').addEventListener('click', toggleSidebar);
451
+ document.getElementById('mainHomeBtn').addEventListener('click', goHome);
452
+
453
+ // Search and reset
454
+ document.getElementById('searchInput').addEventListener('input', debounce(handleSearch, 300));
455
+ document.getElementById('resetBtn').addEventListener('click', resetHighlighting);
456
+ }
457
+
458
+ function toggleSidebar() {
459
+ const sidebar = document.getElementById('sidebar');
460
+ sidebarCollapsed = !sidebarCollapsed;
461
+
462
+ if (sidebarCollapsed) {
463
+ sidebar.classList.add('collapsed');
464
+ } else {
465
+ sidebar.classList.remove('collapsed');
466
+ }
467
+ }
468
+
469
+ function goHome() {
470
+ // Navigate back to main page
471
+ window.location.href = '/';
472
+ }
473
+
474
+ function setupVisualizationSVG() {
475
+ const container = document.querySelector('.main-content');
476
+ const containerRect = container.getBoundingClientRect();
477
+
478
+ svg = d3.select('#graph')
479
+ .attr('width', containerRect.width)
480
+ .attr('height', containerRect.height);
481
+
482
+ g = svg.append('g');
483
+
484
+ // Add zoom behavior
485
+ const zoom = d3.zoom()
486
+ .scaleExtent([0.1, 4])
487
+ .on('zoom', (event) => {
488
+ g.attr('transform', event.transform);
489
+ });
490
+
491
+ svg.call(zoom);
492
+
493
+ // Click on empty space to reset highlighting
494
+ svg.on('click', (event) => {
495
+ if (event.target === event.currentTarget) {
496
+ resetHighlighting();
497
+ }
498
+ });
499
+ }
500
+
501
+ async function loadGraphData(search = '') {
502
+ try {
503
+ showLoading();
504
+ const url = search
505
+ ? `${API_BASE}/graph?search=${encodeURIComponent(search)}`
506
+ : `${API_BASE}/graph`;
507
+
508
+ const response = await fetch(url);
509
+ if (!response.ok) throw new Error('Failed to fetch graph data');
510
+
511
+ graphData = await response.json();
512
+ renderGraph();
513
+ } catch (error) {
514
+ showError('Failed to load graph data: ' + error.message);
515
+ } finally {
516
+ hideLoading();
517
+ }
518
+ }
519
+
520
+ function renderGraph() {
521
+ if (!graphData.nodes || graphData.nodes.length === 0) {
522
+ showError('No data to display');
523
+ return;
524
+ }
525
+
526
+ // Clear existing elements
527
+ g.selectAll('*').remove();
528
+ resetHighlighting();
529
+
530
+ // Get container dimensions
531
+ const width = +svg.attr('width');
532
+ const height = +svg.attr('height');
533
+
534
+ // Create simulation
535
+ simulation = d3.forceSimulation(graphData.nodes)
536
+ .force('link', d3.forceLink(graphData.edges).id(d => d.id).distance(100))
537
+ .force('charge', d3.forceManyBody().strength(-300))
538
+ .force('center', d3.forceCenter(width / 2, height / 2))
539
+ .force('collision', d3.forceCollide().radius(30));
540
+
541
+ // Create links
542
+ const link = g.append('g')
543
+ .selectAll('line')
544
+ .data(graphData.edges)
545
+ .join('line')
546
+ .attr('class', 'link')
547
+ .on('mouseover', showEdgeTooltip)
548
+ .on('mouseout', hideTooltip);
549
+
550
+ // Create nodes
551
+ const node = g.append('g')
552
+ .selectAll('circle')
553
+ .data(graphData.nodes)
554
+ .join('circle')
555
+ .attr('class', 'node')
556
+ .attr('r', 12)
557
+ .attr('fill', d => getNodeColor(d))
558
+ .attr('stroke', '#fff')
559
+ .attr('stroke-width', 2)
560
+ .on('mouseover', showNodeTooltip)
561
+ .on('mouseout', hideTooltip)
562
+ .on('click', handleNodeClick)
563
+ .call(d3.drag()
564
+ .on('start', dragStarted)
565
+ .on('drag', dragged)
566
+ .on('end', dragEnded));
567
+
568
+ // Create labels
569
+ const labels = g.append('g')
570
+ .selectAll('text')
571
+ .data(graphData.nodes)
572
+ .join('text')
573
+ .attr('class', 'node-label')
574
+ .text(d => d.label.length > 12 ? d.label.substring(0, 12) + '...' : d.label);
575
+
576
+ // Update positions on simulation tick
577
+ simulation.on('tick', () => {
578
+ link
579
+ .attr('x1', d => d.source.x)
580
+ .attr('y1', d => d.source.y)
581
+ .attr('x2', d => d.target.x)
582
+ .attr('y2', d => d.target.y);
583
+
584
+ node
585
+ .attr('cx', d => d.x)
586
+ .attr('cy', d => d.y);
587
+
588
+ labels
589
+ .attr('x', d => d.x)
590
+ .attr('y', d => d.y + 20);
591
+ });
592
+ }
593
+
594
+ function getNodeColor(node) {
595
+ const colors = {
596
+ 'concept': '#4CAF50',
597
+ 'disease': '#f44336',
598
+ 'treatment': '#2196F3',
599
+ 'attribute': '#FF9800',
600
+ 'method': '#9C27B0',
601
+ 'default': '#607D8B'
602
+ };
603
+ return colors[node.type] || colors.default;
604
+ }
605
+
606
+ function showNodeTooltip(event, d) {
607
+ const tooltip = d3.select('#tooltip');
608
+ tooltip.transition().duration(200).style('opacity', 1);
609
+
610
+ const connectionCount = graphData.edges.filter(edge =>
611
+ edge.source.id === d.id || edge.target.id === d.id
612
+ ).length;
613
+
614
+ tooltip.html(`
615
+ <h4>${d.label}</h4>
616
+ <p><strong>Type:</strong> ${d.type || 'Node'}</p>
617
+ <p><strong>Connections:</strong> ${connectionCount}</p>
618
+ <p>Click to highlight connections</p>
619
+ `)
620
+ .style('left', (event.pageX + 10) + 'px')
621
+ .style('top', (event.pageY - 28) + 'px');
622
+ }
623
+
624
+ function showEdgeTooltip(event, d) {
625
+ const tooltip = d3.select('#tooltip');
626
+ tooltip.transition().duration(200).style('opacity', 1);
627
+ tooltip.html(`
628
+ <h4>${d.relation}</h4>
629
+ <p><strong>From:</strong> ${d.source.label || d.source.id}</p>
630
+ <p><strong>To:</strong> ${d.target.label || d.target.id}</p>
631
+ `)
632
+ .style('left', (event.pageX + 10) + 'px')
633
+ .style('top', (event.pageY - 28) + 'px');
634
+ }
635
+
636
+ function hideTooltip() {
637
+ d3.select('#tooltip').transition().duration(500).style('opacity', 0);
638
+ }
639
+
640
+ function handleNodeClick(event, d) {
641
+ event.stopPropagation();
642
+
643
+ if (selectedNode && selectedNode.id === d.id) {
644
+ resetHighlighting();
645
+ return;
646
+ }
647
+
648
+ selectedNode = d;
649
+ highlightConnections(d);
650
+ }
651
+
652
+ function highlightConnections(selectedNode) {
653
+ highlightedElements.nodes.clear();
654
+ highlightedElements.edges.clear();
655
+
656
+ graphData.edges.forEach(edge => {
657
+ if (edge.source.id === selectedNode.id || edge.target.id === selectedNode.id) {
658
+ highlightedElements.edges.add(edge);
659
+ highlightedElements.nodes.add(edge.source.id);
660
+ highlightedElements.nodes.add(edge.target.id);
661
+ }
662
+ });
663
+
664
+ applyHighlighting();
665
+ }
666
+
667
+ function applyHighlighting() {
668
+ g.selectAll('.node')
669
+ .classed('highlighted', d => highlightedElements.nodes.has(d.id) && (!selectedNode || d.id !== selectedNode.id))
670
+ .classed('selected', d => selectedNode && d.id === selectedNode.id)
671
+ .classed('dimmed', d => selectedNode && !highlightedElements.nodes.has(d.id));
672
+
673
+ g.selectAll('.link')
674
+ .classed('highlighted', d => highlightedElements.edges.has(d))
675
+ .classed('dimmed', d => selectedNode && !highlightedElements.edges.has(d));
676
+
677
+ g.selectAll('.node-label')
678
+ .classed('highlighted', d => highlightedElements.nodes.has(d.id))
679
+ .classed('dimmed', d => selectedNode && !highlightedElements.nodes.has(d.id));
680
+ }
681
+
682
+ function resetHighlighting() {
683
+ selectedNode = null;
684
+ highlightedElements.nodes.clear();
685
+ highlightedElements.edges.clear();
686
+
687
+ g.selectAll('.node')
688
+ .classed('highlighted', false)
689
+ .classed('selected', false)
690
+ .classed('dimmed', false);
691
+
692
+ g.selectAll('.link')
693
+ .classed('highlighted', false)
694
+ .classed('dimmed', false);
695
+
696
+ g.selectAll('.node-label')
697
+ .classed('highlighted', false)
698
+ .classed('dimmed', false);
699
+ }
700
+
701
+ async function handleSearch() {
702
+ const query = document.getElementById('searchInput').value.trim();
703
+ if (query === currentSearch) return;
704
+
705
+ currentSearch = query;
706
+ await loadGraphData(query);
707
+ }
708
+
709
+ function showError(message) {
710
+ const mainContent = document.querySelector('.main-content');
711
+ const errorDiv = document.createElement('div');
712
+ errorDiv.className = 'error';
713
+ errorDiv.textContent = message;
714
+ mainContent.appendChild(errorDiv);
715
+ setTimeout(() => errorDiv.remove(), 5000);
716
+ }
717
+
718
+ function showLoading() {
719
+ document.getElementById('loading').style.display = 'block';
720
+ }
721
+
722
+ function hideLoading() {
723
+ document.getElementById('loading').style.display = 'none';
724
+ }
725
+
726
+ function debounce(func, wait) {
727
+ let timeout;
728
+ return function executedFunction(...args) {
729
+ const later = () => {
730
+ clearTimeout(timeout);
731
+ func(...args);
732
+ };
733
+ clearTimeout(timeout);
734
+ timeout = setTimeout(later, wait);
735
+ };
736
+ }
737
+
738
+ // Drag functions
739
+ function dragStarted(event, d) {
740
+ if (!event.active) simulation.alphaTarget(0.3).restart();
741
+ d.fx = d.x;
742
+ d.fy = d.y;
743
+ }
744
+
745
+ function dragged(event, d) {
746
+ d.fx = event.x;
747
+ d.fy = event.y;
748
+ }
749
+
750
+ function dragEnded(event, d) {
751
+ if (!event.active) simulation.alphaTarget(0);
752
+ d.fx = null;
753
+ d.fy = null;
754
+ }
755
+
756
+ // Window resize handler
757
+ window.addEventListener('resize', () => {
758
+ const container = document.querySelector('.main-content');
759
+ const containerRect = container.getBoundingClientRect();
760
+
761
+ svg.attr('width', containerRect.width)
762
+ .attr('height', containerRect.height);
763
+
764
+ if (simulation) {
765
+ simulation.force('center', d3.forceCenter(containerRect.width / 2, containerRect.height / 2));
766
+ simulation.alpha(0.3).restart();
767
+ }
768
+ });
769
+
770
+ // Initialize when DOM is loaded
771
+ document.addEventListener('DOMContentLoaded', init);
772
+ </script>
773
+ </body>
774
  </html>