|
|
|
let sigmaInstance; |
|
let graph; |
|
let filter; |
|
let config = {}; |
|
let greyColor = '#666'; |
|
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 } |
|
}; |
|
|
|
|
|
$(document).ready(function() { |
|
|
|
$.getJSON('config.json', function(data) { |
|
config = data; |
|
document.title = config.title || 'Daily Papers Atlas'; |
|
$('#title').text(config.title || 'Daily Papers Atlas'); |
|
|
|
|
|
|
|
loadGraph(); |
|
}); |
|
|
|
|
|
$('#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); |
|
} |
|
}); |
|
|
|
|
|
$('#zoom .z[rel="in"]').click(function() { |
|
if (sigmaInstance) sigmaInstance.camera.goTo({ratio: sigmaInstance.camera.ratio / 1.5}); |
|
}); |
|
|
|
$('#zoom .z[rel="out"]').click(function() { |
|
if (sigmaInstance) sigmaInstance.camera.goTo({ratio: sigmaInstance.camera.ratio * 1.5}); |
|
}); |
|
|
|
$('#zoom .z[rel="center"]').click(function() { |
|
if (sigmaInstance) sigmaInstance.camera.goTo({x: 0, y: 0, ratio: 1}); |
|
}); |
|
|
|
|
|
$('.returntext').click(function() { |
|
closeAttributePane(); |
|
}); |
|
|
|
|
|
$('#color-attribute').change(function() { |
|
let attr = $(this).val(); |
|
colorNodesByAttribute(attr); |
|
}); |
|
|
|
|
|
$('#filter-select').change(function() { |
|
let filterValue = $(this).val(); |
|
filterByNodeType(filterValue); |
|
}); |
|
}); |
|
|
|
|
|
function loadGraph() { |
|
$.getJSON(config.data, function(data) { |
|
graph = data; |
|
|
|
|
|
sigmaInstance = new sigma({ |
|
graph: graph, |
|
container: 'sigma-canvas', |
|
settings: { |
|
labelThreshold: config.labelThreshold || 7, |
|
minEdgeSize: config.minEdgeSize || 0.5, |
|
maxEdgeSize: config.maxEdgeSize || 2, |
|
minNodeSize: config.minNodeSize || 1, |
|
maxNodeSize: config.maxNodeSize || 8, |
|
drawLabels: config.drawLabels !== false, |
|
defaultLabelColor: config.defaultLabelColor || '#000', |
|
defaultEdgeColor: config.defaultEdgeColor || '#999', |
|
edgeColor: 'default', |
|
defaultNodeColor: config.defaultNodeColor || '#666', |
|
defaultNodeBorderColor: config.defaultNodeBorderColor || '#fff', |
|
borderSize: config.borderSize || 1, |
|
nodeHoverColor: 'default', |
|
defaultNodeHoverColor: config.defaultNodeHoverColor || '#000', |
|
defaultHoverLabelBGColor: config.defaultHoverLabelBGColor || '#fff', |
|
defaultLabelHoverColor: config.defaultLabelHoverColor || '#000', |
|
enableEdgeHovering: config.enableEdgeHovering !== false, |
|
edgeHoverColor: 'edge', |
|
edgeHoverSizeRatio: config.edgeHoverSizeRatio || 1.5, |
|
defaultEdgeHoverColor: config.defaultEdgeHoverColor || '#000', |
|
edgeHoverExtremities: config.edgeHoverExtremities !== false, |
|
batchEdgesDrawing: config.batchEdgesDrawing !== false, |
|
hideEdgesOnMove: config.hideEdgesOnMove !== false, |
|
canvasEdgesBatchSize: config.canvasEdgesBatchSize || 500, |
|
animationsTime: config.animationsTime || 1500 |
|
} |
|
}); |
|
|
|
|
|
if (config.forceAtlas2) { |
|
console.log("Starting ForceAtlas2 layout..."); |
|
sigmaInstance.startForceAtlas2({ |
|
worker: true, |
|
barnesHutOptimize: true, |
|
gravity: 1, |
|
scalingRatio: 2, |
|
slowDown: 10 |
|
}); |
|
|
|
setTimeout(function() { |
|
sigmaInstance.stopForceAtlas2(); |
|
console.log("ForceAtlas2 layout completed"); |
|
sigmaInstance.refresh(); |
|
|
|
|
|
applyNodeStyles(); |
|
|
|
|
|
initFilters(); |
|
|
|
|
|
if (config.defaultColorAttribute) { |
|
$('#color-attribute').val(config.defaultColorAttribute); |
|
colorNodesByAttribute(config.defaultColorAttribute); |
|
} else { |
|
updateColorLegend(nodeTypes); |
|
} |
|
|
|
|
|
bindEvents(); |
|
|
|
}, config.forceAtlas2Time || 5000); |
|
} else { |
|
|
|
applyNodeStyles(); |
|
|
|
|
|
initFilters(); |
|
|
|
|
|
if (config.defaultColorAttribute) { |
|
$('#color-attribute').val(config.defaultColorAttribute); |
|
colorNodesByAttribute(config.defaultColorAttribute); |
|
} else { |
|
updateColorLegend(nodeTypes); |
|
} |
|
|
|
|
|
bindEvents(); |
|
} |
|
}).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.'); |
|
}); |
|
} |
|
|
|
|
|
function applyNodeStyles() { |
|
sigmaInstance.graph.nodes().forEach(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(); |
|
} |
|
|
|
|
|
function initFilters() { |
|
try { |
|
filter = new sigma.plugins.filter(sigmaInstance); |
|
console.log("Filter plugin initialized"); |
|
} catch (e) { |
|
console.error("Error initializing filter plugin:", e); |
|
} |
|
} |
|
|
|
|
|
function filterByNodeType(filterValue) { |
|
if (!filter) return; |
|
|
|
filter.undo('node-type'); |
|
|
|
if (filterValue !== 'all') { |
|
filter.nodesBy(function(n) { |
|
return n.type === filterValue; |
|
}, 'node-type'); |
|
} |
|
|
|
filter.apply(); |
|
sigmaInstance.refresh(); |
|
} |
|
|
|
|
|
function bindEvents() { |
|
|
|
sigmaInstance.bind('clickNode', function(e) { |
|
let node = e.data.node; |
|
selectedNode = node; |
|
displayNodeDetails(node); |
|
}); |
|
|
|
|
|
sigmaInstance.bind('clickStage', function() { |
|
closeAttributePane(); |
|
}); |
|
|
|
|
|
sigmaInstance.bind('hovers', function(e) { |
|
if (!e.data.enter.nodes.length) return; |
|
|
|
let nodeId = e.data.enter.nodes[0]; |
|
let toKeep = sigmaInstance.graph.neighbors(nodeId); |
|
toKeep[nodeId] = e.data.enter.nodes[0]; |
|
|
|
activeState.activeNodes = Object.keys(toKeep); |
|
|
|
sigmaInstance.graph.nodes().forEach(function(n) { |
|
if (toKeep[n.id]) { |
|
n.originalColor = n.color; |
|
n.color = n.color; |
|
} else { |
|
n.originalColor = n.originalColor || n.color; |
|
n.color = greyColor; |
|
} |
|
}); |
|
|
|
sigmaInstance.graph.edges().forEach(function(e) { |
|
if (toKeep[e.source] && toKeep[e.target]) { |
|
e.originalColor = e.color; |
|
e.color = e.color; |
|
} else { |
|
e.originalColor = e.originalColor || e.color; |
|
e.color = greyColor; |
|
} |
|
}); |
|
|
|
sigmaInstance.refresh(); |
|
}); |
|
|
|
sigmaInstance.bind('hovers', function(e) { |
|
if (e.data.leave.nodes.length && !selectedNode) { |
|
sigmaInstance.graph.nodes().forEach(function(n) { |
|
n.color = n.originalColor || n.color; |
|
}); |
|
|
|
sigmaInstance.graph.edges().forEach(function(e) { |
|
e.color = e.originalColor || e.color; |
|
}); |
|
|
|
sigmaInstance.refresh(); |
|
} |
|
}); |
|
} |
|
|
|
|
|
function displayNodeDetails(node) { |
|
$('#attributepane').show(); |
|
$('.nodeattributes .name').text(node.label || node.id); |
|
|
|
let dataHTML = ''; |
|
for (let attr in node) { |
|
if (attr !== 'id' && attr !== 'x' && attr !== 'y' && attr !== 'size' && attr !== 'color' && |
|
attr !== 'label' && attr !== 'originalColor' && attr !== 'hidden') { |
|
dataHTML += '<div><strong>' + attr + ':</strong> ' + node[attr] + '</div>'; |
|
} |
|
} |
|
$('.nodeattributes .data').html(dataHTML); |
|
|
|
|
|
let neighbors = sigmaInstance.graph.neighbors(node.id); |
|
let linksHTML = ''; |
|
for (let id in neighbors) { |
|
let neighbor = sigmaInstance.graph.nodes(id); |
|
if (neighbor) { |
|
linksHTML += '<li><a href="#" data-node-id="' + id + '">' + (neighbor.label || id) + '</a></li>'; |
|
} |
|
} |
|
$('.nodeattributes .link ul').html(linksHTML); |
|
|
|
|
|
$('.nodeattributes .link ul li a').click(function(e) { |
|
e.preventDefault(); |
|
let nodeId = $(this).data('node-id'); |
|
let node = sigmaInstance.graph.nodes(nodeId); |
|
if (node) { |
|
selectedNode = node; |
|
displayNodeDetails(node); |
|
sigmaInstance.renderers[0].dispatchEvent('clickNode', { |
|
node: node |
|
}); |
|
} |
|
}); |
|
} |
|
|
|
|
|
function closeAttributePane() { |
|
$('#attributepane').hide(); |
|
selectedNode = null; |
|
|
|
|
|
sigmaInstance.graph.nodes().forEach(function(n) { |
|
n.color = n.originalColor || n.color; |
|
}); |
|
|
|
sigmaInstance.graph.edges().forEach(function(e) { |
|
e.color = e.originalColor || e.color; |
|
}); |
|
|
|
sigmaInstance.refresh(); |
|
} |
|
|
|
|
|
function colorNodesByAttribute(attribute) { |
|
|
|
let values = {}; |
|
let valueCount = 0; |
|
|
|
sigmaInstance.graph.nodes().forEach(function(n) { |
|
let value = n[attribute] || 'unknown'; |
|
if (!values[value]) { |
|
values[value] = true; |
|
valueCount++; |
|
} |
|
}); |
|
|
|
|
|
let valueColors = {}; |
|
let i = 0; |
|
let palette = config.colorPalette || colors; |
|
|
|
for (let value in values) { |
|
valueColors[value] = palette[i % palette.length]; |
|
i++; |
|
} |
|
|
|
|
|
sigmaInstance.graph.nodes().forEach(function(n) { |
|
let value = n[attribute] || 'unknown'; |
|
n.originalColor = valueColors[value]; |
|
if (!selectedNode) { |
|
n.color = valueColors[value]; |
|
} |
|
}); |
|
|
|
sigmaInstance.refresh(); |
|
|
|
|
|
updateColorLegend(valueColors); |
|
} |
|
|
|
|
|
function updateColorLegend(valueColors) { |
|
let legendHTML = ''; |
|
|
|
for (let value in valueColors) { |
|
let color = valueColors[value]; |
|
if (typeof color === 'object') { |
|
color = color.color; |
|
} |
|
legendHTML += '<div class="legenditem"><span class="legendcolor" style="background-color: ' + color + '"></span>' + value + '</div>'; |
|
} |
|
|
|
$('#colorLegend').html(legendHTML); |
|
} |
|
|
|
|
|
function searchNodes(term) { |
|
let results = []; |
|
let lowerTerm = term.toLowerCase(); |
|
|
|
sigmaInstance.graph.nodes().forEach(function(n) { |
|
if ((n.label && n.label.toLowerCase().indexOf(lowerTerm) >= 0) || |
|
(n.id && n.id.toLowerCase().indexOf(lowerTerm) >= 0)) { |
|
results.push(n); |
|
} |
|
}); |
|
|
|
|
|
results = results.slice(0, 10); |
|
|
|
|
|
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); |
|
|
|
|
|
$('.results a').click(function(e) { |
|
e.preventDefault(); |
|
let nodeId = $(this).data('node-id'); |
|
let node = sigmaInstance.graph.nodes(nodeId); |
|
if (node) { |
|
|
|
sigmaInstance.camera.goTo({ |
|
x: node[sigmaInstance.camera.readPrefix + 'x'], |
|
y: node[sigmaInstance.camera.readPrefix + 'y'], |
|
ratio: 0.5 |
|
}); |
|
|
|
|
|
selectedNode = node; |
|
displayNodeDetails(node); |
|
} |
|
}); |
|
} |