feat(viz): dp coin change

This commit is contained in:
2025-09-01 21:29:23 +01:00
parent eb21fc7435
commit 03db344fb2
7 changed files with 1436 additions and 1 deletions

View File

@@ -14,7 +14,7 @@ import {
RelatedPatterns, RelatedPatterns,
} from "@/components/patterns"; } from "@/components/patterns";
import { PatternVisualization } from "@/components/visualization"; import { PatternVisualization } from "@/components/visualization";
import { TwoPointersVisualization, PrefixSumVisualization, LinkedListVisualization, MonotonicStackVisualization, TreeTraversalVisualization, BFSVisualization, DFSVisualization } from "@/components/visualizations-new"; import { TwoPointersVisualization, PrefixSumVisualization, LinkedListVisualization, MonotonicStackVisualization, TreeTraversalVisualization, BFSVisualization, DFSVisualization, CoinChangeVisualization } from "@/components/visualizations-new";
import { twoSumAlgorithm } from "@/content/algorithms/two-sum"; import { twoSumAlgorithm } from "@/content/algorithms/two-sum";
import { slidingWindowAlgorithm } from "@/content/algorithms/sliding-window"; import { slidingWindowAlgorithm } from "@/content/algorithms/sliding-window";
import { binarySearchAlgorithm } from "@/content/algorithms/binary-search"; import { binarySearchAlgorithm } from "@/content/algorithms/binary-search";
@@ -25,6 +25,7 @@ import { monotonicStackAlgorithm } from "@/content/algorithms/monotonic-stack";
import { treeTraversalAlgorithm } from "@/content/algorithms/tree-traversal"; import { treeTraversalAlgorithm } from "@/content/algorithms/tree-traversal";
import { bfsAlgorithm } from "@/content/algorithms/bfs"; import { bfsAlgorithm } from "@/content/algorithms/bfs";
import { dfsAlgorithm } from "@/content/algorithms/dfs"; import { dfsAlgorithm } from "@/content/algorithms/dfs";
import { coinChangeAlgorithm } from "@/content/algorithms/coin-change";
interface PageProps { interface PageProps {
params: Promise<{ slug: string }>; params: Promise<{ slug: string }>;
@@ -137,6 +138,8 @@ export default async function PatternDetailPage({ params }: PageProps) {
<BFSVisualization algorithm={bfsAlgorithm} /> <BFSVisualization algorithm={bfsAlgorithm} />
) : slug === "dfs" ? ( ) : slug === "dfs" ? (
<DFSVisualization algorithm={dfsAlgorithm} /> <DFSVisualization algorithm={dfsAlgorithm} />
) : slug === "dynamic-programming" ? (
<CoinChangeVisualization algorithm={coinChangeAlgorithm} />
) : pattern.visualization_examples && pattern.visualization_examples.length > 0 ? ( ) : pattern.visualization_examples && pattern.visualization_examples.length > 0 ? (
<Card> <Card>
<CardHeader> <CardHeader>

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 { 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>
);
}

View File

@@ -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>
);
}

View File

@@ -14,6 +14,7 @@ export { LinkedListPointer } from "./primitives/linked-list-pointer";
export { StackElement } from "./primitives/stack-element"; export { StackElement } from "./primitives/stack-element";
export { QueueElement } from "./primitives/queue-element"; export { QueueElement } from "./primitives/queue-element";
export { TreeNode } from "./primitives/tree-node"; export { TreeNode } from "./primitives/tree-node";
export { GridCell } from "./primitives/grid-cell";
// Data structures // Data structures
export { ArrayView } from "./data-structures/array-view"; 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 { StackView } from "./data-structures/stack-view";
export { QueueView } from "./data-structures/queue-view"; export { QueueView } from "./data-structures/queue-view";
export { BinaryTreeView } from "./data-structures/binary-tree-view"; export { BinaryTreeView } from "./data-structures/binary-tree-view";
export { GridView } from "./data-structures/grid-view";
// Algorithm visualizations // Algorithm visualizations
export { MonotonicStackVisualization } from "./algorithms/monotonic-stack"; export { MonotonicStackVisualization } from "./algorithms/monotonic-stack";
@@ -30,3 +32,4 @@ export { BFSVisualization } from "./algorithms/bfs";
export { DFSVisualization } from "./algorithms/dfs"; export { DFSVisualization } from "./algorithms/dfs";
export { TwoPointersVisualization } from "./algorithms/two-pointers"; export { TwoPointersVisualization } from "./algorithms/two-pointers";
export { LinkedListVisualization } from "./algorithms/linked-list"; export { LinkedListVisualization } from "./algorithms/linked-list";
export { CoinChangeVisualization } from "./algorithms/coin-change";

View File

@@ -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>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -86,6 +86,9 @@ export interface DataState {
trees?: BinaryTreeState[]; trees?: BinaryTreeState[];
// Queue support // Queue support
queues?: QueueState[]; queues?: QueueState[];
// Grid support (for DP visualizations)
grids?: GridState[];
gridPointers?: GridPointerState[];
} }
/** Single step in the visualization */ /** Single step in the visualization */
@@ -246,3 +249,34 @@ export interface QueueState {
elements: QueueElementState[]; elements: QueueElementState[];
label?: string; label?: string;
} }
// ============================================
// Grid Types (for DP visualizations)
// ============================================
/** State of a grid cell */
export interface GridCellState {
id: string;
value: number | string;
row: number;
col: number;
state: 'normal' | 'highlighted' | 'comparing' | 'computing' | 'success' | 'dimmed';
}
/** Complete grid state */
export interface GridState {
id: string;
cells: GridCellState[][]; // 2D array: cells[row][col]
colLabels?: (string | number)[];
rowLabels?: (string | number)[];
label?: string;
}
/** Pointer for grid visualization */
export interface GridPointerState {
id: string;
name: string;
row: number;
col: number;
color: 'current' | 'dependency' | 'result' | 'default';
}