/** * Convergence Plot * Shows optimization progress over time with running best and improvement rate */ import { useMemo } from 'react'; import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine, Area, ComposedChart, Legend } from 'recharts'; interface Trial { trial_number: number; values: number[]; state?: string; } interface ConvergencePlotProps { trials: Trial[]; objectiveIndex?: number; objectiveName?: string; direction?: 'minimize' | 'maximize'; } export function ConvergencePlot({ trials, objectiveIndex = 0, objectiveName = 'Objective', direction = 'minimize' }: ConvergencePlotProps) { const convergenceData = useMemo(() => { if (!trials || trials.length === 0) return []; // Sort by trial number const sortedTrials = [...trials] .filter(t => t.values && t.values.length > objectiveIndex && t.state !== 'FAIL') .sort((a, b) => a.trial_number - b.trial_number); if (sortedTrials.length === 0) return []; let runningBest = direction === 'minimize' ? Infinity : -Infinity; const data = sortedTrials.map((trial, idx) => { const value = trial.values[objectiveIndex]; // Update running best if (direction === 'minimize') { runningBest = Math.min(runningBest, value); } else { runningBest = Math.max(runningBest, value); } // Calculate improvement from first trial const firstValue = sortedTrials[0].values[objectiveIndex]; const improvement = direction === 'minimize' ? ((firstValue - runningBest) / firstValue) * 100 : ((runningBest - firstValue) / firstValue) * 100; return { trial: trial.trial_number, value, best: runningBest, improvement: Math.max(0, improvement), index: idx + 1 }; }); return data; }, [trials, objectiveIndex, direction]); // Calculate statistics const stats = useMemo(() => { if (convergenceData.length === 0) return null; const values = convergenceData.map(d => d.value); const best = convergenceData[convergenceData.length - 1]?.best ?? 0; const first = convergenceData[0]?.value ?? 0; const improvement = direction === 'minimize' ? ((first - best) / first) * 100 : ((best - first) / first) * 100; // Calculate when we found 90% of the improvement const targetImprovement = 0.9 * improvement; let trialsTo90 = convergenceData.length; for (let i = 0; i < convergenceData.length; i++) { if (convergenceData[i].improvement >= targetImprovement) { trialsTo90 = i + 1; break; } } return { best, first, improvement: Math.max(0, improvement), totalTrials: convergenceData.length, trialsTo90, mean: values.reduce((a, b) => a + b, 0) / values.length, std: Math.sqrt(values.map(v => Math.pow(v - values.reduce((a, b) => a + b, 0) / values.length, 2)).reduce((a, b) => a + b, 0) / values.length) }; }, [convergenceData, direction]); if (convergenceData.length === 0) { return (

Convergence Plot

No trial data available
); } return (

Convergence Plot

{objectiveName} over {convergenceData.length} trials

{stats && (
Best
{stats.best.toFixed(4)}
Improvement
{stats.improvement.toFixed(1)}%
90% at trial
#{stats.trialsTo90}
)}
v.toFixed(2)} label={{ value: objectiveName, angle: -90, position: 'insideLeft', fill: '#94a3b8', fontSize: 12 }} /> { const label = name === 'value' ? 'Trial Value' : name === 'best' ? 'Running Best' : name; return [value.toFixed(4), label]; }} labelFormatter={(label) => `Trial #${label}`} /> value === 'value' ? 'Trial Value' : 'Running Best'} /> {/* Area under trial values */} {/* Trial values as scatter points */} {/* Running best line */} {/* Reference line at best value */} {stats && ( )}
{/* Summary stats bar */} {stats && (
First Value
{stats.first.toFixed(4)}
Mean
{stats.mean.toFixed(4)}
Std Dev
{stats.std.toFixed(4)}
Total Trials
{stats.totalTrials}
)}
); }