From f491d5e7e2a5adeb2b8a4c70ff0f38c51133adae Mon Sep 17 00:00:00 2001 From: *vivek Date: Sun, 4 Jan 2026 11:02:43 +0530 Subject: [PATCH] feat(graph): improve node hover interaction and add GraphVisualizer component --- src/components/dsa/GraphVisualizer.jsx | 257 +++++++++++++++++++++++++ src/pages/DsaVisualization.jsx | 248 +----------------------- 2 files changed, 259 insertions(+), 246 deletions(-) create mode 100644 src/components/dsa/GraphVisualizer.jsx diff --git a/src/components/dsa/GraphVisualizer.jsx b/src/components/dsa/GraphVisualizer.jsx new file mode 100644 index 0000000..3a7c50d --- /dev/null +++ b/src/components/dsa/GraphVisualizer.jsx @@ -0,0 +1,257 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { + Play, Pause, Network, + PlusCircle, Move, Trash2, StepForward, + StepBack +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Switch } from '@/components/ui/switch'; +import * as algorithms from '@/lib/dsaAlgorithms'; +import { cn } from '@/lib/utils'; + +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})}); + })} + +
+
+ ); +}; + +export default GraphVisualizer \ No newline at end of file diff --git a/src/pages/DsaVisualization.jsx b/src/pages/DsaVisualization.jsx index 5caefa3..cd30538 100644 --- a/src/pages/DsaVisualization.jsx +++ b/src/pages/DsaVisualization.jsx @@ -32,8 +32,9 @@ import TreeVisualizer from '@/components/dsa/TreeVisualizer'; import HeapVisualizer from '@/components/dsa/HeapVisualizer'; import DPVisualizer from '@/components/dsa/DPVisualizer'; import ContributorsSection from '@/components/dsa/ContributorsSection'; +import GraphVisualizer from '../components/dsa/GraphVisualizer'; -// --- Existing Sorting/Graph Components --- +// --- Sorting --- const SortingVisualizer = ({ array, algorithmName, isPlaying, speed, onFinished, className, searchTarget }) => { const [displayArray, setDisplayArray] = useState([...array]); @@ -149,251 +150,6 @@ const SortingVisualizer = ({ array, algorithmName, isPlaying, speed, onFinished, ); }; -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 = () => {