import { useState, useRef, useEffect } from "react"; import Plot from "react-plotly.js"; import type { PlotData } from "./types"; import { Button, Card } from "@elvis/ui"; interface OptimizationPlotProps { data: PlotData; isLoading: boolean; xlim: [number, number]; ylim: [number, number]; setAxisLimits: (xlim: [number, number], ylim: [number, number]) => void; } export default function OptimizationPlot({ data, isLoading, xlim, ylim, setAxisLimits }: OptimizationPlotProps) { // data let x: number[] = data.functionValues ? data.functionValues.x : []; let y: number[] = data.functionValues ? data.functionValues.y : []; let z: number[][] = data.functionValues && data.functionValues.z ? data.functionValues.z : []; let trajX: number[] = data.trajectoryValues ? data.trajectoryValues.x : []; let trajY: number[] = data.trajectoryValues ? data.trajectoryValues.y : []; let trajZ: number[] = data.trajectoryValues && data.trajectoryValues.z ? data.trajectoryValues.z : []; const [colorScaleRange, setColorScaleRange] = useState<[number, number] | null>(null); const nextColorScaleRangeRef = useRef<[number, number] | null>(null); useEffect(() => { if (z) { if (!z || z.length === 0) { nextColorScaleRangeRef.current = null; return; } let zMin = Infinity; let zMax = -Infinity; for (let i = 0; i < z.length; i++) { for (let j = 0; j < z[i].length; j++) { const v = z[i][j]; if (!Number.isFinite(v)) { continue; } if (v < zMin) { zMin = v; } if (v > zMax) { zMax = v; } } } const padding = (zMax - zMin) * 0.1; nextColorScaleRangeRef.current = [zMin - padding, zMax + padding]; if (colorScaleRange === null) { setColorScaleRange(nextColorScaleRangeRef.current); } } }, [z]); function updateColorScaleRange() { if (nextColorScaleRangeRef.current) { setColorScaleRange(nextColorScaleRangeRef.current); nextColorScaleRangeRef.current = null; } } const plotRef = useRef(null); const layoutRef = useRef({ dragmode: 'pan', showlegend: false, xaxis: { title: { text: 'x' }, range: xlim, }, yaxis: { title: { text: 'y' }, range: ylim, }, margin: { t: 40, r: 40, b: 40, l: 40 } }) const hasNoData = !data.functionValues && !data.trajectoryValues; if (isLoading && hasNoData) { return (
Loading...
); } if (z.length === 0) { return ( { const x0 = event['xaxis.range[0]']; const x1 = event['xaxis.range[1]']; const y0 = event['yaxis.range[0]']; const y1 = event['yaxis.range[1]']; if ( typeof x0 === "number" && typeof x1 === "number" && typeof y0 === "number" && typeof y1 === "number" ) { setAxisLimits([x0, x1], [y0, y1]); } }} data={[ { x: x, y: y, type: 'scatter', mode: 'lines', line: { color: '#1f77b4', width: 2 }, hoverinfo: "skip", }, { x: trajX, y: trajY, type: 'scatter', mode: 'lines+markers', line: { color: '#d97871', width: 2 }, marker: { color: '#d97871', size: 10 }, hoverinfo: "skip", }, { x: trajX.length > 0 ? [trajX.at(-1)!] : [], y: trajY.length > 0 ? [trajY.at(-1)!] : [], type: 'scatter', mode: 'markers', marker: { color: 'red', size: 12 }, hoverinfo: "skip", } ]} layout={layoutRef.current} style={{ width: '100%', height: '100%' }} config={{ responsive: true, displayModeBar: true, scrollZoom: true, }} /> ); } else { return ( { const x0 = event['xaxis.range[0]']; const x1 = event['xaxis.range[1]']; const y0 = event['yaxis.range[0]']; const y1 = event['yaxis.range[1]']; if ( typeof x0 === "number" && typeof x1 === "number" && typeof y0 === "number" && typeof y1 === "number" ) { setAxisLimits([x0, x1], [y0, y1]); } }} data={[ { x: x, y: y, z: z, zmin: colorScaleRange?.[0], zmax: colorScaleRange?.[1], type: 'contour', colorscale: 'Viridis', hoverinfo: "skip", contours: { coloring: "heatmap", showlines: false, } }, { x: trajX, y: trajY, z: trajZ, type: 'scatter', mode: 'lines+markers', line: { color: '#d97871', width: 2 }, marker: { color: '#d97871', size: 10 }, hoverinfo: "skip", }, { x: trajX.length > 0 ? [trajX.at(-1)!] : [], y: trajY.length > 0 ? [trajY.at(-1)!] : [], z: trajZ.length > 0 ? [trajZ.at(-1)!] : [], type: 'scatter', mode: 'markers', marker: { color: 'red', size: 12 }, hoverinfo: "skip", } ]} layout={layoutRef.current} className="w-full flex-1" config={{ responsive: true, displayModeBar: false, scrollZoom: true, }} />
); } }