linked list visualisations
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
'use client';
|
||||
|
||||
import { useVisualization } from '@/lib/visualizations/use-visualization';
|
||||
import type { AlgorithmDefinition, LinkedListState, LinkedListPointerState } from '@/lib/visualizations/types';
|
||||
import { VisualizationContainer } from '../core/visualization-container';
|
||||
import { LinkedListView } from '../data-structures/linked-list-view';
|
||||
|
||||
interface LinkedListVisualizationProps {
|
||||
algorithm: AlgorithmDefinition;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function LinkedListVisualization({
|
||||
algorithm,
|
||||
className,
|
||||
}: LinkedListVisualizationProps) {
|
||||
const {
|
||||
currentStep,
|
||||
currentStepIndex,
|
||||
totalSteps,
|
||||
playback,
|
||||
controls,
|
||||
currentPhase,
|
||||
progress,
|
||||
} = useVisualization(algorithm);
|
||||
|
||||
const { dataState } = currentStep;
|
||||
|
||||
// Extract linked list and pointers from dataState
|
||||
// We store linkedLists in a custom field, falling back to empty
|
||||
const linkedList: LinkedListState = (dataState as { linkedLists?: LinkedListState[] }).linkedLists?.[0] ?? {
|
||||
id: 'empty',
|
||||
nodes: [],
|
||||
};
|
||||
|
||||
const linkedListPointers: LinkedListPointerState[] =
|
||||
(dataState as { linkedListPointers?: LinkedListPointerState[] }).linkedListPointers ?? [];
|
||||
|
||||
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 justify-center">
|
||||
<LinkedListView
|
||||
list={linkedList}
|
||||
pointers={linkedListPointers}
|
||||
/>
|
||||
</div>
|
||||
</VisualizationContainer>
|
||||
);
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export function VisualizationContainer({
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex min-h-80 items-center justify-center rounded-lg border border-border bg-surface px-8 py-4 lg:h-auto"
|
||||
className="flex min-h-80 min-w-0 items-center justify-center overflow-x-auto rounded-lg border border-border bg-surface px-8 py-4 lg:h-auto"
|
||||
role="img"
|
||||
aria-label="Algorithm visualization"
|
||||
>
|
||||
|
||||
@@ -39,6 +39,30 @@ export function ArrayView({
|
||||
// Calculate total array width for proper pointer alignment
|
||||
const totalWidth = array.elements.length * elementWidth + (array.elements.length - 1) * gap;
|
||||
|
||||
// Check if any pointers are too close together (within 1 index) - hide values to save space
|
||||
const hasClosePointers = pointers.length > 1 && pointers.some((p, i) => {
|
||||
return pointers.some((other, j) => i !== j && Math.abs(p.index - other.index) <= 1);
|
||||
});
|
||||
|
||||
// Group pointers by index to calculate offsets for overlapping pointers at same position
|
||||
const pointersByIndex = pointers.reduce((acc, pointer) => {
|
||||
const idx = pointer.index;
|
||||
if (!acc[idx]) acc[idx] = [];
|
||||
acc[idx].push(pointer);
|
||||
return acc;
|
||||
}, {} as Record<number, PointerState[]>);
|
||||
|
||||
// Calculate horizontal offset for pointers at the same index
|
||||
const getPointerOffset = (pointer: PointerState): number => {
|
||||
const pointersAtIndex = pointersByIndex[pointer.index] || [];
|
||||
if (pointersAtIndex.length <= 1) return 0;
|
||||
|
||||
const pointerIdx = pointersAtIndex.findIndex(p => p.id === pointer.id);
|
||||
const totalPointers = pointersAtIndex.length;
|
||||
const spacing = 36; // px between overlapping pointers
|
||||
return (pointerIdx - (totalPointers - 1) / 2) * spacing;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col items-center", className)}>
|
||||
{array.label && (
|
||||
@@ -58,6 +82,8 @@ export function ArrayView({
|
||||
elementWidth={elementWidth}
|
||||
gap={gap}
|
||||
value={array.elements[pointer.index]?.value}
|
||||
hideValue={hasClosePointers}
|
||||
horizontalOffset={getPointerOffset(pointer)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { LinkedListState, LinkedListPointerState } from '@/lib/visualizations/types';
|
||||
import { LinkedListNode } from '../primitives/linked-list-node';
|
||||
import { LinkedListPointer } from '../primitives/linked-list-pointer';
|
||||
|
||||
interface LinkedListViewProps {
|
||||
list: LinkedListState;
|
||||
pointers: LinkedListPointerState[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const NODE_WIDTH = 40; // w-10 = 40px
|
||||
const ARROW_WIDTH = 22; // 16px line + 6px arrow head
|
||||
|
||||
export function LinkedListView({
|
||||
list,
|
||||
pointers,
|
||||
className,
|
||||
}: LinkedListViewProps) {
|
||||
// Calculate total width for proper pointer alignment
|
||||
const totalWidth = list.nodes.length * NODE_WIDTH + (list.nodes.length - 1) * ARROW_WIDTH;
|
||||
|
||||
// Group pointers by nodeIndex to calculate offsets for overlapping pointers
|
||||
const pointersByIndex = pointers.reduce((acc, pointer) => {
|
||||
const idx = pointer.nodeIndex;
|
||||
if (!acc[idx]) acc[idx] = [];
|
||||
acc[idx].push(pointer);
|
||||
return acc;
|
||||
}, {} as Record<number, LinkedListPointerState[]>);
|
||||
|
||||
// Calculate horizontal offset for each pointer
|
||||
const getPointerOffset = (pointer: LinkedListPointerState): number => {
|
||||
const pointersAtIndex = pointersByIndex[pointer.nodeIndex] || [];
|
||||
if (pointersAtIndex.length <= 1) return 0;
|
||||
|
||||
const pointerIdx = pointersAtIndex.findIndex(p => p.id === pointer.id);
|
||||
const totalPointers = pointersAtIndex.length;
|
||||
const spacing = 28; // px between overlapping pointers
|
||||
// Center the group: offset from center based on position
|
||||
return (pointerIdx - (totalPointers - 1) / 2) * spacing;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center', className)}>
|
||||
{list.label && (
|
||||
<span className="mb-2 text-sm font-medium text-foreground-muted">
|
||||
{list.label}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="relative" style={{ width: totalWidth }}>
|
||||
{/* Pointers row */}
|
||||
<div className="relative mb-1 h-8">
|
||||
{pointers.map((pointer) => (
|
||||
<LinkedListPointer
|
||||
key={pointer.id}
|
||||
pointer={pointer}
|
||||
nodeWidth={NODE_WIDTH}
|
||||
arrowWidth={ARROW_WIDTH}
|
||||
horizontalOffset={getPointerOffset(pointer)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Linked list nodes */}
|
||||
<div className="flex items-center">
|
||||
{list.nodes.map((node, index) => (
|
||||
<LinkedListNode
|
||||
key={node.id}
|
||||
node={node}
|
||||
isLast={index === list.nodes.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,10 +9,14 @@ export { VariableInspector } from "./core/variable-inspector";
|
||||
export { ArrayElement } from "./primitives/array-element";
|
||||
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";
|
||||
|
||||
// Data structures
|
||||
export { ArrayView } from "./data-structures/array-view";
|
||||
export { LinkedListView } from "./data-structures/linked-list-view";
|
||||
|
||||
// Algorithm visualizations
|
||||
export { PrefixSumVisualization } from "./algorithms/prefix-sum";
|
||||
export { TwoPointersVisualization } from "./algorithms/two-pointers";
|
||||
export { LinkedListVisualization } from "./algorithms/linked-list";
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { LinkedListNodeState } from '@/lib/visualizations/types';
|
||||
|
||||
interface LinkedListNodeProps {
|
||||
node: LinkedListNodeState;
|
||||
isLast?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const STATE_CLASSES = {
|
||||
normal: 'bg-surface-variant border-border text-foreground',
|
||||
highlighted: 'bg-primary/20 border-primary text-primary',
|
||||
dimmed: 'bg-surface-variant/50 border-border/50 text-foreground-muted opacity-40',
|
||||
success: 'bg-viz-success/20 border-viz-success text-viz-success',
|
||||
comparing: 'bg-viz-compare/20 border-viz-compare text-viz-compare',
|
||||
} as const;
|
||||
|
||||
export function LinkedListNode({
|
||||
node,
|
||||
isLast = false,
|
||||
className,
|
||||
}: LinkedListNodeProps) {
|
||||
if (node.isNull) {
|
||||
return (
|
||||
<div className={cn('flex items-center', className)}>
|
||||
<div className="flex h-10 items-center justify-center px-3 font-mono text-sm text-foreground-muted">
|
||||
null
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center', className)}>
|
||||
<motion.div
|
||||
layout
|
||||
initial={false}
|
||||
animate={{
|
||||
scale: node.state === 'highlighted' ? 1.05 : 1,
|
||||
}}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={cn(
|
||||
'flex h-10 w-10 items-center justify-center rounded-lg border-2 font-mono font-medium transition-colors duration-200',
|
||||
STATE_CLASSES[node.state]
|
||||
)}
|
||||
>
|
||||
{node.value}
|
||||
</motion.div>
|
||||
{!isLast && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex items-center"
|
||||
>
|
||||
<div className="h-0.5 w-4 bg-border" />
|
||||
<div className="h-0 w-0 border-y-4 border-l-6 border-y-transparent border-l-border" />
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { LinkedListPointerState } from '@/lib/visualizations/types';
|
||||
|
||||
interface LinkedListPointerProps {
|
||||
pointer: LinkedListPointerState;
|
||||
nodeWidth: number;
|
||||
arrowWidth: number;
|
||||
/** Horizontal offset when multiple pointers at same position */
|
||||
horizontalOffset?: number;
|
||||
}
|
||||
|
||||
const COLOR_CLASSES = {
|
||||
slow: 'text-viz-pointer-left',
|
||||
fast: 'text-viz-pointer-right',
|
||||
prev: 'text-viz-pointer-mid',
|
||||
curr: 'text-viz-pointer-left',
|
||||
next: 'text-viz-pointer-right',
|
||||
default: 'text-viz-pointer-default',
|
||||
} as const;
|
||||
|
||||
export function LinkedListPointer({
|
||||
pointer,
|
||||
nodeWidth,
|
||||
arrowWidth,
|
||||
horizontalOffset = 0,
|
||||
}: LinkedListPointerProps) {
|
||||
// Calculate x position based on node index, with offset for overlapping pointers
|
||||
const x = pointer.nodeIndex * (nodeWidth + arrowWidth) + nodeWidth / 2 + horizontalOffset;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ x: x - 12 }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
}}
|
||||
className={cn(
|
||||
'absolute top-0 flex flex-col items-center',
|
||||
COLOR_CLASSES[pointer.color]
|
||||
)}
|
||||
>
|
||||
<span className="text-xs font-medium">{pointer.name}</span>
|
||||
<svg
|
||||
width="16"
|
||||
height="12"
|
||||
viewBox="0 0 16 12"
|
||||
fill="currentColor"
|
||||
className="mt-0.5"
|
||||
>
|
||||
<path d="M8 12L0 0h16L8 12z" />
|
||||
</svg>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,10 @@ interface PointerProps {
|
||||
elementWidth: number;
|
||||
gap: number;
|
||||
value?: number;
|
||||
/** Hide the value to save space when pointers are close */
|
||||
hideValue?: boolean;
|
||||
/** Horizontal offset when multiple pointers at same position */
|
||||
horizontalOffset?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -24,10 +28,12 @@ export function Pointer({
|
||||
elementWidth,
|
||||
gap,
|
||||
value,
|
||||
hideValue = false,
|
||||
horizontalOffset = 0,
|
||||
className,
|
||||
}: PointerProps) {
|
||||
// Calculate center of element: index * (width + gap) + width / 2
|
||||
const offset = pointer.index * (elementWidth + gap) + elementWidth / 2;
|
||||
// Calculate center of element: index * (width + gap) + width / 2, plus any offset for overlapping
|
||||
const offset = pointer.index * (elementWidth + gap) + elementWidth / 2 + horizontalOffset;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -40,7 +46,7 @@ export function Pointer({
|
||||
}}
|
||||
className={cn("absolute top-0 h-full", className)}
|
||||
>
|
||||
{/* Label - centered above the point */}
|
||||
{/* Label - centered above the arrow */}
|
||||
<span
|
||||
className={cn(
|
||||
"absolute left-0 top-0 -translate-x-1/2 whitespace-nowrap rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
@@ -48,7 +54,7 @@ export function Pointer({
|
||||
)}
|
||||
>
|
||||
{pointer.name}
|
||||
{pointer.showValue && value !== undefined && (
|
||||
{!hideValue && pointer.showValue && value !== undefined && (
|
||||
<span className="ml-1 font-mono">= {value}</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user