Skip to content

Commit fa3b059

Browse files
Merge pull request #49 from SHIVA00202/feat/Add-Linked-List-Visualization
feat: Add-Linked-List-Visualization
2 parents 139fe1c + 431b579 commit fa3b059

File tree

5 files changed

+522
-2
lines changed

5 files changed

+522
-2
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// src/algorithms/dataStructure/linkedlist.js
2+
// Generator that yields step objects for visualization.
3+
// Each yield: { array, highlight: { type, index }, message?, done?:boolean }
4+
5+
export function* linkedListOp(startArray = [], action = {}) {
6+
// Work on a shallow copy
7+
const arr = [...startArray];
8+
const variant = action.variant || "singly"; // 'singly' | 'doubly' | 'circular'
9+
const direction = action.direction || "forward"; // for traverse in doubly
10+
11+
const pushStep = (type, index = null, message = null) => {
12+
return { array: [...arr], highlight: index !== null ? { type, index } : { type }, message };
13+
};
14+
15+
// small helper to clamp
16+
const validIndex = (i) => i >= 0 && i <= arr.length;
17+
18+
switch (action.type) {
19+
case "insertHead": {
20+
arr.unshift(action.value);
21+
yield pushStep("insert", 0, `Inserted ${action.value} at head`);
22+
break;
23+
}
24+
25+
case "insertTail": {
26+
arr.push(action.value);
27+
yield pushStep("insert", arr.length - 1, `Inserted ${action.value} at tail`);
28+
break;
29+
}
30+
31+
case "insertAt": {
32+
const idx = Number.isFinite(action.index) ? action.index : arr.length;
33+
if (!validIndex(idx)) {
34+
yield { array: [...arr], highlight: { type: "error" }, message: "Index out of bounds" };
35+
return;
36+
}
37+
arr.splice(idx, 0, action.value);
38+
yield pushStep("insert", idx, `Inserted ${action.value} at index ${idx}`);
39+
break;
40+
}
41+
42+
case "deleteHead": {
43+
if (arr.length === 0) {
44+
yield { array: [...arr], highlight: { type: "error" }, message: "List is empty" };
45+
return;
46+
}
47+
yield pushStep("delete", 0, `Deleting head (${arr[0]})`);
48+
arr.shift();
49+
yield pushStep("done", null, "Deleted head");
50+
break;
51+
}
52+
53+
case "deleteTail": {
54+
if (arr.length === 0) {
55+
yield { array: [...arr], highlight: { type: "error" }, message: "List is empty" };
56+
return;
57+
}
58+
yield pushStep("delete", arr.length - 1, `Deleting tail (${arr[arr.length - 1]})`);
59+
arr.pop();
60+
yield pushStep("done", null, "Deleted tail");
61+
break;
62+
}
63+
64+
case "deleteValue": {
65+
const idx = arr.indexOf(action.value);
66+
if (idx === -1) {
67+
yield { array: [...arr], highlight: { type: "error" }, message: `Value ${action.value} not found` };
68+
return;
69+
}
70+
yield pushStep("delete", idx, `Deleting value ${action.value} at index ${idx}`);
71+
arr.splice(idx, 1);
72+
yield pushStep("done", null, `Deleted ${action.value}`);
73+
break;
74+
}
75+
76+
case "traverse": {
77+
if (arr.length === 0) {
78+
yield { array: [...arr], highlight: { type: "error" }, message: "Nothing to traverse" };
79+
return;
80+
}
81+
82+
// For circular: traverse exactly arr.length nodes (one loop)
83+
// For doubly: support forward/backward via action.direction
84+
if (variant === "doubly" && direction === "backward") {
85+
// traverse from tail to head
86+
for (let i = arr.length - 1; i >= 0; i--) {
87+
yield pushStep("traverse", i, `Visiting index ${i}: ${arr[i]}`);
88+
}
89+
} else {
90+
// forward
91+
for (let i = 0; i < arr.length; i++) {
92+
yield pushStep("traverse", i, `Visiting index ${i}: ${arr[i]}`);
93+
}
94+
}
95+
96+
// For circular, show extra step showing loop back to head
97+
if (variant === "circular" && arr.length > 0) {
98+
yield { array: [...arr], highlight: { type: "loop" }, message: "Looped back to head (circular)" };
99+
}
100+
101+
yield { array: [...arr], highlight: { type: "done" }, done: true };
102+
break;
103+
}
104+
105+
case "clear": {
106+
yield { array: [...arr], highlight: { type: "clear" }, message: "Clearing list..." };
107+
arr.length = 0;
108+
yield { array: [], highlight: { type: "done" }, done: true };
109+
break;
110+
}
111+
112+
case "loadDemo": {
113+
const demo = action.demo || ["A", "B", "C"];
114+
arr.length = 0;
115+
for (let i = 0; i < demo.length; i++) {
116+
arr.push(demo[i]);
117+
yield pushStep("insert", i, `Loaded ${demo[i]}`);
118+
}
119+
yield { array: [...arr], highlight: { type: "done" }, done: true };
120+
break;
121+
}
122+
123+
default:
124+
yield { array: [...arr], highlight: null, done: true };
125+
break;
126+
}
127+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import React, { useEffect, useRef, useState } from "react";
2+
import { motion, AnimatePresence } from "framer-motion";
3+
4+
export default function LinkedListVisualizer({
5+
array = [],
6+
highlight = null,
7+
variant = "singly",
8+
}) {
9+
const containerRef = useRef(null);
10+
const [positions, setPositions] = useState([]);
11+
const [containerBounds, setContainerBounds] = useState(null);
12+
13+
// Recalculate node positions when array changes
14+
useEffect(() => {
15+
if (!containerRef.current || array.length === 0) {
16+
setPositions([]);
17+
return;
18+
}
19+
20+
// Wait briefly for DOM to update
21+
const timer = setTimeout(() => {
22+
const container = containerRef.current?.getBoundingClientRect();
23+
const nodes = Array.from(
24+
containerRef.current.querySelectorAll(".ll-node")
25+
);
26+
27+
if (nodes.length > 0 && container) {
28+
const rects = nodes.map((n) => n.getBoundingClientRect());
29+
setPositions(rects);
30+
setContainerBounds(container);
31+
}
32+
}, 60);
33+
34+
return () => clearTimeout(timer);
35+
}, [array.length]); // triggers on insert/delete
36+
37+
if (!array || array.length === 0)
38+
return (
39+
<div className="flex items-center justify-center text-slate-400 text-lg h-40">
40+
List is empty
41+
</div>
42+
);
43+
44+
const isCircular = variant === "circular";
45+
const isDoubly = variant === "doubly";
46+
47+
// Color logic for different highlight actions
48+
const getNodeColor = (idx) => {
49+
const isActive = highlight && highlight.index === idx;
50+
const type = highlight?.type;
51+
if (type === "insert" && isActive) return "bg-emerald-700 text-white";
52+
if (type === "delete" && isActive) return "bg-rose-600 text-white";
53+
if (type === "traverse" && isActive) return "bg-yellow-500 text-black";
54+
return "bg-slate-800 text-white";
55+
};
56+
57+
// ✅ Improved circular path calculation (perfectly connects)
58+
const getCircularPath = () => {
59+
if (!isCircular || positions.length < 2 || !containerBounds) return null;
60+
61+
const first = positions[0];
62+
const last = positions[positions.length - 1];
63+
64+
// Calculate path start/end near bottom centers
65+
const startX =
66+
last.right - containerBounds.left - last.width * 0.2; // tail bottom-right inward
67+
const startY = last.bottom - containerBounds.top - 5;
68+
69+
const endX = first.left - containerBounds.left + first.width * 0.2; // head bottom-left inward
70+
const endY = first.bottom - containerBounds.top - 5;
71+
72+
// Control point for smooth curve below nodes
73+
const controlY = Math.max(startY, endY) + 60;
74+
const midX = (startX + endX) / 2;
75+
76+
return {
77+
path: `M ${startX} ${startY} Q ${midX} ${controlY} ${endX} ${endY}`,
78+
startX,
79+
startY,
80+
endX,
81+
endY,
82+
};
83+
};
84+
85+
const circularPath = getCircularPath();
86+
87+
return (
88+
<div
89+
ref={containerRef}
90+
className="relative flex items-center justify-center gap-6 p-6 min-h-[240px]"
91+
>
92+
{/* --- SVG Circular Connection --- */}
93+
{isCircular && circularPath && (
94+
<svg
95+
className="absolute inset-0 w-full h-full pointer-events-none"
96+
xmlns="http://www.w3.org/2000/svg"
97+
>
98+
<defs>
99+
<linearGradient id="circular-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
100+
<stop offset="0%" stopColor="#22c55e" />
101+
<stop offset="100%" stopColor="#0ea5e9" />
102+
</linearGradient>
103+
</defs>
104+
105+
<motion.path
106+
d={circularPath.path}
107+
stroke="url(#circular-gradient)"
108+
strokeWidth="2.5"
109+
fill="none"
110+
strokeDasharray="8 5"
111+
initial={{ pathLength: 0 }}
112+
animate={{ pathLength: 1 }}
113+
transition={{ duration: 1.2, ease: "easeInOut" }}
114+
/>
115+
116+
{/* Label on the path */}
117+
<text
118+
x={(circularPath.startX + circularPath.endX) / 2}
119+
y={Math.max(circularPath.startY, circularPath.endY) + 50}
120+
textAnchor="middle"
121+
className="fill-slate-300 text-xs"
122+
>
123+
Tail → Head
124+
</text>
125+
</svg>
126+
)}
127+
128+
{/* --- Nodes --- */}
129+
<AnimatePresence mode="popLayout">
130+
{array.map((val, idx) => (
131+
<motion.div
132+
key={`${val}-${idx}`}
133+
layout
134+
initial={{ scale: 0.8, opacity: 0 }}
135+
animate={{ scale: 1, opacity: 1 }}
136+
exit={{ scale: 0.6, opacity: 0 }}
137+
transition={{ duration: 0.3 }}
138+
className={`relative ll-node flex flex-col items-center justify-center w-16 h-16 rounded-lg shadow-md ${getNodeColor(
139+
idx
140+
)}`}
141+
>
142+
{/* Index label */}
143+
<div className="absolute -top-5 text-xs text-slate-400">
144+
[{idx}]
145+
</div>
146+
147+
{/* Node value */}
148+
<div className="text-base font-semibold">{val}</div>
149+
150+
{/* HEAD label */}
151+
{idx === 0 && (
152+
<span className="absolute -bottom-6 text-xs text-emerald-400 font-semibold">
153+
HEAD
154+
</span>
155+
)}
156+
157+
{/* TAIL label */}
158+
{idx === array.length - 1 && (
159+
<span
160+
className={`absolute text-xs font-semibold ${
161+
isCircular
162+
? "top-[70px] text-cyan-400"
163+
: "-bottom-6 text-rose-400"
164+
}`}
165+
>
166+
TAIL
167+
</span>
168+
)}
169+
170+
{/* Doubly linked backward pointer */}
171+
{isDoubly && idx > 0 && (
172+
<span className="absolute -left-5 text-lg text-slate-500"></span>
173+
)}
174+
175+
{/* Forward pointer */}
176+
{(idx < array.length - 1 || isCircular) && (
177+
<span className="absolute -right-5 text-lg text-slate-500"></span>
178+
)}
179+
</motion.div>
180+
))}
181+
</AnimatePresence>
182+
183+
{/* --- Info Label --- */}
184+
{isCircular && (
185+
<div className="absolute bottom-2 text-slate-400 text-sm italic">
186+
↻ Circular Linked List — Tail connects back to Head
187+
</div>
188+
)}
189+
</div>
190+
);
191+
}
192+

src/pages/dataStructure/datastructurePage.jsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useState } from "react";
22
import { Network, Compass, Rocket } from "lucide-react";
33
import StackPage from "./stack.jsx";
4+
import LinkedListPage from "./linkedlist.jsx"; // ✅ Linked List page import
45

56
export default function DSPage() {
67
const [selectedDS, setSelectedDS] = useState("");
@@ -14,6 +15,13 @@ export default function DSPage() {
1415
</div>
1516
);
1617

18+
case "linkedlist":
19+
return (
20+
<div className="w-full h-full overflow-auto">
21+
<LinkedListPage />
22+
</div>
23+
);
24+
1725
default:
1826
return (
1927
<div className="flex flex-col items-center justify-center text-center p-6">
@@ -27,7 +35,7 @@ export default function DSPage() {
2735
</h2>
2836
<p className="text-gray-400 mb-6 max-w-sm">
2937
Select a data structure from the sidebar to begin visualization.
30-
Watch how elements, stacks, and queues transform step by step! 🧠✨
38+
Watch how stacks, queues, and linked lists transform step by step! 🧠✨
3139
</p>
3240
<div className="flex items-center justify-center gap-6">
3341
<Compass className="w-8 h-8 text-blue-400 animate-pulse" />
@@ -57,7 +65,7 @@ export default function DSPage() {
5765
<option value="">Select Data Structure</option>
5866
<option value="stack">Stack</option>
5967
<option value="queue">Queue</option>
60-
{/* <option value="linkedlist">Linked List</option> */}
68+
<option value="linkedlist">Linked List</option>
6169
</select>
6270

6371
<button

0 commit comments

Comments
 (0)