Visualizing Recursion Trees: A Step-by-Step Guide to Recursive Function Execution
By Han Qi
Recursion Execution Path
How difficult would it be to create a recursion visualization? Turns out, way harder than expected.
How to Reproduce
The following gist can be executed to experiment with recursion visualization:
This script generates two output files in the same directory for visualization:
graph_data.json
visualization.html
This article documents multiple failed attempts before achieving a clear visualization, ultimately providing insights into human-AI interaction.
Motivation
I wanted to understand how recursive functions work, but VSCode’s debugger proved unsuitable because:
- It forces users to click through stack frames one at a time, making it too linear.
- It provides too much granularity—I only wanted to see the overall call graph.
Upon searching for a “recursion tree visualizer,” I found no satisfactory results on the first page of Google.
Requirements
- Allow users to paste any custom function for visualization.
- Provide an easy way to define global variables.
- Include widgets (e.g., sliders) for stepping through execution.
Existing Options
- Visualgo.net – Lacked proper global variable setup.
- Recursion Tree Visualizer (GitHub) – Forced function name standardization and caused errors with global variables that otherwise worked in Jupyter.
The Plan
- Get a basic visualization working with free ChatGPT.
- Experiment with “vibe coding.”
- Assess patience levels before manually fixing AI-generated output.
The Journey
Attempted approaches:
- Basic function visualization.
- Tree structures with indentation.
- Use of Graphviz.
- D3.js (had CORS issues).
- Ipywidgets with Plotly.
- Matplotlib (
ax.plot
) – initially not refreshing. - Creating a simplified working example.
- Switching to
plt.show
for debugging. - Fixing
ax.plot
refresh issues. - Refining visualization with decorators.
The Function: Permutations with Recursion
def permute(nums):
def backtrack(first=0):
if first == len(nums) - 1:
result.append(nums[:])
return
for i in range(first, len(nums)):
nums[first], nums[i] = nums[i], nums[first] # Swap
backtrack(first + 1)
nums[first], nums[i] = nums[i], nums[first] # Swap back
result = []
backtrack()
return result
print(permute([1, 2, 3]))
Output:
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]]
The base case is reached at first == len(nums) - 1
. Visualization clarified that an extra unnecessary call was originally present.
Using Graphviz for Better Visualization
Graphviz was used to generate a graphical representation of recursion.
from graphviz import Digraph
def permute(nums):
graph = Digraph(format="png")
node_id = 0
def backtrack(first=0, depth=0, parent=None):
nonlocal node_id
node_label = f"{first}: {nums}"
current_id = str(node_id)
graph.node(current_id, label=node_label)
node_id += 1
if parent is not None:
graph.edge(parent, current_id)
if first == len(nums) - 1:
return
for i in range(first, len(nums)):
nums[first], nums[i] = nums[i], nums[first]
backtrack(first + 1, depth + 1, current_id)
nums[first], nums[i] = nums[i], nums[first]
backtrack()
return graph
Advantages:
- Shows all calls at once.
- Highlights hierarchy clearly.
- Provides interactive visualization with colors.
Adding Interactivity with D3.js
Generated interactive visualizations using HTML and JSON. However, CORS issues required running a local server:
python3 -m http.server 8000 --bind 0.0.0.0
Refactoring with Decorators
Class-based decorators tracked execution while preserving function signatures.
class Visualizer:
def __init__(self, func):
self.func = func
self.graph = Digraph(format="png")
self.node_id = 0
self.parent = None
self.depth = -1
self.nodes_data = []
def __call__(self, *args, **kwargs):
node_label = f"{args=}"
current_id = str(self.node_id)
self.node_id += 1
self.graph.node(current_id, label=node_label)
if self.parent is not None:
self.graph.edge(self.parent, current_id)
self.nodes_data.append((current_id, self.depth + 1))
previous_parent = self.parent
self.parent = current_id
self.depth += 1
self.func(*args, **kwargs)
self.parent = previous_parent
self.depth -= 1
return self.nodes_data, self.graph
Lessons Learned
- Prompting Efficiency – Refining GPT usage improved results.
- Debugging Methods – Order of operations mattered.
- Trade-offs in Automation – Some tasks still required manual debugging.
- Balancing Constraints & AI Use – AI was useful for partial solutions but had nuance limitations.
Conclusion
While AI-assisted coding can help accelerate development, critical thinking and debugging remain essential. Developers must balance human intuition with AI-generated code for efficiency and accuracy.