An interactive, real-time graph simulation tool built to explore graph algorithims and visualize hidden structures. Built with optimization, clean memory mangament, and intuitive user interaction in mind, this project sits at the intersection of performance at scale and usability.
Graph Editing: Add Node, Remove Node, Add Edges, Remove Edges, Create Graph

Interface for manually adding edges between nodes
Sidebar Lookup: User can filter, sort (ascending & descending), and select by any node attribute.

Interactive sidebar for inspecting, filtering, and sorting nodes
Visualization: BFS DFS, Dijkstra's, A*, Minimum Spanning Tree, Outdegree, Indegree, Centrality, Spectral Clustering
![]() Dijkstra’s Algorithm Shortest path visualization on a weighted graph |
![]() Spectral Clustering Finding clusters in dense graphs |
Runtime: Simulates over 2000 nodes smoothly, compared to less than 500 initially.
![]() Optimized — 2000 nodes |
![]() Unoptimized — 500 nodes |
- React Components – Render UI elements like buttons, overlays, sidebars, and the main canvas.
- HTML5 Canvas – Renders the 2D graph visualization and handles node/edge drawing.
- CSS / Styling – Handles layout, hover states, animations, and dynamic visual feedback.
- Custom Physics Module – Implements centerForce, springForce, and repulsiveForce to simulate realistic node movements.
- Custom Spatial Hash Grid – Optimizes neighbor queries, reducing computational complexity for large graphs.
- React Hooks (useState, useRef, useEffect) – Manage component state, user interactions, and side effects.
- Event Listeners / Shortcuts – Handle keyboard shortcuts, mouse interactions, and dynamic user inputs.
- Buffers & History – Tracks changes to nodes and edges for undo/redo functionality.
- Custom Graph Class (Python) – Executes graph algorithms like BFS, DFS, Dijkstra’s, A*, Minimum Spanning Tree, Outdegree, Indegree, Centrality, and Spectral Clustering.
- Fetch API (Flask) – Handles asynchronous requests from frontend to backend.
Tight simulation loops exposed how costly repeated property access and object indirection can be. I learned to cache frequently accessed values into local variables and to design data structures around these tight loops. To my surpirse, these small optimizations had a disproportionate impact on frame rate, deomnstrating how performance is often dictated by implementation details rather than high-level algorithm choice.
A naïve implementation of repulsive forces resulted in O(n²) behavior that did not scale. Implementing a spatial hash grid reduced unnecessary pairwise calculations by limiting interactions to local neighborhoods. This provided a concrete, practical lesson in how algorithmic complexity directly affects real-time systems.
I learned that visually plausible behavior is not necessarily numerically stable. Early versions of the simulation suffered from oscillations, divergence, and sensitivty to external forces. Switching to a velocity-based integration model and carefully tuning damping parameters taught me how timestep size, force scaling, and integration methods interact in long-running simulations.
Implementing A* really deepened my understanding of heuristic-driven search, especially with visualizing the tradeoff between exploration and exploitation. It was cool to compare with Dijkstra on the same graph and compare the paths and runtimes of both algorithms.
One of the most significant learning outcomes was implementing spectral clustering. I learned how graph structure can be encoded in the Laplacian matrix, and how its eigenvalues and eigenvectors reveal latent community structure. I really enjoyed reading A Tutorial on Spectral Clustering by Ulrike von Luxburg particularly because of they grounded the eigenvector equations in real world interpretations like random walks and approximating Ncut. Overall, learning the framework for spectral clustering was a beautiful mesh of linear algebra, graph theory, and unsupervised learning.
I learned the importance of clean boundaries between graph data, algorithm execution, and rendering. Treating algorithm outputs as streams of visual events rather than direct mutations made the system easier to reason about and extend. This separation simplified backend–frontend communication and allowed algorithms to evolve independently of visualization concerns.
At multiple points throughout the design process I felt unsatisfied with the many of the components being near-replicas and only differing in minute details. This led me to rethink how to generalize functionality across components while still preserving flexibility by introducing parameterized components and shared utility functions. For example, action overlays for adding/removing nodes and edges originally had nearly identical logic, differing only in their input. By abstracting the form handling and input into reusable modules, I could plug in different parameters or behaviors without duplicating code. The same was true for handling keyboard shortcuts and mouse interactions which ultimatley caused me to develop a catch-all handleKeyDown function.



