feat(viz): monotonic stack viz

This commit is contained in:
2025-08-24 17:32:27 +01:00
parent e7ba79e49c
commit 45105e0b77
6 changed files with 1071 additions and 1 deletions

View File

@@ -0,0 +1,81 @@
'use client';
import { useVisualization } from '@/lib/visualizations/use-visualization';
import type { AlgorithmDefinition } from '@/lib/visualizations/types';
import { VisualizationContainer } from '../core/visualization-container';
import { ArrayView } from '../data-structures/array-view';
import { StackView } from '../data-structures/stack-view';
interface MonotonicStackVisualizationProps {
algorithm: AlgorithmDefinition;
className?: string;
}
export function MonotonicStackVisualization({
algorithm,
className,
}: MonotonicStackVisualizationProps) {
const {
currentStep,
currentStepIndex,
totalSteps,
playback,
controls,
currentPhase,
progress,
} = useVisualization(algorithm);
const { dataState } = currentStep;
// Input array with i pointer
const inputArray = dataState.arrays.find((a) => a.id === 'input');
// Result array
const resultArray = dataState.arrays.find((a) => a.id === 'result');
// Stack
const stack = dataState.stacks?.[0];
// Filter pointers for the input array (only show 'i' pointer on input)
const inputPointers = dataState.pointers.filter((p) => p.id === 'i');
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-4">
{/* Input array with i pointer */}
{inputArray && (
<ArrayView
array={inputArray}
pointers={inputPointers}
elementSize="sm"
/>
)}
<div className="flex items-end gap-6">
{/* Stack (left side) */}
{stack && <StackView stack={stack} />}
{/* Result array (right side) */}
{resultArray && (
<ArrayView array={resultArray} pointers={[]} elementSize="sm" />
)}
</div>
</div>
</VisualizationContainer>
);
}

View File

@@ -0,0 +1,48 @@
'use client';
import { AnimatePresence } from 'framer-motion';
import { cn } from '@/lib/utils';
import type { StackState } from '@/lib/visualizations/types';
import { StackElement } from '../primitives/stack-element';
interface StackViewProps {
stack: StackState;
className?: string;
}
export function StackView({ stack, className }: StackViewProps) {
const hasElements = stack.elements.length > 0;
return (
<div className={cn('flex flex-col items-center', className)}>
{stack.label && (
<span className="mb-2 whitespace-nowrap text-sm font-medium text-[var(--muted-foreground)]">
{stack.label}
</span>
)}
<div className="relative flex min-h-[88px] flex-col-reverse items-center gap-1">
{/* Empty state */}
{!hasElements && (
<div className="flex h-10 w-10 items-center justify-center rounded-lg border-2 border-dashed border-[var(--border)] text-xs text-[var(--muted-foreground)]">
empty
</div>
)}
{/* Stack elements - flex-col-reverse makes first element appear at bottom */}
<AnimatePresence mode="popLayout">
{stack.elements.map((element, index) => (
<StackElement
key={element.id}
element={element}
isTop={index === stack.elements.length - 1}
/>
))}
</AnimatePresence>
</div>
{/* Bottom indicator */}
<div className="mt-1 h-1 w-12 rounded-full bg-[var(--border)]" />
</div>
);
}

View File

@@ -11,12 +11,15 @@ export { Pointer } from "./primitives/pointer";
export { CalculationBubble } from "./primitives/calculation-bubble";
export { LinkedListNode } from "./primitives/linked-list-node";
export { LinkedListPointer } from "./primitives/linked-list-pointer";
export { StackElement } from "./primitives/stack-element";
// Data structures
export { ArrayView } from "./data-structures/array-view";
export { LinkedListView } from "./data-structures/linked-list-view";
export { StackView } from "./data-structures/stack-view";
// Algorithm visualizations
export { MonotonicStackVisualization } from "./algorithms/monotonic-stack";
export { PrefixSumVisualization } from "./algorithms/prefix-sum";
export { TwoPointersVisualization } from "./algorithms/two-pointers";
export { LinkedListVisualization } from "./algorithms/linked-list";

View File

@@ -0,0 +1,54 @@
'use client';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
import type { StackElementState } from '@/lib/visualizations/types';
interface StackElementProps {
element: StackElementState;
isTop?: boolean;
className?: string;
}
const STATE_CLASSES = {
normal: 'bg-[var(--surface-variant)] border-[var(--border)] text-[var(--foreground)]',
highlighted: 'bg-[var(--primary)]/20 border-[var(--primary)] text-[var(--primary)]',
popped: 'bg-[var(--error)]/20 border-[var(--error)] text-[var(--error)]',
pushing: 'bg-[var(--success)]/20 border-[var(--success)] text-[var(--success)]',
} as const;
export function StackElement({
element,
isTop = false,
className,
}: StackElementProps) {
return (
<motion.div
layout
initial={{ opacity: 0, y: -20, scale: 0.8 }}
animate={{
opacity: element.state === 'popped' ? 0 : 1,
y: 0,
scale: element.state === 'highlighted' ? 1.05 : 1,
}}
exit={{ opacity: 0, y: -20, scale: 0.8 }}
transition={{
type: 'spring',
stiffness: 300,
damping: 30,
}}
className={cn(
'relative flex h-10 w-10 items-center justify-center rounded-lg border-2 font-mono font-medium transition-colors duration-200',
STATE_CLASSES[element.state],
className
)}
>
{element.value}
{isTop && (
<span className="absolute -right-8 text-[10px] font-medium text-[var(--muted-foreground)]">
TOP
</span>
)}
</motion.div>
);
}