// Global variables let sigmaInstance; let graph; let filter; let config = {}; let greyColor = '#ccc'; let activeState = { activeNodes: [], activeEdges: [] }; 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, checking Sigma.js availability"); if (typeof sigma === 'undefined') { console.error("Sigma.js is not loaded!"); return; } console.log("Sigma.js version:", sigma.version); // 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 Paper Atlas'; $('#title').text(config.text.title || 'Daily Paper Atlas'); $('#titletext').text(config.text.intro || ''); loadGraph(); }).fail(function(jqXHR, textStatus, errorThrown) { console.error("Failed to load config:", textStatus, errorThrown); }); // Set up search functionality $('#search-input').keyup(function(e) { let searchTerm = $(this).val(); if (searchTerm.length > 2) { searchNodes(searchTerm); } else { $('.results').empty(); } }); $('#search-button').click(function() { let searchTerm = $('#search-input').val(); if (searchTerm.length > 2) { 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 color selector $('#color-attribute').change(function() { let attr = $(this).val(); colorNodesByAttribute(attr); }); // Set up filter selector $('#filter-select').change(function() { let filterValue = $(this).val(); filterByNodeType(filterValue); }); // Call updateLegend immediately to ensure it runs setTimeout(function() { console.log("Forcing legend update from document ready"); 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]; 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 }); } // 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, color: edge.color || '#ccc' }); } // Configure drawing properties sigmaInstance.drawingProperties({ labelThreshold: config.sigma?.drawingProperties?.labelThreshold || 8, 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: 'target', defaultEdgeColor: '#ccc' }); // 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"); // Bind events console.log("Binding 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 { 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; } }); 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 function bindEvents() { if (!sigmaInstance) { console.error("Sigma instance not found when binding events"); return; } console.log("Starting to bind sigma events..."); try { // When a node is clicked, display its details sigmaInstance.bind('clickNode', function(e) { console.log("Node clicked!", e); if (!e || !e.data || !e.data.node) { console.error("Click event missing node data"); return; } var node = e.data.node; console.log("Clicked node:", node); if (e.data.captor.isDragging) { console.log("Ignoring click while dragging"); return; } nodeActive(node.id); }); // When stage is clicked, close the attribute pane sigmaInstance.bind('clickStage', function(e) { console.log("Stage clicked!", e); if (!e.data.node) { nodeNormal(); } }); // Add direct DOM click handler as backup document.getElementById('sigma-canvas').addEventListener('click', function(e) { console.log("Direct canvas click detected", e); }); // Highlight connected nodes on hover sigmaInstance.bind('overNode', function(e) { // --- Completely disable hover effects when a node is selected --- if (sigmaInstance.detail) { return; } var node = e.data.node; var nodeId = node.id; console.log("Node hover enter:", nodeId); var neighbors = {}; sigmaInstance.iterEdges(function(edge) { if (edge.source == nodeId || edge.target == nodeId) { neighbors[edge.source == nodeId ? edge.target : edge.source] = true; } }); sigmaInstance.iterNodes(function(n) { // Store original color only if not already stored if (n.originalColor === undefined) n.originalColor = n.color; if (n.id != nodeId && !neighbors[n.id]) { n.color = greyColor; } }); sigmaInstance.iterEdges(function(edge) { // Store original color only if not already stored if (edge.originalColor === undefined) edge.originalColor = edge.color; if (edge.source != nodeId && edge.target != nodeId) { edge.color = greyColor; } }); sigmaInstance.refresh(); }); sigmaInstance.bind('outNode', function(e) { // --- Completely disable hover effects when a node is selected --- if (sigmaInstance.detail) { return; } var node = e.data.node; var nodeId = node.id; console.log("Node hover leave:", nodeId); // Restore original colors and clean up sigmaInstance.iterNodes(function(n) { if (n.originalColor !== undefined) { n.color = n.originalColor; delete n.originalColor; } }); sigmaInstance.iterEdges(function(e_edge) { if (e_edge.originalColor !== undefined) { e_edge.color = e_edge.originalColor; delete e_edge.originalColor; } }); sigmaInstance.refresh(); }); console.log("Event binding completed successfully"); } catch (e) { console.error("Error in bindEvents:", e); } } // Display node details (used when a node is clicked) function nodeActive(nodeId) { console.log("nodeActive called with id:", nodeId); if (!sigmaInstance) { console.error("Sigma instance not ready for nodeActive"); return; } // Find the selected node var selected = null; sigmaInstance.iterNodes(function(n) { if (n.id == nodeId) { selected = n; console.log("Found selected node:", n); // Store original size if not already stored if (n.originalSize === undefined) n.originalSize = n.size; } }); if (!selected) { console.error("Node not found:", nodeId); return; } console.log("Node found:", selected); sigmaInstance.detail = true; selectedNode = selected; // Find neighbors var neighbors = {}; neighbors[nodeId] = true; // Include the selected node itself sigmaInstance.iterEdges(function(e) { if (e.source == nodeId) { neighbors[e.target] = true; } else if (e.target == nodeId) { neighbors[e.source] = true; } }); var neighborIds = Object.keys(neighbors); console.log("Neighbors found (including self):", neighborIds.length); // Dim non-neighbor nodes and edges sigmaInstance.iterNodes(function(n) { if (neighbors[n.id]) { n.color = n.originalColor || n.color; if (n.id === nodeId) { n.size = (n.originalSize || n.size) * 1.5; } } else { n.color = greyColor; } }); sigmaInstance.iterEdges(function(e) { if (neighbors[e.source] && neighbors[e.target]) { e.color = e.originalColor || e.color; } else { e.color = greyColor; } }); // Show node details panel try { console.log("Displaying attribute pane"); $('#attributepane') .show() .css({ 'display': 'block', 'visibility': 'visible', 'opacity': '1' }); $('.nodeattributes .name').text(selected.label || selected.id); let dataHTML = ''; for (let attr in selected) { if (attr !== 'id' && attr !== 'x' && attr !== 'y' && attr !== 'size' && attr !== 'color' && attr !== 'label' && attr !== 'originalColor' && attr !== 'originalSize' && attr !== 'hidden' && typeof selected[attr] !== 'function' && attr !== 'displayX' && attr !== 'displayY' && attr !== 'displaySize' && !attr.startsWith('_')) { dataHTML += '