Files
codetutor/frontend/src/components/visualization/visualizers/array-visualizer.tsx

264 lines
7.7 KiB
TypeScript

"use client";
import { useMemo } from "react";
import type { ArrayState, ElementState } from "@/types/visualization";
interface ArrayVisualizerProps {
data: ArrayState;
name?: string;
}
const CELL_WIDTH = 56;
const CELL_HEIGHT = 56;
const CELL_GAP = 4;
const INDEX_AREA_HEIGHT = 20;
const POINTER_HEIGHT = 24;
const ANNOTATION_HEIGHT = 20;
const SVG_PADDING = 16;
function getStateColor(state: ElementState): string {
switch (state) {
case "active":
return "var(--viz-active)";
case "comparing":
return "var(--viz-comparing)";
case "found":
return "var(--viz-found)";
case "visited":
return "var(--viz-visited)";
case "swapping":
return "var(--viz-swapping)";
default:
return "var(--viz-default)";
}
}
function getTextColor(state: ElementState): string {
switch (state) {
case "active":
case "found":
case "swapping":
return "white";
case "comparing":
return "var(--foreground)";
case "visited":
return "var(--muted-foreground)";
default:
return "var(--foreground)";
}
}
export function ArrayVisualizer({ data, name }: ArrayVisualizerProps) {
const { values, pointers = {} } = data;
const dimensions = useMemo(() => {
const hasPointers = Object.keys(pointers).length > 0;
const hasAnnotations = values.some(
(v) => v.annotations && v.annotations.length > 0
);
const contentWidth =
values.length * CELL_WIDTH + (values.length - 1) * CELL_GAP;
const width = contentWidth + SVG_PADDING * 2;
let height = CELL_HEIGHT + INDEX_AREA_HEIGHT + SVG_PADDING * 2;
if (hasPointers) height += POINTER_HEIGHT + 16;
if (hasAnnotations) height += ANNOTATION_HEIGHT;
return {
width,
height,
contentWidth,
hasPointers,
hasAnnotations,
startX: SVG_PADDING,
startY: hasAnnotations
? SVG_PADDING + ANNOTATION_HEIGHT
: SVG_PADDING + 8,
};
}, [values, pointers]);
// Group pointers by position for stacking
const pointersByPosition = useMemo(() => {
const grouped: Record<number, string[]> = {};
for (const [label, index] of Object.entries(pointers)) {
if (!grouped[index]) {
grouped[index] = [];
}
grouped[index].push(label);
}
return grouped;
}, [pointers]);
return (
<div className="flex flex-col gap-2">
{name && (
<span className="text-sm font-medium text-muted-foreground">
{name}
</span>
)}
<div className="overflow-x-auto">
<svg
width={dimensions.width}
height={dimensions.height}
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
className="viz-array"
role="img"
aria-label={`Array visualization with ${values.length} elements`}
>
{/* Array cells */}
<g>
{values.map((element, index) => {
const x = dimensions.startX + index * (CELL_WIDTH + CELL_GAP);
const y = dimensions.startY;
return (
<g
key={index}
className="viz-element"
data-state={element.state}
>
{/* Cell background */}
<rect
x={x}
y={y}
width={CELL_WIDTH}
height={CELL_HEIGHT}
rx={6}
fill={getStateColor(element.state)}
className="viz-cell-bg"
/>
{/* Cell border */}
<rect
x={x}
y={y}
width={CELL_WIDTH}
height={CELL_HEIGHT}
rx={6}
fill="none"
stroke="var(--border)"
strokeWidth={1}
/>
{/* Value */}
<text
x={x + CELL_WIDTH / 2}
y={y + CELL_HEIGHT / 2}
textAnchor="middle"
dominantBaseline="middle"
className="viz-value"
fill={getTextColor(element.state)}
fontSize={18}
fontWeight={500}
fontFamily="var(--font-mono, monospace)"
>
{String(element.value)}
</text>
{/* Annotations (above cell) */}
{element.annotations && element.annotations.length > 0 && (
<text
x={x + CELL_WIDTH / 2}
y={y - 6}
textAnchor="middle"
className="viz-annotation"
fill="var(--primary)"
fontSize={12}
fontWeight={600}
>
{element.annotations.join(", ")}
</text>
)}
</g>
);
})}
</g>
{/* Pointers (below cells, before indices) */}
{dimensions.hasPointers && (
<g className="viz-pointers">
{Object.entries(pointersByPosition).map(([posStr, labels]) => {
const position = parseInt(posStr, 10);
if (position < 0 || position >= values.length) return null;
const x =
dimensions.startX +
position * (CELL_WIDTH + CELL_GAP) +
CELL_WIDTH / 2;
const y =
dimensions.startY + CELL_HEIGHT + INDEX_AREA_HEIGHT + POINTER_HEIGHT;
return (
<g key={posStr} className="viz-pointer">
{/* Arrow */}
<line
x1={x}
y1={y}
x2={x}
y2={y - 10}
stroke="var(--primary)"
strokeWidth={2}
markerEnd="url(#arrowhead)"
/>
{/* Label(s) */}
<text
x={x}
y={y + 12}
textAnchor="middle"
fill="var(--primary)"
fontSize={12}
fontWeight={600}
fontFamily="var(--font-mono, monospace)"
>
{labels.join(", ")}
</text>
</g>
);
})}
</g>
)}
{/* Index labels (rendered last to appear on top) */}
<g className="viz-indices">
{values.map((_, index) => {
const x = dimensions.startX + index * (CELL_WIDTH + CELL_GAP);
const y = dimensions.startY;
return (
<text
key={index}
x={x + CELL_WIDTH / 2}
y={y + CELL_HEIGHT + INDEX_AREA_HEIGHT / 2 + 4}
textAnchor="middle"
dominantBaseline="middle"
className="viz-index"
fill="var(--muted-foreground)"
fontSize={11}
>
{index}
</text>
);
})}
</g>
{/* Arrow marker definition */}
<defs>
<marker
id="arrowhead"
markerWidth="8"
markerHeight="6"
refX="0"
refY="3"
orient="auto"
>
<polygon points="0 0, 8 3, 0 6" fill="var(--primary)" />
</marker>
</defs>
</svg>
</div>
</div>
);
}