karthik.dev
Back to blogDSA

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.