// 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('
  • ' + (type === 'unknown' ? typeName : typeName.charAt(0).toUpperCase() + typeName.slice(1) + 's') + ' (' + neighborsByType[type].length + '):
  • '); // 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('
  • ' + neighbor.label + labelHint + '
  • '); }); }); // 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 = '
    Degree: ' + selected.degree + '
    '; } if (dataHTML === '') dataHTML = '
    No additional attributes
    '; $('.nodeattributes .data').html(dataHTML); // Build connection list $('.nodeattributes .link ul') .html(connectionList.length ? connectionList.join('') : '
  • No connections
  • ') .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 += '' + (n.label || n.id) + ''; }); } else { resultsHTML = '
    No results found
    '; } $('.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 += `
    ${type}
    `; } } // Add edge color information - always grey legendHTML += `
    Edge (Solid Grey)
    `; // Set the HTML $('#colorLegend').html(legendHTML); }