import type { Data, Layout } from "plotly.js"; import Plot from "@/lib/plotly"; import { DESIGN_BOUNDS, TARGET_META, type RegistryEntrySummary, type SweepResponse, } from "@/types/api"; import { roverColor } from "@/lib/rover-colors"; interface SweepChartProps { /** Latest sweep response from `/sweep`. */ data: SweepResponse; /** Registry rovers to overlay (already filtered to "selected"). */ overlayRovers: RegistryEntrySummary[]; } /** * Plotly chart for parametric sweeps. * * - 1-D sweeps render as a line of the target metric vs the X * variable, with optional dashed vertical markers at each * selected real rover's value of X. * - 2-D sweeps render as a heatmap of the target metric over the * X × Y plane, with optional scatter dots at each selected real * rover's (X, Y) coordinates. * * Real-rover overlay markers use the same colour palette as the * single-design page so a user toggling between tabs sees a * consistent visual identity per rover. */ export function SweepChart({ data, overlayRovers }: SweepChartProps) { const targetMeta = TARGET_META[data.target]; const xLabel = formatAxisLabel(data.x_variable); const yLabel = data.y_variable ? formatAxisLabel(data.y_variable) : null; const zLabel = `${targetMeta.label} (${targetMeta.unit})`; if (data.y_variable === null) { const z = data.z_values as number[]; const traces: Data[] = [ { type: "scatter", mode: "lines+markers", x: data.x_values, y: z, line: { color: "rgb(40, 75, 180)", width: 2 }, marker: { color: "rgb(40, 75, 180)", size: 6 }, name: targetMeta.label, hovertemplate: `${xLabel}: %{x}
${zLabel}: %{y:.3g}`, }, ]; // Vertical markers for overlay rovers, at each rover's value of // the X variable. Rovers whose X value falls outside the swept // range are clipped to the visible window by Plotly automatically. overlayRovers.forEach((rover) => { const xVal = (rover.design as unknown as Record)[ data.x_variable ]; if (typeof xVal !== "number" || !isFinite(xVal)) return; traces.push({ type: "scatter", mode: "lines", x: [xVal, xVal], y: [Math.min(...z), Math.max(...z)], line: { color: roverColor(rover.rover_name), dash: "dash", width: 1.5 }, name: rover.rover_name, hoverinfo: "name", }); }); const layout: Partial = { height: 420, margin: { l: 70, r: 30, t: 20, b: 60 }, paper_bgcolor: "rgba(0,0,0,0)", plot_bgcolor: "rgba(0,0,0,0)", xaxis: { title: { text: xLabel }, zeroline: false }, yaxis: { title: { text: zLabel }, zeroline: false }, showlegend: overlayRovers.length > 0, legend: { orientation: "h", y: -0.2 }, }; return ( ); } // 2-D heatmap const z = data.z_values as number[][]; const traces: Data[] = [ { type: "heatmap", x: data.x_values, y: data.y_values ?? [], z, colorscale: "Viridis", colorbar: { title: { text: zLabel }, len: 0.8 }, hovertemplate: `${xLabel}: %{x}
${yLabel}: %{y}
${zLabel}: %{z:.3g}`, }, ]; if (overlayRovers.length > 0) { const xs: number[] = []; const ys: number[] = []; const text: string[] = []; const colors: string[] = []; overlayRovers.forEach((r) => { const xv = (r.design as unknown as Record)[ data.x_variable ]; const yv = (r.design as unknown as Record)[ data.y_variable as string ]; if ( typeof xv !== "number" || typeof yv !== "number" || !isFinite(xv) || !isFinite(yv) ) { return; } xs.push(xv); ys.push(yv); text.push(r.rover_name); colors.push(roverColor(r.rover_name)); }); if (xs.length > 0) { traces.push({ type: "scatter", mode: "text+markers", x: xs, y: ys, text, textposition: "top center", textfont: { color: "white", size: 11 }, marker: { color: colors, size: 11, line: { color: "white", width: 1.5 }, symbol: "diamond", }, hovertemplate: "%{text}
(%{x}, %{y})", name: "Real rovers", showlegend: false, }); } } const layout: Partial = { height: 480, margin: { l: 70, r: 30, t: 20, b: 60 }, paper_bgcolor: "rgba(0,0,0,0)", plot_bgcolor: "rgba(0,0,0,0)", xaxis: { title: { text: xLabel } }, yaxis: { title: { text: yLabel ?? "" } }, showlegend: false, }; return ( ); } function formatAxisLabel(variable: string): string { const meta = ( DESIGN_BOUNDS as Record )[variable]; if (!meta) return variable; return meta.unit ? `${meta.label} (${meta.unit})` : meta.label; }