refactor(dashboard): Remove unused Plotly components
Removed plotly/ directory with unused chart wrappers: - PlotlyConvergencePlot, PlotlyCorrelationHeatmap - PlotlyFeasibilityChart, PlotlyParallelCoordinates - PlotlyParameterImportance, PlotlyParetoPlot - PlotlyRunComparison, PlotlySurrogateQuality These were replaced by Recharts-based implementations.
This commit is contained in:
@@ -1,260 +0,0 @@
|
||||
/**
|
||||
* PlotlyConvergencePlot - Interactive convergence plot using Plotly
|
||||
*
|
||||
* Features:
|
||||
* - Line plot showing objective vs trial number
|
||||
* - Best-so-far trace overlay
|
||||
* - FEA vs NN trial differentiation
|
||||
* - Hover tooltips with trial details
|
||||
* - Range slider for zooming
|
||||
* - Log scale toggle
|
||||
* - Export to PNG/SVG
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import Plot from 'react-plotly.js';
|
||||
|
||||
interface Trial {
|
||||
trial_number: number;
|
||||
values: number[];
|
||||
params: Record<string, number>;
|
||||
user_attrs?: Record<string, any>;
|
||||
source?: 'FEA' | 'NN' | 'V10_FEA';
|
||||
constraint_satisfied?: boolean;
|
||||
}
|
||||
|
||||
// Penalty threshold - objectives above this are considered failed/penalty trials
|
||||
const PENALTY_THRESHOLD = 100000;
|
||||
|
||||
interface PlotlyConvergencePlotProps {
|
||||
trials: Trial[];
|
||||
objectiveIndex?: number;
|
||||
objectiveName?: string;
|
||||
direction?: 'minimize' | 'maximize';
|
||||
height?: number;
|
||||
showRangeSlider?: boolean;
|
||||
showLogScaleToggle?: boolean;
|
||||
}
|
||||
|
||||
export function PlotlyConvergencePlot({
|
||||
trials,
|
||||
objectiveIndex = 0,
|
||||
objectiveName = 'Objective',
|
||||
direction = 'minimize',
|
||||
height = 400,
|
||||
showRangeSlider = true,
|
||||
showLogScaleToggle = true
|
||||
}: PlotlyConvergencePlotProps) {
|
||||
const [useLogScale, setUseLogScale] = useState(false);
|
||||
|
||||
// Process trials and calculate best-so-far
|
||||
const { feaData, nnData, bestSoFar, allX, allY } = useMemo(() => {
|
||||
if (!trials.length) return { feaData: { x: [], y: [], text: [] }, nnData: { x: [], y: [], text: [] }, bestSoFar: { x: [], y: [] }, allX: [], allY: [] };
|
||||
|
||||
// Sort by trial number
|
||||
const sorted = [...trials].sort((a, b) => a.trial_number - b.trial_number);
|
||||
|
||||
const fea: { x: number[]; y: number[]; text: string[] } = { x: [], y: [], text: [] };
|
||||
const nn: { x: number[]; y: number[]; text: string[] } = { x: [], y: [], text: [] };
|
||||
const best: { x: number[]; y: number[] } = { x: [], y: [] };
|
||||
const xs: number[] = [];
|
||||
const ys: number[] = [];
|
||||
|
||||
let bestValue = direction === 'minimize' ? Infinity : -Infinity;
|
||||
|
||||
sorted.forEach(t => {
|
||||
const val = t.values?.[objectiveIndex] ?? t.user_attrs?.[objectiveName] ?? null;
|
||||
if (val === null || !isFinite(val)) return;
|
||||
|
||||
// Filter out failed/penalty trials:
|
||||
// 1. Objective above penalty threshold (e.g., 1000000 = solver failure)
|
||||
// 2. constraint_satisfied explicitly false
|
||||
// 3. user_attrs indicates pruned/failed
|
||||
const isPenalty = val >= PENALTY_THRESHOLD;
|
||||
const isFailed = t.constraint_satisfied === false;
|
||||
const isPruned = t.user_attrs?.pruned === true || t.user_attrs?.fail_reason;
|
||||
if (isPenalty || isFailed || isPruned) return;
|
||||
|
||||
const source = t.source || t.user_attrs?.source || 'FEA';
|
||||
const hoverText = `Trial #${t.trial_number}<br>${objectiveName}: ${val.toFixed(4)}<br>Source: ${source}`;
|
||||
|
||||
xs.push(t.trial_number);
|
||||
ys.push(val);
|
||||
|
||||
if (source === 'NN') {
|
||||
nn.x.push(t.trial_number);
|
||||
nn.y.push(val);
|
||||
nn.text.push(hoverText);
|
||||
} else {
|
||||
fea.x.push(t.trial_number);
|
||||
fea.y.push(val);
|
||||
fea.text.push(hoverText);
|
||||
}
|
||||
|
||||
// Update best-so-far
|
||||
if (direction === 'minimize') {
|
||||
if (val < bestValue) bestValue = val;
|
||||
} else {
|
||||
if (val > bestValue) bestValue = val;
|
||||
}
|
||||
best.x.push(t.trial_number);
|
||||
best.y.push(bestValue);
|
||||
});
|
||||
|
||||
return { feaData: fea, nnData: nn, bestSoFar: best, allX: xs, allY: ys };
|
||||
}, [trials, objectiveIndex, objectiveName, direction]);
|
||||
|
||||
if (!trials.length || allX.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64 text-gray-500">
|
||||
No trial data available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const traces: any[] = [];
|
||||
|
||||
// FEA trials scatter
|
||||
if (feaData.x.length > 0) {
|
||||
traces.push({
|
||||
type: 'scatter',
|
||||
mode: 'markers',
|
||||
name: `FEA (${feaData.x.length})`,
|
||||
x: feaData.x,
|
||||
y: feaData.y,
|
||||
text: feaData.text,
|
||||
hoverinfo: 'text',
|
||||
marker: {
|
||||
color: '#3B82F6',
|
||||
size: 8,
|
||||
opacity: 0.7,
|
||||
line: { color: '#1E40AF', width: 1 }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// NN trials scatter
|
||||
if (nnData.x.length > 0) {
|
||||
traces.push({
|
||||
type: 'scatter',
|
||||
mode: 'markers',
|
||||
name: `NN (${nnData.x.length})`,
|
||||
x: nnData.x,
|
||||
y: nnData.y,
|
||||
text: nnData.text,
|
||||
hoverinfo: 'text',
|
||||
marker: {
|
||||
color: '#F97316',
|
||||
size: 6,
|
||||
symbol: 'cross',
|
||||
opacity: 0.6
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Best-so-far line
|
||||
if (bestSoFar.x.length > 0) {
|
||||
traces.push({
|
||||
type: 'scatter',
|
||||
mode: 'lines',
|
||||
name: 'Best So Far',
|
||||
x: bestSoFar.x,
|
||||
y: bestSoFar.y,
|
||||
line: {
|
||||
color: '#10B981',
|
||||
width: 3,
|
||||
shape: 'hv' // Step line
|
||||
},
|
||||
hoverinfo: 'y'
|
||||
});
|
||||
}
|
||||
|
||||
const layout: any = {
|
||||
height,
|
||||
margin: { l: 60, r: 30, t: 30, b: showRangeSlider ? 80 : 50 },
|
||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
xaxis: {
|
||||
title: 'Trial Number',
|
||||
gridcolor: '#E5E7EB',
|
||||
zerolinecolor: '#D1D5DB',
|
||||
rangeslider: showRangeSlider ? { visible: true } : undefined
|
||||
},
|
||||
yaxis: {
|
||||
title: useLogScale ? `log₁₀(${objectiveName})` : objectiveName,
|
||||
gridcolor: '#E5E7EB',
|
||||
zerolinecolor: '#D1D5DB',
|
||||
type: useLogScale ? 'log' : 'linear'
|
||||
},
|
||||
legend: {
|
||||
x: 1,
|
||||
y: 1,
|
||||
xanchor: 'right',
|
||||
bgcolor: 'rgba(255,255,255,0.8)',
|
||||
bordercolor: '#E5E7EB',
|
||||
borderwidth: 1
|
||||
},
|
||||
font: { family: 'Inter, system-ui, sans-serif' },
|
||||
hovermode: 'closest'
|
||||
};
|
||||
|
||||
// Best value annotation
|
||||
const bestVal = direction === 'minimize'
|
||||
? Math.min(...allY)
|
||||
: Math.max(...allY);
|
||||
const bestIdx = allY.indexOf(bestVal);
|
||||
const bestTrial = allX[bestIdx];
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Summary stats and controls */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex gap-6 text-sm">
|
||||
<div className="text-gray-600">
|
||||
Best: <span className="font-semibold text-green-600">{bestVal.toFixed(4)}</span>
|
||||
<span className="text-gray-400 ml-1">(Trial #{bestTrial})</span>
|
||||
</div>
|
||||
<div className="text-gray-600">
|
||||
Current: <span className="font-semibold">{allY[allY.length - 1].toFixed(4)}</span>
|
||||
</div>
|
||||
<div className="text-gray-600">
|
||||
Trials: <span className="font-semibold">{allX.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Log scale toggle */}
|
||||
{showLogScaleToggle && (
|
||||
<button
|
||||
onClick={() => setUseLogScale(!useLogScale)}
|
||||
className={`px-3 py-1 text-xs rounded transition-colors ${
|
||||
useLogScale
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
title="Toggle logarithmic scale - better for viewing early improvements"
|
||||
>
|
||||
{useLogScale ? 'Log Scale' : 'Linear Scale'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Plot
|
||||
data={traces}
|
||||
layout={layout}
|
||||
config={{
|
||||
displayModeBar: true,
|
||||
displaylogo: false,
|
||||
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
|
||||
toImageButtonOptions: {
|
||||
format: 'png',
|
||||
filename: 'convergence_plot',
|
||||
height: 600,
|
||||
width: 1200,
|
||||
scale: 2
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import Plot from 'react-plotly.js';
|
||||
|
||||
interface TrialData {
|
||||
trial_number: number;
|
||||
values: number[];
|
||||
params: Record<string, number>;
|
||||
}
|
||||
|
||||
interface PlotlyCorrelationHeatmapProps {
|
||||
trials: TrialData[];
|
||||
objectiveName?: string;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
// Calculate Pearson correlation coefficient
|
||||
function pearsonCorrelation(x: number[], y: number[]): number {
|
||||
const n = x.length;
|
||||
if (n === 0 || n !== y.length) return 0;
|
||||
|
||||
const meanX = x.reduce((a, b) => a + b, 0) / n;
|
||||
const meanY = y.reduce((a, b) => a + b, 0) / n;
|
||||
|
||||
let numerator = 0;
|
||||
let denomX = 0;
|
||||
let denomY = 0;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const dx = x[i] - meanX;
|
||||
const dy = y[i] - meanY;
|
||||
numerator += dx * dy;
|
||||
denomX += dx * dx;
|
||||
denomY += dy * dy;
|
||||
}
|
||||
|
||||
const denominator = Math.sqrt(denomX) * Math.sqrt(denomY);
|
||||
return denominator === 0 ? 0 : numerator / denominator;
|
||||
}
|
||||
|
||||
export function PlotlyCorrelationHeatmap({
|
||||
trials,
|
||||
objectiveName = 'Objective',
|
||||
height = 500
|
||||
}: PlotlyCorrelationHeatmapProps) {
|
||||
const { matrix, labels, annotations } = useMemo(() => {
|
||||
if (trials.length < 3) {
|
||||
return { matrix: [], labels: [], annotations: [] };
|
||||
}
|
||||
|
||||
// Get parameter names
|
||||
const paramNames = Object.keys(trials[0].params);
|
||||
const allLabels = [...paramNames, objectiveName];
|
||||
|
||||
// Extract data columns
|
||||
const columns: Record<string, number[]> = {};
|
||||
paramNames.forEach(name => {
|
||||
columns[name] = trials.map(t => t.params[name]).filter(v => v !== undefined && !isNaN(v));
|
||||
});
|
||||
columns[objectiveName] = trials.map(t => t.values[0]).filter(v => v !== undefined && !isNaN(v));
|
||||
|
||||
// Calculate correlation matrix
|
||||
const n = allLabels.length;
|
||||
const correlationMatrix: number[][] = [];
|
||||
const annotationData: any[] = [];
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const row: number[] = [];
|
||||
for (let j = 0; j < n; j++) {
|
||||
const col1 = columns[allLabels[i]];
|
||||
const col2 = columns[allLabels[j]];
|
||||
|
||||
// Ensure same length
|
||||
const minLen = Math.min(col1.length, col2.length);
|
||||
const corr = pearsonCorrelation(col1.slice(0, minLen), col2.slice(0, minLen));
|
||||
row.push(corr);
|
||||
|
||||
// Add annotation
|
||||
annotationData.push({
|
||||
x: allLabels[j],
|
||||
y: allLabels[i],
|
||||
text: corr.toFixed(2),
|
||||
showarrow: false,
|
||||
font: {
|
||||
color: Math.abs(corr) > 0.5 ? '#fff' : '#888',
|
||||
size: 11
|
||||
}
|
||||
});
|
||||
}
|
||||
correlationMatrix.push(row);
|
||||
}
|
||||
|
||||
return {
|
||||
matrix: correlationMatrix,
|
||||
labels: allLabels,
|
||||
annotations: annotationData
|
||||
};
|
||||
}, [trials, objectiveName]);
|
||||
|
||||
if (trials.length < 3) {
|
||||
return (
|
||||
<div className="h-64 flex items-center justify-center text-dark-400">
|
||||
<p>Need at least 3 trials to compute correlations</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Plot
|
||||
data={[
|
||||
{
|
||||
z: matrix,
|
||||
x: labels,
|
||||
y: labels,
|
||||
type: 'heatmap',
|
||||
colorscale: [
|
||||
[0, '#ef4444'], // -1: strong negative (red)
|
||||
[0.25, '#f87171'], // -0.5: moderate negative
|
||||
[0.5, '#1a1b26'], // 0: no correlation (dark)
|
||||
[0.75, '#60a5fa'], // 0.5: moderate positive
|
||||
[1, '#3b82f6'] // 1: strong positive (blue)
|
||||
],
|
||||
zmin: -1,
|
||||
zmax: 1,
|
||||
showscale: true,
|
||||
colorbar: {
|
||||
title: { text: 'Correlation', font: { color: '#888' } },
|
||||
tickfont: { color: '#888' },
|
||||
len: 0.8
|
||||
},
|
||||
hovertemplate: '%{y} vs %{x}<br>Correlation: %{z:.3f}<extra></extra>'
|
||||
}
|
||||
]}
|
||||
layout={{
|
||||
title: {
|
||||
text: 'Parameter-Objective Correlation Matrix',
|
||||
font: { color: '#fff', size: 14 }
|
||||
},
|
||||
height,
|
||||
margin: { l: 120, r: 60, t: 60, b: 120 },
|
||||
paper_bgcolor: 'transparent',
|
||||
plot_bgcolor: 'transparent',
|
||||
xaxis: {
|
||||
tickangle: 45,
|
||||
tickfont: { color: '#888', size: 10 },
|
||||
gridcolor: 'rgba(255,255,255,0.05)'
|
||||
},
|
||||
yaxis: {
|
||||
tickfont: { color: '#888', size: 10 },
|
||||
gridcolor: 'rgba(255,255,255,0.05)'
|
||||
},
|
||||
annotations: annotations
|
||||
}}
|
||||
config={{
|
||||
displayModeBar: true,
|
||||
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
|
||||
displaylogo: false
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import Plot from 'react-plotly.js';
|
||||
|
||||
interface TrialData {
|
||||
trial_number: number;
|
||||
values: number[];
|
||||
constraint_satisfied?: boolean;
|
||||
}
|
||||
|
||||
interface PlotlyFeasibilityChartProps {
|
||||
trials: TrialData[];
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export function PlotlyFeasibilityChart({
|
||||
trials,
|
||||
height = 350
|
||||
}: PlotlyFeasibilityChartProps) {
|
||||
const { trialNumbers, cumulativeFeasibility, windowedFeasibility } = useMemo(() => {
|
||||
if (trials.length === 0) {
|
||||
return { trialNumbers: [], cumulativeFeasibility: [], windowedFeasibility: [] };
|
||||
}
|
||||
|
||||
// Sort trials by number
|
||||
const sorted = [...trials].sort((a, b) => a.trial_number - b.trial_number);
|
||||
|
||||
const numbers: number[] = [];
|
||||
const cumulative: number[] = [];
|
||||
const windowed: number[] = [];
|
||||
|
||||
let feasibleCount = 0;
|
||||
const windowSize = Math.min(20, Math.floor(sorted.length / 5) || 1);
|
||||
|
||||
sorted.forEach((trial, idx) => {
|
||||
numbers.push(trial.trial_number);
|
||||
|
||||
// Cumulative feasibility
|
||||
if (trial.constraint_satisfied !== false) {
|
||||
feasibleCount++;
|
||||
}
|
||||
cumulative.push((feasibleCount / (idx + 1)) * 100);
|
||||
|
||||
// Windowed (rolling) feasibility
|
||||
const windowStart = Math.max(0, idx - windowSize + 1);
|
||||
const windowTrials = sorted.slice(windowStart, idx + 1);
|
||||
const windowFeasible = windowTrials.filter(t => t.constraint_satisfied !== false).length;
|
||||
windowed.push((windowFeasible / windowTrials.length) * 100);
|
||||
});
|
||||
|
||||
return { trialNumbers: numbers, cumulativeFeasibility: cumulative, windowedFeasibility: windowed };
|
||||
}, [trials]);
|
||||
|
||||
if (trials.length === 0) {
|
||||
return (
|
||||
<div className="h-64 flex items-center justify-center text-dark-400">
|
||||
<p>No trials to display</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Plot
|
||||
data={[
|
||||
{
|
||||
x: trialNumbers,
|
||||
y: cumulativeFeasibility,
|
||||
type: 'scatter',
|
||||
mode: 'lines',
|
||||
name: 'Cumulative Feasibility',
|
||||
line: { color: '#22c55e', width: 2 },
|
||||
hovertemplate: 'Trial %{x}<br>Cumulative: %{y:.1f}%<extra></extra>'
|
||||
},
|
||||
{
|
||||
x: trialNumbers,
|
||||
y: windowedFeasibility,
|
||||
type: 'scatter',
|
||||
mode: 'lines',
|
||||
name: 'Rolling (20-trial)',
|
||||
line: { color: '#60a5fa', width: 2, dash: 'dot' },
|
||||
hovertemplate: 'Trial %{x}<br>Rolling: %{y:.1f}%<extra></extra>'
|
||||
}
|
||||
]}
|
||||
layout={{
|
||||
height,
|
||||
margin: { l: 60, r: 30, t: 30, b: 50 },
|
||||
paper_bgcolor: 'transparent',
|
||||
plot_bgcolor: 'transparent',
|
||||
xaxis: {
|
||||
title: { text: 'Trial Number', font: { color: '#888' } },
|
||||
tickfont: { color: '#888' },
|
||||
gridcolor: 'rgba(255,255,255,0.05)',
|
||||
zeroline: false
|
||||
},
|
||||
yaxis: {
|
||||
title: { text: 'Feasibility Rate (%)', font: { color: '#888' } },
|
||||
tickfont: { color: '#888' },
|
||||
gridcolor: 'rgba(255,255,255,0.1)',
|
||||
zeroline: false,
|
||||
range: [0, 105]
|
||||
},
|
||||
legend: {
|
||||
font: { color: '#888' },
|
||||
bgcolor: 'rgba(0,0,0,0.5)',
|
||||
x: 0.02,
|
||||
y: 0.98,
|
||||
xanchor: 'left',
|
||||
yanchor: 'top'
|
||||
},
|
||||
showlegend: true,
|
||||
hovermode: 'x unified'
|
||||
}}
|
||||
config={{
|
||||
displayModeBar: true,
|
||||
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
|
||||
displaylogo: false
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
/**
|
||||
* PlotlyParallelCoordinates - Interactive parallel coordinates plot using Plotly
|
||||
*
|
||||
* Features:
|
||||
* - Native zoom, pan, and selection
|
||||
* - Hover tooltips with trial details
|
||||
* - Brush filtering on each axis
|
||||
* - FEA vs NN color differentiation
|
||||
* - Export to PNG/SVG
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import Plot from 'react-plotly.js';
|
||||
|
||||
interface Trial {
|
||||
trial_number: number;
|
||||
values: number[];
|
||||
params: Record<string, number>;
|
||||
user_attrs?: Record<string, any>;
|
||||
constraint_satisfied?: boolean;
|
||||
source?: 'FEA' | 'NN' | 'V10_FEA';
|
||||
}
|
||||
|
||||
interface Objective {
|
||||
name: string;
|
||||
direction?: 'minimize' | 'maximize';
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
interface DesignVariable {
|
||||
name: string;
|
||||
unit?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
interface PlotlyParallelCoordinatesProps {
|
||||
trials: Trial[];
|
||||
objectives: Objective[];
|
||||
designVariables: DesignVariable[];
|
||||
paretoFront?: Trial[];
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export function PlotlyParallelCoordinates({
|
||||
trials,
|
||||
objectives,
|
||||
designVariables,
|
||||
paretoFront = [],
|
||||
height = 500
|
||||
}: PlotlyParallelCoordinatesProps) {
|
||||
// Create set of Pareto front trial numbers
|
||||
const paretoSet = useMemo(() => new Set(paretoFront.map(t => t.trial_number)), [paretoFront]);
|
||||
|
||||
// Build dimensions array for parallel coordinates
|
||||
const { dimensions, colorValues, colorScale } = useMemo(() => {
|
||||
if (!trials.length) return { dimensions: [], colorValues: [], colorScale: [] };
|
||||
|
||||
const dims: any[] = [];
|
||||
const colors: number[] = [];
|
||||
|
||||
// Get all design variable names
|
||||
const dvNames = designVariables.map(dv => dv.name);
|
||||
const objNames = objectives.map(obj => obj.name);
|
||||
|
||||
// Add design variable dimensions
|
||||
dvNames.forEach((name, idx) => {
|
||||
const dv = designVariables[idx];
|
||||
const values = trials.map(t => t.params[name] ?? 0);
|
||||
const validValues = values.filter(v => v !== null && v !== undefined && isFinite(v));
|
||||
|
||||
if (validValues.length === 0) return;
|
||||
|
||||
dims.push({
|
||||
label: name,
|
||||
values: values,
|
||||
range: [
|
||||
dv?.min ?? Math.min(...validValues),
|
||||
dv?.max ?? Math.max(...validValues)
|
||||
],
|
||||
constraintrange: undefined
|
||||
});
|
||||
});
|
||||
|
||||
// Add objective dimensions
|
||||
objNames.forEach((name, idx) => {
|
||||
const obj = objectives[idx];
|
||||
const values = trials.map(t => {
|
||||
// Try to get from values array first, then user_attrs
|
||||
if (t.values && t.values[idx] !== undefined) {
|
||||
return t.values[idx];
|
||||
}
|
||||
return t.user_attrs?.[name] ?? 0;
|
||||
});
|
||||
const validValues = values.filter(v => v !== null && v !== undefined && isFinite(v));
|
||||
|
||||
if (validValues.length === 0) return;
|
||||
|
||||
dims.push({
|
||||
label: `${name}${obj.unit ? ` (${obj.unit})` : ''}`,
|
||||
values: values,
|
||||
range: [Math.min(...validValues) * 0.95, Math.max(...validValues) * 1.05]
|
||||
});
|
||||
});
|
||||
|
||||
// Build color array: 0 = V10_FEA, 1 = FEA, 2 = NN, 3 = Pareto
|
||||
trials.forEach(t => {
|
||||
const source = t.source || t.user_attrs?.source || 'FEA';
|
||||
const isPareto = paretoSet.has(t.trial_number);
|
||||
|
||||
if (isPareto) {
|
||||
colors.push(3); // Pareto - special color
|
||||
} else if (source === 'NN') {
|
||||
colors.push(2); // NN trials
|
||||
} else if (source === 'V10_FEA') {
|
||||
colors.push(0); // V10 FEA
|
||||
} else {
|
||||
colors.push(1); // V11 FEA
|
||||
}
|
||||
});
|
||||
|
||||
// Color scale: V10_FEA (light blue), FEA (blue), NN (orange), Pareto (green)
|
||||
const scale: [number, string][] = [
|
||||
[0, '#93C5FD'], // V10_FEA - light blue
|
||||
[0.33, '#2563EB'], // FEA - blue
|
||||
[0.66, '#F97316'], // NN - orange
|
||||
[1, '#10B981'] // Pareto - green
|
||||
];
|
||||
|
||||
return { dimensions: dims, colorValues: colors, colorScale: scale };
|
||||
}, [trials, objectives, designVariables, paretoSet]);
|
||||
|
||||
if (!trials.length || dimensions.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64 text-gray-500">
|
||||
No trial data available for parallel coordinates
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Count trial types for legend
|
||||
const feaCount = trials.filter(t => {
|
||||
const source = t.source || t.user_attrs?.source || 'FEA';
|
||||
return source === 'FEA' || source === 'V10_FEA';
|
||||
}).length;
|
||||
const nnCount = trials.filter(t => {
|
||||
const source = t.source || t.user_attrs?.source || 'FEA';
|
||||
return source === 'NN';
|
||||
}).length;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Legend */}
|
||||
<div className="flex gap-4 justify-center mb-2 text-sm">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-4 h-1 rounded" style={{ backgroundColor: '#2563EB' }} />
|
||||
<span className="text-gray-600">FEA ({feaCount})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-4 h-1 rounded" style={{ backgroundColor: '#F97316' }} />
|
||||
<span className="text-gray-600">NN ({nnCount})</span>
|
||||
</div>
|
||||
{paretoFront.length > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-4 h-1 rounded" style={{ backgroundColor: '#10B981' }} />
|
||||
<span className="text-gray-600">Pareto ({paretoFront.length})</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Plot
|
||||
data={[
|
||||
{
|
||||
type: 'parcoords',
|
||||
line: {
|
||||
color: colorValues,
|
||||
colorscale: colorScale as any,
|
||||
showscale: false
|
||||
},
|
||||
dimensions: dimensions,
|
||||
labelangle: -30,
|
||||
labelfont: {
|
||||
size: 11,
|
||||
color: '#374151'
|
||||
},
|
||||
tickfont: {
|
||||
size: 10,
|
||||
color: '#6B7280'
|
||||
}
|
||||
} as any
|
||||
]}
|
||||
layout={{
|
||||
height: height,
|
||||
margin: { l: 80, r: 80, t: 30, b: 30 },
|
||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
font: {
|
||||
family: 'Inter, system-ui, sans-serif'
|
||||
}
|
||||
}}
|
||||
config={{
|
||||
displayModeBar: true,
|
||||
displaylogo: false,
|
||||
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
|
||||
toImageButtonOptions: {
|
||||
format: 'png',
|
||||
filename: 'parallel_coordinates',
|
||||
height: 800,
|
||||
width: 1400,
|
||||
scale: 2
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
|
||||
<p className="text-xs text-gray-500 text-center mt-2">
|
||||
Drag along axes to filter. Double-click to reset.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
/**
|
||||
* PlotlyParameterImportance - Interactive parameter importance chart using Plotly
|
||||
*
|
||||
* Features:
|
||||
* - Horizontal bar chart showing correlation/importance
|
||||
* - Color coding by positive/negative correlation
|
||||
* - Hover tooltips with details
|
||||
* - Sortable by importance
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import Plot from 'react-plotly.js';
|
||||
|
||||
interface Trial {
|
||||
trial_number: number;
|
||||
values: number[];
|
||||
params: Record<string, number>;
|
||||
user_attrs?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface DesignVariable {
|
||||
name: string;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
interface PlotlyParameterImportanceProps {
|
||||
trials: Trial[];
|
||||
designVariables: DesignVariable[];
|
||||
objectiveIndex?: number;
|
||||
objectiveName?: string;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
// Calculate Pearson correlation coefficient
|
||||
function pearsonCorrelation(x: number[], y: number[]): number {
|
||||
const n = x.length;
|
||||
if (n === 0) return 0;
|
||||
|
||||
const sumX = x.reduce((a, b) => a + b, 0);
|
||||
const sumY = y.reduce((a, b) => a + b, 0);
|
||||
const sumXY = x.reduce((acc, xi, i) => acc + xi * y[i], 0);
|
||||
const sumX2 = x.reduce((acc, xi) => acc + xi * xi, 0);
|
||||
const sumY2 = y.reduce((acc, yi) => acc + yi * yi, 0);
|
||||
|
||||
const numerator = n * sumXY - sumX * sumY;
|
||||
const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
|
||||
|
||||
if (denominator === 0) return 0;
|
||||
return numerator / denominator;
|
||||
}
|
||||
|
||||
export function PlotlyParameterImportance({
|
||||
trials,
|
||||
designVariables,
|
||||
objectiveIndex = 0,
|
||||
objectiveName = 'Objective',
|
||||
height = 400
|
||||
}: PlotlyParameterImportanceProps) {
|
||||
const [sortBy, setSortBy] = useState<'importance' | 'name'>('importance');
|
||||
|
||||
// Calculate correlations for each parameter
|
||||
const correlations = useMemo(() => {
|
||||
if (!trials.length || !designVariables.length) return [];
|
||||
|
||||
// Get objective values
|
||||
const objValues = trials.map(t => {
|
||||
if (t.values && t.values[objectiveIndex] !== undefined) {
|
||||
return t.values[objectiveIndex];
|
||||
}
|
||||
return t.user_attrs?.[objectiveName] ?? null;
|
||||
}).filter((v): v is number => v !== null && isFinite(v));
|
||||
|
||||
if (objValues.length < 3) return []; // Need at least 3 points for correlation
|
||||
|
||||
const results: { name: string; correlation: number; absCorrelation: number }[] = [];
|
||||
|
||||
designVariables.forEach(dv => {
|
||||
const paramValues = trials
|
||||
.map((t) => {
|
||||
const objVal = t.values?.[objectiveIndex] ?? t.user_attrs?.[objectiveName];
|
||||
if (objVal === null || objVal === undefined || !isFinite(objVal)) return null;
|
||||
return { param: t.params[dv.name], obj: objVal };
|
||||
})
|
||||
.filter((v): v is { param: number; obj: number } => v !== null && v.param !== undefined);
|
||||
|
||||
if (paramValues.length < 3) return;
|
||||
|
||||
const x = paramValues.map(v => v.param);
|
||||
const y = paramValues.map(v => v.obj);
|
||||
const corr = pearsonCorrelation(x, y);
|
||||
|
||||
results.push({
|
||||
name: dv.name,
|
||||
correlation: corr,
|
||||
absCorrelation: Math.abs(corr)
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by absolute correlation or name
|
||||
if (sortBy === 'importance') {
|
||||
results.sort((a, b) => b.absCorrelation - a.absCorrelation);
|
||||
} else {
|
||||
results.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
return results;
|
||||
}, [trials, designVariables, objectiveIndex, objectiveName, sortBy]);
|
||||
|
||||
if (!correlations.length) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64 text-gray-500">
|
||||
Not enough data to calculate parameter importance
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Build bar chart data
|
||||
const names = correlations.map(c => c.name);
|
||||
const values = correlations.map(c => c.correlation);
|
||||
const colors = values.map(v => v > 0 ? '#EF4444' : '#22C55E'); // Red for positive (worse), Green for negative (better) when minimizing
|
||||
const hoverTexts = correlations.map(c =>
|
||||
`${c.name}<br>Correlation: ${c.correlation.toFixed(4)}<br>|r|: ${c.absCorrelation.toFixed(4)}<br>${c.correlation > 0 ? 'Higher → Higher objective' : 'Higher → Lower objective'}`
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Controls */}
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="text-sm text-gray-600">
|
||||
Correlation with <span className="font-semibold">{objectiveName}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setSortBy('importance')}
|
||||
className={`px-3 py-1 text-xs rounded ${sortBy === 'importance' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700'}`}
|
||||
>
|
||||
By Importance
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSortBy('name')}
|
||||
className={`px-3 py-1 text-xs rounded ${sortBy === 'name' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700'}`}
|
||||
>
|
||||
By Name
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Plot
|
||||
data={[
|
||||
{
|
||||
type: 'bar',
|
||||
orientation: 'h',
|
||||
y: names,
|
||||
x: values,
|
||||
text: hoverTexts,
|
||||
hoverinfo: 'text',
|
||||
marker: {
|
||||
color: colors,
|
||||
line: { color: '#fff', width: 1 }
|
||||
}
|
||||
}
|
||||
]}
|
||||
layout={{
|
||||
height: Math.max(height, correlations.length * 30 + 80),
|
||||
margin: { l: 150, r: 30, t: 10, b: 50 },
|
||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
xaxis: {
|
||||
title: { text: 'Correlation Coefficient' },
|
||||
range: [-1, 1],
|
||||
gridcolor: '#E5E7EB',
|
||||
zerolinecolor: '#9CA3AF',
|
||||
zerolinewidth: 2
|
||||
},
|
||||
yaxis: {
|
||||
automargin: true
|
||||
},
|
||||
font: { family: 'Inter, system-ui, sans-serif', size: 11 },
|
||||
bargap: 0.3
|
||||
}}
|
||||
config={{
|
||||
displayModeBar: true,
|
||||
displaylogo: false,
|
||||
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
|
||||
toImageButtonOptions: {
|
||||
format: 'png',
|
||||
filename: 'parameter_importance',
|
||||
height: 600,
|
||||
width: 800,
|
||||
scale: 2
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex gap-6 justify-center mt-3 text-xs">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-4 h-3 rounded" style={{ backgroundColor: '#EF4444' }} />
|
||||
<span className="text-gray-600">Positive correlation (higher param → higher objective)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-4 h-3 rounded" style={{ backgroundColor: '#22C55E' }} />
|
||||
<span className="text-gray-600">Negative correlation (higher param → lower objective)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,448 +0,0 @@
|
||||
/**
|
||||
* PlotlyParetoPlot - Interactive Pareto front visualization using Plotly
|
||||
*
|
||||
* Features:
|
||||
* - 2D scatter with Pareto front highlighted
|
||||
* - 3D scatter for 3-objective problems
|
||||
* - Hover tooltips with trial details
|
||||
* - Pareto front connection line
|
||||
* - FEA vs NN differentiation
|
||||
* - Constraint satisfaction highlighting
|
||||
* - Dark mode styling
|
||||
* - Zoom, pan, and export
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import Plot from 'react-plotly.js';
|
||||
|
||||
interface Trial {
|
||||
trial_number: number;
|
||||
values: number[];
|
||||
params: Record<string, number>;
|
||||
user_attrs?: Record<string, any>;
|
||||
source?: 'FEA' | 'NN' | 'V10_FEA';
|
||||
constraint_satisfied?: boolean;
|
||||
}
|
||||
|
||||
interface Objective {
|
||||
name: string;
|
||||
direction?: 'minimize' | 'maximize';
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
interface PlotlyParetoPlotProps {
|
||||
trials: Trial[];
|
||||
paretoFront: Trial[];
|
||||
objectives: Objective[];
|
||||
height?: number;
|
||||
showParetoLine?: boolean;
|
||||
showInfeasible?: boolean;
|
||||
}
|
||||
|
||||
export function PlotlyParetoPlot({
|
||||
trials,
|
||||
paretoFront,
|
||||
objectives,
|
||||
height = 500,
|
||||
showParetoLine = true,
|
||||
showInfeasible = true
|
||||
}: PlotlyParetoPlotProps) {
|
||||
const [viewMode, setViewMode] = useState<'2d' | '3d'>(objectives.length >= 3 ? '3d' : '2d');
|
||||
const [selectedObjectives, setSelectedObjectives] = useState<[number, number, number]>([0, 1, 2]);
|
||||
|
||||
const paretoSet = useMemo(() => new Set(paretoFront.map(t => t.trial_number)), [paretoFront]);
|
||||
|
||||
// Separate trials by source, Pareto status, and constraint satisfaction
|
||||
const { feaTrials, nnTrials, paretoTrials, infeasibleTrials, stats } = useMemo(() => {
|
||||
const fea: Trial[] = [];
|
||||
const nn: Trial[] = [];
|
||||
const pareto: Trial[] = [];
|
||||
const infeasible: Trial[] = [];
|
||||
|
||||
trials.forEach(t => {
|
||||
const source = t.source || t.user_attrs?.source || 'FEA';
|
||||
const isFeasible = t.constraint_satisfied !== false && t.user_attrs?.constraint_satisfied !== false;
|
||||
|
||||
if (!isFeasible && showInfeasible) {
|
||||
infeasible.push(t);
|
||||
} else if (paretoSet.has(t.trial_number)) {
|
||||
pareto.push(t);
|
||||
} else if (source === 'NN') {
|
||||
nn.push(t);
|
||||
} else {
|
||||
fea.push(t);
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate statistics
|
||||
const stats = {
|
||||
totalTrials: trials.length,
|
||||
paretoCount: pareto.length,
|
||||
feaCount: fea.length + pareto.filter(t => (t.source || 'FEA') !== 'NN').length,
|
||||
nnCount: nn.length + pareto.filter(t => t.source === 'NN').length,
|
||||
infeasibleCount: infeasible.length,
|
||||
hypervolume: 0 // Could calculate if needed
|
||||
};
|
||||
|
||||
return { feaTrials: fea, nnTrials: nn, paretoTrials: pareto, infeasibleTrials: infeasible, stats };
|
||||
}, [trials, paretoSet, showInfeasible]);
|
||||
|
||||
// Helper to get objective value
|
||||
const getObjValue = (trial: Trial, idx: number): number => {
|
||||
if (trial.values && trial.values[idx] !== undefined) {
|
||||
return trial.values[idx];
|
||||
}
|
||||
const objName = objectives[idx]?.name;
|
||||
return trial.user_attrs?.[objName] ?? 0;
|
||||
};
|
||||
|
||||
// Build hover text
|
||||
const buildHoverText = (trial: Trial): string => {
|
||||
const lines = [`Trial #${trial.trial_number}`];
|
||||
objectives.forEach((obj, i) => {
|
||||
const val = getObjValue(trial, i);
|
||||
lines.push(`${obj.name}: ${val.toFixed(4)}${obj.unit ? ` ${obj.unit}` : ''}`);
|
||||
});
|
||||
const source = trial.source || trial.user_attrs?.source || 'FEA';
|
||||
lines.push(`Source: ${source}`);
|
||||
return lines.join('<br>');
|
||||
};
|
||||
|
||||
// Create trace data
|
||||
const createTrace = (
|
||||
trialList: Trial[],
|
||||
name: string,
|
||||
color: string,
|
||||
symbol: string,
|
||||
size: number,
|
||||
opacity: number
|
||||
) => {
|
||||
const [i, j, k] = selectedObjectives;
|
||||
|
||||
if (viewMode === '3d' && objectives.length >= 3) {
|
||||
return {
|
||||
type: 'scatter3d' as const,
|
||||
mode: 'markers' as const,
|
||||
name,
|
||||
x: trialList.map(t => getObjValue(t, i)),
|
||||
y: trialList.map(t => getObjValue(t, j)),
|
||||
z: trialList.map(t => getObjValue(t, k)),
|
||||
text: trialList.map(buildHoverText),
|
||||
hoverinfo: 'text' as const,
|
||||
marker: {
|
||||
color,
|
||||
size,
|
||||
symbol,
|
||||
opacity,
|
||||
line: { color: '#fff', width: 1 }
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'scatter' as const,
|
||||
mode: 'markers' as const,
|
||||
name,
|
||||
x: trialList.map(t => getObjValue(t, i)),
|
||||
y: trialList.map(t => getObjValue(t, j)),
|
||||
text: trialList.map(buildHoverText),
|
||||
hoverinfo: 'text' as const,
|
||||
marker: {
|
||||
color,
|
||||
size,
|
||||
symbol,
|
||||
opacity,
|
||||
line: { color: '#fff', width: 1 }
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Sort Pareto trials by first objective for line connection
|
||||
const sortedParetoTrials = useMemo(() => {
|
||||
const [i] = selectedObjectives;
|
||||
return [...paretoTrials].sort((a, b) => getObjValue(a, i) - getObjValue(b, i));
|
||||
}, [paretoTrials, selectedObjectives]);
|
||||
|
||||
// Create Pareto front line trace (2D only)
|
||||
const createParetoLine = () => {
|
||||
if (!showParetoLine || viewMode === '3d' || sortedParetoTrials.length < 2) return null;
|
||||
const [i, j] = selectedObjectives;
|
||||
return {
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines' as const,
|
||||
name: 'Pareto Front',
|
||||
x: sortedParetoTrials.map(t => getObjValue(t, i)),
|
||||
y: sortedParetoTrials.map(t => getObjValue(t, j)),
|
||||
line: {
|
||||
color: '#10B981',
|
||||
width: 2,
|
||||
dash: 'dot'
|
||||
},
|
||||
hoverinfo: 'skip' as const,
|
||||
showlegend: false
|
||||
};
|
||||
};
|
||||
|
||||
const traces = [
|
||||
// Infeasible trials (background, red X)
|
||||
...(showInfeasible && infeasibleTrials.length > 0 ? [
|
||||
createTrace(infeasibleTrials, `Infeasible (${infeasibleTrials.length})`, '#EF4444', 'x', 7, 0.4)
|
||||
] : []),
|
||||
// FEA trials (blue circles)
|
||||
createTrace(feaTrials, `FEA (${feaTrials.length})`, '#3B82F6', 'circle', 8, 0.6),
|
||||
// NN trials (purple diamonds)
|
||||
createTrace(nnTrials, `NN (${nnTrials.length})`, '#A855F7', 'diamond', 8, 0.5),
|
||||
// Pareto front line (2D only)
|
||||
createParetoLine(),
|
||||
// Pareto front points (highlighted)
|
||||
createTrace(sortedParetoTrials, `Pareto (${sortedParetoTrials.length})`, '#10B981', 'star', 14, 1.0)
|
||||
].filter(trace => trace && (trace.x as number[]).length > 0);
|
||||
|
||||
const [i, j, k] = selectedObjectives;
|
||||
|
||||
// Dark mode color scheme
|
||||
const colors = {
|
||||
text: '#E5E7EB',
|
||||
textMuted: '#9CA3AF',
|
||||
grid: 'rgba(255,255,255,0.1)',
|
||||
zeroline: 'rgba(255,255,255,0.2)',
|
||||
legendBg: 'rgba(30,30,30,0.9)',
|
||||
legendBorder: 'rgba(255,255,255,0.1)'
|
||||
};
|
||||
|
||||
const layout: any = viewMode === '3d' && objectives.length >= 3
|
||||
? {
|
||||
height,
|
||||
margin: { l: 50, r: 50, t: 30, b: 50 },
|
||||
paper_bgcolor: 'transparent',
|
||||
plot_bgcolor: 'transparent',
|
||||
scene: {
|
||||
xaxis: {
|
||||
title: { text: objectives[i]?.name || 'Objective 1', font: { color: colors.text } },
|
||||
gridcolor: colors.grid,
|
||||
zerolinecolor: colors.zeroline,
|
||||
tickfont: { color: colors.textMuted }
|
||||
},
|
||||
yaxis: {
|
||||
title: { text: objectives[j]?.name || 'Objective 2', font: { color: colors.text } },
|
||||
gridcolor: colors.grid,
|
||||
zerolinecolor: colors.zeroline,
|
||||
tickfont: { color: colors.textMuted }
|
||||
},
|
||||
zaxis: {
|
||||
title: { text: objectives[k]?.name || 'Objective 3', font: { color: colors.text } },
|
||||
gridcolor: colors.grid,
|
||||
zerolinecolor: colors.zeroline,
|
||||
tickfont: { color: colors.textMuted }
|
||||
},
|
||||
bgcolor: 'transparent'
|
||||
},
|
||||
legend: {
|
||||
x: 1,
|
||||
y: 1,
|
||||
font: { color: colors.text },
|
||||
bgcolor: colors.legendBg,
|
||||
bordercolor: colors.legendBorder,
|
||||
borderwidth: 1
|
||||
},
|
||||
font: { family: 'Inter, system-ui, sans-serif', color: colors.text }
|
||||
}
|
||||
: {
|
||||
height,
|
||||
margin: { l: 60, r: 30, t: 30, b: 60 },
|
||||
paper_bgcolor: 'transparent',
|
||||
plot_bgcolor: 'transparent',
|
||||
xaxis: {
|
||||
title: { text: objectives[i]?.name || 'Objective 1', font: { color: colors.text } },
|
||||
gridcolor: colors.grid,
|
||||
zerolinecolor: colors.zeroline,
|
||||
tickfont: { color: colors.textMuted }
|
||||
},
|
||||
yaxis: {
|
||||
title: { text: objectives[j]?.name || 'Objective 2', font: { color: colors.text } },
|
||||
gridcolor: colors.grid,
|
||||
zerolinecolor: colors.zeroline,
|
||||
tickfont: { color: colors.textMuted }
|
||||
},
|
||||
legend: {
|
||||
x: 1,
|
||||
y: 1,
|
||||
xanchor: 'right',
|
||||
font: { color: colors.text },
|
||||
bgcolor: colors.legendBg,
|
||||
bordercolor: colors.legendBorder,
|
||||
borderwidth: 1
|
||||
},
|
||||
font: { family: 'Inter, system-ui, sans-serif', color: colors.text },
|
||||
hovermode: 'closest' as const
|
||||
};
|
||||
|
||||
if (!trials.length) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64 text-dark-400">
|
||||
No trial data available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Stats Bar */}
|
||||
<div className="flex gap-4 mb-4 text-sm">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-dark-700 rounded-lg">
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full" />
|
||||
<span className="text-dark-300">Pareto:</span>
|
||||
<span className="text-green-400 font-medium">{stats.paretoCount}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-dark-700 rounded-lg">
|
||||
<div className="w-3 h-3 bg-blue-500 rounded-full" />
|
||||
<span className="text-dark-300">FEA:</span>
|
||||
<span className="text-blue-400 font-medium">{stats.feaCount}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-dark-700 rounded-lg">
|
||||
<div className="w-3 h-3 bg-purple-500 rounded-full" />
|
||||
<span className="text-dark-300">NN:</span>
|
||||
<span className="text-purple-400 font-medium">{stats.nnCount}</span>
|
||||
</div>
|
||||
{stats.infeasibleCount > 0 && (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-dark-700 rounded-lg">
|
||||
<div className="w-3 h-3 bg-red-500 rounded-full" />
|
||||
<span className="text-dark-300">Infeasible:</span>
|
||||
<span className="text-red-400 font-medium">{stats.infeasibleCount}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex gap-4 items-center justify-between mb-3">
|
||||
<div className="flex gap-2 items-center">
|
||||
{objectives.length >= 3 && (
|
||||
<div className="flex rounded-lg overflow-hidden border border-dark-600">
|
||||
<button
|
||||
onClick={() => setViewMode('2d')}
|
||||
className={`px-3 py-1.5 text-sm font-medium transition-colors ${
|
||||
viewMode === '2d'
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
2D
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('3d')}
|
||||
className={`px-3 py-1.5 text-sm font-medium transition-colors ${
|
||||
viewMode === '3d'
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
3D
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Objective selectors */}
|
||||
<div className="flex gap-2 items-center text-sm">
|
||||
<label className="text-dark-400">X:</label>
|
||||
<select
|
||||
value={selectedObjectives[0]}
|
||||
onChange={(e) => setSelectedObjectives([parseInt(e.target.value), selectedObjectives[1], selectedObjectives[2]])}
|
||||
className="px-2 py-1.5 bg-dark-700 border border-dark-600 rounded text-white text-sm"
|
||||
>
|
||||
{objectives.map((obj, idx) => (
|
||||
<option key={idx} value={idx}>{obj.name}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<label className="text-dark-400 ml-2">Y:</label>
|
||||
<select
|
||||
value={selectedObjectives[1]}
|
||||
onChange={(e) => setSelectedObjectives([selectedObjectives[0], parseInt(e.target.value), selectedObjectives[2]])}
|
||||
className="px-2 py-1.5 bg-dark-700 border border-dark-600 rounded text-white text-sm"
|
||||
>
|
||||
{objectives.map((obj, idx) => (
|
||||
<option key={idx} value={idx}>{obj.name}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{viewMode === '3d' && objectives.length >= 3 && (
|
||||
<>
|
||||
<label className="text-dark-400 ml-2">Z:</label>
|
||||
<select
|
||||
value={selectedObjectives[2]}
|
||||
onChange={(e) => setSelectedObjectives([selectedObjectives[0], selectedObjectives[1], parseInt(e.target.value)])}
|
||||
className="px-2 py-1.5 bg-dark-700 border border-dark-600 rounded text-white text-sm"
|
||||
>
|
||||
{objectives.map((obj, idx) => (
|
||||
<option key={idx} value={idx}>{obj.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Plot
|
||||
data={traces as any}
|
||||
layout={layout}
|
||||
config={{
|
||||
displayModeBar: true,
|
||||
displaylogo: false,
|
||||
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
|
||||
toImageButtonOptions: {
|
||||
format: 'png',
|
||||
filename: 'pareto_front',
|
||||
height: 800,
|
||||
width: 1200,
|
||||
scale: 2
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
|
||||
{/* Pareto Front Table for 2D view */}
|
||||
{viewMode === '2d' && sortedParetoTrials.length > 0 && (
|
||||
<div className="mt-4 max-h-48 overflow-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 bg-dark-800">
|
||||
<tr className="border-b border-dark-600">
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Trial</th>
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">{objectives[i]?.name || 'Obj 1'}</th>
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">{objectives[j]?.name || 'Obj 2'}</th>
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedParetoTrials.slice(0, 10).map(trial => (
|
||||
<tr key={trial.trial_number} className="border-b border-dark-700 hover:bg-dark-750">
|
||||
<td className="py-2 px-3 font-mono text-white">#{trial.trial_number}</td>
|
||||
<td className="py-2 px-3 font-mono text-green-400">
|
||||
{getObjValue(trial, i).toExponential(4)}
|
||||
</td>
|
||||
<td className="py-2 px-3 font-mono text-green-400">
|
||||
{getObjValue(trial, j).toExponential(4)}
|
||||
</td>
|
||||
<td className="py-2 px-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${
|
||||
(trial.source || trial.user_attrs?.source) === 'NN'
|
||||
? 'bg-purple-500/20 text-purple-400'
|
||||
: 'bg-blue-500/20 text-blue-400'
|
||||
}`}>
|
||||
{trial.source || trial.user_attrs?.source || 'FEA'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{sortedParetoTrials.length > 10 && (
|
||||
<div className="text-center py-2 text-dark-500 text-xs">
|
||||
Showing 10 of {sortedParetoTrials.length} Pareto-optimal solutions
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import Plot from 'react-plotly.js';
|
||||
import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
|
||||
|
||||
interface Run {
|
||||
run_id: number;
|
||||
name: string;
|
||||
source: 'FEA' | 'NN';
|
||||
trial_count: number;
|
||||
best_value: number | null;
|
||||
avg_value: number | null;
|
||||
first_trial: string | null;
|
||||
last_trial: string | null;
|
||||
}
|
||||
|
||||
interface PlotlyRunComparisonProps {
|
||||
runs: Run[];
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export function PlotlyRunComparison({ runs, height = 400 }: PlotlyRunComparisonProps) {
|
||||
const chartData = useMemo(() => {
|
||||
if (runs.length === 0) return null;
|
||||
|
||||
// Separate FEA and NN runs
|
||||
const feaRuns = runs.filter(r => r.source === 'FEA');
|
||||
const nnRuns = runs.filter(r => r.source === 'NN');
|
||||
|
||||
// Create bar chart for trial counts
|
||||
const trialCountData = {
|
||||
x: runs.map(r => r.name),
|
||||
y: runs.map(r => r.trial_count),
|
||||
type: 'bar' as const,
|
||||
name: 'Trial Count',
|
||||
marker: {
|
||||
color: runs.map(r => r.source === 'NN' ? 'rgba(147, 51, 234, 0.8)' : 'rgba(59, 130, 246, 0.8)'),
|
||||
line: { color: runs.map(r => r.source === 'NN' ? 'rgb(147, 51, 234)' : 'rgb(59, 130, 246)'), width: 1 }
|
||||
},
|
||||
hovertemplate: '<b>%{x}</b><br>Trials: %{y}<extra></extra>'
|
||||
};
|
||||
|
||||
// Create line chart for best values
|
||||
const bestValueData = {
|
||||
x: runs.map(r => r.name),
|
||||
y: runs.map(r => r.best_value),
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines+markers' as const,
|
||||
name: 'Best Value',
|
||||
yaxis: 'y2',
|
||||
line: { color: 'rgba(16, 185, 129, 1)', width: 2 },
|
||||
marker: { size: 8, color: 'rgba(16, 185, 129, 1)' },
|
||||
hovertemplate: '<b>%{x}</b><br>Best: %{y:.4e}<extra></extra>'
|
||||
};
|
||||
|
||||
return { trialCountData, bestValueData, feaRuns, nnRuns };
|
||||
}, [runs]);
|
||||
|
||||
// Calculate statistics
|
||||
const stats = useMemo(() => {
|
||||
if (runs.length === 0) return null;
|
||||
|
||||
const totalTrials = runs.reduce((sum, r) => sum + r.trial_count, 0);
|
||||
const feaTrials = runs.filter(r => r.source === 'FEA').reduce((sum, r) => sum + r.trial_count, 0);
|
||||
const nnTrials = runs.filter(r => r.source === 'NN').reduce((sum, r) => sum + r.trial_count, 0);
|
||||
|
||||
const bestValues = runs.map(r => r.best_value).filter((v): v is number => v !== null);
|
||||
const overallBest = bestValues.length > 0 ? Math.min(...bestValues) : null;
|
||||
|
||||
// Calculate improvement from first FEA run to overall best
|
||||
const feaRuns = runs.filter(r => r.source === 'FEA');
|
||||
const firstFEA = feaRuns.length > 0 ? feaRuns[0].best_value : null;
|
||||
const improvement = firstFEA && overallBest ? ((firstFEA - overallBest) / Math.abs(firstFEA)) * 100 : null;
|
||||
|
||||
return {
|
||||
totalTrials,
|
||||
feaTrials,
|
||||
nnTrials,
|
||||
overallBest,
|
||||
improvement,
|
||||
totalRuns: runs.length,
|
||||
feaRuns: runs.filter(r => r.source === 'FEA').length,
|
||||
nnRuns: runs.filter(r => r.source === 'NN').length
|
||||
};
|
||||
}, [runs]);
|
||||
|
||||
if (!chartData || !stats) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64 text-dark-400">
|
||||
No run data available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Stats Summary */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||||
<div className="bg-dark-750 rounded-lg p-3">
|
||||
<div className="text-xs text-dark-400 mb-1">Total Runs</div>
|
||||
<div className="text-xl font-bold text-white">{stats.totalRuns}</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-3">
|
||||
<div className="text-xs text-dark-400 mb-1">Total Trials</div>
|
||||
<div className="text-xl font-bold text-white">{stats.totalTrials}</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-3">
|
||||
<div className="text-xs text-dark-400 mb-1">FEA Trials</div>
|
||||
<div className="text-xl font-bold text-blue-400">{stats.feaTrials}</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-3">
|
||||
<div className="text-xs text-dark-400 mb-1">NN Trials</div>
|
||||
<div className="text-xl font-bold text-purple-400">{stats.nnTrials}</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-3">
|
||||
<div className="text-xs text-dark-400 mb-1">Best Value</div>
|
||||
<div className="text-xl font-bold text-green-400">
|
||||
{stats.overallBest !== null ? stats.overallBest.toExponential(3) : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-3">
|
||||
<div className="text-xs text-dark-400 mb-1">Improvement</div>
|
||||
<div className="text-xl font-bold text-primary-400 flex items-center gap-1">
|
||||
{stats.improvement !== null ? (
|
||||
<>
|
||||
{stats.improvement > 0 ? <TrendingDown className="w-4 h-4" /> :
|
||||
stats.improvement < 0 ? <TrendingUp className="w-4 h-4" /> :
|
||||
<Minus className="w-4 h-4" />}
|
||||
{Math.abs(stats.improvement).toFixed(1)}%
|
||||
</>
|
||||
) : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<Plot
|
||||
data={[chartData.trialCountData, chartData.bestValueData]}
|
||||
layout={{
|
||||
height,
|
||||
margin: { l: 60, r: 60, t: 40, b: 100 },
|
||||
paper_bgcolor: 'transparent',
|
||||
plot_bgcolor: 'transparent',
|
||||
font: { color: '#9ca3af', size: 11 },
|
||||
showlegend: true,
|
||||
legend: {
|
||||
orientation: 'h',
|
||||
y: 1.12,
|
||||
x: 0.5,
|
||||
xanchor: 'center',
|
||||
bgcolor: 'transparent'
|
||||
},
|
||||
xaxis: {
|
||||
tickangle: -45,
|
||||
gridcolor: 'rgba(75, 85, 99, 0.3)',
|
||||
linecolor: 'rgba(75, 85, 99, 0.5)',
|
||||
tickfont: { size: 10 }
|
||||
},
|
||||
yaxis: {
|
||||
title: { text: 'Trial Count' },
|
||||
gridcolor: 'rgba(75, 85, 99, 0.3)',
|
||||
linecolor: 'rgba(75, 85, 99, 0.5)',
|
||||
zeroline: false
|
||||
},
|
||||
yaxis2: {
|
||||
title: { text: 'Best Value' },
|
||||
overlaying: 'y',
|
||||
side: 'right',
|
||||
gridcolor: 'rgba(75, 85, 99, 0.1)',
|
||||
linecolor: 'rgba(75, 85, 99, 0.5)',
|
||||
zeroline: false,
|
||||
tickformat: '.2e'
|
||||
},
|
||||
bargap: 0.3,
|
||||
hovermode: 'x unified'
|
||||
}}
|
||||
config={{
|
||||
displayModeBar: true,
|
||||
displaylogo: false,
|
||||
modeBarButtonsToRemove: ['select2d', 'lasso2d', 'autoScale2d']
|
||||
}}
|
||||
className="w-full"
|
||||
useResizeHandler
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
|
||||
{/* Runs Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-dark-600">
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Run Name</th>
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Source</th>
|
||||
<th className="text-right py-2 px-3 text-dark-400 font-medium">Trials</th>
|
||||
<th className="text-right py-2 px-3 text-dark-400 font-medium">Best Value</th>
|
||||
<th className="text-right py-2 px-3 text-dark-400 font-medium">Avg Value</th>
|
||||
<th className="text-left py-2 px-3 text-dark-400 font-medium">Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{runs.map((run) => {
|
||||
// Calculate duration if times available
|
||||
let duration = '-';
|
||||
if (run.first_trial && run.last_trial) {
|
||||
const start = new Date(run.first_trial);
|
||||
const end = new Date(run.last_trial);
|
||||
const diffMs = end.getTime() - start.getTime();
|
||||
const diffMins = Math.round(diffMs / 60000);
|
||||
if (diffMins < 60) {
|
||||
duration = `${diffMins}m`;
|
||||
} else {
|
||||
const hours = Math.floor(diffMins / 60);
|
||||
const mins = diffMins % 60;
|
||||
duration = `${hours}h ${mins}m`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={run.run_id} className="border-b border-dark-700 hover:bg-dark-750">
|
||||
<td className="py-2 px-3 font-mono text-white">{run.name}</td>
|
||||
<td className="py-2 px-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${
|
||||
run.source === 'NN'
|
||||
? 'bg-purple-500/20 text-purple-400'
|
||||
: 'bg-blue-500/20 text-blue-400'
|
||||
}`}>
|
||||
{run.source}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-white">{run.trial_count}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-green-400">
|
||||
{run.best_value !== null ? run.best_value.toExponential(4) : '-'}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-dark-300">
|
||||
{run.avg_value !== null ? run.avg_value.toExponential(4) : '-'}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-dark-400">{duration}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlotlyRunComparison;
|
||||
@@ -1,202 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import Plot from 'react-plotly.js';
|
||||
|
||||
interface TrialData {
|
||||
trial_number: number;
|
||||
values: number[];
|
||||
source?: 'FEA' | 'NN' | 'V10_FEA';
|
||||
user_attrs?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface PlotlySurrogateQualityProps {
|
||||
trials: TrialData[];
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export function PlotlySurrogateQuality({
|
||||
trials,
|
||||
height = 400
|
||||
}: PlotlySurrogateQualityProps) {
|
||||
const { feaTrials, nnTrials, timeline } = useMemo(() => {
|
||||
const fea = trials.filter(t => t.source === 'FEA' || t.source === 'V10_FEA');
|
||||
const nn = trials.filter(t => t.source === 'NN');
|
||||
|
||||
// Sort by trial number for timeline
|
||||
const sorted = [...trials].sort((a, b) => a.trial_number - b.trial_number);
|
||||
|
||||
// Calculate source distribution over time
|
||||
const timeline: { trial: number; feaCount: number; nnCount: number }[] = [];
|
||||
let feaCount = 0;
|
||||
let nnCount = 0;
|
||||
|
||||
sorted.forEach(t => {
|
||||
if (t.source === 'NN') nnCount++;
|
||||
else feaCount++;
|
||||
|
||||
timeline.push({
|
||||
trial: t.trial_number,
|
||||
feaCount,
|
||||
nnCount
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
feaTrials: fea,
|
||||
nnTrials: nn,
|
||||
timeline
|
||||
};
|
||||
}, [trials]);
|
||||
|
||||
if (nnTrials.length === 0) {
|
||||
return (
|
||||
<div className="h-64 flex items-center justify-center text-dark-400">
|
||||
<p>No neural network evaluations in this study</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Objective distribution by source
|
||||
const feaObjectives = feaTrials.map(t => t.values[0]).filter(v => v !== undefined && !isNaN(v));
|
||||
const nnObjectives = nnTrials.map(t => t.values[0]).filter(v => v !== undefined && !isNaN(v));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Source Distribution Over Time */}
|
||||
<Plot
|
||||
data={[
|
||||
{
|
||||
x: timeline.map(t => t.trial),
|
||||
y: timeline.map(t => t.feaCount),
|
||||
type: 'scatter',
|
||||
mode: 'lines',
|
||||
name: 'FEA Cumulative',
|
||||
line: { color: '#3b82f6', width: 2 },
|
||||
fill: 'tozeroy',
|
||||
fillcolor: 'rgba(59, 130, 246, 0.2)'
|
||||
},
|
||||
{
|
||||
x: timeline.map(t => t.trial),
|
||||
y: timeline.map(t => t.nnCount),
|
||||
type: 'scatter',
|
||||
mode: 'lines',
|
||||
name: 'NN Cumulative',
|
||||
line: { color: '#a855f7', width: 2 },
|
||||
fill: 'tozeroy',
|
||||
fillcolor: 'rgba(168, 85, 247, 0.2)'
|
||||
}
|
||||
]}
|
||||
layout={{
|
||||
title: {
|
||||
text: 'Evaluation Source Over Time',
|
||||
font: { color: '#fff', size: 14 }
|
||||
},
|
||||
height: height * 0.6,
|
||||
margin: { l: 60, r: 30, t: 50, b: 50 },
|
||||
paper_bgcolor: 'transparent',
|
||||
plot_bgcolor: 'transparent',
|
||||
xaxis: {
|
||||
title: { text: 'Trial Number', font: { color: '#888' } },
|
||||
tickfont: { color: '#888' },
|
||||
gridcolor: 'rgba(255,255,255,0.05)'
|
||||
},
|
||||
yaxis: {
|
||||
title: { text: 'Cumulative Count', font: { color: '#888' } },
|
||||
tickfont: { color: '#888' },
|
||||
gridcolor: 'rgba(255,255,255,0.1)'
|
||||
},
|
||||
legend: {
|
||||
font: { color: '#888' },
|
||||
bgcolor: 'rgba(0,0,0,0.5)',
|
||||
orientation: 'h',
|
||||
y: 1.1
|
||||
},
|
||||
showlegend: true
|
||||
}}
|
||||
config={{
|
||||
displayModeBar: true,
|
||||
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
|
||||
displaylogo: false
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
|
||||
{/* Objective Distribution by Source */}
|
||||
<Plot
|
||||
data={[
|
||||
{
|
||||
x: feaObjectives,
|
||||
type: 'histogram',
|
||||
name: 'FEA',
|
||||
marker: { color: 'rgba(59, 130, 246, 0.7)' },
|
||||
opacity: 0.8
|
||||
} as any,
|
||||
{
|
||||
x: nnObjectives,
|
||||
type: 'histogram',
|
||||
name: 'NN',
|
||||
marker: { color: 'rgba(168, 85, 247, 0.7)' },
|
||||
opacity: 0.8
|
||||
} as any
|
||||
]}
|
||||
layout={{
|
||||
title: {
|
||||
text: 'Objective Distribution by Source',
|
||||
font: { color: '#fff', size: 14 }
|
||||
},
|
||||
height: height * 0.5,
|
||||
margin: { l: 60, r: 30, t: 50, b: 50 },
|
||||
paper_bgcolor: 'transparent',
|
||||
plot_bgcolor: 'transparent',
|
||||
xaxis: {
|
||||
title: { text: 'Objective Value', font: { color: '#888' } },
|
||||
tickfont: { color: '#888' },
|
||||
gridcolor: 'rgba(255,255,255,0.05)'
|
||||
},
|
||||
yaxis: {
|
||||
title: { text: 'Count', font: { color: '#888' } },
|
||||
tickfont: { color: '#888' },
|
||||
gridcolor: 'rgba(255,255,255,0.1)'
|
||||
},
|
||||
barmode: 'overlay',
|
||||
legend: {
|
||||
font: { color: '#888' },
|
||||
bgcolor: 'rgba(0,0,0,0.5)',
|
||||
orientation: 'h',
|
||||
y: 1.1
|
||||
},
|
||||
showlegend: true
|
||||
}}
|
||||
config={{
|
||||
displayModeBar: true,
|
||||
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
|
||||
displaylogo: false
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
|
||||
{/* FEA vs NN Best Values Comparison */}
|
||||
{feaObjectives.length > 0 && nnObjectives.length > 0 && (
|
||||
<div className="grid grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-dark-750 rounded-lg p-4 border border-dark-600">
|
||||
<div className="text-xs text-dark-400 uppercase mb-2">FEA Best</div>
|
||||
<div className="text-xl font-mono text-blue-400">
|
||||
{Math.min(...feaObjectives).toExponential(4)}
|
||||
</div>
|
||||
<div className="text-xs text-dark-500 mt-1">
|
||||
from {feaObjectives.length} evaluations
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-4 border border-dark-600">
|
||||
<div className="text-xs text-dark-400 uppercase mb-2">NN Best</div>
|
||||
<div className="text-xl font-mono text-purple-400">
|
||||
{Math.min(...nnObjectives).toExponential(4)}
|
||||
</div>
|
||||
<div className="text-xs text-dark-500 mt-1">
|
||||
from {nnObjectives.length} predictions
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
# Plotly Chart Components
|
||||
|
||||
Interactive visualization components using Plotly.js for the Atomizer Dashboard.
|
||||
|
||||
## Overview
|
||||
|
||||
These components provide enhanced interactivity compared to Recharts:
|
||||
- Native zoom, pan, and selection
|
||||
- Export to PNG/SVG
|
||||
- Hover tooltips with detailed information
|
||||
- Brush filtering (parallel coordinates)
|
||||
- 3D visualization support
|
||||
|
||||
## Components
|
||||
|
||||
### PlotlyParallelCoordinates
|
||||
|
||||
Multi-dimensional data visualization showing relationships between all variables.
|
||||
|
||||
```tsx
|
||||
import { PlotlyParallelCoordinates } from '../components/plotly';
|
||||
|
||||
<PlotlyParallelCoordinates
|
||||
trials={allTrials}
|
||||
objectives={studyMetadata.objectives}
|
||||
designVariables={studyMetadata.design_variables}
|
||||
paretoFront={paretoFront}
|
||||
height={450}
|
||||
/>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| trials | Trial[] | All trial data |
|
||||
| objectives | Objective[] | Objective definitions |
|
||||
| designVariables | DesignVariable[] | Design variable definitions |
|
||||
| paretoFront | Trial[] | Pareto-optimal trials (optional) |
|
||||
| height | number | Chart height in pixels |
|
||||
|
||||
**Features:**
|
||||
- Drag on axes to filter data
|
||||
- Double-click to reset filters
|
||||
- Color coding: FEA (blue), NN (orange), Pareto (green)
|
||||
|
||||
### PlotlyParetoPlot
|
||||
|
||||
2D/3D scatter plot for Pareto front visualization.
|
||||
|
||||
```tsx
|
||||
<PlotlyParetoPlot
|
||||
trials={allTrials}
|
||||
paretoFront={paretoFront}
|
||||
objectives={studyMetadata.objectives}
|
||||
height={350}
|
||||
/>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| trials | Trial[] | All trial data |
|
||||
| paretoFront | Trial[] | Pareto-optimal trials |
|
||||
| objectives | Objective[] | Objective definitions |
|
||||
| height | number | Chart height in pixels |
|
||||
|
||||
**Features:**
|
||||
- Toggle between 2D and 3D views
|
||||
- Axis selector for multi-objective problems
|
||||
- Click to select trials
|
||||
- Hover for trial details
|
||||
|
||||
### PlotlyConvergencePlot
|
||||
|
||||
Optimization progress over trials.
|
||||
|
||||
```tsx
|
||||
<PlotlyConvergencePlot
|
||||
trials={allTrials}
|
||||
objectiveIndex={0}
|
||||
objectiveName="weighted_objective"
|
||||
direction="minimize"
|
||||
height={350}
|
||||
/>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| trials | Trial[] | All trial data |
|
||||
| objectiveIndex | number | Which objective to plot |
|
||||
| objectiveName | string | Objective display name |
|
||||
| direction | 'minimize' \| 'maximize' | Optimization direction |
|
||||
| height | number | Chart height |
|
||||
| showRangeSlider | boolean | Show zoom slider |
|
||||
|
||||
**Features:**
|
||||
- Scatter points for each trial
|
||||
- Best-so-far step line
|
||||
- Range slider for zooming
|
||||
- FEA vs NN differentiation
|
||||
|
||||
### PlotlyParameterImportance
|
||||
|
||||
Correlation-based parameter sensitivity analysis.
|
||||
|
||||
```tsx
|
||||
<PlotlyParameterImportance
|
||||
trials={allTrials}
|
||||
designVariables={studyMetadata.design_variables}
|
||||
objectiveIndex={0}
|
||||
objectiveName="weighted_objective"
|
||||
height={350}
|
||||
/>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| trials | Trial[] | All trial data |
|
||||
| designVariables | DesignVariable[] | Design variables |
|
||||
| objectiveIndex | number | Which objective |
|
||||
| objectiveName | string | Objective display name |
|
||||
| height | number | Chart height |
|
||||
|
||||
**Features:**
|
||||
- Horizontal bar chart of correlations
|
||||
- Sort by importance or name
|
||||
- Color: Red (positive), Green (negative)
|
||||
- Pearson correlation coefficient
|
||||
|
||||
## Bundle Optimization
|
||||
|
||||
To minimize bundle size, we use:
|
||||
|
||||
1. **plotly.js-basic-dist**: Smaller bundle (~1MB vs 3.5MB)
|
||||
- Includes: scatter, bar, parcoords
|
||||
- Excludes: 3D plots, maps, animations
|
||||
|
||||
2. **Lazy Loading**: Components loaded on demand
|
||||
```tsx
|
||||
const PlotlyParetoPlot = lazy(() =>
|
||||
import('./plotly/PlotlyParetoPlot')
|
||||
.then(m => ({ default: m.PlotlyParetoPlot }))
|
||||
);
|
||||
```
|
||||
|
||||
3. **Code Splitting**: Vite config separates Plotly into its own chunk
|
||||
```ts
|
||||
manualChunks: {
|
||||
plotly: ['plotly.js-basic-dist', 'react-plotly.js']
|
||||
}
|
||||
```
|
||||
|
||||
## Usage with Suspense
|
||||
|
||||
Always wrap Plotly components with Suspense:
|
||||
|
||||
```tsx
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyParetoPlot {...props} />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
## Type Definitions
|
||||
|
||||
```typescript
|
||||
interface Trial {
|
||||
trial_number: number;
|
||||
values: number[];
|
||||
params: Record<string, number>;
|
||||
user_attrs?: Record<string, any>;
|
||||
source?: 'FEA' | 'NN' | 'V10_FEA';
|
||||
}
|
||||
|
||||
interface Objective {
|
||||
name: string;
|
||||
direction?: 'minimize' | 'maximize';
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
interface DesignVariable {
|
||||
name: string;
|
||||
unit?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
Components use transparent backgrounds for dark theme compatibility:
|
||||
- `paper_bgcolor: 'rgba(0,0,0,0)'`
|
||||
- `plot_bgcolor: 'rgba(0,0,0,0)'`
|
||||
- Font: Inter, system-ui, sans-serif
|
||||
- Grid colors: Tailwind gray palette
|
||||
|
||||
## Export Options
|
||||
|
||||
All Plotly charts include a mode bar with:
|
||||
- Download PNG
|
||||
- Download SVG (via menu)
|
||||
- Zoom, Pan, Reset
|
||||
- Auto-scale
|
||||
|
||||
Configure export in the `config` prop:
|
||||
```tsx
|
||||
config={{
|
||||
toImageButtonOptions: {
|
||||
format: 'png',
|
||||
filename: 'my_chart',
|
||||
height: 600,
|
||||
width: 1200,
|
||||
scale: 2
|
||||
}
|
||||
}}
|
||||
```
|
||||
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* Plotly-based interactive chart components
|
||||
*
|
||||
* These components provide enhanced interactivity compared to Recharts:
|
||||
* - Native zoom/pan
|
||||
* - Brush selection on axes
|
||||
* - 3D views for multi-objective problems
|
||||
* - Export to PNG/SVG
|
||||
* - Detailed hover tooltips
|
||||
*/
|
||||
|
||||
export { PlotlyParallelCoordinates } from './PlotlyParallelCoordinates';
|
||||
export { PlotlyParetoPlot } from './PlotlyParetoPlot';
|
||||
export { PlotlyConvergencePlot } from './PlotlyConvergencePlot';
|
||||
export { PlotlyParameterImportance } from './PlotlyParameterImportance';
|
||||
Reference in New Issue
Block a user