diff --git a/README.md b/README.md
index 150e3a4..150c53f 100644
--- a/README.md
+++ b/README.md
@@ -15,11 +15,14 @@ So far there are these segments
- Graph Traversal (BFS / DFS)
- Shortest Path
- Minimum Spanning Tree
+- Connectivity
+- Network Flow
- Prime Numbers
- Sorting Algorithms
- N Queen
- Convex Hull
- Binary Search Game
+- Binary Search Tree
- Recursion Tree
- Turing Machine
- Game of Life
@@ -37,6 +40,8 @@ I have implemented a total of `30+ algorithms` so far. And will try to add more
- Recursive Maze Creation
- Data Structures
- Linked List (insert, delete, search, reverse — singly & doubly)
+- Trees
+ - Binary Search Tree (insert, delete, search with animated re-layout)
- Graph Traversal
- BFS
- DFS
@@ -46,6 +51,14 @@ I have implemented a total of `30+ algorithms` so far. And will try to add more
- Minimum Spanning Tree
- Kruskal
- Prim
+- Connectivity
+ - Connected Components
+ - Strongly Connected Components (Tarjan)
+ - Weakly Connected Components
+- Network Flow
+ - Edmonds-Karp
+ - Ford-Fulkerson
+ - Min Cut
- Sorting
- Bubble sort
- Selection sort
@@ -83,6 +96,8 @@ I am not sure if anyone would like to contribute to this project or not. But any
- Jun 2026: Added Linked List visualizer (singly & doubly) with staged insert/delete animations
- Jun 2026: Added interactive Graph Traversal (BFS / DFS) built on React Flow
- Jun 2026: Added Shortest Path (Dijkstra / Bellman-Ford) and Minimum Spanning Tree (Kruskal / Prim) on a shared, reusable graph workspace
+- Jun 2026: Added Connectivity (components / SCC / WCC) and Network Flow (Edmonds-Karp / Ford-Fulkerson, max flow / min cut)
+- Jun 2026: Added a reusable SVG tree component with a Binary Search Tree visualizer, and migrated the Recursion Tree onto it
### Acknowledgement
diff --git a/public/images/bst.png b/public/images/bst.png
new file mode 100644
index 0000000..d7d90bb
Binary files /dev/null and b/public/images/bst.png differ
diff --git a/public/images/connectivity.png b/public/images/connectivity.png
new file mode 100644
index 0000000..37ed0bd
Binary files /dev/null and b/public/images/connectivity.png differ
diff --git a/public/images/network-flow.png b/public/images/network-flow.png
new file mode 100644
index 0000000..18f2618
Binary files /dev/null and b/public/images/network-flow.png differ
diff --git a/src/app/about/page.jsx b/src/app/about/page.jsx
index 8d6df05..7d54002 100644
--- a/src/app/about/page.jsx
+++ b/src/app/about/page.jsx
@@ -15,12 +15,20 @@ const algorithms = [
"Linked List — insert, delete, search, reverse (singly & doubly)",
],
},
+ {
+ category: "Trees",
+ items: [
+ "Binary Search Tree — insert, delete, search with animated re-layout",
+ ],
+ },
{
category: "Interactive Graphs",
items: [
"Graph Traversal — BFS / DFS",
"Shortest Path — Dijkstra & Bellman-Ford (with negative-cycle detection)",
"Minimum Spanning Tree — Kruskal & Prim",
+ "Connectivity — Connected Components, Strongly & Weakly Connected",
+ "Network Flow — max flow / min cut (Edmonds-Karp & Ford-Fulkerson)",
],
},
{
diff --git a/src/app/bst/page.jsx b/src/app/bst/page.jsx
new file mode 100644
index 0000000..4ee4cfa
--- /dev/null
+++ b/src/app/bst/page.jsx
@@ -0,0 +1,45 @@
+"use client";
+
+import { useState } from 'react';
+import Navbar from '@/components/navbar';
+import { fromValues, insertActions, deleteActions, searchActions } from '@/lib/algorithms/bst';
+import { binaryLayout } from '@/components/tree/layout';
+import { useTreeEditor } from '@/components/tree/use-tree-editor';
+import TreeCanvas from '@/components/tree/tree-canvas';
+import TreeMenu from '@/components/tree/tree-menu';
+
+export default function Bst() {
+ const [initialTree] = useState(() => fromValues([50, 30, 70, 20, 40, 60, 80]));
+ const g = useTreeEditor({ initialTree });
+
+ return (
+
+
+
+
g.run(insertActions(g.getContext().tree, v))}
+ onDelete={(v) => g.run(deleteActions(g.getContext().tree, v))}
+ onSearch={(v) => g.run(searchActions(g.getContext().tree, v))}
+ onClear={g.clear}
+ onSpeedChange={g.setSpeed}
+ />
+
+ {g.status && (
+
+ {g.status}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/app/components/algorithm-cards.jsx b/src/app/components/algorithm-cards.jsx
index 26d35c1..c28c7f7 100644
--- a/src/app/components/algorithm-cards.jsx
+++ b/src/app/components/algorithm-cards.jsx
@@ -25,6 +25,21 @@ const algorithms = [
title: 'Minimum Spanning Tree',
description: "Build a weighted graph and watch Kruskal and Prim grow the minimum spanning tree",
image: '/AlgorithmVisualizer/images/mst.png?height=200&width=300'
+ },{
+ id: 'connectivity',
+ title: 'Connectivity',
+ description: "Build a graph and color its connected components and strongly connected components",
+ image: '/AlgorithmVisualizer/images/connectivity.png?height=200&width=300'
+ },{
+ id: 'network-flow',
+ title: 'Network Flow',
+ description: "Compute max flow / min cut with Edmonds-Karp and Ford-Fulkerson on a capacity network",
+ image: '/AlgorithmVisualizer/images/network-flow.png?height=200&width=300'
+ },{
+ id: 'bst',
+ title: 'Binary Search Tree',
+ description: "Insert, delete, and search on a BST with animated tree restructuring",
+ image: '/AlgorithmVisualizer/images/bst.png?height=200&width=300'
},
{
id: 'recursion-tree',
diff --git a/src/app/connectivity/page.jsx b/src/app/connectivity/page.jsx
new file mode 100644
index 0000000..c38ae35
--- /dev/null
+++ b/src/app/connectivity/page.jsx
@@ -0,0 +1,61 @@
+"use client";
+
+import { useState } from 'react';
+import { ReactFlowProvider } from '@xyflow/react';
+import Navbar from '@/components/navbar';
+import { adjacency } from '@/lib/algorithms/graph';
+import { CONNECTIVITY_PRESETS, connectedComponentsActions, sccActions } from '@/lib/algorithms/connectivity';
+import { useGraphEditor } from '@/components/graph/use-graph-editor';
+import GraphCanvas from '@/components/graph/graph-canvas';
+import GraphMenu from '@/components/graph/graph-menu';
+
+function ConnectivityInner() {
+ const g = useGraphEditor({ initialPreset: CONNECTIVITY_PRESETS[0] });
+ const [algo, setAlgo] = useState(0);
+
+ // Undirected: just Connected Components. Directed: SCC (strongly) + WCC (weakly).
+ const algorithms = g.directed
+ ? ['Strongly Connected (SCC)', 'Weakly Connected (WCC)']
+ : ['Connected Components'];
+
+ const onDirectedChange = (val) => { g.setDirected(val); setAlgo(0); };
+
+ const onVisualize = () => {
+ const { nodes, edges, directed } = g.getContext();
+ const ids = nodes.map((n) => n.id);
+ // SCC only when directed + first option; everything else is the
+ // (undirected) connected-components computation — i.e. CC or WCC.
+ g.run(directed && algo === 0
+ ? sccActions(adjacency(edges, true), ids)
+ : connectedComponentsActions(adjacency(edges, false), ids));
+ };
+
+ return (
+
+
+
+ g.loadPreset(CONNECTIVITY_PRESETS[i])}
+ onSpeedChange={g.setSpeed}
+ onVisualize={onVisualize}
+ onClear={g.clear}
+ />
+
+
+
+ );
+}
+
+export default function Connectivity() {
+ return (
+
+
+
+ );
+}
diff --git a/src/app/network-flow/page.jsx b/src/app/network-flow/page.jsx
new file mode 100644
index 0000000..16e0b31
--- /dev/null
+++ b/src/app/network-flow/page.jsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { useState } from 'react';
+import { ReactFlowProvider } from '@xyflow/react';
+import Navbar from '@/components/navbar';
+import { FLOW_PRESETS, maxFlowActions } from '@/lib/algorithms/networkFlow';
+import { useGraphEditor } from '@/components/graph/use-graph-editor';
+import GraphCanvas from '@/components/graph/graph-canvas';
+import GraphMenu from '@/components/graph/graph-menu';
+
+function NetworkFlowInner() {
+ const g = useGraphEditor({ weighted: true, initialDirected: true, initialPreset: FLOW_PRESETS[0] });
+ const [algo, setAlgo] = useState(0); // 0 = Edmonds-Karp, 1 = Ford-Fulkerson
+
+ const onVisualize = () => {
+ const { edges, startId, finishId } = g.getContext();
+ g.run(maxFlowActions(edges, startId, finishId, algo === 1 ? 'ff' : 'ek'));
+ };
+
+ return (
+
+
+
+ g.loadPreset(FLOW_PRESETS[i])}
+ onSpeedChange={g.setSpeed}
+ onVisualize={onVisualize}
+ onClear={g.clear}
+ />
+
+
+
+ );
+}
+
+export default function NetworkFlow() {
+ return (
+
+
+
+ );
+}
diff --git a/src/app/recursion-tree/Tree.js b/src/app/recursion-tree/Tree.js
deleted file mode 100644
index 84edd46..0000000
--- a/src/app/recursion-tree/Tree.js
+++ /dev/null
@@ -1,235 +0,0 @@
-// draw tree class functions start :/
-
-
-export class Tree{
- constructor(node=0,children=[],label="") {
- this.id = 0;
- this.node = node;
- this.label = label;
- this.width = node.length;
- this.children = children;
- }
-}
-
-export class DrawTree{
- constructor(tree,parent=undefined,depth=0,number=1) {
- this.x =-1;
- this.y = depth;
- this.tree = tree;
- this.children = [];
- for( let i=0;i{
- if( this.thread!==undefined ) return this.thread;
- if( this.children.length!==0 ) return this.children[0];
- return undefined;
- return this.thread || this.children.length && this.children[0];
- }
- right = ()=>{
- if( this.thread ) return this.thread;
- if( this.children.length ) return this.children[this.children.length-1];
- return undefined;
- return this.thread || this.children.length && this.children[-1];
- }
- lbrother = ()=>{
- let n = undefined;
- if( this.parent ){
- // for(let node in this.parent.children)
- for(let i=0;i{
- if( !this._lmost_sibling && this.parent && this!==this.parent.children[0] ){
- this._lmost_sibling = this.parent.children[0];
- }
- return this._lmost_sibling;
- }
-
-
-}
-
-
-
-
-export function buchheim(tree) {
- let dt = firstwalk(new DrawTree(tree))
- let min = second_walk(dt)
- if (min < 0) {
- third_walk(dt, -min);
- }
- return dt
-}
-
-function third_walk(tree, n) {
- tree.x += n;
- //for (let c in tree.children)
- for(let i=0;i 0) {
- move_subtree(ancestor(vil, v, default_ancestor), v, shift);
- sir = sir + shift;
- sor = sor + shift;
- }
- sil += vil.mod;
- sir += vir.mod;
- sol += vol.mod;
- sor += vor.mod;
- }
- if (vil.right() && !vor.right()) {
- vor.thread = vil.right();
- vor.mod += sil - sor;
- } else {
- if (vir.left() && !vol.left()) {
- vol.thread = vir.left();
- vol.mod += sir - sol;
- }
- default_ancestor = v
- }
- }
- return default_ancestor
-}
-
-function move_subtree(wl, wr, shift) {
- let subtrees = wr.number - wl.number;
- // console.log(wl.tree, "is conflicted with", wr.tree, 'moving', subtrees, 'shift', shift);
- // print wl, wr, wr.number, wl.number, shift, subtrees, shift / subtrees
- wr.change -= shift / subtrees;
- wr.shift += shift;
- wl.change += shift / subtrees;
- wr.x += shift;
- wr.mod += shift;
-}
-
-function execute_shifts(v) {
- let shift, change;
- shift = change = 0;
- // for (let w in v.children[:: - 1])
- for(let i=v.children.length-1;i>=0;i--){
- let w = v.children[i];
- //console.log("shift:", w.tree.node, shift, w.change);
- w.x += shift;
- w.mod += shift;
- change += w.change;
- shift += w.shift + change;
- }
-}
-
-function ancestor(vil, v, default_ancestor) {
-
- if (vil.ancestor in v.parent.children){
-
- return vil.ancestor;
- }
- else
- return default_ancestor;
-}
-
-function second_walk(v, m = 0, depth = 0, min = undefined) {
- v.x += m;
- v.y = depth;
-
- if (min === undefined || v.x < min)
- min = v.x;
-
- // for (let w in v.children)
- for(let i=0;i
-
-
- );
-}
diff --git a/src/app/recursion-tree/edge.jsx b/src/app/recursion-tree/edge.jsx
deleted file mode 100644
index 2d9d026..0000000
--- a/src/app/recursion-tree/edge.jsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { useEffect, useRef } from 'react';
-
-export default function Edge({ id, pos }) {
- const prevX1 = useRef(pos.x1);
-
- useEffect(() => {
- document.getElementById('vbanim1' + id)?.beginElement();
- document.getElementById('vbanim2' + id)?.beginElement();
- }, []);
-
- useEffect(() => {
- if (prevX1.current !== pos.x1) {
- prevX1.current = pos.x1;
- document.getElementById('vbanim1' + id)?.beginElement();
- document.getElementById('vbanim2' + id)?.beginElement();
- }
- }, [pos.x1]);
-
- const getEndX = () => {
- let { x1, y1, x2, y2 } = pos;
- let l = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
- let r = 6.5;
- return (x2 * (l - r) + x1 * r) / l;
- };
-
- const getEndY = () => {
- let { x1, y1, x2, y2 } = pos;
- let l = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
- let r = 6.5;
- return (y2 * (l - r) + y1 * r) / l;
- };
-
- return (
-
-
-
-
-
-
- );
-}
diff --git a/src/app/recursion-tree/fib.jsx b/src/app/recursion-tree/fib.jsx
index 8f10281..6fa56bf 100644
--- a/src/app/recursion-tree/fib.jsx
+++ b/src/app/recursion-tree/fib.jsx
@@ -1,79 +1,99 @@
-import {Tree,buchheim} from './Tree';
-export function getTree(n,algo=0,r=0){
- if(algo === 0)
- return buchheim( fib(n) );
- else if( algo === 1 )
- return buchheim(NcR(n,r));
- else if( algo === 2 )
- return buchheim(derangement(n));
- else if( algo === 3 )
- return buchheim(bigmod(n,r));
- else if( algo === 4 )
- return buchheim(stirling2(n,r));
-}
+// Recursion-tree builders + action planner for the tree component.
+//
+// Each builder returns a logical tree of nodes shaped for the tree component:
+// { id, value, ret, children: [] }
+// where `value` is the call label shown in the node and `ret` is the return
+// value, revealed as the node's secondary label on the way back up.
+
+let counter = 0;
+const nid = () => 'r' + counter++;
-function fib(n){
- let tree = new Tree(n,[],n+"");
- if( n <2 ) return tree;
- tree.children.push( fib(n-1) );
- tree.children.push( fib(n-2) );
- tree.node = tree.children[0].node+tree.children[1].node;
- return tree;
+export function getTree(n, algo = 0, r = 0) {
+ counter = 0;
+ if (algo === 0) return fib(n);
+ if (algo === 1) return NcR(n, r);
+ if (algo === 2) return derangement(n);
+ if (algo === 3) return bigmod(n, r);
+ return stirling2(n, r);
}
-function NcR(n,r){
- if (r > n)
- return new Tree(-1,[],"("+n+","+r+")");
+// today's DFS reveal, expressed as an action list: lay out the full tree once
+// (hidden), then reveal each node as it's entered (current → waiting → done)
+// and set its return value as the secondary label on the way back up.
+export function recursionActions(root) {
+ const actions = [{ type: 'setTree', tree: root, hidden: true }];
+ const visit = (node) => {
+ actions.push({ type: 'markNode', id: node.id, state: 'current' });
+ for (const child of node.children) {
+ actions.push({ type: 'markNode', id: node.id, state: 'normal' }); // waiting on a child
+ visit(child);
+ actions.push({ type: 'markNode', id: node.id, state: 'current' }); // control returned
+ }
+ actions.push({ type: 'setLabel', id: node.id, secondary: '= ' + node.ret });
+ actions.push({ type: 'markNode', id: node.id, state: 'visited' });
+ };
+ visit(root);
+ return actions;
+}
- if (n === r)
- return new Tree(1,[],"("+n+","+r+")");
+function fib(n) {
+ const node = { id: nid(), value: '' + n, ret: n, children: [] };
+ if (n < 2) return node;
+ node.children.push(fib(n - 1));
+ node.children.push(fib(n - 2));
+ node.ret = node.children[0].ret + node.children[1].ret;
+ return node;
+}
- if (r === 0)
- return new Tree(1,[],"("+n+","+r+")");;
+function NcR(n, r) {
+ const label = '(' + n + ',' + r + ')';
+ if (r > n) return { id: nid(), value: label, ret: -1, children: [] };
+ if (n === r) return { id: nid(), value: label, ret: 1, children: [] };
+ if (r === 0) return { id: nid(), value: label, ret: 1, children: [] };
// nCr(n, r) = nCr(n - 1, r - 1) + nCr(n - 1, r)
- let tree = new Tree(0,[],"("+n+","+r+")");
- tree.children.push( NcR(n-1,r-1) );
- tree.children.push( NcR(n-1,r) );
- tree.node = tree.children[0].node+tree.children[1].node;
- return tree;
+ const node = { id: nid(), value: label, ret: 0, children: [] };
+ node.children.push(NcR(n - 1, r - 1));
+ node.children.push(NcR(n - 1, r));
+ node.ret = node.children[0].ret + node.children[1].ret;
+ return node;
}
-function derangement(n){
- if( n == 0 ) return new Tree(1,[],n+"");
- if( n == 1 ) return new Tree(0,[],n+"");
- let tree = new Tree(0,[],n+"");
- tree.children.push( derangement(n-1) );
- tree.children.push( derangement(n-2) );
- tree.node = (n-1)*(tree.children[0].node+tree.children[1].node);
- return tree;
+function derangement(n) {
+ const label = '' + n;
+ if (n === 0) return { id: nid(), value: label, ret: 1, children: [] };
+ if (n === 1) return { id: nid(), value: label, ret: 0, children: [] };
+ const node = { id: nid(), value: label, ret: 0, children: [] };
+ node.children.push(derangement(n - 1));
+ node.children.push(derangement(n - 2));
+ node.ret = (n - 1) * (node.children[0].ret + node.children[1].ret);
+ return node;
}
-function bigmod(n,r){
- if( r === 0 ) return new Tree(1,[],"("+n+","+r+")");
- if( r === 1 ) return new Tree(n,[],"("+n+","+r+")");
- let tree = new Tree(1,[],"("+n+","+r+")");
- if( r%2 === 1 ){
- tree.children.push( bigmod(n,(r-1)/2 ) );
- tree.children.push( bigmod(n,(r-1)/2) );
- tree.children.push( bigmod(n,1) );
- }else{
- tree.children.push( bigmod(n,r/2 ) );
- tree.children.push( bigmod(n,r/2) );
+function bigmod(n, r) {
+ const label = '(' + n + ',' + r + ')';
+ if (r === 0) return { id: nid(), value: label, ret: 1, children: [] };
+ if (r === 1) return { id: nid(), value: label, ret: n, children: [] };
+ const node = { id: nid(), value: label, ret: 1, children: [] };
+ if (r % 2 === 1) {
+ node.children.push(bigmod(n, (r - 1) / 2));
+ node.children.push(bigmod(n, (r - 1) / 2));
+ node.children.push(bigmod(n, 1));
+ } else {
+ node.children.push(bigmod(n, r / 2));
+ node.children.push(bigmod(n, r / 2));
}
- for(let i=0;i {
- if (isRunningRef.current) return;
- setIsRunning(true);
- isRunningRef.current = true;
-
- let tree = getTree(n, algo, r);
- setEdges([]);
- setVertices([]);
- setOffset(tree.x);
- verticesRef.current = [];
- edgesRef.current = [];
- await recur(tree, undefined);
- setIsRunning(false);
- isRunningRef.current = false;
- };
-
- const recur = async (node, parent) => {
- let verts = verticesRef.current;
- let currentIdx = verts.length;
-
- let newVertex;
- if (parent !== undefined) {
- newVertex = node.children.length
- ? { label: node.tree.label, val: 0, x: node.x, y: node.y, px: parent.x, py: parent.y, completed: false }
- : { label: node.tree.label, val: node.tree.node, x: node.x, y: node.y, px: parent.x, py: parent.y, completed: false };
-
- verts = [...verts, newVertex];
- verticesRef.current = verts;
- setVertices([...verts]);
- setCurrent(currentIdx);
-
- let newEdge = { x1: parent.x, y1: parent.y, x2: node.x, y2: node.y };
- edgesRef.current = [...edgesRef.current, newEdge];
- setEdges([...edgesRef.current]);
- } else {
- newVertex = node.children.length
- ? { label: node.tree.label, val: 0, x: node.x, y: node.y, px: node.x, py: node.y, completed: false }
- : { label: node.tree.label, val: node.tree.node, x: node.x, y: node.y, px: node.x, py: node.y, completed: false };
-
- verts = [...verts, newVertex];
- verticesRef.current = verts;
- setVertices([...verts]);
- setCurrent(currentIdx);
- }
- await sleep(500);
-
- for (let i = 0; i < node.children.length; i++) {
- await recur(node.children[i], node);
- setCurrent(currentIdx);
- await sleep(500);
- }
-
- let updatedVerts = [...verticesRef.current];
- updatedVerts[currentIdx] = { ...updatedVerts[currentIdx], val: node.tree.node, completed: true };
- verticesRef.current = updatedVerts;
- setVertices([...updatedVerts]);
- };
+ const onStart = () => g.run(recursionActions(getTree(n, algo, r)));
return (
@@ -86,24 +24,24 @@ export default function Graph() {
setN={setN}
setR={setR}
setAlgo={setAlgo}
- onStart={addNumber}
- disabled={isRunning}
+ onStart={onStart}
+ disabled={g.isRunning}
/>
-
-
-
-
+
+ {g.status && (
+
+ {g.status}
+
+ )}
+
);
}
-
-function sleep(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
-}
diff --git a/src/app/recursion-tree/vertex.jsx b/src/app/recursion-tree/vertex.jsx
deleted file mode 100644
index 39081aa..0000000
--- a/src/app/recursion-tree/vertex.jsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { useEffect } from 'react';
-
-export default function Vertex({ id, pos, current, completed, label, ret }) {
- useEffect(() => {
- if (id === 0) return;
- document.getElementById('cxanim' + id)?.beginElement();
- document.getElementById('cyanim' + id)?.beginElement();
- document.getElementById('tanim' + id)?.beginElement();
- }, []);
-
- const fillColor = current ? '#f59e0b' : completed ? '#334155' : '#0d9488';
- const strokeColor = current ? '#d97706' : completed ? '#475569' : '#0f766e';
- const textColor = '#f8fafc';
-
- return (
-
-
-
-
-
-
-
- N:{label}
- R:{ret}
-
-
- );
-}
diff --git a/src/components/graph/floating-edge.jsx b/src/components/graph/floating-edge.jsx
index 40e21d2..ca3b208 100644
--- a/src/components/graph/floating-edge.jsx
+++ b/src/components/graph/floating-edge.jsx
@@ -1,5 +1,5 @@
import { createContext, useContext, useState } from 'react';
-import { BaseEdge, EdgeLabelRenderer, getStraightPath, useInternalNode } from '@xyflow/react';
+import { BaseEdge, EdgeLabelRenderer, getStraightPath, useInternalNode, useStore } from '@xyflow/react';
// Edge that connects node centers (no handles), trimmed to each node's border.
// Stroke color encodes the edge state; during traversal a big arrow is drawn at
@@ -31,13 +31,28 @@ export default function FloatingEdge({ id, source, target, markerEnd, data, sele
const setWeight = useContext(EdgeWeightContext);
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState('');
+ // true when an opposite edge (target -> source) also exists
+ const hasOpposite = useStore((s) => s.edges.some((e) => e.source === target && e.target === source));
const sourceNode = useInternalNode(source);
const targetNode = useInternalNode(target);
if (!sourceNode || !targetNode) return null;
- const sc = center(sourceNode);
- const tc = center(targetNode);
+ let sc = center(sourceNode);
+ let tc = center(targetNode);
+
+ // For a bidirectional pair, shift each edge to its own side (parallel) so the
+ // two arrows + labels don't overlap. Each edge's stable perpendicular points
+ // opposite to its twin's, so they separate cleanly.
+ if (hasOpposite) {
+ const dxx = tc.x - sc.x;
+ const dyy = tc.y - sc.y;
+ const dl = Math.hypot(dxx, dyy) || 1;
+ const ox = (-dyy / dl) * 6;
+ const oy = (dxx / dl) * 6;
+ sc = { x: sc.x + ox, y: sc.y + oy };
+ tc = { x: tc.x + ox, y: tc.y + oy };
+ }
const state = data?.state || 'normal';
const travelTo = data?.travelTo;
@@ -138,7 +153,8 @@ export default function FloatingEdge({ id, source, target, markerEnd, data, sele
style={{ width: 36, border: 'none', outline: 'none', font: 'inherit', textAlign: 'center' }}
/>
) : (
- data.weight
+ // show "flow / capacity" during a flow run, else just the capacity
+ data.flow != null ? `${data.flow} / ${data.weight}` : data.weight
)}
diff --git a/src/components/graph/graph-menu.jsx b/src/components/graph/graph-menu.jsx
index 1daaf43..7a50a92 100644
--- a/src/components/graph/graph-menu.jsx
+++ b/src/components/graph/graph-menu.jsx
@@ -49,6 +49,9 @@ export default function GraphMenu({
)}
1100 - s * 10;
-// preset -> { nodes, edges, startId } with the first node marked as start
+// preset -> { nodes, edges, startId, finishId }. The start/finish roles come
+// from preset.source / preset.sink when given, else start = first node.
function seed(preset) {
const { nodes, edges } = toFlow(preset);
- if (nodes[0]) nodes[0] = { ...nodes[0], data: { ...nodes[0].data, role: 'start' } };
- return { nodes, edges, startId: nodes[0]?.id ?? null };
+ const startId = preset.source ?? nodes[0]?.id ?? null;
+ const finishId = preset.sink ?? null;
+ const withRoles = nodes.map((n) => {
+ if (n.id === startId) return { ...n, data: { ...n.data, role: 'start' } };
+ if (n.id === finishId) return { ...n, data: { ...n.data, role: 'finish' } };
+ return n;
+ });
+ return { nodes: withRoles, edges, startId, finishId };
}
-export function useGraphEditor({ weighted = false, initialPreset }) {
- const [initial] = useState(() => seed(initialPreset));
+export function useGraphEditor({ weighted = false, initialDirected = false, initialPreset }) {
+ const [initial] = useState(() => {
+ const s = seed(initialPreset);
+ if (initialDirected) s.edges = s.edges.map((e) => ({ ...e, markerEnd: ARROW }));
+ return s;
+ });
const [nodes, setNodes, onNodesChange] = useNodesState(initial.nodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initial.edges);
- const [directed, setDirectedState] = useState(false);
+ const [directed, setDirectedState] = useState(initialDirected);
const [mode, setMode] = useState('idle');
const [status, setStatus] = useState('');
const [isRunning, setIsRunning] = useState(false);
@@ -36,9 +47,9 @@ export function useGraphEditor({ weighted = false, initialPreset }) {
const speedRef = useRef(toDelay(50));
const modeRef = useRef('idle');
const pendingEdgeRef = useRef(null);
- const directedRef = useRef(false);
+ const directedRef = useRef(initialDirected);
const startIdRef = useRef(initial.startId);
- const finishIdRef = useRef(null);
+ const finishIdRef = useRef(initial.finishId);
const labelRef = useRef(0);
useEffect(() => { nodesRef.current = nodes; }, [nodes]);
@@ -52,9 +63,13 @@ export function useGraphEditor({ weighted = false, initialPreset }) {
setEdges((es) => es.map((e) => (e.id === id ? { ...e, data: { ...e.data, state, travelTo: to ?? null } } : e)));
const setDist = (id, dist) =>
setNodes((ns) => ns.map((n) => (n.id === id ? { ...n, data: { ...n.data, dist } } : n)));
+ const colorNode = (id, color) =>
+ setNodes((ns) => ns.map((n) => (n.id === id ? { ...n, data: { ...n.data, color } } : n)));
+ const setFlow = (id, flow) =>
+ setEdges((es) => es.map((e) => (e.id === id ? { ...e, data: { ...e.data, flow } } : e)));
const clearMarks = () => {
- setNodes((ns) => ns.map((n) => ({ ...n, data: { ...n.data, state: 'normal', dist: undefined } })));
- setEdges((es) => es.map((e) => ({ ...e, data: { ...e.data, state: 'normal', travelTo: null } })));
+ setNodes((ns) => ns.map((n) => ({ ...n, data: { ...n.data, state: 'normal', dist: undefined, color: undefined } })));
+ setEdges((es) => es.map((e) => ({ ...e, data: { ...e.data, state: 'normal', travelTo: null, flow: undefined } })));
setStatus('');
};
@@ -62,6 +77,8 @@ export function useGraphEditor({ weighted = false, initialPreset }) {
if (action.type === 'markNode') markNode(action.id, action.state);
else if (action.type === 'markEdge') markEdge(action.id, action.state, action.to);
else if (action.type === 'setDist') setDist(action.id, action.dist);
+ else if (action.type === 'colorNode') colorNode(action.id, action.color);
+ else if (action.type === 'setFlow') setFlow(action.id, action.flow);
else if (action.type === 'status') setStatus(action.text);
else if (action.type === 'clear') clearMarks();
};
@@ -215,14 +232,28 @@ export function useGraphEditor({ weighted = false, initialPreset }) {
const setDirected = (val) => {
directedRef.current = val;
setDirectedState(val);
- setEdges((es) => es.map((e) => ({ ...e, markerEnd: val ? ARROW : undefined })));
+ setEdges((es) => {
+ let next = es;
+ // an undirected graph shouldn't keep both A->B and B->A: collapse
+ // reverse-duplicate pairs (keep the first of each unordered pair)
+ if (!val) {
+ const seen = new Set();
+ next = es.filter((e) => {
+ const key = e.source < e.target ? `${e.source}~${e.target}` : `${e.target}~${e.source}`;
+ if (seen.has(key)) return false;
+ seen.add(key);
+ return true;
+ });
+ }
+ return next.map((e) => ({ ...e, markerEnd: val ? ARROW : undefined }));
+ });
};
const loadPreset = (preset) => {
if (isRunningRef.current) return;
const g = seed(preset);
const e = directedRef.current ? g.edges.map((x) => ({ ...x, markerEnd: ARROW })) : g.edges;
nodesRef.current = g.nodes; edgesRef.current = e;
- startIdRef.current = g.startId; finishIdRef.current = null;
+ startIdRef.current = g.startId; finishIdRef.current = g.finishId;
labelRef.current = 0;
setNodes(g.nodes); setEdges(e);
setModeBoth('idle'); setStatus(''); pendingEdgeRef.current = null;
diff --git a/src/components/tree/layout.js b/src/components/tree/layout.js
new file mode 100644
index 0000000..6932489
--- /dev/null
+++ b/src/components/tree/layout.js
@@ -0,0 +1,215 @@
+// Tree layout helpers. A layout takes a logical tree and returns
+// { pos: { [id]: { x, y } }, cols, depth }
+// where x is a column index (0..cols-1) and y is the depth. The canvas scales
+// these into pixels. Binary layout preserves strict left/right (x by in-order
+// index) which a tidy n-ary layout would not. (Buchheim is added in phase 2.)
+
+// children of a node, handling both binary (left/right) and n-ary (children[])
+export function childrenOf(node) {
+ if (node.children) return node.children.filter(Boolean);
+ return [node.left, node.right].filter(Boolean);
+}
+
+export function flattenTree(root) {
+ const nodes = [];
+ const edges = [];
+ const walk = (node, parent) => {
+ if (!node) return;
+ nodes.push(node);
+ if (parent) edges.push({ parent: parent.id, child: node.id });
+ for (const c of childrenOf(node)) walk(c, node);
+ };
+ walk(root, null);
+ return { nodes, edges };
+}
+
+// In-order x, depth y — keeps a single child on its correct side.
+export function binaryLayout(root) {
+ const pos = {};
+ let idx = 0;
+ let depth = 0;
+ const walk = (node, d) => {
+ if (!node) return;
+ walk(node.left, d + 1);
+ pos[node.id] = { x: idx++, y: d };
+ if (d > depth) depth = d;
+ walk(node.right, d + 1);
+ };
+ walk(root, 0);
+ return { pos, cols: idx, depth };
+}
+
+// Buchheim tidy-tree layout for general n-ary trees (recursion trees). Ported
+// from the recursion-tree's original Tree.js. Returns the same
+// { pos, cols, depth } shape as binaryLayout; x positions are normalised so the
+// leftmost node sits at x = 0.
+export function buchheimLayout(root) {
+ const dt = buchheim(root);
+ const pos = {};
+ let minX = Infinity;
+ let maxX = -Infinity;
+ let depth = 0;
+ const collect = (d) => {
+ pos[d.tree.id] = { x: d.x, y: d.y };
+ if (d.x < minX) minX = d.x;
+ if (d.x > maxX) maxX = d.x;
+ if (d.y > depth) depth = d.y;
+ for (const c of d.children) collect(c);
+ };
+ collect(dt);
+ for (const id in pos) pos[id].x -= minX; // normalise so leftmost is 0
+ return { pos, cols: maxX - minX + 1, depth };
+}
+
+class DrawTree {
+ constructor(tree, parent = undefined, depth = 0, number = 1) {
+ this.x = -1;
+ this.y = depth;
+ this.tree = tree;
+ this.children = (tree.children || []).map(
+ (child, i) => new DrawTree(child, this, depth + 1, i + 1),
+ );
+ this.parent = parent;
+ this.thread = undefined;
+ this.mod = 0;
+ this.ancestor = this;
+ this.change = 0;
+ this.shift = 0;
+ this._lmost_sibling = undefined;
+ this.number = number;
+ }
+
+ left = () => this.thread || (this.children.length ? this.children[0] : undefined);
+ right = () => this.thread || (this.children.length ? this.children[this.children.length - 1] : undefined);
+
+ lbrother = () => {
+ let n;
+ if (this.parent) {
+ for (const node of this.parent.children) {
+ if (node === this) return n;
+ n = node;
+ }
+ }
+ return n;
+ };
+
+ get_lmost_sibling = () => {
+ if (!this._lmost_sibling && this.parent && this !== this.parent.children[0]) {
+ this._lmost_sibling = this.parent.children[0];
+ }
+ return this._lmost_sibling;
+ };
+}
+
+function buchheim(tree) {
+ const dt = firstwalk(new DrawTree(tree));
+ const min = second_walk(dt);
+ if (min < 0) third_walk(dt, -min);
+ return dt;
+}
+
+function third_walk(tree, n) {
+ tree.x += n;
+ for (const c of tree.children) third_walk(c, n);
+}
+
+function firstwalk(v, distance = 1) {
+ if (v.children.length === 0) {
+ v.x = v.get_lmost_sibling() ? v.lbrother().x + distance : 0;
+ } else {
+ let default_ancestor = v.children[0];
+ for (const w of v.children) {
+ firstwalk(w);
+ default_ancestor = apportion(w, default_ancestor, distance);
+ }
+ execute_shifts(v);
+ const midpoint = (v.children[0].x + v.children[v.children.length - 1].x) / 2;
+ const w = v.lbrother();
+ if (w) {
+ v.x = w.x + distance;
+ v.mod = v.x - midpoint;
+ } else {
+ v.x = midpoint;
+ }
+ }
+ return v;
+}
+
+function apportion(v, default_ancestor, distance) {
+ const w = v.lbrother();
+ if (w !== undefined) {
+ let vir = v;
+ let vor = v;
+ let vil = w;
+ let vol = v.get_lmost_sibling();
+ let sir = v.mod;
+ let sor = v.mod;
+ let sil = vil.mod;
+ let sol = vol.mod;
+ while (vil.right() && vir.left()) {
+ vil = vil.right();
+ vir = vir.left();
+ vol = vol.left();
+ vor = vor.right();
+ vor.ancestor = v;
+ const shift = vil.x + sil - (vir.x + sir) + distance;
+ if (shift > 0) {
+ move_subtree(ancestor(vil, v, default_ancestor), v, shift);
+ sir += shift;
+ sor += shift;
+ }
+ sil += vil.mod;
+ sir += vir.mod;
+ sol += vol.mod;
+ sor += vor.mod;
+ }
+ if (vil.right() && !vor.right()) {
+ vor.thread = vil.right();
+ vor.mod += sil - sor;
+ } else {
+ if (vir.left() && !vol.left()) {
+ vol.thread = vir.left();
+ vol.mod += sir - sol;
+ }
+ default_ancestor = v;
+ }
+ }
+ return default_ancestor;
+}
+
+function move_subtree(wl, wr, shift) {
+ const subtrees = wr.number - wl.number;
+ wr.change -= shift / subtrees;
+ wr.shift += shift;
+ wl.change += shift / subtrees;
+ wr.x += shift;
+ wr.mod += shift;
+}
+
+function execute_shifts(v) {
+ let shift = 0;
+ let change = 0;
+ for (let i = v.children.length - 1; i >= 0; i--) {
+ const w = v.children[i];
+ w.x += shift;
+ w.mod += shift;
+ change += w.change;
+ shift += w.shift + change;
+ }
+}
+
+// The original shipped a simplified Buchheim where this always fell through to
+// default_ancestor; kept as-is so the recursion-tree layout is unchanged.
+function ancestor(_vil, _v, default_ancestor) {
+ return default_ancestor;
+}
+
+function second_walk(v, m = 0, depth = 0, min = undefined) {
+ v.x += m;
+ v.y = depth;
+ if (min === undefined || v.x < min) min = v.x;
+ for (const w of v.children) {
+ min = second_walk(w, m + v.mod, depth + 1, min);
+ }
+ return min;
+}
diff --git a/src/components/tree/tree-canvas.jsx b/src/components/tree/tree-canvas.jsx
new file mode 100644
index 0000000..08bfd61
--- /dev/null
+++ b/src/components/tree/tree-canvas.jsx
@@ -0,0 +1,117 @@
+import { useEffect, useMemo, useRef, useState } from 'react';
+import TreeNode from './tree-node';
+import TreeEdge from './tree-edge';
+import { flattenTree } from './layout';
+
+// Renders a logical tree. On layout change, node positions are tweened with a
+// single requestAnimationFrame loop and BOTH nodes and edges read the same
+// interpolated coordinates each frame — so edges stay attached to the nodes
+// (SVG line endpoints can't be CSS-transitioned, which is why we tween in JS).
+
+const COLGAP = 46;
+const ROWGAP = 66;
+const PAD = 28;
+const DURATION = 600;
+
+const ease = (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2);
+
+export default function TreeCanvas({ tree, layout, nodeState = {}, edgeState = {}, labels = {} }) {
+ const [disp, setDisp] = useState(new Map()); // id -> currently displayed {x, y}
+ const [box, setBox] = useState(null); // tweened viewBox { W, H }
+ const rafRef = useRef(0);
+
+ const model = useMemo(() => {
+ if (!tree) return null;
+ const { pos, cols, depth } = layout(tree);
+ const { nodes, edges } = flattenTree(tree);
+ const W = PAD * 2 + Math.max(cols - 1, 0) * COLGAP;
+ const H = PAD * 2 + depth * ROWGAP;
+ const target = new Map();
+ for (const n of nodes) {
+ target.set(n.id, { x: PAD + pos[n.id].x * COLGAP, y: PAD + pos[n.id].y * ROWGAP });
+ }
+ return { nodes, edges, W, H, target };
+ }, [tree, layout]);
+
+ // signature of target positions; the tween restarts only when these change
+ const sig = model
+ ? [...model.target.entries()].map(([id, p]) => `${id}:${p.x},${p.y}`).join('|')
+ : '';
+
+ useEffect(() => {
+ if (!model) return undefined;
+ // existing nodes slide from where they are now; a NEW node emerges from
+ // its parent's current spot and slides in with everyone else (so it
+ // doesn't pop into place ahead of the nodes that still need to shift)
+ const parentOf = new Map();
+ for (const e of model.edges) parentOf.set(e.child, e.parent);
+ const startPos = new Map();
+ for (const n of model.nodes) {
+ if (disp.has(n.id)) {
+ startPos.set(n.id, disp.get(n.id));
+ } else {
+ const pid = parentOf.get(n.id);
+ const from = (pid && (disp.get(pid) || model.target.get(pid))) || model.target.get(n.id);
+ startPos.set(n.id, from);
+ }
+ }
+ // tween the viewBox too, so the auto-fit rescale on growth happens
+ // smoothly in lockstep with the nodes (an instant rescale would make
+ // nodes appear to arrive at different times in screen space)
+ const startBox = box || { W: model.W, H: model.H };
+ const start = performance.now();
+ cancelAnimationFrame(rafRef.current);
+ const step = (now) => {
+ const t = Math.min(1, (now - start) / DURATION);
+ const e = ease(t);
+ const next = new Map();
+ for (const n of model.nodes) {
+ const s = startPos.get(n.id);
+ const g = model.target.get(n.id);
+ next.set(n.id, { x: s.x + (g.x - s.x) * e, y: s.y + (g.y - s.y) * e });
+ }
+ setDisp(next);
+ setBox({ W: startBox.W + (model.W - startBox.W) * e, H: startBox.H + (model.H - startBox.H) * e });
+ if (t < 1) rafRef.current = requestAnimationFrame(step);
+ };
+ rafRef.current = requestAnimationFrame(step);
+ return () => cancelAnimationFrame(rafRef.current);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [sig]);
+
+ if (!model) {
+ return empty tree
;
+ }
+
+ const at = (id) => disp.get(id) || model.target.get(id);
+ const vbW = box ? box.W : model.W;
+ const vbH = box ? box.H : model.H;
+
+ return (
+
+ );
+}
diff --git a/src/components/tree/tree-edge.jsx b/src/components/tree/tree-edge.jsx
new file mode 100644
index 0000000..32e7b97
--- /dev/null
+++ b/src/components/tree/tree-edge.jsx
@@ -0,0 +1,24 @@
+import { TREE_NODE_R } from './tree-node';
+
+// Parent→child line, trimmed to the node radius. Transitions so it follows
+// nodes as they slide on re-layout.
+
+const STROKE = { tree: '#f59e0b', path: '#10b981', normal: '#64748b' };
+
+export default function TreeEdge({ x1, y1, x2, y2, state }) {
+ const dx = x2 - x1;
+ const dy = y2 - y1;
+ const len = Math.hypot(dx, dy) || 1;
+ const ux = dx / len;
+ const uy = dy / len;
+ return (
+
+ );
+}
diff --git a/src/components/tree/tree-menu.jsx b/src/components/tree/tree-menu.jsx
new file mode 100644
index 0000000..f5337cc
--- /dev/null
+++ b/src/components/tree/tree-menu.jsx
@@ -0,0 +1,60 @@
+import { useState } from 'react';
+import { CustomSlider } from '@/components/custom-slider';
+import { Button } from '@/components/ui/button';
+import { Plus, Minus, Search, RotateCcw } from 'lucide-react';
+
+// Sidebar for tree visualizers: a value field + insert/delete/search actions,
+// clear, and a speed slider. Callers pass the op handlers (they get the number).
+
+export default function TreeMenu({ title, disabled, onInsert, onDelete, onSearch, onClear, onSpeedChange }) {
+ const [value, setValue] = useState('');
+ const num = () => Number(value);
+ const valid = value.trim() !== '' && Number.isFinite(num());
+
+ return (
+
+
{title}
+
+
+
+
+
+ setValue(e.target.value)}
+ disabled={disabled}
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/tree/tree-node.jsx b/src/components/tree/tree-node.jsx
new file mode 100644
index 0000000..f6d771b
--- /dev/null
+++ b/src/components/tree/tree-node.jsx
@@ -0,0 +1,46 @@
+// SVG tree node: a circle with a primary value and an optional secondary label.
+// Position is a CSS transform transition so nodes slide when the tree re-lays
+// out; a one-shot SMIL fade-in plays when a node first appears.
+
+const FILL = {
+ normal: ['#0d9488', '#0f766e'],
+ current: ['#f59e0b', '#d97706'],
+ visited: ['#334155', '#475569'],
+ found: ['#10b981', '#059669'],
+ path: ['#10b981', '#059669'],
+ remove: ['#f43f5e', '#be123c'],
+};
+
+const R = 16;
+
+export default function TreeNode({ x, y, value, secondary, state }) {
+ if (state === 'hidden') return null;
+ const [bg, border] = FILL[state] || FILL.normal;
+ // shrink the primary label so longer values (e.g. recursion call labels
+ // like "(4,3)") still fit inside the circle
+ const len = String(value ?? '').length;
+ const fontSize = len <= 2 ? 13 : len <= 4 ? 10 : 8;
+
+ return (
+
+
+
+
+ {value}
+
+ {secondary != null && (
+
+ {secondary}
+
+ )}
+
+ );
+}
+
+export const TREE_NODE_R = R;
diff --git a/src/components/tree/use-tree-editor.js b/src/components/tree/use-tree-editor.js
new file mode 100644
index 0000000..54ce98a
--- /dev/null
+++ b/src/components/tree/use-tree-editor.js
@@ -0,0 +1,67 @@
+import { useState, useRef } from 'react';
+import { flattenTree } from './layout';
+
+// Reusable tree editor + action-log executor (SVG tree counterpart of
+// useGraphEditor). Holds the logical tree + visual marks and plays an action
+// list with a delay. Algorithm-agnostic: callers build actions and call run().
+
+const toDelay = (s) => 1100 - s * 10;
+
+export function useTreeEditor({ initialTree = null } = {}) {
+ const [tree, setTreeState] = useState(initialTree);
+ const [nodeState, setNodeState] = useState({});
+ const [edgeState, setEdgeState] = useState({});
+ const [labels, setLabels] = useState({});
+ const [status, setStatus] = useState('');
+ const [isRunning, setIsRunning] = useState(false);
+
+ const treeRef = useRef(initialTree);
+ const isRunningRef = useRef(false);
+ const speedRef = useRef(toDelay(50));
+
+ const applyTree = (t) => { treeRef.current = t; setTreeState(t); };
+ const clearMarks = () => { setNodeState({}); setEdgeState({}); setLabels({}); setStatus(''); };
+
+ const applyAction = (a) => {
+ if (a.type === 'setTree') {
+ applyTree(a.tree);
+ // recursion reveals a fixed tree: lay it out once, hidden, then
+ // un-hide nodes via markNode as the walk visits them
+ if (a.hidden && a.tree) {
+ const hidden = {};
+ for (const n of flattenTree(a.tree).nodes) hidden[n.id] = 'hidden';
+ setNodeState(hidden);
+ }
+ } else if (a.type === 'markNode') setNodeState((s) => ({ ...s, [a.id]: a.state }));
+ else if (a.type === 'markEdge') setEdgeState((s) => ({ ...s, [a.childId]: a.state }));
+ else if (a.type === 'setLabel') setLabels((s) => ({ ...s, [a.id]: a.secondary }));
+ else if (a.type === 'status') setStatus(a.text);
+ else if (a.type === 'clear') clearMarks();
+ };
+
+ const run = async (actions) => {
+ if (!actions || !actions.length || isRunningRef.current) return;
+ isRunningRef.current = true;
+ setIsRunning(true);
+ clearMarks();
+ for (const a of actions) {
+ applyAction(a);
+ // a structural change triggers the ~0.6s re-layout slide; always give
+ // it time to finish, even when the speed slider is set fast
+ const delay = a.type === 'setTree' ? Math.max(speedRef.current, 650) : speedRef.current;
+ await sleep(delay);
+ }
+ isRunningRef.current = false;
+ setIsRunning(false);
+ };
+
+ const getContext = () => ({ tree: treeRef.current });
+ const clear = () => { if (isRunningRef.current) return; applyTree(null); clearMarks(); };
+ const setSpeed = (s) => { speedRef.current = toDelay(s); };
+
+ return { tree, nodeState, edgeState, labels, status, isRunning, run, getContext, clear, setSpeed };
+}
+
+function sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
diff --git a/src/lib/algorithms/bst.js b/src/lib/algorithms/bst.js
new file mode 100644
index 0000000..8a28b9a
--- /dev/null
+++ b/src/lib/algorithms/bst.js
@@ -0,0 +1,101 @@
+// Binary Search Tree — immutable tree ops (preserving node ids so the renderer
+// can animate) and action-log planners for insert / delete / search.
+//
+// Node: { id, value, left, right }. Actions use setTree (re-layout) + markNode.
+
+let counter = 0;
+const makeNode = (value) => ({ id: `b${++counter}`, value, left: null, right: null });
+
+export function fromValues(values) {
+ let root = null;
+ for (const v of values) root = insert(root, v).root;
+ return root;
+}
+
+// insert returning { root, id, inserted } — clones the path, keeps other ids
+function insert(root, value) {
+ if (!root) {
+ const n = makeNode(value);
+ return { root: n, id: n.id, inserted: true };
+ }
+ if (value === root.value) return { root, id: root.id, inserted: false };
+ if (value < root.value) {
+ const r = insert(root.left, value);
+ return { root: { ...root, left: r.root }, id: r.id, inserted: r.inserted };
+ }
+ const r = insert(root.right, value);
+ return { root: { ...root, right: r.root }, id: r.id, inserted: r.inserted };
+}
+
+// immutable delete; two-child case copies the in-order successor's value into
+// the node (id preserved) and removes the successor
+function removeValue(root, value) {
+ if (!root) return null;
+ if (value < root.value) return { ...root, left: removeValue(root.left, value) };
+ if (value > root.value) return { ...root, right: removeValue(root.right, value) };
+ if (!root.left) return root.right;
+ if (!root.right) return root.left;
+ let s = root.right;
+ while (s.left) s = s.left;
+ return { ...root, value: s.value, right: removeValue(root.right, s.value) };
+}
+
+function pathTo(root, value) {
+ const path = [];
+ let cur = root;
+ while (cur) {
+ path.push(cur);
+ if (value === cur.value) break;
+ cur = value < cur.value ? cur.left : cur.right;
+ }
+ return path;
+}
+
+export function insertActions(tree, value) {
+ const actions = [];
+ for (const n of pathTo(tree, value)) actions.push({ type: 'markNode', id: n.id, state: 'current' });
+ const { root, id, inserted } = insert(tree, value);
+ if (!inserted) {
+ actions.push({ type: 'status', text: `${value} is already in the tree` });
+ actions.push({ type: 'clear' });
+ return actions;
+ }
+ for (const n of pathTo(tree, value)) actions.push({ type: 'markNode', id: n.id, state: 'normal' });
+ actions.push({ type: 'setTree', tree: root });
+ actions.push({ type: 'markNode', id, state: 'found' });
+ actions.push({ type: 'status', text: `Inserted ${value}` });
+ return actions;
+}
+
+export function searchActions(tree, value) {
+ const actions = [];
+ let cur = tree;
+ while (cur) {
+ actions.push({ type: 'markNode', id: cur.id, state: 'current' });
+ if (value === cur.value) {
+ actions.push({ type: 'markNode', id: cur.id, state: 'found' });
+ actions.push({ type: 'status', text: `Found ${value}` });
+ return actions;
+ }
+ actions.push({ type: 'markNode', id: cur.id, state: 'visited' });
+ cur = value < cur.value ? cur.left : cur.right;
+ }
+ actions.push({ type: 'status', text: `${value} not found` });
+ return actions;
+}
+
+export function deleteActions(tree, value) {
+ const actions = [];
+ const path = pathTo(tree, value);
+ for (const n of path) actions.push({ type: 'markNode', id: n.id, state: 'current' });
+ const target = path[path.length - 1];
+ if (!target || target.value !== value) {
+ actions.push({ type: 'status', text: `${value} not found` });
+ actions.push({ type: 'clear' });
+ return actions;
+ }
+ actions.push({ type: 'markNode', id: target.id, state: 'remove' });
+ actions.push({ type: 'setTree', tree: removeValue(tree, value) });
+ actions.push({ type: 'status', text: `Deleted ${value}` });
+ return actions;
+}
diff --git a/src/lib/algorithms/connectivity.js b/src/lib/algorithms/connectivity.js
new file mode 100644
index 0000000..4967ebc
--- /dev/null
+++ b/src/lib/algorithms/connectivity.js
@@ -0,0 +1,136 @@
+// Connectivity — presets, a color palette, and planners for Connected
+// Components (undirected / weakly connected) and Strongly Connected Components
+// (Tarjan, directed). Both emit the shared action log and use `colorNode` to
+// tint each component a distinct color.
+
+export const COMPONENT_PALETTE = [
+ '#0ea5e9', '#f97316', '#a855f7', '#22c55e',
+ '#ec4899', '#eab308', '#14b8a6', '#ef4444',
+];
+
+export const CONNECTIVITY_PRESETS = [
+ {
+ name: 'Islands',
+ nodes: [
+ { id: 'n1', x: 90, y: 80, label: 'A' },
+ { id: 'n2', x: 210, y: 70, label: 'B' },
+ { id: 'n3', x: 150, y: 170, label: 'C' },
+ { id: 'n4', x: 350, y: 80, label: 'D' },
+ { id: 'n5', x: 470, y: 90, label: 'E' },
+ { id: 'n6', x: 410, y: 190, label: 'F' },
+ { id: 'n7', x: 170, y: 300, label: 'G' },
+ { id: 'n8', x: 310, y: 300, label: 'H' },
+ ],
+ edges: [
+ ['n1', 'n2'], ['n2', 'n3'], ['n1', 'n3'],
+ ['n4', 'n5'], ['n5', 'n6'], ['n4', 'n6'],
+ ['n7', 'n8'],
+ ],
+ },
+ {
+ name: 'Connected',
+ nodes: [
+ { id: 'n1', x: 110, y: 90, label: 'A' },
+ { id: 'n2', x: 250, y: 70, label: 'B' },
+ { id: 'n3', x: 390, y: 110, label: 'C' },
+ { id: 'n4', x: 180, y: 230, label: 'D' },
+ { id: 'n5', x: 340, y: 240, label: 'E' },
+ ],
+ edges: [['n1', 'n2'], ['n2', 'n3'], ['n1', 'n4'], ['n4', 'n5'], ['n3', 'n5']],
+ },
+ {
+ name: 'SCC demo (use Directed)',
+ nodes: [
+ { id: 'n1', x: 90, y: 70, label: 'A' },
+ { id: 'n2', x: 210, y: 60, label: 'B' },
+ { id: 'n3', x: 150, y: 170, label: 'C' },
+ { id: 'n4', x: 330, y: 80, label: 'D' },
+ { id: 'n5', x: 450, y: 70, label: 'E' },
+ { id: 'n6', x: 400, y: 180, label: 'F' },
+ { id: 'n7', x: 250, y: 290, label: 'G' },
+ { id: 'n8', x: 400, y: 300, label: 'H' },
+ ],
+ edges: [
+ ['n1', 'n2'], ['n2', 'n3'], ['n3', 'n1'], // SCC: A B C
+ ['n3', 'n4'], // connector
+ ['n4', 'n5'], ['n5', 'n6'], ['n6', 'n4'], // SCC: D E F
+ ['n6', 'n7'], // connector
+ ['n7', 'n8'], ['n8', 'n7'], // SCC: G H
+ ],
+ },
+];
+
+// BFS flood-fill; one palette color per component.
+export function connectedComponentsActions(adj, nodeIds) {
+ const ids = [...nodeIds].sort();
+ const visited = new Set();
+ const actions = [];
+ let comp = 0;
+
+ for (const start of ids) {
+ if (visited.has(start)) continue;
+ const color = COMPONENT_PALETTE[comp % COMPONENT_PALETTE.length];
+ comp++;
+ const queue = [start];
+ visited.add(start);
+ while (queue.length) {
+ const u = queue.shift();
+ actions.push({ type: 'markNode', id: u, state: 'current' });
+ for (const { node: v } of adj[u] || []) {
+ if (!visited.has(v)) { visited.add(v); queue.push(v); }
+ }
+ actions.push({ type: 'colorNode', id: u, color });
+ }
+ }
+ actions.push({ type: 'status', text: `${comp} component${comp === 1 ? '' : 's'}` });
+ return actions;
+}
+
+// Tarjan's algorithm. Shows the low-link value on each node and colors each SCC
+// when its root is finalized.
+export function sccActions(adj, nodeIds) {
+ const ids = [...nodeIds].sort();
+ const actions = [];
+ const index = {};
+ const low = {};
+ const onStack = new Set();
+ const stack = [];
+ let idx = 0;
+ let comp = 0;
+
+ const strongconnect = (u) => {
+ index[u] = idx;
+ low[u] = idx;
+ idx++;
+ stack.push(u);
+ onStack.add(u);
+ actions.push({ type: 'markNode', id: u, state: 'current' });
+ actions.push({ type: 'setDist', id: u, dist: low[u] });
+
+ for (const { node: v } of adj[u] || []) {
+ if (index[v] === undefined) {
+ strongconnect(v);
+ if (low[v] < low[u]) { low[u] = low[v]; actions.push({ type: 'setDist', id: u, dist: low[u] }); }
+ } else if (onStack.has(v)) {
+ if (index[v] < low[u]) { low[u] = index[v]; actions.push({ type: 'setDist', id: u, dist: low[u] }); }
+ }
+ }
+
+ if (low[u] === index[u]) {
+ const color = COMPONENT_PALETTE[comp % COMPONENT_PALETTE.length];
+ comp++;
+ let w;
+ do {
+ w = stack.pop();
+ onStack.delete(w);
+ actions.push({ type: 'colorNode', id: w, color });
+ } while (w !== u);
+ }
+ };
+
+ for (const u of ids) {
+ if (index[u] === undefined) strongconnect(u);
+ }
+ actions.push({ type: 'status', text: `${comp} strongly-connected component${comp === 1 ? '' : 's'}` });
+ return actions;
+}
diff --git a/src/lib/algorithms/networkFlow.js b/src/lib/algorithms/networkFlow.js
new file mode 100644
index 0000000..3e0c40c
--- /dev/null
+++ b/src/lib/algorithms/networkFlow.js
@@ -0,0 +1,172 @@
+// Network Flow — presets and max-flow planners (Edmonds-Karp / Ford-Fulkerson).
+//
+// Directed graph; edge weight = capacity. Emits the shared action log, using
+// `setFlow` to show flow/capacity and `markEdge`/`markNode` to animate the
+// augmenting path. Finishes with a min-cut highlight. Phase 1: flow values are
+// faithful; backward (cancel) steps show as a forward edge's flow decreasing.
+
+export const FLOW_PRESETS = [
+ {
+ // classic CLRS max-flow network (max flow = 23)
+ name: 'Classic (s→t)',
+ source: 'n1',
+ sink: 'n6',
+ nodes: [
+ { id: 'n1', x: 60, y: 160, label: 's' },
+ { id: 'n2', x: 210, y: 70, label: 'a' },
+ { id: 'n3', x: 210, y: 250, label: 'b' },
+ { id: 'n4', x: 370, y: 70, label: 'c' },
+ { id: 'n5', x: 370, y: 250, label: 'd' },
+ { id: 'n6', x: 520, y: 160, label: 't' },
+ ],
+ edges: [
+ ['n1', 'n2', 16], ['n1', 'n3', 13], ['n2', 'n3', 10], ['n3', 'n2', 4],
+ ['n2', 'n4', 12], ['n4', 'n3', 9], ['n3', 'n5', 14], ['n5', 'n4', 7],
+ ['n4', 'n6', 20], ['n5', 'n6', 4],
+ ],
+ },
+ {
+ name: 'Diamond',
+ source: 'n1',
+ sink: 'n4',
+ nodes: [
+ { id: 'n1', x: 90, y: 160, label: 's' },
+ { id: 'n2', x: 260, y: 70, label: 'a' },
+ { id: 'n3', x: 260, y: 250, label: 'b' },
+ { id: 'n4', x: 430, y: 160, label: 't' },
+ ],
+ edges: [
+ ['n1', 'n2', 10], ['n1', 'n3', 5], ['n2', 'n3', 4],
+ ['n2', 'n4', 6], ['n3', 'n4', 10],
+ ],
+ },
+];
+
+const key = (a, b) => `${a}|${b}`;
+
+export function maxFlowActions(edges, sourceId, sinkId, variant = 'ek') {
+ const actions = [];
+ if (sourceId == null || sinkId == null || sourceId === sinkId) {
+ actions.push({ type: 'status', text: 'Set a source (S) and a sink (F)' });
+ return actions;
+ }
+
+ const cap = {};
+ const edgeIdOf = {};
+ const nbr = {};
+ for (const e of edges) {
+ const c = e.data?.weight ?? 1;
+ cap[key(e.source, e.target)] = (cap[key(e.source, e.target)] || 0) + c;
+ edgeIdOf[key(e.source, e.target)] = e.id;
+ (nbr[e.source] = nbr[e.source] || new Set()).add(e.target);
+ (nbr[e.target] = nbr[e.target] || new Set()).add(e.source);
+ }
+ const residual = { ...cap };
+ const getRes = (a, b) => residual[key(a, b)] ?? 0;
+ const neighbors = (u) => [...(nbr[u] || [])].sort();
+ const realEdge = (a, b) => edgeIdOf[key(a, b)] ?? edgeIdOf[key(b, a)];
+
+ // reconstruct path of [a,b] steps from a prev map
+ const rebuild = (prev) => {
+ const path = [];
+ let cur = sinkId;
+ while (cur !== sourceId) {
+ path.push([prev[cur], cur]);
+ cur = prev[cur];
+ }
+ return path.reverse();
+ };
+
+ const findBFS = () => {
+ const prev = {};
+ const visited = new Set([sourceId]);
+ const q = [sourceId];
+ while (q.length) {
+ const u = q.shift();
+ if (u === sinkId) break;
+ for (const w of neighbors(u)) {
+ if (!visited.has(w) && getRes(u, w) > 0) { visited.add(w); prev[w] = u; q.push(w); }
+ }
+ }
+ return visited.has(sinkId) ? rebuild(prev) : null;
+ };
+
+ const findDFS = () => {
+ const prev = {};
+ const visited = new Set([sourceId]);
+ const stack = [sourceId];
+ while (stack.length) {
+ const u = stack.pop();
+ if (u === sinkId) break;
+ for (const w of neighbors(u)) {
+ if (!visited.has(w) && getRes(u, w) > 0) { visited.add(w); prev[w] = u; stack.push(w); }
+ }
+ }
+ return visited.has(sinkId) ? rebuild(prev) : null;
+ };
+
+ const find = variant === 'ff' ? findDFS : findBFS;
+
+ // start every edge at flow 0
+ for (const e of edges) actions.push({ type: 'setFlow', id: e.id, flow: 0 });
+
+ let total = 0;
+ let pathNo = 0;
+ while (true) {
+ const path = find();
+ if (!path) break;
+ pathNo += 1;
+
+ let bottleneck = Infinity;
+ for (const [a, b] of path) bottleneck = Math.min(bottleneck, getRes(a, b));
+
+ // highlight the augmenting path
+ for (const [a, b] of path) {
+ const eid = realEdge(a, b);
+ if (eid) actions.push({ type: 'markEdge', id: eid, state: 'path' });
+ actions.push({ type: 'markNode', id: a, state: 'current' });
+ actions.push({ type: 'markNode', id: b, state: 'current' });
+ }
+ actions.push({ type: 'status', text: `Augmenting path ${pathNo}: bottleneck ${bottleneck}` });
+
+ // push flow along the path
+ for (const [a, b] of path) {
+ residual[key(a, b)] = getRes(a, b) - bottleneck;
+ residual[key(b, a)] = getRes(b, a) + bottleneck;
+ }
+ total += bottleneck;
+
+ // refresh flow labels on real edges
+ for (const e of edges) {
+ const flow = Math.max(0, (cap[key(e.source, e.target)] || 0) - getRes(e.source, e.target));
+ actions.push({ type: 'setFlow', id: e.id, flow });
+ }
+ actions.push({ type: 'status', text: `Total flow: ${total}` });
+
+ // clear the path highlight before the next iteration
+ for (const [a, b] of path) {
+ const eid = realEdge(a, b);
+ if (eid) actions.push({ type: 'markEdge', id: eid, state: 'normal' });
+ actions.push({ type: 'markNode', id: a, state: 'normal' });
+ actions.push({ type: 'markNode', id: b, state: 'normal' });
+ }
+ }
+
+ // min cut: nodes reachable from source in the final residual graph
+ const reach = new Set([sourceId]);
+ const q = [sourceId];
+ while (q.length) {
+ const u = q.shift();
+ for (const w of neighbors(u)) {
+ if (!reach.has(w) && getRes(u, w) > 0) { reach.add(w); q.push(w); }
+ }
+ }
+ for (const u of reach) actions.push({ type: 'colorNode', id: u, color: '#0ea5e9' });
+ for (const e of edges) {
+ if (reach.has(e.source) && !reach.has(e.target)) {
+ actions.push({ type: 'markEdge', id: e.id, state: 'negcycle' });
+ }
+ }
+ actions.push({ type: 'status', text: `Max flow = ${total} · min cut` });
+ return actions;
+}