Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ function App() {
);
}

export default App;
export default App;
54 changes: 54 additions & 0 deletions src/algorithms/graph/kruskal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// src/algorithms/graph/kruskal.js

class DSU {
constructor(n) {
this.parent = Array.from({ length: n }, (_, i) => i);
this.rank = Array(n).fill(0);
}

find(x) {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]);
}
return this.parent[x];
}

union(x, y) {
const rx = this.find(x);
const ry = this.find(y);
if (rx === ry) return false;

if (this.rank[rx] < this.rank[ry]) this.parent[rx] = ry;
else if (this.rank[rx] > this.rank[ry]) this.parent[ry] = rx;
else {
this.parent[ry] = rx;
this.rank[rx]++;
}
return true;
}

getState() {
return [...this.parent];
}
}

// 🧠 Generator that yields every visualization step
export function* kruskalSteps(edges, nodeCount) {
const sortedEdges = [...edges].sort((a, b) => a.weight - b.weight);
const dsu = new DSU(nodeCount);
const mst = [];

for (let edge of sortedEdges) {
yield { type: "consider", edge, dsu: dsu.getState() };

const merged = dsu.union(edge.from - 1, edge.to - 1);
if (merged) {
mst.push(edge);
yield { type: "add", edge, mst: [...mst], dsu: dsu.getState() };
} else {
yield { type: "skip", edge, dsu: dsu.getState() };
}
}

yield { type: "done", mst, dsu: dsu.getState() };
}
262 changes: 262 additions & 0 deletions src/components/graph/KruskalVisualizer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import React, { useState, useRef } from "react";
import { kruskalSteps } from "../../algorithms/graph/kruskal";

export default function KruskalVisualizer() {
const svgRef = useRef(null);
const [nodes, setNodes] = useState([]);
const [edges, setEdges] = useState([]);
const [selected, setSelected] = useState({ from: null, to: null });
const [running, setRunning] = useState(false);
const [status, setStatus] = useState("Idle");
const [stepIndex, setStepIndex] = useState(0);
const [dsuState, setDsuState] = useState([]);
const [highlight, setHighlight] = useState(null);

// 🟢 Add node
const addNode = (e) => {
if (running) return;
const rect = svgRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const newNode = {
id: nodes.length + 1,
x,
y,
label: `N${nodes.length + 1}`,
};
setNodes([...nodes, newNode]);
};

// 🟣 Add edges
const handleNodeClick = (id) => {
if (running) return;
if (!selected.from) {
setSelected({ from: id, to: null });
} else if (selected.from && !selected.to && selected.from !== id) {
const weight = parseInt(prompt("Enter edge weight:"), 10);
if (!isNaN(weight)) {
setEdges([
...edges,
{ from: selected.from, to: id, weight, colorClass: "stroke-gray-500" },
]);
}
setSelected({ from: null, to: null });
}
};

// 🧩 Kruskal visualization
const startVisualization = async () => {
if (running || edges.length === 0) return;
setRunning(true);
setStatus("Running...");
const steps = kruskalSteps(edges, nodes.length);
const interval = 1000;

for (let step of steps) {
const { type, edge, dsu } = step;
setDsuState(dsu || []);
setHighlight({ from: edge.from - 1, to: edge.to - 1 });

setEdges((prev) =>
prev.map((e) =>
e.from === edge.from && e.to === edge.to
? {
...e,
colorClass:
type === "consider"
? "stroke-blue-400"
: type === "add"
? "stroke-green-400"
: "stroke-red-500",
}
: e
)
);

setStatus(
type === "consider"
? `Considering edge (${edge.from}, ${edge.to})`
: type === "add"
? `Added edge (${edge.from}, ${edge.to}) to MST`
: type === "skip"
? `Skipped edge (${edge.from}, ${edge.to}) — forms cycle`
: "Done"
);

await new Promise((r) => setTimeout(r, interval));
setStepIndex((i) => i + 1);
}

setRunning(false);
setStatus("Completed!");
};

const resetGraph = () => {
setNodes([]);
setEdges([]);
setSelected({ from: null, to: null });
setRunning(false);
setStatus("Idle");
setDsuState([]);
setStepIndex(0);
};

// 🧩 Render edges
const renderEdges = () =>
edges.map((edge, i) => {
const fromNode = nodes.find((n) => n.id === edge.from);
const toNode = nodes.find((n) => n.id === edge.to);
if (!fromNode || !toNode) return null;

const { x: x1, y: y1 } = fromNode;
const { x: x2, y: y2 } = toNode;
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;

return (
<g key={i}>
<line
x1={x1}
y1={y1}
x2={x2}
y2={y2}
strokeWidth={4}
className={`${edge.colorClass} transition-all duration-500`}
strokeLinecap="round"
/>
<text
x={cx}
y={cy - 8}
fontSize={12}
textAnchor="middle"
className="fill-indigo-300"
>
{edge.weight}
</text>
</g>
);
});

// 🧩 Render nodes
const renderNodes = () =>
nodes.map((n) => (
<g
key={n.id}
transform={`translate(${n.x}, ${n.y})`}
onClick={() => handleNodeClick(n.id)}
className="cursor-pointer"
>
<circle
r={18}
className={`stroke-indigo-400 stroke-2 ${
selected.from === n.id ? "fill-indigo-700" : "fill-gray-900"
} transition-all duration-300`}
/>
<text
x={0}
y={5}
textAnchor="middle"
className="fill-indigo-200 font-semibold"
>
{n.label}
</text>
</g>
));

return (
<div className="w-full">
{/* 🧭 Manual / Instructions */}
<div className="max-w-4xl mx-auto mb-6 p-4 bg-gray-900 border border-gray-700 rounded-lg shadow-md text-sm text-gray-300">
<h3 className="text-indigo-400 font-bold mb-2 text-center">
How to Use the Kruskal Visualizer
</h3>
<ul className="list-disc pl-6 space-y-1">
<li> <b>Double-click</b> on the canvas to create a node.</li>
<li> <b>Click one node</b> and then another to create an edge.</li>
<li> You’ll be prompted to enter the <b>edge weight</b>.</li>
<li> Once your graph is ready, click <b>Start Visualization</b>.</li>
<li> The algorithm will highlight edges being considered, added, or skipped.</li>
<li> The <b>DSU panel</b> on the left updates in real time to show connected components.</li>
<li> Click <b>Reset</b> to clear and start again.</li>
</ul>
</div>
{/* Controls */}
<div className="flex gap-4 mb-6 justify-center">
<button
className={`px-6 py-2 rounded-lg font-semibold text-white shadow-md transition-all duration-300 ${
running
? "bg-indigo-800 text-gray-400 cursor-not-allowed"
: "bg-indigo-600 hover:bg-indigo-500"
}`}
onClick={startVisualization}
disabled={running}
>
{running ? "Visualizing..." : "Start Visualization"}
</button>

<button
onClick={resetGraph}
className="bg-gray-700 hover:bg-gray-600 px-6 py-2 rounded-lg text-white font-semibold shadow-md transition-all duration-300"
>
Reset
</button>
</div>

{/* Status + Step */}
<div className="text-sm text-center text-indigo-300 mb-4">
<p>
Status: <span className="font-medium text-indigo-400">{status}</span>
</p>
<p>
Step: <span className="font-medium text-indigo-400">{stepIndex}</span>
</p>
</div>



{/* Layout: DSU Panel + Graph */}
<div className="flex w-full max-w-6xl mx-auto">
{/* Left DSU Panel */}
<div className="w-1/4 bg-gray-900 border border-gray-700 rounded-lg p-3 mr-6 shadow-lg">
<h2 className="text-lg font-bold mb-3 text-center text-indigo-400">
Disjoint Set (DSU)
</h2>

<div className="flex flex-wrap justify-center gap-3">
{nodes.map((n, i) => (
<div key={n.id} className="relative text-center">
<div className="text-sm font-medium text-indigo-300 mb-1">
{n.label}
</div>
<div
className={`w-10 h-10 flex items-center justify-center rounded-full border border-indigo-500 ${
dsuState[i] === i ? "bg-green-700" : "bg-indigo-800"
}`}
>
<span className="text-indigo-200 font-semibold">
N{(dsuState[i] ?? i) + 1}
</span>
</div>
</div>
))}
</div>
</div>

{/* Right Graph */}
<div className="flex-1 border border-gray-700 rounded-lg overflow-hidden shadow-lg">
<svg
ref={svgRef}
onDoubleClick={addNode}
width="100%"
height={500}
viewBox="0 0 800 500"
className="bg-gray-950"
>
{renderEdges()}
{renderNodes()}
</svg>
</div>
</div>
</div>
);
}
13 changes: 8 additions & 5 deletions src/pages/graph/GraphPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import React, { useState } from "react";
import { Network, Compass, Rocket } from "lucide-react";
import BellmanFord from "./BellmanFord";
import UnionFindPage from "./UnionFind";
// import Dijkstra from "./Dijkstra";
// import Kruskal from "./Kruskal";
import Kruskal from "./Kruskal";

export default function GraphPage() {
const [selectedAlgo, setSelectedAlgo] = useState("");
Expand All @@ -22,10 +21,14 @@ export default function GraphPage() {
<UnionFindPage />
</div>
);
case "kruskal":
return (
<div className="w-full h-full overflow-auto p-">
<Kruskal />
</div>
);
// case "dijkstra":
// return <Dijkstra />;
// case "kruskal":
// return <Kruskal />;
default:
return (
<div className="flex flex-col items-center justify-center text-center p-6">
Expand Down Expand Up @@ -69,7 +72,7 @@ export default function GraphPage() {
<option value="">Select Algorithm</option>
<option value="bellman-ford">Bellman–Ford</option>
<option value="union-find">Union Find</option>
{/* <option value="kruskal">Kruskal</option> */}
<option value="kruskal">Kruskal</option> {/* ✅ New dropdown option */}
</select>

<button
Expand Down
13 changes: 13 additions & 0 deletions src/pages/graph/Kruskal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";
import KruskalVisualizer from "../../components/graph/KruskalVisualizer";

export default function Kruskal() {
return (
<div className="min-h-screen bg-black text-gray-200 flex flex-col items-center p-6">
<h1 className="text-4xl font-extrabold mb-8 text-indigo-400 drop-shadow-lg">
Kruskal’s Minimum Spanning Tree Visualizer
</h1>
<KruskalVisualizer />
</div>
);
}
Loading