diff --git a/package-lock.json b/package-lock.json
index 210d3cd..c952e7e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -74,6 +74,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -2453,6 +2454,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true,
+ "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -2462,6 +2464,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"devOptional": true,
+ "peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -2497,6 +2500,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2649,6 +2653,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -2812,7 +2817,8 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "peer": true
},
"node_modules/d3-color": {
"version": "3.1.0",
@@ -2871,6 +2877,7 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
+ "peer": true,
"engines": {
"node": ">=12"
}
@@ -3088,6 +3095,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"dev": true,
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -4054,6 +4062,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -4079,6 +4088,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -4116,6 +4126,7 @@
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4124,6 +4135,7 @@
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -4152,6 +4164,7 @@
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@@ -4302,7 +4315,8 @@
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
- "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "peer": true
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@@ -4661,6 +4675,7 @@
"version": "7.1.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
diff --git a/src/algorithms/graph/prim.js b/src/algorithms/graph/prim.js
new file mode 100644
index 0000000..4e67546
--- /dev/null
+++ b/src/algorithms/graph/prim.js
@@ -0,0 +1,96 @@
+// src/algorithms/graph/prim.js
+
+// Build adjacency list from list of edges
+function buildAdjacency(edges, n) {
+ const adj = Array.from({ length: n }, () => []);
+ for (const e of edges) {
+ const u = e.from - 1;
+ const v = e.to - 1;
+ const w = e.weight;
+
+ adj[u].push({ u, v, w });
+ adj[v].push({ u: v, v: u, w }); // undirected graph
+ }
+ return adj;
+}
+
+/**
+ * Prim's Algorithm – Step Generator
+ * Yields one step at a time:
+ * type: "consider" | "add" | "skip" | "done"
+ * edge: {from, to, weight}
+ * visited: boolean[]
+ * frontier: edge[]
+ * mst: collected MST edges
+ */
+export function* primSteps(edges, nodeCount, startNode = 1) {
+ if (nodeCount === 0) {
+ yield { type: "done", mst: [], visited: [], frontier: [] };
+ return;
+ }
+
+ const adj = buildAdjacency(edges, nodeCount);
+ const visited = Array(nodeCount).fill(false);
+ const mst = [];
+ const frontier = [];
+
+ const pushEdges = (u) => {
+ for (const { v, w } of adj[u]) {
+ if (!visited[v]) {
+ frontier.push({ from: u + 1, to: v + 1, weight: w });
+ }
+ }
+ };
+
+ const startIdx = Math.max(1, Math.min(startNode, nodeCount)) - 1;
+ visited[startIdx] = true;
+ pushEdges(startIdx);
+
+ while (mst.length < nodeCount - 1 && frontier.length > 0) {
+ frontier.sort((a, b) => a.weight - b.weight);
+ const edge = frontier.shift();
+
+ yield {
+ type: "consider",
+ edge,
+ visited: [...visited],
+ frontier: [...frontier],
+ mst: [...mst],
+ };
+
+ const u = edge.from - 1;
+ const v = edge.to - 1;
+
+ if (visited[u] && visited[v]) {
+ yield {
+ type: "skip",
+ edge,
+ visited: [...visited],
+ frontier: [...frontier],
+ mst: [...mst],
+ };
+ continue;
+ }
+
+ const nextNode = visited[u] ? v : u;
+ visited[nextNode] = true;
+
+ mst.push(edge);
+ pushEdges(nextNode);
+
+ yield {
+ type: "add",
+ edge,
+ visited: [...visited],
+ frontier: [...frontier],
+ mst: [...mst],
+ };
+ }
+
+ yield {
+ type: "done",
+ mst: [...mst],
+ visited: [...visited],
+ frontier: [...frontier],
+ };
+}
diff --git a/src/components/graph/PrimsVisualizer.jsx b/src/components/graph/PrimsVisualizer.jsx
new file mode 100644
index 0000000..3658a16
--- /dev/null
+++ b/src/components/graph/PrimsVisualizer.jsx
@@ -0,0 +1,337 @@
+// src/components/graph/PrimsVisualizer.jsx
+
+import React, { useRef, useState } from "react";
+import { primSteps } from "../../algorithms/graph/prim";
+
+export default function PrimsVisualizer() {
+ const svgRef = useRef(null);
+
+ const [nodes, setNodes] = useState([]);
+ const [edges, setEdges] = useState([]);
+ const [selected, setSelected] = useState({ from: null, to: null });
+ const [running, setRunning] = useState(false);
+
+ const [startNode, setStartNode] = useState(1);
+ const [status, setStatus] = useState("Idle");
+ const [stepIndex, setStepIndex] = useState(0);
+
+ const [visitedSnap, setVisitedSnap] = useState([]);
+ const [frontierSnap, setFrontierSnap] = useState([]);
+
+ // Node creation
+ const addNode = (e) => {
+ if (running) return;
+
+ const rect = svgRef.current.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ const id = nodes.length + 1;
+
+ setNodes([...nodes, { id, x, y, label: `N${id}` }]);
+ };
+
+ // Edge creation
+ const handleNodeClick = (id) => {
+ if (running) return;
+
+ if (!selected.from) {
+ setSelected({ from: id, to: null });
+ return;
+ }
+
+ if (selected.from === id) return;
+
+ const w = parseInt(prompt("Enter edge weight:"), 10);
+ if (!isNaN(w)) {
+ setEdges((prev) => [
+ ...prev,
+ {
+ from: selected.from,
+ to: id,
+ weight: w,
+ colorClass: "stroke-gray-500",
+ },
+ ]);
+ }
+
+ setSelected({ from: null, to: null });
+ };
+
+ // Start visualization
+ const start = async () => {
+ if (running || nodes.length < 2 || edges.length === 0) {
+ alert("Create nodes and edges first.");
+ return;
+ }
+
+ setRunning(true);
+ setStatus("Running...");
+ setStepIndex(0);
+
+ const delay = 900;
+ const steps = primSteps(edges, nodes.length, startNode);
+
+ for (const step of steps) {
+ const { type, edge, visited = [], frontier = [], mst = [] } = step;
+
+ setVisitedSnap(visited);
+ setFrontierSnap(frontier);
+
+ if (edge) {
+ setEdges((prev) =>
+ prev.map((e) =>
+ (e.from === edge.from && e.to === edge.to) ||
+ (e.from === edge.to && e.to === edge.from)
+ ? {
+ ...e,
+ colorClass:
+ type === "consider"
+ ? "stroke-blue-400"
+ : type === "add"
+ ? "stroke-green-400"
+ : type === "skip"
+ ? "stroke-red-500"
+ : "stroke-gray-500",
+ }
+ : e
+ )
+ );
+ }
+
+ setStatus(
+ type === "consider"
+ ? `Considering edge (${edge.from}, ${edge.to})`
+ : type === "add"
+ ? `Added edge (${edge.from}, ${edge.to})`
+ : type === "skip"
+ ? `Skipped edge (${edge.from}, ${edge.to})`
+ : `Completed! MST size = ${mst.length}`
+ );
+
+ await new Promise((r) => setTimeout(r, delay));
+ setStepIndex((i) => i + 1);
+ }
+
+ setRunning(false);
+ };
+
+ const reset = () => {
+ setNodes([]);
+ setEdges([]);
+ setSelected({ from: null, to: null });
+
+ setVisitedSnap([]);
+ setFrontierSnap([]);
+
+ setStartNode(1);
+ setStatus("Idle");
+ setStepIndex(0);
+
+ setRunning(false);
+ };
+
+ // Render nodes
+ const renderNodes = () =>
+ nodes.map((n, idx) => {
+ const isVisited = visitedSnap[idx] ?? false;
+ const isStart = n.id === startNode;
+
+ return (
+
+ Status:{" "} + {status} +
++ Step:{" "} + {stepIndex} +
+Empty
+ )} +