viz primitives for data structures
This commit is contained in:
94
frontend/src/components/visualization/code-pane.tsx
Normal file
94
frontend/src/components/visualization/code-pane.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { useVisualization } from "./visualization-context";
|
||||
|
||||
interface CodePaneProps {
|
||||
code: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export function CodePane({ code, language = "python" }: CodePaneProps) {
|
||||
const { currentStep } = useVisualization();
|
||||
|
||||
const highlightedLines = useMemo(() => {
|
||||
if (!currentStep?.codeHighlight) return new Set<number>();
|
||||
const { startLine, endLine } = currentStep.codeHighlight;
|
||||
const lines = new Set<number>();
|
||||
for (let i = startLine; i <= endLine; i++) {
|
||||
lines.add(i);
|
||||
}
|
||||
return lines;
|
||||
}, [currentStep]);
|
||||
|
||||
const languageMap: Record<string, string> = {
|
||||
python: "python",
|
||||
javascript: "javascript",
|
||||
typescript: "typescript",
|
||||
java: "java",
|
||||
cpp: "cpp",
|
||||
c: "c",
|
||||
go: "go",
|
||||
rust: "rust",
|
||||
};
|
||||
|
||||
const prismLanguage = languageMap[language.toLowerCase()] || language;
|
||||
|
||||
const customStyle = useMemo(
|
||||
() => ({
|
||||
margin: 0,
|
||||
padding: "0.75rem 0",
|
||||
fontSize: "0.8125rem",
|
||||
borderRadius: "0.5rem",
|
||||
background: "var(--code-bg, #282c34)",
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg overflow-hidden border border-border">
|
||||
<div className="bg-muted/50 px-3 py-1.5 border-b border-border">
|
||||
<span className="text-xs font-medium text-muted-foreground capitalize">
|
||||
{language}
|
||||
</span>
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
language={prismLanguage}
|
||||
style={oneDark}
|
||||
customStyle={customStyle}
|
||||
showLineNumbers
|
||||
wrapLines
|
||||
lineNumberStyle={(lineNumber) => ({
|
||||
minWidth: "2.5em",
|
||||
paddingRight: "1em",
|
||||
textAlign: "right",
|
||||
userSelect: "none",
|
||||
color: highlightedLines.has(lineNumber)
|
||||
? "var(--primary)"
|
||||
: "var(--muted-foreground)",
|
||||
fontWeight: highlightedLines.has(lineNumber) ? 600 : 400,
|
||||
})}
|
||||
lineProps={(lineNumber) => {
|
||||
const isHighlighted = highlightedLines.has(lineNumber);
|
||||
return {
|
||||
style: {
|
||||
display: "block",
|
||||
padding: "0 0.75rem",
|
||||
backgroundColor: isHighlighted
|
||||
? "rgba(var(--primary-rgb, 59, 130, 246), 0.15)"
|
||||
: "transparent",
|
||||
borderLeft: isHighlighted
|
||||
? "3px solid var(--primary)"
|
||||
: "3px solid transparent",
|
||||
transition: "background-color 0.2s ease, border-color 0.2s ease",
|
||||
},
|
||||
};
|
||||
}}
|
||||
>
|
||||
{code.trim()}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,4 +8,11 @@ export { StepDescription } from "./step-description";
|
||||
export { VariablesPane } from "./variables-pane";
|
||||
export { StructureRenderer } from "./structure-renderer";
|
||||
export { PatternVisualization } from "./pattern-visualization";
|
||||
export { ArrayVisualizer } from "./visualizers";
|
||||
export { CodePane } from "./code-pane";
|
||||
export {
|
||||
ArrayVisualizer,
|
||||
HistogramVisualizer,
|
||||
LinkedListVisualizer,
|
||||
StackVisualizer,
|
||||
TreeVisualizer,
|
||||
} from "./visualizers";
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
import { useVisualization } from "./visualization-context";
|
||||
import { ArrayVisualizer } from "./visualizers/array-visualizer";
|
||||
import { HistogramVisualizer } from "./visualizers/histogram-visualizer";
|
||||
import { LinkedListVisualizer } from "./visualizers/linkedlist-visualizer";
|
||||
import { StackVisualizer } from "./visualizers/stack-visualizer";
|
||||
import { TreeVisualizer } from "./visualizers/tree-visualizer";
|
||||
import type { DataStructureState } from "@/types/visualization";
|
||||
|
||||
interface StructureVisualizerProps {
|
||||
@@ -13,36 +17,21 @@ function StructureVisualizer({ name, data }: StructureVisualizerProps) {
|
||||
switch (data.type) {
|
||||
case "array":
|
||||
return <ArrayVisualizer data={data} name={name} />;
|
||||
case "histogram":
|
||||
return <HistogramVisualizer data={data} name={name} />;
|
||||
case "linkedlist":
|
||||
// Phase 3: LinkedListVisualizer
|
||||
return (
|
||||
<div className="text-muted-foreground">
|
||||
LinkedList visualization coming soon
|
||||
</div>
|
||||
);
|
||||
return <LinkedListVisualizer data={data} name={name} />;
|
||||
case "stack":
|
||||
// Phase 3: StackVisualizer
|
||||
return (
|
||||
<div className="text-muted-foreground">
|
||||
Stack visualization coming soon
|
||||
</div>
|
||||
);
|
||||
return <StackVisualizer data={data} name={name} />;
|
||||
case "tree":
|
||||
// Phase 3: TreeVisualizer
|
||||
return (
|
||||
<div className="text-muted-foreground">
|
||||
Tree visualization coming soon
|
||||
</div>
|
||||
);
|
||||
return <TreeVisualizer data={data} name={name} />;
|
||||
case "graph":
|
||||
// Phase 5: GraphVisualizer
|
||||
return (
|
||||
<div className="text-muted-foreground">
|
||||
Graph visualization coming soon
|
||||
</div>
|
||||
);
|
||||
case "matrix":
|
||||
// Phase 5: MatrixVisualizer
|
||||
return (
|
||||
<div className="text-muted-foreground">
|
||||
Matrix visualization coming soon
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import type { HistogramState, ElementState } from "@/types/visualization";
|
||||
|
||||
interface HistogramVisualizerProps {
|
||||
data: HistogramState;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const BAR_WIDTH = 48;
|
||||
const BAR_GAP = 4;
|
||||
const MAX_BAR_HEIGHT = 180;
|
||||
const INDEX_AREA_HEIGHT = 20;
|
||||
const POINTER_HEIGHT = 24;
|
||||
const ANNOTATION_HEIGHT = 20;
|
||||
const AREA_LABEL_HEIGHT = 24;
|
||||
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 HistogramVisualizer({ data, name }: HistogramVisualizerProps) {
|
||||
const { bars, rectangle, pointers = {}, maxArea } = data;
|
||||
|
||||
const dimensions = useMemo(() => {
|
||||
const hasPointers = Object.keys(pointers).length > 0;
|
||||
const hasAnnotations = bars.some(
|
||||
(b) => b.annotations && b.annotations.length > 0
|
||||
);
|
||||
const hasRectangle = !!rectangle;
|
||||
|
||||
// Find max value to scale bars
|
||||
const maxValue = Math.max(...bars.map((b) => b.value), 1);
|
||||
|
||||
const contentWidth = bars.length * BAR_WIDTH + (bars.length - 1) * BAR_GAP;
|
||||
const width = contentWidth + SVG_PADDING * 2;
|
||||
|
||||
// Calculate height
|
||||
let height = MAX_BAR_HEIGHT + INDEX_AREA_HEIGHT + SVG_PADDING * 2;
|
||||
if (hasPointers) height += POINTER_HEIGHT + 8;
|
||||
if (hasAnnotations) height += ANNOTATION_HEIGHT;
|
||||
if (hasRectangle) height += AREA_LABEL_HEIGHT;
|
||||
|
||||
// Calculate where bars start (from bottom, going up)
|
||||
const barBaseY = hasAnnotations
|
||||
? SVG_PADDING + ANNOTATION_HEIGHT + MAX_BAR_HEIGHT
|
||||
: SVG_PADDING + MAX_BAR_HEIGHT;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
contentWidth,
|
||||
maxValue,
|
||||
hasPointers,
|
||||
hasAnnotations,
|
||||
hasRectangle,
|
||||
startX: SVG_PADDING,
|
||||
barBaseY,
|
||||
};
|
||||
}, [bars, pointers, rectangle]);
|
||||
|
||||
// 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]);
|
||||
|
||||
// Calculate bar height from value
|
||||
const getBarHeight = (value: number): number => {
|
||||
return (value / dimensions.maxValue) * MAX_BAR_HEIGHT;
|
||||
};
|
||||
|
||||
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-histogram"
|
||||
role="img"
|
||||
aria-label={`Histogram visualization with ${bars.length} bars`}
|
||||
>
|
||||
{/* Rectangle overlay (rendered behind bars) */}
|
||||
{rectangle && (
|
||||
<g className="viz-rectangle">
|
||||
{/* Semi-transparent rectangle */}
|
||||
<rect
|
||||
x={
|
||||
dimensions.startX +
|
||||
rectangle.startIndex * (BAR_WIDTH + BAR_GAP)
|
||||
}
|
||||
y={
|
||||
dimensions.barBaseY -
|
||||
getBarHeight(rectangle.height)
|
||||
}
|
||||
width={
|
||||
(rectangle.endIndex - rectangle.startIndex + 1) * BAR_WIDTH +
|
||||
(rectangle.endIndex - rectangle.startIndex) * BAR_GAP
|
||||
}
|
||||
height={getBarHeight(rectangle.height)}
|
||||
fill={
|
||||
rectangle.state === "found"
|
||||
? "var(--viz-found)"
|
||||
: "var(--viz-comparing)"
|
||||
}
|
||||
opacity={0.3}
|
||||
rx={4}
|
||||
/>
|
||||
{/* Rectangle border */}
|
||||
<rect
|
||||
x={
|
||||
dimensions.startX +
|
||||
rectangle.startIndex * (BAR_WIDTH + BAR_GAP)
|
||||
}
|
||||
y={
|
||||
dimensions.barBaseY -
|
||||
getBarHeight(rectangle.height)
|
||||
}
|
||||
width={
|
||||
(rectangle.endIndex - rectangle.startIndex + 1) * BAR_WIDTH +
|
||||
(rectangle.endIndex - rectangle.startIndex) * BAR_GAP
|
||||
}
|
||||
height={getBarHeight(rectangle.height)}
|
||||
fill="none"
|
||||
stroke={
|
||||
rectangle.state === "found"
|
||||
? "var(--viz-found)"
|
||||
: "var(--viz-comparing)"
|
||||
}
|
||||
strokeWidth={2}
|
||||
strokeDasharray={rectangle.state === "found" ? "none" : "4 2"}
|
||||
rx={4}
|
||||
/>
|
||||
{/* Area label - positioned inside the rectangle */}
|
||||
{rectangle.label && (
|
||||
<text
|
||||
x={
|
||||
dimensions.startX +
|
||||
rectangle.startIndex * (BAR_WIDTH + BAR_GAP) +
|
||||
((rectangle.endIndex - rectangle.startIndex + 1) *
|
||||
BAR_WIDTH +
|
||||
(rectangle.endIndex - rectangle.startIndex) * BAR_GAP) /
|
||||
2
|
||||
}
|
||||
y={
|
||||
dimensions.barBaseY -
|
||||
getBarHeight(rectangle.height) / 2
|
||||
}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="white"
|
||||
fontSize={14}
|
||||
fontWeight={700}
|
||||
fontFamily="var(--font-mono, monospace)"
|
||||
style={{ textShadow: "0 1px 2px rgba(0,0,0,0.8)" }}
|
||||
>
|
||||
{rectangle.label}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* Histogram bars */}
|
||||
<g>
|
||||
{bars.map((bar, index) => {
|
||||
const x = dimensions.startX + index * (BAR_WIDTH + BAR_GAP);
|
||||
const barHeight = getBarHeight(bar.value);
|
||||
const y = dimensions.barBaseY - barHeight;
|
||||
|
||||
return (
|
||||
<g
|
||||
key={index}
|
||||
className="viz-element"
|
||||
data-state={bar.state}
|
||||
>
|
||||
{/* Bar background */}
|
||||
<rect
|
||||
x={x}
|
||||
y={y}
|
||||
width={BAR_WIDTH}
|
||||
height={barHeight}
|
||||
rx={4}
|
||||
fill={getStateColor(bar.state)}
|
||||
className="viz-bar-bg"
|
||||
/>
|
||||
|
||||
{/* Bar border */}
|
||||
<rect
|
||||
x={x}
|
||||
y={y}
|
||||
width={BAR_WIDTH}
|
||||
height={barHeight}
|
||||
rx={4}
|
||||
fill="none"
|
||||
stroke="var(--border)"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
|
||||
{/* Value label (inside bar or above if too short) */}
|
||||
<text
|
||||
x={x + BAR_WIDTH / 2}
|
||||
y={barHeight > 30 ? y + 20 : y - 6}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
className="viz-value"
|
||||
fill={barHeight > 30 ? getTextColor(bar.state) : "var(--foreground)"}
|
||||
fontSize={14}
|
||||
fontWeight={500}
|
||||
fontFamily="var(--font-mono, monospace)"
|
||||
>
|
||||
{bar.value}
|
||||
</text>
|
||||
|
||||
{/* Annotations (above bar) */}
|
||||
{bar.annotations && bar.annotations.length > 0 && (
|
||||
<text
|
||||
x={x + BAR_WIDTH / 2}
|
||||
y={dimensions.barBaseY - MAX_BAR_HEIGHT - 8}
|
||||
textAnchor="middle"
|
||||
className="viz-annotation"
|
||||
fill="var(--primary)"
|
||||
fontSize={12}
|
||||
fontWeight={600}
|
||||
>
|
||||
{bar.annotations.join(", ")}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
|
||||
{/* Baseline */}
|
||||
<line
|
||||
x1={dimensions.startX - 4}
|
||||
y1={dimensions.barBaseY}
|
||||
x2={dimensions.startX + dimensions.contentWidth + 4}
|
||||
y2={dimensions.barBaseY}
|
||||
stroke="var(--border)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
|
||||
{/* Pointers (below baseline) */}
|
||||
{dimensions.hasPointers && (
|
||||
<g className="viz-pointers">
|
||||
{Object.entries(pointersByPosition).map(([posStr, labels]) => {
|
||||
const position = parseInt(posStr, 10);
|
||||
if (position < 0 || position >= bars.length) return null;
|
||||
|
||||
const x =
|
||||
dimensions.startX +
|
||||
position * (BAR_WIDTH + BAR_GAP) +
|
||||
BAR_WIDTH / 2;
|
||||
const y =
|
||||
dimensions.barBaseY + 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(#histogram-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 */}
|
||||
<g className="viz-indices">
|
||||
{bars.map((_, index) => {
|
||||
const x = dimensions.startX + index * (BAR_WIDTH + BAR_GAP);
|
||||
|
||||
return (
|
||||
<text
|
||||
key={index}
|
||||
x={x + BAR_WIDTH / 2}
|
||||
y={dimensions.barBaseY + INDEX_AREA_HEIGHT / 2 + 4}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
className="viz-index"
|
||||
fill="var(--muted-foreground)"
|
||||
fontSize={11}
|
||||
>
|
||||
{index}
|
||||
</text>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
|
||||
{/* Max area display */}
|
||||
{maxArea !== undefined && (
|
||||
<text
|
||||
x={dimensions.width - SVG_PADDING}
|
||||
y={SVG_PADDING + 12}
|
||||
textAnchor="end"
|
||||
fill="var(--primary)"
|
||||
fontSize={14}
|
||||
fontWeight={600}
|
||||
fontFamily="var(--font-mono, monospace)"
|
||||
>
|
||||
Max Area: {maxArea}
|
||||
</text>
|
||||
)}
|
||||
|
||||
{/* Arrow marker definition */}
|
||||
<defs>
|
||||
<marker
|
||||
id="histogram-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>
|
||||
);
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
export { ArrayVisualizer } from "./array-visualizer";
|
||||
export { HistogramVisualizer } from "./histogram-visualizer";
|
||||
export { LinkedListVisualizer } from "./linkedlist-visualizer";
|
||||
export { StackVisualizer } from "./stack-visualizer";
|
||||
export { TreeVisualizer } from "./tree-visualizer";
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import type { LinkedListState, ElementState } from "@/types/visualization";
|
||||
|
||||
interface LinkedListVisualizerProps {
|
||||
data: LinkedListState;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const NODE_WIDTH = 56;
|
||||
const NODE_HEIGHT = 56;
|
||||
const ARROW_WIDTH = 40;
|
||||
const POINTER_HEIGHT = 24;
|
||||
const ANNOTATION_HEIGHT = 20;
|
||||
const SVG_PADDING = 16;
|
||||
const NULL_WIDTH = 40;
|
||||
|
||||
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 LinkedListVisualizer({ data, name }: LinkedListVisualizerProps) {
|
||||
const { nodes, pointers = {} } = data;
|
||||
|
||||
const dimensions = useMemo(() => {
|
||||
const hasPointers = Object.keys(pointers).length > 0;
|
||||
const hasAnnotations = nodes.some(
|
||||
(n) => n.annotations && n.annotations.length > 0
|
||||
);
|
||||
|
||||
const nodeCount = nodes.length;
|
||||
const contentWidth =
|
||||
nodeCount * NODE_WIDTH +
|
||||
(nodeCount > 0 ? nodeCount * ARROW_WIDTH : 0) +
|
||||
NULL_WIDTH;
|
||||
const width = contentWidth + SVG_PADDING * 2;
|
||||
|
||||
let height = NODE_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,
|
||||
};
|
||||
}, [nodes, pointers]);
|
||||
|
||||
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]);
|
||||
|
||||
const getNodeCenterX = (index: number) =>
|
||||
dimensions.startX + index * (NODE_WIDTH + ARROW_WIDTH) + NODE_WIDTH / 2;
|
||||
|
||||
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-linkedlist"
|
||||
role="img"
|
||||
aria-label={`Linked list visualization with ${nodes.length} nodes`}
|
||||
>
|
||||
<defs>
|
||||
<marker
|
||||
id="linkedlist-arrowhead"
|
||||
markerWidth="10"
|
||||
markerHeight="7"
|
||||
refX="9"
|
||||
refY="3.5"
|
||||
orient="auto"
|
||||
>
|
||||
<polygon
|
||||
points="0 0, 10 3.5, 0 7"
|
||||
fill="var(--muted-foreground)"
|
||||
/>
|
||||
</marker>
|
||||
<marker
|
||||
id="pointer-arrowhead"
|
||||
markerWidth="8"
|
||||
markerHeight="6"
|
||||
refX="0"
|
||||
refY="3"
|
||||
orient="auto"
|
||||
>
|
||||
<polygon points="0 0, 8 3, 0 6" fill="var(--primary)" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
{/* Nodes and arrows */}
|
||||
<g>
|
||||
{nodes.map((node, index) => {
|
||||
const x = dimensions.startX + index * (NODE_WIDTH + ARROW_WIDTH);
|
||||
const y = dimensions.startY;
|
||||
|
||||
return (
|
||||
<g key={index} className="viz-element" data-state={node.state}>
|
||||
{/* Node box */}
|
||||
<rect
|
||||
x={x}
|
||||
y={y}
|
||||
width={NODE_WIDTH}
|
||||
height={NODE_HEIGHT}
|
||||
rx={6}
|
||||
fill={getStateColor(node.state)}
|
||||
className="viz-node-bg"
|
||||
/>
|
||||
|
||||
{/* Node border */}
|
||||
<rect
|
||||
x={x}
|
||||
y={y}
|
||||
width={NODE_WIDTH}
|
||||
height={NODE_HEIGHT}
|
||||
rx={6}
|
||||
fill="none"
|
||||
stroke="var(--border)"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
|
||||
{/* Value */}
|
||||
<text
|
||||
x={x + NODE_WIDTH / 2}
|
||||
y={y + NODE_HEIGHT / 2}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
className="viz-value"
|
||||
fill={getTextColor(node.state)}
|
||||
fontSize={18}
|
||||
fontWeight={500}
|
||||
fontFamily="var(--font-mono, monospace)"
|
||||
>
|
||||
{String(node.value)}
|
||||
</text>
|
||||
|
||||
{/* Annotations (above node) */}
|
||||
{node.annotations && node.annotations.length > 0 && (
|
||||
<text
|
||||
x={x + NODE_WIDTH / 2}
|
||||
y={y - 6}
|
||||
textAnchor="middle"
|
||||
className="viz-annotation"
|
||||
fill="var(--primary)"
|
||||
fontSize={12}
|
||||
fontWeight={600}
|
||||
>
|
||||
{node.annotations.join(", ")}
|
||||
</text>
|
||||
)}
|
||||
|
||||
{/* Arrow to next node */}
|
||||
<line
|
||||
x1={x + NODE_WIDTH + 4}
|
||||
y1={y + NODE_HEIGHT / 2}
|
||||
x2={x + NODE_WIDTH + ARROW_WIDTH - 4}
|
||||
y2={y + NODE_HEIGHT / 2}
|
||||
stroke="var(--muted-foreground)"
|
||||
strokeWidth={2}
|
||||
markerEnd="url(#linkedlist-arrowhead)"
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Null terminator */}
|
||||
{nodes.length > 0 && (
|
||||
<g className="viz-null">
|
||||
<text
|
||||
x={
|
||||
dimensions.startX +
|
||||
nodes.length * (NODE_WIDTH + ARROW_WIDTH) +
|
||||
NULL_WIDTH / 2 -
|
||||
8
|
||||
}
|
||||
y={dimensions.startY + NODE_HEIGHT / 2}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="var(--muted-foreground)"
|
||||
fontSize={14}
|
||||
fontFamily="var(--font-mono, monospace)"
|
||||
>
|
||||
null
|
||||
</text>
|
||||
</g>
|
||||
)}
|
||||
</g>
|
||||
|
||||
{/* Pointers */}
|
||||
{dimensions.hasPointers && (
|
||||
<g className="viz-pointers">
|
||||
{Object.entries(pointersByPosition).map(([posStr, labels]) => {
|
||||
const position = parseInt(posStr, 10);
|
||||
if (position < 0 || position >= nodes.length) return null;
|
||||
|
||||
const x = getNodeCenterX(position);
|
||||
const y = dimensions.startY + NODE_HEIGHT + POINTER_HEIGHT;
|
||||
|
||||
return (
|
||||
<g key={posStr} className="viz-pointer">
|
||||
{/* Arrow pointing up to node */}
|
||||
<line
|
||||
x1={x}
|
||||
y1={y}
|
||||
x2={x}
|
||||
y2={y - 10}
|
||||
stroke="var(--primary)"
|
||||
strokeWidth={2}
|
||||
markerEnd="url(#pointer-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>
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import type { StackState, ElementState } from "@/types/visualization";
|
||||
|
||||
interface StackVisualizerProps {
|
||||
data: StackState;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const CELL_WIDTH = 56;
|
||||
const CELL_HEIGHT = 48;
|
||||
const CELL_GAP = 4;
|
||||
const CONTAINER_PADDING = 8;
|
||||
const WALL_THICKNESS = 3;
|
||||
const MIN_CONTAINER_HEIGHT = 200;
|
||||
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 StackVisualizer({ data, name }: StackVisualizerProps) {
|
||||
const { values } = data;
|
||||
|
||||
const dimensions = useMemo(() => {
|
||||
const stackContentHeight =
|
||||
values.length * CELL_HEIGHT + (values.length > 0 ? (values.length - 1) * CELL_GAP : 0);
|
||||
|
||||
const containerInnerHeight = Math.max(MIN_CONTAINER_HEIGHT, stackContentHeight + CONTAINER_PADDING * 2);
|
||||
const containerInnerWidth = CELL_WIDTH + CONTAINER_PADDING * 2;
|
||||
|
||||
const width = containerInnerWidth + WALL_THICKNESS * 2 + SVG_PADDING * 2;
|
||||
const height = containerInnerHeight + WALL_THICKNESS + SVG_PADDING * 2;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
containerInnerWidth,
|
||||
containerInnerHeight,
|
||||
containerX: SVG_PADDING,
|
||||
containerY: SVG_PADDING,
|
||||
stackContentHeight,
|
||||
};
|
||||
}, [values]);
|
||||
|
||||
// Bottom of container interior (where first element sits)
|
||||
const containerBottom = dimensions.containerY + dimensions.containerInnerHeight;
|
||||
const containerLeft = dimensions.containerX + WALL_THICKNESS;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{name && (
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{name}
|
||||
</span>
|
||||
)}
|
||||
<div className="overflow-auto">
|
||||
<svg
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
|
||||
className="viz-stack"
|
||||
role="img"
|
||||
aria-label={`Stack visualization with ${values.length} elements`}
|
||||
>
|
||||
{/* Container walls - left, right, and bottom */}
|
||||
<g className="viz-container">
|
||||
{/* Left wall */}
|
||||
<line
|
||||
x1={dimensions.containerX + WALL_THICKNESS / 2}
|
||||
y1={dimensions.containerY}
|
||||
x2={dimensions.containerX + WALL_THICKNESS / 2}
|
||||
y2={containerBottom + WALL_THICKNESS}
|
||||
stroke="var(--primary)"
|
||||
strokeWidth={WALL_THICKNESS}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
{/* Right wall */}
|
||||
<line
|
||||
x1={dimensions.containerX + WALL_THICKNESS + dimensions.containerInnerWidth + WALL_THICKNESS / 2}
|
||||
y1={dimensions.containerY}
|
||||
x2={dimensions.containerX + WALL_THICKNESS + dimensions.containerInnerWidth + WALL_THICKNESS / 2}
|
||||
y2={containerBottom + WALL_THICKNESS}
|
||||
stroke="var(--primary)"
|
||||
strokeWidth={WALL_THICKNESS}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
{/* Bottom wall */}
|
||||
<line
|
||||
x1={dimensions.containerX}
|
||||
y1={containerBottom + WALL_THICKNESS / 2}
|
||||
x2={dimensions.containerX + WALL_THICKNESS * 2 + dimensions.containerInnerWidth}
|
||||
y2={containerBottom + WALL_THICKNESS / 2}
|
||||
stroke="var(--primary)"
|
||||
strokeWidth={WALL_THICKNESS}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</g>
|
||||
|
||||
{/* Empty stack indicator */}
|
||||
{values.length === 0 && (
|
||||
<text
|
||||
x={containerLeft + dimensions.containerInnerWidth / 2}
|
||||
y={containerBottom - dimensions.containerInnerHeight / 2}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="var(--muted-foreground)"
|
||||
fontSize={12}
|
||||
fontStyle="italic"
|
||||
>
|
||||
empty
|
||||
</text>
|
||||
)}
|
||||
|
||||
{/* Stack elements (bottom to top) */}
|
||||
<g>
|
||||
{values.map((element, index) => {
|
||||
// Elements stack from bottom up
|
||||
const x = containerLeft + CONTAINER_PADDING;
|
||||
const y = containerBottom - CONTAINER_PADDING - CELL_HEIGHT - index * (CELL_HEIGHT + CELL_GAP);
|
||||
|
||||
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={4}
|
||||
fill={getStateColor(element.state)}
|
||||
className="viz-cell-bg"
|
||||
/>
|
||||
|
||||
{/* Cell border */}
|
||||
<rect
|
||||
x={x}
|
||||
y={y}
|
||||
width={CELL_WIDTH}
|
||||
height={CELL_HEIGHT}
|
||||
rx={4}
|
||||
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>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import type { TreeState, TreeNode, ElementState } from "@/types/visualization";
|
||||
|
||||
interface TreeVisualizerProps {
|
||||
data: TreeState;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface PositionedNode {
|
||||
node: TreeNode;
|
||||
x: number;
|
||||
y: number;
|
||||
depth: number;
|
||||
parentX?: number;
|
||||
parentY?: number;
|
||||
}
|
||||
|
||||
const NODE_RADIUS = 24;
|
||||
const LEVEL_HEIGHT = 72;
|
||||
const MIN_NODE_SPACING = 56;
|
||||
const SVG_PADDING = 24;
|
||||
|
||||
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)";
|
||||
}
|
||||
}
|
||||
|
||||
function getTreeDepth(node: TreeNode | null): number {
|
||||
if (!node) return 0;
|
||||
return 1 + Math.max(getTreeDepth(node.left ?? null), getTreeDepth(node.right ?? null));
|
||||
}
|
||||
|
||||
function getNodeCount(node: TreeNode | null): number {
|
||||
if (!node) return 0;
|
||||
return 1 + getNodeCount(node.left ?? null) + getNodeCount(node.right ?? null);
|
||||
}
|
||||
|
||||
function calculatePositions(
|
||||
root: TreeNode | null,
|
||||
treeWidth: number
|
||||
): PositionedNode[] {
|
||||
if (!root) return [];
|
||||
|
||||
const positions: PositionedNode[] = [];
|
||||
|
||||
function traverse(
|
||||
node: TreeNode | null,
|
||||
level: number,
|
||||
left: number,
|
||||
right: number,
|
||||
parentX?: number,
|
||||
parentY?: number
|
||||
): void {
|
||||
if (!node) return;
|
||||
|
||||
const x = (left + right) / 2;
|
||||
const y = SVG_PADDING + level * LEVEL_HEIGHT + NODE_RADIUS;
|
||||
|
||||
positions.push({
|
||||
node,
|
||||
x,
|
||||
y,
|
||||
depth: level,
|
||||
parentX,
|
||||
parentY,
|
||||
});
|
||||
|
||||
const childWidth = (right - left) / 2;
|
||||
traverse(node.left ?? null, level + 1, left, left + childWidth, x, y);
|
||||
traverse(node.right ?? null, level + 1, left + childWidth, right, x, y);
|
||||
}
|
||||
|
||||
traverse(root, 0, 0, treeWidth);
|
||||
return positions;
|
||||
}
|
||||
|
||||
export function TreeVisualizer({ data, name }: TreeVisualizerProps) {
|
||||
const { root, pointers = {} } = data;
|
||||
|
||||
const { positions, dimensions } = useMemo(() => {
|
||||
if (!root) {
|
||||
return {
|
||||
positions: [],
|
||||
dimensions: { width: 200, height: 100 },
|
||||
};
|
||||
}
|
||||
|
||||
const depth = getTreeDepth(root);
|
||||
const nodeCount = getNodeCount(root);
|
||||
const maxNodesAtLevel = Math.pow(2, depth - 1);
|
||||
const treeWidth = Math.max(
|
||||
maxNodesAtLevel * MIN_NODE_SPACING,
|
||||
nodeCount * MIN_NODE_SPACING
|
||||
);
|
||||
|
||||
const width = treeWidth + SVG_PADDING * 2;
|
||||
const height = depth * LEVEL_HEIGHT + SVG_PADDING * 2;
|
||||
|
||||
const positions = calculatePositions(root, treeWidth);
|
||||
positions.forEach((pos) => {
|
||||
pos.x += SVG_PADDING;
|
||||
});
|
||||
|
||||
return {
|
||||
positions,
|
||||
dimensions: { width, height },
|
||||
};
|
||||
}, [root]);
|
||||
|
||||
const nodePointers = useMemo(() => {
|
||||
const result: Record<number, string[]> = {};
|
||||
for (const [label, nodeValue] of Object.entries(pointers)) {
|
||||
const index = positions.findIndex(
|
||||
(p) => String(p.node.value) === nodeValue
|
||||
);
|
||||
if (index >= 0) {
|
||||
if (!result[index]) {
|
||||
result[index] = [];
|
||||
}
|
||||
result[index].push(label);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, [pointers, positions]);
|
||||
|
||||
if (!root) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{name && (
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{name}
|
||||
</span>
|
||||
)}
|
||||
<div className="text-muted-foreground text-center p-4">
|
||||
Empty tree
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{name && (
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{name}
|
||||
</span>
|
||||
)}
|
||||
<div className="overflow-auto">
|
||||
<svg
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
|
||||
className="viz-tree"
|
||||
role="img"
|
||||
aria-label={`Binary tree visualization with ${positions.length} nodes`}
|
||||
>
|
||||
{/* Edges (drawn first, behind nodes) */}
|
||||
<g className="viz-edges">
|
||||
{positions.map((pos, index) => {
|
||||
if (pos.parentX === undefined || pos.parentY === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<line
|
||||
key={`edge-${index}`}
|
||||
x1={pos.parentX}
|
||||
y1={pos.parentY + NODE_RADIUS}
|
||||
x2={pos.x}
|
||||
y2={pos.y - NODE_RADIUS}
|
||||
stroke="var(--border)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
|
||||
{/* Nodes */}
|
||||
<g className="viz-nodes">
|
||||
{positions.map((pos, index) => {
|
||||
const pointerLabels = nodePointers[index];
|
||||
|
||||
return (
|
||||
<g
|
||||
key={index}
|
||||
className="viz-element"
|
||||
data-state={pos.node.state}
|
||||
>
|
||||
{/* Node circle */}
|
||||
<circle
|
||||
cx={pos.x}
|
||||
cy={pos.y}
|
||||
r={NODE_RADIUS}
|
||||
fill={getStateColor(pos.node.state)}
|
||||
className="viz-node-bg"
|
||||
/>
|
||||
|
||||
{/* Node border */}
|
||||
<circle
|
||||
cx={pos.x}
|
||||
cy={pos.y}
|
||||
r={NODE_RADIUS}
|
||||
fill="none"
|
||||
stroke="var(--border)"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
|
||||
{/* Value */}
|
||||
<text
|
||||
x={pos.x}
|
||||
y={pos.y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
className="viz-value"
|
||||
fill={getTextColor(pos.node.state)}
|
||||
fontSize={16}
|
||||
fontWeight={500}
|
||||
fontFamily="var(--font-mono, monospace)"
|
||||
>
|
||||
{String(pos.node.value)}
|
||||
</text>
|
||||
|
||||
{/* Annotations */}
|
||||
{pos.node.annotations && pos.node.annotations.length > 0 && (
|
||||
<text
|
||||
x={pos.x}
|
||||
y={pos.y - NODE_RADIUS - 8}
|
||||
textAnchor="middle"
|
||||
className="viz-annotation"
|
||||
fill="var(--primary)"
|
||||
fontSize={11}
|
||||
fontWeight={600}
|
||||
>
|
||||
{pos.node.annotations.join(", ")}
|
||||
</text>
|
||||
)}
|
||||
|
||||
{/* Pointer labels */}
|
||||
{pointerLabels && pointerLabels.length > 0 && (
|
||||
<text
|
||||
x={pos.x}
|
||||
y={pos.y + NODE_RADIUS + 16}
|
||||
textAnchor="middle"
|
||||
fill="var(--primary)"
|
||||
fontSize={11}
|
||||
fontWeight={600}
|
||||
fontFamily="var(--font-mono, monospace)"
|
||||
>
|
||||
{pointerLabels.join(", ")}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -94,6 +94,29 @@ export interface MatrixState {
|
||||
pointers?: Record<string, { row: number; col: number }>;
|
||||
}
|
||||
|
||||
// Histogram Data Structure (for bar chart visualizations)
|
||||
export interface HistogramBar {
|
||||
value: number;
|
||||
state: ElementState;
|
||||
annotations?: string[];
|
||||
}
|
||||
|
||||
export interface HistogramRectangle {
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
height: number;
|
||||
state?: ElementState;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface HistogramState {
|
||||
type: "histogram";
|
||||
bars: HistogramBar[];
|
||||
rectangle?: HistogramRectangle;
|
||||
pointers?: Record<string, number>;
|
||||
maxArea?: number;
|
||||
}
|
||||
|
||||
// Union type for all data structures
|
||||
export type DataStructureState =
|
||||
| ArrayState
|
||||
@@ -101,7 +124,8 @@ export type DataStructureState =
|
||||
| StackState
|
||||
| TreeState
|
||||
| GraphState
|
||||
| MatrixState;
|
||||
| MatrixState
|
||||
| HistogramState;
|
||||
|
||||
// Code highlighting range
|
||||
export interface CodeHighlight {
|
||||
|
||||
Reference in New Issue
Block a user