/** * Parallel Coordinates Plot - Enhanced Multi-Objective Visualization * Shows design variables → objectives → constraints in proper research format * Light theme with high visibility */ import { useState, useMemo } from 'react'; interface ParetoTrial { trial_number: number; values: number[]; params: Record; user_attrs?: Record; constraint_satisfied?: boolean; source?: 'FEA' | 'NN' | 'V10_FEA'; // Trial source for FEA vs NN differentiation } interface Objective { name: string; type?: 'minimize' | 'maximize'; direction?: 'minimize' | 'maximize'; // Alternative name used in some configs unit?: string; } interface DesignVariable { name: string; parameter?: string; // Optional: the actual parameter name if different from name unit?: string; min?: number; max?: number; } interface Constraint { name: string; threshold: number; type: 'less_than' | 'greater_than'; unit?: string; } interface ParallelCoordinatesPlotProps { paretoData: ParetoTrial[]; objectives: Objective[]; designVariables: DesignVariable[]; constraints?: Constraint[]; paretoFront?: ParetoTrial[]; maxTrials?: number; // Limit displayed trials for performance (default: 1000) } export function ParallelCoordinatesPlot({ paretoData, objectives, designVariables, constraints: _constraints = [], paretoFront = [], maxTrials = 500 }: ParallelCoordinatesPlotProps) { const [hoveredTrial, setHoveredTrial] = useState(null); const [selectedTrials, setSelectedTrials] = useState>(new Set()); const [showNN, setShowNN] = useState(true); // Toggle for NN trials visibility // Create set of Pareto front trial numbers for easy lookup const paretoTrialNumbers = new Set(paretoFront.map(t => t.trial_number)); // Check if we have any NN trials (to show/hide toggle) const hasNNTrials = useMemo(() => { return paretoData.some(t => t.source === 'NN' || t.user_attrs?.source === 'NN'); }, [paretoData]); // Filter to best N trials for performance when dataset is large // Also filter by source type based on toggle const filteredParetoData = useMemo(() => { // First filter by source if NN is hidden let data = paretoData; if (!showNN) { data = data.filter(t => { const source = t.source || t.user_attrs?.source || 'FEA'; return source !== 'NN'; }); } if (data.length <= maxTrials) return data; // Sort by first objective value and take best N const sorted = [...data].sort((a, b) => { const aVal = a.values[0] ?? Infinity; const bVal = b.values[0] ?? Infinity; return aVal - bVal; }); return sorted.slice(0, maxTrials); }, [paretoData, maxTrials, showNN]); const totalTrialsCount = paretoData.length; const displayedTrialsCount = filteredParetoData.length; const isFiltered = totalTrialsCount > maxTrials; // Safety checks if (!paretoData || paretoData.length === 0) { return (

Parallel Coordinates Plot

No Pareto front data available
); } if (!objectives || objectives.length === 0 || !designVariables || designVariables.length === 0) { return (

Parallel Coordinates Plot

Missing objectives or design variables metadata
); } // Structure axes: Design Variables → Objectives → Constraints const axes: Array<{ name: string; label: string; type: 'design_var' | 'objective' | 'constraint'; unit?: string; }> = []; // Add design variables designVariables.forEach(dv => { const paramName = dv.parameter || dv.name; // Support both formats axes.push({ name: paramName, label: dv.unit ? `${paramName}\n(${dv.unit})` : paramName, type: 'design_var', unit: dv.unit }); }); // Add objectives objectives.forEach((obj, i) => { axes.push({ name: `objective_${i}`, label: obj.unit ? `${obj.name}\n(${obj.unit})` : obj.name, type: 'objective', unit: obj.unit }); }); // Add constraints (extract from user_attrs) const constraintNames = new Set(); filteredParetoData.forEach(trial => { if (trial.user_attrs) { // Common constraint metrics if (trial.user_attrs.max_stress !== undefined) constraintNames.add('max_stress'); if (trial.user_attrs.max_displacement !== undefined) constraintNames.add('max_displacement'); if (trial.user_attrs.frequency !== undefined && objectives.findIndex(obj => obj.name.toLowerCase().includes('freq')) === -1) { constraintNames.add('frequency'); } if (trial.user_attrs.mass !== undefined && objectives.findIndex(obj => obj.name.toLowerCase().includes('mass')) === -1) { constraintNames.add('mass'); } } }); constraintNames.forEach(name => { const unit = name.includes('stress') ? 'MPa' : name.includes('displacement') ? 'mm' : name.includes('frequency') ? 'Hz' : name.includes('mass') ? 'g' : ''; axes.push({ name: `constraint_${name}`, label: unit ? `${name}\n(${unit})` : name, type: 'constraint', unit }); }); // Extract values for each axis const trialData = filteredParetoData.map(trial => { const values: number[] = []; // Design variables - use .parameter field from metadata designVariables.forEach(dv => { const paramName = dv.parameter || dv.name; // Support both formats values.push(trial.params[paramName] ?? 0); }); // Objectives trial.values.forEach(val => { values.push(val); }); // Constraints constraintNames.forEach(name => { values.push(trial.user_attrs?.[name] ?? 0); }); return { trial_number: trial.trial_number, values, feasible: trial.constraint_satisfied !== false, objectiveValues: trial.values || [], source: trial.source || trial.user_attrs?.source || 'FEA' // Default to FEA }; }); // Rank trials by their first objective (for multi-objective, this is just one metric) // For proper multi-objective ranking, we use Pareto dominance const rankedTrials = [...trialData].sort((a, b) => { // Primary: Pareto front members come first const aIsPareto = paretoTrialNumbers.has(a.trial_number); const bIsPareto = paretoTrialNumbers.has(b.trial_number); if (aIsPareto && !bIsPareto) return -1; if (!aIsPareto && bIsPareto) return 1; // Secondary: Sort by first objective value (minimize assumed) const aObj = a.objectiveValues[0] ?? Infinity; const bObj = b.objectiveValues[0] ?? Infinity; return aObj - bObj; }); // Create ranking map: trial_number -> rank (0-indexed) const trialRanks = new Map(); rankedTrials.forEach((trial, index) => { trialRanks.set(trial.trial_number, index); }); // Calculate min/max for normalization const ranges = axes.map((_, axisIdx) => { const values = trialData.map(d => d.values[axisIdx]); return { min: Math.min(...values), max: Math.max(...values) }; }); // Normalize to [0, 1] const normalize = (value: number, axisIdx: number): number => { const range = ranges[axisIdx]; if (range.max === range.min) return 0.5; return (value - range.min) / (range.max - range.min); }; // Chart dimensions const width = 1400; const height = 500; const margin = { top: 100, right: 50, bottom: 60, left: 50 }; const plotWidth = width - margin.left - margin.right; const plotHeight = height - margin.top - margin.bottom; const axisSpacing = plotWidth / (axes.length - 1); // Toggle trial selection const toggleTrial = (trialNum: number) => { const newSelected = new Set(selectedTrials); if (newSelected.has(trialNum)) { newSelected.delete(trialNum); } else { newSelected.add(trialNum); } setSelectedTrials(newSelected); }; // Color scheme - FEA trials: blue gradient, NN trials: orange gradient const totalTrials = trialData.length; const getLineColor = (trial: typeof trialData[0], isHovered: boolean, isSelected: boolean) => { if (isSelected) return '#FF6B00'; // Bright orange for selected if (isHovered) return '#2563EB'; // Blue for hover if (!trial.feasible) return '#DC2626'; // Red for infeasible const rank = trialRanks.get(trial.trial_number) ?? totalTrials - 1; const t = totalTrials > 1 ? rank / (totalTrials - 1) : 0; // FEA trials: dark blue to light blue gradient (validated, trustworthy) // NN trials: dark orange to light orange gradient (predictions, less reliable) const isNN = trial.source === 'NN'; if (isNN) { // Orange gradient: rgb(194, 65, 12) to rgb(254, 215, 170) const r = Math.round(194 + t * (254 - 194)); const g = Math.round(65 + t * (215 - 65)); const b = Math.round(12 + t * (170 - 12)); return `rgb(${r}, ${g}, ${b})`; } else { // Blue gradient: rgb(30, 64, 175) to rgb(191, 219, 254) const r = Math.round(30 + t * (191 - 30)); const g = Math.round(64 + t * (219 - 64)); const b = Math.round(175 + t * (254 - 175)); return `rgb(${r}, ${g}, ${b})`; } }; // Opacity gradient: best trials are more opaque, worst are more transparent const getLineOpacity = (trial: typeof trialData[0], isHighlighted: boolean, hasSelection: boolean, isSelected: boolean) => { if (hasSelection) { return isSelected ? 0.95 : 0.08; } if (isHighlighted) return 0.95; const rank = trialRanks.get(trial.trial_number) ?? totalTrials - 1; const t = totalTrials > 1 ? rank / (totalTrials - 1) : 0; // Best trials: 0.9 opacity, worst trials: 0.2 opacity return 0.9 - t * 0.7; }; return (

Parallel Coordinates Plot ({displayedTrialsCount} solutions{isFiltered ? ` of ${totalTrialsCount}` : ''})

Design Variables → Objectives → Constraints {isFiltered && (showing best {maxTrials} for performance)}

{/* NN Toggle - only show if there are NN trials */} {hasNNTrials && ( )} {selectedTrials.size > 0 && ( )}
{/* Draw axes */} {axes.map((axis, i) => { const x = i * axisSpacing; const bgColor = axis.type === 'design_var' ? '#EFF6FF' : axis.type === 'objective' ? '#F0FDF4' : '#FEF3C7'; return ( {/* Background highlight */} {/* Axis line */} {/* Axis label */} {(axis.label || '').split('\n').map((line, idx) => ( {line} ))} {/* Type badge */} {axis.type === 'design_var' ? 'DESIGN VAR' : axis.type === 'objective' ? 'OBJECTIVE' : 'CONSTRAINT'} {/* Min/max labels */} {ranges[i].min.toFixed(2)} {ranges[i].max.toFixed(2)} ); })} {/* Draw polylines for each trial - sorted so best trials render on top */} {[...trialData] .sort((a, b) => { // Render worst trials first (bottom), best trials last (top) const rankA = trialRanks.get(a.trial_number) ?? totalTrials; const rankB = trialRanks.get(b.trial_number) ?? totalTrials; return rankB - rankA; // Higher rank (worse) renders first }) .map(trial => { const isHovered = hoveredTrial === trial.trial_number; const isSelected = selectedTrials.has(trial.trial_number); const isHighlighted = isHovered || isSelected; // Build path const pathData = axes.map((_, i) => { const x = i * axisSpacing; const normalizedY = normalize(trial.values[i], i); const y = plotHeight * (1 - normalizedY); return i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`; }).join(' '); return ( 0, isSelected)} strokeLinecap="round" strokeLinejoin="round" className="transition-all duration-150 cursor-pointer" style={{ filter: isHighlighted ? 'drop-shadow(0 2px 4px rgba(0,0,0,0.2))' : 'none' }} onMouseEnter={() => setHoveredTrial(trial.trial_number)} onMouseLeave={() => setHoveredTrial(null)} onClick={() => toggleTrial(trial.trial_number)} /> ); })} {/* Hover tooltip */} {hoveredTrial !== null && ( Trial #{hoveredTrial} Click to select/deselect {selectedTrials.has(hoveredTrial) ? '✓ Selected' : '○ Not selected'} )}
{/* Legend */}
FEA (validated)
NN (predicted)
Infeasible
Selected
Hover
{/* Axis type legend */}
Design Variables
Objectives
Constraints
); }