Skip to content

Commit 1174033

Browse files
Merge pull request #34 from shivam7147/main
Kruskal Algorithm Visualizer
2 parents e696d27 + 87eeeaf commit 1174033

File tree

5 files changed

+338
-6
lines changed

5 files changed

+338
-6
lines changed

src/App.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ function App() {
1818
);
1919
}
2020

21-
export default App;
21+
export default App;

src/algorithms/graph/kruskal.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// src/algorithms/graph/kruskal.js
2+
3+
class DSU {
4+
constructor(n) {
5+
this.parent = Array.from({ length: n }, (_, i) => i);
6+
this.rank = Array(n).fill(0);
7+
}
8+
9+
find(x) {
10+
if (this.parent[x] !== x) {
11+
this.parent[x] = this.find(this.parent[x]);
12+
}
13+
return this.parent[x];
14+
}
15+
16+
union(x, y) {
17+
const rx = this.find(x);
18+
const ry = this.find(y);
19+
if (rx === ry) return false;
20+
21+
if (this.rank[rx] < this.rank[ry]) this.parent[rx] = ry;
22+
else if (this.rank[rx] > this.rank[ry]) this.parent[ry] = rx;
23+
else {
24+
this.parent[ry] = rx;
25+
this.rank[rx]++;
26+
}
27+
return true;
28+
}
29+
30+
getState() {
31+
return [...this.parent];
32+
}
33+
}
34+
35+
// 🧠 Generator that yields every visualization step
36+
export function* kruskalSteps(edges, nodeCount) {
37+
const sortedEdges = [...edges].sort((a, b) => a.weight - b.weight);
38+
const dsu = new DSU(nodeCount);
39+
const mst = [];
40+
41+
for (let edge of sortedEdges) {
42+
yield { type: "consider", edge, dsu: dsu.getState() };
43+
44+
const merged = dsu.union(edge.from - 1, edge.to - 1);
45+
if (merged) {
46+
mst.push(edge);
47+
yield { type: "add", edge, mst: [...mst], dsu: dsu.getState() };
48+
} else {
49+
yield { type: "skip", edge, dsu: dsu.getState() };
50+
}
51+
}
52+
53+
yield { type: "done", mst, dsu: dsu.getState() };
54+
}
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import React, { useState, useRef } from "react";
2+
import { kruskalSteps } from "../../algorithms/graph/kruskal";
3+
4+
export default function KruskalVisualizer() {
5+
const svgRef = useRef(null);
6+
const [nodes, setNodes] = useState([]);
7+
const [edges, setEdges] = useState([]);
8+
const [selected, setSelected] = useState({ from: null, to: null });
9+
const [running, setRunning] = useState(false);
10+
const [status, setStatus] = useState("Idle");
11+
const [stepIndex, setStepIndex] = useState(0);
12+
const [dsuState, setDsuState] = useState([]);
13+
const [highlight, setHighlight] = useState(null);
14+
15+
// 🟢 Add node
16+
const addNode = (e) => {
17+
if (running) return;
18+
const rect = svgRef.current.getBoundingClientRect();
19+
const x = e.clientX - rect.left;
20+
const y = e.clientY - rect.top;
21+
const newNode = {
22+
id: nodes.length + 1,
23+
x,
24+
y,
25+
label: `N${nodes.length + 1}`,
26+
};
27+
setNodes([...nodes, newNode]);
28+
};
29+
30+
// 🟣 Add edges
31+
const handleNodeClick = (id) => {
32+
if (running) return;
33+
if (!selected.from) {
34+
setSelected({ from: id, to: null });
35+
} else if (selected.from && !selected.to && selected.from !== id) {
36+
const weight = parseInt(prompt("Enter edge weight:"), 10);
37+
if (!isNaN(weight)) {
38+
setEdges([
39+
...edges,
40+
{ from: selected.from, to: id, weight, colorClass: "stroke-gray-500" },
41+
]);
42+
}
43+
setSelected({ from: null, to: null });
44+
}
45+
};
46+
47+
// 🧩 Kruskal visualization
48+
const startVisualization = async () => {
49+
if (running || edges.length === 0) return;
50+
setRunning(true);
51+
setStatus("Running...");
52+
const steps = kruskalSteps(edges, nodes.length);
53+
const interval = 1000;
54+
55+
for (let step of steps) {
56+
const { type, edge, dsu } = step;
57+
setDsuState(dsu || []);
58+
setHighlight({ from: edge.from - 1, to: edge.to - 1 });
59+
60+
setEdges((prev) =>
61+
prev.map((e) =>
62+
e.from === edge.from && e.to === edge.to
63+
? {
64+
...e,
65+
colorClass:
66+
type === "consider"
67+
? "stroke-blue-400"
68+
: type === "add"
69+
? "stroke-green-400"
70+
: "stroke-red-500",
71+
}
72+
: e
73+
)
74+
);
75+
76+
setStatus(
77+
type === "consider"
78+
? `Considering edge (${edge.from}, ${edge.to})`
79+
: type === "add"
80+
? `Added edge (${edge.from}, ${edge.to}) to MST`
81+
: type === "skip"
82+
? `Skipped edge (${edge.from}, ${edge.to}) — forms cycle`
83+
: "Done"
84+
);
85+
86+
await new Promise((r) => setTimeout(r, interval));
87+
setStepIndex((i) => i + 1);
88+
}
89+
90+
setRunning(false);
91+
setStatus("Completed!");
92+
};
93+
94+
const resetGraph = () => {
95+
setNodes([]);
96+
setEdges([]);
97+
setSelected({ from: null, to: null });
98+
setRunning(false);
99+
setStatus("Idle");
100+
setDsuState([]);
101+
setStepIndex(0);
102+
};
103+
104+
// 🧩 Render edges
105+
const renderEdges = () =>
106+
edges.map((edge, i) => {
107+
const fromNode = nodes.find((n) => n.id === edge.from);
108+
const toNode = nodes.find((n) => n.id === edge.to);
109+
if (!fromNode || !toNode) return null;
110+
111+
const { x: x1, y: y1 } = fromNode;
112+
const { x: x2, y: y2 } = toNode;
113+
const cx = (x1 + x2) / 2;
114+
const cy = (y1 + y2) / 2;
115+
116+
return (
117+
<g key={i}>
118+
<line
119+
x1={x1}
120+
y1={y1}
121+
x2={x2}
122+
y2={y2}
123+
strokeWidth={4}
124+
className={`${edge.colorClass} transition-all duration-500`}
125+
strokeLinecap="round"
126+
/>
127+
<text
128+
x={cx}
129+
y={cy - 8}
130+
fontSize={12}
131+
textAnchor="middle"
132+
className="fill-indigo-300"
133+
>
134+
{edge.weight}
135+
</text>
136+
</g>
137+
);
138+
});
139+
140+
// 🧩 Render nodes
141+
const renderNodes = () =>
142+
nodes.map((n) => (
143+
<g
144+
key={n.id}
145+
transform={`translate(${n.x}, ${n.y})`}
146+
onClick={() => handleNodeClick(n.id)}
147+
className="cursor-pointer"
148+
>
149+
<circle
150+
r={18}
151+
className={`stroke-indigo-400 stroke-2 ${
152+
selected.from === n.id ? "fill-indigo-700" : "fill-gray-900"
153+
} transition-all duration-300`}
154+
/>
155+
<text
156+
x={0}
157+
y={5}
158+
textAnchor="middle"
159+
className="fill-indigo-200 font-semibold"
160+
>
161+
{n.label}
162+
</text>
163+
</g>
164+
));
165+
166+
return (
167+
<div className="w-full">
168+
{/* 🧭 Manual / Instructions */}
169+
<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">
170+
<h3 className="text-indigo-400 font-bold mb-2 text-center">
171+
How to Use the Kruskal Visualizer
172+
</h3>
173+
<ul className="list-disc pl-6 space-y-1">
174+
<li> <b>Double-click</b> on the canvas to create a node.</li>
175+
<li> <b>Click one node</b> and then another to create an edge.</li>
176+
<li> You’ll be prompted to enter the <b>edge weight</b>.</li>
177+
<li> Once your graph is ready, click <b>Start Visualization</b>.</li>
178+
<li> The algorithm will highlight edges being considered, added, or skipped.</li>
179+
<li> The <b>DSU panel</b> on the left updates in real time to show connected components.</li>
180+
<li> Click <b>Reset</b> to clear and start again.</li>
181+
</ul>
182+
</div>
183+
{/* Controls */}
184+
<div className="flex gap-4 mb-6 justify-center">
185+
<button
186+
className={`px-6 py-2 rounded-lg font-semibold text-white shadow-md transition-all duration-300 ${
187+
running
188+
? "bg-indigo-800 text-gray-400 cursor-not-allowed"
189+
: "bg-indigo-600 hover:bg-indigo-500"
190+
}`}
191+
onClick={startVisualization}
192+
disabled={running}
193+
>
194+
{running ? "Visualizing..." : "Start Visualization"}
195+
</button>
196+
197+
<button
198+
onClick={resetGraph}
199+
className="bg-gray-700 hover:bg-gray-600 px-6 py-2 rounded-lg text-white font-semibold shadow-md transition-all duration-300"
200+
>
201+
Reset
202+
</button>
203+
</div>
204+
205+
{/* Status + Step */}
206+
<div className="text-sm text-center text-indigo-300 mb-4">
207+
<p>
208+
Status: <span className="font-medium text-indigo-400">{status}</span>
209+
</p>
210+
<p>
211+
Step: <span className="font-medium text-indigo-400">{stepIndex}</span>
212+
</p>
213+
</div>
214+
215+
216+
217+
{/* Layout: DSU Panel + Graph */}
218+
<div className="flex w-full max-w-6xl mx-auto">
219+
{/* Left DSU Panel */}
220+
<div className="w-1/4 bg-gray-900 border border-gray-700 rounded-lg p-3 mr-6 shadow-lg">
221+
<h2 className="text-lg font-bold mb-3 text-center text-indigo-400">
222+
Disjoint Set (DSU)
223+
</h2>
224+
225+
<div className="flex flex-wrap justify-center gap-3">
226+
{nodes.map((n, i) => (
227+
<div key={n.id} className="relative text-center">
228+
<div className="text-sm font-medium text-indigo-300 mb-1">
229+
{n.label}
230+
</div>
231+
<div
232+
className={`w-10 h-10 flex items-center justify-center rounded-full border border-indigo-500 ${
233+
dsuState[i] === i ? "bg-green-700" : "bg-indigo-800"
234+
}`}
235+
>
236+
<span className="text-indigo-200 font-semibold">
237+
N{(dsuState[i] ?? i) + 1}
238+
</span>
239+
</div>
240+
</div>
241+
))}
242+
</div>
243+
</div>
244+
245+
{/* Right Graph */}
246+
<div className="flex-1 border border-gray-700 rounded-lg overflow-hidden shadow-lg">
247+
<svg
248+
ref={svgRef}
249+
onDoubleClick={addNode}
250+
width="100%"
251+
height={500}
252+
viewBox="0 0 800 500"
253+
className="bg-gray-950"
254+
>
255+
{renderEdges()}
256+
{renderNodes()}
257+
</svg>
258+
</div>
259+
</div>
260+
</div>
261+
);
262+
}

src/pages/graph/GraphPage.jsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import React, { useState } from "react";
22
import { Network, Compass, Rocket } from "lucide-react";
33
import BellmanFord from "./BellmanFord";
44
import UnionFindPage from "./UnionFind";
5-
// import Dijkstra from "./Dijkstra";
6-
// import Kruskal from "./Kruskal";
5+
import Kruskal from "./Kruskal";
76

87
export default function GraphPage() {
98
const [selectedAlgo, setSelectedAlgo] = useState("");
@@ -22,10 +21,14 @@ export default function GraphPage() {
2221
<UnionFindPage />
2322
</div>
2423
);
24+
case "kruskal":
25+
return (
26+
<div className="w-full h-full overflow-auto p-">
27+
<Kruskal />
28+
</div>
29+
);
2530
// case "dijkstra":
2631
// return <Dijkstra />;
27-
// case "kruskal":
28-
// return <Kruskal />;
2932
default:
3033
return (
3134
<div className="flex flex-col items-center justify-center text-center p-6">
@@ -69,7 +72,7 @@ export default function GraphPage() {
6972
<option value="">Select Algorithm</option>
7073
<option value="bellman-ford">Bellman–Ford</option>
7174
<option value="union-find">Union Find</option>
72-
{/* <option value="kruskal">Kruskal</option> */}
75+
<option value="kruskal">Kruskal</option> {/* ✅ New dropdown option */}
7376
</select>
7477

7578
<button

src/pages/graph/Kruskal.jsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from "react";
2+
import KruskalVisualizer from "../../components/graph/KruskalVisualizer";
3+
4+
export default function Kruskal() {
5+
return (
6+
<div className="min-h-screen bg-black text-gray-200 flex flex-col items-center p-6">
7+
<h1 className="text-4xl font-extrabold mb-8 text-indigo-400 drop-shadow-lg">
8+
Kruskal’s Minimum Spanning Tree Visualizer
9+
</h1>
10+
<KruskalVisualizer />
11+
</div>
12+
);
13+
}

0 commit comments

Comments
 (0)