/** * Parallel Coordinates Plot - Protocol 13 * High-dimensional visualization for multi-objective Pareto fronts * Shows objectives and design variables as parallel axes */ import { useState } from 'react'; interface ParetoTrial { trial_number: number; values: number[]; params: Record; constraint_satisfied?: boolean; } interface Objective { name: string; type: 'minimize' | 'maximize'; unit?: string; } interface DesignVariable { name: string; unit?: string; min: number; max: number; } interface ParallelCoordinatesPlotProps { paretoData: ParetoTrial[]; objectives: Objective[]; designVariables: DesignVariable[]; } export function ParallelCoordinatesPlot({ paretoData, objectives, designVariables }: ParallelCoordinatesPlotProps) { const [hoveredTrial, setHoveredTrial] = useState(null); const [selectedTrials, setSelectedTrials] = useState>(new Set()); if (paretoData.length === 0) { return (

Parallel Coordinates

No Pareto front data yet
); } // Combine objectives and design variables into axes const axes: Array<{name: string, label: string, type: 'objective' | 'param'}> = [ ...objectives.map((obj, i) => ({ name: `obj_${i}`, label: obj.unit ? `${obj.name} (${obj.unit})` : obj.name, type: 'objective' as const })), ...designVariables.map(dv => ({ name: dv.name, label: dv.unit ? `${dv.name} (${dv.unit})` : dv.name, type: 'param' as const })) ]; // Normalize data to [0, 1] for each axis const normalizedData = paretoData.map(trial => { const allValues: number[] = []; // Add objectives trial.values.forEach(val => allValues.push(val)); // Add design variables designVariables.forEach(dv => { allValues.push(trial.params[dv.name]); }); return { trial_number: trial.trial_number, values: allValues, feasible: trial.constraint_satisfied !== false }; }); // Calculate min/max for each axis const ranges = axes.map((_, axisIdx) => { const values = normalizedData.map(d => d.values[axisIdx]); return { min: Math.min(...values), max: Math.max(...values) }; }); // Normalize function 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 = 800; const height = 400; const margin = { top: 80, right: 20, bottom: 40, left: 20 }; 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); }; return (

Parallel Coordinates ({paretoData.length} solutions)

{selectedTrials.size > 0 && ( )}
{/* Draw axes */} {axes.map((axis, i) => { const x = i * axisSpacing; return ( {/* Axis line */} {/* Axis label */} {axis.label} {/* Min/max labels */} {ranges[i].min.toFixed(2)} {ranges[i].max.toFixed(2)} ); })} {/* Draw lines for each trial */} {normalizedData.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 ? 1 : 0.1) : (isHighlighted ? 1 : 0.4) } strokeLinecap="round" strokeLinejoin="round" className="transition-all duration-200 cursor-pointer" onMouseEnter={() => setHoveredTrial(trial.trial_number)} onMouseLeave={() => setHoveredTrial(null)} onClick={() => toggleTrial(trial.trial_number)} /> ); })} {/* Hover tooltip */} {hoveredTrial !== null && ( Trial #{hoveredTrial} Click to select {selectedTrials.has(hoveredTrial) ? '✓ Selected' : '○ Not selected'} )} {/* Legend */}
Feasible
Infeasible
Selected
); }