feat(viz): heap pattern with kth largest
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { useVisualization } from '@/lib/visualizations/use-visualization';
|
||||
import type { AlgorithmDefinition } from '@/lib/visualizations/types';
|
||||
import { VisualizationContainer } from '../core/visualization-container';
|
||||
import { HeapView } from '../data-structures/heap-view';
|
||||
import { ArrayView } from '../data-structures/array-view';
|
||||
|
||||
interface HeapVisualizationProps {
|
||||
algorithm: AlgorithmDefinition;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function HeapVisualization({
|
||||
algorithm,
|
||||
className,
|
||||
}: HeapVisualizationProps) {
|
||||
const {
|
||||
currentStep,
|
||||
currentStepIndex,
|
||||
totalSteps,
|
||||
playback,
|
||||
controls,
|
||||
currentPhase,
|
||||
progress,
|
||||
} = useVisualization(algorithm);
|
||||
|
||||
const { dataState } = currentStep;
|
||||
const heap = dataState.heaps?.[0] ?? null;
|
||||
const inputArray = dataState.arrays?.[0] ?? null;
|
||||
|
||||
return (
|
||||
<VisualizationContainer
|
||||
title={algorithm.title}
|
||||
pattern={algorithm.pattern}
|
||||
code={algorithm.code}
|
||||
currentLine={currentStep.codeLine}
|
||||
highlightLines={currentStep.codeHighlightLines}
|
||||
explanation={currentStep.explanation}
|
||||
decision={currentStep.decision}
|
||||
phase={currentPhase}
|
||||
variables={dataState.variables}
|
||||
currentStepIndex={currentStepIndex}
|
||||
totalSteps={totalSteps}
|
||||
isPlaying={playback.isPlaying}
|
||||
speed={playback.speed}
|
||||
controls={controls}
|
||||
progress={progress}
|
||||
className={className}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
{/* Input array */}
|
||||
{inputArray && (
|
||||
<ArrayView
|
||||
array={inputArray}
|
||||
pointers={dataState.pointers}
|
||||
elementSize="sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Heap visualization */}
|
||||
{heap && (
|
||||
<HeapView
|
||||
heap={heap}
|
||||
showArrayView={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</VisualizationContainer>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,8 @@ export { StackElement } from "./primitives/stack-element";
|
||||
export { QueueElement } from "./primitives/queue-element";
|
||||
export { TreeNode } from "./primitives/tree-node";
|
||||
export { GridCell } from "./primitives/grid-cell";
|
||||
export { DecisionNode } from "./primitives/decision-node";
|
||||
export { HeapNode } from "./primitives/heap-node";
|
||||
|
||||
// Data structures
|
||||
export { ArrayView } from "./data-structures/array-view";
|
||||
@@ -23,6 +25,8 @@ export { StackView } from "./data-structures/stack-view";
|
||||
export { QueueView } from "./data-structures/queue-view";
|
||||
export { BinaryTreeView } from "./data-structures/binary-tree-view";
|
||||
export { GridView } from "./data-structures/grid-view";
|
||||
export { DecisionTreeView } from "./data-structures/decision-tree-view";
|
||||
export { HeapView } from "./data-structures/heap-view";
|
||||
|
||||
// Algorithm visualizations
|
||||
export { MonotonicStackVisualization } from "./algorithms/monotonic-stack";
|
||||
@@ -33,3 +37,5 @@ export { DFSVisualization } from "./algorithms/dfs";
|
||||
export { TwoPointersVisualization } from "./algorithms/two-pointers";
|
||||
export { LinkedListVisualization } from "./algorithms/linked-list";
|
||||
export { CoinChangeVisualization } from "./algorithms/coin-change";
|
||||
export { BacktrackingVisualization } from "./algorithms/backtracking";
|
||||
export { HeapVisualization } from "./algorithms/heap";
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { HeapNodeState } from '@/lib/visualizations/types';
|
||||
|
||||
interface HeapNodeProps {
|
||||
node: HeapNodeState;
|
||||
x: number;
|
||||
y: number;
|
||||
radius?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const STATE_CLASSES = {
|
||||
normal: 'fill-[var(--surface-variant)] stroke-[var(--border)]',
|
||||
comparing: 'fill-[var(--info)]/20 stroke-[var(--info)]',
|
||||
swapping: 'fill-[var(--warning)]/20 stroke-[var(--warning)]',
|
||||
settled: 'fill-[var(--success)]/20 stroke-[var(--success)]',
|
||||
highlighted: 'fill-[var(--primary)]/30 stroke-[var(--primary)]',
|
||||
removing: 'fill-[var(--destructive)]/20 stroke-[var(--destructive)] opacity-50',
|
||||
} as const;
|
||||
|
||||
const TEXT_CLASSES = {
|
||||
normal: 'fill-[var(--foreground)]',
|
||||
comparing: 'fill-[var(--info)]',
|
||||
swapping: 'fill-[var(--warning)]',
|
||||
settled: 'fill-[var(--success)]',
|
||||
highlighted: 'fill-[var(--primary)]',
|
||||
removing: 'fill-[var(--destructive)] opacity-50',
|
||||
} as const;
|
||||
|
||||
export function HeapNode({
|
||||
node,
|
||||
x,
|
||||
y,
|
||||
radius = 24,
|
||||
className,
|
||||
}: HeapNodeProps) {
|
||||
const isActive = node.state === 'comparing' || node.state === 'swapping' || node.state === 'highlighted';
|
||||
const isRemoving = node.state === 'removing';
|
||||
|
||||
return (
|
||||
<motion.g
|
||||
initial={false}
|
||||
animate={{
|
||||
scale: isActive ? 1.1 : isRemoving ? 0.8 : 1,
|
||||
opacity: isRemoving ? 0.5 : 1,
|
||||
}}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
}}
|
||||
style={{ transformOrigin: `${x}px ${y}px` }}
|
||||
className={cn(className)}
|
||||
>
|
||||
<motion.circle
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={radius}
|
||||
strokeWidth={2}
|
||||
className={cn(
|
||||
'transition-colors duration-200',
|
||||
STATE_CLASSES[node.state]
|
||||
)}
|
||||
initial={false}
|
||||
animate={{
|
||||
filter: isActive ? 'drop-shadow(0 0 8px var(--primary))' : 'none',
|
||||
}}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
className={cn(
|
||||
'pointer-events-none select-none font-mono text-sm font-medium',
|
||||
TEXT_CLASSES[node.state]
|
||||
)}
|
||||
>
|
||||
{node.value}
|
||||
</text>
|
||||
</motion.g>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user