|
|
|
let svg = d3.select("#container").append("svg")
|
|
.attr("width", "100%")
|
|
.attr("height", "100%"),
|
|
width = window.innerWidth,
|
|
height = window.innerHeight,
|
|
mainGroup = svg.append("g").attr("class", "main-container"),
|
|
simulation = null,
|
|
linkDistance = 80,
|
|
chargeStrength = -120,
|
|
gravityStrength = 0.1;
|
|
|
|
|
|
let mainNodeScale = 1.0;
|
|
let bridgeNodeScale = 1.0;
|
|
let normalNodeScale = 1.0;
|
|
|
|
|
|
let originalData = null;
|
|
let selectedNode = null;
|
|
let nodes = [], links = [];
|
|
|
|
|
|
svg.append("defs").append("marker")
|
|
.attr("id", "arrow")
|
|
.attr("viewBox", "0 -5 10 10")
|
|
.attr("refX", 20)
|
|
.attr("refY", 0)
|
|
.attr("markerWidth", 6)
|
|
.attr("markerHeight", 6)
|
|
.attr("orient", "auto")
|
|
.append("path")
|
|
.attr("d", "M0,-5L10,0L0,5")
|
|
.attr("fill", "#999");
|
|
|
|
|
|
svg.append("defs")
|
|
.append("path")
|
|
.attr("id", "star")
|
|
.attr("d", d3.symbol().type(d3.symbolStar).size(300)());
|
|
|
|
|
|
let rafId = null;
|
|
let isSimulationRunning = false;
|
|
|
|
function startSimulationLoop() {
|
|
if (!isSimulationRunning && simulation) {
|
|
isSimulationRunning = true;
|
|
rafId = requestAnimationFrame(simulationTick);
|
|
}
|
|
}
|
|
|
|
function stopSimulationLoop() {
|
|
isSimulationRunning = false;
|
|
if (rafId !== null) {
|
|
cancelAnimationFrame(rafId);
|
|
rafId = null;
|
|
}
|
|
}
|
|
|
|
|
|
function simulationTick() {
|
|
if (isSimulationRunning && simulation && simulation.alpha() > 0.001) {
|
|
simulation.tick();
|
|
updateVisualElements();
|
|
rafId = requestAnimationFrame(simulationTick);
|
|
} else {
|
|
stopSimulationLoop();
|
|
}
|
|
}
|
|
|
|
|
|
function updateVisualElements() {
|
|
|
|
const linkElements = mainGroup.selectAll(".link");
|
|
const nodeElements = mainGroup.selectAll(".node");
|
|
|
|
|
|
linkElements.each(function(d) {
|
|
d3.select(this)
|
|
.attr("x1", d.source.x)
|
|
.attr("y1", d.source.y)
|
|
.attr("x2", d.target.x)
|
|
.attr("y2", d.target.y);
|
|
});
|
|
|
|
|
|
nodeElements.attr("transform", d => `translate(${d.x}, ${d.y})`);
|
|
}
|
|
|
|
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function(...args) {
|
|
const context = this;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => func.apply(context, args), wait);
|
|
};
|
|
}
|
|
|
|
|
|
const nodeRadiusCache = new Map();
|
|
|
|
|
|
d3.select("#linkDistanceSlider").on("input", debounce(function() {
|
|
linkDistance = +this.value;
|
|
d3.select("#linkDistanceValue").text(linkDistance);
|
|
if (simulation) {
|
|
simulation.force("link").distance(linkDistance);
|
|
simulation.alpha(0.3).restart();
|
|
startSimulationLoop();
|
|
}
|
|
}, 50));
|
|
|
|
d3.select("#chargeSlider").on("input", debounce(function() {
|
|
chargeStrength = +this.value;
|
|
d3.select("#chargeValue").text(chargeStrength);
|
|
if (simulation) {
|
|
simulation.force("charge").strength(chargeStrength);
|
|
simulation.alpha(0.3).restart();
|
|
startSimulationLoop();
|
|
}
|
|
}, 50));
|
|
|
|
d3.select("#gravitySlider").on("input", debounce(function() {
|
|
gravityStrength = +this.value;
|
|
d3.select("#gravityValue").text(gravityStrength);
|
|
if (simulation) {
|
|
simulation.force("gravity", d3.forceRadial(0, width / 2, height / 2).strength(gravityStrength));
|
|
simulation.alpha(0.3).restart();
|
|
startSimulationLoop();
|
|
}
|
|
}, 50));
|
|
|
|
|
|
d3.select("#closePanelButton").on("click", function() {
|
|
closeDetailsPanel();
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let allPapers = [];
|
|
let currentTimeIndex = 0;
|
|
let timeChunks = [];
|
|
let isLoadingTimeChunk = false;
|
|
|
|
|
|
d3.select("#prevTimeButton").on("click", function() {
|
|
if (currentTimeIndex > 0 && !isLoadingTimeChunk) {
|
|
currentTimeIndex--;
|
|
loadTimeChunk(currentTimeIndex);
|
|
updateTimeControls();
|
|
}
|
|
});
|
|
|
|
d3.select("#nextTimeButton").on("click", function() {
|
|
if (currentTimeIndex < timeChunks.length - 1 && !isLoadingTimeChunk) {
|
|
currentTimeIndex++;
|
|
loadTimeChunk(currentTimeIndex);
|
|
updateTimeControls();
|
|
}
|
|
});
|
|
|
|
d3.select("#timeSlider").on("input", function() {
|
|
if (!isLoadingTimeChunk) {
|
|
currentTimeIndex = +this.value;
|
|
loadTimeChunk(currentTimeIndex);
|
|
updateTimeControls();
|
|
}
|
|
});
|
|
|
|
|
|
function updateTimeControls() {
|
|
|
|
d3.select("#prevTimeButton").property("disabled", currentTimeIndex === 0);
|
|
d3.select("#nextTimeButton").property("disabled", currentTimeIndex === timeChunks.length - 1);
|
|
|
|
|
|
d3.select("#timeSlider")
|
|
.property("value", currentTimeIndex)
|
|
.property("max", timeChunks.length - 1);
|
|
|
|
|
|
if (timeChunks.length > 0) {
|
|
const currentChunk = timeChunks[currentTimeIndex];
|
|
d3.select("#currentTimePeriod")
|
|
.text(`${formatDate(currentChunk.startDate)} - ${formatDate(currentChunk.endDate)}`);
|
|
|
|
|
|
if (timeChunks.length > 0) {
|
|
d3.select("#startTimeLabel").text(formatDate(timeChunks[0].startDate));
|
|
d3.select("#endTimeLabel").text(formatDate(timeChunks[timeChunks.length - 1].endDate));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeDetailsPanel() {
|
|
d3.select("#paperDetailsPanel").style("display", "none");
|
|
|
|
|
|
if (selectedNode) {
|
|
d3.selectAll(".node").classed("selected", false);
|
|
selectedNode = null;
|
|
}
|
|
}
|
|
|
|
|
|
function updateNodeSizes() {
|
|
if (!simulation) return;
|
|
|
|
|
|
|
|
const updates = [];
|
|
|
|
|
|
mainGroup.selectAll(".node").each(function(d) {
|
|
const element = d3.select(this);
|
|
const radius = getNodeRadius(d);
|
|
|
|
updates.push({
|
|
element,
|
|
d,
|
|
radius
|
|
});
|
|
});
|
|
|
|
|
|
updates.forEach(update => {
|
|
const { element, d, radius } = update;
|
|
|
|
if (d.type === 'main' && d.isBridge) {
|
|
|
|
const starSize = radius * radius * 3;
|
|
element.select("path")
|
|
.attr("d", d3.symbol().type(d3.symbolStar).size(starSize)());
|
|
} else if (d.isBridge) {
|
|
|
|
element.select("rect")
|
|
.attr("width", radius * 2)
|
|
.attr("height", radius * 2)
|
|
.attr("transform", `rotate(45) translate(${-radius}, ${-radius})`);
|
|
} else {
|
|
|
|
element.select("circle")
|
|
.attr("r", radius);
|
|
}
|
|
});
|
|
|
|
|
|
simulation.force("collide").radius(d => getNodeRadius(d) * 1.5);
|
|
|
|
|
|
simulation.alpha(0.1).restart();
|
|
startSimulationLoop();
|
|
}
|
|
|
|
|
|
const debouncedUpdateNodeSizes = debounce(updateNodeSizes, 200);
|
|
|
|
|
|
d3.select("#mainNodeScaleSlider").on("input", debounce(function() {
|
|
mainNodeScale = +this.value;
|
|
d3.select("#mainNodeScaleValue").text(mainNodeScale.toFixed(1));
|
|
nodeRadiusCache.clear();
|
|
debouncedUpdateNodeSizes();
|
|
}, 100));
|
|
|
|
d3.select("#bridgeNodeScaleSlider").on("input", debounce(function() {
|
|
bridgeNodeScale = +this.value;
|
|
d3.select("#bridgeNodeScaleValue").text(bridgeNodeScale.toFixed(1));
|
|
nodeRadiusCache.clear();
|
|
debouncedUpdateNodeSizes();
|
|
}, 100));
|
|
|
|
d3.select("#normalNodeScaleSlider").on("input", debounce(function() {
|
|
normalNodeScale = +this.value;
|
|
d3.select("#normalNodeScaleValue").text(normalNodeScale.toFixed(1));
|
|
nodeRadiusCache.clear();
|
|
debouncedUpdateNodeSizes();
|
|
}, 100));
|
|
|
|
d3.select("#resetButton").on("click", function() {
|
|
if (simulation) {
|
|
|
|
simulation.nodes().forEach(node => {
|
|
node.x = width / 2 + (Math.random() - 0.5) * 100;
|
|
node.y = height / 2 + (Math.random() - 0.5) * 100;
|
|
node.vx = 0;
|
|
node.vy = 0;
|
|
});
|
|
simulation.alpha(1).restart();
|
|
startSimulationLoop();
|
|
}
|
|
});
|
|
|
|
|
|
const zoom = d3.zoom()
|
|
.scaleExtent([0.1, 8])
|
|
.on("zoom", (event) => {
|
|
|
|
mainGroup.attr("transform", event.transform);
|
|
});
|
|
|
|
svg.call(zoom);
|
|
|
|
d3.select("#centerViewButton").on("click", function() {
|
|
|
|
svg.transition()
|
|
.duration(750)
|
|
.call(zoom.transform, d3.zoomIdentity.translate(width/2, height/2).scale(1));
|
|
});
|
|
|
|
|
|
const handleResize = debounce(function() {
|
|
width = window.innerWidth;
|
|
height = window.innerHeight;
|
|
svg.attr("width", width).attr("height", height);
|
|
if (simulation) {
|
|
simulation.force("center", d3.forceCenter(width / 2, height / 2));
|
|
simulation.alpha(0.3).restart();
|
|
startSimulationLoop();
|
|
}
|
|
}, 100);
|
|
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
|
|
async function loadCitationData() {
|
|
const progressBar = d3.select("#loadingProgress");
|
|
const statusMessage = d3.select("#statusMessage");
|
|
|
|
try {
|
|
statusMessage.text("Fetching citation data...");
|
|
progressBar.style("width", "0%");
|
|
|
|
|
|
const interval = setInterval(() => {
|
|
const currentWidth = parseFloat(progressBar.style("width")) || 0;
|
|
if (currentWidth < 90) {
|
|
progressBar.style("width", `${currentWidth + 10}%`);
|
|
}
|
|
}, 200);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
clearInterval(interval);
|
|
progressBar.style("width", "100%");
|
|
|
|
statusMessage.text("Processing data...");
|
|
|
|
const dataFromBackend = window.__INITIAL_DATA__;
|
|
|
|
|
|
console.log(dataFromBackend , " here data")
|
|
allPapers=dataFromBackend
|
|
|
|
|
|
processTimeChunks(allPapers);
|
|
|
|
|
|
if (timeChunks.length > 0) {
|
|
loadTimeChunk(0);
|
|
updateTimeControls();
|
|
}
|
|
|
|
statusMessage.text("Citation network loaded successfully!");
|
|
setTimeout(() => {
|
|
progressBar.style("display", "none");
|
|
statusMessage.style("display", "none");
|
|
d3.select(".loading-section").style("display", "none");
|
|
}, 1000);
|
|
|
|
} catch (error) {
|
|
console.error("Error loading citation data:", error);
|
|
statusMessage.text("Failed to load citation network.");
|
|
progressBar.style("width", "0%");
|
|
}
|
|
}
|
|
|
|
|
|
function processTimeChunks(papers) {
|
|
|
|
papers.sort((a, b) => {
|
|
const dateA = a.publication_date ? new Date(a.publication_date) : new Date(0);
|
|
const dateB = b.publication_date ? new Date(b.publication_date) : new Date(0);
|
|
return dateA - dateB;
|
|
});
|
|
|
|
|
|
let minDate = new Date();
|
|
let maxDate = new Date(0);
|
|
|
|
papers.forEach(paper => {
|
|
if (paper.publication_date) {
|
|
const date = new Date(paper.publication_date);
|
|
if (date < minDate) minDate = new Date(date);
|
|
if (date > maxDate) maxDate = new Date(date);
|
|
}
|
|
});
|
|
|
|
|
|
const chunks = [];
|
|
let currentStart = new Date(minDate);
|
|
|
|
while (currentStart < maxDate) {
|
|
|
|
const endDate = new Date(currentStart);
|
|
endDate.setMonth(endDate.getMonth() + 3);
|
|
|
|
|
|
chunks.push({
|
|
startDate: new Date(currentStart),
|
|
endDate: new Date(endDate),
|
|
papers: []
|
|
});
|
|
|
|
|
|
currentStart = new Date(endDate);
|
|
}
|
|
|
|
|
|
papers.forEach(paper => {
|
|
if (paper.publication_date) {
|
|
const pubDate = new Date(paper.publication_date);
|
|
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
const chunk = chunks[i];
|
|
if (pubDate >= chunk.startDate && pubDate < chunk.endDate) {
|
|
chunk.papers.push(paper);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
timeChunks = chunks.filter(chunk => chunk.papers.length > 0);
|
|
|
|
|
|
d3.select("#timeSlider").property("max", timeChunks.length - 1);
|
|
}
|
|
|
|
|
|
function loadTimeChunk(index) {
|
|
if (isLoadingTimeChunk || index < 0 || index >= timeChunks.length) return;
|
|
|
|
isLoadingTimeChunk = true;
|
|
|
|
|
|
d3.select("#timeLoader").style("width", "0%");
|
|
|
|
|
|
const loadingInterval = setInterval(() => {
|
|
const currentWidth = parseFloat(d3.select("#timeLoader").style("width")) || 0;
|
|
if (currentWidth < 90) {
|
|
d3.select("#timeLoader").style("width", `${currentWidth + 10}%`);
|
|
}
|
|
}, 50);
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
const chunkData = timeChunks[index].papers;
|
|
processData(chunkData);
|
|
|
|
|
|
clearInterval(loadingInterval);
|
|
d3.select("#timeLoader").style("width", "100%");
|
|
|
|
|
|
setTimeout(() => {
|
|
d3.select("#timeLoader").style("width", "0%");
|
|
isLoadingTimeChunk = false;
|
|
}, 500);
|
|
|
|
}, 100);
|
|
}
|
|
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
loadCitationData();
|
|
});
|
|
|
|
function processData(data) {
|
|
|
|
if (simulation) {
|
|
simulation.stop();
|
|
stopSimulationLoop();
|
|
}
|
|
|
|
|
|
mainGroup.selectAll("*").remove();
|
|
svg.select("defs").remove();
|
|
|
|
|
|
closeDetailsPanel();
|
|
|
|
|
|
svg.append("defs").append("marker")
|
|
.attr("id", "arrow")
|
|
.attr("viewBox", "0 -5 10 10")
|
|
.attr("refX", 20)
|
|
.attr("refY", 0)
|
|
.attr("markerWidth", 6)
|
|
.attr("markerHeight", 6)
|
|
.attr("orient", "auto")
|
|
.append("path")
|
|
.attr("d", "M0,-5L10,0L0,5")
|
|
.attr("fill", "#999");
|
|
|
|
svg.append("defs")
|
|
.append("path")
|
|
.attr("id", "star")
|
|
.attr("d", d3.symbol().type(d3.symbolStar).size(300)());
|
|
|
|
|
|
nodeRadiusCache.clear();
|
|
|
|
|
|
nodes = [];
|
|
links = [];
|
|
const nodeMap = new Map();
|
|
|
|
|
|
const paperOccurrences = new Map();
|
|
|
|
|
|
|
|
data.forEach(paper => {
|
|
|
|
incrementPaperCount(paperOccurrences, paper.id);
|
|
|
|
|
|
if (paper.referenced_works) {
|
|
paper.referenced_works.forEach(ref => {
|
|
incrementPaperCount(paperOccurrences, ref);
|
|
});
|
|
}
|
|
|
|
|
|
if (paper.cited_by_ids) {
|
|
paper.cited_by_ids.forEach(cite => {
|
|
incrementPaperCount(paperOccurrences, cite);
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
let estimatedNodeCount = data.length;
|
|
let estimatedLinkCount = 0;
|
|
|
|
data.forEach(paper => {
|
|
if (paper.referenced_works) estimatedNodeCount += paper.referenced_works.length;
|
|
if (paper.cited_by_ids) estimatedNodeCount += paper.cited_by_ids.length;
|
|
|
|
if (paper.referenced_works) estimatedLinkCount += paper.referenced_works.length;
|
|
if (paper.cited_by_ids) estimatedLinkCount += paper.cited_by_ids.length;
|
|
});
|
|
|
|
|
|
nodes = new Array(estimatedNodeCount);
|
|
links = new Array(estimatedLinkCount);
|
|
let nodeIndex = 0;
|
|
let linkIndex = 0;
|
|
|
|
|
|
data.forEach(paper => {
|
|
|
|
if (!nodeMap.has(paper.id)) {
|
|
const node = {
|
|
id: paper.id,
|
|
title: paper.title || "Unknown Title",
|
|
cited_by_count: paper.cited_by_count || 0,
|
|
concepts: paper.concepts || [],
|
|
publication_date: paper.publication_date || null,
|
|
referenced_works: paper.referenced_works || [],
|
|
cited_by_ids: paper.cited_by_ids || [],
|
|
type: 'main',
|
|
isBridge: paperOccurrences.get(paper.id) > 1,
|
|
|
|
x: width / 2 + (Math.random() - 0.5) * 100,
|
|
y: height / 2 + (Math.random() - 0.5) * 100
|
|
};
|
|
nodes[nodeIndex++] = node;
|
|
nodeMap.set(paper.id, node);
|
|
}
|
|
|
|
|
|
if (paper.referenced_works) {
|
|
paper.referenced_works.forEach(ref => {
|
|
if (!nodeMap.has(ref)) {
|
|
const node = {
|
|
id: ref,
|
|
title: "Referenced Paper",
|
|
type: 'reference',
|
|
isBridge: paperOccurrences.get(ref) > 1,
|
|
|
|
x: nodeMap.get(paper.id).x + (Math.random() - 0.5) * 50,
|
|
y: nodeMap.get(paper.id).y + (Math.random() - 0.5) * 50
|
|
};
|
|
nodes[nodeIndex++] = node;
|
|
nodeMap.set(ref, node);
|
|
}
|
|
|
|
links[linkIndex++] = {
|
|
source: paper.id,
|
|
target: ref,
|
|
direction: 'outgoing'
|
|
};
|
|
});
|
|
}
|
|
|
|
|
|
if (paper.cited_by_ids) {
|
|
paper.cited_by_ids.forEach(cite => {
|
|
if (!nodeMap.has(cite)) {
|
|
const node = {
|
|
id: cite,
|
|
title: "Citing Paper",
|
|
type: 'citation',
|
|
isBridge: paperOccurrences.get(cite) > 1,
|
|
|
|
x: nodeMap.get(paper.id).x + (Math.random() - 0.5) * 50,
|
|
y: nodeMap.get(paper.id).y + (Math.random() - 0.5) * 50
|
|
};
|
|
nodes[nodeIndex++] = node;
|
|
nodeMap.set(cite, node);
|
|
}
|
|
|
|
links[linkIndex++] = {
|
|
source: cite,
|
|
target: paper.id,
|
|
direction: 'incoming'
|
|
};
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
nodes = nodes.slice(0, nodeIndex);
|
|
links = links.slice(0, linkIndex);
|
|
|
|
createVisualization(nodes, links);
|
|
}
|
|
|
|
function incrementPaperCount(map, id) {
|
|
if (!map.has(id)) {
|
|
map.set(id, 1);
|
|
} else {
|
|
map.set(id, map.get(id) + 1);
|
|
}
|
|
}
|
|
|
|
function createVisualization(nodes, links) {
|
|
|
|
simulation = d3.forceSimulation(nodes)
|
|
.force("link", d3.forceLink(links).id(d => d.id).distance(linkDistance))
|
|
.force("charge", d3.forceManyBody().strength(chargeStrength).theta(0.8))
|
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
.force("gravity", d3.forceRadial(0, width / 2, height / 2).strength(gravityStrength))
|
|
|
|
.force("collide", d3.forceCollide().radius(d => getNodeRadius(d) * 1.5).strength(0.7));
|
|
|
|
|
|
simulation.alphaDecay(0.02);
|
|
|
|
|
|
simulation.on("tick", null);
|
|
|
|
|
|
const linkGroup = mainGroup.append("g").attr("class", "links");
|
|
|
|
|
|
const link = linkGroup.selectAll("line")
|
|
.data(links)
|
|
.enter().append("line")
|
|
.attr("class", "link")
|
|
.attr("stroke", "#999")
|
|
.attr("stroke-width", 1)
|
|
.attr("marker-end", "url(#arrow)");
|
|
|
|
|
|
const nodeGroup = mainGroup.append("g").attr("class", "nodes");
|
|
|
|
|
|
const node = nodeGroup.selectAll(".node")
|
|
.data(nodes)
|
|
.enter().append("g")
|
|
.attr("class", "node")
|
|
.call(d3.drag()
|
|
.on("start", dragstarted)
|
|
.on("drag", dragged)
|
|
.on("end", dragended)
|
|
)
|
|
.on("click", nodeClicked);
|
|
|
|
|
|
|
|
node.each(function(d) {
|
|
const element = d3.select(this);
|
|
const radius = getNodeRadius(d);
|
|
|
|
if (d.type === 'main' && d.isBridge) {
|
|
|
|
const starSize = radius * radius * 3;
|
|
element.append("path")
|
|
.attr("d", d3.symbol().type(d3.symbolStar).size(starSize)())
|
|
.attr("fill", "#4285f4");
|
|
} else if (d.isBridge) {
|
|
|
|
element.append("rect")
|
|
.attr("width", radius * 2)
|
|
.attr("height", radius * 2)
|
|
.attr("transform", `rotate(45) translate(${-radius}, ${-radius})`)
|
|
.attr("fill", "#fbbc05");
|
|
} else if (d.type === 'main') {
|
|
|
|
element.append("circle")
|
|
.attr("r", radius)
|
|
.attr("fill", "#4285f4");
|
|
} else if (d.type === 'reference') {
|
|
|
|
element.append("circle")
|
|
.attr("r", radius)
|
|
.attr("fill", "#34a853");
|
|
} else if (d.type === 'citation') {
|
|
|
|
element.append("circle")
|
|
.attr("r", radius)
|
|
.attr("fill", "#ea4335");
|
|
} else {
|
|
|
|
element.append("circle")
|
|
.attr("r", radius)
|
|
.attr("fill", "#999");
|
|
}
|
|
});
|
|
|
|
|
|
const tooltip = d3.select(".tooltip");
|
|
|
|
|
|
|
|
nodeGroup.on("mouseover", function(event) {
|
|
const target = event.target;
|
|
if (target.closest(".node")) {
|
|
const d = d3.select(target.closest(".node")).datum();
|
|
|
|
|
|
let tooltipContent = `<strong>${d.title}</strong>`;
|
|
|
|
|
|
if (d.publication_date) {
|
|
tooltipContent += `<br>Published: ${formatDate(d.publication_date)}`;
|
|
}
|
|
|
|
|
|
if (d.cited_by_count) {
|
|
tooltipContent += `<br>Citations: ${d.cited_by_count}`;
|
|
}
|
|
|
|
|
|
if (d.concepts && d.concepts.length > 0) {
|
|
tooltipContent += `<br>Concepts: ${formatConcepts(d.concepts)}`;
|
|
}
|
|
|
|
|
|
if (d.isBridge) {
|
|
tooltipContent += "<br><em>Bridge Paper</em>";
|
|
}
|
|
|
|
|
|
tooltip.style("display", "block")
|
|
.style("left", (event.pageX + 15) + "px")
|
|
.style("top", (event.pageY - 30) + "px")
|
|
.html(tooltipContent);
|
|
}
|
|
})
|
|
.on("mouseout", function(event) {
|
|
const target = event.target;
|
|
if (target.closest(".node")) {
|
|
tooltip.style("display", "none");
|
|
}
|
|
})
|
|
.on("mousemove", function(event) {
|
|
tooltip.style("left", (event.pageX + 15) + "px")
|
|
.style("top", (event.pageY - 30) + "px");
|
|
});
|
|
|
|
|
|
startSimulationLoop();
|
|
}
|
|
|
|
|
|
function getNodeRadius(d) {
|
|
|
|
if (nodeRadiusCache.has(d.id)) {
|
|
return nodeRadiusCache.get(d.id);
|
|
}
|
|
|
|
let radius;
|
|
|
|
if (d.type === 'main') {
|
|
|
|
const baseCitationSize = Math.max(5, Math.min(15, 5 + Math.log(d.cited_by_count + 1)));
|
|
radius = baseCitationSize * mainNodeScale;
|
|
} else if (d.isBridge) {
|
|
|
|
radius = 6 * bridgeNodeScale;
|
|
} else {
|
|
|
|
radius = 4 * normalNodeScale;
|
|
}
|
|
|
|
|
|
nodeRadiusCache.set(d.id, radius);
|
|
return radius;
|
|
}
|
|
|
|
|
|
function dragstarted(event, d) {
|
|
stopSimulationLoop();
|
|
if (!event.active) simulation.alphaTarget(0.3);
|
|
d.fx = d.x;
|
|
d.fy = d.y;
|
|
}
|
|
|
|
function dragged(event, d) {
|
|
d.fx = event.x;
|
|
d.fy = event.y;
|
|
|
|
|
|
d3.select(event.sourceEvent.target.closest(".node"))
|
|
.attr("transform", `translate(${d.fx}, ${d.fy})`);
|
|
|
|
|
|
mainGroup.selectAll(".link").each(function(linkData) {
|
|
if (linkData.source === d || linkData.target === d) {
|
|
d3.select(this)
|
|
.attr("x1", linkData.source.x)
|
|
.attr("y1", linkData.source.y)
|
|
.attr("x2", linkData.target.x)
|
|
.attr("y2", linkData.target.y);
|
|
}
|
|
});
|
|
}
|
|
|
|
function dragended(event, d) {
|
|
if (!event.active) simulation.alphaTarget(0);
|
|
|
|
|
|
|
|
|
|
|
|
startSimulationLoop();
|
|
}
|
|
|
|
|
|
function formatDate(dateString) {
|
|
if (!dateString) return "Unknown";
|
|
|
|
try {
|
|
const date = new Date(dateString);
|
|
|
|
if (isNaN(date.getTime())) return dateString;
|
|
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
} catch (e) {
|
|
return dateString;
|
|
}
|
|
}
|
|
|
|
|
|
function formatConcepts(concepts) {
|
|
if (!concepts || concepts.length === 0) return "None";
|
|
|
|
|
|
const conceptNames = concepts.map(concept => {
|
|
if (typeof concept === 'string') return concept;
|
|
return concept.display_name || concept.name || "Unknown concept";
|
|
});
|
|
|
|
|
|
if (conceptNames.length > 3) {
|
|
return conceptNames.slice(0, 3).join(", ") + ` +${conceptNames.length - 3} more`;
|
|
}
|
|
|
|
return conceptNames.join(", ");
|
|
}
|
|
|
|
|
|
|
|
function nodeClicked(event, d) {
|
|
event.stopPropagation();
|
|
|
|
|
|
selectedNode = d;
|
|
|
|
|
|
d3.selectAll(".node.selected").classed("selected", false);
|
|
d3.select(this).classed("selected", true);
|
|
|
|
|
|
let paperDetails = d;
|
|
if (d.type === 'main' && originalData) {
|
|
paperDetails = originalData.find(paper => paper.id === d.id) || d;
|
|
}
|
|
|
|
|
|
const paperInfoUpdates = {
|
|
"#paperTitle": paperDetails.title || "Unknown Title",
|
|
"#paperCitations": paperDetails.cited_by_count !== undefined ? `Citations: ${paperDetails.cited_by_count}` : ""
|
|
};
|
|
|
|
|
|
if (paperDetails.publication_date) {
|
|
paperInfoUpdates["#paperType"] = `${d.type === 'main' ? "Main Paper" :
|
|
d.type === 'reference' ? "Referenced Paper" :
|
|
d.type === 'citation' ? "Citing Paper" : "Paper"}${d.isBridge ? " (Bridge)" : ""} · Published: ${formatDate(paperDetails.publication_date)}`;
|
|
} else {
|
|
|
|
let typeText = "";
|
|
if (d.type === 'main') typeText = "Main Paper";
|
|
else if (d.type === 'reference') typeText = "Referenced Paper";
|
|
else if (d.type === 'citation') typeText = "Citing Paper";
|
|
|
|
if (d.isBridge) typeText += " (Bridge)";
|
|
paperInfoUpdates["#paperType"] = typeText;
|
|
}
|
|
|
|
|
|
Object.entries(paperInfoUpdates).forEach(([selector, text]) => {
|
|
d3.select(selector).text(text);
|
|
});
|
|
|
|
|
|
const conceptsDiv = d3.select("#paperConcepts");
|
|
conceptsDiv.html("");
|
|
|
|
if (paperDetails.concepts && paperDetails.concepts.length > 0) {
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
|
|
const conceptLabel = document.createElement("div");
|
|
conceptLabel.className = "concept-label";
|
|
conceptLabel.textContent = "Concepts:";
|
|
fragment.appendChild(conceptLabel);
|
|
|
|
paperDetails.concepts.forEach(concept => {
|
|
const span = document.createElement("span");
|
|
span.className = "concept-tag";
|
|
span.textContent = typeof concept === 'string' ? concept : (concept.display_name || concept.name || "Unknown");
|
|
fragment.appendChild(span);
|
|
});
|
|
|
|
conceptsDiv.node().appendChild(fragment);
|
|
}
|
|
|
|
|
|
d3.select("#openAlexLink")
|
|
.attr("href", `${d.id}`);
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
populateRelatedPapersList(paperDetails, "citing");
|
|
populateRelatedPapersList(paperDetails, "referenced");
|
|
|
|
|
|
d3.select("#paperDetailsPanel").style("display", "block");
|
|
}, 0);
|
|
}
|
|
|
|
|
|
function populateRelatedPapersList(paperDetails, listType) {
|
|
const listElement = listType === "citing" ?
|
|
d3.select("#citingPapersList") :
|
|
d3.select("#referencedPapersList");
|
|
|
|
listElement.html("");
|
|
|
|
const paperIds = listType === "citing" ?
|
|
paperDetails.cited_by_ids :
|
|
paperDetails.referenced_works;
|
|
|
|
if (!paperIds || paperIds.length === 0) {
|
|
listElement.append("li")
|
|
.text("No papers available");
|
|
return;
|
|
}
|
|
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
|
|
paperIds.forEach(id => {
|
|
const li = document.createElement("li");
|
|
|
|
|
|
let paperInfo = null;
|
|
if (originalData) {
|
|
paperInfo = originalData.find(paper => paper.id === id);
|
|
}
|
|
|
|
const a = document.createElement("a");
|
|
if (paperInfo) {
|
|
a.href = "#";
|
|
a.setAttribute("data-id", id);
|
|
a.textContent = paperInfo.title || id;
|
|
a.addEventListener("click", function(event) {
|
|
event.preventDefault();
|
|
const paperId = this.getAttribute("data-id");
|
|
const nodeElement = mainGroup.selectAll(".node").filter(d => d.id === paperId);
|
|
if (!nodeElement.empty()) {
|
|
nodeClicked.call(nodeElement.node(), event, nodeElement.datum());
|
|
}
|
|
});
|
|
} else {
|
|
a.href = `${id}`;
|
|
a.target = "_blank";
|
|
a.textContent = id;
|
|
}
|
|
|
|
li.appendChild(a);
|
|
fragment.appendChild(li);
|
|
});
|
|
|
|
listElement.node().appendChild(fragment);
|
|
}
|
|
|
|
|
|
svg.on("click", function() {
|
|
closeDetailsPanel();
|
|
});
|
|
|
|
|
|
window.addEventListener('beforeunload', function() {
|
|
if (simulation) {
|
|
simulation.stop();
|
|
}
|
|
stopSimulationLoop();
|
|
});
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}); |