Spaces:
Running
Running
Download frontends/react/src/OptimizationPlot.tsx from elvis-hf/optimization: direct link, hf CLI and curl.
- Browser
- Download file 14.7 kB
-
https://huggingface.co/spaces/elvis-hf/optimization/resolve/main/frontends/react/src/OptimizationPlot.tsx
- Command line
-
hf download hf://spaces/elvis-hf/optimization/frontends/react/src/OptimizationPlot.tsx
-
curl -L -o OptimizationPlot.tsx https://huggingface.co/spaces/elvis-hf/optimization/resolve/main/frontends/react/src/OptimizationPlot.tsx
14.7 kB
| import { useMemo, useRef } from "react"; | |
| import Plot from "react-plotly.js"; | |
| import type { BivariatePlotView, ComparisonEntry, TrajectoryValues, PlotData } from "./types"; | |
| import { Card } from "@elvis/ui"; | |
| interface OptimizationPlotProps { | |
| comparisons?: (ComparisonEntry & { values?: TrajectoryValues })[]; | |
| data: PlotData; | |
| isLoading: boolean; | |
| xlim: [number, number]; | |
| ylim: [number, number]; | |
| setAxisLimits: (xlim: [number, number], ylim: [number, number]) => void; | |
| currentIteration: number; | |
| showGradientField: boolean; | |
| bivariatePlotView: BivariatePlotView; | |
| } | |
| export default function OptimizationPlot({ comparisons, data, isLoading, xlim, ylim, setAxisLimits, currentIteration, showGradientField, bivariatePlotView }: OptimizationPlotProps) { | |
| const isSurface = bivariatePlotView === "Function surface" && Boolean(data.functionValues?.z); | |
| const comparisonTraces = (comparisons ?? []).flatMap((entry, index) => entry.visible ? [ | |
| { x: entry.values?.x ?? [], y: entry.values?.y ?? [], z: isSurface ? entry.values?.z ?? [] : undefined, | |
| type: isSurface ? "scatter3d" as const : "scatter" as const, mode: "lines+markers" as const, | |
| name: `${index + 1}. ${entry.settings.algorithm}`, legendgroup: String(entry.id), showlegend: true, | |
| line: { color: entry.color, width: isSurface ? 6 : 2 }, | |
| marker: { color: entry.color, size: isSurface ? 2.5 : 6 } }, | |
| { x: entry.values?.x.slice(-1) ?? [], y: entry.values?.y.slice(-1) ?? [], z: isSurface ? entry.values?.z?.slice(-1) ?? [] : undefined, | |
| type: isSurface ? "scatter3d" as const : "scatter" as const, mode: "markers" as const, | |
| name: `${index + 1}. ${entry.settings.algorithm}`, legendgroup: String(entry.id), showlegend: false, | |
| marker: { color: entry.color, size: isSurface ? 4 : 12, line: { color: "white", width: isSurface ? 1 : 2 } } } | |
| ] : []); | |
| // 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 gradientX: number[][] = data.functionValues?.gradientX ?? []; | |
| let gradientY: number[][] = data.functionValues?.gradientY ?? []; | |
| console.log('gradientX', gradientX) | |
| console.log('gradientY', gradientY) | |
| 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 = useMemo<[number, number] | null>(() => { | |
| if (z.length === 0) return null; | |
| let zMin = Infinity; | |
| let zMax = -Infinity; | |
| for (const row of z) { | |
| for (const value of row) { | |
| if (!Number.isFinite(value)) continue; | |
| zMin = Math.min(zMin, value); | |
| zMax = Math.max(zMax, value); | |
| } | |
| } | |
| if (!Number.isFinite(zMin) || !Number.isFinite(zMax)) return null; | |
| const padding = zMax === zMin ? Math.max(Math.abs(zMin) * 0.1, 1) : (zMax - zMin) * 0.1; | |
| return [zMin - padding, zMax + padding]; | |
| }, [z]); | |
| const gradientField = useMemo(() => { | |
| const emptyField = { | |
| shaftX: [] as (number | null)[], | |
| shaftY: [] as (number | null)[], | |
| headX: [] as number[], | |
| headY: [] as number[], | |
| headAngles: [] as number[], | |
| hoverData: [] as number[][], | |
| }; | |
| if ( | |
| !showGradientField | |
| || gradientX.length !== y.length | |
| || gradientY.length !== y.length | |
| || x.length === 0 | |
| || y.length === 0 | |
| ) return emptyField; | |
| const rowStep = 5 //Math.max(1, Math.ceil(y.length / 10)); | |
| const columnStep = 5 //Math.max(1, Math.ceil(x.length / 10)); | |
| const shaftX: (number | null)[] = []; | |
| const shaftY: (number | null)[] = []; | |
| const headX: number[] = []; | |
| const headY: number[] = []; | |
| const headAngles: number[] = []; | |
| const hoverData: number[][] = []; | |
| const gradientLengths: number[] = []; | |
| for (let row = 0; row < gradientX.length; row++) { | |
| for (let column = 0; column < gradientX[row].length; column++) { | |
| const gx = gradientX[row][column]; | |
| const gy = gradientY[row][column]; | |
| const length = Math.hypot(gx, gy); | |
| if (Number.isFinite(length)) { | |
| gradientLengths.push(length); | |
| } | |
| } | |
| } | |
| const averageGradientLength = gradientLengths.length > 0 ? | |
| gradientLengths.reduce((sum, length) => sum + length, 0) / gradientLengths.length | |
| : 0; | |
| console.log('average_grad_length', averageGradientLength); | |
| console.log('cell diagonal', Math.hypot(x[columnStep]-x[0], y[rowStep]-y[0])); | |
| const grad_scale = 1.8*averageGradientLength / Math.hypot(x[columnStep]-x[0], y[rowStep]-y[0]); | |
| console.log('grad_scale', grad_scale); | |
| for (let row = 0; row < y.length; row += rowStep) { | |
| if (!gradientX[row] || !gradientY[row]) continue; | |
| for (let column = 0; column < x.length; column += columnStep) { | |
| const gx = gradientX[row][column]; | |
| const gy = gradientY[row][column]; | |
| const magnitude = Math.hypot(gx, gy); | |
| if (!Number.isFinite(magnitude) || magnitude === 0) continue; | |
| const vx = gx / grad_scale; | |
| const vy = gy / grad_scale; | |
| const arrowX = x[column] + vx; | |
| const arrowY = y[row] + vy; | |
| shaftX.push(x[column], arrowX, null); | |
| shaftY.push(y[row], arrowY, null); | |
| headX.push(arrowX); | |
| headY.push(arrowY); | |
| // Plotly marker angles rotate clockwise from the upward direction. | |
| headAngles.push(Math.atan2(vx, vy) * 180 / Math.PI); | |
| hoverData.push([x[column], y[row], gx, gy]); | |
| } | |
| } | |
| return { shaftX, shaftY, headX, headY, headAngles, hoverData }; | |
| }, [showGradientField, x, y, gradientX, gradientY]); | |
| 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; | |
| function zoomSurfaceAxes(event: React.WheelEvent) { | |
| event.preventDefault(); | |
| const factor = event.deltaY > 0 ? 1.2 : 1 / 1.2; | |
| const resizeRange = (range: [number, number]): [number, number] => { | |
| const center = (range[0] + range[1]) / 2; | |
| const halfWidth = (range[1] - range[0]) * factor / 2; | |
| return [center - halfWidth, center + halfWidth]; | |
| }; | |
| setAxisLimits(resizeRange(xlim), resizeRange(ylim)); | |
| } | |
| if (isLoading && hasNoData) { | |
| return ( | |
| <Card className="min-h-[320px] flex flex-1 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] flex-1"> | |
| <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", | |
| showlegend: false, | |
| }, | |
| { | |
| x: trajX, | |
| y: trajY, | |
| type: 'scatter', | |
| mode: 'lines+markers', | |
| line: { color: '#d97871', width: 2 }, | |
| marker: { color: '#d97871', size: 10 }, | |
| hoverinfo: "skip", | |
| showlegend: false, | |
| }, | |
| { | |
| x: trajX[currentIteration] !== undefined ? [trajX[currentIteration]] : [], | |
| y: trajY[currentIteration] !== undefined ? [trajY[currentIteration]] : [], | |
| type: 'scatter', | |
| mode: 'markers', | |
| marker: { color: 'red', size: 12 }, | |
| hoverinfo: "skip", | |
| showlegend: false, | |
| } | |
| ,...comparisonTraces | |
| ]} | |
| layout={{ ...layoutRef.current, showlegend: comparisons !== undefined, legend: { x: 0.02, y: 0.98, xanchor: "left", yanchor: "top", bgcolor: "rgba(255,255,255,0.75)" } }} | |
| style={{ width: '100%', height: '100%' }} | |
| config={{ | |
| responsive: true, | |
| displayModeBar: true, | |
| scrollZoom: true, | |
| }} | |
| /> | |
| </Card> | |
| ); | |
| } else { | |
| if (isSurface) { | |
| return ( | |
| <div className="min-h-[420px] flex-1" onWheel={zoomSurfaceAxes}> | |
| <Card className="h-full"> | |
| <Plot | |
| ref={plotRef} | |
| data={[ | |
| { | |
| x, y, z, | |
| type: 'surface', | |
| colorscale: 'Viridis', | |
| opacity: 0.82, | |
| showscale: true, | |
| hovertemplate: 'x: %{x:.5g}<br>y: %{y:.5g}<br>z: %{z:.5g}<extra></extra>', | |
| }, | |
| { | |
| x: trajX, y: trajY, z: trajZ, | |
| type: 'scatter3d', mode: 'lines+markers', | |
| line: { color: '#d97871', width: 6 }, | |
| marker: { color: '#d97871', size: 2.5 }, | |
| hoverinfo: 'skip', showlegend: false, | |
| }, | |
| { | |
| x: trajX[currentIteration] !== undefined ? [trajX[currentIteration]] : [], | |
| y: trajY[currentIteration] !== undefined ? [trajY[currentIteration]] : [], | |
| z: trajZ[currentIteration] !== undefined ? [trajZ[currentIteration]] : [], | |
| type: 'scatter3d', mode: 'markers', | |
| marker: { color: 'red', size: 7 }, | |
| hoverinfo: 'skip', showlegend: false, | |
| }, | |
| ...comparisonTraces, | |
| ]} | |
| layout={{ | |
| // Keep the user's camera angle while animation updates the trajectory data. | |
| uirevision: 'bivariate-surface', | |
| dragmode: 'orbit', | |
| showlegend: comparisons !== undefined, | |
| legend: { x: 0.02, y: 0.98, xanchor: 'left', yanchor: 'top', bgcolor: 'rgba(255,255,255,0.75)' }, | |
| scene: { | |
| aspectmode: 'cube', | |
| xaxis: { title: { text: 'x' }, range: xlim, autorange: false }, | |
| yaxis: { title: { text: 'y' }, range: ylim, autorange: false }, | |
| zaxis: { title: { text: 'z' }, range: colorScaleRange ?? undefined, autorange: false }, | |
| }, | |
| margin: { t: 30, r: 20, b: 20, l: 20 }, | |
| }} | |
| style={{ width: '100%', height: '100%' }} | |
| config={{ responsive: true, displayModeBar: true, scrollZoom: false }} | |
| /> | |
| </Card> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <Card className="min-h-[420px] flex-1"> | |
| <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", | |
| showlegend: false, | |
| contours: { | |
| coloring: "heatmap", | |
| showlines: false, | |
| } | |
| }, | |
| { | |
| x: gradientField.shaftX, | |
| y: gradientField.shaftY, | |
| type: 'scatter', | |
| mode: 'lines', | |
| line: { color: 'rgba(255, 255, 255, 0.8)', width: 1 }, | |
| hoverinfo: 'skip', | |
| showlegend: false, | |
| cliponaxis: true, | |
| }, | |
| { | |
| x: gradientField.headX, | |
| y: gradientField.headY, | |
| customdata: gradientField.hoverData, | |
| type: 'scatter', | |
| mode: 'markers', | |
| marker: { | |
| symbol: 'triangle-up', | |
| size: 5, | |
| color: 'rgba(255, 255, 255, 0.8)', | |
| angle: gradientField.headAngles, | |
| angleref: 'up', | |
| } as any, | |
| hovertemplate: | |
| 'x: %{customdata[0]:.5g}<br>' | |
| + 'y: %{customdata[1]:.5g}<br>' | |
| + 'gx: %{customdata[2]:.5g}<br>' | |
| + 'gy: %{customdata[3]:.5g}<extra></extra>', | |
| showlegend: false, | |
| cliponaxis: true, | |
| }, | |
| { | |
| x: trajX, | |
| y: trajY, | |
| customdata: trajZ, | |
| type: 'scatter', | |
| mode: 'lines+markers', | |
| line: { color: '#d97871', width: 2 }, | |
| marker: { color: '#d97871', size: 10 }, | |
| hovertemplate: 'x: %{x:.5g}<br>y: %{y:.5g}<br>f(x, y): %{customdata:.5g}<extra></extra>', | |
| showlegend: false, | |
| }, | |
| { | |
| x: trajX[currentIteration] !== undefined ? [trajX[currentIteration]] : [], | |
| y: trajY[currentIteration] !== undefined ? [trajY[currentIteration]] : [], | |
| customdata: trajZ[currentIteration] !== undefined ? [trajZ[currentIteration]] : [], | |
| type: 'scatter', | |
| mode: 'markers', | |
| marker: { color: 'red', size: 12 }, | |
| hovertemplate: 'x: %{x:.5g}<br>y: %{y:.5g}<br>f(x, y): %{customdata:.5g}<extra></extra>', | |
| showlegend: false, | |
| } | |
| ,...comparisonTraces | |
| ]} | |
| layout={{ ...layoutRef.current, annotations: [], showlegend: comparisons !== undefined, legend: { x: 0.02, y: 0.98, xanchor: "left", yanchor: "top", bgcolor: "rgba(255,255,255,0.75)" } }} | |
| style={{ width: '100%', height: '100%' }} | |
| config={{ | |
| responsive: true, | |
| displayModeBar: true, | |
| scrollZoom: true, | |
| }} | |
| /> | |
| </Card> | |
| ); | |
| } | |
| } | |