Files
codetutor/frontend/src/components/visualizations-new/data-structures/heap-view.tsx

196 lines
6.0 KiB
TypeScript

'use client';
import { useMemo } from 'react';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
import type { HeapState, HeapNodeState } from '@/lib/visualizations/types';
import { HeapNode } from '../primitives/heap-node';
interface HeapViewProps {
heap: HeapState;
className?: string;
showArrayView?: boolean;
}
const NODE_RADIUS = 24;
const LEVEL_HEIGHT = 72;
const MIN_NODE_SPACING = 60;
const SVG_PADDING = 32;
const ARRAY_HEIGHT = 60;
interface NodePosition {
node: HeapNodeState;
x: number;
y: number;
parentX?: number;
parentY?: number;
}
function calculateHeapLayout(elements: HeapNodeState[], totalWidth: number): NodePosition[] {
const positions: NodePosition[] = [];
if (elements.length === 0) return positions;
for (let i = 0; i < elements.length; i++) {
const level = Math.floor(Math.log2(i + 1));
const posInLevel = i - (Math.pow(2, level) - 1);
const nodesInLevel = Math.pow(2, level);
const levelWidth = totalWidth / nodesInLevel;
const x = levelWidth * (posInLevel + 0.5);
const y = SVG_PADDING + level * LEVEL_HEIGHT + NODE_RADIUS;
let parentX: number | undefined;
let parentY: number | undefined;
if (i > 0) {
const parentIndex = Math.floor((i - 1) / 2);
const parentLevel = Math.floor(Math.log2(parentIndex + 1));
const parentPosInLevel = parentIndex - (Math.pow(2, parentLevel) - 1);
const parentNodesInLevel = Math.pow(2, parentLevel);
const parentLevelWidth = totalWidth / parentNodesInLevel;
parentX = parentLevelWidth * (parentPosInLevel + 0.5);
parentY = SVG_PADDING + parentLevel * LEVEL_HEIGHT + NODE_RADIUS;
}
positions.push({
node: elements[i],
x,
y,
parentX,
parentY,
});
}
return positions;
}
export function HeapView({
heap,
className,
showArrayView = true,
}: HeapViewProps) {
const { positions, svgWidth, svgHeight, treeHeight } = useMemo(() => {
if (heap.elements.length === 0) {
return { positions: [], svgWidth: 200, svgHeight: 100, treeHeight: 50 };
}
const depth = Math.floor(Math.log2(heap.elements.length)) + 1;
const maxNodesAtBottom = Math.pow(2, depth - 1);
const width = Math.max(
maxNodesAtBottom * MIN_NODE_SPACING + SVG_PADDING * 2,
200
);
const treeH = depth * LEVEL_HEIGHT + SVG_PADDING;
const height = treeH + (showArrayView ? ARRAY_HEIGHT : SVG_PADDING);
const pos = calculateHeapLayout(heap.elements, width);
return { positions: pos, svgWidth: width, svgHeight: height, treeHeight: treeH };
}, [heap.elements, showArrayView]);
const heapTypeLabel = heap.heapType === 'min' ? 'Min-Heap' : 'Max-Heap';
const sizeLabel = heap.maxSize ? ` (size ≤ ${heap.maxSize})` : '';
return (
<div className={cn('flex flex-col items-center', className)}>
{(heap.label || true) && (
<span className="mb-2 text-sm font-medium text-[var(--muted-foreground)]">
{heap.label || heapTypeLabel}{sizeLabel}
</span>
)}
<svg
width={svgWidth}
height={svgHeight}
viewBox={`0 0 ${svgWidth} ${svgHeight}`}
className="overflow-visible"
>
{/* Draw edges first (behind nodes) */}
{positions.map(({ node, x, y, parentX, parentY }) =>
parentX !== undefined && parentY !== undefined ? (
<motion.line
key={`edge-${node.id}`}
x1={parentX}
y1={parentY + NODE_RADIUS}
x2={x}
y2={y - NODE_RADIUS}
className="stroke-[var(--border)]"
strokeWidth={2}
initial={false}
animate={{
opacity: node.state === 'removing' ? 0.3 : 1,
}}
transition={{ duration: 0.2 }}
/>
) : null
)}
{/* Draw nodes */}
{positions.map(({ node, x, y }) => (
<HeapNode
key={node.id}
node={node}
x={x}
y={y}
radius={NODE_RADIUS}
/>
))}
{/* Root indicator (kth largest for min-heap top-k) */}
{positions.length > 0 && positions[0].node.state === 'highlighted' && (
<g>
<motion.polygon
points={`${positions[0].x},${positions[0].y - NODE_RADIUS - 8} ${positions[0].x - 6},${positions[0].y - NODE_RADIUS - 18} ${positions[0].x + 6},${positions[0].y - NODE_RADIUS - 18}`}
className="fill-[var(--primary)]"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
/>
<motion.text
x={positions[0].x}
y={positions[0].y - NODE_RADIUS - 24}
textAnchor="middle"
className="fill-[var(--primary)] text-xs font-medium"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
root
</motion.text>
</g>
)}
{/* Array representation below tree */}
{showArrayView && heap.elements.length > 0 && (
<g transform={`translate(0, ${treeHeight})`}>
<text
x={SVG_PADDING}
y={12}
className="fill-[var(--muted-foreground)] text-xs"
>
Array: [{heap.elements.map(e => e.value).join(', ')}]
</text>
{/* Index markers */}
<text
x={SVG_PADDING}
y={28}
className="fill-[var(--muted-foreground)] text-[10px] opacity-70"
>
idx: {heap.elements.map((_, i) => i).join(' ')}
</text>
</g>
)}
{/* Empty state indicator */}
{heap.elements.length === 0 && (
<text
x={svgWidth / 2}
y={svgHeight / 2}
textAnchor="middle"
className="fill-[var(--muted-foreground)] text-sm italic"
>
(empty)
</text>
)}
</svg>
</div>
);
}