optimization / frontends /react /src /OptimizationPlot.tsx
joel-woodfield's picture
Show when plot is loading
4f4a1ca
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<any>(null);
const layoutRef = useRef<any>({
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 (
<Card className="min-h-[320px] flex items-center justify-center p-6">
<div className="text-center">
<div className="text-lg font-medium">Loading...</div>
</div>
</Card>
);
}
if (z.length === 0) {
return (
<Card className="min-h-[320px]">
<Plot
ref={plotRef}
onRelayout={(event) => {
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,
}}
/>
</Card>
);
} else {
return (
<Card className="flex flex-col min-h-[420px]">
<Plot
ref={plotRef}
onRelayout={(event) => {
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,
}}
/>
<div className="mt-2 flex justify-end">
<Button label="Update Color Scale" onClick={updateColorScaleRange} />
</div>
</Card>
);
}
}