Skip to content

Commit e696d27

Browse files
Merge pull request #21 from akash205sharma/feat/bellman
[Feature]: Bellman–Ford Algorithm Visualizer
2 parents a182826 + 00bdef8 commit e696d27

File tree

7 files changed

+708
-6
lines changed

7 files changed

+708
-6
lines changed

public/graph.png

4.99 KB
Loading

src/App.jsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import React from "react";
22
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
3-
import UnionFindPage from "../src/pages/graph/UnionFind.jsx"; // ✅ Import Union-Find Page
3+
// import UnionFindPage from "../src/pages/graph/UnionFind.jsx"; // ✅ Import Union-Find Page
44
import SortingPage from "./pages/sorting/SortingPage";
5+
import GraphPage from "./pages/graph/GraphPage";
56
import Homepage from "./pages/Homepage.jsx";
7+
68
function App() {
79
return (
810
<Router>
911
<Routes>
1012
<Route path="/" element={<Homepage />} />
11-
<Route path="/graph/union-find" element={<UnionFindPage />} /> {/* ✅ Added route */}
13+
{/* <Route path="/graph/union-find" element={<UnionFindPage />} /> */}
1214
<Route path="/sorting" element={<SortingPage />} />
15+
<Route path="/graph" element={<GraphPage />} />
1316
</Routes>
1417
</Router>
1518
);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export default function runBellmanFord(nodes, edges, source) {
2+
const dist = {};
3+
nodes.forEach(n => dist[n] = Infinity);
4+
dist[source] = 0;
5+
6+
const steps = [];
7+
8+
// Relax edges |V|-1 times
9+
for (let i = 0; i < nodes.length - 1; i++) {
10+
for (const edge of edges) {
11+
const { from, to, weight } = edge;
12+
const step = {
13+
type: "relax",
14+
iteration: i + 1,
15+
edge,
16+
prevDistance: dist[to]
17+
};
18+
19+
if (dist[from] + weight < dist[to]) {
20+
dist[to] = dist[from] + weight;
21+
step.updatedDistance = dist[to];
22+
}
23+
24+
steps.push(step);
25+
}
26+
}
27+
28+
// Check for negative weight cycles
29+
for (const edge of edges) {
30+
const { from, to, weight } = edge;
31+
if (dist[from] + weight < dist[to]) {
32+
steps.push({ type: "negativeCycle", edge });
33+
}
34+
}
35+
36+
return steps;
37+
}
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
import React, { useState, useEffect, useMemo } from "react";
2+
3+
export default function BellmanFordGraph({ nodes = [], edges = [], distances = {}, highlight = {} }) {
4+
const [positions, setPositions] = useState({});
5+
6+
// Arrange nodes in a circle when nodes change
7+
useEffect(() => {
8+
if (nodes.length === 0) return;
9+
10+
const radius = 180;
11+
const centerX = 300;
12+
const centerY = 250;
13+
const newPositions = {};
14+
nodes.forEach((node, index) => {
15+
const angle = (2 * Math.PI * index) / nodes.length;
16+
newPositions[node] = {
17+
x: centerX + radius * Math.cos(angle),
18+
y: centerY + radius * Math.sin(angle),
19+
};
20+
});
21+
setPositions(newPositions);
22+
}, [nodes]);
23+
24+
// ✅ Memoize dummy nodes and edges at top level
25+
const dummyGraph = useMemo(() => {
26+
const dummyNodes = Array.from({ length: 6 }).map((_, i) => {
27+
const radius = 180;
28+
const centerX = 300;
29+
const centerY = 250;
30+
const angle = (2 * Math.PI * i) / 6;
31+
return {
32+
id: i,
33+
x: centerX + radius * Math.cos(angle),
34+
y: centerY + radius * Math.sin(angle),
35+
};
36+
});
37+
38+
const dummyEdges = Array.from({ length: 8 }).map(() => {
39+
const u = Math.floor(Math.random() * 6);
40+
const v = (u + 1 + Math.floor(Math.random() * 5)) % 6;
41+
return { u, v };
42+
});
43+
44+
return { dummyNodes, dummyEdges };
45+
}, []); // runs only once
46+
47+
return (
48+
<div className="w-full flex flex-col lg:flex-row items-start gap-6 mt-10">
49+
<div className="flex justify-center flex-1 relative">
50+
<svg width="600" height="500" className="border border-gray-700 bg-gray-900 rounded-lg shadow-lg">
51+
<defs>
52+
<marker
53+
id="arrowhead-dummy"
54+
markerWidth="10"
55+
markerHeight="7"
56+
refX="8"
57+
refY="3.5"
58+
orient="auto"
59+
fill="#9ca3af"
60+
>
61+
<polygon points="0 0, 10 3.5, 0 7" />
62+
</marker>
63+
<filter id="glow-dummy">
64+
<feGaussianBlur stdDeviation="2.5" result="coloredBlur" />
65+
<feMerge>
66+
<feMergeNode in="coloredBlur" />
67+
<feMergeNode in="SourceGraphic" />
68+
</feMerge>
69+
</filter>
70+
</defs>
71+
<defs>
72+
<marker
73+
id="arrowhead"
74+
markerWidth="10"
75+
markerHeight="7"
76+
refX="8"
77+
refY="3.5"
78+
orient="auto"
79+
fill="currentColor"
80+
>
81+
<polygon points="0 0, 10 3.5, 0 7" />
82+
</marker>
83+
<filter id="glow">
84+
<feGaussianBlur stdDeviation="3.5" result="coloredBlur" />
85+
<feMerge>
86+
<feMergeNode in="coloredBlur" />
87+
<feMergeNode in="SourceGraphic" />
88+
</feMerge>
89+
</filter>
90+
</defs>
91+
92+
{/* Dummy graph if no nodes yet */}
93+
{nodes.length === 0 && (
94+
<>
95+
{dummyGraph.dummyNodes.map((n) => (
96+
<g key={n.id}>
97+
<circle
98+
cx={n.x}
99+
cy={n.y}
100+
r="22"
101+
fill="#374151"
102+
stroke="#6b7280"
103+
strokeWidth="2"
104+
style={{
105+
opacity: 0.7,
106+
animation: `pulseDummy ${1.5 + Math.random()}s infinite alternate`,
107+
}}
108+
/>
109+
<text
110+
x={n.x}
111+
y={n.y + 5}
112+
textAnchor="middle"
113+
fill="#9ca3af"
114+
fontSize="14"
115+
fontWeight="bold"
116+
>
117+
?
118+
</text>
119+
</g>
120+
))}
121+
122+
{dummyGraph.dummyEdges.map((e, i) => {
123+
const x1 = dummyGraph.dummyNodes[e.u].x;
124+
const y1 = dummyGraph.dummyNodes[e.u].y;
125+
const x2 = dummyGraph.dummyNodes[e.v].x;
126+
const y2 = dummyGraph.dummyNodes[e.v].y;
127+
return (
128+
<line
129+
key={i}
130+
x1={x1}
131+
y1={y1}
132+
x2={x2}
133+
y2={y2}
134+
stroke="#6b7280"
135+
strokeWidth="3"
136+
strokeDasharray="12 8"
137+
strokeDashoffset={Math.random() * 20}
138+
markerEnd="url(#arrowhead-dummy)"
139+
style={{
140+
filter: "url(#glow-dummy)",
141+
animation: `dashMoveDummy ${2 + Math.random()}s linear infinite`,
142+
}}
143+
/>
144+
);
145+
})}
146+
147+
<text
148+
x="300"
149+
y="480"
150+
textAnchor="middle"
151+
fill="#9ca3af"
152+
fontSize="14"
153+
fontWeight="bold"
154+
>
155+
🎯 Demo graph: edges light up like traversal!
156+
</text>
157+
158+
<style>{`
159+
@keyframes pulseDummy { from { opacity: 0.5; } to { opacity: 0.9; } }
160+
@keyframes dashMoveDummy { to { stroke-dashoffset: -20; } }
161+
`}</style>
162+
</>
163+
)}
164+
165+
{/* Real edges and nodes rendering (same as before) */}
166+
167+
168+
{/* Edges */}
169+
{edges.map((e, idx) => {
170+
const from = positions[e.u];
171+
const to = positions[e.v];
172+
if (!from || !to) return null;
173+
174+
const isHighlighted =
175+
highlight?.edge && highlight.edge.u === e.u && highlight.edge.v === e.v;
176+
177+
return (
178+
<g key={idx}>
179+
<line
180+
x1={from.x}
181+
y1={from.y}
182+
x2={to.x}
183+
y2={to.y}
184+
stroke={isHighlighted ? "#34d399" : "#888"}
185+
strokeWidth={isHighlighted ? 5 : 2}
186+
markerEnd="url(#arrowhead)"
187+
style={{
188+
color: isHighlighted ? "#34d399" : "#fff",
189+
filter: isHighlighted ? "url(#glow)" : "none",
190+
transition: "stroke 0.3s ease, stroke-width 0.3s ease",
191+
strokeDasharray: isHighlighted ? "12 6" : "0",
192+
animation: isHighlighted ? "dashMove 1s linear infinite" : "none",
193+
}}
194+
/>
195+
<text
196+
x={(from.x + to.x) / 2}
197+
y={(from.y + to.y) / 2 - 8}
198+
fill="white"
199+
fontSize="12"
200+
textAnchor="middle"
201+
>
202+
{e.w}
203+
</text>
204+
</g>
205+
);
206+
})}
207+
208+
{/* Nodes */}
209+
{nodes.map((n) => {
210+
const pos = positions[n];
211+
if (!pos) return null;
212+
213+
const isHighlighted = highlight?.dist && highlight.dist[n] !== undefined;
214+
215+
return (
216+
<g key={n} style={{ transition: "all 0.3s ease" }}>
217+
<circle
218+
cx={pos.x}
219+
cy={pos.y}
220+
r="22"
221+
fill={isHighlighted ? "#065f46" : "#1f2937"}
222+
stroke={isHighlighted ? "#34d399" : "#9ca3af"}
223+
strokeWidth={isHighlighted ? 3 : 2}
224+
style={{ transition: "all 0.3s ease" }}
225+
className="cursor-pointer hover:stroke-emerald-400"
226+
/>
227+
<text
228+
x={pos.x}
229+
y={pos.y - 5}
230+
textAnchor="middle"
231+
fill="white"
232+
fontSize="14"
233+
fontWeight="bold"
234+
>
235+
{n}
236+
</text>
237+
<text
238+
x={pos.x}
239+
y={pos.y + 15}
240+
textAnchor="middle"
241+
fill="#d1d5db"
242+
fontSize="12"
243+
>
244+
{distances[n] === undefined || distances[n] === Infinity ? "∞" : distances[n]}
245+
</text>
246+
</g>
247+
);
248+
})}
249+
</svg>
250+
</div>
251+
252+
{/* Result / Details Panel */}
253+
<div className="flex-1 max-w-md bg-gray-800 border border-gray-700 rounded-lg shadow-md p-4">
254+
<h2 className="text-xl font-semibold text-white mb-4">📊 Algorithm Details</h2>
255+
256+
{Object.keys(distances).length === 0 ? (
257+
<p className="text-gray-400 text-sm">
258+
Run the algorithm to see node distances and steps here.
259+
</p>
260+
) : (
261+
<>
262+
{highlight && highlight.type && (
263+
<div className="mb-4 p-2 bg-gray-700 rounded">
264+
{highlight.type === "relax" && (
265+
<p className="text-emerald-300">
266+
Iteration {highlight.iteration}: Relaxing edge{" "}
267+
<strong>{highlight.edge.u}{highlight.edge.v}</strong>
268+
</p>
269+
)}
270+
{highlight.type === "skip" && (
271+
<p className="text-gray-400">
272+
Iteration {highlight.iteration}: Skipped edge{" "}
273+
<strong>{highlight.edge.u}{highlight.edge.v}</strong>
274+
</p>
275+
)}
276+
{highlight.type === "negative-cycle" && (
277+
<p className="text-red-400 font-semibold">
278+
❌ Negative weight cycle detected on edge{" "}
279+
<strong>{highlight.edge.u}{highlight.edge.v}</strong>
280+
</p>
281+
)}
282+
{highlight.type === "done" && (
283+
<p className="text-emerald-400 font-semibold">
284+
✅ Algorithm finished. Final distances updated!
285+
</p>
286+
)}
287+
</div>
288+
)}
289+
290+
<div className="grid grid-cols-2 gap-2">
291+
{nodes.map((n) => (
292+
<div
293+
key={n}
294+
className="flex justify-between bg-gray-700 rounded px-3 py-1 text-sm text-gray-200 hover:bg-gray-600 transition"
295+
>
296+
<span className="font-semibold">Node {n}</span>
297+
<span>
298+
{distances[n] === undefined || distances[n] === Infinity ? "∞" : distances[n]}
299+
</span>
300+
</div>
301+
))}
302+
</div>
303+
</>
304+
)}
305+
</div>
306+
307+
<style>{`
308+
@keyframes dashMove {
309+
to {
310+
stroke-dashoffset: -18;
311+
}
312+
}
313+
`}
314+
</style>
315+
</div>
316+
);
317+
}

src/pages/Homepage.jsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,9 @@ const sections = [
3636
description:
3737
"Explore BFS, DFS, Kruskal’s, Prim’s, and now Union-Find — all brought to life interactively.",
3838
phase: "Phase 2",
39-
img: "",
40-
route: "/graph/union-find", // ✅ Route to Union-Find page
41-
link: "/graphs",
42-
flag: true
39+
img: "/graph.png",
40+
link: "/graph",
41+
flag: false
4342
},
4443
{
4544
title: "Recursion & Backtracking",

0 commit comments

Comments
 (0)