264 lines
7.7 KiB
TypeScript
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>
|
|
);
|
|
}
|