Skip to content
Sullivan Xiong

Data Structures and Algorithms Visualizer

DSaA Visualization tool. Built-in editor and interpreter that allows interacting with the DSaA.

Data Structures and Algorithms Visualizer, screenshot 1 of 6

The Data Structures and Algorithms (DSaA) Visualizer is an interactive educational tool designed to help users understand the internal behavior of data structures like linked lists, heaps, and trees through animated step-by-step visualizations. Users can write code in a simplified JavaScript-like syntax, see each line executed in real time, and observe how operations mutate the underlying data structures.

My Approach

I built this project from the ground up to review core fundamental computer science concepts and my full-stack engineering skills. I broke down this project into 3 core components:

  1. The Data Structures and Algorithms implementation in a separate Repository. I created the repository, https://github.com/SullivanXiong/DSaAPractice, as an Advent of Code-like approach to learning. So far I’ve added support for JavaScript, Python, and Java that provides skeleton class templates and testing infrastructure to help users focus on implementing and verifying. I added a feature to DSaA that took my personal implementations of Data Structures and packages it into an NPM library that I then imported in my personal-portfolio.

  2. The personal-portfolio imports the DSaA library and then wraps it using Object-Oriented approaches such as Encapsulation, Polymorphism, Inheritance, and Generics to create a structured approach to visualizing these data structures.

  3. The front-end has custom logic that allows us to render each of these data structures appropriately after taking the Abstract Syntax Nodes and breaking them into step-by-step (per-line) execution. This allows us to render and step through and step back through the visualization of the executed user-code.

Object-Oriented Visualizable Wrapper

We’ll go more in depth with the Visualizable wrapper because it’s interesting how I decided to implement a “renderable” component. 

It starts out with a Visualizable interface that encapsulated all of the logic for what defines something that can be rendered by the Visualizer UI component. https://github.com/SullivanXiong/personal-portfolio/blob/main/personal-portfolio/src/lib/visualizer/Visualizable.ts

// Visualizable.ts
import { ExecutionStep } from "@/components/visualizer/visualizer";
export interface Visualizable<T> {
  traceLog: ExecutionStep[];
  setSteps(steps: ExecutionStep[]): void;
  setSetSteps(_setSteps: (steps: ExecutionStep[]) => void): void;
  tracer(state: T[]): void;
  serialize(): T[];
  clear(): void;
}

https://github.com/SullivanXiong/personal-portfolio/blob/main/personal-portfolio/src/lib/visualizer/VisualizableLinkedList.ts

// lib/visualizable/VisualizableLinkedList.ts
import { ExecutionStep } from "@/components/visualizer/visualizer";
import { LinkedList } from "@sullivanxiong/dsaa";
import { Visualizable } from "./Visualizable";
export class VisualizableLinkedList<T> extends LinkedList<T> implements Visualizable<T> {
  public traceLog: ExecutionStep[] = [];
  public setSteps: (steps: ExecutionStep[]) => void = () => {};
  public constructor() {
    super();
  }
  public setSetSteps(_setSteps: (steps: ExecutionStep[]) => void) {
    this.setSteps = _setSteps;
  }
  public tracer(state: T[]): void {
    this.traceLog.push({ state: state } as ExecutionStep);
    this.setSteps([...this.traceLog]);
  }
  public clear(): void {
    super.clear();
    this.traceLog = [];
    this.emit();
  }
  private emit(): void {
    this.tracer(this.serialize());
  }
  // --- Override Mutating Methods ---
  public override add(value: T, index?: number): void {
    super.add(value, index);
    this.emit();
  }
  public override addFirst(value: T): void {
    super.addFirst(value);
    this.emit();
  }
  public override addLast(value: T): void {
    super.addLast(value);
    this.emit();
  }
  public override remove(index?: number): T {
    const val = super.remove(index);
    this.emit();
    return val;
  }
  public override removeFirst(): T {
    const val = super.removeFirst();
    this.emit();
    return val;
  }
  public override removeLast(): T {
    const val = super.removeLast();
    this.emit();
    return val;
  }
  // --- Visualization Snapshot ---
  public serialize(): T[] {
    const result: T[] = [];
    let current = this.getHead();
    while (current) {
      result.push(current.value);
      current = current.next;
    }
    return result;
  }
}

This approach allows me to reuse my already implemented Data Structures while just wrapping it into a visualizable. It makes it so that I can easily add new data structures or algorithms in the future while also providing a structure that is clean, concise, and logical.

The UI component is a Visualizer that is made generic with the data type that it can store, the Visualizable type, and some DSaA render props type.

https://github.com/SullivanXiong/personal-portfolio/blob/main/personal-portfolio/src/components/visualizer/visualizer.tsx// visualizer.tsx

// visualizer.tsx
"use client";

import { interpreter } from "@/lib/interpreter/interpreter";

import { Visualizable } from "@/lib/visualizer/Visualizable";

import { ComponentType, useEffect, useState } from "react";

import CodeTextDisplay from "../text-components/code-text-display/code-text-display";

import CodeTextEditor from "../text-components/code-text-editor/code-text-editor";

export interface DSaAVisualizerProps {

  steps: ExecutionStep[];

  currentStep: number;

  onStepChange: (step: number) => void;

}

export interface ExecutionStep {

  state: [];

  line?: number;

}

export type VisualizerProps<V, VP> = {

  initialCode: string;

  title?: string;

  rawFileUrl?: string;

  theme?: string;

  language?: string;

  createVisualizable: () => V;

  Visualizer: ComponentType<VP>;

};

export default function Visualizer<T, V extends Visualizable<T>, VP extends DSaAVisualizerProps>({

  initialCode = "",

  title,

  rawFileUrl,

  theme,

  language,

  createVisualizable,

  Visualizer,

}: VisualizerProps<V, VP>) {

  const [steps, setSteps] = useState<ExecutionStep[]>([]);

  const [code, setCode] = useState(initialCode);

  const [dsaa] = useState(() => {

    const dsaa = createVisualizable();

    dsaa.setSetSteps(setSteps);

    return dsaa;

  });

  const [highlightedLine, setHighlightedLine] = useState<number | null>(0);

  const [currentStep, setCurrentStep] = useState(0);

  function handleCodeRun(code: string) {

    // Reset before execution

    dsaa.clear();

    setSteps([]);

    setCurrentStep(0);

    setHighlightedLine(0);

    try {

      interpreter(code, dsaa);

    } catch (err) {

      console.error("Execution failed:", err);

    }

  }

  // Update highlighted line on step change

  useEffect(() => {

    const step = steps[currentStep];

    setHighlightedLine(step?.line ?? null);

  }, [currentStep, steps]);

  // Run default code on first mount

  useEffect(() => {

    handleCodeRun(code);

  }, []);

  <h1 className="text-white text-3xl font-bold mb-6">{title} Visualizer</h1>;

  return (

    <div className="max-h-[67vh] flex flex-row lg:flex-row gap-6">

      {/* Left side: Code editor + visualizer */}

      <div className="w-2/3">

        {/* Code Editor */}

        <div className="h-1/3 bg-zinc-900 p-4 rounded-t-md">

          <CodeTextEditor

            initialCode={code}

            onCodeChange={(newCode) => setCode(newCode)}

            highlightedLine={highlightedLine}

          />

          <div className="flex justify-end mt-2">

            <button

              onClick={() => handleCodeRun(code)}

              className="bg-zinc-800 hover:bg-zinc-700 px-3 rounded text-white font-semibold"

            >

              ▶️ Run Code

            </button>

          </div>

        </div>

        {/* Visualizer */}

        <div className="h-2/3 bg-zinc-900 p-4 rounded-b-md">

          <Visualizer

            {...({

              steps,

              currentStep,

              onStepChange: (step: number) => setCurrentStep(step),

            } as VP)}

          />

        </div>

      </div>

      {/* Right side: Code source file display */}

      <div className="w-1/3 overflow-y-auto">

        <CodeTextDisplay

          url={rawFileUrl}

          language={

            language === "typescript" || language === "javascript" || language === "python" || language === "java"

              ? language

              : "javascript"

          }

          theme={theme}

          height="100%"

        />

      </div>

    </div>

  );

}

All of the core logic is handled within this generic Visualizer Component such as passing in the user-defined code into the interpreter to parse and execute, passing in the ExecutionSteps into the visualizer, and rendering the DSaA source code and visualizer.

Each Visualizer has custom render logic that must be handled accordingly, so if we take a look at LinkedList here we can see that basically just loops through an serialized array and renders it as boxes separated by “—> “ which is correct visually.

https://github.com/SullivanXiong/personal-portfolio/blob/main/personal-portfolio/src/components/visualizer/linked-list-visualizer.tsx

// linked-list-visualizer.tsx
"use client";
import { AnimatePresence, motion } from "framer-motion";
import React from "react";
import { DSaAVisualizerProps } from "./visualizer";
export default function LinkedListVisualizer({ steps, currentStep, onStepChange }: DSaAVisualizerProps) {
  const current = steps[currentStep]?.state ?? [];
  return (
    <div className="flex flex-col h-full">
      {/* Linked list visual nodes */}
      <div className="flex items-center gap-4 p-4 bg-zinc-900 rounded-md overflow-x-auto">
        <AnimatePresence initial={false}>
          {current.length === 0 ? (
            <motion.div
              key="empty"
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              className="text-zinc-400 italic"
            >
              Empty Linked List
            </motion.div>
          ) : (
            current.map((value, index) => (
              <React.Fragment key={index}>
                <motion.div
                  layout
                  initial={{ y: 10, opacity: 0 }}
                  animate={{ y: 0, opacity: 1 }}
                  exit={{ y: -10, opacity: 0 }}
                  transition={{ duration: 0.3 }}
                  className="relative bg-blue-600 text-white px-4 py-2 rounded shadow-md min-w-[50px] text-center"
                >
                  <div className="text-sm absolute -top-5 left-1/2 -translate-x-1/2 text-zinc-400">{index}</div>
                  {String(value)}
                </motion.div>
                {index < current.length - 1 && (
                  <motion.div
                    layout
                    initial={{ opacity: 0 }}
                    animate={{ opacity: 1 }}
                    exit={{ opacity: 0 }}
                    className="text-white text-xl"
                  >
                    →
                  </motion.div>
                )}
              </React.Fragment>
            ))
          )}
        </AnimatePresence>
      </div>
      {/* Step control */}
      <div className="flex justify-center items-center gap-4 mt-auto">
        <button
          onClick={() => onStepChange(Math.max(0, currentStep - 1))}
          disabled={currentStep === 0}
          className="bg-zinc-700 hover:bg-zinc-600 px-3 py-1 rounded text-white disabled:opacity-30"
        >
          ⏮ Prev
        </button>
        <div className="text-sm text-center text-zinc-400">
          Step {currentStep + 1} of {steps.length}
        </div>
        <button
          onClick={() => onStepChange(Math.min(steps.length - 1, currentStep + 1))}
          disabled={currentStep === steps.length - 1}
          className="bg-zinc-700 hover:bg-zinc-600 px-3 py-1 rounded text-white disabled:opacity-30"
        >
          Next ⏭
        </button>
      </div>
    </div>
  );
}

This allowed me to keep my logic for the actual page simple by just defining the visualizable, the visualizer to use, and some other initializing props. I’ve noticed that after structuring my code this way I could implement and add new Data Structures in minutes now rather than hours of writing custom implementation and rendering logic for each data structure.

https://github.com/SullivanXiong/personal-portfolio/blob/main/personal-portfolio/src/app/(portfolio)/projects/dsaa-visualizer/page.tsx

// dsaa-visualizer/page.tsx
"use client";
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import BinarySearchTreeVisualizer from "@/components/visualizer/binary-search-tree-visualizer";
import DoublyLinkedListVisualizer from "@/components/visualizer/doubly-linked-list-visualizer";
import LinkedListVisualizer from "@/components/visualizer/linked-list-visualizer";
…
type AnyVisualizerProps = DSaAVisualizerProps | QueueArrayDSaAVisualizerProps;
const DSaAs: Record<string, VisualizerProps<Visualizable<number>, AnyVisualizerProps>> = {
  "linked-list": {
    initialCode: "let list = new LinkedList();\nlist.add(1);\nlist.add(2);\nlist.remove(0);\nlist.add(3);",
    title: "Linked List",
    rawFileUrl:
      "https://raw.githubusercontent.com/SullivanXiong/DSaAPractice/refs/heads/main/sullivanxiong-dsaa-lib/src/linked-list.ts",
    theme: "atomonedark",
    language: "javascript",
    createVisualizable: () => new VisualizableLinkedList<number>(),
    Visualizer: LinkedListVisualizer as ComponentType<AnyVisualizerProps>,
  },
  …
};
const SELECTABLES = Object.keys(DSaAs).map((key) => ({
  value: key,
  label: DSaAs[key].title,
}));
export default function QueueArrayVisualizerPage() {
  const [dsaa, setDSaA] = useState<string>();
  return (
    <div className="max-w-7xl mx-auto p-6">
      <div className="w-full mb-4">
        <Select value={dsaa} onValueChange={(value) => setDSaA(value)}>
          <SelectTrigger>
            <SelectValue placeholder="Select a DSaA to visualize" />
          </SelectTrigger>
          <SelectContent>
            <SelectGroup>
              <SelectLabel>DSaA</SelectLabel>
              {SELECTABLES.map((item, i) => (
                <SelectItem key={i} value={item.value}>
                  {item.label}
                </SelectItem>
              ))}
            </SelectGroup>
          </SelectContent>
        </Select>
      </div>
      {dsaa && <Visualizer<number, Visualizable<number>, DSaAVisualizerProps> key={dsaa} {...DSaAs[dsaa]} />}
    </div>
  );
}