From e9adb38be01a2d70f23a1b39580c967790b5d1ae Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Fri, 12 Sep 2025 15:26:17 +0100 Subject: [PATCH] viz primitives for data structures --- .../components/visualization/code-pane.tsx | 94 +++++ .../src/components/visualization/index.ts | 9 +- .../visualization/structure-renderer.tsx | 29 +- .../visualizers/histogram-visualizer.tsx | 384 ++++++++++++++++++ .../visualization/visualizers/index.ts | 4 + .../visualizers/linkedlist-visualizer.tsx | 282 +++++++++++++ .../visualizers/stack-visualizer.tsx | 202 +++++++++ .../visualizers/tree-visualizer.tsx | 289 +++++++++++++ frontend/src/types/visualization.ts | 26 +- 9 files changed, 1297 insertions(+), 22 deletions(-) create mode 100644 frontend/src/components/visualization/code-pane.tsx create mode 100644 frontend/src/components/visualization/visualizers/histogram-visualizer.tsx create mode 100644 frontend/src/components/visualization/visualizers/linkedlist-visualizer.tsx create mode 100644 frontend/src/components/visualization/visualizers/stack-visualizer.tsx create mode 100644 frontend/src/components/visualization/visualizers/tree-visualizer.tsx diff --git a/frontend/src/components/visualization/code-pane.tsx b/frontend/src/components/visualization/code-pane.tsx new file mode 100644 index 0000000..4afb5ab --- /dev/null +++ b/frontend/src/components/visualization/code-pane.tsx @@ -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(); + const { startLine, endLine } = currentStep.codeHighlight; + const lines = new Set(); + for (let i = startLine; i <= endLine; i++) { + lines.add(i); + } + return lines; + }, [currentStep]); + + const languageMap: Record = { + 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 ( +
+
+ + {language} + +
+ ({ + 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()} + +
+ ); +} diff --git a/frontend/src/components/visualization/index.ts b/frontend/src/components/visualization/index.ts index 113ec83..676874f 100644 --- a/frontend/src/components/visualization/index.ts +++ b/frontend/src/components/visualization/index.ts @@ -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"; diff --git a/frontend/src/components/visualization/structure-renderer.tsx b/frontend/src/components/visualization/structure-renderer.tsx index 52e4c1b..0e9751b 100644 --- a/frontend/src/components/visualization/structure-renderer.tsx +++ b/frontend/src/components/visualization/structure-renderer.tsx @@ -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 ; + case "histogram": + return ; case "linkedlist": - // Phase 3: LinkedListVisualizer - return ( -
- LinkedList visualization coming soon -
- ); + return ; case "stack": - // Phase 3: StackVisualizer - return ( -
- Stack visualization coming soon -
- ); + return ; case "tree": - // Phase 3: TreeVisualizer - return ( -
- Tree visualization coming soon -
- ); + return ; case "graph": - // Phase 5: GraphVisualizer return (
Graph visualization coming soon
); case "matrix": - // Phase 5: MatrixVisualizer return (
Matrix visualization coming soon diff --git a/frontend/src/components/visualization/visualizers/histogram-visualizer.tsx b/frontend/src/components/visualization/visualizers/histogram-visualizer.tsx new file mode 100644 index 0000000..093daf4 --- /dev/null +++ b/frontend/src/components/visualization/visualizers/histogram-visualizer.tsx @@ -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 = {}; + 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 ( +
+ {name && ( + + {name} + + )} +
+ + {/* Rectangle overlay (rendered behind bars) */} + {rectangle && ( + + {/* Semi-transparent rectangle */} + + {/* Rectangle border */} + + {/* Area label - positioned inside the rectangle */} + {rectangle.label && ( + + {rectangle.label} + + )} + + )} + + {/* Histogram bars */} + + {bars.map((bar, index) => { + const x = dimensions.startX + index * (BAR_WIDTH + BAR_GAP); + const barHeight = getBarHeight(bar.value); + const y = dimensions.barBaseY - barHeight; + + return ( + + {/* Bar background */} + + + {/* Bar border */} + + + {/* Value label (inside bar or above if too short) */} + 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} + + + {/* Annotations (above bar) */} + {bar.annotations && bar.annotations.length > 0 && ( + + {bar.annotations.join(", ")} + + )} + + ); + })} + + + {/* Baseline */} + + + {/* Pointers (below baseline) */} + {dimensions.hasPointers && ( + + {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 ( + + {/* Arrow */} + + + {/* Label(s) */} + + {labels.join(", ")} + + + ); + })} + + )} + + {/* Index labels */} + + {bars.map((_, index) => { + const x = dimensions.startX + index * (BAR_WIDTH + BAR_GAP); + + return ( + + {index} + + ); + })} + + + {/* Max area display */} + {maxArea !== undefined && ( + + Max Area: {maxArea} + + )} + + {/* Arrow marker definition */} + + + + + + +
+
+ ); +} diff --git a/frontend/src/components/visualization/visualizers/index.ts b/frontend/src/components/visualization/visualizers/index.ts index 2c5c8df..043d0e6 100644 --- a/frontend/src/components/visualization/visualizers/index.ts +++ b/frontend/src/components/visualization/visualizers/index.ts @@ -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"; diff --git a/frontend/src/components/visualization/visualizers/linkedlist-visualizer.tsx b/frontend/src/components/visualization/visualizers/linkedlist-visualizer.tsx new file mode 100644 index 0000000..8509285 --- /dev/null +++ b/frontend/src/components/visualization/visualizers/linkedlist-visualizer.tsx @@ -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 = {}; + 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 ( +
+ {name && ( + + {name} + + )} +
+ + + + + + + + + + + {/* Nodes and arrows */} + + {nodes.map((node, index) => { + const x = dimensions.startX + index * (NODE_WIDTH + ARROW_WIDTH); + const y = dimensions.startY; + + return ( + + {/* Node box */} + + + {/* Node border */} + + + {/* Value */} + + {String(node.value)} + + + {/* Annotations (above node) */} + {node.annotations && node.annotations.length > 0 && ( + + {node.annotations.join(", ")} + + )} + + {/* Arrow to next node */} + + + ); + })} + + {/* Null terminator */} + {nodes.length > 0 && ( + + + null + + + )} + + + {/* Pointers */} + {dimensions.hasPointers && ( + + {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 ( + + {/* Arrow pointing up to node */} + + + {/* Label(s) */} + + {labels.join(", ")} + + + ); + })} + + )} + +
+
+ ); +} diff --git a/frontend/src/components/visualization/visualizers/stack-visualizer.tsx b/frontend/src/components/visualization/visualizers/stack-visualizer.tsx new file mode 100644 index 0000000..94a164e --- /dev/null +++ b/frontend/src/components/visualization/visualizers/stack-visualizer.tsx @@ -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 ( +
+ {name && ( + + {name} + + )} +
+ + {/* Container walls - left, right, and bottom */} + + {/* Left wall */} + + {/* Right wall */} + + {/* Bottom wall */} + + + + {/* Empty stack indicator */} + {values.length === 0 && ( + + empty + + )} + + {/* Stack elements (bottom to top) */} + + {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 ( + + {/* Cell background */} + + + {/* Cell border */} + + + {/* Value */} + + {String(element.value)} + + + ); + })} + + +
+
+ ); +} diff --git a/frontend/src/components/visualization/visualizers/tree-visualizer.tsx b/frontend/src/components/visualization/visualizers/tree-visualizer.tsx new file mode 100644 index 0000000..323556f --- /dev/null +++ b/frontend/src/components/visualization/visualizers/tree-visualizer.tsx @@ -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 = {}; + 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 ( +
+ {name && ( + + {name} + + )} +
+ Empty tree +
+
+ ); + } + + return ( +
+ {name && ( + + {name} + + )} +
+ + {/* Edges (drawn first, behind nodes) */} + + {positions.map((pos, index) => { + if (pos.parentX === undefined || pos.parentY === undefined) { + return null; + } + + return ( + + ); + })} + + + {/* Nodes */} + + {positions.map((pos, index) => { + const pointerLabels = nodePointers[index]; + + return ( + + {/* Node circle */} + + + {/* Node border */} + + + {/* Value */} + + {String(pos.node.value)} + + + {/* Annotations */} + {pos.node.annotations && pos.node.annotations.length > 0 && ( + + {pos.node.annotations.join(", ")} + + )} + + {/* Pointer labels */} + {pointerLabels && pointerLabels.length > 0 && ( + + {pointerLabels.join(", ")} + + )} + + ); + })} + + +
+
+ ); +} diff --git a/frontend/src/types/visualization.ts b/frontend/src/types/visualization.ts index 9c892e6..7469431 100644 --- a/frontend/src/types/visualization.ts +++ b/frontend/src/types/visualization.ts @@ -94,6 +94,29 @@ export interface MatrixState { pointers?: Record; } +// 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; + 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 {