feat: Implement Protocol 13 - Real-Time Dashboard Tracking
Complete implementation of Protocol 13 featuring real-time web dashboard for monitoring multi-objective optimization studies. ## New Features ### Backend (Python) - Real-time tracking system with per-trial JSON writes - New API endpoints for metadata, optimizer state, and Pareto fronts - Unit inference from objective descriptions - Multi-objective support using Optuna's best_trials API ### Frontend (React + TypeScript) - OptimizerPanel: Real-time optimizer state (phase, strategy, progress) - ParetoPlot: Pareto front visualization with normalization toggle - 3 modes: Raw, Min-Max [0-1], Z-Score standardization - Pareto front line connecting optimal points - ParallelCoordinatesPlot: High-dimensional interactive visualization - Objectives + design variables on parallel axes - Click-to-select, hover-to-highlight - Color-coded feasibility - Dynamic units throughout all visualizations ### Documentation - Comprehensive Protocol 13 guide with architecture, data flow, usage ## Files Added - `docs/PROTOCOL_13_DASHBOARD.md` - `atomizer-dashboard/frontend/src/components/OptimizerPanel.tsx` - `atomizer-dashboard/frontend/src/components/ParetoPlot.tsx` - `atomizer-dashboard/frontend/src/components/ParallelCoordinatesPlot.tsx` - `optimization_engine/realtime_tracking.py` ## Files Modified - `atomizer-dashboard/frontend/src/pages/Dashboard.tsx` - `atomizer-dashboard/backend/api/routes/optimization.py` - `optimization_engine/intelligent_optimizer.py` ## Testing - Tested with bracket_stiffness_optimization_V2 (30 trials, 20 Pareto solutions) - Dashboard running on localhost:3001 - All P1 and P2 features verified working 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
155
atomizer-dashboard/frontend/src/components/OptimizerPanel.tsx
Normal file
155
atomizer-dashboard/frontend/src/components/OptimizerPanel.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Intelligent Optimizer Panel - Protocol 13
|
||||
* Displays real-time optimizer state: phase, strategy, progress, confidence
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface OptimizerState {
|
||||
available: boolean;
|
||||
current_phase?: string;
|
||||
current_strategy?: string;
|
||||
trial_number?: number;
|
||||
total_trials?: number;
|
||||
is_multi_objective?: boolean;
|
||||
latest_recommendation?: {
|
||||
strategy: string;
|
||||
confidence: number;
|
||||
reasoning: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function OptimizerPanel({ studyId }: { studyId: string }) {
|
||||
const [state, setState] = useState<OptimizerState | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchState = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/optimization/studies/${studyId}/optimizer-state`);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
setState(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch optimizer state:', err);
|
||||
setError('Failed to load');
|
||||
}
|
||||
};
|
||||
|
||||
fetchState();
|
||||
const interval = setInterval(fetchState, 1000); // Update every second
|
||||
return () => clearInterval(interval);
|
||||
}, [studyId]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
|
||||
<h3 className="text-lg font-semibold mb-4 text-dark-100">Intelligent Optimizer</h3>
|
||||
<div className="text-dark-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!state?.available) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Format phase name for display
|
||||
const formatPhase = (phase?: string) => {
|
||||
if (!phase) return 'Unknown';
|
||||
return phase
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
// Format strategy name for display
|
||||
const formatStrategy = (strategy?: string) => {
|
||||
if (!strategy) return 'Not set';
|
||||
return strategy.toUpperCase();
|
||||
};
|
||||
|
||||
const progress = state.trial_number && state.total_trials
|
||||
? (state.trial_number / state.total_trials) * 100
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
|
||||
<h3 className="text-lg font-semibold mb-4 text-dark-100 flex items-center gap-2">
|
||||
Intelligent Optimizer
|
||||
{state.is_multi_objective && (
|
||||
<span className="text-xs bg-purple-500/20 text-purple-300 px-2 py-1 rounded">
|
||||
Multi-Objective
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Phase */}
|
||||
<div>
|
||||
<div className="text-sm text-dark-300 mb-1">Phase</div>
|
||||
<div className="text-lg font-semibold text-primary-400">
|
||||
{formatPhase(state.current_phase)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Strategy */}
|
||||
<div>
|
||||
<div className="text-sm text-dark-300 mb-1">Current Strategy</div>
|
||||
<div className="text-lg font-semibold text-blue-400">
|
||||
{formatStrategy(state.current_strategy)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
<div>
|
||||
<div className="text-sm text-dark-300 mb-1">Progress</div>
|
||||
<div className="text-lg text-dark-100">
|
||||
{state.trial_number || 0} / {state.total_trials || 0} trials
|
||||
</div>
|
||||
<div className="w-full bg-dark-500 rounded-full h-2 mt-2">
|
||||
<div
|
||||
className="bg-primary-400 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confidence (if available) */}
|
||||
{state.latest_recommendation && (
|
||||
<div>
|
||||
<div className="text-sm text-dark-300 mb-1">Confidence</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 bg-dark-500 rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-400 h-2 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${state.latest_recommendation.confidence * 100}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-mono text-dark-200 min-w-[3rem] text-right">
|
||||
{(state.latest_recommendation.confidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reasoning (if available) */}
|
||||
{state.latest_recommendation && (
|
||||
<div>
|
||||
<div className="text-sm text-dark-300 mb-1">Reasoning</div>
|
||||
<div className="text-sm text-dark-100 bg-dark-800 rounded p-3 border border-dark-600">
|
||||
{state.latest_recommendation.reasoning}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* 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<string, number>;
|
||||
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<number | null>(null);
|
||||
const [selectedTrials, setSelectedTrials] = useState<Set<number>>(new Set());
|
||||
|
||||
if (paretoData.length === 0) {
|
||||
return (
|
||||
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
|
||||
<h3 className="text-lg font-semibold mb-4 text-dark-100">Parallel Coordinates</h3>
|
||||
<div className="h-96 flex items-center justify-center text-dark-300">
|
||||
No Pareto front data yet
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-dark-100">
|
||||
Parallel Coordinates ({paretoData.length} solutions)
|
||||
</h3>
|
||||
{selectedTrials.size > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedTrials(new Set())}
|
||||
className="text-xs px-3 py-1 bg-dark-600 hover:bg-dark-500 rounded text-dark-200"
|
||||
>
|
||||
Clear Selection ({selectedTrials.size})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<svg width={width} height={height} className="overflow-visible">
|
||||
<g transform={`translate(${margin.left}, ${margin.top})`}>
|
||||
{/* Draw axes */}
|
||||
{axes.map((axis, i) => {
|
||||
const x = i * axisSpacing;
|
||||
return (
|
||||
<g key={axis.name} transform={`translate(${x}, 0)`}>
|
||||
{/* Axis line */}
|
||||
<line
|
||||
y1={0}
|
||||
y2={plotHeight}
|
||||
stroke="#475569"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
|
||||
{/* Axis label */}
|
||||
<text
|
||||
y={-10}
|
||||
textAnchor="middle"
|
||||
fill="#94a3b8"
|
||||
fontSize={12}
|
||||
className="select-none"
|
||||
transform={`rotate(-45, 0, -10)`}
|
||||
>
|
||||
{axis.label}
|
||||
</text>
|
||||
|
||||
{/* Min/max labels */}
|
||||
<text
|
||||
y={plotHeight + 15}
|
||||
textAnchor="middle"
|
||||
fill="#64748b"
|
||||
fontSize={10}
|
||||
>
|
||||
{ranges[i].min.toFixed(2)}
|
||||
</text>
|
||||
<text
|
||||
y={-25}
|
||||
textAnchor="middle"
|
||||
fill="#64748b"
|
||||
fontSize={10}
|
||||
>
|
||||
{ranges[i].max.toFixed(2)}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 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 (
|
||||
<path
|
||||
key={trial.trial_number}
|
||||
d={pathData}
|
||||
fill="none"
|
||||
stroke={
|
||||
isSelected ? '#fbbf24' :
|
||||
trial.feasible ? '#10b981' : '#ef4444'
|
||||
}
|
||||
strokeWidth={isHighlighted ? 2.5 : 1}
|
||||
opacity={
|
||||
selectedTrials.size > 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 && (
|
||||
<g transform={`translate(${plotWidth + 10}, 20)`}>
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={120}
|
||||
height={60}
|
||||
fill="#1e293b"
|
||||
stroke="#334155"
|
||||
strokeWidth={1}
|
||||
rx={4}
|
||||
/>
|
||||
<text x={10} y={20} fill="#e2e8f0" fontSize={12} fontWeight="bold">
|
||||
Trial #{hoveredTrial}
|
||||
</text>
|
||||
<text x={10} y={38} fill="#94a3b8" fontSize={10}>
|
||||
Click to select
|
||||
</text>
|
||||
<text x={10} y={52} fill="#94a3b8" fontSize={10}>
|
||||
{selectedTrials.has(hoveredTrial) ? '✓ Selected' : '○ Not selected'}
|
||||
</text>
|
||||
</g>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex gap-6 justify-center mt-4 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-0.5 bg-green-400" />
|
||||
<span className="text-dark-200">Feasible</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-0.5 bg-red-400" />
|
||||
<span className="text-dark-200">Infeasible</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-0.5 bg-yellow-400" style={{ height: '2px' }} />
|
||||
<span className="text-dark-200">Selected</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
247
atomizer-dashboard/frontend/src/components/ParetoPlot.tsx
Normal file
247
atomizer-dashboard/frontend/src/components/ParetoPlot.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* Pareto Front Plot - Protocol 13
|
||||
* Visualizes Pareto-optimal solutions for multi-objective optimization
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ScatterChart, Scatter, Line, XAxis, YAxis, CartesianGrid, Tooltip, Cell, ResponsiveContainer, Legend } from 'recharts';
|
||||
|
||||
interface ParetoTrial {
|
||||
trial_number: number;
|
||||
values: [number, number];
|
||||
params: Record<string, number>;
|
||||
constraint_satisfied?: boolean;
|
||||
}
|
||||
|
||||
interface Objective {
|
||||
name: string;
|
||||
type: 'minimize' | 'maximize';
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
interface ParetoPlotProps {
|
||||
paretoData: ParetoTrial[];
|
||||
objectives: Objective[];
|
||||
}
|
||||
|
||||
type NormalizationMode = 'raw' | 'minmax' | 'zscore';
|
||||
|
||||
export function ParetoPlot({ paretoData, objectives }: ParetoPlotProps) {
|
||||
const [normMode, setNormMode] = useState<NormalizationMode>('raw');
|
||||
|
||||
if (paretoData.length === 0) {
|
||||
return (
|
||||
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
|
||||
<h3 className="text-lg font-semibold mb-4 text-dark-100">Pareto Front</h3>
|
||||
<div className="h-64 flex items-center justify-center text-dark-300">
|
||||
No Pareto front data yet
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Extract raw values
|
||||
const rawData = paretoData.map(trial => ({
|
||||
x: trial.values[0],
|
||||
y: trial.values[1],
|
||||
trial_number: trial.trial_number,
|
||||
feasible: trial.constraint_satisfied !== false
|
||||
}));
|
||||
|
||||
// Calculate statistics for normalization
|
||||
const xValues = rawData.map(d => d.x);
|
||||
const yValues = rawData.map(d => d.y);
|
||||
|
||||
const xMin = Math.min(...xValues);
|
||||
const xMax = Math.max(...xValues);
|
||||
const yMin = Math.min(...yValues);
|
||||
const yMax = Math.max(...yValues);
|
||||
|
||||
const xMean = xValues.reduce((a, b) => a + b, 0) / xValues.length;
|
||||
const yMean = yValues.reduce((a, b) => a + b, 0) / yValues.length;
|
||||
|
||||
const xStd = Math.sqrt(xValues.reduce((sum, val) => sum + Math.pow(val - xMean, 2), 0) / xValues.length);
|
||||
const yStd = Math.sqrt(yValues.reduce((sum, val) => sum + Math.pow(val - yMean, 2), 0) / yValues.length);
|
||||
|
||||
// Normalize data based on selected mode
|
||||
const normalizeX = (val: number): number => {
|
||||
if (normMode === 'minmax') {
|
||||
return xMax === xMin ? 0.5 : (val - xMin) / (xMax - xMin);
|
||||
} else if (normMode === 'zscore') {
|
||||
return xStd === 0 ? 0 : (val - xMean) / xStd;
|
||||
}
|
||||
return val; // raw
|
||||
};
|
||||
|
||||
const normalizeY = (val: number): number => {
|
||||
if (normMode === 'minmax') {
|
||||
return yMax === yMin ? 0.5 : (val - yMin) / (yMax - yMin);
|
||||
} else if (normMode === 'zscore') {
|
||||
return yStd === 0 ? 0 : (val - yMean) / yStd;
|
||||
}
|
||||
return val; // raw
|
||||
};
|
||||
|
||||
// Transform data with normalization
|
||||
const data = rawData.map(d => ({
|
||||
x: normalizeX(d.x),
|
||||
y: normalizeY(d.y),
|
||||
rawX: d.x,
|
||||
rawY: d.y,
|
||||
trial_number: d.trial_number,
|
||||
feasible: d.feasible
|
||||
}));
|
||||
|
||||
// Sort data by x-coordinate for Pareto front line
|
||||
const sortedData = [...data].sort((a, b) => a.x - b.x);
|
||||
|
||||
// Get objective labels with normalization indicator
|
||||
const normSuffix = normMode === 'minmax' ? ' [0-1]' : normMode === 'zscore' ? ' [z-score]' : '';
|
||||
const xLabel = objectives[0]
|
||||
? `${objectives[0].name}${objectives[0].unit ? ` (${objectives[0].unit})` : ''}${normSuffix}`
|
||||
: `Objective 1${normSuffix}`;
|
||||
const yLabel = objectives[1]
|
||||
? `${objectives[1].name}${objectives[1].unit ? ` (${objectives[1].unit})` : ''}${normSuffix}`
|
||||
: `Objective 2${normSuffix}`;
|
||||
|
||||
// Custom tooltip (always shows raw values)
|
||||
const CustomTooltip = ({ active, payload }: any) => {
|
||||
if (!active || !payload || payload.length === 0) return null;
|
||||
|
||||
const point = payload[0].payload;
|
||||
return (
|
||||
<div className="bg-dark-800 border border-dark-600 rounded p-3 shadow-lg">
|
||||
<div className="text-sm font-semibold text-dark-100 mb-2">
|
||||
Trial #{point.trial_number}
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="text-dark-200">
|
||||
{objectives[0]?.name || 'Obj 1'}: <span className="font-mono">{point.rawX.toFixed(4)}</span>
|
||||
</div>
|
||||
<div className="text-dark-200">
|
||||
{objectives[1]?.name || 'Obj 2'}: <span className="font-mono">{point.rawY.toFixed(4)}</span>
|
||||
</div>
|
||||
<div className={point.feasible ? 'text-green-400' : 'text-red-400'}>
|
||||
{point.feasible ? '✓ Feasible' : '✗ Infeasible'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-dark-100">
|
||||
Pareto Front ({paretoData.length} solutions)
|
||||
</h3>
|
||||
|
||||
{/* Normalization Toggle */}
|
||||
<div className="flex gap-1 bg-dark-800 rounded p-1 border border-dark-600">
|
||||
<button
|
||||
onClick={() => setNormMode('raw')}
|
||||
className={`px-3 py-1 text-xs rounded transition-colors ${
|
||||
normMode === 'raw'
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'text-dark-300 hover:text-dark-100'
|
||||
}`}
|
||||
>
|
||||
Raw
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setNormMode('minmax')}
|
||||
className={`px-3 py-1 text-xs rounded transition-colors ${
|
||||
normMode === 'minmax'
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'text-dark-300 hover:text-dark-100'
|
||||
}`}
|
||||
>
|
||||
Min-Max
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setNormMode('zscore')}
|
||||
className={`px-3 py-1 text-xs rounded transition-colors ${
|
||||
normMode === 'zscore'
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'text-dark-300 hover:text-dark-100'
|
||||
}`}
|
||||
>
|
||||
Z-Score
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<ScatterChart margin={{ top: 20, right: 20, bottom: 60, left: 60 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
||||
<XAxis
|
||||
type="number"
|
||||
dataKey="x"
|
||||
name={objectives[0]?.name || 'Objective 1'}
|
||||
stroke="#94a3b8"
|
||||
label={{
|
||||
value: xLabel,
|
||||
position: 'insideBottom',
|
||||
offset: -45,
|
||||
fill: '#94a3b8',
|
||||
style: { fontSize: '14px' }
|
||||
}}
|
||||
tick={{ fill: '#94a3b8' }}
|
||||
/>
|
||||
<YAxis
|
||||
type="number"
|
||||
dataKey="y"
|
||||
name={objectives[1]?.name || 'Objective 2'}
|
||||
stroke="#94a3b8"
|
||||
label={{
|
||||
value: yLabel,
|
||||
angle: -90,
|
||||
position: 'insideLeft',
|
||||
offset: -45,
|
||||
fill: '#94a3b8',
|
||||
style: { fontSize: '14px' }
|
||||
}}
|
||||
tick={{ fill: '#94a3b8' }}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Legend
|
||||
verticalAlign="top"
|
||||
height={36}
|
||||
content={() => (
|
||||
<div className="flex gap-4 justify-center mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-green-400" />
|
||||
<span className="text-sm text-dark-200">Feasible</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-400" />
|
||||
<span className="text-sm text-dark-200">Infeasible</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{/* Pareto front line */}
|
||||
<Line
|
||||
type="monotone"
|
||||
data={sortedData}
|
||||
dataKey="y"
|
||||
stroke="#8b5cf6"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
connectNulls={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Scatter name="Pareto Front" data={data}>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={entry.feasible ? '#10b981' : '#ef4444'}
|
||||
r={entry.feasible ? 6 : 4}
|
||||
opacity={entry.feasible ? 1 : 0.6}
|
||||
/>
|
||||
))}
|
||||
</Scatter>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user