196 lines
6.0 KiB
TypeScript
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>
|
|
);
|
|
}
|