File size: 30,057 Bytes
35e5b82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
934d8c3
35e5b82
 
 
 
 
 
 
 
3e40086
 
934d8c3
 
1e6e2b3
 
 
 
 
 
0b2054b
 
 
35e5b82
 
 
 
 
 
513a8f8
35e5b82
 
 
 
 
 
 
 
513a8f8
 
 
 
 
 
 
35e5b82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675149c
7c3814c
675149c
 
 
35e5b82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675149c
 
 
 
 
 
 
35e5b82
 
 
 
 
 
 
 
 
 
675149c
 
35e5b82
 
fa01707
 
 
 
 
 
 
 
 
 
675149c
 
 
35e5b82
 
 
 
 
675149c
35e5b82
 
278f515
 
 
 
 
35e5b82
 
675149c
 
35e5b82
 
 
513a8f8
 
35e5b82
 
 
 
 
8daace0
35e5b82
 
 
 
 
 
 
 
675149c
0b2054b
675149c
35e5b82
 
0b2054b
 
35e5b82
 
 
 
 
675149c
35e5b82
 
675149c
 
35e5b82
675149c
 
7c3814c
 
 
 
 
 
675149c
 
 
 
35e5b82
 
675149c
35e5b82
 
 
 
 
 
 
513a8f8
35e5b82
 
 
 
 
 
 
 
 
513a8f8
 
35e5b82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c3814c
35e5b82
 
 
 
 
 
7c3814c
35e5b82
7c3814c
 
513a8f8
7c3814c
 
513a8f8
 
 
 
 
 
 
 
 
 
 
7c3814c
 
 
8daace0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c3814c
 
513a8f8
 
 
 
 
 
 
 
 
 
7c3814c
 
35e5b82
 
7c3814c
35e5b82
 
 
675149c
 
 
 
 
7c3814c
 
 
 
 
 
 
 
675149c
 
35e5b82
 
675149c
35e5b82
 
 
675149c
35e5b82
 
 
 
fa01707
 
 
 
 
 
 
 
7c3814c
35e5b82
7c3814c
 
675149c
35e5b82
 
 
 
7c3814c
fa01707
 
 
 
 
 
 
35e5b82
 
 
7c3814c
 
35e5b82
7c3814c
 
 
8daace0
 
 
7c3814c
1df1529
7c3814c
1df1529
 
8daace0
 
 
 
 
7c3814c
 
 
 
1df1529
7c3814c
1df1529
 
8daace0
 
35e5b82
 
 
7c3814c
1df1529
 
 
 
 
675149c
1df1529
7c3814c
 
1df1529
 
7c3814c
5b82be9
1df1529
 
 
 
 
7c3814c
 
1df1529
 
7c3814c
1df1529
7c3814c
1df1529
 
 
7c3814c
 
1df1529
7c3814c
1df1529
 
 
7c3814c
 
1df1529
 
 
 
 
 
 
7c3814c
1df1529
675149c
1df1529
 
 
 
 
513a8f8
1df1529
 
513a8f8
5b82be9
1df1529
 
 
 
 
35e5b82
675149c
7c3814c
1df1529
 
7c3814c
 
 
1df1529
 
 
513a8f8
1df1529
 
513a8f8
 
 
 
1df1529
 
 
 
 
 
 
 
7c3814c
35e5b82
675149c
 
 
 
 
 
 
fa01707
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35e5b82
1df1529
675149c
35e5b82
278f515
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1df1529
 
35e5b82
1df1529
 
35e5b82
 
675149c
35e5b82
 
675149c
 
 
 
 
 
35e5b82
 
675149c
 
35e5b82
 
 
 
 
 
 
7c3814c
35e5b82
 
7c3814c
 
 
675149c
35e5b82
675149c
 
 
7c3814c
675149c
7c3814c
 
 
8daace0
7c3814c
 
675149c
7c3814c
 
8daace0
7c3814c
 
675149c
7c3814c
8daace0
 
 
 
 
7c3814c
 
675149c
 
7c3814c
675149c
7c3814c
1df1529
 
7c3814c
 
675149c
1df1529
 
 
 
 
675149c
 
 
 
 
 
 
 
 
 
 
7c3814c
 
513a8f8
 
 
 
 
 
 
 
 
35e5b82
 
7c3814c
 
 
 
 
 
 
 
35e5b82
7c3814c
 
 
 
 
 
 
 
 
35e5b82
 
 
7c3814c
 
35e5b82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675149c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b2054b
 
 
 
 
 
7c3814c
 
 
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
// Global variables
let sigmaInstance;
let graph;
let filter;
let config = {};
let greyColor = '#ccc';
let selectedNode = null;
let colorAttributes = [];
let colors = [
  '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', 
  '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'
];
let nodeTypes = {
  'paper': { color: '#2ca02c', size: 3 },
  'author': { color: '#9467bd', size: 5 },
  'organization': { color: '#1f77b4', size: 4 },
  'unknown': { color: '#ff7f0e', size: 3 }
};

// Initialize when document is ready
$(document).ready(function() {
  console.log("Document ready, initializing 🤗 Daily Papers Atlas");
  
  // Initialize attribute pane
  $('#attributepane').css('display', 'none');
  
  // Load configuration
  $.getJSON('config.json', function(data) {
    console.log("Configuration loaded:", data);
    config = data;
    document.title = config.text.title || 'Daily Papers Atlas';
    $('#title').text(config.text.title || 'Daily Papers Atlas');
    
    // Don't modify the intro text at all - using hardcoded HTML
    
    // Update the path to load data from the data folder
    if (config.data && !config.data.startsWith('data/')) {
      config.data = 'data/' + config.data;
    }
    
    // Initialize the edge color toggle after config is loaded
    $('#grey-edges-toggle').prop('checked', config.features?.useGreyEdges || false);
    
    loadGraph();
  }).fail(function(jqXHR, textStatus, errorThrown) {
    console.error("Failed to load config:", textStatus, errorThrown);
  });

  // Set up search functionality
  $('#search-input').on('input', function(e) {
    let searchTerm = $(this).val();
    if (searchTerm.length > 2) {
      searchNodes(searchTerm);
    } else {
      $('.results').empty();
    }
  });
  
  // Add functionality for Enter key in search
  $('#search-input').keypress(function(e) {
    if (e.which === 13) { // Enter key
      let searchTerm = $(this).val();
      if (searchTerm.length > 0) {
        searchNodes(searchTerm);
      }
    }
  });

  // Set up zoom buttons
  $('#zoom .z[rel="in"]').click(function() {
    if (sigmaInstance) {
      let a = sigmaInstance._core;
      sigmaInstance.zoomTo(a.domElements.nodes.width / 2, a.domElements.nodes.height / 2, a.mousecaptor.ratio * 1.5);
    }
  });
  
  $('#zoom .z[rel="out"]').click(function() {
    if (sigmaInstance) {
      let a = sigmaInstance._core;
      sigmaInstance.zoomTo(a.domElements.nodes.width / 2, a.domElements.nodes.height / 2, a.mousecaptor.ratio * 0.5);
    }
  });
  
  $('#zoom .z[rel="center"]').click(function() {
    if (sigmaInstance) {
      sigmaInstance.position(0, 0, 1).draw();
    }
  });

  // Set up attribute pane functionality
  $('.returntext').click(function() {
    nodeNormal();
  });

  // Set up filter selector
  $('#filter-select').change(function() {
    let filterValue = $(this).val();
    filterByNodeType(filterValue);
  });

  // Call updateLegend to ensure it runs
  setTimeout(function() {
    updateLegend();
  }, 500);
});

// Load graph data
function loadGraph() {
  console.log("Loading graph data from:", config.data);
  
  // Check if data is a .gz file and needs decompression
  if (config.data && config.data.endsWith('.gz')) {
    console.log("Compressed data detected, loading via fetch and pako");
    
    fetch(config.data)
      .then(response => response.arrayBuffer())
      .then(arrayBuffer => {
        try {
          // Decompress the gzipped data
          const uint8Array = new Uint8Array(arrayBuffer);
          const decompressed = pako.inflate(uint8Array, { to: 'string' });
          
          // Parse the JSON data
          const data = JSON.parse(decompressed);
          console.log("Graph data decompressed and parsed successfully");
          initializeGraph(data);
        } catch (error) {
          console.error("Error decompressing data:", error);
        }
      })
      .catch(error => {
        console.error("Error fetching compressed data:", error);
      });
  } else {
    // Load uncompressed JSON directly
    $.getJSON(config.data, function(data) {
      console.log("Graph data loaded successfully");
      initializeGraph(data);
    }).fail(function(jqXHR, textStatus, errorThrown) {
      console.error("Failed to load graph data:", textStatus, errorThrown);
      alert('Failed to load graph data. Please check the console for more details.');
    });
  }
}

// Initialize the graph with the loaded data
function initializeGraph(data) {
  graph = data;
  console.log("Initializing graph with nodes:", graph.nodes.length, "edges:", graph.edges.length);
  
  try {
    // Initialize Sigma instance using the older sigma.init pattern
    sigmaInstance = sigma.init(document.getElementById('sigma-canvas'));
    
    console.log("Sigma instance created:", sigmaInstance);
    
    if (!sigmaInstance) {
      console.error("Failed to create sigma instance");
      return;
    }
    
    // Configure mouse properties to ensure events work
    sigmaInstance.mouseProperties({
      maxRatio: 32,
      minRatio: 0.5,
      mouseEnabled: true,
      mouseInertia: 0.8
    });
    
    console.log("Sigma mouse properties configured");
    
    // Add nodes to the graph
    console.log("Adding nodes to sigma instance...");
    for (let i = 0; i < graph.nodes.length; i++) {
      let node = graph.nodes[i];
      
      // Try to detect node type if not already set
      if (!node.type && node.id && node.id.includes('_')) {
        const idParts = node.id.split('_');
        if (idParts.length >= 2 && ['paper', 'author', 'organization'].includes(idParts[0])) {
          node.type = idParts[0];
          console.log(`Detected type from ID: ${node.id}${node.type}`);
        }
      }
      
      let nodeColor = node.color || (node.type && config.nodeTypes && config.nodeTypes[node.type] ? 
                      config.nodeTypes[node.type].color : nodeTypes[node.type]?.color || '#666');
      
      sigmaInstance.addNode(node.id, {
        label: node.label || node.id,
        x: node.x || Math.random() * 100,
        y: node.y || Math.random() * 100,
        size: node.size || 1,
        color: nodeColor,
        type: node.type
      });
      
      // Debug output for a few nodes to verify type is set
      if (i < 3) {
        console.log("Added node:", node.id, "with type:", node.type);
      }
    }
    
    // Add edges to the graph
    console.log("Adding edges to sigma instance...");
    for (let i = 0; i < graph.edges.length; i++) {
      let edge = graph.edges[i];
      sigmaInstance.addEdge(edge.id, edge.source, edge.target, {
        size: edge.size || 1
        // Don't set edge color here - let the drawing properties handle it
      });
    }
    
    // Configure drawing properties
    sigmaInstance.drawingProperties({
      labelThreshold: 3000, // Set to a high value to hide all labels by default
      defaultLabelColor: config.sigma?.drawingProperties?.defaultLabelColor || '#000',
      defaultLabelSize: config.sigma?.drawingProperties?.defaultLabelSize || 14,
      defaultEdgeType: config.sigma?.drawingProperties?.defaultEdgeType || 'curve',
      defaultHoverLabelBGColor: config.sigma?.drawingProperties?.defaultHoverLabelBGColor || '#002147',
      defaultLabelHoverColor: config.sigma?.drawingProperties?.defaultLabelHoverColor || '#fff',
      borderSize: 2,
      nodeBorderColor: '#fff',
      defaultNodeBorderColor: '#fff',
      defaultNodeHoverColor: '#fff',
      edgeColor: 'default', // Always use solid grey edges
      defaultEdgeColor: '#ccc'
    });
    
    console.log("Edge color mode: solid grey");
    
    // Configure graph properties
    sigmaInstance.graphProperties({
      minNodeSize: config.sigma?.graphProperties?.minNodeSize || 1,
      maxNodeSize: config.sigma?.graphProperties?.maxNodeSize || 8,
      minEdgeSize: config.sigma?.graphProperties?.minEdgeSize || 0.5,
      maxEdgeSize: config.sigma?.graphProperties?.maxEdgeSize || 2
    });
    
    // Force initial rendering
    sigmaInstance.draw();
    
    console.log("Graph data loaded into sigma instance");
    
    // Initialize filters
    initFilters();
    
    // Update the legend
    updateLegend();
    
    // Bind events
    bindEvents();
    
    console.log("Graph initialization complete");
    
  } catch (e) {
    console.error("Error in initializeGraph:", e, e.stack);
  }
}

// Apply node styles based on node type
function applyNodeStyles() {
  if (!sigmaInstance) return;
  try {
    // First update node colors
    sigmaInstance.iterNodes(function(node) {
      if (node.type && config.nodeTypes && config.nodeTypes[node.type]) {
        node.color = config.nodeTypes[node.type].color;
        node.size = config.nodeTypes[node.type].size;
      } else if (node.type && nodeTypes[node.type]) {
        node.color = nodeTypes[node.type].color;
        node.size = nodeTypes[node.type].size;
      }
    });
    
    // Ensure edges match the target node colors by redrawing
    sigmaInstance.refresh();
  } catch (e) {
    console.error("Error applying node styles:", e);
  }
}

// Initialize filters
function initFilters() {
  try {
    if (sigma.plugins && sigma.plugins.filter) {
      filter = new sigma.plugins.filter(sigmaInstance);
      console.log("Filter plugin initialized");
    } else {
      console.warn("Sigma filter plugin not available");
    }
  } catch (e) {
    console.error("Error initializing filter plugin:", e);
  }
}

// Filter nodes by type
function filterByNodeType(filterValue) {
  if (!filter) return;
  try {
    filter.undo('node-type');
    
    if (filterValue === 'papers') {
      filter.nodesBy(function(n) {
        return n.type === 'paper';
      }, 'node-type');
    } else if (filterValue === 'authors') {
      filter.nodesBy(function(n) {
        return n.type === 'author';
      }, 'node-type');
    }
    
    filter.apply();
    sigmaInstance.refresh();
  } catch (e) {
    console.error("Error filtering nodes:", e);
  }
}

// Bind events based on the Model-Atlas implementation
function bindEvents() {
  if (!sigmaInstance) {
    console.error("Sigma instance not found when binding events");
    return;
  }
  
  console.log("Binding events to sigma instance");
  
  // When a node is clicked, display its details
  sigmaInstance.bind('upnodes', function(event) {
    console.log("Node clicked event fired:", event);
    if (event.content && event.content.length > 0) {
      var nodeId = event.content[0];
      console.log("Processing node click for node:", nodeId);
      // Set a flag to indicate we're processing a node click
      sigmaInstance.isMouseDown = true;
      // Call nodeActive with a slight delay to ensure event handling is complete
      setTimeout(function() {
        nodeActive(nodeId);
        // Reset the flag after processing
        setTimeout(function() {
          sigmaInstance.isMouseDown = false;
        }, 10);
      }, 10);
    }
  });
  
  // Show label when hovering over a node
  sigmaInstance.bind('overnodes', function(event) {
    if (event.content && event.content.length > 0) {
      var nodeId = event.content[0];
      
      sigmaInstance.iterNodes(function(n) {
        if (n.id === nodeId) {
          // Allow hover label to appear for any node being hovered over
          n.forceLabel = true;
          
          // But in detail view, don't allow this to override the selected node's neighbors
          if (sigmaInstance.detail && selectedNode && n.id !== selectedNode.id) {
            // Store the hover state to know we need to reset this node specifically
            n.attr = n.attr || {};
            n.attr.isHovered = true;
          }
        }
      });
      
      sigmaInstance.draw(2, 2, 2, 2);
    }
  });
  
  // Hide label when mouse leaves the node
  sigmaInstance.bind('outnodes', function(event) {
    // Handle nodes that were being hovered over
    if (event.content && event.content.length > 0) {
      var nodeId = event.content[0];
      
      sigmaInstance.iterNodes(function(n) {
        if (n.id === nodeId) {
          // Remove hover flag
          if (n.attr && n.attr.isHovered) {
            delete n.attr.isHovered;
          }
          
          // In detail view, only the selected node should keep its label
          if (sigmaInstance.detail) {
            if (selectedNode && n.id !== selectedNode.id) {
              n.forceLabel = false;
            }
          } else {
            // In normal view, always hide the label when hover ends
            n.forceLabel = false;
          }
        }
      });
    }
    
    sigmaInstance.draw(2, 2, 2, 2);
  });
  
  // When stage is clicked, close the attribute pane
  document.getElementById('sigma-canvas').addEventListener('click', function(evt) {
    // If we're in detail view and didn't click on a node, return to full graph
    if (sigmaInstance.detail && !sigmaInstance.isMouseDown) {
      // Give priority to node click events by waiting
      setTimeout(function() {
        // Only proceed if isMouseDown is still false after the delay
        if (!sigmaInstance.isMouseDown) {
          console.log("Canvas clicked while in detail view - returning to full view");
          nodeNormal();
        }
      }, 100);
    }
  });
}

// Display node details - without color changes
function nodeActive(nodeId) {
  console.log("nodeActive called with id:", nodeId);
  
  if (!sigmaInstance) {
    console.error("Sigma instance not ready for nodeActive");
    return;
  }
  
  if (sigmaInstance.detail && selectedNode && selectedNode.id === nodeId) {
    // Already active, no need to redraw
    return;
  }
  
  // Reset previous selection if any
  nodeNormal();
  
  // Find the selected node
  var selected = null;
  sigmaInstance.iterNodes(function(n) {
    if (n.id == nodeId) {
      selected = n;
    }
  });
  
  if (!selected) {
    console.error("Node not found:", nodeId);
    return;
  }
  
  // Debug: Log the structure of the selected node to understand available properties
  console.log("Selected node structure:", selected);
  if (selected.id && selected.id.includes('_')) {
    console.log("ID parts:", selected.id.split('_'));
  }
  console.log("Node type from property:", selected.type);
  console.log("Node color:", selected.color);
  
  // Mark as in detail view
  sigmaInstance.detail = true;
  
  // Store reference to selected node
  selectedNode = selected;
  
  // Find neighbors
  var neighbors = {};
  sigmaInstance.iterEdges(function(e) {
    if (e.source == nodeId || e.target == nodeId) {
      // Get ID of the neighbor node
      const neighborId = e.source == nodeId ? e.target : e.source;
      // Initialize the neighbor object if it doesn't exist
      neighbors[neighborId] = neighbors[neighborId] || {};
      // Store edge information if needed
      neighbors[neighborId].edgeLabel = e.label || "";
      neighbors[neighborId].edgeColor = e.color;
    }
  });
  
  // In Sigma.js v0.1, we need to use a different approach for focus
  // Store original colors for all nodes and edges
  sigmaInstance.iterNodes(function(n) {
    n.attr = n.attr || {};
    n.attr.originalColor = n.color;
    
    // Store original forceLabel state
    n.attr.originalForceLabel = n.forceLabel;
    
    if (n.id === nodeId) {
      // Make selected node slightly larger based on config
      n.attr.originalSize = n.size;
      const sizeFactor = config.highlighting?.selectedNodeSizeFactor ?? 1.5;
      n.size = n.size * sizeFactor;
      // Force label to show for selected node
      n.forceLabel = true;
    } else if (neighbors[n.id]) {
      // Do not show labels for neighbors, only keep them visible
      n.forceLabel = false;
    } else if (!neighbors[n.id]) {
      // For non-neighbor nodes, we use a custom attribute to track they should be dimmed
      // (Sigma v0.1 doesn't support opacity directly)
      n.attr.dimmed = true;
      // Apply a transparent version of the original color using configured opacity
      var rgb = getRGBColor(n.color);
      const opacity = config.highlighting?.nodeOpacity ?? 0.2;
      n.color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + opacity + ')';
      // Hide labels for non-neighbor nodes
      n.forceLabel = false;
    }
  });
  
  // Apply the same to edges
  let debugCounts = { connected: 0, notConnected: 0 };
  let edgeCount = 0;
  
  console.log("Starting edge processing for node:", nodeId);
  
  sigmaInstance.iterEdges(function(e) {
    edgeCount++;
    e.attr = e.attr || {};
    
    // First, ensure we store the original color (only once)
    if (typeof e.attr.originalColor === 'undefined') {
      e.attr.originalColor = e.color;
      // console.log("Storing original color for edge:", e.id, "Color:", e.color);
    }
    
    // Store original size for edges (only once)
    if (typeof e.attr.originalSize === 'undefined') {
      e.attr.originalSize = e.size || 1;
    }
    
    // Get the actual source and target IDs from the edge
    let sourceId, targetId;
    
    // Handle source ID extraction
    if (typeof e.source === 'object' && e.source !== null) {
      sourceId = e.source.id;
    } else {
      sourceId = String(e.source);
    }
    
    // Handle target ID extraction
    if (typeof e.target === 'object' && e.target !== null) {
      targetId = e.target.id;
    } else {
      targetId = String(e.target);
    }
    
    // For safe comparison, convert nodeId to string as well
    const selectedNodeId = String(nodeId);
    
    // Check if this edge is connected to the selected node
    const isConnected = (sourceId === selectedNodeId || targetId === selectedNodeId);
    
    // Track counts for debugging
    if (isConnected) {
      debugCounts.connected++;
    } else {
      debugCounts.notConnected++;
    }
    
    // Apply different styles based on connection status
    if (isConnected) {
      // For connected edges, keep their original color and just increase size
      const sizeFactor = config.highlighting?.highlightedEdgeSizeFactor ?? 2;
      e.size = (e.attr.originalSize) * sizeFactor;
      // Don't change the color property at all - preserve exactly as is
      // console.log("Edge connected to selected node:", e.id, "Source:", sourceId, "Target:", targetId, "Keeping original color");
    } else {
      // For non-connected edges, use a very light gray that's almost invisible
      // RGBA doesn't seem to work consistently in Sigma.js v0.1
      e.color = '#ededed';  // Very light gray
      e.size = e.attr.originalSize * 0.5;  // Make non-connected edges thinner
    }
  });
  
  console.log("Edge processing complete. Total edges:", edgeCount, "Connected:", debugCounts.connected, "Not connected:", debugCounts.notConnected);
  
  // Force redraw
  sigmaInstance.draw(2, 2, 2, 2);
  
  // Add debug check after redraw to verify edge colors
  setTimeout(function() {
    console.log("Verifying edge colors after redraw:");
    let colorCount = { original: 0, greyed: 0, other: 0 };
    
    sigmaInstance.iterEdges(function(e) {
      if (e.color === '#ededed') {
        colorCount.greyed++;
      } else if (e.attr && e.attr.originalColor && e.color === e.attr.originalColor) {
        colorCount.original++;
      } else {
        colorCount.other++;
      }
    });
    
    console.log("Edge color counts:", colorCount);
  }, 100);
  
  // Show node details panel and populate it
  try {
    $('#attributepane')
      .show()
      .css({
        'display': 'block',
        'visibility': 'visible',
        'opacity': '1'
      });
  
  // Collect neighbor node information for the information panel
  sigmaInstance.iterNodes(function(n) {
    if (neighbors[n.id]) {
      neighbors[n.id].label = n.label || n.id;
      
      // Determine node type using multiple methods
      let nodeType = "unknown";
      
      // Method 1: Direct type property
      if (n.type) {
        nodeType = n.type;
      } 
      // Method 2: Parse from ID if it follows the format type_number
      else if (n.id && n.id.includes('_')) {
        const idParts = n.id.split('_');
        if (idParts.length >= 2 && ['paper', 'author', 'organization'].includes(idParts[0])) {
          nodeType = idParts[0];
        }
      }
      // Method 3: Try to determine from color
      else if (n.color) {
        // Match the color to known node types
        for (const type in config.nodeTypes) {
          if (config.nodeTypes[type].color === n.color) {
            nodeType = type;
            break;
          }
        }
        // Try with default node types if not found in config
        if (nodeType === "unknown") {
          for (const type in nodeTypes) {
            if (nodeTypes[type].color === n.color) {
              nodeType = type;
              break;
            }
          }
        }
      }
      
      neighbors[n.id].type = nodeType;
      neighbors[n.id].color = n.color;
    }
  });
  
  // Populate connection list
  var connectionList = [];
  // Group neighbors by type
  var neighborsByType = {};
  
  for (var id in neighbors) {
    var neighbor = neighbors[id];
    if (neighbor) {
      // Initialize array for this type if it doesn't exist
      neighborsByType[neighbor.type] = neighborsByType[neighbor.type] || [];
      // Add this neighbor to its type group
      neighborsByType[neighbor.type].push({
        id: id,
        label: neighbor.label || id,
        color: neighbor.color
      });
    }
  }
  
  // For each type, add a header and then list the connections
  // Sort types to have known types first, 'unknown' last
  let sortedTypes = Object.keys(neighborsByType).sort((a, b) => {
    if (a === 'unknown') return 1;
    if (b === 'unknown') return -1;
    return a.localeCompare(b);
  });
  
  sortedTypes.forEach(function(type) {
    // Get the color for this type from config
    var typeColor = config.nodeTypes && config.nodeTypes[type] ? 
                   config.nodeTypes[type].color : 
                   nodeTypes[type]?.color || '#666';
    
    // Debug connection types
    console.log(`Found ${neighborsByType[type].length} neighbors of type: ${type}`);
    
    // Get a readable type name
    let typeName = type;
    if (type === 'unknown') {
      typeName = 'Other Connections';
    }
    
    // Add a header for this type with appropriate styling
    connectionList.push('<li class="connection-type-header" style="margin-top: 8px; margin-bottom: 5px; font-weight: bold; color: ' + typeColor + ';">' + 
                       (type === 'unknown' ? typeName : typeName.charAt(0).toUpperCase() + typeName.slice(1) + 's') + 
                       ' (' + neighborsByType[type].length + '):</li>');
    
    // Add each connection of this type
    neighborsByType[type].forEach(function(neighbor) {
      // For unknown type connections, try to get a hint from the ID if available
      let labelHint = '';
      if (type === 'unknown' && neighbor.id && neighbor.id.includes('_')) {
        const idParts = neighbor.id.split('_');
        if (idParts.length >= 2) {
          labelHint = ` (${idParts[0]})`;
        }
      }
      
      connectionList.push('<li><a href="#" data-node-id="' + neighbor.id + '" style="color: ' + typeColor + ';">' + 
                         neighbor.label + labelHint + '</a></li>');
    });
  });
    
    // Set the node name/title
    $('.nodeattributes .name').text(selected.label || selected.id);
    
    // Debug the node object to see what fields are available
    console.log("Selected node:", selected);
    console.log("Node properties:");
    for (let prop in selected) {
      console.log(`- ${prop}: ${selected[prop]}`);
    }
    
    // Display the node type by parsing the ID
    let nodeType = null;
    
    // Try to parse the node type from the ID (format: type_number)
    if (selected.id && selected.id.includes('_')) {
      const idParts = selected.id.split('_');
      if (idParts.length >= 2) {
        nodeType = idParts[0];
        console.log("Extracted type from ID:", nodeType);
      }
    } 
    // Fallbacks if we couldn't get the type from ID
    else if (selected.type) {
      nodeType = selected.type;
      console.log("Node has type directly:", selected.type);
    } else if (selected.attr && selected.attr.type) {
      nodeType = selected.attr.type;
      console.log("Node has type in attr:", selected.attr.type);
    }
    
    // Format the type nicely - capitalize first letter
    if (nodeType) {
      nodeType = nodeType.charAt(0).toUpperCase() + nodeType.slice(1);
      $('.nodeattributes .nodetype').text('Type: ' + nodeType).show();
    } else {
      $('.nodeattributes .nodetype').hide();
    }
    
    // Simplify data display to only show degree
    let dataHTML = '';
    if (typeof selected.degree !== 'undefined') {
      dataHTML = '<div><strong>Degree:</strong> ' + selected.degree + '</div>';
    }
    
    if (dataHTML === '') dataHTML = '<div>No additional attributes</div>';
    $('.nodeattributes .data').html(dataHTML);
    
    // Build connection list
    $('.nodeattributes .link ul')
      .html(connectionList.length ? connectionList.join('') : '<li>No connections</li>')
      .css('display', 'block');
    
    // Bind click events for neighbor links
    $('.nodeattributes .link ul li a').click(function(e) {
      e.preventDefault();
      var nextNodeId = $(this).data('node-id');
      nodeActive(nextNodeId);
    });
    
  } catch (e) {
    console.error("Error updating attribute pane:", e);
  }
}

// Reset display - without color changes
function nodeNormal() {
  console.log("nodeNormal called");
  
  if (!sigmaInstance || !sigmaInstance.detail) {
    // Not in detail view, nothing to reset
    return;
  }
  
  sigmaInstance.detail = false;
  
  // Restore all original node attributes
  sigmaInstance.iterNodes(function(n) {
    n.attr = n.attr || {};
    
    // Restore original color
    if (typeof n.attr.originalColor !== 'undefined') {
      n.color = n.attr.originalColor;
      delete n.attr.originalColor;
    }
    
    // Restore original size if it was modified
    if (typeof n.attr.originalSize !== 'undefined') {
      n.size = n.attr.originalSize;
      delete n.attr.originalSize;
    }
    
    // When returning to full network, always hide all labels
    // Don't rely on originalForceLabel as it may maintain visibility
    n.forceLabel = false;
    delete n.attr.originalForceLabel;
    
    // Remove dimmed flag
    delete n.attr.dimmed;
  });
  
  // Restore original edge colors
  sigmaInstance.iterEdges(function(e) {
    e.attr = e.attr || {};
    // Restore color with explicit check for undefined
    if (typeof e.attr.originalColor !== 'undefined') {
      e.color = e.attr.originalColor;
      delete e.attr.originalColor;
    }
    // Restore size with explicit check for undefined
    if (typeof e.attr.originalSize !== 'undefined') {
      e.size = e.attr.originalSize;
      delete e.attr.originalSize;
    }
  });
  
  // Reset selected node
  selectedNode = null;
  
  // Hide attribute pane
  $('#attributepane').css({
    'display': 'none',
    'visibility': 'hidden'
  });
  
  // Force redraw
  sigmaInstance.draw(2, 2, 2, 2);
  
  // Ensure edge colors match target nodes after restoring
  try {
    if (typeof forceEdgeColors === 'function') {
      forceEdgeColors();
    }
  } catch (e) {
    console.error("Error refreshing edge colors:", e);
  }
}

// Helper function to convert colors to RGB
function getRGBColor(color) {
  // Handle hex colors
  if (color.charAt(0) === '#') {
    var r = parseInt(color.substr(1, 2), 16);
    var g = parseInt(color.substr(3, 2), 16);
    var b = parseInt(color.substr(5, 2), 16);
    return { r: r, g: g, b: b };
  }
  // Handle rgb colors
  else if (color.startsWith('rgb')) {
    var parts = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
    if (parts) {
      return {
        r: parseInt(parts[1], 10),
        g: parseInt(parts[2], 10),
        b: parseInt(parts[3], 10)
      };
    }
  }
  
  // Default fallback color
  return { r: 100, g: 100, b: 100 };
}

// Search nodes by term
function searchNodes(term) {
  if (!sigmaInstance) return;
  
  let results = [];
  let lowerTerm = term.toLowerCase();
  
  sigmaInstance.iterNodes(function(n) {
    if ((n.label && n.label.toLowerCase().indexOf(lowerTerm) >= 0) || 
        (n.id && n.id.toLowerCase().indexOf(lowerTerm) >= 0)) {
      results.push(n);
    }
  });
  
  // Limit to top 10 results
  results = results.slice(0, 10);
  
  // Display results
  let resultsHTML = '';
  if (results.length > 0) {
    results.forEach(function(n) {
      resultsHTML += '<a href="#" data-node-id="' + n.id + '">' + (n.label || n.id) + '</a>';
    });
  } else {
    resultsHTML = '<div>No results found</div>';
  }
  
  $('.results').html(resultsHTML);
  
  // Set up click event for results
  $('.results a').click(function(e) {
    e.preventDefault();
    let nodeId = $(this).data('node-id');
    nodeActive(nodeId);
  });
}

// Update the legend with node type information
function updateLegend() {
  console.log("Updating legend with node types");
  
  // Use configured node types with fallback to default types
  let typesToShow = config.nodeTypes || nodeTypes;
  
  // Create the HTML for the legend
  let legendHTML = '';
  
  // Make sure we're iterating through the object properties properly
  for (let type in typesToShow) {
    if (typesToShow.hasOwnProperty(type)) {
      let typeConfig = typesToShow[type];
      let color = typeConfig.color || '#ccc';
      
      legendHTML += `<div class="legend-item">
                     <div class="legend-color" style="background-color: ${color};"></div>
                     <div class="legend-label">${type}</div>
                   </div>`;
    }
  }
  
  // Add edge color information - always grey
  legendHTML += `<div class="legend-item">
                 <div class="legend-line" style="background-color: #ccc;"></div>
                 <div class="legend-label">Edge (Solid Grey)</div>
               </div>`;
  
  // Set the HTML
  $('#colorLegend').html(legendHTML);
}