feat(viz): heap pattern with kth largest
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user