richdoherty.dev

Sorting Algorithm Visualizer

Richard Doherty | March 3, 2023

My sorting algorithm visualizer website.

View Project

View Code

Background

I tried to create this project a few years ago and was not able to figure it out. But recently I decided to give it another shot. I also wanted to try Solidjs since that framework seems to be getting more and more popular. I am happy to say I was able to figure it out this time, and I'll show you how I did it.

What I Used to Make This Site

First Steps

The first step, as always, is to create the project. In Solidjs, that is done with the following commands:

> npx degit solidjs/templates/ts my-app
> cd my-app
> npm i # or yarn or pnpm
> npm run dev # or yarn or pnpm
> npx degit solidjs/templates/ts my-app
> cd my-app
> npm i # or yarn or pnpm
> npm run dev # or yarn or pnpm

Then, I created the bars that are going to be sorted and added some text and buttons.

For the bars I used Solidjs' "For" tag to render a bar for each element in the bars array (I'll get to that in the next section):

<div class={styles.App}>
    <header class={styles.header}>
        <h1>Sorting Algorithms</h1>
    </header>
    <div id="visualizer" class={styles.visualizer}>
        <For each={bars}>
            {(bar, i) => (
                <div
                    id={"bar" + i().toString()}
                    class={styles.bar}
                    style={`height: ${bar}%;
                width: ${numOfBars()}%;`}
                ></div>
            )}
        </For>
    </div>
</div>
<div class={styles.App}>
    <header class={styles.header}>
        <h1>Sorting Algorithms</h1>
    </header>
    <div id="visualizer" class={styles.visualizer}>
        <For each={bars}>
            {(bar, i) => (
                <div
                    id={"bar" + i().toString()}
                    class={styles.bar}
                    style={`height: ${bar}%;
                width: ${numOfBars()}%;`}
                ></div>
            )}
        </For>
    </div>
</div>

I also added the buttons and an input where the user can enter how many bars they want to appear on the screen:

<div class={styles.App}>
    <header class={styles.header}>
        <h1>Sorting Algorithms</h1>
    </header>
    <div id="visualizer" class={styles.visualizer}>
        <For each={bars}>
            {(bar, i) => (
                <div
                    id={"bar" + i().toString()}
                    class={styles.bar}
                    style={`height: ${bar}%;
                width: ${numOfBars()}%;`}
                ></div>
            )}
        </For>
    </div>
    <div class={styles.input}>
        <div id="buttonDiv" class={styles.buttonDiv}>
            <button
                class={styles.resetButton}
                onClick={() => {
                    const val = parseInt(
                        (
                            document.getElementById(
                                "numOfBars"
                            ) as HTMLInputElement
                        ).value
                    );
                    if (val < 10 || val > 100) {
                        return alert("Number must be between 10 and 100.");
                    }
                    setNumOfBars(val);
                    newBarsArray();
                }}
            >
                Reset
            </button>
            <button
                class={styles.sortButton}
                onClick={() => {
                    bubbleAnimation(bubbleSort(bars));
                }}
            >
                Bubble Sort
            </button>
            <button
                class={styles.sortButton}
                onClick={() => {
                    selectionAnimation(selectionSort(bars));
                }}
            >
                Selection Sort
            </button>
        </div>
    </div>
</div>
<div class={styles.App}>
    <header class={styles.header}>
        <h1>Sorting Algorithms</h1>
    </header>
    <div id="visualizer" class={styles.visualizer}>
        <For each={bars}>
            {(bar, i) => (
                <div
                    id={"bar" + i().toString()}
                    class={styles.bar}
                    style={`height: ${bar}%;
                width: ${numOfBars()}%;`}
                ></div>
            )}
        </For>
    </div>
    <div class={styles.input}>
        <div id="buttonDiv" class={styles.buttonDiv}>
            <button
                class={styles.resetButton}
                onClick={() => {
                    const val = parseInt(
                        (
                            document.getElementById(
                                "numOfBars"
                            ) as HTMLInputElement
                        ).value
                    );
                    if (val < 10 || val > 100) {
                        return alert("Number must be between 10 and 100.");
                    }
                    setNumOfBars(val);
                    newBarsArray();
                }}
            >
                Reset
            </button>
            <button
                class={styles.sortButton}
                onClick={() => {
                    bubbleAnimation(bubbleSort(bars));
                }}
            >
                Bubble Sort
            </button>
            <button
                class={styles.sortButton}
                onClick={() => {
                    selectionAnimation(selectionSort(bars));
                }}
            >
                Selection Sort
            </button>
        </div>
    </div>
</div>

Signals

According to the Solidjs tutorial, "Signals are the cornerstone of reactivity in Solid. They contain values that change over time; when you change a signal's value, it automatically updates anything that uses it."

To use it you must first import it:

import { createSignal } from "solid-js";
import { createSignal } from "solid-js";

I wanted to use it to determine the number of bars appearing on the screen and I set it up like this:

const [numOfBars, setNumOfBars] = createSignal(50);
const [numOfBars, setNumOfBars] = createSignal(50);

Inside the brackets are a getter and a setter. numOfBars is a function that returns the current value. It isn't actually the value itself. setNumOfBars is a function that updates the current value.

Stores

In Solidjs, "stores" allow for nested reactivity. I used this to hold the array of numbers which determine the values of each bar:

const [bars, setBars] = createStore([0]);
const [bars, setBars] = createStore([0]);

Using a store allows us to update the array and see the changes without having to replace it with a clone of itself.

I used both of these features in conjucntion to randomize the array when the page loads and whenever the "reset" button is clicked:

const newBarsArray = () => {
    setBars(
        Array.from({ length: numOfBars() }, () =>
            Math.floor(Math.random() * 98 + 1)
        )
    );
};
 
newBarsArray();
const newBarsArray = () => {
    setBars(
        Array.from({ length: numOfBars() }, () =>
            Math.floor(Math.random() * 98 + 1)
        )
    );
};
 
newBarsArray();
<button
    class={styles.resetButton}
    onClick={() => {
        const val = parseInt(
            (document.getElementById("numOfBars") as HTMLInputElement).value
        );
        if (val < 10 || val > 100) {
            return alert("Number must be between 10 and 100.");
        }
        setNumOfBars(val);
        newBarsArray();
    }}
>
    Reset
</button>
<button
    class={styles.resetButton}
    onClick={() => {
        const val = parseInt(
            (document.getElementById("numOfBars") as HTMLInputElement).value
        );
        if (val < 10 || val > 100) {
            return alert("Number must be between 10 and 100.");
        }
        setNumOfBars(val);
        newBarsArray();
    }}
>
    Reset
</button>

Animations

Now for the fun part, the animations!

The sorting algorithms I have implemented so far are:

Bubble Sort

The first algorithm I decided to implement an animation for is bubble sort. I won't get into exactly how these algorithms work in this post but here is the code for it:

const bubbleSort = (arr: number[]) => {
    let sortedArr = Array.from(arr);
    let animations = [];
    for (let i = 0; i < sortedArr.length - 1; i++) {
        for (let j = 0; j < sortedArr.length - i - 1; j++) {
            animations.push("compare");
            if (sortedArr[j] > sortedArr[j + 1]) {
                animations.push("first");
                animations.push("swap");
                [sortedArr[j], sortedArr[j + 1]] = [
                    sortedArr[j + 1],
                    sortedArr[j],
                ];
            } else {
                animations.push("second");
            }
            animations.push("reset");
        }
    }
    return animations;
};
const bubbleSort = (arr: number[]) => {
    let sortedArr = Array.from(arr);
    let animations = [];
    for (let i = 0; i < sortedArr.length - 1; i++) {
        for (let j = 0; j < sortedArr.length - i - 1; j++) {
            animations.push("compare");
            if (sortedArr[j] > sortedArr[j + 1]) {
                animations.push("first");
                animations.push("swap");
                [sortedArr[j], sortedArr[j + 1]] = [
                    sortedArr[j + 1],
                    sortedArr[j],
                ];
            } else {
                animations.push("second");
            }
            animations.push("reset");
        }
    }
    return animations;
};

You probably noticed the animations array since that is not present on a normal bubble sort function. That array stores the frames for the animation. Each string that is pushed to the array represents what is happening in the algorithm.

Bubble Sort Animations

Next, I used the resulting animations array in another function which makes use of the commands and actually changes the positions and colors of the bars.

const bubbleAnimation = (animations: string[]) => {
    let i = 0;
    let barId = 0;
    let pass = 0;
    const iid = setInterval(() => {
        if (i < animations.length) {
            const bar1 = document.getElementById("bar" + barId.toString());
            const bar2 = document.getElementById(
                "bar" + (barId + 1).toString()
            );
            if (bar1 && bar2 && barId < bars.length - pass - 1) {
                if (animations[i] === "compare") {
                    bar1.style.backgroundColor = "#f6c177";
                    bar2.style.backgroundColor = "#f6c177";
                } else if (animations[i] === "first") {
                    bar1.style.backgroundColor = "#eb6f92";
                } else if (animations[i] === "swap") {
                    setBars(
                        produce((bars) => {
                            [bars[barId], bars[barId + 1]] = [
                                bars[barId + 1],
                                bars[barId],
                            ];
                        })
                    );
                } else if (animations[i] === "second") {
                    bar2.style.backgroundColor = "#eb6f92";
                } else if (animations[i] === "reset") {
                    bar1.style.backgroundColor = "#ebbcba";
                    bar2.style.backgroundColor = "#ebbcba";
                    barId++;
                }
                i++;
            } else {
                pass++;
                barId = 0;
            }
        } else {
            clearInterval(iid);
        }
    }, speed());
};
const bubbleAnimation = (animations: string[]) => {
    let i = 0;
    let barId = 0;
    let pass = 0;
    const iid = setInterval(() => {
        if (i < animations.length) {
            const bar1 = document.getElementById("bar" + barId.toString());
            const bar2 = document.getElementById(
                "bar" + (barId + 1).toString()
            );
            if (bar1 && bar2 && barId < bars.length - pass - 1) {
                if (animations[i] === "compare") {
                    bar1.style.backgroundColor = "#f6c177";
                    bar2.style.backgroundColor = "#f6c177";
                } else if (animations[i] === "first") {
                    bar1.style.backgroundColor = "#eb6f92";
                } else if (animations[i] === "swap") {
                    setBars(
                        produce((bars) => {
                            [bars[barId], bars[barId + 1]] = [
                                bars[barId + 1],
                                bars[barId],
                            ];
                        })
                    );
                } else if (animations[i] === "second") {
                    bar2.style.backgroundColor = "#eb6f92";
                } else if (animations[i] === "reset") {
                    bar1.style.backgroundColor = "#ebbcba";
                    bar2.style.backgroundColor = "#ebbcba";
                    barId++;
                }
                i++;
            } else {
                pass++;
                barId = 0;
            }
        } else {
            clearInterval(iid);
        }
    }, speed());
};

I'll break this function down piece by piece.

First, the variables at the start.

Next, I set up a setInterval function. This repeats the code inside the interval after a set time. I have it set to speed() which I set in this line:

const [speed, setSpeed] = createSignal(200);
const [speed, setSpeed] = createSignal(200);

The speed is in milliseconds so in this case it would be 200 milliseconds.

Then we have these lines of code:

const bar1 = document.getElementById("bar" + barId.toString());
const bar2 = document.getElementById("bar" + (barId + 1).toString());
const bar1 = document.getElementById("bar" + barId.toString());
const bar2 = document.getElementById("bar" + (barId + 1).toString());

Since we need to compare a bar with the one that comes after, we get the first one using barId and the next with barId + 1.

Then we get into some ifs and else ifs. For example:

if (bar1 && bar2 && barId < bars.length - pass - 1) {
if (bar1 && bar2 && barId < bars.length - pass - 1) {

For each pass, the function should leave out the bars that are already in the correct position at the end, so subtracting pass from the length is necessary so function knows when to reset barId to 0.

The next few statements are pretty self explanatory. They check the current element in the animations array and changes the color based on what step it is, "compare," "first," "swap," "second," and "reset."

One thing to clarify is when it is time to swap the bars. setBars() can be used to change the array, but to actually mutate the array without replacing the current array with a new one, the produce() function must be called inside setBars().

Finally, when else is reached, pass is incremented and barId is set back to 0.

When the end of the animations array is reached, it is important not to forget to clear the interval using clearInterval(iid).

And there we have it! The bubble sort animation is complete.

Selection Sort

I set up selection sort like this:

const selectionSort = (arr: number[]) => {
    let sortedArr = Array.from(arr);
    let animations = [];
    let minIndex = 0;
    for (let i = 0; i < sortedArr.length - 1; i++) {
        minIndex = i;
        for (let j = i + 1; j < sortedArr.length; j++) {
            if (sortedArr[j] < sortedArr[minIndex]) {
                minIndex = j;
            }
            animations.push(minIndex);
        }
        [sortedArr[i], sortedArr[minIndex]] = [
            sortedArr[minIndex],
            sortedArr[i],
        ];
    }
    return animations;
};
const selectionSort = (arr: number[]) => {
    let sortedArr = Array.from(arr);
    let animations = [];
    let minIndex = 0;
    for (let i = 0; i < sortedArr.length - 1; i++) {
        minIndex = i;
        for (let j = i + 1; j < sortedArr.length; j++) {
            if (sortedArr[j] < sortedArr[minIndex]) {
                minIndex = j;
            }
            animations.push(minIndex);
        }
        [sortedArr[i], sortedArr[minIndex]] = [
            sortedArr[minIndex],
            sortedArr[i],
        ];
    }
    return animations;
};

Similarly to my bubbleSort function, selectionSort is the same as a normal selection sort but with the animations array. However, instead of pushing a command like "compare" or "swap," I pushed the minIndex. I tried pushing commands for this function as well, but there were issues with the bars not swapping properly. Since the function looks through each element in the array and only swaps elements at the end of a loop each time, passing the minIndex is sufficient.

Selection Sort Animations

const selectionAnimation = (animations: number[]) => {
    let i = 0;
    let barId = 1;
    let pass = 0;
    let minIndex = 0;
    const iid = setInterval(() => {
        const lastBar = document.getElementById(
            "bar" + (numOfBars() - 1).toString()
        );
        if (i < animations.length) {
            const minBar = document.getElementById("bar" + minIndex.toString());
            const compareBar = document.getElementById(
                "bar" + barId.toString()
            );
            const swapBar = document.getElementById("bar" + pass.toString());
            const prevBar = document.getElementById(
                "bar" + (barId - 1).toString()
            );
            if (minBar && compareBar && barId < bars.length - 1) {
                if (lastBar) {
                    lastBar.style.backgroundColor = "#ebbcba";
                }
                if (swapBar) {
                    swapBar.style.backgroundColor = "#9ccfd8";
                }
                compareBar.style.backgroundColor = "#f6c177";
                minBar.style.backgroundColor = "#31748f";
                if (prevBar && prevBar !== minBar) {
                    prevBar.style.backgroundColor = "#ebbcba";
                }
                if (swapBar && minIndex !== animations[i]) {
                    minBar.style.backgroundColor = "#ebbcba";
                    compareBar.style.backgroundColor = "#31748f";
                    swapBar.style.backgroundColor = "#9ccfd8";
                }
                minIndex = animations[i];
                barId++;
            } else {
                if (compareBar) {
                    compareBar.style.backgroundColor = "#f6c177";
                }
                if (prevBar) {
                    prevBar.style.backgroundColor = "#ebbcba";
                }
                if (minBar) {
                    minBar.style.backgroundColor = "#ebbcba";
                }
                minIndex = animations[i];
                setBars(
                    produce((bars) => {
                        [bars[minIndex], bars[pass]] = [
                            bars[pass],
                            bars[minIndex],
                        ];
                    })
                );
                pass++;
                minIndex = pass;
                barId = 1 + pass;
            }
            i++;
        } else {
            if (lastBar) {
                lastBar.style.backgroundColor = "#ebbcba";
            }
            clearInterval(iid);
        }
    }, speed());
};
const selectionAnimation = (animations: number[]) => {
    let i = 0;
    let barId = 1;
    let pass = 0;
    let minIndex = 0;
    const iid = setInterval(() => {
        const lastBar = document.getElementById(
            "bar" + (numOfBars() - 1).toString()
        );
        if (i < animations.length) {
            const minBar = document.getElementById("bar" + minIndex.toString());
            const compareBar = document.getElementById(
                "bar" + barId.toString()
            );
            const swapBar = document.getElementById("bar" + pass.toString());
            const prevBar = document.getElementById(
                "bar" + (barId - 1).toString()
            );
            if (minBar && compareBar && barId < bars.length - 1) {
                if (lastBar) {
                    lastBar.style.backgroundColor = "#ebbcba";
                }
                if (swapBar) {
                    swapBar.style.backgroundColor = "#9ccfd8";
                }
                compareBar.style.backgroundColor = "#f6c177";
                minBar.style.backgroundColor = "#31748f";
                if (prevBar && prevBar !== minBar) {
                    prevBar.style.backgroundColor = "#ebbcba";
                }
                if (swapBar && minIndex !== animations[i]) {
                    minBar.style.backgroundColor = "#ebbcba";
                    compareBar.style.backgroundColor = "#31748f";
                    swapBar.style.backgroundColor = "#9ccfd8";
                }
                minIndex = animations[i];
                barId++;
            } else {
                if (compareBar) {
                    compareBar.style.backgroundColor = "#f6c177";
                }
                if (prevBar) {
                    prevBar.style.backgroundColor = "#ebbcba";
                }
                if (minBar) {
                    minBar.style.backgroundColor = "#ebbcba";
                }
                minIndex = animations[i];
                setBars(
                    produce((bars) => {
                        [bars[minIndex], bars[pass]] = [
                            bars[pass],
                            bars[minIndex],
                        ];
                    })
                );
                pass++;
                minIndex = pass;
                barId = 1 + pass;
            }
            i++;
        } else {
            if (lastBar) {
                lastBar.style.backgroundColor = "#ebbcba";
            }
            clearInterval(iid);
        }
    }, speed());
};

There are a bunch more bars in this one than in bubble sort. lastBar is there to reset the color of the last bar right before clearInterval() is called. minBar is the bar with the minimum value during the current pass. compareBar is the current bar being looked at. swapBar is the initial bar in the pass which will be swapped at the end of the pass. prevBar is the bar before compareBar. This is needed to change the color back to pink after it is checked.

This function goes from the first bar to the final bar in the array, changing colors along the way. When the last bar is reached we go to the else statement where the minBar is swapped with the bar whose index is represented by pass.

Conclusion

I enjoyed making this project and though it was frustrating at times when the animations wouldn't work properly, it was incredibly satisfying to get it working. I may come back and update this with other algorithms in the future, but for now I hope you enjoyed seeing the process and I hope you learned something from this.

Thanks for reading!

GitHubLinkedIn

richdoherty7@gmail.com

This site was built with Next.jsMDXTailwind,  and Vercel.