<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enhanced Graph Visualizer with Operations</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background: #f0f2f5;
display: flex;
height: 100vh;
overflow: hidden;
}
/* Left Sidebar Tool Palette */
.sidebar {
width: 80px;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
border-right: 1px solid #1a252f;
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 0;
box-shadow: 2px 0 8px rgba(0,0,0,0.15);
z-index: 1000;
}
.sidebar-header {
color: white;
font-size: 12px;
font-weight: bold;
text-align: center;
margin-bottom: 20px;
padding: 0 5px;
}
.tool-group {
margin-bottom: 15px;
border-bottom: 1px solid #4a5f7a;
padding-bottom: 10px;
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2px;
align-items: center;
justify-items: center;
}
.tool-group:last-child {
border-bottom: none;
margin-bottom: 0;
}
.tool-btn {
width: 35px;
height: 35px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 6px;
color: white;
font-size: 9px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin: 1px;
transition: all 0.3s ease;
position: relative;
text-align: center;
line-height: 1;
}
.tool-btn:hover {
background: rgba(255,255,255,0.2);
border-color: rgba(255,255,255,0.4);
transform: translateX(3px);
}
.tool-btn:active {
background: rgba(255,255,255,0.3);
}
.tool-btn.active {
background: #3498db;
border-color: #2980b9;
}
.tool-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.tool-btn:disabled:hover {
transform: none;
background: rgba(255,255,255,0.1);
}
/* Tooltips */
.tool-btn::after {
content: attr(data-tooltip);
position: absolute;
left: 65px;
top: 50%;
transform: translateY(-50%);
background: #2c3e50;
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
z-index: 1001;
}
.tool-btn:hover::after {
opacity: 1;
}
.tool-btn::before {
content: '';
position: absolute;
left: 58px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-right: 7px solid #2c3e50;
opacity: 0;
transition: opacity 0.3s ease;
}
.tool-btn:hover::before {
opacity: 1;
}
/* Main Content Area */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
height: 100vh;
}
.header {
background: white;
padding: 15px 25px;
border-bottom: 1px solid #e1e5e9;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.header h1 {
margin: 0;
color: #2c3e50;
font-size: 24px;
font-weight: 600;
}
.header .subtitle {
color: #7f8c8d;
font-size: 14px;
margin-top: 5px;
}
/* Graph Container */
.graph-container {
flex: 1;
background: white;
margin: 20px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
overflow: hidden;
}
.graph-toolbar {
background: #f8f9fa;
border-bottom: 1px solid #e1e5e9;
padding: 10px 20px;
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.toolbar-group {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 10px;
background: white;
border-radius: 6px;
border: 1px solid #e1e5e9;
}
.toolbar-group label {
font-size: 12px;
color: #555;
font-weight: 500;
}
#graph {
flex: 1;
background: #fafbfc;
}
/* Results Panel */
.results-panel {
position: fixed;
top: 80px;
right: 20px;
width: 300px;
max-height: calc(100vh - 120px);
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
border: 1px solid #e1e5e9;
overflow: hidden;
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 500;
}
.results-panel.visible {
transform: translateX(0);
}
.results-header {
background: #3498db;
color: white;
padding: 15px 20px;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.results-content {
padding: 20px;
max-height: 500px;
overflow-y: auto;
}
.close-results {
background: none;
border: none;
color: white;
font-size: 18px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
/* Control Inputs */
input[type="text"], input[type="number"], select {
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 12px;
background: white;
}
input[type="range"] {
width: 80px;
}
/* Status Messages */
.status-message {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 16px;
border-radius: 8px;
font-weight: 500;
z-index: 1000;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
.status-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-warning {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.status-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
/* Node and Link Styles */
.node {
cursor: pointer;
stroke: #fff;
stroke-width: 2px;
transition: all 0.3s;
}
.node:hover {
stroke-width: 3px;
}
.node.selected {
stroke: #ff6b6b;
stroke-width: 4px;
}
.node.highlighted {
fill: #ff6b6b !important;
stroke: #d32f2f !important;
stroke-width: 4px !important;
}
.node.path-node {
fill: #ffa726 !important;
stroke: #f57c00 !important;
stroke-width: 3px !important;
}
.node.start-node {
fill: #66bb6a !important;
stroke: #388e3c !important;
stroke-width: 4px !important;
}
.node.end-node {
fill: #ef5350 !important;
stroke: #c62828 !important;
stroke-width: 4px !important;
}
.node.path-intermediate {
fill: #ffa726 !important;
stroke: #f57c00 !important;
stroke-width: 3px !important;
}
.node.collapsed {
fill: #9e9e9e !important;
stroke: #616161 !important;
stroke-width: 3px !important;
cursor: pointer;
}
.node.collapsed:hover {
fill: #757575 !important;
stroke: #424242 !important;
stroke-width: 4px !important;
}
.node.has-children:hover {
stroke: #ff9800 !important;
stroke-width: 3px !important;
}
.node.hidden {
display: none;
}
.link.hidden {
display: none;
}
.link {
stroke: #999;
stroke-opacity: 0.8;
stroke-width: 2px;
fill: none;
marker-end: url(#arrowhead);
cursor: pointer;
transition: all 0.3s;
}
.link:hover {
stroke: #333;
stroke-width: 3px;
}
.link.highlighted {
stroke: #ff6b6b !important;
stroke-width: 4px !important;
stroke-opacity: 1 !important;
}
.link.path-link {
stroke: #ffa726 !important;
stroke-width: 3px !important;
stroke-opacity: 1 !important;
}
.node-label {
font-size: 12px;
font-weight: bold;
text-anchor: middle;
dominant-baseline: central;
pointer-events: none;
fill: #333;
}
/* Result Items */
.result-item {
background: #f8f9fa;
padding: 10px 14px;
margin: 8px 0;
border-radius: 8px;
border-left: 4px solid #3498db;
font-size: 13px;
}
.result-item strong {
color: #2c3e50;
}
</style>
</head>
<body>
<!-- Left Sidebar Tool Palette -->
<div class="sidebar">
<div class="sidebar-header">Graph Tools</div>
<!-- Create Tools -->
<div class="tool-group">
<div class="tool-btn" onclick="focusNodeInput()" data-tooltip="Add Node (N)">Add</div>
<div class="tool-btn" id="addEdgeBtn" onclick="addEdge()" data-tooltip="Add Edge (E)" disabled>Link</div>
<div class="tool-btn" onclick="loadJSON()" data-tooltip="Load JSON File">Load</div>
</div>
<!-- Algorithm Tools -->
<div class="tool-group">
<div class="tool-btn" id="dfsBtn" onclick="runDFS()" data-tooltip="Run DFS Traversal" disabled>DFS</div>
<div class="tool-btn" id="bfsBtn" onclick="runBFS()" data-tooltip="Run BFS Traversal" disabled>BFS</div>
<div class="tool-btn" id="pathBtn" onclick="showPathDialog()" data-tooltip="Find Shortest Path" disabled>Path</div>
</div>
<!-- Centrality Analysis -->
<div class="tool-group">
<div class="tool-btn" id="degreeBtn" onclick="runDegreeCentrality()" data-tooltip="Degree Centrality" disabled>Degree</div>
<div class="tool-btn" id="betweennessBtn" onclick="runBetweennessCentrality()" data-tooltip="Betweenness Centrality" disabled>Between</div>
<div class="tool-btn" id="closenessBtn" onclick="runClosenessCentrality()" data-tooltip="Closeness Centrality" disabled>Close</div>
<div class="tool-btn" id="eigenvectorBtn" onclick="runEigenvectorCentrality()" data-tooltip="Eigenvector Centrality" disabled>Eigen</div>
</div>
<!-- Layout Tools -->
<div class="tool-group">
<div class="tool-btn" onclick="expandAll()" data-tooltip="Expand All Nodes">Expand</div>
<div class="tool-btn" onclick="collapseAll()" data-tooltip="Collapse All Nodes">Collapse</div>
<div class="tool-btn" onclick="resetLayout()" data-tooltip="Reset Layout">Reset</div>
</div>
<!-- View Tools -->
<div class="tool-group">
<div class="tool-btn" onclick="clearHighlights()" data-tooltip="Clear Highlights">Clear</div>
<div class="tool-btn" onclick="clearCentralityVisualization()" data-tooltip="Clear Centrality">Clean</div>
<div class="tool-btn" onclick="toggleResultsPanel()" data-tooltip="Toggle Results">Results</div>
</div>
<!-- Export Tools -->
<div class="tool-group">
<div class="tool-btn" onclick="exportGraph()" data-tooltip="Export CSV Matrix">CSV</div>
<div class="tool-btn" onclick="exportJSON()" data-tooltip="Export JSON">JSON</div>
<div class="tool-btn" onclick="clearGraph()" data-tooltip="Clear Graph">Delete</div>
</div>
</div>
<!-- Main Content Area -->
<div class="main-content">
<!-- Header -->
<div class="header">
<h1>Enhanced Graph Visualizer</h1>
<div class="subtitle">Professional graph analysis with centrality measures and interactive tools</div>
</div>
<!-- Graph Container -->
<div class="graph-container">
<!-- Graph Toolbar -->
<div class="graph-toolbar">
<div class="toolbar-group">
<label for="nodeInput">Add Node:</label>
<input type="text" id="nodeInput" placeholder="Node name" onkeypress="handleNodeInputKeypress(event)">
</div>
<div class="toolbar-group">
<label for="startNode">Start:</label>
<select id="startNode">
<option value="">Select node...</option>
</select>
</div>
<div class="toolbar-group">
<label for="linkDistance">Spacing:</label>
<input type="range" id="linkDistance" min="50" max="300" value="100" oninput="updateSpacing()">
<span id="linkDistanceValue">100</span>
</div>
<div class="toolbar-group">
<label for="chargeStrength">Repulsion:</label>
<input type="range" id="chargeStrength" min="-1000" max="-100" value="-300" oninput="updateRepulsion()">
<span id="chargeStrengthValue">-300</span>
</div>
<div class="toolbar-group">
<label for="centralityVisualization">View:</label>
<select id="centralityVisualization">
<option value="color">Color</option>
<option value="size">Size</option>
<option value="both">Both</option>
</select>
</div>
<div class="toolbar-group">
<span id="selectionInfo" style="color: #666; font-size: 11px;">Select 2 nodes for edge</span>
</div>
<div class="toolbar-group">
<span style="color: #666; font-size: 11px;">Nodes: <span id="nodeCount">0</span> | Edges: <span id="edgeCount">0</span></span>
</div>
</div>
<!-- Graph SVG -->
<div id="graph"></div>
</div>
</div>
<!-- Results Panel (Hidden by default) -->
<div class="results-panel" id="resultsPanel">
<div class="results-header">
<span>Analysis Results</span>
<button class="close-results" onclick="toggleResultsPanel()">×</button>
</div>
<div class="results-content" id="operationResults">
<p style="color: #666; font-style: italic;">Run an analysis operation to see results...</p>
</div>
</div>
<!-- Status Messages Container -->
<div id="statusMessage"></div>
<!-- Hidden Controls -->
<input type="file" id="jsonFileInput" accept=".json" style="display: none;" onchange="handleJSONFile(event)">
<input type="text" id="exportFilename" value="graph_matrix" style="display: none;">
<select id="fromNode" style="display: none;"><option value="">Select from node...</option></select>
<select id="toNode" style="display: none;"><option value="">Select to node...</option></select>
<select id="collapseNode" style="display: none;"><option value="">Select node to collapse...</option></select>
<script>
// Graph data
let graphData = { nodes: [], links: [] };
let selectedNodes = [];
let nodeIdCounter = 0;
let collapsedNodes = new Set();
let hiddenNodes = new Set();
let hiddenLinks = new Set();
let currentCentralityScores = null;
let currentCentralityType = null;
// D3 setup - dynamically sized
const width = window.innerWidth - 120; // Account for sidebar
const height = window.innerHeight - 200; // Account for header and toolbar
const svg = d3.select("#graph")
.append("svg")
.attr("width", width)
.attr("height", height);
// Define arrow marker
svg.append("defs").append("marker")
.attr("id", "arrowhead")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 25)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", "#666");
// Force simulation
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(d => d.id).distance(100))
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(25))
.force("x", d3.forceX(width / 2).strength(0.1))
.force("y", d3.forceY(height / 2).strength(0.1));
// Graph elements
let link = svg.append("g").selectAll("line");
let node = svg.append("g").selectAll("circle");
let label = svg.append("g").selectAll("text");
// Initialize with empty graph
updateVisualization();
updateStats();
updateStartNodeOptions();
// Check if graph data was pre-loaded and update UI accordingly
if (graphData.nodes && graphData.nodes.length > 0) {
showStatus(`Graph loaded with ${graphData.nodes.length} nodes and ${graphData.links.length} links.`, "success");
// Update dropdown options multiple times to ensure they populate
updateStartNodeOptions();
setTimeout(() => {
updateStartNodeOptions();
console.log('Second dropdown update completed');
}, 100);
setTimeout(() => {
updateStartNodeOptions();
console.log('Third dropdown update completed');
}, 500);
} else {
showStatus("Welcome! Start by adding nodes to create your graph.", "success");
}
// New UI helper functions
function focusNodeInput() {
document.getElementById('nodeInput').focus();
}
function handleNodeInputKeypress(event) {
if (event.key === 'Enter') {
addNode();
}
}
function toggleResultsPanel() {
const panel = document.getElementById('resultsPanel');
panel.classList.toggle('visible');
}
function showPathDialog() {
const fromNode = prompt("Enter start node:");
const toNode = prompt("Enter end node:");
if (fromNode && toNode) {
// Set the hidden selectors
document.getElementById('fromNode').value = fromNode;
document.getElementById('toNode').value = toNode;
findPath();
}
}
function toggleLayoutPanel() {
// For now, just show a status message
showStatus("Layout controls are in the top toolbar", "success");
}
// Layout control functions
function updateSpacing() {
const distance = document.getElementById('linkDistance').value;
document.getElementById('linkDistanceValue').textContent = distance;
simulation.force("link").distance(distance);
simulation.alpha(0.3).restart();
}
function updateRepulsion() {
const strength = document.getElementById('chargeStrength').value;
document.getElementById('chargeStrengthValue').textContent = strength;
simulation.force("charge").strength(strength);
simulation.alpha(0.3).restart();
}
function resetLayout() {
document.getElementById('linkDistance').value = 100;
document.getElementById('chargeStrength').value = -300;
document.getElementById('linkDistanceValue').textContent = '100';
document.getElementById('chargeStrengthValue').textContent = '-300';
simulation.force("link").distance(100);
simulation.force("charge").strength(-300);
simulation.alpha(0.3).restart();
showStatus("Layout reset to defaults.", "success");
}
// Node collapsing functions
function getChildren(nodeId) {
return graphData.links
.filter(l => (l.source.id || l.source) === nodeId)
.map(l => l.target.id || l.target);
}
function getParents(nodeId) {
return graphData.links
.filter(l => (l.target.id || l.target) === nodeId)
.map(l => l.source.id || l.source);
}
function getAllDescendants(nodeId, visited = new Set()) {
if (visited.has(nodeId)) return [];
visited.add(nodeId);
const children = getChildren(nodeId);
let descendants = [...children];
children.forEach(child => {
descendants = descendants.concat(getAllDescendants(child, visited));
});
return [...new Set(descendants)];
}
function toggleCollapse() {
const nodeId = document.getElementById('collapseNode').value;
if (!nodeId) {
showStatus("Please select a node to collapse/expand.", "warning");
return;
}
if (collapsedNodes.has(nodeId)) {
expandNode(nodeId);
} else {
collapseNode(nodeId);
}
}
function collapseNode(nodeId) {
const descendants = getAllDescendants(nodeId);
if (descendants.length === 0) {
showStatus(`Node ${nodeId} has no children to collapse.`, "warning");
return;
}
collapsedNodes.add(nodeId);
descendants.forEach(desc => hiddenNodes.add(desc));
// Hide all links involving hidden nodes
graphData.links.forEach(link => {
const sourceId = link.source.id || link.source;
const targetId = link.target.id || link.target;
if (hiddenNodes.has(sourceId) || hiddenNodes.has(targetId)) {
hiddenLinks.add(`${sourceId}-${targetId}`);
}
});
updateVisualization();
showStatus(`Collapsed ${descendants.length} descendants of ${nodeId}.`, "success");
}
function expandNode(nodeId) {
const wasCollapsed = collapsedNodes.has(nodeId);
if (!wasCollapsed) {
showStatus(`Node ${nodeId} is not collapsed.`, "warning");
return;
}
collapsedNodes.delete(nodeId);
const directChildren = getChildren(nodeId);
// Show direct children
directChildren.forEach(child => hiddenNodes.delete(child));
// Remove hidden links for visible nodes
const linksToShow = [];
graphData.links.forEach(link => {
const sourceId = link.source.id || link.source;
const targetId = link.target.id || link.target;
const linkKey = `${sourceId}-${targetId}`;
if (!hiddenNodes.has(sourceId) && !hiddenNodes.has(targetId)) {
hiddenLinks.delete(linkKey);
linksToShow.push(linkKey);
}
});
updateVisualization();
showStatus(`Expanded ${directChildren.length} children of ${nodeId}.`, "success");
}
function expandAll() {
collapsedNodes.clear();
hiddenNodes.clear();
hiddenLinks.clear();
updateVisualization();
showStatus("All nodes expanded.", "success");
}
function expandAllDescendants(nodeId) {
// Recursively expand all collapsed descendants
const descendants = getAllDescendants(nodeId);
let expandedCount = 0;
// First expand the selected node if it's collapsed
if (collapsedNodes.has(nodeId)) {
collapsedNodes.delete(nodeId);
expandedCount++;
}
// Then expand all collapsed descendants
descendants.forEach(desc => {
if (collapsedNodes.has(desc)) {
collapsedNodes.delete(desc);
expandedCount++;
}
hiddenNodes.delete(desc);
});
// Recalculate visible links
hiddenLinks.clear();
graphData.links.forEach(link => {
const sourceId = link.source.id || link.source;
const targetId = link.target.id || link.target;
if (hiddenNodes.has(sourceId) || hiddenNodes.has(targetId)) {
hiddenLinks.add(`${sourceId}-${targetId}`);
}
});
updateVisualization();
return expandedCount;
}
function collapseAllDescendants(nodeId) {
// Recursively collapse all descendants
const descendants = getAllDescendants(nodeId);
if (descendants.length === 0) {
return 0;
}
// Mark the node as collapsed
collapsedNodes.add(nodeId);
// Hide all descendants
descendants.forEach(desc => {
hiddenNodes.add(desc);
// Also mark descendants as collapsed if they have children
const grandchildren = getAllDescendants(desc);
if (grandchildren.length > 0) {
collapsedNodes.add(desc);
}
});
// Hide all links involving hidden nodes
graphData.links.forEach(link => {
const sourceId = link.source.id || link.source;
const targetId = link.target.id || link.target;
if (hiddenNodes.has(sourceId) || hiddenNodes.has(targetId)) {
hiddenLinks.add(`${sourceId}-${targetId}`);
}
});
updateVisualization();
return descendants.length;
}
// Centrality Measures Implementation
function calculateDegreeCentrality() {
const centrality = {};
// Initialize all nodes with zero counts
graphData.nodes.forEach(node => {
centrality[node.id] = {
inDegree: 0,
outDegree: 0,
totalDegree: 0
};
});
// Count connections
graphData.links.forEach(link => {
const sourceId = link.source.id || link.source;
const targetId = link.target.id || link.target;
if (centrality[sourceId]) centrality[sourceId].outDegree++;
if (centrality[targetId]) centrality[targetId].inDegree++;
});
// Calculate total degree
Object.keys(centrality).forEach(nodeId => {
centrality[nodeId].totalDegree = centrality[nodeId].inDegree + centrality[nodeId].outDegree;
});
return centrality;
}
function calculateBetweennessCentrality() {
const centrality = {};
const nodes = graphData.nodes.map(n => n.id);
// Initialize centrality scores
nodes.forEach(node => {
centrality[node] = 0;
});
// For each pair of nodes, find shortest paths and count passages
for (let s = 0; s < nodes.length; s++) {
for (let t = s + 1; t < nodes.length; t++) {
const source = nodes[s];
const target = nodes[t];
// Find all shortest paths between source and target
const paths = findAllShortestPaths(source, target);
if (paths.length === 0) continue;
// Count how many times each node appears in shortest paths
const pathCount = paths.length;
paths.forEach(path => {
// Don't count source and target themselves
for (let i = 1; i < path.length - 1; i++) {
centrality[path[i]] += 1.0 / pathCount;
}
});
}
}
// Normalize by the number of pairs
const normalizer = ((nodes.length - 1) * (nodes.length - 2)) / 2;
if (normalizer > 0) {
nodes.forEach(node => {
centrality[node] /= normalizer;
});
}
return centrality;
}
function calculateClosenessCentrality() {
const centrality = {};
const nodes = graphData.nodes.map(n => n.id);
nodes.forEach(source => {
let totalDistance = 0;
let reachableNodes = 0;
nodes.forEach(target => {
if (source !== target) {
const path = findShortestPath(source, target);
if (path.length > 0) {
totalDistance += path.length - 1; // Length - 1 = number of edges
reachableNodes++;
}
}
});
// Closeness is inverse of average distance
centrality[source] = reachableNodes > 0 ? reachableNodes / totalDistance : 0;
});
return centrality;
}
function calculateEigenvectorCentrality(maxIterations = 100, tolerance = 1e-6) {
const nodes = graphData.nodes.map(n => n.id);
const n = nodes.length;
const nodeIndex = {};
// Create node index mapping
nodes.forEach((node, i) => {
nodeIndex[node] = i;
});
// Build adjacency matrix
const adjacencyMatrix = Array(n).fill().map(() => Array(n).fill(0));
graphData.links.forEach(link => {
const sourceId = link.source.id || link.source;
const targetId = link.target.id || link.target;
const sourceIdx = nodeIndex[sourceId];
const targetIdx = nodeIndex[targetId];
if (sourceIdx !== undefined && targetIdx !== undefined) {
adjacencyMatrix[sourceIdx][targetIdx] = 1;
// For undirected behavior (optional)
adjacencyMatrix[targetIdx][sourceIdx] = 1;
}
});
// Initialize eigenvector
let eigenvector = Array(n).fill(1.0);
// Power iteration
for (let iter = 0; iter < maxIterations; iter++) {
const newEigenvector = Array(n).fill(0);
// Matrix-vector multiplication
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
newEigenvector[i] += adjacencyMatrix[i][j] * eigenvector[j];
}
}
// Normalize
const norm = Math.sqrt(newEigenvector.reduce((sum, val) => sum + val * val, 0));
if (norm === 0) break;
for (let i = 0; i < n; i++) {
newEigenvector[i] /= norm;
}
// Check convergence
let maxDiff = 0;
for (let i = 0; i < n; i++) {
maxDiff = Math.max(maxDiff, Math.abs(newEigenvector[i] - eigenvector[i]));
}
eigenvector = newEigenvector;
if (maxDiff < tolerance) break;
}
// Convert back to node-indexed object
const centrality = {};
nodes.forEach((node, i) => {
centrality[node] = eigenvector[i];
});
return centrality;
}
function findAllShortestPaths(start, end) {
const visited = new Set();
const distances = {};
const paths = {};
const queue = [{node: start, path: [start], distance: 0}];
distances[start] = 0;
paths[start] = [[start]];
let shortestDistance = Infinity;
const allPaths = [];
while (queue.length > 0) {
const {node, path, distance} = queue.shift();
if (distance > shortestDistance) continue;
if (node === end) {
if (distance < shortestDistance) {
shortestDistance = distance;
allPaths.length = 0; // Clear previous longer paths
allPaths.push([...path]);
} else if (distance === shortestDistance) {
allPaths.push([...path]);
}
continue;
}
if (visited.has(node) && distances[node] < distance) continue;
visited.add(node);
distances[node] = distance;
// Find neighbors (both directions for undirected)
const neighbors = new Set();
graphData.links.forEach(l => {
const sourceId = l.source.id || l.source;
const targetId = l.target.id || l.target;
if (sourceId === node) neighbors.add(targetId);
if (targetId === node) neighbors.add(sourceId);
});
for (const neighbor of neighbors) {
if (!path.includes(neighbor)) {
queue.push({
node: neighbor,
path: [...path, neighbor],
distance: distance + 1
});
}
}
}
return allPaths;
}
function collapseAll() {
// Find root nodes (nodes with no parents)
const rootNodes = graphData.nodes.filter(node => {
const parents = getParents(node.id);
return parents.length === 0;
});
if (rootNodes.length === 0) {
showStatus("No root nodes found to collapse from.", "warning");
return;
}
let totalCollapsed = 0;
rootNodes.forEach(root => {
const descendants = getAllDescendants(root.id);
if (descendants.length > 0) {
collapsedNodes.add(root.id);
descendants.forEach(desc => hiddenNodes.add(desc));
totalCollapsed += descendants.length;
}
});
// Hide all links involving hidden nodes
graphData.links.forEach(link => {
const sourceId = link.source.id || link.source;
const targetId = link.target.id || link.target;
if (hiddenNodes.has(sourceId) || hiddenNodes.has(targetId)) {
hiddenLinks.add(`${sourceId}-${targetId}`);
}
});
updateVisualization();
showStatus(`Collapsed ${totalCollapsed} nodes from ${rootNodes.length} root(s).`, "success");
}
// Centrality execution functions
function runDegreeCentrality() {
if (graphData.nodes.length === 0) {
showStatus("No nodes to analyze!", "warning");
return;
}
showStatus("Calculating degree centrality...", "success");
currentCentralityScores = calculateDegreeCentrality();
currentCentralityType = "Degree";
applyCentralityVisualization();
showCentralityResults("Degree", currentCentralityScores);
}
function runBetweennessCentrality() {
if (graphData.nodes.length === 0) {
showStatus("No nodes to analyze!", "warning");
return;
}
if (graphData.nodes.length > 20) {
if (!confirm("Betweenness centrality calculation may be slow for large graphs. Continue?")) {
return;
}
}
showStatus("Calculating betweenness centrality...", "success");
currentCentralityScores = calculateBetweennessCentrality();
currentCentralityType = "Betweenness";
applyCentralityVisualization();
showCentralityResults("Betweenness", currentCentralityScores);
}
function runClosenessCentrality() {
if (graphData.nodes.length === 0) {
showStatus("No nodes to analyze!", "warning");
return;
}
showStatus("Calculating closeness centrality...", "success");
currentCentralityScores = calculateClosenessCentrality();
currentCentralityType = "Closeness";
applyCentralityVisualization();
showCentralityResults("Closeness", currentCentralityScores);
}
function runEigenvectorCentrality() {
if (graphData.nodes.length === 0) {
showStatus("No nodes to analyze!", "warning");
return;
}
showStatus("Calculating eigenvector centrality...", "success");
currentCentralityScores = calculateEigenvectorCentrality();
currentCentralityType = "Eigenvector";
applyCentralityVisualization();
showCentralityResults("Eigenvector", currentCentralityScores);
}
function clearCentralityVisualization() {
currentCentralityScores = null;
currentCentralityType = null;
// Reset node appearance
node.attr("r", 12)
.attr("fill", "#4CAF50");
clearOperationResults();
showStatus("Centrality visualization cleared.", "success");
}
function applyCentralityVisualization() {
if (!currentCentralityScores) return;
const visualizationType = document.getElementById('centralityVisualization').value;
const nodeIds = graphData.nodes.map(n => n.id);
// Get scores for current centrality type
let scores;
if (currentCentralityType === "Degree") {
scores = nodeIds.map(id => currentCentralityScores[id].totalDegree);
} else {
scores = nodeIds.map(id => currentCentralityScores[id] || 0);
}
const maxScore = Math.max(...scores);
const minScore = Math.min(...scores);
// Apply visualization
node.each(function(d, i) {
const score = currentCentralityType === "Degree" ?
currentCentralityScores[d.id].totalDegree :
currentCentralityScores[d.id] || 0;
const normalizedScore = maxScore > minScore ?
(score - minScore) / (maxScore - minScore) : 0;
const element = d3.select(this);
// Apply color
if (visualizationType === 'color' || visualizationType === 'both') {
const intensity = Math.round(255 * (1 - normalizedScore));
const color = `rgb(255, ${intensity}, ${intensity})`;
element.attr("fill", color);
}
// Apply size
if (visualizationType === 'size' || visualizationType === 'both') {
const radius = 8 + (normalizedScore * 12); // Range: 8-20
element.attr("r", radius);
}
if (visualizationType === 'color' && !(visualizationType === 'both')) {
element.attr("r", 12); // Keep default size
}
if (visualizationType === 'size' && !(visualizationType === 'both')) {
element.attr("fill", "#4CAF50"); // Keep default color
}
});
}
function showCentralityResults(type, scores) {
const resultsDiv = document.getElementById('operationResults');
const resultsPanel = document.getElementById('resultsPanel');
// Convert scores to array for sorting
let scoreArray;
if (type === "Degree") {
scoreArray = Object.entries(scores).map(([node, data]) => ({
node,
score: data.totalDegree,
details: `In: ${data.inDegree}, Out: ${data.outDegree}, Total: ${data.totalDegree}`
}));
} else {
scoreArray = Object.entries(scores).map(([node, score]) => ({
node,
score: score || 0,
details: `Score: ${(score || 0).toFixed(4)}`
}));
}
// Sort by score (descending)
scoreArray.sort((a, b) => b.score - a.score);
let description = "";
switch(type) {
case "Degree":
description = "Measures direct connectivity. Higher values indicate more connections.";
break;
case "Betweenness":
description = "Measures bridge/broker positions. Higher values indicate control over information flow.";
break;
case "Closeness":
description = "Measures communication efficiency. Higher values indicate faster information spread.";
break;
case "Eigenvector":
description = "Measures quality of connections. Higher values indicate connections to important nodes.";
break;
}
resultsDiv.innerHTML = `
<h4>${type} Centrality</h4>
<p style="font-size: 12px; color: #666; margin-bottom: 15px;">${description}</p>
<div>
${scoreArray.slice(0, 10).map((item, i) => `
<div class="result-item">
<strong>#${i + 1}: ${item.node}</strong><br>
<span style="font-size: 11px; color: #666;">${item.details}</span>
</div>
`).join('')}
${scoreArray.length > 10 ? `<div style="margin-top: 10px; font-size: 11px; color: #666; text-align: center;">... and ${scoreArray.length - 10} more nodes</div>` : ''}
</div>
`;
// Show the results panel
resultsPanel.classList.add('visible');
}
function addNode() {
const input = document.getElementById('nodeInput');
const nodeName = input.value.trim();
if (!nodeName) {
showStatus("Please enter a node name.", "warning");
return;
}
if (graphData.nodes.find(n => n.id === nodeName)) {
showStatus("Node already exists!", "error");
return;
}
graphData.nodes.push({
id: nodeName,
name: nodeName,
x: Math.random() * (width - 100) + 50,
y: Math.random() * (height - 100) + 50
});
input.value = '';
updateVisualization();
updateStats();
updateStartNodeOptions();
showStatus(`Added node: ${nodeName}`, "success");
}
function addEdge() {
if (selectedNodes.length !== 2) {
showStatus("Select exactly 2 nodes to add an edge.", "warning");
return;
}
const [source, target] = selectedNodes;
// Check if edge already exists
if (graphData.links.find(l => l.source.id === source && l.target.id === target)) {
showStatus("Edge already exists!", "error");
return;
}
graphData.links.push({
source: source,
target: target,
weight: 1
});
selectedNodes = [];
updateVisualization();
updateStats();
updateSelectionUI();
showStatus(`Added edge: ${source} → ${target}`, "success");
}
function clearGraph() {
if (graphData.nodes.length > 0 && !confirm('Clear all nodes and edges?')) {
return;
}
graphData = { nodes: [], links: [] };
selectedNodes = [];
collapsedNodes.clear();
hiddenNodes.clear();
hiddenLinks.clear();
updateVisualization();
updateStats();
updateStartNodeOptions();
clearOperationResults();
hideMatrix();
showStatus("Graph cleared.", "success");
}
function loadSample() {
graphData = {
nodes: [
{ id: "A", name: "A", x: width * 0.2, y: height * 0.3 },
{ id: "B", name: "B", x: width * 0.4, y: height * 0.2 },
{ id: "C", name: "C", x: width * 0.6, y: height * 0.3 },
{ id: "D", name: "D", x: width * 0.4, y: height * 0.6 },
{ id: "E", name: "E", x: width * 0.5, y: height * 0.8 }
],
links: [
{ source: "A", target: "B", weight: 1 },
{ source: "B", target: "C", weight: 1 },
{ source: "B", target: "D", weight: 1 },
{ source: "C", target: "E", weight: 1 },
{ source: "D", target: "E", weight: 1 }
]
};
selectedNodes = [];
collapsedNodes.clear();
hiddenNodes.clear();
hiddenLinks.clear();
updateVisualization();
updateStats();
updateStartNodeOptions();
showStatus("Sample graph loaded!", "success");
}
function loadJSON() {
document.getElementById('jsonFileInput').click();
}
function handleJSONFile(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = JSON.parse(e.target.result);
// Handle different JSON formats
if (data.nodes && data.links) {
// D3 format
graphData = data;
} else if (data.nodes && data.edges) {
// CLI format - convert edges to links
graphData = {
nodes: data.nodes.map(n => ({
id: n.id,
name: n.label || n.id,
x: n.x || Math.random() * (width - 100) + 50,
y: n.y || Math.random() * (height - 100) + 50
})),
links: data.edges.map(e => ({
source: e.from,
target: e.to,
weight: e.weight || 1
}))
};
} else {
throw new Error("Unsupported JSON format");
}
selectedNodes = [];
collapsedNodes.clear();
hiddenNodes.clear();
hiddenLinks.clear();
updateVisualization();
updateStats();
updateStartNodeOptions();
showStatus(`JSON loaded: ${graphData.nodes.length} nodes, ${graphData.links.length} edges`, "success");
} catch (error) {
showStatus(`Error loading JSON: ${error.message}`, "error");
}
};
reader.readAsText(file);
}
function runDFS() {
const startNodeId = document.getElementById('startNode').value;
if (!startNodeId) {
showStatus("Please select a start node.", "warning");
return;
}
clearHighlights();
const visited = new Set();
const path = [];
function dfsRecursive(nodeId) {
if (visited.has(nodeId)) return;
visited.add(nodeId);
path.push(nodeId);
// Find neighbors (outgoing edges)
const neighbors = graphData.links
.filter(l => (l.source.id || l.source) === nodeId)
.map(l => l.target.id || l.target);
neighbors.forEach(neighbor => {
if (!visited.has(neighbor)) {
dfsRecursive(neighbor);
}
});
}
dfsRecursive(startNodeId);
highlightPath(path, startNodeId);
showOperationResults("DFS Traversal", path, startNodeId);
}
function runBFS() {
const startNodeId = document.getElementById('startNode').value;
if (!startNodeId) {
showStatus("Please select a start node.", "warning");
return;
}
clearHighlights();
const visited = new Set();
const queue = [startNodeId];
const path = [];
while (queue.length > 0) {
const current = queue.shift();
if (visited.has(current)) continue;
visited.add(current);
path.push(current);
// Find neighbors (outgoing edges)
const neighbors = graphData.links
.filter(l => (l.source.id || l.source) === current)
.map(l => l.target.id || l.target);
neighbors.forEach(neighbor => {
if (!visited.has(neighbor)) {
queue.push(neighbor);
}
});
}
highlightPath(path, startNodeId);
showOperationResults("BFS Traversal", path, startNodeId);
}
function findPath() {
const fromNodeId = document.getElementById('fromNode').value;
const toNodeId = document.getElementById('toNode').value;
if (!fromNodeId || !toNodeId) {
showStatus("Please select both from and to nodes.", "warning");
return;
}
if (fromNodeId === toNodeId) {
showStatus("From and to nodes cannot be the same.", "warning");
return;
}
clearHighlights();
// Find shortest path using BFS
const path = findShortestPath(fromNodeId, toNodeId);
if (path.length === 0) {
showStatus(`No path found between ${fromNodeId} and ${toNodeId}.`, "error");
showOperationResults("Path Finding", [], fromNodeId);
return;
}
// Highlight the path
highlightPathBetweenNodes(path, fromNodeId, toNodeId);
showOperationResults("Shortest Path", path, fromNodeId, toNodeId);
showStatus(`Path found: ${path.join(' → ')}`, "success");
}
function findShortestPath(start, end) {
const visited = new Set();
const queue = [{node: start, path: [start]}];
while (queue.length > 0) {
const {node, path} = queue.shift();
if (node === end) {
return path;
}
if (visited.has(node)) {
continue;
}
visited.add(node);
// Find all neighbors (both outgoing and incoming edges for undirected behavior)
const neighbors = new Set();
// Outgoing edges
graphData.links.forEach(l => {
if ((l.source.id || l.source) === node) {
neighbors.add(l.target.id || l.target);
}
// Incoming edges (treat as undirected)
if ((l.target.id || l.target) === node) {
neighbors.add(l.source.id || l.source);
}
});
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
queue.push({
node: neighbor,
path: [...path, neighbor]
});
}
}
}
return []; // No path found
}
function highlightPathBetweenNodes(path, fromNode, toNode) {
// Clear all previous highlights
node.classed("highlighted", false)
.classed("start-node", false)
.classed("end-node", false)
.classed("path-node", false)
.classed("path-intermediate", false);
link.classed("highlighted", false)
.classed("path-link", false);
// Highlight nodes in the path
node.classed("start-node", d => d.id === fromNode)
.classed("end-node", d => d.id === toNode)
.classed("path-intermediate", d => path.includes(d.id) && d.id !== fromNode && d.id !== toNode);
// Highlight edges in the path
for (let i = 0; i < path.length - 1; i++) {
const sourceId = path[i];
const targetId = path[i + 1];
link.classed("path-link", l => {
const linkSource = l.source.id || l.source;
const linkTarget = l.target.id || l.target;
return (linkSource === sourceId && linkTarget === targetId) ||
(linkSource === targetId && linkTarget === sourceId); // Handle undirected
});
}
}
function highlightPath(path, startNodeId) {
// Clear all previous highlights
node.classed("highlighted", false)
.classed("start-node", false)
.classed("path-node", false);
link.classed("highlighted", false)
.classed("path-link", false);
// Apply new highlights
node.classed("start-node", d => d.id === startNodeId)
.classed("path-node", d => path.includes(d.id) && d.id !== startNodeId);
// Highlight edges in path order
for (let i = 0; i < path.length - 1; i++) {
const sourceId = path[i];
const targetId = path[i + 1];
link.classed("highlighted", l =>
(l.source.id || l.source) === sourceId &&
(l.target.id || l.target) === targetId
);
}
}
function clearHighlights() {
node.classed("highlighted", false)
.classed("start-node", false)
.classed("end-node", false)
.classed("path-node", false)
.classed("path-intermediate", false)
.classed("selected", false);
link.classed("highlighted", false)
.classed("path-link", false);
}
function showOperationResults(operation, results, startNode, endNode = null) {
const resultsDiv = document.getElementById('operationResults');
const resultsPanel = document.getElementById('resultsPanel');
if (operation === "Shortest Path") {
resultsDiv.innerHTML = `
<h4>${operation}</h4>
<p style="font-size: 12px; margin-bottom: 15px;"><strong>From:</strong> ${startNode} <strong>To:</strong> ${endNode}</p>
<p style="font-size: 12px; margin-bottom: 15px;"><strong>Path:</strong> ${results.join(' → ')}</p>
<p style="font-size: 12px; margin-bottom: 15px;"><strong>Length:</strong> ${results.length} nodes (${results.length - 1} edges)</p>
<div>
${results.map((node, i) => `
<div class="result-item">
<strong>Step ${i + 1}:</strong> ${node}${i === 0 ? ' (start)' : i === results.length - 1 ? ' (end)' : ''}
</div>
`).join('')}
</div>
`;
} else {
resultsDiv.innerHTML = `
<h4>${operation}</h4>
<p style="font-size: 12px; margin-bottom: 15px;"><strong>Start:</strong> ${startNode} | <strong>Visited:</strong> ${results.length} nodes</p>
<div>
${results.map((node, i) => `
<div class="result-item">
<strong>Step ${i + 1}:</strong> ${node}
</div>
`).join('')}
</div>
`;
}
// Show the results panel
resultsPanel.classList.add('visible');
}
function clearOperationResults() {
document.getElementById('operationResults').innerHTML =
'<p style="color: #666; font-style: italic;">Run an operation to see results...</p>';
}
function exportGraph() {
if (graphData.nodes.length === 0) {
showStatus("No graph to export!", "warning");
return;
}
// Create adjacency matrix
const nodes = graphData.nodes.map(n => n.id);
const matrix = nodes.map(() => nodes.map(() => 0));
graphData.links.forEach(link => {
const sourceId = link.source.id || link.source;
const targetId = link.target.id || link.target;
const sourceIndex = nodes.indexOf(sourceId);
const targetIndex = nodes.indexOf(targetId);
if (sourceIndex >= 0 && targetIndex >= 0) {
matrix[sourceIndex][targetIndex] = link.weight || 1;
}
});
// Get custom filename
const filenameInput = document.getElementById('exportFilename');
let filename = filenameInput.value.trim() || 'graph_matrix';
// Ensure .csv extension
if (!filename.endsWith('.csv')) {
filename += '.csv';
}
// Create CSV content
const csvContent = matrix.map(row => row.join(',')).join('\\n');
// Download file
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
// Show matrix in panel
showMatrix(matrix, nodes);
showStatus(`Graph exported as ${filename}!`, "success");
}
function exportJSON() {
if (graphData.nodes.length === 0) {
showStatus("No graph to export!", "warning");
return;
}
// Export in CLI-compatible format
const exportData = {
nodes: graphData.nodes.map(n => ({
id: n.id,
label: n.name || n.id,
x: n.x || 0,
y: n.y || 0
})),
edges: graphData.links.map(l => ({
from: l.source.id || l.source,
to: l.target.id || l.target,
weight: l.weight || 1,
label: ""
})),
properties: {
vertices: graphData.nodes.length,
edges: graphData.links.length,
density: calculateDensity()
}
};
// Get custom filename
const filenameInput = document.getElementById('exportFilename');
let filename = filenameInput.value.trim() || 'graph_export';
// Ensure .json extension
if (!filename.endsWith('.json')) {
filename += '.json';
}
const jsonContent = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonContent], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
showStatus(`Graph exported as ${filename}!`, "success");
}
function showMatrix(matrix, nodeLabels) {
const matrixDiv = document.getElementById('matrixDisplay');
let matrixText = ' ' + nodeLabels.join(' ') + '\\n';
matrix.forEach((row, i) => {
matrixText += nodeLabels[i].padEnd(3) + ' ' + row.join(' ') + '\\n';
});
matrixDiv.textContent = matrixText;
matrixDiv.style.display = 'block';
}
function hideMatrix() {
document.getElementById('matrixDisplay').style.display = 'none';
}
function calculateDensity() {
const nodeCount = graphData.nodes.length;
const edgeCount = graphData.links.length;
const maxEdges = nodeCount * (nodeCount - 1);
return maxEdges > 0 ? (edgeCount / maxEdges) : 0;
}
function updateVisualization() {
// Filter visible links
const visibleLinks = graphData.links.filter(l => {
const sourceId = l.source.id || l.source;
const targetId = l.target.id || l.target;
const linkKey = `${sourceId}-${targetId}`;
return !hiddenLinks.has(linkKey) && !hiddenNodes.has(sourceId) && !hiddenNodes.has(targetId);
});
// Filter visible nodes
const visibleNodes = graphData.nodes.filter(n => !hiddenNodes.has(n.id));
// Update links
link = link.data(visibleLinks, d => `${d.source.id || d.source}-${d.target.id || d.target}`);
link.exit().remove();
link = link.enter().append("line")
.attr("class", "link")
.merge(link);
// Update nodes
node = node.data(visibleNodes, d => d.id);
node.exit().remove();
const nodeEnter = node.enter().append("circle")
.attr("class", "node")
.attr("r", 12)
.attr("fill", "#4CAF50")
.on("click", handleNodeClick)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node = nodeEnter.merge(node);
// Update node appearance based on collapsed state and children
node.classed("collapsed", d => collapsedNodes.has(d.id))
.classed("has-children", d => {
const children = getChildren(d.id);
return children.length > 0 && !collapsedNodes.has(d.id);
});
// Update labels
label = label.data(visibleNodes, d => d.id);
label.exit().remove();
label = label.enter().append("text")
.attr("class", "node-label")
.merge(label)
.text(d => d.name);
// Update simulation with visible nodes and links
simulation.nodes(visibleNodes);
simulation.force("link").links(visibleLinks);
simulation.alpha(0.3).restart();
simulation.on("tick", () => {
// Constrain nodes to canvas bounds
graphData.nodes.forEach(d => {
d.x = Math.max(25, Math.min(width - 25, d.x));
d.y = Math.max(25, Math.min(height - 25, d.y));
});
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
label
.attr("x", d => d.x)
.attr("y", d => d.y);
});
// Always update dropdowns when visualization changes
updateStartNodeOptions();
}
function handleNodeClick(event, d) {
event.stopPropagation();
// Check for modifier keys
const isCtrlClick = event.ctrlKey || event.metaKey; // Ctrl on Windows/Linux, Cmd on Mac
const isShiftClick = event.shiftKey;
if (isCtrlClick) {
// Ctrl+Click: Toggle collapse/expand
if (collapsedNodes.has(d.id)) {
expandNode(d.id);
showStatus(`Expanded node: ${d.id}`, "success");
} else {
const descendants = getAllDescendants(d.id);
if (descendants.length > 0) {
collapseNode(d.id);
showStatus(`Collapsed node: ${d.id} (${descendants.length} children hidden)`, "success");
} else {
showStatus(`Node ${d.id} has no children to collapse.`, "warning");
}
}
return;
}
if (isShiftClick) {
// Shift+Click: Quick expand all descendants (if collapsed) or collapse all descendants
if (collapsedNodes.has(d.id)) {
expandAllDescendants(d.id);
showStatus(`Fully expanded node: ${d.id}`, "success");
} else {
collapseAllDescendants(d.id);
showStatus(`Fully collapsed node: ${d.id}`, "success");
}
return;
}
// Regular click: Node selection for edge creation
if (selectedNodes.includes(d.id)) {
selectedNodes = selectedNodes.filter(id => id !== d.id);
} else if (selectedNodes.length < 2) {
selectedNodes.push(d.id);
} else {
selectedNodes = [d.id];
}
// Update node selection visual
node.classed("selected", n => selectedNodes.includes(n.id));
updateSelectionUI();
}
function updateSelectionUI() {
const addEdgeBtn = document.getElementById('addEdgeBtn');
const selectionInfo = document.getElementById('selectionInfo');
if (selectedNodes.length === 0) {
addEdgeBtn.disabled = true;
selectionInfo.textContent = "Select 2 nodes to add edge";
} else if (selectedNodes.length === 1) {
addEdgeBtn.disabled = true;
selectionInfo.textContent = `Selected: ${selectedNodes[0]} (select 1 more)`;
} else if (selectedNodes.length === 2) {
addEdgeBtn.disabled = false;
selectionInfo.textContent = `Selected: ${selectedNodes.join(' → ')}`;
}
}
function updateStats() {
document.getElementById('nodeCount').textContent = graphData.nodes.length;
document.getElementById('edgeCount').textContent = graphData.links.length;
document.getElementById('density').textContent = calculateDensity().toFixed(3);
}
function updateStartNodeOptions() {
const startSelect = document.getElementById('startNode');
const fromSelect = document.getElementById('fromNode');
const toSelect = document.getElementById('toNode');
const collapseSelect = document.getElementById('collapseNode');
// Debug logging
console.log('updateStartNodeOptions called, nodes:', graphData.nodes.length);
// Clear existing options
startSelect.innerHTML = '<option value="">Select node...</option>';
fromSelect.innerHTML = '<option value="">Select from node...</option>';
toSelect.innerHTML = '<option value="">Select to node...</option>';
collapseSelect.innerHTML = '<option value="">Select node to collapse...</option>';
// Populate all dropdowns
graphData.nodes.forEach(node => {
// Start node dropdown
const startOption = document.createElement('option');
startOption.value = node.id;
startOption.textContent = node.name;
startSelect.appendChild(startOption);
// From node dropdown
const fromOption = document.createElement('option');
fromOption.value = node.id;
fromOption.textContent = node.name;
fromSelect.appendChild(fromOption);
// To node dropdown
const toOption = document.createElement('option');
toOption.value = node.id;
toOption.textContent = node.name;
toSelect.appendChild(toOption);
// Collapse node dropdown (only visible nodes)
if (!hiddenNodes.has(node.id)) {
const collapseOption = document.createElement('option');
collapseOption.value = node.id;
const isCollapsed = collapsedNodes.has(node.id);
collapseOption.textContent = `${node.name}${isCollapsed ? ' (collapsed)' : ''}`;
collapseSelect.appendChild(collapseOption);
}
});
// Enable/disable operation buttons
const hasNodes = graphData.nodes.length > 0;
const hasVisibleNodes = graphData.nodes.filter(n => !hiddenNodes.has(n.id)).length > 0;
document.getElementById('dfsBtn').disabled = !hasNodes;
document.getElementById('bfsBtn').disabled = !hasNodes;
document.getElementById('pathBtn').disabled = !hasNodes;
document.getElementById('collapseBtn').disabled = !hasVisibleNodes;
// Enable/disable centrality buttons
document.getElementById('degreeBtn').disabled = !hasNodes;
document.getElementById('betweennessBtn').disabled = !hasNodes;
document.getElementById('closenessBtn').disabled = !hasNodes;
document.getElementById('eigenvectorBtn').disabled = !hasNodes;
// Update collapse button text based on selection
const collapseBtn = document.getElementById('collapseBtn');
collapseSelect.addEventListener('change', function() {
const selectedNode = this.value;
if (selectedNode && collapsedNodes.has(selectedNode)) {
collapseBtn.textContent = 'Expand';
} else {
collapseBtn.textContent = 'Collapse';
}
collapseBtn.disabled = !selectedNode;
});
}
function findPath() {
const fromNodeId = document.getElementById('fromNode').value;
const toNodeId = document.getElementById('toNode').value;
if (!fromNodeId || !toNodeId) {
showStatus("Please select both from and to nodes.", "warning");
return;
}
if (fromNodeId === toNodeId) {
showStatus("From and to nodes cannot be the same.", "warning");
return;
}
clearHighlights();
const path = findShortestPath(fromNodeId, toNodeId);
if (path.length === 0) {
showStatus(`No path found from ${fromNodeId} to ${toNodeId}`, "error");
return;
}
highlightPathBetweenNodes(path, fromNodeId, toNodeId);
showOperationResults("Shortest Path", path, fromNodeId, toNodeId);
}
function findShortestPath(start, end) {
const visited = new Set();
const queue = [{node: start, path: [start]}];
while (queue.length > 0) {
const {node, path} = queue.shift();
if (node === end) {
return path;
}
if (visited.has(node)) {
continue;
}
visited.add(node);
// Find all neighbors (both outgoing and incoming edges for undirected behavior)
const neighbors = new Set();
// Outgoing edges
graphData.links.forEach(l => {
if ((l.source.id || l.source) === node) {
neighbors.add(l.target.id || l.target);
}
// Incoming edges (treat as undirected)
if ((l.target.id || l.target) === node) {
neighbors.add(l.source.id || l.source);
}
});
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
queue.push({
node: neighbor,
path: [...path, neighbor]
});
}
}
}
return []; // No path found
}
function highlightPathBetweenNodes(path, startNodeId, endNodeId) {
// Clear all previous highlights
node.classed("highlighted", false)
.classed("start-node", false)
.classed("end-node", false)
.classed("path-intermediate", false);
link.classed("highlighted", false)
.classed("path-link", false);
// Apply new highlights
node.classed("start-node", d => d.id === startNodeId)
.classed("end-node", d => d.id === endNodeId)
.classed("path-intermediate", d =>
path.includes(d.id) && d.id !== startNodeId && d.id !== endNodeId);
// Highlight edges in path order
for (let i = 0; i < path.length - 1; i++) {
const sourceId = path[i];
const targetId = path[i + 1];
link.classed("path-link", l => {
const linkSource = l.source.id || l.source;
const linkTarget = l.target.id || l.target;
// Check both directions for undirected graph
return (linkSource === sourceId && linkTarget === targetId) ||
(linkSource === targetId && linkTarget === sourceId);
});
}
}
function showStatus(message, type) {
const statusDiv = document.getElementById('statusMessage');
statusDiv.innerHTML = `<div class="status-message status-${type}">${message}</div>`;
setTimeout(() => {
statusDiv.innerHTML = '';
}, 3000);
}
// Drag functions
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = Math.max(25, Math.min(width - 25, event.x));
d.fy = Math.max(25, Math.min(height - 25, event.y));
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
// Clear selection when clicking on empty space
svg.on("click", function() {
selectedNodes = [];
node.classed("selected", false);
updateSelectionUI();
});
// Update centrality visualization when type changes
document.getElementById('centralityVisualization').addEventListener('change', function() {
if (currentCentralityScores) {
applyCentralityVisualization();
}
});
</script>
</body>
</html>