/** * ConvergenceSparkline - Tiny SVG chart showing optimization convergence * * Displays the last N trial values as a mini line chart. * Used on ObjectiveNode to show convergence trend. */ import { useMemo } from 'react'; interface ConvergenceSparklineProps { /** Array of values (most recent last) */ values: number[]; /** Width in pixels */ width?: number; /** Height in pixels */ height?: number; /** Line color */ color?: string; /** Best value line color */ bestColor?: string; /** Whether to show the best value line */ showBest?: boolean; /** Direction: minimize shows lower as better, maximize shows higher as better */ direction?: 'minimize' | 'maximize'; /** Show dots at each point */ showDots?: boolean; /** Number of points to display */ maxPoints?: number; } export function ConvergenceSparkline({ values, width = 80, height = 24, color = '#60a5fa', bestColor = '#34d399', showBest = true, direction = 'minimize', showDots = false, maxPoints = 20, }: ConvergenceSparklineProps) { const { path, bestY, points } = useMemo(() => { if (!values || values.length === 0) { return { path: '', bestY: null, points: [], minVal: 0, maxVal: 1 }; } // Take last N points const data = values.slice(-maxPoints); if (data.length === 0) { return { path: '', bestY: null, points: [], minVal: 0, maxVal: 1 }; } // Calculate bounds with padding const minVal = Math.min(...data); const maxVal = Math.max(...data); const range = maxVal - minVal || 1; const padding = range * 0.1; const yMin = minVal - padding; const yMax = maxVal + padding; const yRange = yMax - yMin; // Calculate best value const bestVal = direction === 'minimize' ? Math.min(...data) : Math.max(...data); // Map values to SVG coordinates const xStep = width / Math.max(data.length - 1, 1); const mapY = (v: number) => height - ((v - yMin) / yRange) * height; // Build path const points = data.map((v, i) => ({ x: i * xStep, y: mapY(v), value: v, })); const pathParts = points.map((p, i) => i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}` ); return { path: pathParts.join(' '), bestY: mapY(bestVal), points, minVal, maxVal, }; }, [values, width, height, maxPoints, direction]); if (!values || values.length === 0) { return (