/** * LossChart & ProgressRing Components * * SVG-based visualizations for training metrics: * - LossChart: Line chart for training/validation loss curves * - ProgressRing: Circular progress indicator */ 'use client' import type { TrainingDataPoint } from './training-metrics-types' /** * Simple line chart component for loss visualization */ export function LossChart({ data, height = 200, className = '' }: { data: TrainingDataPoint[] height?: number className?: string }) { if (data.length === 0) { return (
Keine Daten
) } // Calculate bounds const losses = data.map(d => d.loss) const valLosses = data.filter(d => d.val_loss !== undefined).map(d => d.val_loss!) const allLosses = [...losses, ...valLosses] const minLoss = Math.min(...allLosses) * 0.9 const maxLoss = Math.max(...allLosses) * 1.1 const lossRange = maxLoss - minLoss || 1 // SVG dimensions const width = 400 const padding = { top: 20, right: 20, bottom: 30, left: 50 } const chartWidth = width - padding.left - padding.right const chartHeight = height - padding.top - padding.bottom // Generate path for loss line const generatePath = (values: number[]) => { if (values.length === 0) return '' return values .map((loss, idx) => { const x = padding.left + (idx / (values.length - 1 || 1)) * chartWidth const y = padding.top + chartHeight - ((loss - minLoss) / lossRange) * chartHeight return `${idx === 0 ? 'M' : 'L'} ${x} ${y}` }) .join(' ') } const lossPath = generatePath(losses) const valLossPath = valLosses.length > 0 ? generatePath(valLosses) : '' // Y-axis labels const yLabels = [maxLoss, (maxLoss + minLoss) / 2, minLoss].map(v => v.toFixed(3)) return ( {/* Grid lines */} {[0, 0.5, 1].map((ratio, idx) => ( ))} {/* Y-axis labels */} {yLabels.map((label, idx) => ( {label} ))} {/* X-axis label */} Epoche / Schritt {/* Y-axis label */} Loss {/* Training loss line */} {/* Validation loss line (dashed) */} {valLossPath && ( )} {/* Data points */} {data.map((point, idx) => { const x = padding.left + (idx / (data.length - 1 || 1)) * chartWidth const y = padding.top + chartHeight - ((point.loss - minLoss) / lossRange) * chartHeight return ( Epoch {point.epoch}, Step {point.step}: {point.loss.toFixed(4)} ) })} {/* Legend */} Training Loss {valLossPath && ( <> Val Loss )} ) } /** * Progress ring component */ export function ProgressRing({ progress, size = 80, strokeWidth = 6, className = '' }: { progress: number size?: number strokeWidth?: number className?: string }) { const radius = (size - strokeWidth) / 2 const circumference = radius * 2 * Math.PI const offset = circumference - (progress / 100) * circumference return ( {/* Background circle */} {/* Progress circle */} {/* Center text */} {progress.toFixed(0)}% ) }