Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/images/binary-heap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 86 additions & 0 deletions src/app/binary-heap/menu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useState } from 'react';
import { CustomSlider } from '@/components/custom-slider';
import { Button } from '@/components/ui/button';
import { Plus, ArrowUpFromLine, Wand2, ArrowDownWideNarrow, RotateCcw } from 'lucide-react';

// Sidebar for the binary heap (min-heap): insert a value, extract the min,
// build a heap from a typed list, and run heapsort.

export default function HeapMenu({ disabled, onInsert, onExtract, onBuild, onHeapsort, onClear, onSpeedChange }) {
const [value, setValue] = useState('');
const [list, setList] = useState('5, 3, 8, 1, 9, 2, 7');

const num = () => Number(value);
const valid = value.trim() !== '' && Number.isFinite(num());
const parsedList = () => list.split(/[\s,]+/).map(Number).filter((n) => Number.isFinite(n));

return (
<div className="w-64 bg-gray-100 p-4 space-y-6 overflow-auto">
<h2 className="text-lg font-semibold">Binary Heap (min)</h2>

<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="h-px flex-1 bg-gray-300" />
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">Operation</span>
<div className="h-px flex-1 bg-gray-300" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium whitespace-nowrap">Value</label>
<input
type="number"
value={value}
onChange={(e) => setValue(e.target.value)}
disabled={disabled}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
/>
</div>
<div className="flex gap-2">
<Button className="flex-1" onClick={() => valid && onInsert(num())} disabled={disabled || !valid}>
<Plus /> Insert
</Button>
<Button className="flex-1" variant="outline" onClick={onExtract} disabled={disabled}>
<ArrowUpFromLine /> Extract
</Button>
</div>
</div>

<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="h-px flex-1 bg-gray-300" />
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">Build</span>
<div className="h-px flex-1 bg-gray-300" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium whitespace-nowrap">List</label>
<input
type="text"
value={list}
onChange={(e) => setList(e.target.value)}
disabled={disabled}
placeholder="e.g. 5, 3, 8, 1"
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
/>
</div>
<Button className="w-full" variant="outline" onClick={() => onBuild(parsedList())} disabled={disabled || parsedList().length === 0}>
<Wand2 /> Build heap
</Button>
</div>

<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="h-px flex-1 bg-gray-300" />
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</span>
<div className="h-px flex-1 bg-gray-300" />
</div>
<Button className="w-full" variant="outline" onClick={onHeapsort} disabled={disabled}>
<ArrowDownWideNarrow /> Heapsort
</Button>
<p className="text-xs text-gray-500">Min-heap heapsort sorts in descending order.</p>
<CustomSlider title="Speed" defaultValue={50} min={10} max={100} step={1} onChange={onSpeedChange} />
<Button className="w-full" variant="outline" onClick={onClear} disabled={disabled}>
<RotateCcw /> Clear
</Button>
</div>
</div>
);
}
45 changes: 45 additions & 0 deletions src/app/binary-heap/page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import { useState } from 'react';
import Navbar from '@/components/navbar';
import { fromValues, insertActions, extractActions, buildActions, heapsortActions } from '@/lib/algorithms/heap';
import { binaryLayout } from '@/components/tree/layout';
import { useTreeEditor } from '@/components/tree/use-tree-editor';
import TreeCanvas from '@/components/tree/tree-canvas';
import HeapMenu from './menu';

export default function BinaryHeap() {
const [initialTree] = useState(() => fromValues([50, 30, 70, 20, 40, 60, 80]));
const g = useTreeEditor({ initialTree });

return (
<div className="flex flex-col h-screen">
<Navbar />
<div className="flex flex-1 overflow-hidden">
<HeapMenu
disabled={g.isRunning}
onInsert={(v) => g.run(insertActions(g.getContext().tree, v))}
onExtract={() => g.run(extractActions(g.getContext().tree))}
onBuild={(values) => g.run(buildActions(values))}
onHeapsort={() => g.run(heapsortActions(g.getContext().tree))}
onClear={g.clear}
onSpeedChange={g.setSpeed}
/>
<div className="relative flex-1 p-6">
{g.status && (
<div className="absolute top-3 left-3 z-10 rounded-md bg-slate-800 px-3 py-1.5 text-xs font-semibold text-white shadow">
{g.status}
</div>
)}
<TreeCanvas
tree={g.tree}
layout={binaryLayout}
nodeState={g.nodeState}
edgeState={g.edgeState}
labels={g.labels}
/>
</div>
</div>
</div>
);
}
27 changes: 21 additions & 6 deletions src/app/bst/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,41 @@

import { useState } from 'react';
import Navbar from '@/components/navbar';
import { fromValues, insertActions, deleteActions, searchActions } from '@/lib/algorithms/bst';
import * as bst from '@/lib/algorithms/bst';
import * as redBlack from '@/lib/algorithms/redBlack';
import { binaryLayout } from '@/components/tree/layout';
import { useTreeEditor } from '@/components/tree/use-tree-editor';
import TreeCanvas from '@/components/tree/tree-canvas';
import TreeMenu from '@/components/tree/tree-menu';

const MODES = ['Binary Search Tree', 'Red-Black Tree'];
const SEED = [50, 30, 70, 20, 40, 60, 80];
const seedFor = (mode) => (mode === 1 ? redBlack.fromValues(SEED) : bst.fromValues(SEED));

export default function Bst() {
const [initialTree] = useState(() => fromValues([50, 30, 70, 20, 40, 60, 80]));
const [mode, setMode] = useState(0);
const [initialTree] = useState(() => seedFor(0));
const g = useTreeEditor({ initialTree });
const algo = mode === 1 ? redBlack : bst;

const onModeChange = (m) => {
if (g.isRunning) return;
setMode(m);
g.reset(seedFor(m));
};

return (
<div className="flex flex-col h-screen">
<Navbar />
<div className="flex flex-1 overflow-hidden">
<TreeMenu
title="Binary Search Tree"
title={MODES[mode]}
modes={MODES}
onModeChange={onModeChange}
disabled={g.isRunning}
onInsert={(v) => g.run(insertActions(g.getContext().tree, v))}
onDelete={(v) => g.run(deleteActions(g.getContext().tree, v))}
onSearch={(v) => g.run(searchActions(g.getContext().tree, v))}
onInsert={(v) => g.run(algo.insertActions(g.getContext().tree, v))}
onDelete={(v) => g.run(algo.deleteActions(g.getContext().tree, v))}
onSearch={(v) => g.run(algo.searchActions(g.getContext().tree, v))}
onClear={g.clear}
onSpeedChange={g.setSpeed}
/>
Expand Down
9 changes: 7 additions & 2 deletions src/app/components/algorithm-cards.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,14 @@ const algorithms = [
image: '/AlgorithmVisualizer/images/network-flow.png?height=200&width=300'
},{
id: 'bst',
title: 'Binary Search Tree',
description: "Insert, delete, and search on a BST with animated tree restructuring",
title: 'Binary Search Trees',
description: "Insert, delete, and search on a plain BST or a Red-Black tree, with animated rotations and recoloring",
image: '/AlgorithmVisualizer/images/bst.png?height=200&width=300'
},{
id: 'binary-heap',
title: 'Binary Heap',
description: "Min-heap: insert, extract-min, build-heap from a list, and heapsort with animated sift up and down",
image: '/AlgorithmVisualizer/images/binary-heap.png?height=200&width=300'
},
{
id: 'recursion-tree',
Expand Down
25 changes: 0 additions & 25 deletions src/app/recursion-tree/bst.js

This file was deleted.

2 changes: 1 addition & 1 deletion src/app/recursion-tree/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useState } from 'react';
import Navbar from '@/components/navbar';
import { getTree, recursionActions } from './fib';
import { getTree, recursionActions } from '@/lib/algorithms/recursion';
import { buchheimLayout } from '@/components/tree/layout';
import { useTreeEditor } from '@/components/tree/use-tree-editor';
import TreeCanvas from '@/components/tree/tree-canvas';
Expand Down
1 change: 1 addition & 0 deletions src/components/tree/tree-canvas.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export default function TreeCanvas({ tree, layout, nodeState = {}, edgeState = {
value={n.value}
secondary={labels[n.id]}
state={nodeState[n.id]}
color={n.color}
/>
);
})}
Expand Down
19 changes: 16 additions & 3 deletions src/components/tree/tree-menu.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useState } from 'react';
import { CustomSlider } from '@/components/custom-slider';
import { CustomSelect } from '@/components/custom-select';
import { Button } from '@/components/ui/button';
import { Plus, Minus, Search, RotateCcw } from 'lucide-react';

// Sidebar for tree visualizers: a value field + insert/delete/search actions,
// clear, and a speed slider. Callers pass the op handlers (they get the number).
// Sidebar for tree visualizers: an optional tree-type dropdown, a value field +
// insert/delete/search actions, clear, and a speed slider. Callers pass the op
// handlers (they get the number).

export default function TreeMenu({ title, disabled, onInsert, onDelete, onSearch, onClear, onSpeedChange }) {
export default function TreeMenu({ title, disabled, modes, onModeChange, onInsert, onDelete, onSearch, onClear, onSpeedChange }) {
const [value, setValue] = useState('');
const num = () => Number(value);
const valid = value.trim() !== '' && Number.isFinite(num());
Expand All @@ -15,6 +17,17 @@ export default function TreeMenu({ title, disabled, onInsert, onDelete, onSearch
<div className="w-64 bg-gray-100 p-4 space-y-6 overflow-auto">
<h2 className="text-lg font-semibold">{title}</h2>

{modes && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="h-px flex-1 bg-gray-300" />
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">Tree type</span>
<div className="h-px flex-1 bg-gray-300" />
</div>
<CustomSelect title="Type" options={modes} onChange={onModeChange} disabled={disabled} />
</div>
)}

<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="h-px flex-1 bg-gray-300" />
Expand Down
17 changes: 14 additions & 3 deletions src/components/tree/tree-node.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,21 @@ const FILL = {
remove: ['#f43f5e', '#be123c'],
};

// red/black node fills (used when a `color` is given, e.g. Red-Black trees)
const RB_FILL = {
red: ['#dc2626', '#b91c1c'],
black: ['#1f2937', '#0f172a'],
};
// when a node has a fixed red/black fill, the operation highlight is drawn as
// an outer ring instead of recoloring the fill
const RING = { current: '#f59e0b', found: '#10b981', visited: '#94a3b8', remove: '#f43f5e' };

const R = 16;

export default function TreeNode({ x, y, value, secondary, state }) {
export default function TreeNode({ x, y, value, secondary, state, color }) {
if (state === 'hidden') return null;
const [bg, border] = FILL[state] || FILL.normal;
const [bg, border] = color ? RB_FILL[color] || RB_FILL.black : FILL[state] || FILL.normal;
const ring = color ? RING[state] : null; // operation highlight for colored nodes
// shrink the primary label so longer values (e.g. recursion call labels
// like "(4,3)") still fit inside the circle
const len = String(value ?? '').length;
Expand All @@ -24,7 +34,8 @@ export default function TreeNode({ x, y, value, secondary, state }) {
return (
<g style={{ transform: `translate(${x}px, ${y}px)` }}>
<animate attributeName="opacity" from="0" to="1" dur="0.4s" begin="0s" />
<circle r={R} fill={bg} stroke={border} strokeWidth="1.5" filter="url(#treeShadow)" />
{ring && <circle r={R + 3} fill="none" stroke={ring} strokeWidth="2" />}
<circle r={R} fill={bg} stroke={border} strokeWidth="1.5" filter="url(#treeShadow)" style={{ transition: 'fill 0.3s' }} />
<text
textAnchor="middle"
dominantBaseline="central"
Expand Down
3 changes: 2 additions & 1 deletion src/components/tree/use-tree-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ export function useTreeEditor({ initialTree = null } = {}) {

const getContext = () => ({ tree: treeRef.current });
const clear = () => { if (isRunningRef.current) return; applyTree(null); clearMarks(); };
const reset = (t) => { if (isRunningRef.current) return; applyTree(t); clearMarks(); };
const setSpeed = (s) => { speedRef.current = toDelay(s); };

return { tree, nodeState, edgeState, labels, status, isRunning, run, getContext, clear, setSpeed };
return { tree, nodeState, edgeState, labels, status, isRunning, run, getContext, clear, reset, setSpeed };
}

function sleep(ms) {
Expand Down
Loading
Loading