Created
March 20, 2025 22:15
-
-
Save PoisonAlien/f38fc048108e27a0bf6b7e098e4bb4ca to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Research Paper Visualization</title> | |
<script src="https://d3js.org/d3.v7.min.js"></script> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
margin: 0; | |
padding: 20px; | |
background-color: #f5f5f5; | |
} | |
.container { | |
max-width: 1200px; | |
margin: 0 auto; | |
background-color: white; | |
padding: 20px; | |
border-radius: 8px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
} | |
h1 { | |
text-align: center; | |
color: #333; | |
} | |
.upload-container { | |
text-align: center; | |
padding: 20px; | |
border: 2px dashed #ccc; | |
border-radius: 8px; | |
margin-bottom: 20px; | |
} | |
#file-input { | |
display: none; | |
} | |
.upload-button { | |
background-color: #4285F4; | |
color: white; | |
padding: 10px 20px; | |
border: none; | |
border-radius: 4px; | |
cursor: pointer; | |
font-size: 16px; | |
} | |
.controls { | |
display: flex; | |
justify-content: center; | |
margin-bottom: 20px; | |
gap: 10px; | |
} | |
button { | |
background-color: #4285F4; | |
color: white; | |
border: none; | |
padding: 8px 16px; | |
border-radius: 4px; | |
cursor: pointer; | |
} | |
button:hover { | |
background-color: #3367D6; | |
} | |
#visualization { | |
width: 100%; | |
height: 700px; | |
overflow: auto; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
margin-bottom: 20px; | |
} | |
.node circle { | |
fill: #fff; | |
stroke-width: 2px; | |
} | |
.node text { | |
font: 12px sans-serif; | |
} | |
.link { | |
fill: none; | |
stroke: #ccc; | |
stroke-width: 1.5px; | |
} | |
.relationship { | |
fill: none; | |
stroke: #ff5722; | |
stroke-width: 1.5px; | |
stroke-dasharray: 5, 3; | |
opacity: 0.7; | |
} | |
.tooltip { | |
position: absolute; | |
padding: 10px; | |
background-color: rgba(255, 255, 255, 0.95); | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
pointer-events: none; | |
max-width: 300px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.2); | |
font-size: 13px; | |
z-index: 10; | |
opacity: 0; | |
} | |
.tooltip h4 { | |
margin-top: 0; | |
margin-bottom: 5px; | |
color: #333; | |
border-bottom: 1px solid #ddd; | |
padding-bottom: 5px; | |
} | |
.tooltip p { | |
margin: 5px 0; | |
} | |
.figure-ref { | |
color: #0066cc; | |
font-style: italic; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>Research Paper Visualization</h1> | |
<div class="upload-container"> | |
<input type="file" id="file-input" accept=".json"/> | |
<label for="file-input" class="upload-button">Upload JSON File</label> | |
<p id="file-name">No file selected</p> | |
</div> | |
<div class="controls" id="controls" style="display: none;"> | |
<button id="expandAll">Expand All</button> | |
<button id="collapseAll">Collapse All</button> | |
<button id="resetView">Reset View</button> | |
<button id="toggleRelationships">Toggle Relationships</button> | |
</div> | |
<div id="visualization"></div> | |
<div id="tooltip" class="tooltip"></div> | |
</div> | |
<script> | |
// Global variables | |
let root; | |
let treeData; | |
let svg; | |
let showRelationships = true; | |
let i = 0; // Counter for node IDs | |
let nodeMap = new Map(); | |
let tooltip; | |
// Get DOM elements | |
const fileInput = document.getElementById('file-input'); | |
const fileName = document.getElementById('file-name'); | |
const controls = document.getElementById('controls'); | |
const visualizationContainer = document.getElementById('visualization'); | |
// Add event listener for file input | |
fileInput.addEventListener('change', handleFileUpload); | |
// Function to handle file upload | |
function handleFileUpload(event) { | |
const file = event.target.files[0]; | |
if (!file) return; | |
fileName.textContent = file.name; | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
try { | |
const jsonData = JSON.parse(e.target.result); | |
initializeVisualization(jsonData); | |
controls.style.display = 'flex'; | |
} catch (error) { | |
alert('Error parsing JSON file: ' + error.message); | |
} | |
}; | |
reader.onerror = function() { | |
alert('Error reading file'); | |
}; | |
reader.readAsText(file); | |
} | |
// Function to initialize the visualization | |
function initializeVisualization(data) { | |
// Clear previous visualization | |
visualizationContainer.innerHTML = ''; | |
nodeMap.clear(); | |
i = 0; | |
// Set up the dimensions and margins | |
const margin = {top: 20, right: 120, bottom: 20, left: 120}; | |
const width = visualizationContainer.clientWidth - margin.left - margin.right; | |
const height = visualizationContainer.clientHeight - margin.top - margin.bottom; | |
// Create the svg canvas | |
svg = d3.select('#visualization') | |
.append('svg') | |
.attr('width', width + margin.left + margin.right) | |
.attr('height', height + margin.top + margin.bottom) | |
.append('g') | |
.attr('transform', `translate(${margin.left},${margin.top})`); | |
// Initialize the tooltip | |
tooltip = d3.select('#tooltip'); | |
// Create the tree layout | |
const tree = d3.tree().size([height, width]); | |
// Get hierarchical data or create it from the data structure | |
if (data.hierarchicalData) { | |
treeData = data.hierarchicalData; | |
} else { | |
// If not in expected format, try to adapt | |
treeData = adaptDataToHierarchy(data); | |
} | |
// Create the root node | |
root = d3.hierarchy(treeData); | |
// Set initial positions | |
root.x0 = height / 2; | |
root.y0 = 0; | |
// Collapse all nodes initially | |
if (root.children) { | |
root.children.forEach(collapse); | |
} | |
// Define arrow marker for relationship lines | |
svg.append("defs").append("marker") | |
.attr("id", "arrowhead") | |
.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", "#ff5722"); | |
// Update the visualization | |
update(root); | |
// Add event listeners for controls | |
document.getElementById('expandAll').addEventListener('click', function() { | |
expandAll(root); | |
update(root); | |
}); | |
document.getElementById('collapseAll').addEventListener('click', function() { | |
if (root.children) { | |
root.children.forEach(collapse); | |
update(root); | |
} | |
}); | |
document.getElementById('resetView').addEventListener('click', function() { | |
if (root.children) { | |
root.children.forEach(collapse); | |
update(root); | |
} | |
}); | |
document.getElementById('toggleRelationships').addEventListener('click', function() { | |
showRelationships = !showRelationships; | |
update(root); | |
this.textContent = showRelationships ? "Hide Relationships" : "Show Relationships"; | |
}); | |
} | |
// Function to adapt different data formats to hierarchy | |
function adaptDataToHierarchy(data) { | |
// This is a simple adaptation, enhance based on your specific data format | |
return { | |
name: data.metadata?.title || "Research Paper", | |
nodeId: "root", | |
children: [ | |
createSectionNode(data, "background", "Background"), | |
createSectionNode(data, "methods", "Methods"), | |
createSectionNode(data, "results", "Results"), | |
createSectionNode(data, "discussion", "Discussion") | |
] | |
}; | |
} | |
// Helper function to create section nodes | |
function createSectionNode(data, sectionKey, sectionName) { | |
// Create a basic section node with empty children | |
// In a real implementation, you would extract actual children from your data | |
return { | |
name: sectionName, | |
nodeId: sectionKey, | |
type: sectionKey, | |
details: `${sectionName} section of the paper`, | |
children: [] | |
}; | |
} | |
// Function to collapse nodes | |
function collapse(d) { | |
if (d.children) { | |
d._children = d.children; | |
d._children.forEach(collapse); | |
d.children = null; | |
} | |
} | |
// Function to expand all nodes | |
function expandAll(d) { | |
if (d._children) { | |
d.children = d._children; | |
d._children = null; | |
} | |
if (d.children) { | |
d.children.forEach(expandAll); | |
} | |
} | |
// Main update function | |
function update(source) { | |
// Compute the new tree layout | |
const tree = d3.tree().size([visualizationContainer.clientHeight - 40, visualizationContainer.clientWidth - 240]); | |
const nodes = tree(root); | |
// Get all nodes as array | |
const allNodes = nodes.descendants(); | |
const links = nodes.links(); | |
// Clear node map and rebuild | |
nodeMap.clear(); | |
// Normalize for fixed-depth and add to node map | |
allNodes.forEach(function(d) { | |
d.y = d.depth * 180; | |
// Add to node map if it has an ID | |
if (d.data.nodeId) { | |
nodeMap.set(d.data.nodeId, d); | |
} | |
}); | |
// Update the nodes | |
const node = svg.selectAll('g.node') | |
.data(allNodes, function(d) { | |
return d.id || (d.id = ++i); | |
}); | |
// Enter new nodes at the parent's previous position | |
const nodeEnter = node.enter().append('g') | |
.attr('class', 'node') | |
.attr("transform", function(d) { | |
return "translate(" + source.y0 + "," + source.x0 + ")"; | |
}) | |
.on('click', function(event, d) { | |
// Toggle children on click | |
if (d.children) { | |
d._children = d.children; | |
d.children = null; | |
} else { | |
d.children = d._children; | |
d._children = null; | |
} | |
update(d); | |
}) | |
.on('mouseover', function(event, d) { | |
tooltip.transition() | |
.duration(200) | |
.style("opacity", 0.9); | |
let content = "<h4>" + d.data.name + "</h4>"; | |
if (d.data.details) { | |
content += "<p>" + d.data.details + "</p>"; | |
} | |
if (d.data.figureRef) { | |
content += `<p class="figure-ref">📊 ${d.data.figureRef}</p>`; | |
} | |
tooltip.html(content) | |
.style("left", (event.pageX + 10) + "px") | |
.style("top", (event.pageY - 28) + "px"); | |
}) | |
.on('mouseout', function() { | |
tooltip.transition() | |
.duration(500) | |
.style("opacity", 0); | |
}); | |
// Add Circle for the nodes | |
nodeEnter.append('circle') | |
.attr('r', 1e-6) | |
.style("fill", function(d) { | |
return d._children ? getNodeColor(d) : "#fff"; | |
}) | |
.style("stroke", function(d) { | |
return getNodeStroke(d); | |
}); | |
// Add figure reference icon if available | |
nodeEnter.filter(d => d.data.figureRef) | |
.append('text') | |
.attr('class', 'figure-icon') | |
.attr('dy', '-0.7em') | |
.attr('dx', function(d) { | |
return d.children || d._children ? -25 : 13; | |
}) | |
.text('📊'); | |
// Add labels for the nodes | |
nodeEnter.append('text') | |
.attr("dy", ".35em") | |
.attr("x", function(d) { | |
return d.children || d._children ? -13 : 13; | |
}) | |
.attr("text-anchor", function(d) { | |
return d.children || d._children ? "end" : "start"; | |
}) | |
.text(function(d) { return d.data.name; }); | |
// UPDATE | |
const nodeUpdate = nodeEnter.merge(node); | |
// Transition to the proper position for the node | |
nodeUpdate.transition() | |
.duration(750) | |
.attr("transform", function(d) { | |
return "translate(" + d.y + "," + d.x + ")"; | |
}); | |
// Update the node attributes and style | |
nodeUpdate.select('circle') | |
.attr('r', 8) | |
.style("fill", function(d) { | |
return d._children ? getNodeColor(d) : "#fff"; | |
}) | |
.style("stroke", function(d) { | |
return getNodeStroke(d); | |
}) | |
.attr('cursor', 'pointer'); | |
// Remove any exiting nodes | |
const nodeExit = node.exit().transition() | |
.duration(750) | |
.attr("transform", function(d) { | |
return "translate(" + source.y + "," + source.x + ")"; | |
}) | |
.remove(); | |
// On exit reduce the node circles size to 0 | |
nodeExit.select('circle') | |
.attr('r', 1e-6); | |
// On exit reduce the opacity of text labels | |
nodeExit.select('text') | |
.style('fill-opacity', 1e-6); | |
// ****************** links section *************************** | |
// Update the links... | |
const link = svg.selectAll('path.link') | |
.data(links, function(d) { return d.target.id; }); | |
// Enter any new links at the parent's previous position | |
const linkEnter = link.enter().insert('path', "g") | |
.attr("class", "link") | |
.attr('d', function(d){ | |
const o = {x: source.x0, y: source.y0}; | |
return diagonal(o, o); | |
}); | |
// UPDATE | |
const linkUpdate = linkEnter.merge(link); | |
// Transition back to the parent element position | |
linkUpdate.transition() | |
.duration(750) | |
.attr('d', function(d){ return diagonal(d.source, d.target); }); | |
// Remove any exiting links | |
link.exit().transition() | |
.duration(750) | |
.attr('d', function(d) { | |
const o = {x: source.x, y: source.y}; | |
return diagonal(o, o); | |
}) | |
.remove(); | |
// Store the old positions for transition | |
allNodes.forEach(function(d){ | |
d.x0 = d.x; | |
d.y0 = d.y; | |
}); | |
// Remove old relationship lines | |
svg.selectAll("path.relationship").remove(); | |
// Draw relationship connections if they exist and are visible | |
if (showRelationships && treeData.nodeId) { | |
drawRelationships(); | |
} | |
} | |
// Function to get color based on node type | |
function getNodeColor(d) { | |
switch(d.data.type) { | |
case "background": return "#e1f5fe"; | |
case "methods": return "#e8f5e9"; | |
case "results": return "#fff8e1"; | |
case "discussion": return "#f3e5f5"; | |
default: return "#f5f5f5"; | |
} | |
} | |
// Function to get stroke color based on node type | |
function getNodeStroke(d) { | |
switch(d.data.type) { | |
case "background": return "#01579b"; | |
case "methods": return "#2e7d32"; | |
case "results": return "#ff8f00"; | |
case "discussion": return "#6a1b9a"; | |
default: return "#333333"; | |
} | |
} | |
// Creates a curved (diagonal) path from parent to the child nodes | |
function diagonal(s, d) { | |
const path = `M ${s.y} ${s.x} | |
C ${(s.y + d.y) / 2} ${s.x}, | |
${(s.y + d.y) / 2} ${d.x}, | |
${d.y} ${d.x}`; | |
return path; | |
} | |
// Function to draw relationship lines | |
function drawRelationships() { | |
// Check if resultRelationships exists | |
if (!treeData.resultRelationships) return; | |
treeData.resultRelationships.forEach(rel => { | |
const sourceNode = nodeMap.get(rel.source); | |
const targetNode = nodeMap.get(rel.target); | |
// Only draw if both nodes exist on the screen | |
if (sourceNode && targetNode) { | |
// Create a path between these two nodes | |
const startX = sourceNode.y; | |
const startY = sourceNode.x; | |
const endX = targetNode.y; | |
const endY = targetNode.x; | |
// Create a curved path | |
const path = `M ${startX} ${startY} | |
C ${(startX + endX) / 2 + 50} ${startY}, | |
${(startX + endX) / 2 - 50} ${endY}, | |
${endX} ${endY}`; | |
// Add path to SVG | |
svg.append("path") | |
.attr("class", "relationship") | |
.attr("d", path) | |
.attr("marker-end", "url(#arrowhead)") | |
.on('mouseover', function(event) { | |
tooltip.transition() | |
.duration(200) | |
.style("opacity", 0.9); | |
const content = `<h4>Relationship</h4><p>${rel.description || "Relationship between findings"}</p>`; | |
tooltip.html(content) | |
.style("left", (event.pageX + 10) + "px") | |
.style("top", (event.pageY - 28) + "px"); | |
}) | |
.on('mouseout', function() { | |
tooltip.transition() | |
.duration(500) | |
.style("opacity", 0); | |
}); | |
} | |
}); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment