feat(viz): dp coin change
This commit is contained in:
@@ -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 { GridView } from '../data-structures/grid-view';
|
||||
import { CalculationBubble } from '../primitives/calculation-bubble';
|
||||
|
||||
interface CoinChangeVisualizationProps {
|
||||
algorithm: AlgorithmDefinition;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CoinChangeVisualization({
|
||||
algorithm,
|
||||
className,
|
||||
}: CoinChangeVisualizationProps) {
|
||||
const {
|
||||
currentStep,
|
||||
currentStepIndex,
|
||||
totalSteps,
|
||||
playback,
|
||||
controls,
|
||||
currentPhase,
|
||||
progress,
|
||||
} = useVisualization(algorithm);
|
||||
|
||||
const { dataState } = currentStep;
|
||||
const coinsArray = dataState.arrays.find((a) => a.id === 'coins') ?? null;
|
||||
const dpGrid = dataState.grids?.[0] ?? null;
|
||||
const calculation = dataState.calculations[0] ?? null;
|
||||
const gridPointers = dataState.gridPointers ?? [];
|
||||
|
||||
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">
|
||||
{/* Calculation bubble */}
|
||||
<div className="flex h-8 items-center justify-center">
|
||||
<CalculationBubble calculation={calculation} />
|
||||
</div>
|
||||
|
||||
{/* Coins array */}
|
||||
{coinsArray && (
|
||||
<ArrayView
|
||||
array={coinsArray}
|
||||
pointers={[]}
|
||||
elementSize="sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* DP Table grid */}
|
||||
{dpGrid && (
|
||||
<GridView
|
||||
grid={dpGrid}
|
||||
pointers={gridPointers}
|
||||
cellSize="sm"
|
||||
showColLabels={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</VisualizationContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { GridState, GridPointerState } from "@/lib/visualizations/types";
|
||||
import { GridCell } from "../primitives/grid-cell";
|
||||
|
||||
interface GridViewProps {
|
||||
grid: GridState;
|
||||
pointers?: GridPointerState[];
|
||||
cellSize?: "sm" | "md" | "lg";
|
||||
showColLabels?: boolean;
|
||||
showRowLabels?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CELL_WIDTHS = {
|
||||
sm: 40,
|
||||
md: 56,
|
||||
lg: 72,
|
||||
} as const;
|
||||
|
||||
const CELL_GAPS = {
|
||||
sm: 2,
|
||||
md: 4,
|
||||
lg: 6,
|
||||
} as const;
|
||||
|
||||
const POINTER_COLORS = {
|
||||
current: "border-blue-500 text-blue-500",
|
||||
dependency: "border-amber-500 text-amber-500",
|
||||
result: "border-green-500 text-green-500",
|
||||
default: "border-[var(--primary)] text-[var(--primary)]",
|
||||
} as const;
|
||||
|
||||
export function GridView({
|
||||
grid,
|
||||
pointers = [],
|
||||
cellSize = "sm",
|
||||
showColLabels = true,
|
||||
showRowLabels = false,
|
||||
className,
|
||||
}: GridViewProps) {
|
||||
const cellWidth = CELL_WIDTHS[cellSize];
|
||||
const gap = CELL_GAPS[cellSize];
|
||||
|
||||
// For 1D grids (single row), flatten for easier rendering
|
||||
const is1D = grid.cells.length === 1;
|
||||
|
||||
// Calculate pointer positions for highlighting
|
||||
const pointerPositions = new Map<string, GridPointerState>();
|
||||
for (const pointer of pointers) {
|
||||
const key = `${pointer.row}-${pointer.col}`;
|
||||
pointerPositions.set(key, pointer);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col items-center", className)}>
|
||||
{grid.label && (
|
||||
<span className="mb-2 text-sm font-medium text-[var(--muted-foreground)]">
|
||||
{grid.label}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Column labels */}
|
||||
{showColLabels && grid.colLabels && (
|
||||
<div
|
||||
className="flex mb-1"
|
||||
style={{
|
||||
gap: `${gap}px`,
|
||||
marginLeft: showRowLabels ? `${cellWidth + gap}px` : 0,
|
||||
}}
|
||||
>
|
||||
{grid.colLabels.map((label, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-center justify-center text-xs text-[var(--muted-foreground)]"
|
||||
style={{ width: `${cellWidth}px` }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Grid rows */}
|
||||
{is1D ? (
|
||||
// 1D grid (single row)
|
||||
<div className="relative">
|
||||
<div className="flex" style={{ gap: `${gap}px` }}>
|
||||
{grid.cells[0].map((cell, colIdx) => {
|
||||
const pointer = pointerPositions.get(`0-${colIdx}`);
|
||||
return (
|
||||
<div key={cell.id} className="relative">
|
||||
{pointer && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute -top-6 left-1/2 -translate-x-1/2 text-xs font-medium whitespace-nowrap",
|
||||
POINTER_COLORS[pointer.color]
|
||||
)}
|
||||
>
|
||||
{pointer.name}
|
||||
</div>
|
||||
)}
|
||||
<GridCell cell={cell} size={cellSize} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// 2D grid
|
||||
<div className="flex flex-col" style={{ gap: `${gap}px` }}>
|
||||
{grid.cells.map((row, rowIdx) => (
|
||||
<div key={rowIdx} className="flex items-center" style={{ gap: `${gap}px` }}>
|
||||
{/* Row label */}
|
||||
{showRowLabels && grid.rowLabels && (
|
||||
<div
|
||||
className="flex items-center justify-center text-xs text-[var(--muted-foreground)]"
|
||||
style={{ width: `${cellWidth}px` }}
|
||||
>
|
||||
{grid.rowLabels[rowIdx]}
|
||||
</div>
|
||||
)}
|
||||
{/* Row cells */}
|
||||
{row.map((cell, colIdx) => {
|
||||
const pointer = pointerPositions.get(`${rowIdx}-${colIdx}`);
|
||||
return (
|
||||
<div key={cell.id} className="relative">
|
||||
{pointer && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute -top-5 left-1/2 -translate-x-1/2 text-xs font-medium whitespace-nowrap",
|
||||
POINTER_COLORS[pointer.color]
|
||||
)}
|
||||
>
|
||||
{pointer.name}
|
||||
</div>
|
||||
)}
|
||||
<GridCell cell={cell} size={cellSize} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export { LinkedListPointer } from "./primitives/linked-list-pointer";
|
||||
export { StackElement } from "./primitives/stack-element";
|
||||
export { QueueElement } from "./primitives/queue-element";
|
||||
export { TreeNode } from "./primitives/tree-node";
|
||||
export { GridCell } from "./primitives/grid-cell";
|
||||
|
||||
// Data structures
|
||||
export { ArrayView } from "./data-structures/array-view";
|
||||
@@ -21,6 +22,7 @@ export { LinkedListView } from "./data-structures/linked-list-view";
|
||||
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";
|
||||
|
||||
// Algorithm visualizations
|
||||
export { MonotonicStackVisualization } from "./algorithms/monotonic-stack";
|
||||
@@ -30,3 +32,4 @@ export { BFSVisualization } from "./algorithms/bfs";
|
||||
export { DFSVisualization } from "./algorithms/dfs";
|
||||
export { TwoPointersVisualization } from "./algorithms/two-pointers";
|
||||
export { LinkedListVisualization } from "./algorithms/linked-list";
|
||||
export { CoinChangeVisualization } from "./algorithms/coin-change";
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { GridCellState } from "@/lib/visualizations/types";
|
||||
|
||||
interface GridCellProps {
|
||||
cell: GridCellState;
|
||||
size?: "sm" | "md" | "lg";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SIZE_CLASSES = {
|
||||
sm: "w-10 h-10 text-sm",
|
||||
md: "w-14 h-14 text-base",
|
||||
lg: "w-18 h-18 text-lg",
|
||||
} as const;
|
||||
|
||||
const STATE_CLASSES = {
|
||||
normal: "bg-[var(--muted)] border-[var(--border)] text-[var(--foreground)]",
|
||||
highlighted: "bg-[var(--primary)]/20 border-[var(--primary)] text-[var(--primary)]",
|
||||
dimmed: "bg-[var(--muted)]/50 border-[var(--border)]/50 text-[var(--muted-foreground)] opacity-30",
|
||||
success: "bg-green-500/20 border-green-500 text-green-500",
|
||||
comparing: "bg-amber-500/20 border-amber-500 text-amber-500",
|
||||
computing: "bg-blue-500/20 border-blue-500 text-blue-500",
|
||||
} as const;
|
||||
|
||||
export function GridCell({
|
||||
cell,
|
||||
size = "md",
|
||||
className,
|
||||
}: GridCellProps) {
|
||||
const isComputing = cell.state === "computing";
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
initial={false}
|
||||
animate={{
|
||||
scale: cell.state === "highlighted" || cell.state === "computing" ? 1.05 : 1,
|
||||
}}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-lg border-2 font-mono font-medium transition-colors duration-200",
|
||||
SIZE_CLASSES[size],
|
||||
STATE_CLASSES[cell.state],
|
||||
isComputing && "animate-pulse",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{cell.value === Infinity ? "∞" : cell.value}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user