From 7cf219149a82bd481b78e5accda66204e4052581 Mon Sep 17 00:00:00 2001 From: jeevanvishnu Date: Thu, 1 Jan 2026 11:39:05 +0530 Subject: [PATCH 1/2] feat: Add Sorting, Stack, and Graph visualizer components and integrate them into the DSA visualization page. --- src/components/dsa/GraphVisualizer.jsx | 254 +++++++++++++ src/components/dsa/SortingVisualizer.jsx | 120 ++++++ src/components/dsa/StackVisualizer.jsx | 118 ++++++ src/pages/DsaVisualization.jsx | 462 +++-------------------- 4 files changed, 549 insertions(+), 405 deletions(-) create mode 100644 src/components/dsa/GraphVisualizer.jsx create mode 100644 src/components/dsa/SortingVisualizer.jsx create mode 100644 src/components/dsa/StackVisualizer.jsx diff --git a/src/components/dsa/GraphVisualizer.jsx b/src/components/dsa/GraphVisualizer.jsx new file mode 100644 index 0000000..2dada1e --- /dev/null +++ b/src/components/dsa/GraphVisualizer.jsx @@ -0,0 +1,254 @@ + +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import { Button } from '@/components/ui/button'; +import { Switch } from '@/components/ui/switch'; +import { Move, PlusCircle, Network, Trash2, StepBack, StepForward, Play, Pause } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import * as algorithms from '@/lib/dsaAlgorithms'; + +const GraphVisualizer = ({ algorithmName, isPlaying, setIsPlaying, speed, onFinished, darkMode }) => { + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + const [isDirected, setIsDirected] = useState(false); + const [isWeighted, setIsWeighted] = useState(false); + const [editMode, setEditMode] = useState('move'); + const [history, setHistory] = useState([]); + const [currentStep, setCurrentStep] = useState(-1); + const [startNode, setStartNode] = useState(0); + const [endNode, setEndNode] = useState(null); + const [draggingNode, setDraggingNode] = useState(null); + const [edgeStartNode, setEdgeStartNode] = useState(null); + const containerRef = useRef(null); + const animationRef = useRef(null); + + const visualState = useMemo(() => { + const visited = new Set(); + const path = new Set(); + const dists = {}; + let activeLink = null; + + if (currentStep >= 0 && currentStep < history.length) { + for (let i = 0; i <= currentStep; i++) { + const step = history[i]; + if (step.type === 'visit' || step.type === 'visit-node') { + visited.add(step.node); + } else if (step.type === 'path') { + step.path.forEach(n => path.add(n)); + } else if (step.type === 'update-dist') { + dists[step.node] = step.dist; + } + if (i === currentStep) { + if (step.type === 'traverse' || step.type === 'check-edge') { + activeLink = { from: step.from, to: step.to }; + } + } + } + } + return { visited, path, dists, activeLink }; + }, [currentStep, history]); + + useEffect(() => { resetGraph(); }, []); + + useEffect(() => { + if (!isPlaying && nodes.length > 0) { + calculateAlgorithmSteps(); + } + }, [nodes, edges, startNode, endNode, algorithmName, isDirected]); + + const calculateAlgorithmSteps = () => { + const numNodes = Math.max(...nodes.map(n => n.id), -1) + 1; + const adj = Array.from({ length: numNodes }, () => []); + edges.forEach(edge => { + if (nodes.find(n => n.id === edge.from) && nodes.find(n => n.id === edge.to)) { + adj[edge.from].push({ to: edge.to, weight: edge.weight }); + if (!isDirected) adj[edge.to].push({ to: edge.from, weight: edge.weight }); + } + }); + if (!nodes.find(n => n.id === startNode)) return; + let actualEnd = endNode; + if (endNode === null && nodes.length > 0) actualEnd = nodes[nodes.length - 1].id; + + const algoFunc = algorithms[`generate${algorithmName}Steps`]; + if (algoFunc) { + let steps = []; + if (algorithmName === 'AStar') { + const nodeObjMap = nodes.reduce((acc, n) => ({ ...acc, [n.id]: n }), {}); + steps = algoFunc(numNodes, adj, startNode, actualEnd, nodeObjMap); + } else { + steps = algoFunc(numNodes, adj, startNode, actualEnd); + } + setHistory(steps); + } + }; + + useEffect(() => { + if (isPlaying) { + const delay = Math.max(50, 1000 - (speed * 9)); + animationRef.current = setInterval(() => { + setCurrentStep(prev => { + if (prev < history.length - 1) return prev + 1; + setIsPlaying(false); + if (onFinished) onFinished(); + return prev; + }); + }, delay); + } else { + clearInterval(animationRef.current); + } + return () => clearInterval(animationRef.current); + }, [isPlaying, history.length, speed]); + + const resetGraph = () => { + setIsPlaying(false); + setCurrentStep(-1); + const width = containerRef.current ? containerRef.current.clientWidth : 800; + const height = 400; + const numNodes = 10; + const newNodes = []; + for (let i = 0; i < numNodes; i++) newNodes.push({ id: i, x: Math.random() * (width - 100) + 50, y: Math.random() * (height - 100) + 50 }); + const newEdges = []; + for (let i = 1; i < numNodes; i++) { + const target = Math.floor(Math.random() * i); + const weight = Math.floor(Math.random() * 20) + 1; + newEdges.push({ from: i, to: target, weight }); + } + for (let i = 0; i < numNodes / 2; i++) { + const u = Math.floor(Math.random() * numNodes); + const v = Math.floor(Math.random() * numNodes); + if (u !== v && !newEdges.find(e => (e.from === u && e.to === v) || (e.from === v && e.to === u))) { + newEdges.push({ from: u, to: v, weight: Math.floor(Math.random() * 20) + 1 }); + } + } + setNodes(newNodes); + setEdges(newEdges); + setStartNode(0); + setEndNode(numNodes - 1); + }; + + const handleMouseDown = (e, nodeId) => { + if (isPlaying) return; + e.stopPropagation(); + if (editMode === 'move' && nodeId !== undefined) setDraggingNode(nodeId); + else if (editMode === 'add-edge' && nodeId !== undefined) { + if (edgeStartNode === null) setEdgeStartNode(nodeId); + else { + if (edgeStartNode !== nodeId) { + const weight = isWeighted ? Math.floor(Math.random() * 20) + 1 : 1; + const exists = edges.some(edge => (edge.from === edgeStartNode && edge.to === nodeId) || (!isDirected && edge.from === nodeId && edge.to === edgeStartNode)); + if (!exists) setEdges(prev => [...prev, { from: edgeStartNode, to: nodeId, weight }]); + } + setEdgeStartNode(null); + } + } else if (editMode === 'delete' && nodeId !== undefined) { + setNodes(prev => prev.filter(n => n.id !== nodeId)); + setEdges(prev => prev.filter(e => e.from !== nodeId && e.to !== nodeId)); + if (startNode === nodeId) setStartNode(nodes.length > 0 ? nodes[0].id : null); + } + }; + + const handleCanvasClick = (e) => { + if (isPlaying) return; + if (editMode === 'add-node') { + const rect = containerRef.current.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const newId = nodes.length > 0 ? Math.max(...nodes.map(n => n.id)) + 1 : 0; + setNodes(prev => [...prev, { id: newId, x, y }]); + } + }; + + const handleMouseMove = (e) => { + if (draggingNode === null || !containerRef.current) return; + const rect = containerRef.current.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + setNodes(prev => prev.map(n => n.id === draggingNode ? { ...n, x, y } : n)); + }; + + const handleMouseUp = () => setDraggingNode(null); + + const handleNodeContext = (e, nodeId) => { + e.preventDefault(); + if (editMode === 'move') { setStartNode(nodeId); setCurrentStep(-1); } + }; + + return ( +
+
+ {/* Graph Toolbar Buttons: Added explicit colors for visibility */} +
+ + + + +
+
+
+
+
+
+
+
+ + + +
+
+ +
+
+
+

Start: {startNode}

+

End: {endNode ?? 'Last'}

+

{editMode === 'move' ? "Drag nodes. Right-click to set Start." : "Click to edit."}

+
+
+ + + {edges.map((edge, idx) => { + const start = nodes.find(n => n.id === edge.from); + const end = nodes.find(n => n.id === edge.to); + if (!start || !end) return null; + const isActive = visualState.activeLink && ((visualState.activeLink.from === edge.from && visualState.activeLink.to === edge.to) || (!isDirected && visualState.activeLink.from === edge.to && visualState.activeLink.to === edge.from)); + return ({isWeighted && ({edge.weight})}); + })} + {editMode === 'add-edge' && edgeStartNode !== null && (() => { const start = nodes.find(n => n.id === edgeStartNode); if (start) return ; return null; })()} + {nodes.map((node) => { + const isStart = node.id === startNode; + const isEnd = node.id === endNode; + const isVisited = visualState.visited.has(node.id); + const isPath = visualState.path.has(node.id); + const dist = visualState.dists[node.id]; + return ( handleMouseDown(e, node.id)} onClick={() => editMode === 'move' && setEndNode(node.id)} onContextMenu={(e) => handleNodeContext(e, node.id)}>{node.id}{(algorithmName === 'Dijkstra' || algorithmName === 'AStar') && dist !== undefined && ({dist === Infinity ? '∞' : dist})}); + })} + +
+
+ ); +}; + +export default GraphVisualizer; diff --git a/src/components/dsa/SortingVisualizer.jsx b/src/components/dsa/SortingVisualizer.jsx new file mode 100644 index 0000000..e5a7764 --- /dev/null +++ b/src/components/dsa/SortingVisualizer.jsx @@ -0,0 +1,120 @@ + +import React, { useState, useEffect, useRef } from 'react'; +import * as algorithms from '@/lib/dsaAlgorithms'; +import { cn } from '@/lib/utils'; + +const SortingVisualizer = ({ array, algorithmName, isPlaying, speed, onFinished, className, searchTarget }) => { + const [displayArray, setDisplayArray] = useState([...array]); + const timeoutsRef = useRef([]); + + const showLabels = array.length <= 35; + + useEffect(() => { + setDisplayArray([...array]); + clearAllTimeouts(); + }, [array]); + + useEffect(() => { + if (isPlaying) { + runAlgorithm(); + } else { + clearAllTimeouts(); + } + return () => clearAllTimeouts(); + }, [isPlaying, algorithmName]); + + const clearAllTimeouts = () => { + timeoutsRef.current.forEach(clearTimeout); + timeoutsRef.current = []; + }; + + const runAlgorithm = () => { + clearAllTimeouts(); + let steps = []; + const algoFunc = algorithms[`generate${algorithmName}Steps`]; + if (!algoFunc) return; + + // Pass searchTarget if it's a search algorithm + if (algorithmName.includes('Search')) { + steps = algoFunc(array, searchTarget); + } else { + steps = algoFunc(array); + } + + animate(steps); + }; + + const animate = (steps) => { + const delay = Math.max(1, 100 - speed); + steps.forEach((step, index) => { + const timeoutId = setTimeout(() => { + setDisplayArray(prev => { + // Use a new array reference to trigger re-renders + const newArr = [...prev]; + if (step.type === 'swap') { + newArr[step.indices[0]] = step.values[0]; + newArr[step.indices[1]] = step.values[1]; + } else if (step.type === 'overwrite') { + newArr[step.indices[0]] = step.value; + } + return newArr; + }); + + const bars = document.getElementsByClassName(`bar-${className}`); + if(bars.length > 0) { + if (step.type === 'compare') { + step.indices.forEach(idx => { if(bars[idx]) bars[idx].style.backgroundColor = '#ef4444' }); + } else if (step.type === 'swap' || step.type === 'overwrite') { + step.indices.forEach(idx => { if(bars[idx]) bars[idx].style.backgroundColor = '#eab308' }); + } else if (step.type === 'sorted' || step.type === 'found') { + step.indices.forEach(idx => { if(bars[idx]) bars[idx].style.backgroundColor = '#10b981' }); + } else if (step.type === 'revert') { + step.indices.forEach(idx => { if(bars[idx]) bars[idx].style.backgroundColor = '' }); + } else if (step.type === 'discard') { + // Gray out discarded parts of the array for Binary Search + if (step.range) { + for(let i=step.range[0]; i<=step.range[1]; i++) { + if(bars[i]) bars[i].style.opacity = '0.2'; + } + } + } + } + + if (index === steps.length - 1) { + if (onFinished) onFinished(); + } + }, index * delay); + timeoutsRef.current.push(timeoutId); + }); + }; + + return ( +
+ {displayArray.map((val, idx) => ( +
+ {showLabels && ( + + {val} + + )} +
+
+ ))} +
+ ); +}; + +export default SortingVisualizer; diff --git a/src/components/dsa/StackVisualizer.jsx b/src/components/dsa/StackVisualizer.jsx new file mode 100644 index 0000000..60ab920 --- /dev/null +++ b/src/components/dsa/StackVisualizer.jsx @@ -0,0 +1,118 @@ + +import React, { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { ArrowDown, ArrowUp, Trash2, Layers } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { motion, AnimatePresence } from 'framer-motion'; + +const StackVisualizer = ({ darkMode }) => { + const [stack, setStack] = useState([]); + const [inputValue, setInputValue] = useState(''); + const [message, setMessage] = useState('Stack is empty'); + + const handlePush = () => { + if (!inputValue) return; + if (stack.length >= 10) { + setMessage('Stack Overflow! Max size reached.'); + return; + } + const newVal = inputValue; + setStack(prev => [...prev, newVal]); + setInputValue(''); + setMessage(`Pushed ${newVal} onto the stack`); + }; + + const handlePop = () => { + if (stack.length === 0) { + setMessage('Stack Underflow! Stack is empty.'); + return; + } + const popped = stack[stack.length - 1]; + setStack(prev => prev.slice(0, -1)); + setMessage(`Popped ${popped} from the stack`); + }; + + const handlePeek = () => { + if (stack.length === 0) { + setMessage('Stack is empty'); + } else { + setMessage(`Top element is ${stack[stack.length - 1]}`); + } + }; + + const clearStack = () => { + setStack([]); + setMessage('Stack cleared'); + }; + + return ( +
+
+
+ setInputValue(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handlePush()} + /> + + + + +
+
+ {message} +
+
+ +
+ {/* Stack Container */} +
+ + {stack.map((item, index) => ( + + {item} + {index === stack.length - 1 && ( + + ← Top + + )} + + ))} + + {stack.length === 0 && ( +
+ Empty +
+ )} +
+
+ +
+ LIFO (Last-In, First-Out) Principle +
+
+ ); +}; + +export default StackVisualizer; diff --git a/src/pages/DsaVisualization.jsx b/src/pages/DsaVisualization.jsx index 5caefa3..45ffcc4 100644 --- a/src/pages/DsaVisualization.jsx +++ b/src/pages/DsaVisualization.jsx @@ -2,23 +2,23 @@ import React, { useState, useEffect, useRef, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Helmet } from 'react-helmet'; -import { - Play, Pause, RotateCcw, Settings, ChevronRight, ChevronLeft, - LayoutGrid, List, BarChart2, Activity, Moon, Sun, - GitCompare, SplitSquareHorizontal, ArrowRight, Network, - PlusCircle, Move, MousePointer2, Trash2, StepForward, - StepBack, Binary, BoxSelect, Layers, Search, Github +import { + Play, Pause, RotateCcw, Settings, ChevronRight, ChevronLeft, + LayoutGrid, List, BarChart2, Activity, Moon, Sun, + GitCompare, SplitSquareHorizontal, ArrowRight, Network, + PlusCircle, Move, MousePointer2, Trash2, StepForward, + StepBack, Binary, BoxSelect, Layers, Search, Github } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Slider } from '@/components/ui/slider'; import { Switch } from '@/components/ui/switch'; import { Input } from '@/components/ui/input'; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { Card, CardContent } from '@/components/ui/card'; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; @@ -31,369 +31,11 @@ import LinkedListVisualizer from '@/components/dsa/LinkedListVisualizer'; import TreeVisualizer from '@/components/dsa/TreeVisualizer'; import HeapVisualizer from '@/components/dsa/HeapVisualizer'; import DPVisualizer from '@/components/dsa/DPVisualizer'; +import SortingVisualizer from '@/components/dsa/SortingVisualizer'; +import GraphVisualizer from '@/components/dsa/GraphVisualizer'; +import StackVisualizer from '@/components/dsa/StackVisualizer'; import ContributorsSection from '@/components/dsa/ContributorsSection'; -// --- Existing Sorting/Graph Components --- - -const SortingVisualizer = ({ array, algorithmName, isPlaying, speed, onFinished, className, searchTarget }) => { - const [displayArray, setDisplayArray] = useState([...array]); - const timeoutsRef = useRef([]); - - const showLabels = array.length <= 35; - - useEffect(() => { - setDisplayArray([...array]); - clearAllTimeouts(); - }, [array]); - - useEffect(() => { - if (isPlaying) { - runAlgorithm(); - } else { - clearAllTimeouts(); - } - return () => clearAllTimeouts(); - }, [isPlaying, algorithmName]); - - const clearAllTimeouts = () => { - timeoutsRef.current.forEach(clearTimeout); - timeoutsRef.current = []; - }; - - const runAlgorithm = () => { - clearAllTimeouts(); - let steps = []; - const algoFunc = algorithms[`generate${algorithmName}Steps`]; - if (!algoFunc) return; - - // Pass searchTarget if it's a search algorithm - if (algorithmName.includes('Search')) { - steps = algoFunc(array, searchTarget); - } else { - steps = algoFunc(array); - } - - animate(steps); - }; - - const animate = (steps) => { - const delay = Math.max(1, 100 - speed); - steps.forEach((step, index) => { - const timeoutId = setTimeout(() => { - setDisplayArray(prev => { - // Use a new array reference to trigger re-renders - const newArr = [...prev]; - if (step.type === 'swap') { - newArr[step.indices[0]] = step.values[0]; - newArr[step.indices[1]] = step.values[1]; - } else if (step.type === 'overwrite') { - newArr[step.indices[0]] = step.value; - } - return newArr; - }); - - const bars = document.getElementsByClassName(`bar-${className}`); - if(bars.length > 0) { - if (step.type === 'compare') { - step.indices.forEach(idx => { if(bars[idx]) bars[idx].style.backgroundColor = '#ef4444' }); - } else if (step.type === 'swap' || step.type === 'overwrite') { - step.indices.forEach(idx => { if(bars[idx]) bars[idx].style.backgroundColor = '#eab308' }); - } else if (step.type === 'sorted' || step.type === 'found') { - step.indices.forEach(idx => { if(bars[idx]) bars[idx].style.backgroundColor = '#10b981' }); - } else if (step.type === 'revert') { - step.indices.forEach(idx => { if(bars[idx]) bars[idx].style.backgroundColor = '' }); - } else if (step.type === 'discard') { - // Gray out discarded parts of the array for Binary Search - if (step.range) { - for(let i=step.range[0]; i<=step.range[1]; i++) { - if(bars[i]) bars[i].style.opacity = '0.2'; - } - } - } - } - - if (index === steps.length - 1) { - if (onFinished) onFinished(); - } - }, index * delay); - timeoutsRef.current.push(timeoutId); - }); - }; - - return ( -
- {displayArray.map((val, idx) => ( -
- {showLabels && ( - - {val} - - )} -
-
- ))} -
- ); -}; - -const GraphVisualizer = ({ algorithmName, isPlaying, setIsPlaying, speed, onFinished, darkMode }) => { - const [nodes, setNodes] = useState([]); - const [edges, setEdges] = useState([]); - const [isDirected, setIsDirected] = useState(false); - const [isWeighted, setIsWeighted] = useState(false); - const [editMode, setEditMode] = useState('move'); - const [history, setHistory] = useState([]); - const [currentStep, setCurrentStep] = useState(-1); - const [startNode, setStartNode] = useState(0); - const [endNode, setEndNode] = useState(null); - const [draggingNode, setDraggingNode] = useState(null); - const [edgeStartNode, setEdgeStartNode] = useState(null); - const containerRef = useRef(null); - const animationRef = useRef(null); - - const visualState = useMemo(() => { - const visited = new Set(); - const path = new Set(); - const dists = {}; - let activeLink = null; - - if (currentStep >= 0 && currentStep < history.length) { - for (let i = 0; i <= currentStep; i++) { - const step = history[i]; - if (step.type === 'visit' || step.type === 'visit-node') { - visited.add(step.node); - } else if (step.type === 'path') { - step.path.forEach(n => path.add(n)); - } else if (step.type === 'update-dist') { - dists[step.node] = step.dist; - } - if (i === currentStep) { - if (step.type === 'traverse' || step.type === 'check-edge') { - activeLink = { from: step.from, to: step.to }; - } - } - } - } - return { visited, path, dists, activeLink }; - }, [currentStep, history]); - - useEffect(() => { resetGraph(); }, []); - - useEffect(() => { - if (!isPlaying && nodes.length > 0) { - calculateAlgorithmSteps(); - } - }, [nodes, edges, startNode, endNode, algorithmName, isDirected]); - - const calculateAlgorithmSteps = () => { - const numNodes = Math.max(...nodes.map(n => n.id), -1) + 1; - const adj = Array.from({ length: numNodes }, () => []); - edges.forEach(edge => { - if(nodes.find(n=>n.id === edge.from) && nodes.find(n=>n.id === edge.to)) { - adj[edge.from].push({ to: edge.to, weight: edge.weight }); - if (!isDirected) adj[edge.to].push({ to: edge.from, weight: edge.weight }); - } - }); - if (!nodes.find(n => n.id === startNode)) return; - let actualEnd = endNode; - if (endNode === null && nodes.length > 0) actualEnd = nodes[nodes.length-1].id; - - const algoFunc = algorithms[`generate${algorithmName}Steps`]; - if (algoFunc) { - let steps = []; - if (algorithmName === 'AStar') { - const nodeObjMap = nodes.reduce((acc, n) => ({...acc, [n.id]: n}), {}); - steps = algoFunc(numNodes, adj, startNode, actualEnd, nodeObjMap); - } else { - steps = algoFunc(numNodes, adj, startNode, actualEnd); - } - setHistory(steps); - } - }; - - useEffect(() => { - if (isPlaying) { - const delay = Math.max(50, 1000 - (speed * 9)); - animationRef.current = setInterval(() => { - setCurrentStep(prev => { - if (prev < history.length - 1) return prev + 1; - setIsPlaying(false); - if (onFinished) onFinished(); - return prev; - }); - }, delay); - } else { - clearInterval(animationRef.current); - } - return () => clearInterval(animationRef.current); - }, [isPlaying, history.length, speed]); - - const resetGraph = () => { - setIsPlaying(false); - setCurrentStep(-1); - const width = containerRef.current ? containerRef.current.clientWidth : 800; - const height = 400; - const numNodes = 10; - const newNodes = []; - for (let i = 0; i < numNodes; i++) newNodes.push({ id: i, x: Math.random() * (width - 100) + 50, y: Math.random() * (height - 100) + 50 }); - const newEdges = []; - for(let i = 1; i < numNodes; i++) { - const target = Math.floor(Math.random() * i); - const weight = Math.floor(Math.random() * 20) + 1; - newEdges.push({ from: i, to: target, weight }); - } - for(let i=0; i(e.from===u&&e.to===v) || (e.from===v&&e.to===u))) { - newEdges.push({ from: u, to: v, weight: Math.floor(Math.random()*20)+1 }); - } - } - setNodes(newNodes); - setEdges(newEdges); - setStartNode(0); - setEndNode(numNodes-1); - }; - - const handleMouseDown = (e, nodeId) => { - if (isPlaying) return; - e.stopPropagation(); - if (editMode === 'move' && nodeId !== undefined) setDraggingNode(nodeId); - else if (editMode === 'add-edge' && nodeId !== undefined) { - if (edgeStartNode === null) setEdgeStartNode(nodeId); - else { - if (edgeStartNode !== nodeId) { - const weight = isWeighted ? Math.floor(Math.random() * 20) + 1 : 1; - const exists = edges.some(edge => (edge.from === edgeStartNode && edge.to === nodeId) || (!isDirected && edge.from === nodeId && edge.to === edgeStartNode)); - if (!exists) setEdges(prev => [...prev, { from: edgeStartNode, to: nodeId, weight }]); - } - setEdgeStartNode(null); - } - } else if (editMode === 'delete' && nodeId !== undefined) { - setNodes(prev => prev.filter(n => n.id !== nodeId)); - setEdges(prev => prev.filter(e => e.from !== nodeId && e.to !== nodeId)); - if(startNode === nodeId) setStartNode(nodes.length > 0 ? nodes[0].id : null); - } - }; - - const handleCanvasClick = (e) => { - if (isPlaying) return; - if (editMode === 'add-node') { - const rect = containerRef.current.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - const newId = nodes.length > 0 ? Math.max(...nodes.map(n=>n.id)) + 1 : 0; - setNodes(prev => [...prev, { id: newId, x, y }]); - } - }; - - const handleMouseMove = (e) => { - if (draggingNode === null || !containerRef.current) return; - const rect = containerRef.current.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - setNodes(prev => prev.map(n => n.id === draggingNode ? { ...n, x, y } : n)); - }; - - const handleMouseUp = () => setDraggingNode(null); - - const handleNodeContext = (e, nodeId) => { - e.preventDefault(); - if(editMode === 'move') { setStartNode(nodeId); setCurrentStep(-1); } - }; - - return ( -
-
- {/* Graph Toolbar Buttons: Added explicit colors for visibility */} -
- - - - -
-
-
-
-
-
-
-
- - - -
-
- -
-
-
-

Start: {startNode}

-

End: {endNode ?? 'Last'}

-

{editMode === 'move' ? "Drag nodes. Right-click to set Start." : "Click to edit."}

-
-
- - - {edges.map((edge, idx) => { - const start = nodes.find(n => n.id === edge.from); - const end = nodes.find(n => n.id === edge.to); - if (!start || !end) return null; - const isActive = visualState.activeLink && ((visualState.activeLink.from === edge.from && visualState.activeLink.to === edge.to) || (!isDirected && visualState.activeLink.from === edge.to && visualState.activeLink.to === edge.from)); - return ( {isWeighted && ({edge.weight})}); - })} - {editMode === 'add-edge' && edgeStartNode !== null && (() => { const start = nodes.find(n => n.id === edgeStartNode); if(start) return ; return null; })()} - {nodes.map((node) => { - const isStart = node.id === startNode; - const isEnd = node.id === endNode; - const isVisited = visualState.visited.has(node.id); - const isPath = visualState.path.has(node.id); - const dist = visualState.dists[node.id]; - return ( handleMouseDown(e, node.id)} onClick={() => editMode === 'move' && setEndNode(node.id)} onContextMenu={(e) => handleNodeContext(e, node.id)}>{node.id}{(algorithmName === 'Dijkstra' || algorithmName === 'AStar') && dist !== undefined && ({dist === Infinity ? '∞' : dist})}); - })} - -
-
- ); -}; - // --- Main Page --- const DsaVisualization = () => { @@ -401,14 +43,14 @@ const DsaVisualization = () => { const [activeTab, setActiveTab] = useState('sorting'); const [algorithm, setAlgorithm] = useState('BubbleSort'); const [secondAlgorithm, setSecondAlgorithm] = useState('QuickSort'); - const [arraySize, setArraySize] = useState(20); + const [arraySize, setArraySize] = useState(20); const [speed, setSpeed] = useState(50); const [isPlaying, setIsPlaying] = useState(false); const [isComparisonMode, setIsComparisonMode] = useState(false); const [darkMode, setDarkMode] = useState(true); const [customInput, setCustomInput] = useState(''); const [searchTarget, setSearchTarget] = useState(42); // Default target - + const [array, setArray] = useState([]); useEffect(() => { @@ -423,7 +65,7 @@ const DsaVisualization = () => { // To keep it simple, we generate random. // If user selects BinarySearch, we might want to auto-sort or let them visualize fail/sort. const newArr = Array.from({ length: arraySize }, () => Math.floor(Math.random() * 100) + 5); - + // If searching, ensure the target is present randomly 50% of the time for fun, or just random. // Let's just keep random. setArray(newArr); @@ -433,11 +75,11 @@ const DsaVisualization = () => { bar.style.opacity = '1'; }); }; - + // Auto-sort if switching to Binary Search to avoid confusion useEffect(() => { if (algorithm === 'BinarySearch') { - setArray(prev => [...prev].sort((a,b) => a-b)); + setArray(prev => [...prev].sort((a, b) => a - b)); } }, [algorithm]); @@ -456,7 +98,8 @@ const DsaVisualization = () => { linkedlist: [], trees: [], heaps: [], - dp: [] + dp: [], + stack: [] }; return ( @@ -499,8 +142,8 @@ const DsaVisualization = () => {

-
)} - + {/* Fix: Added Search Target Input */} {activeTab === 'searching' && (
@@ -616,11 +259,11 @@ const DsaVisualization = () => { Search Target
- setSearchTarget(parseInt(e.target.value) || 0)} + setSearchTarget(parseInt(e.target.value) || 0)} className="h-9 text-sm" />
@@ -650,11 +293,11 @@ const DsaVisualization = () => {
- 공급 + 공급 {isComparisonMode && ( -
+

{secondAlgorithm.replace(/([A-Z])/g, ' $1').trim()}

@@ -672,7 +315,7 @@ const DsaVisualization = () => { ) : activeTab === 'linkedlist' ? ( - +

Linked List Visualizer

@@ -690,7 +333,7 @@ const DsaVisualization = () => { ) : activeTab === 'heaps' ? ( - +

Heap Visualizer (Max)

@@ -699,7 +342,7 @@ const DsaVisualization = () => { ) : activeTab === 'dp' ? ( - +

Dynamic Programming

@@ -707,8 +350,17 @@ const DsaVisualization = () => { + ) : activeTab === 'stack' ? ( + + +
+

Stack Visualizer

+
+ +
+
) : ( - +

Under Construction

)} From c802c7914a09e56d42ae9db129145b8f25feaa54 Mon Sep 17 00:00:00 2001 From: jeevanvishnu Date: Wed, 7 Jan 2026 21:05:12 +0530 Subject: [PATCH 2/2] feat: Add new contributor and apply minor formatting and visualization step adjustments to several algorithms. --- src/data/contributors.js | 7 + src/lib/dsaAlgorithms.js | 422 +++++++++++++++++++-------------------- 2 files changed, 218 insertions(+), 211 deletions(-) diff --git a/src/data/contributors.js b/src/data/contributors.js index 1e75b78..e475b3a 100644 --- a/src/data/contributors.js +++ b/src/data/contributors.js @@ -29,5 +29,12 @@ export const dsaContributors = [ role: "Backend Architecture", github: "https://github.com/jamesw", avatar: "https://images.unsplash.com/photo-1599566150163-29194dcaad36?w=150&h=150&fit=crop&crop=faces" + }, + { + name: "Jeevan vishnu", + role: "Full Stack Developer", + github: "https://github.com/jeevanvishnu", + avatar: "https://github.com/jeevanvishnu.png" } + ]; diff --git a/src/lib/dsaAlgorithms.js b/src/lib/dsaAlgorithms.js index f6a4286..0627210 100644 --- a/src/lib/dsaAlgorithms.js +++ b/src/lib/dsaAlgorithms.js @@ -31,11 +31,11 @@ export const generateBubbleSortSteps = (array) => { // Mark as sorted steps.push({ type: 'sorted', indices: [n - i - 1] }); if (!swapped) { - // Mark remaining as sorted - for(let k=0; k < n-i-1; k++) { - steps.push({ type: 'sorted', indices: [k] }); - } - break; + // Mark remaining as sorted + for (let k = 0; k < n - i - 1; k++) { + steps.push({ type: 'sorted', indices: [k] }); + } + break; } } steps.push({ type: 'sorted', indices: [0] }); // First element is sorted @@ -88,14 +88,14 @@ export const generateInsertionSortSteps = (array) => { steps.push({ type: 'compare', indices: [j, j + 1] }); steps.push({ type: 'overwrite', indices: [j + 1], value: arr[j] }); // Shift arr[j + 1] = arr[j]; - steps.push({ type: 'revert', indices: [j, j+1] }); + steps.push({ type: 'revert', indices: [j, j + 1] }); j = j - 1; } steps.push({ type: 'overwrite', indices: [j + 1], value: key }); // Insert arr[j + 1] = key; - + // Mark all up to i as sorted (conceptually) - for(let k=0; k<=i; k++) steps.push({ type: 'sorted', indices: [k] }); + for (let k = 0; k <= i; k++) steps.push({ type: 'sorted', indices: [k] }); } return steps; }; @@ -132,18 +132,18 @@ function doMerge(mainArray, startIdx, middleIdx, endIdx, auxArray, steps) { steps.push({ type: 'overwrite', indices: [k], value: auxArray[j] }); mainArray[k++] = auxArray[j++]; } - steps.push({ type: 'revert', indices: [i-1, j-1] }); // Rough revert for visualization - steps.push({ type: 'sorted', indices: [k-1] }); // Mark placed items as sorted (temporarily) + steps.push({ type: 'revert', indices: [i - 1, j - 1] }); // Rough revert for visualization + steps.push({ type: 'sorted', indices: [k - 1] }); // Mark placed items as sorted (temporarily) } while (i <= middleIdx) { steps.push({ type: 'overwrite', indices: [k], value: auxArray[i] }); mainArray[k++] = auxArray[i++]; - steps.push({ type: 'sorted', indices: [k-1] }); + steps.push({ type: 'sorted', indices: [k - 1] }); } while (j <= endIdx) { steps.push({ type: 'overwrite', indices: [k], value: auxArray[j] }); mainArray[k++] = auxArray[j++]; - steps.push({ type: 'sorted', indices: [k-1] }); + steps.push({ type: 'sorted', indices: [k - 1] }); } } @@ -162,7 +162,7 @@ function quickSortHelper(arr, low, high, steps) { quickSortHelper(arr, low, pi - 1, steps); quickSortHelper(arr, pi + 1, high, steps); } else if (low === high) { - steps.push({ type: 'sorted', indices: [low] }); + steps.push({ type: 'sorted', indices: [low] }); } } @@ -179,7 +179,7 @@ function partition(arr, low, high, steps) { swap(arr, i, j); steps.push({ type: 'revert', indices: [i, j] }); } else { - steps.push({ type: 'revert', indices: [j] }); + steps.push({ type: 'revert', indices: [j] }); } } steps.push({ type: 'swap', indices: [i + 1, high], values: [arr[high], arr[i + 1]] }); @@ -192,236 +192,236 @@ function partition(arr, low, high, steps) { // --- Searching Algorithms --- export const generateLinearSearchSteps = (array, target) => { - const steps = []; - for (let i = 0; i < array.length; i++) { - steps.push({ type: 'compare', indices: [i] }); - if (array[i] === target) { - steps.push({ type: 'found', indices: [i] }); - return steps; - } - steps.push({ type: 'visited', indices: [i] }); + const steps = []; + for (let i = 0; i < array.length; i++) { + steps.push({ type: 'compare', indices: [i] }); + if (array[i] === target) { + steps.push({ type: 'found', indices: [i] }); + return steps; } - return steps; + steps.push({ type: 'visited', indices: [i] }); + } + return steps; } export const generateBinarySearchSteps = (array, target) => { - // Expects sorted array, but for visualization we might run it on unsorted to show failure or sort first. - // We will assume the UI sorts it or provides sorted data for BS. - const steps = []; - let left = 0; - let right = array.length - 1; - - while (left <= right) { - const mid = Math.floor((left + right) / 2); - steps.push({ type: 'compare', indices: [mid], range: [left, right] }); // Highlight mid and range - - if (array[mid] === target) { - steps.push({ type: 'found', indices: [mid] }); - return steps; - } - - if (array[mid] < target) { - steps.push({ type: 'discard', indices: [], range: [left, mid] }); // Grey out left half - left = mid + 1; - } else { - steps.push({ type: 'discard', indices: [], range: [mid, right] }); // Grey out right half - right = mid - 1; - } + // Expects sorted array, but for visualization we might run it on unsorted to show failure or sort first. + // We will assume the UI sorts it or provides sorted data for BS. + const steps = []; + let left = 0; + let right = array.length - 1; + + while (left <= right) { + const mid = Math.floor((left + right) / 2); + steps.push({ type: 'compare', indices: [mid], range: [left, right] }); // Highlight mid and range + + if (array[mid] === target) { + steps.push({ type: 'found', indices: [mid] }); + return steps; } - return steps; + + if (array[mid] < target) { + steps.push({ type: 'discard', indices: [], range: [left, mid] }); // Grey out left half + left = mid + 1; + } else { + steps.push({ type: 'discard', indices: [], range: [mid, right] }); // Grey out right half + right = mid - 1; + } + } + return steps; } // --- Graph Algorithms --- // Helper to get Euclidean distance for A* heuristic const getDist = (nodeA, nodeB) => { - return Math.sqrt(Math.pow(nodeA.x - nodeB.x, 2) + Math.pow(nodeA.y - nodeB.y, 2)); + return Math.sqrt(Math.pow(nodeA.x - nodeB.x, 2) + Math.pow(nodeA.y - nodeB.y, 2)); }; export const generateBFSSteps = (numNodes, adj, startNode, endNode) => { - const steps = []; - const visited = new Array(numNodes).fill(false); - const queue = [startNode]; - visited[startNode] = true; - const prev = new Array(numNodes).fill(null); - - steps.push({ type: 'visit', node: startNode }); - - while(queue.length > 0) { - const u = queue.shift(); - if (u === endNode) break; - - for(let edge of adj[u]) { - const v = edge.to; - if(!visited[v]) { - visited[v] = true; - prev[v] = u; - steps.push({ type: 'traverse', from: u, to: v }); - steps.push({ type: 'visit', node: v }); - queue.push(v); - if (v === endNode) break; - } - } + const steps = []; + const visited = new Array(numNodes).fill(false); + const queue = [startNode]; + visited[startNode] = true; + const prev = new Array(numNodes).fill(null); + + steps.push({ type: 'visit', node: startNode }); + + while (queue.length > 0) { + const u = queue.shift(); + if (u === endNode) break; + + for (let edge of adj[u]) { + const v = edge.to; + if (!visited[v]) { + visited[v] = true; + prev[v] = u; + steps.push({ type: 'traverse', from: u, to: v }); + steps.push({ type: 'visit', node: v }); + queue.push(v); + if (v === endNode) break; + } } + } - // Reconstruct path - if (visited[endNode]) { - const path = []; - let curr = endNode; - while(curr !== null) { - path.unshift(curr); - curr = prev[curr]; - } - steps.push({ type: 'path', path }); + // Reconstruct path + if (visited[endNode]) { + const path = []; + let curr = endNode; + while (curr !== null) { + path.unshift(curr); + curr = prev[curr]; } + steps.push({ type: 'path', path }); + } - return steps; + return steps; }; export const generateDFSSteps = (numNodes, adj, startNode, endNode) => { - const steps = []; - const visited = new Array(numNodes).fill(false); - const prev = new Array(numNodes).fill(null); - - const dfs = (u) => { - visited[u] = true; - steps.push({ type: 'visit', node: u }); - - if (u === endNode) return true; - - for(let edge of adj[u]) { - const v = edge.to; - if(!visited[v]) { - prev[v] = u; - steps.push({ type: 'traverse', from: u, to: v }); - if (dfs(v)) return true; - } - } - return false; - }; - - dfs(startNode); - - // Reconstruct path - if (visited[endNode]) { - const path = []; - let curr = endNode; - while(curr !== null) { - path.unshift(curr); - curr = prev[curr]; - } - steps.push({ type: 'path', path }); + const steps = []; + const visited = new Array(numNodes).fill(false); + const prev = new Array(numNodes).fill(null); + + const dfs = (u) => { + visited[u] = true; + steps.push({ type: 'visit', node: u }); + + if (u === endNode) return true; + + for (let edge of adj[u]) { + const v = edge.to; + if (!visited[v]) { + prev[v] = u; + steps.push({ type: 'traverse', from: u, to: v }); + if (dfs(v)) return true; + } } - return steps; + return false; + }; + + dfs(startNode); + + // Reconstruct path + if (visited[endNode]) { + const path = []; + let curr = endNode; + while (curr !== null) { + path.unshift(curr); + curr = prev[curr]; + } + steps.push({ type: 'path', path }); + } + return steps; }; export const generateDijkstraSteps = (numNodes, adj, startNode, endNode) => { - const steps = []; - const dist = new Array(numNodes).fill(Infinity); - const prev = new Array(numNodes).fill(null); - const visited = new Array(numNodes).fill(false); - - dist[startNode] = 0; - // Using array as priority queue for simplicity in visualization - const pq = [{ node: startNode, dist: 0 }]; - - steps.push({ type: 'update-dist', node: startNode, dist: 0 }); - - while (pq.length > 0) { - pq.sort((a, b) => a.dist - b.dist); - const { node: u } = pq.shift(); - - if (visited[u]) continue; - visited[u] = true; - steps.push({ type: 'visit', node: u }); - - if (u === endNode) break; - - for (let edge of adj[u]) { - const v = edge.to; - const weight = edge.weight; - if (!visited[v]) { - steps.push({ type: 'check-edge', from: u, to: v }); - const alt = dist[u] + weight; - if (alt < dist[v]) { - dist[v] = alt; - prev[v] = u; - pq.push({ node: v, dist: alt }); - steps.push({ type: 'update-dist', node: v, dist: alt }); - } - } + const steps = []; + const dist = new Array(numNodes).fill(Infinity); + const prev = new Array(numNodes).fill(null); + const visited = new Array(numNodes).fill(false); + + dist[startNode] = 0; + // Using array as priority queue for simplicity in visualization + const pq = [{ node: startNode, dist: 0 }]; + + steps.push({ type: 'update-dist', node: startNode, dist: 0 }); + + while (pq.length > 0) { + pq.sort((a, b) => a.dist - b.dist); + const { node: u } = pq.shift(); + + if (visited[u]) continue; + visited[u] = true; + steps.push({ type: 'visit', node: u }); + + if (u === endNode) break; + + for (let edge of adj[u]) { + const v = edge.to; + const weight = edge.weight; + if (!visited[v]) { + steps.push({ type: 'check-edge', from: u, to: v }); + const alt = dist[u] + weight; + if (alt < dist[v]) { + dist[v] = alt; + prev[v] = u; + pq.push({ node: v, dist: alt }); + steps.push({ type: 'update-dist', node: v, dist: alt }); } + } } - - // Path reconstruction - if (dist[endNode] !== Infinity) { - const path = []; - let curr = endNode; - while (curr !== null) { - path.unshift(curr); - curr = prev[curr]; - } - steps.push({ type: 'path', path }); + } + + // Path reconstruction + if (dist[endNode] !== Infinity) { + const path = []; + let curr = endNode; + while (curr !== null) { + path.unshift(curr); + curr = prev[curr]; } - - return steps; + steps.push({ type: 'path', path }); + } + + return steps; }; export const generateAStarSteps = (numNodes, adj, startNode, endNode, nodes) => { - const steps = []; - const gScore = new Array(numNodes).fill(Infinity); - const fScore = new Array(numNodes).fill(Infinity); - const prev = new Array(numNodes).fill(null); - const visited = new Array(numNodes).fill(false); - - gScore[startNode] = 0; - fScore[startNode] = getDist(nodes[startNode], nodes[endNode]); - - const pq = [{ node: startNode, f: fScore[startNode] }]; - - steps.push({ type: 'update-dist', node: startNode, dist: 0 }); // Visually similar to dist update - - while (pq.length > 0) { - pq.sort((a, b) => a.f - b.f); - const { node: u } = pq.shift(); - - if (visited[u]) continue; - visited[u] = true; - steps.push({ type: 'visit', node: u }); - - if (u === endNode) break; - - for (let edge of adj[u]) { - const v = edge.to; - const weight = edge.weight; - - // A* specific: neighbor check - steps.push({ type: 'check-edge', from: u, to: v }); - - const tentativeG = gScore[u] + weight; - if (tentativeG < gScore[v]) { - prev[v] = u; - gScore[v] = tentativeG; - fScore[v] = gScore[v] + getDist(nodes[v], nodes[endNode]); // h(n) = euclidean - - // Update visual dist (show gScore) - steps.push({ type: 'update-dist', node: v, dist: Math.round(tentativeG) }); - - // Add to open set if not present (or re-add, simplified) - pq.push({ node: v, f: fScore[v] }); - } - } + const steps = []; + const gScore = new Array(numNodes).fill(Infinity); + const fScore = new Array(numNodes).fill(Infinity); + const prev = new Array(numNodes).fill(null); + const visited = new Array(numNodes).fill(false); + + gScore[startNode] = 0; + fScore[startNode] = getDist(nodes[startNode], nodes[endNode]); + + const pq = [{ node: startNode, f: fScore[startNode] }]; + + steps.push({ type: 'update-dist', node: startNode, dist: 0 }); // Visually similar to dist update + + while (pq.length > 0) { + pq.sort((a, b) => a.f - b.f); + const { node: u } = pq.shift(); + + if (visited[u]) continue; + visited[u] = true; + steps.push({ type: 'visit', node: u }); + + if (u === endNode) break; + + for (let edge of adj[u]) { + const v = edge.to; + const weight = edge.weight; + + // A* specific: neighbor check + steps.push({ type: 'check-edge', from: u, to: v }); + + const tentativeG = gScore[u] + weight; + if (tentativeG < gScore[v]) { + prev[v] = u; + gScore[v] = tentativeG; + fScore[v] = gScore[v] + getDist(nodes[v], nodes[endNode]); // h(n) = euclidean + + // Update visual dist (show gScore) + steps.push({ type: 'update-dist', node: v, dist: Math.round(tentativeG) }); + + // Add to open set if not present (or re-add, simplified) + pq.push({ node: v, f: fScore[v] }); + } } + } - if (gScore[endNode] !== Infinity) { - const path = []; - let curr = endNode; - while (curr !== null) { - path.unshift(curr); - curr = prev[curr]; - } - steps.push({ type: 'path', path }); + if (gScore[endNode] !== Infinity) { + const path = []; + let curr = endNode; + while (curr !== null) { + path.unshift(curr); + curr = prev[curr]; } + steps.push({ type: 'path', path }); + } - return steps; + return steps; }