Tutorial · Algorithms · JavaScript · Canvas 2D
📅 July 2026 ⏱ ≈ 25 min 🎯 Intermediate

Build a Sorting Visualizer in 100 Lines of JavaScript

The classic sorting-algorithm bar-chart animation looks like it needs a full state machine to pause and resume mid-comparison. It doesn't — JavaScript generator functions let you write the algorithms exactly as you'd find them in a textbook, and get frame-by-frame animation almost for free. This tutorial builds a working visualizer with four algorithms in about 100 lines of plain JavaScript and Canvas 2D.

1. The Core Trick: Generators as Pausable Algorithms

A regular JavaScript function runs to completion the instant you call it — there's no built-in way to pause a Bubble Sort mid-swap and resume it 16 milliseconds later on the next animation frame. A generator function (declared with function*) solves this exactly: calling yield suspends execution and hands a value back to the caller, who can resume it later with .next() — picking up on the very next line, with all local variables intact.

function* countUp(n) {
  for (let i = 0; i < n; i++) yield i; // pauses here every iteration
}
const gen = countUp(3);
gen.next(); // { value: 0, done: false }
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: undefined, done: true }

The whole visualizer is built on one idea: write each sorting algorithm as a generator that yields every time it compares or swaps two elements, then let a requestAnimationFrame loop pull one step at a time and redraw the array between steps.

2. Data and Canvas Setup

The data is just an array of numbers representing bar heights, and the renderer draws each one as a coloured vertical bar. Two indices — compare — get highlighted so you can see exactly what the algorithm is looking at on each step:

const canvas = document.getElementById('sortCanvas');
const ctx = canvas.getContext('2d');
const N = 80;
let arr = Array.from({ length: N }, () => Math.floor(Math.random() * 100) + 5);

function drawBars(highlight = []) {
  const w = canvas.width / arr.length;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  arr.forEach((val, i) => {
    ctx.fillStyle = highlight.includes(i) ? '#f87171' : '#60a5fa';
    const h = (val / 105) * canvas.height;
    ctx.fillRect(i * w, canvas.height - h, w - 1, h);
  });
}

3. Bubble Sort as a Generator

Bubble Sort repeatedly steps through the array, swapping adjacent elements that are out of order. Written as a generator, it's identical to the textbook version except for one added yield after every comparison:

function* bubbleSort(a) {
  for (let i = 0; i < a.length - 1; i++) {
    for (let j = 0; j < a.length - i - 1; j++) {
      yield [j, j + 1]; // about to compare these two indices
      if (a[j] > a[j + 1]) {
        [a[j], a[j + 1]] = [a[j + 1], a[j]]; // swap
        yield [j, j + 1]; // show the swapped state too
      }
    }
  }
}

4. Insertion Sort as a Generator

Insertion Sort grows a sorted prefix one element at a time, shifting larger elements right to make room. Yielding on every shift visually shows the "insertion" happening in slow motion:

function* insertionSort(a) {
  for (let i = 1; i < a.length; i++) {
    const key = a[i];
    let j = i - 1;
    yield [i, j];
    while (j >= 0 && a[j] > key) {
      a[j + 1] = a[j]; // shift right
      j--;
      yield [j, j + 1];
    }
    a[j + 1] = key; // drop the key into its slot
    yield [j + 1];
  }
}

5. Quick Sort as a Generator

Quick Sort is recursive, so its generator version needs yield* ("yield delegation") to forward every value yielded by a recursive call up to the top-level caller — without it, only the outermost call's yields would ever be seen:

function* quickSort(a, lo = 0, hi = a.length - 1) {
  if (lo >= hi) return;
  const pivot = a[hi];
  let i = lo - 1;
  for (let j = lo; j < hi; j++) {
    yield [j, hi]; // compare a[j] against the pivot
    if (a[j] < pivot) {
      i++;
      [a[i], a[j]] = [a[j], a[i]];
      yield [i, j];
    }
  }
  [a[i + 1], a[hi]] = [a[hi], a[i + 1]]; // place pivot at its final position
  yield [i + 1, hi];
  yield* quickSort(a, lo, i);       // delegate: forward every yield from the left half
  yield* quickSort(a, i + 2, hi); // … and the right half
}
Why yield* and not just calling the function: a plain recursive call quickSort(a, lo, i) would run the entire left half to completion instantly, since nothing is pulling values out of it. yield* turns the recursive call into a sub-iterator whose values flow through the parent generator one at a time, exactly interleaved with the top-level loop pulling frames.

6. Merge Sort as a Generator

Merge Sort splits, recursively sorts both halves, then merges them back using an auxiliary array. Yielding during both the split and the merge-back phase shows the "divide and conquer" structure directly on screen:

function* mergeSort(a, lo = 0, hi = a.length - 1) {
  if (lo >= hi) return;
  const mid = (lo + hi) >> 1;
  yield* mergeSort(a, lo, mid);
  yield* mergeSort(a, mid + 1, hi);

  const left = a.slice(lo, mid + 1), right = a.slice(mid + 1, hi + 1);
  let i = 0, j = 0, k = lo;
  while (i < left.length && j < right.length) {
    a[k] = left[i] <= right[j] ? left[i++] : right[j++];
    yield [k];  // index k just received its merged value
    k++;
  }
  while (i < left.length)  { a[k] = left[i++];  yield [k++]; }
  while (j < right.length) { a[k] = right[j++]; yield [k++]; }
}

7. The Render Loop

The loop pulls one {value, done} pair from the active generator per animation frame, redraws the bars with the yielded indices highlighted, and stops when done is true. Pulling multiple steps per frame controls playback speed without touching the algorithm code at all:

let sorter = bubbleSort(arr);
let stepsPerFrame = 3; // higher = faster playback

function animate() {
  let result;
  for (let s = 0; s < stepsPerFrame; s++) {
    result = sorter.next();
    if (result.done) break;
  }
  drawBars(result.done ? [] : result.value);
  if (!result.done) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Swapping algorithms: because every algorithm is just a generator with the same "yield an array of indices to highlight" contract, switching from Bubble Sort to Quick Sort is exactly one line — sorter = quickSort(arr) — with zero changes to the render loop.

8. The Full ~100-Line Visualizer

Putting every piece together into one self-contained script — a working, animated sorting visualizer with four selectable algorithms:

// ── Sorting visualizer in ~100 lines: generators + Canvas 2D ──
const canvas = document.getElementById('sortCanvas');
const ctx = canvas.getContext('2d');
const N = 80;
let arr = [];
let sorter = null;
const stepsPerFrame = 3;

function resetArray() {
  arr = Array.from({ length: N }, () => Math.floor(Math.random() * 100) + 5);
}

function drawBars(highlight = []) {
  const w = canvas.width / arr.length;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  arr.forEach((val, i) => {
    ctx.fillStyle = highlight.includes(i) ? '#f87171' : '#60a5fa';
    const h = (val / 105) * canvas.height;
    ctx.fillRect(i * w, canvas.height - h, w - 1, h);
  });
}

function* bubbleSort(a) {
  for (let i = 0; i < a.length - 1; i++)
    for (let j = 0; j < a.length - i - 1; j++) {
      yield [j, j + 1];
      if (a[j] > a[j + 1]) { [a[j], a[j + 1]] = [a[j + 1], a[j]]; yield [j, j + 1]; }
    }
}

function* insertionSort(a) {
  for (let i = 1; i < a.length; i++) {
    const key = a[i]; let j = i - 1;
    yield [i, j];
    while (j >= 0 && a[j] > key) { a[j + 1] = a[j]; j--; yield [j, j + 1]; }
    a[j + 1] = key; yield [j + 1];
  }
}

function* quickSort(a, lo = 0, hi = a.length - 1) {
  if (lo >= hi) return;
  const pivot = a[hi]; let i = lo - 1;
  for (let j = lo; j < hi; j++) {
    yield [j, hi];
    if (a[j] < pivot) { i++; [a[i], a[j]] = [a[j], a[i]]; yield [i, j]; }
  }
  [a[i + 1], a[hi]] = [a[hi], a[i + 1]]; yield [i + 1, hi];
  yield* quickSort(a, lo, i);
  yield* quickSort(a, i + 2, hi);
}

function* mergeSort(a, lo = 0, hi = a.length - 1) {
  if (lo >= hi) return;
  const mid = (lo + hi) >> 1;
  yield* mergeSort(a, lo, mid);
  yield* mergeSort(a, mid + 1, hi);
  const left = a.slice(lo, mid + 1), right = a.slice(mid + 1, hi + 1);
  let i = 0, j = 0, k = lo;
  while (i < left.length && j < right.length) { a[k] = left[i] <= right[j] ? left[i++] : right[j++]; yield [k++]; }
  while (i < left.length)  { a[k] = left[i++];  yield [k++]; }
  while (j < right.length) { a[k] = right[j++]; yield [k++]; }
}

const ALGORITHMS = { bubble: bubbleSort, insertion: insertionSort, quick: quickSort, merge: mergeSort };

function start(name) {
  resetArray();
  sorter = ALGORITHMS[name](arr);
  requestAnimationFrame(animate);
}

function animate() {
  let result;
  for (let s = 0; s < stepsPerFrame; s++) {
    result = sorter.next();
    if (result.done) break;
  }
  drawBars(result.done ? [] : result.value);
  if (!result.done) requestAnimationFrame(animate);
}

start('bubble'); // call start('quick'), start('merge'), start('insertion') to switch
Next steps: Add a <select> dropdown wired to start(select.value) to switch algorithms live, track a comparison/swap counter to display Big-O in action empirically, or add a Web Audio oscillator whose frequency is driven by the compared value for the classic "sorting algorithm sound" effect.

Frequently Asked Questions

What will I learn in this tutorial?

Build an animated sorting algorithm visualizer in about 100 lines of plain JavaScript and Canvas 2D: generators as pausable algorithms, Bubble/Insertion/Quick/Merge Sort, and colour-coded comparisons and swaps.

What topics are covered in this tutorial?

This tutorial covers: The Core Trick: Generators as Pausable Algorithms, Data and Canvas Setup, Bubble Sort as a Generator, Insertion Sort as a Generator, Quick Sort as a Generator, Merge Sort as a Generator, The Render Loop, The Full ~100-Line Visualizer.

How long does this tutorial take?

This tutorial takes approximately 25 minutes to complete.

What prerequisites do I need before starting?

This is a Intermediate-level tutorial — no special preparation beyond basic JavaScript is assumed.