Skip to content

Commit 96ca629

Browse files
Merge pull request #51 from Ayushivam22/feat/dfs
feat: added dfs graph traversal
2 parents 16fed6c + 4a4493b commit 96ca629

File tree

3 files changed

+314
-0
lines changed

3 files changed

+314
-0
lines changed
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import React, { useRef, useState } from "react";
2+
3+
// Simple sleep utility consistent with other components
4+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
5+
6+
export default function DFSTraversal() {
7+
const svgRef = useRef(null);
8+
const [nodes, setNodes] = useState([]);
9+
const [edges, setEdges] = useState([]);
10+
const [selected, setSelected] = useState({ from: null, to: null });
11+
const [startNode, setStartNode] = useState(null);
12+
const [running, setRunning] = useState(false);
13+
const [paused, setPaused] = useState(false);
14+
const [status, setStatus] = useState("Idle");
15+
const [intervalMs, setIntervalMs] = useState(700);
16+
const [stackView, setStackView] = useState([]);
17+
18+
// Build adjacency list from current edges (undirected)
19+
const buildAdj = () => {
20+
const adj = new Map();
21+
nodes.forEach((n) => adj.set(n.id, []));
22+
edges.forEach((e) => {
23+
adj.get(e.from)?.push(e.to);
24+
adj.get(e.to)?.push(e.from);
25+
});
26+
// sort neighbors by id for deterministic traversal
27+
for (const [k, arr] of adj.entries()) arr.sort((a, b) => a - b);
28+
return adj;
29+
};
30+
31+
// Add node on double click
32+
const addNode = (e) => {
33+
if (running) return;
34+
const rect = svgRef.current.getBoundingClientRect();
35+
const x = e.clientX - rect.left;
36+
const y = e.clientY - rect.top;
37+
const newNode = {
38+
id: nodes.length + 1,
39+
x,
40+
y,
41+
label: `N${nodes.length + 1}`,
42+
state: "default", // default | current | visited
43+
};
44+
setNodes((prev) => [...prev, newNode]);
45+
};
46+
47+
// Click to create edge or to select start node (if shift key held)
48+
const handleNodeClick = (id, evt) => {
49+
if (running) return;
50+
51+
if (evt && evt.shiftKey) {
52+
setStartNode(id);
53+
setStatus(`Start node set to ${id}`);
54+
return;
55+
}
56+
57+
if (!selected.from) {
58+
setSelected({ from: id, to: null });
59+
} else if (selected.from && !selected.to && selected.from !== id) {
60+
setEdges((prev) => [
61+
...prev,
62+
{ from: selected.from, to: id, color: "stroke-gray-500" },
63+
]);
64+
setSelected({ from: null, to: null });
65+
} else if (selected.from === id) {
66+
setSelected({ from: null, to: null });
67+
}
68+
};
69+
70+
const resetGraph = () => {
71+
setNodes([]);
72+
setEdges([]);
73+
setSelected({ from: null, to: null });
74+
setStartNode(null);
75+
setRunning(false);
76+
setPaused(false);
77+
setStatus("Idle");
78+
setStackView([]);
79+
};
80+
81+
const highlightNode = (id, state) => {
82+
setNodes((prev) => prev.map((n) => (n.id === id ? { ...n, state } : n)));
83+
};
84+
85+
const highlightEdge = (u, v, tempColor) => {
86+
setEdges((prev) =>
87+
prev.map((e) => {
88+
const match =
89+
(e.from === u && e.to === v) || (e.from === v && e.to === u);
90+
if (!match) return e;
91+
return { ...e, color: tempColor };
92+
})
93+
);
94+
};
95+
96+
const waitIfPaused = async () => {
97+
while (paused) {
98+
// poll while paused without blocking UI
99+
// eslint-disable-next-line no-await-in-loop
100+
await sleep(150);
101+
}
102+
};
103+
104+
const runDFS = async () => {
105+
if (running) return;
106+
if (nodes.length === 0) return;
107+
108+
const adj = buildAdj();
109+
let start = startNode ?? nodes[0]?.id;
110+
setRunning(true);
111+
setPaused(false);
112+
setStatus("Running DFS...");
113+
114+
const visited = new Set();
115+
116+
const dfsVisit = async (u) => {
117+
await waitIfPaused();
118+
visited.add(u);
119+
setStackView((s) => [...s, u]);
120+
highlightNode(u, "current");
121+
setStatus(`Visiting ${u}`);
122+
await sleep(intervalMs);
123+
124+
const neighbors = adj.get(u) || [];
125+
for (const v of neighbors) {
126+
await waitIfPaused();
127+
if (!visited.has(v)) {
128+
highlightEdge(u, v, "stroke-orange-400"); // exploring edge
129+
await sleep(intervalMs / 2);
130+
await dfsVisit(v);
131+
highlightEdge(u, v, "stroke-blue-400"); // finalized traversal edge
132+
}
133+
}
134+
135+
// backtrack
136+
highlightNode(u, "visited");
137+
setStackView((s) => s.filter((x) => x !== u));
138+
await sleep(intervalMs / 2);
139+
};
140+
141+
// Ensure all components are covered
142+
const order = [start, ...nodes.map((n) => n.id).filter((id) => id !== start)];
143+
for (const u of order) {
144+
if (!visited.has(u)) {
145+
await dfsVisit(u);
146+
}
147+
}
148+
149+
setRunning(false);
150+
setStatus("Completed");
151+
};
152+
153+
const togglePause = () => {
154+
if (!running) return;
155+
setPaused((p) => !p);
156+
setStatus((s) => (paused ? "Running DFS..." : "Paused"));
157+
};
158+
159+
const renderEdges = () =>
160+
edges.map((e, i) => {
161+
const a = nodes.find((n) => n.id === e.from);
162+
const b = nodes.find((n) => n.id === e.to);
163+
if (!a || !b) return null;
164+
return (
165+
<line
166+
key={i}
167+
x1={a.x}
168+
y1={a.y}
169+
x2={b.x}
170+
y2={b.y}
171+
strokeWidth={4}
172+
className={`${e.color || "stroke-gray-500"} transition-all duration-300`}
173+
strokeLinecap="round"
174+
/>
175+
);
176+
});
177+
178+
const renderNodes = () =>
179+
nodes.map((n) => (
180+
<g
181+
key={n.id}
182+
transform={`translate(${n.x}, ${n.y})`}
183+
onClick={(evt) => handleNodeClick(n.id, evt)}
184+
className="cursor-pointer"
185+
>
186+
<circle
187+
r={18}
188+
className={`stroke-indigo-400 stroke-2 ${
189+
n.state === "current"
190+
? "fill-orange-500"
191+
: n.state === "visited"
192+
? "fill-blue-600"
193+
: selected.from === n.id
194+
? "fill-indigo-700"
195+
: "fill-gray-900"
196+
} transition-all duration-300`}
197+
/>
198+
<text x={0} y={5} textAnchor="middle" className="fill-indigo-200 font-semibold">
199+
{n.label}
200+
</text>
201+
</g>
202+
));
203+
204+
return (
205+
<div className="w-full">
206+
{/* Help */}
207+
<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">
208+
<h3 className="text-indigo-400 font-bold mb-2 text-center">How to Use DFS Traversal</h3>
209+
<ul className="list-disc pl-6 space-y-1">
210+
<li>Double-click on the canvas to create nodes.</li>
211+
<li>Click one node and then another to create an undirected edge.</li>
212+
<li>Shift+Click a node to set it as the start node.</li>
213+
<li>Click Run DFS to start. Pause/Resume with Pause.</li>
214+
<li>DFS will continue to other components after finishing the start component.</li>
215+
</ul>
216+
</div>
217+
218+
{/* Controls */}
219+
<div className="flex flex-wrap gap-3 mb-4 justify-center">
220+
<button
221+
onClick={runDFS}
222+
disabled={running && !paused}
223+
className={`px-5 py-2 rounded-lg font-semibold text-white shadow-md transition-all duration-300 ${
224+
running && !paused ? "bg-indigo-800 cursor-not-allowed" : "bg-indigo-600 hover:bg-indigo-500"
225+
}`}
226+
>
227+
{running ? (paused ? "Resume DFS" : "Running...") : "Run DFS"}
228+
</button>
229+
<button
230+
onClick={togglePause}
231+
disabled={!running}
232+
className={`px-5 py-2 rounded-lg font-semibold text-white shadow-md transition-all duration-300 ${
233+
running ? "bg-yellow-600 hover:bg-yellow-500" : "bg-gray-700 cursor-not-allowed"
234+
}`}
235+
>
236+
{paused ? "Resume" : "Pause"}
237+
</button>
238+
<button
239+
onClick={resetGraph}
240+
className="px-5 py-2 rounded-lg font-semibold text-white shadow-md transition-all duration-300 bg-gray-700 hover:bg-gray-600"
241+
>
242+
Reset
243+
</button>
244+
<div className="flex items-center gap-2 text-sm text-gray-300">
245+
<label>Speed:</label>
246+
<input
247+
type="range"
248+
min={200}
249+
max={1500}
250+
step={50}
251+
value={intervalMs}
252+
onChange={(e) => setIntervalMs(parseInt(e.target.value, 10))}
253+
/>
254+
</div>
255+
<div className="text-sm text-indigo-300 self-center">Status: <span className="text-indigo-400 font-medium">{status}</span></div>
256+
</div>
257+
258+
{/* Layout: Stack + Graph */}
259+
<div className="flex w-full max-w-6xl mx-auto">
260+
{/* Recursion Stack */}
261+
<div className="w-1/4 bg-gray-900 border border-gray-700 rounded-lg p-3 mr-6 shadow-lg">
262+
<h2 className="text-lg font-bold mb-3 text-center text-indigo-400">Recursion Stack</h2>
263+
<div className="space-y-2">
264+
{stackView.map((id, idx) => (
265+
<div key={`${id}-${idx}`} className="px-3 py-2 rounded bg-indigo-900/40 border border-indigo-700 text-indigo-200">
266+
Node {id}
267+
</div>
268+
))}
269+
{stackView.length === 0 && <div className="text-sm text-gray-400 text-center">Empty</div>}
270+
</div>
271+
<div className="mt-4 text-xs text-gray-400">
272+
Tip: Shift+Click to set start node. Current node is orange. Visited nodes are blue.
273+
</div>
274+
</div>
275+
276+
{/* Graph Canvas */}
277+
<div className="flex-1 border border-gray-700 rounded-lg overflow-hidden shadow-lg">
278+
<svg
279+
ref={svgRef}
280+
onDoubleClick={addNode}
281+
width="100%"
282+
height={500}
283+
viewBox="0 0 800 500"
284+
className="bg-gray-950"
285+
>
286+
{renderEdges()}
287+
{renderNodes()}
288+
</svg>
289+
</div>
290+
</div>
291+
</div>
292+
);
293+
}

src/pages/graph/DFSTraversal.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 DFSTraversal from "../../components/graph/DFSTraversal";
3+
4+
export default function DFSTraversalPage() {
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+
Depth-First Search (DFS) Traversal
9+
</h1>
10+
<DFSTraversal />
11+
</div>
12+
);
13+
}

src/pages/graph/GraphPage.jsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import UnionFindPage from "./UnionFind";
55
import Kruskal from "./Kruskal";
66
import FloydWarshall from "./FloydWarshall";
77
import CycleDetection from "./CycleDetection";
8+
import DFSTraversal from "./DFSTraversal";
89

910
export default function GraphPage() {
1011
const [selectedAlgo, setSelectedAlgo] = useState("");
@@ -41,6 +42,12 @@ export default function GraphPage() {
4142
<CycleDetection />
4243
</div>
4344
);
45+
case "dfs-traversal":
46+
return (
47+
<div className="w-full h-full overflow-auto p-">
48+
<DFSTraversal />
49+
</div>
50+
);
4451
default:
4552
return (
4653
<div className="flex flex-col items-center justify-center text-center p-6">
@@ -88,6 +95,7 @@ export default function GraphPage() {
8895
<option value="kruskal">Kruskal</option>
8996
<option value="floyd-warshall">Floyd–Warshall</option>
9097
<option value="cycle-detection">Cycle Detection</option> {/* ✅ Added */}
98+
<option value="dfs-traversal">DFS Traversal</option>
9199
</select>
92100

93101
<button

0 commit comments

Comments
 (0)