diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 2ce3571..5808bdb 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -12,6 +12,25 @@ export function meta({ }: Route.MetaArgs) { type Row = Record; +type PlotType = "scatter" | "surface"; + +const COLORSCALES = [ + "Viridis", + "Plasma", + "Inferno", + "Magma", + "Cividis", + "Hot", + "Electric", + "Bluered", + "Portland", + "Rainbow", + "RdBu", + "Earth", + "Teal", + "Turbo", +] as const; + export default function Home() { const plotRef = useRef(null); @@ -20,8 +39,33 @@ export default function Home() { const [x, setX] = useState(""); const [y, setY] = useState(""); const [z, setZ] = useState(""); + const [plotType, setPlotType] = useState("scatter"); + const [colorscale, setColorscale] = useState(COLORSCALES[0]); - // Render plot when axes change + // Build surface grid from scattered (x,y,z) points. Returns null if data isn't a grid. + function buildSurfaceGrid(): { + xUnique: number[]; + yUnique: number[]; + zMatrix: number[][]; + } | null { + const xVals = rows.map((r) => Number(r[x])); + const yVals = rows.map((r) => Number(r[y])); + const zVals = rows.map((r) => Number(r[z])); + const xUnique = [...new Set(xVals)].sort((a, b) => a - b); + const yUnique = [...new Set(yVals)].sort((a, b) => a - b); + const keyToZ = new Map(); + for (let i = 0; i < rows.length; i++) { + const k = `${xVals[i]},${yVals[i]}`; + if (!keyToZ.has(k)) keyToZ.set(k, zVals[i]); + } + if (xUnique.length * yUnique.length !== keyToZ.size) return null; + const zMatrix: number[][] = yUnique.map((yi) => + xUnique.map((xj) => keyToZ.get(`${xj},${yi}`) ?? NaN) + ); + return { xUnique, yUnique, zMatrix }; + } + + // Render plot when axes or options change useEffect(() => { if (!plotRef.current || !x || !y || !z) return; @@ -35,18 +79,30 @@ export default function Home() { const yVals = rows.map((r) => Number(r[y])); const zVals = rows.map((r) => Number(r[z])); - const trace = { - type: "scatter3d" as const, - mode: "markers" as const, - x: xVals, - y: yVals, - z: zVals, - marker: { - size: 4, - color: zVals, - colorscale: "Viridis" as const, - }, - }; + const grid = buildSurfaceGrid(); + const useSurface = plotType === "surface" && grid !== null; + + const trace = useSurface && grid + ? { + type: "surface" as const, + x: grid.xUnique, + y: grid.yUnique, + z: grid.zMatrix, + colorscale: colorscale as "Viridis", + colorbar: { title: { text: z } }, + } + : { + type: "scatter3d" as const, + mode: "markers" as const, + x: xVals, + y: yVals, + z: zVals, + marker: { + size: 4, + color: zVals, + colorscale: colorscale as "Viridis", + }, + }; Plotly.newPlot( plotRef.current!, @@ -69,7 +125,7 @@ export default function Home() { return () => { cancelled = true; }; - }, [rows, x, y, z]); + }, [rows, x, y, z, plotType, colorscale]); function onFile(file: File) { @@ -188,6 +244,39 @@ export default function Home() { + + + + + + + + + {plotType === "surface" && + rows.length > 0 && + buildSurfaceGrid() === null && ( +

+ Surface needs a full X×Y grid of points. Showing dots instead. +

+ )} +