diff --git a/app/graph/[algorithm]/layout.tsx b/app/graph/[algorithm]/layout.tsx new file mode 100644 index 0000000..fc0d5bc --- /dev/null +++ b/app/graph/[algorithm]/layout.tsx @@ -0,0 +1,49 @@ +import { AlgorithmProvider } from "@/context/AlgorithmContext"; +import { Metadata } from "next"; +import { availableAlgorithms } from "@/lib/algorithms/metadata"; +import { constructMetadata } from "@/lib/seo/metadata"; + +type Props = { + params: Promise<{ algorithm: string }>; +}; + +export async function generateMetadata(props: Props): Promise { + const params = await props.params; + const { algorithm } = params; + + // Find the algorithm in our available list + const algorithmInfo = availableAlgorithms[algorithm]; + + if (!algorithmInfo) { + return constructMetadata({ + title: "Algorithm Not Found", + description: "The requested algorithm could not be found.", + path: `/graph/${algorithm}`, + }); + } + + const { name, description, difficulty, category } = algorithmInfo; + + return constructMetadata({ + title: name, + description, + path: `/graph/${algorithm}`, + keywords: [ + algorithm, + name.toLowerCase(), + `${name.toLowerCase()} visualization`, + `${name.toLowerCase()} explanation`, + `${name.toLowerCase()} algorithm`, + `${difficulty} algorithm`, + `${category} algorithm`, + ], + }); +} + +export default function GraphLayout({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/app/graph/[algorithm]/page.tsx b/app/graph/[algorithm]/page.tsx new file mode 100644 index 0000000..ed53974 --- /dev/null +++ b/app/graph/[algorithm]/page.tsx @@ -0,0 +1,79 @@ +"use client"; + +import React, { useEffect } from "react"; +import { useParams } from "next/navigation"; +import PageLayout from "@/components/layout/PageLayout"; +import AlgorithmVisualizer from "@/components/visualizer/AlgorithmVisualizer"; +import { useAlgorithm } from "@/context/AlgorithmContext"; +import { getAlgorithmByName } from "@/lib/algorithms"; +import { availableAlgorithms } from "@/lib/algorithms/metadata"; + +export default function GraphPage() { + const params = useParams(); + const algorithmKey = params.algorithm as string; + const { dispatch, state } = useAlgorithm(); + + const algorithmInfo = availableAlgorithms[algorithmKey]; + + // Set the current algorithm and generate visualization + useEffect(() => { + if (algorithmKey) { + dispatch({ type: "SET_ALGORITHM", payload: algorithmKey }); + + // Generate visualization if not already generated OR if algorithm changed + if (!state.visualizationData || algorithmKey !== state.algorithm) { + const algorithmFunction = getAlgorithmByName(algorithmKey); + if (algorithmFunction) { + try { + // For graph algorithms, ensure we're using a valid start vertex + // Our graph only has vertices 0-5, so ensure the start vertex is in this range + // If there's no valid target set, use vertex 0 as default + const startVertex = + state.target === undefined + ? 0 + : state.target >= 0 && state.target < 6 + ? state.target + : 0; + // Set the target so it's properly displayed in the UI + dispatch({ type: "SET_TARGET", payload: startVertex }); + const viz = algorithmFunction([], startVertex); + dispatch({ type: "GENERATE_VISUALIZATION", payload: viz }); + } catch (error) { + console.error("Error generating visualization:", error); + } + } + } + } + }, [ + algorithmKey, + dispatch, + state.algorithm, + state.data, + state.visualizationData, + state.target, + ]); + + if (!algorithmInfo) { + return ( + +
+

Algorithm Not Found

+

+ The algorithm you are looking for does not exist or is not + available. +

+
+
+ ); + } + + return ( + + + + ); +} diff --git a/app/graph/page.tsx b/app/graph/page.tsx new file mode 100644 index 0000000..87c2ce6 --- /dev/null +++ b/app/graph/page.tsx @@ -0,0 +1,22 @@ +import PageLayout from "@/components/layout/PageLayout"; +import AlgorithmCard from "@/components/AlgorithmCard"; +import { availableAlgorithms } from "@/lib/algorithms/metadata"; + +export default function GraphAlgorithms() { + const graphAlgorithms = Object.entries(availableAlgorithms).filter( + ([, algo]) => algo.category === "graph" + ); + + return ( + +
+ {graphAlgorithms.map((algorithm) => ( + + ))} +
+
+ ); +} diff --git a/components/visualizer/AlgorithmVisualizer.tsx b/components/visualizer/AlgorithmVisualizer.tsx index d48f896..abadc7b 100644 --- a/components/visualizer/AlgorithmVisualizer.tsx +++ b/components/visualizer/AlgorithmVisualizer.tsx @@ -2,20 +2,38 @@ import SortingVisualization from "./SortingVisualization"; import SearchVisualization from "./SearchVisualization"; +import GraphVisualization from "./GraphVisualization"; import VisualizerControls from "./VisualizerControls"; import AlgorithmInfo from "./AlgorithmInfo"; import AlgorithmPseudocode from "./AlgorithmPseudocode"; import ColorLegend from "./ColorLegend"; import { useAlgorithm } from "@/context/AlgorithmContext"; import { getAlgorithmByName } from "@/lib/algorithms"; -import { SearchStep, SortingStep } from "@/lib/types"; +import { GraphStep, SearchStep, SortingStep } from "@/lib/types"; export default function AlgorithmVisualizer() { const { state, dispatch } = useAlgorithm(); const { currentStep, algorithm, data, visualizationData, target } = state; - // Generate a new random array - const handleGenerateNewArray = () => { + // Function to generate new data based on algorithm type + const handleGenerateNewData = () => { + // For graph algorithms, we just need to regenerate with a new starting vertex + if (category === "graph") { + const algorithmFunction = getAlgorithmByName(algorithm); + if (algorithmFunction) { + try { + // Generate a random starting vertex between 0-5 + const startVertex = Math.floor(Math.random() * 6); + const viz = algorithmFunction([], startVertex); + dispatch({ type: "GENERATE_VISUALIZATION", payload: viz }); + } catch (error) { + console.error("Error generating graph visualization:", error); + } + } + return; + } + + // For array-based algorithms, generate a new random array dispatch({ type: "GENERATE_RANDOM_DATA", payload: { min: 5, max: 95, length: 15 }, @@ -25,14 +43,17 @@ export default function AlgorithmVisualizer() { const algorithmFunction = getAlgorithmByName(algorithm); if (algorithmFunction) { try { - const newData = [...state.data]; + let viz; - // Special handling for binary search - ensure sorted array and target exists if (algorithm === "binarySearch") { - newData.sort((a, b) => a - b); + // For binary search - ensure sorted array and target exists + const newData = [...state.data].sort((a, b) => a - b); + viz = algorithmFunction(newData, target); + } else { + // For other algorithms like sorting + viz = algorithmFunction(state.data, target); } - const viz = algorithmFunction(newData, target); dispatch({ type: "GENERATE_VISUALIZATION", payload: viz }); } catch (error) { console.error("Error generating visualization:", error); @@ -40,9 +61,9 @@ export default function AlgorithmVisualizer() { } }; - // Check if the current algorithm is a search algorithm - const isSearchAlgorithm = () => { - return visualizationData?.category === "searching"; + // Check the algorithm category + const getAlgorithmCategory = () => { + return visualizationData?.category || ""; }; // Find the maximum value in the array for scaling @@ -59,7 +80,7 @@ export default function AlgorithmVisualizer() { ); } - const isSearching = isSearchAlgorithm(); + const category = getAlgorithmCategory(); return (
@@ -68,15 +89,21 @@ export default function AlgorithmVisualizer() {
- {isSearching ? ( + {category === "sorting" && ( + + )} + {category === "searching" && ( - ) : ( - )}
@@ -84,10 +111,14 @@ export default function AlgorithmVisualizer() { - +
diff --git a/components/visualizer/ColorLegend.tsx b/components/visualizer/ColorLegend.tsx index 936d077..215801b 100644 --- a/components/visualizer/ColorLegend.tsx +++ b/components/visualizer/ColorLegend.tsx @@ -1,9 +1,11 @@ interface ColorLegendProps { isSearchAlgorithm?: boolean; + isGraphAlgorithm?: boolean; } export default function ColorLegend({ isSearchAlgorithm = false, + isGraphAlgorithm = false, }: ColorLegendProps) { const sortingLegendItems = [ { color: "bg-blue-400", label: "Unsorted" }, @@ -19,9 +21,20 @@ export default function ColorLegend({ { color: "bg-green-500", label: "Found" }, ]; - const legendItems = isSearchAlgorithm - ? searchLegendItems - : sortingLegendItems; + const graphLegendItems = [ + { color: "bg-blue-300", label: "Unvisited Vertex" }, + { color: "bg-yellow-300", label: "Current Vertex" }, + { color: "bg-green-300", label: "Visited Vertex" }, + { color: "bg-red-400", label: "Path Edge" }, + ]; + + let legendItems = sortingLegendItems; + + if (isSearchAlgorithm) { + legendItems = searchLegendItems; + } else if (isGraphAlgorithm) { + legendItems = graphLegendItems; + } return (
diff --git a/components/visualizer/GraphVisualization.tsx b/components/visualizer/GraphVisualization.tsx new file mode 100644 index 0000000..b3bdd5d --- /dev/null +++ b/components/visualizer/GraphVisualization.tsx @@ -0,0 +1,141 @@ +import { GraphStep } from "@/lib/types"; +import { useState, useEffect } from "react"; + +interface GraphVisualizationProps { + step: GraphStep; +} + +export default function GraphVisualization({ step }: GraphVisualizationProps) { + const { adjacencyList, current, visited, stack, path } = step; + const [graphSize, setGraphSize] = useState({ width: 0, height: 0 }); + + // Calculate layout dimensions based on window size + useEffect(() => { + const updateSize = () => { + const width = Math.min(600, window.innerWidth - 40); + const height = Math.min(400, width * 0.8); + setGraphSize({ width, height }); + }; + + updateSize(); + window.addEventListener("resize", updateSize); + return () => window.removeEventListener("resize", updateSize); + }, []); + + // Check if adjacencyList is valid + if ( + !adjacencyList || + !Array.isArray(adjacencyList) || + adjacencyList.length === 0 + ) { + return ( +
+ Invalid graph data. Please check the adjacency list. +
+ ); + } + + // Calculate coordinates for each vertex in a circular layout + const vertexPositions = adjacencyList.map((_, index) => { + const angle = (2 * Math.PI * index) / adjacencyList.length; + const radius = Math.min(graphSize.width, graphSize.height) * 0.35; + return { + x: graphSize.width / 2 + radius * Math.cos(angle), + y: graphSize.height / 2 + radius * Math.sin(angle), + }; + }); + + // Function to determine vertex color based on its state + const getVertexColor = (index: number) => { + if (index === current) return "#FCD34D"; // yellow-300 + if (visited.includes(index)) return "#6EE7B7"; // green-300 + return "#93C5FD"; // blue-300 + }; + + // Function to determine edge color based on whether it's in the path + const getEdgeColor = (source: number, target: number) => { + // Check if this edge is part of the current path + const sourceInPath = path.includes(source); + const targetInPath = path.includes(target); + const adjacentInPath = + sourceInPath && + targetInPath && + Math.abs(path.indexOf(source) - path.indexOf(target)) === 1; + + return adjacentInPath ? "#F87171" : "#CBD5E1"; // red-400 : gray-300 + }; + + return ( +
+
+
+ Visited: {visited.map((v) => v).join(" → ")} +
+
+ Stack: [{stack.join(", ")}] +
+
+ +
+ {/* Draw edges */} + + {adjacencyList.map((neighbors, vertex) => + neighbors.map( + (neighbor) => + // Only draw each edge once + vertex < neighbor && ( + + ) + ) + )} + + + {/* Draw vertices */} + {vertexPositions.map((pos, index) => ( +
+ {index} +
+ ))} +
+ +
+
+ Current State: +
+
+ {current >= 0 ? ( +

Exploring vertex {current} and its unvisited neighbors.

+ ) : stack.length > 0 ? ( +

Starting exploration from vertex {stack[0] || 0}.

+ ) : path.length > 0 ? ( +

DFS traversal complete. Path: {path.join(" → ")}

+ ) : ( +

DFS traversal will start from a selected vertex.

+ )} +
+
+
+ ); +} diff --git a/components/visualizer/VisualizerControls.tsx b/components/visualizer/VisualizerControls.tsx index c6f5f1c..1f5f3ab 100644 --- a/components/visualizer/VisualizerControls.tsx +++ b/components/visualizer/VisualizerControls.tsx @@ -6,12 +6,14 @@ interface VisualizerControlsProps { currentStep: number; totalSteps: number; onGenerateNewArray: () => void; + algorithmCategory?: string; } export default function VisualizerControls({ currentStep, totalSteps, onGenerateNewArray, + algorithmCategory = "sorting", }: VisualizerControlsProps) { const { state, dispatch } = useAlgorithm(); const { isPlaying, speed } = state; @@ -28,6 +30,10 @@ export default function VisualizerControls({ const handleStepChange = (e: React.ChangeEvent) => dispatch({ type: "SET_CURRENT_STEP", payload: parseInt(e.target.value) }); + // Determine button text based on algorithm category + const newDataButtonText = + algorithmCategory === "graph" ? "New Graph" : "New Array"; + return (
@@ -204,7 +210,7 @@ export default function VisualizerControls({ d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> - New Array + {newDataButtonText}
diff --git a/lib/algorithms/graph/dfs.ts b/lib/algorithms/graph/dfs.ts new file mode 100644 index 0000000..66a69b3 --- /dev/null +++ b/lib/algorithms/graph/dfs.ts @@ -0,0 +1,113 @@ +import { AlgorithmVisualization, GraphStep } from "../../types"; +import { createVisualization } from "../utils"; + +export function dfs( + array: number[], + target: number = 0 +): AlgorithmVisualization { + // Ignore the input array and use a predefined adjacency list for graph visualization + // The target parameter will be used as the starting vertex, but ensure it's valid + // Create a sample graph as an adjacency list first + const adjacencyList: number[][] = [ + [1, 2], // Vertex 0 connected to 1, 2 + [0, 3, 4], // Vertex 1 connected to 0, 3, 4 + [0, 5], // Vertex 2 connected to 0, 5 + [1], // Vertex 3 connected to 1 + [1, 5], // Vertex 4 connected to 1, 5 + [2, 4], // Vertex 5 connected to 2, 4 + ]; + + // Ensure startVertex is valid (0 to 5) + const startVertex = target >= 0 && target < adjacencyList.length ? target : 0; + + // We've already defined the adjacencyList above + + const steps: GraphStep[] = []; + const visited: number[] = []; + const stack: number[] = [startVertex]; + const path: number[] = []; + + // Initial state + steps.push({ + adjacencyList: adjacencyList.map((row) => [...row]), // Deep copy + current: -1, + visited: [...visited], + stack: [...stack], + path: [...path], + }); + + while (stack.length > 0) { + // Get the next vertex from the stack + const current = stack.pop()!; + + // Skip if already visited + if (visited.includes(current)) continue; + + // Make sure current vertex is valid + if (current < 0 || current >= adjacencyList.length) continue; + + // Mark as visited + visited.push(current); + path.push(current); + + // Add step for current vertex visit + steps.push({ + adjacencyList: adjacencyList.map((row) => [...row]), + current, + visited: [...visited], + stack: [...stack], + path: [...path], + }); + + // Get all adjacent vertices in reverse order (so they come off the stack in correct order) + // Ensure the current index is valid in the adjacency list + const neighbors = [...adjacencyList[current]].reverse(); + + // For each neighbor + for (const neighbor of neighbors) { + // If not visited, add to stack + if (!visited.includes(neighbor)) { + stack.push(neighbor); + + // Add step showing stack update + steps.push({ + adjacencyList: adjacencyList.map((row) => [...row]), + current, + visited: [...visited], + stack: [...stack], + path: [...path], + }); + } + } + } + + // Final state + steps.push({ + adjacencyList: adjacencyList.map((row) => [...row]), + current: -1, + visited: [...visited], + stack: [], + path: [...path], + }); + + return createVisualization("dfs", steps, { + timeComplexity: "O(V + E)", // Vertices + Edges + spaceComplexity: "O(V)", // Vertices for stack and visited set + reference: "https://en.wikipedia.org/wiki/Depth-first_search", + pseudoCode: [ + "procedure DFS(G, start_v)", + " let S be a stack", + " S.push(start_v)", + " while S is not empty do", + " v = S.pop()", + " if v is not visited then", + " mark v as visited", + " for all neighbors w of v do", + " S.push(w)", + " end for", + " end if", + " end while", + "end procedure", + ], + }); +} diff --git a/lib/algorithms/index.ts b/lib/algorithms/index.ts index c65bb2f..d933bb8 100644 --- a/lib/algorithms/index.ts +++ b/lib/algorithms/index.ts @@ -7,12 +7,20 @@ import { quickSort } from "./sorting/quickSort"; import { heapSort } from "./sorting/heapSort"; import { linearSearch } from "./searching/linearSearch"; import { binarySearch } from "./searching/binarySearch"; +import { dfs } from "./graph/dfs"; + +// Define types for different algorithm categories +type SortingOrSearchFunction = ( + array: number[], + target?: number +) => AlgorithmVisualization; +type GraphFunction = ( + array: number[], + startVertex?: number +) => AlgorithmVisualization; // Map algorithm names to their implementation functions -const algorithms: Record< - string, - (array: number[], target?: number) => AlgorithmVisualization -> = { +const algorithms: Record = { bubbleSort, selectionSort, insertionSort, @@ -21,6 +29,7 @@ const algorithms: Record< heapSort, linearSearch, binarySearch, + dfs, }; // Get algorithm function by name @@ -40,4 +49,5 @@ export { heapSort, linearSearch, binarySearch, + dfs, }; diff --git a/lib/algorithms/metadata.ts b/lib/algorithms/metadata.ts index 15e1c26..63937c0 100644 --- a/lib/algorithms/metadata.ts +++ b/lib/algorithms/metadata.ts @@ -73,4 +73,14 @@ export const availableAlgorithms: Record = { "Binary search finds the position of a target value within a sorted array by repeatedly dividing the search interval in half. With a logarithmic time complexity of O(log n), it's dramatically more efficient than linear search for large datasets. Binary search requires the array to be sorted beforehand, making it ideal for situations where searching occurs frequently on relatively static data. This algorithm is the foundation for many efficient data structures like binary search trees and is widely used in database systems, dictionaries, and numerous programming applications.", difficulty: "medium", }, + dfs: { + name: "Depth-First Search", + key: "dfs", + category: "graph", + subtitle: + "Graph traversal algorithm that explores as far as possible along each branch before backtracking", + description: + "Depth-First Search (DFS) is a graph traversal algorithm that explores as far as possible along each branch before backtracking. It uses a stack data structure (often implemented using recursion) to keep track of vertices to visit next. DFS has applications in topological sorting, finding connected components, solving mazes, and detecting cycles in graphs.", + difficulty: "medium", + }, }; diff --git a/lib/algorithms/utils.ts b/lib/algorithms/utils.ts index 19fc359..9c965ea 100644 --- a/lib/algorithms/utils.ts +++ b/lib/algorithms/utils.ts @@ -1,4 +1,9 @@ -import { AlgorithmVisualization, SearchStep, SortingStep } from "../types"; +import { + AlgorithmVisualization, + GraphStep, + SearchStep, + SortingStep, +} from "../types"; import { availableAlgorithms } from "./metadata"; /** @@ -7,7 +12,7 @@ import { availableAlgorithms } from "./metadata"; */ export function createVisualization( key: keyof typeof availableAlgorithms, - steps: SortingStep[] | SearchStep[], + steps: SortingStep[] | SearchStep[] | GraphStep[], details: { timeComplexity: string; spaceComplexity: string; diff --git a/lib/types.ts b/lib/types.ts index e92568c..cf3738b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -13,8 +13,16 @@ export interface SearchStep { visited: number[]; // Indices already visited } +export interface GraphStep { + adjacencyList: number[][]; // Adjacency list representation + current: number; // Current vertex index + visited: number[]; // Visited vertices + stack: number[]; // Stack for DFS traversal + path: number[]; // Path taken so far +} + export interface AlgorithmVisualization { - steps: SortingStep[] | SearchStep[]; + steps: SortingStep[] | SearchStep[] | GraphStep[]; name: string; description: string; timeComplexity: string;