Download js/main.js from hesamation/Daily-Papers-Atlas: direct link, hf CLI and curl.
- Browser
- Download file 30.1 kB
-
https://huggingface.co/spaces/hesamation/Daily-Papers-Atlas/resolve/main/js/main.js
- Command line
-
hf download hf://spaces/hesamation/Daily-Papers-Atlas/js/main.js
-
curl -L -o main.js https://huggingface.co/spaces/hesamation/Daily-Papers-Atlas/resolve/main/js/main.js
30.1 kB
| // 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); | |
| } |