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