Skip to content

Instantly share code, notes, and snippets.

@PoisonAlien
Created March 20, 2025 22:15
Show Gist options
  • Save PoisonAlien/f38fc048108e27a0bf6b7e098e4bb4ca to your computer and use it in GitHub Desktop.
Save PoisonAlien/f38fc048108e27a0bf6b7e098e4bb4ca to your computer and use it in GitHub Desktop.
<!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