DSA Algorithm Visualizer: Animating 20+ Algorithms Step by Step
2026-05-20 · 5 min read
Why Visualizing Algorithms Matters
Most people learn sorting algorithms by reading pseudocode. Pseudocode tells you what happens — but it doesn't build intuition for why QuickSort performs better than BubbleSort on average, or how Dijkstra's algorithm propagates distances through a graph.
Animation answers these questions. Watching a pivot partition an array, seeing a BFS frontier expand level by level, observing an AVL tree rotate — these create mental models that pseudocode alone can't.
Architecture: Generator Functions as Frame Producers
The core design: each algorithm is implemented as a JavaScript generator function that yields visualization frames instead of producing a final result. The UI consumes these frames at a controlled playback speed.
// src/algorithms/sorting/quicksort.ts
interface SortFrame {
array: number[];
comparing: [number, number] | null;
swapping: [number, number] | null;
pivot: number | null;
sorted: number[];
pseudocodeLine: number;
}
function* quicksort(arr: number[], low: number, high: number): Generator<SortFrame> {
if (low >= high) return;
const pivot = arr[high];
let i = low - 1;
yield { array: [...arr], comparing: null, swapping: null, pivot: high, sorted: [], pseudocodeLine: 2 };
for (let j = low; j < high; j++) {
yield { array: [...arr], comparing: [j, high], swapping: null, pivot: high, sorted: [], pseudocodeLine: 4 };
if (arr[j] <= pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
yield { array: [...arr], comparing: null, swapping: [i, j], pivot: high, sorted: [], pseudocodeLine: 6 };
}
}
[arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
yield { array: [...arr], comparing: null, swapping: [i + 1, high], pivot: null, sorted: [i + 1], pseudocodeLine: 9 };
yield* quicksort(arr, low, i);
yield* quicksort(arr, i + 2, high);
}
The generator yields a complete state snapshot at each significant step. The UI is a pure function of the current frame — no imperative animation logic.
The Playback Controller
// src/hooks/useAlgorithmPlayer.ts
export function useAlgorithmPlayer<T>(generator: Generator<T>) {
const [currentFrame, setCurrentFrame] = useState<T | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [speed, setSpeed] = useState(500); // ms per frame
const generatorRef = useRef(generator);
const timerRef = useRef<NodeJS.Timeout>();
const step = useCallback(() => {
const result = generatorRef.current.next();
if (result.done) {
setIsPlaying(false);
return;
}
setCurrentFrame(result.value);
}, []);
useEffect(() => {
if (!isPlaying) {
clearInterval(timerRef.current);
return;
}
timerRef.current = setInterval(step, speed);
return () => clearInterval(timerRef.current);
}, [isPlaying, speed, step]);
return { currentFrame, isPlaying, speed, setIsPlaying, setSpeed, step };
}
Sorting Algorithms: Bar Chart + Framer Motion
// src/components/SortingVisualizer.tsx
function SortingBar({ value, max, state }: BarProps) {
const height = `${(value / max) * 100}%`;
const color = state === "comparing" ? "#f59e0b"
: state === "swapping" ? "#ef4444"
: state === "pivot" ? "#8b5cf6"
: state === "sorted" ? "#10b981"
: "#6366f1";
return (
<motion.div
layout
className="relative flex-1 rounded-t"
style={{ height, backgroundColor: color }}
transition={{ duration: 0.1 }}
/>
);
}
layout on Framer Motion handles the position animation when elements swap — no explicit keyframes needed.
Graph Traversal: Canvas API
For graph algorithms (BFS, DFS, Dijkstra), I used the Canvas API. Graphs are complex enough that DOM-based rendering becomes slow at scale.
// src/components/GraphVisualizer.tsx
function drawGraph(
ctx: CanvasRenderingContext2D,
nodes: GraphNode[],
edges: GraphEdge[],
frame: GraphFrame,
) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// Draw edges
for (const edge of edges) {
const from = nodes.find(n => n.id === edge.from)!;
const to = nodes.find(n => n.id === edge.to)!;
ctx.strokeStyle = frame.visitedEdges.includes(edge.id) ? "#6366f1" : "#374151";
ctx.lineWidth = frame.visitedEdges.includes(edge.id) ? 2 : 1;
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.stroke();
if (edge.weight) {
drawEdgeWeight(ctx, from, to, edge.weight);
}
}
// Draw nodes
for (const node of nodes) {
const isVisited = frame.visitedNodes.includes(node.id);
const isFrontier = frame.frontierNodes.includes(node.id);
const isCurrent = frame.currentNode === node.id;
ctx.fillStyle = isCurrent ? "#f59e0b" : isFrontier ? "#8b5cf6" : isVisited ? "#6366f1" : "#1f2937";
ctx.beginPath();
ctx.arc(node.x, node.y, 20, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#ffffff";
ctx.fillText(node.label, node.x, node.y + 4);
}
}
Pseudocode Synchronization
The pseudocode panel highlights the active line for each frame:
// src/components/PseudocodePanel.tsx
function PseudocodeLine({ line, lineNumber, isActive }: LineProps) {
return (
<motion.div
className={`font-mono text-sm px-3 py-1 rounded ${isActive ? "bg-violet-500/20 text-violet-300" : "text-gray-400"}`}
animate={{ opacity: isActive ? 1 : 0.5 }}
>
<span className="text-gray-600 mr-3">{lineNumber}</span>
{line}
</motion.div>
);
}
Algorithms Visualized
Sorting (8): Bubble Sort, Selection Sort, Insertion Sort, Merge Sort, Quick Sort, Heap Sort, Counting Sort, Radix Sort
Graph (5): BFS, DFS, Dijkstra's Shortest Path, Prim's MST, Kruskal's MST
Tree (4): BST Insert/Delete/Search, AVL Rotations, Red-Black Tree Insert, Heap Insert/Delete
Dynamic Programming (3): Longest Common Subsequence, 0/1 Knapsack, Coin Change
The DP visualizations show the table being filled cell-by-cell — the most valuable visualization for understanding why DP works.
The Lesson
Building the visualizer taught me something about algorithms I didn't fully appreciate before: the relationship between the algorithm's structure and its performance. Watching MergeSort's divide-and-conquer balanced against QuickSort's in-place partitioning is more instructive than any Big-O comparison table.