hesamation's picture
Updated footer credits in index.html, refined CSS styles for better layout and responsiveness, and modified JavaScript to ensure edge colors match target nodes. Enhanced search functionality to support Enter key input.
513a8f8
raw
history blame
24.9 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 Paper 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 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').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];
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: 'target', // Use target node color for edges
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");
// 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;
}
// 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) {
neighbors[e.source == nodeId ? e.target : e.source] = true;
}
});
// 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'
});
// 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
var connectionList = [];
for (var id in neighbors) {
var neighborNode = null;
sigmaInstance.iterNodes(function(n) {
if (n.id == id) neighborNode = n;
});
if (neighborNode) {
connectionList.push('<li><a href="#" data-node-id="' + id + '">' + (neighborNode.label || id) + '</a></li>');
}
}
$('.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>`;
}
}
// Set the HTML
$('#colorLegend').html(legendHTML);
}