Testing functionality

This commit is contained in:
2026-01-29 20:22:57 +01:00
parent 80004489e7
commit 0c76ae7928
4 changed files with 203 additions and 42 deletions
+155 -36
View File
@@ -1,71 +1,190 @@
import type React from "react";
import { useEffect, useRef, useState } from "react";
import * as papa from "papaparse";
import type { Route } from "./+types/home";
export function meta({ }: Route.MetaArgs) {
return [
{ title: "New React Router App" },
{ name: "description", content: "Welcome to React Router!" },
{ title: "3dplot-gen" },
{ name: "description", content: "CSV → 3D Plot generator" },
];
}
type Row = Record<string, number>;
export default function Home() {
const plotRef = useRef<HTMLDivElement>(null);
const [rows, setRows] = useState<Row[]>([]);
const [columns, setColumns] = useState<string[]>([]);
const [x, setX] = useState("");
const [y, setY] = useState("");
const [z, setZ] = useState("");
// Render plot when axes change
useEffect(() => {
if (!plotRef.current || !x || !y || !z) return;
let cancelled = false;
(async () => {
const Plotly = await import("plotly.js-dist-min");
if (cancelled) return;
const trace = {
type: "scatter3d",
mode: "markers",
x: rows.map(r => r[x]),
y: rows.map(r => r[y]),
z: rows.map(r => r[z]),
marker: {
size: 4,
color: rows.map(r => r[z]),
colorscale: "Viridis",
},
};
Plotly.newPlot(
plotRef.current!,
[trace as any],
{
paper_bgcolor: "#0b0d12",
plot_bgcolor: "#0b0d12",
font: { color: "#cfd3ff" },
scene: {
xaxis: { title: { text: x } },
yaxis: { title: { text: y } },
zaxis: { title: { text: z } },
},
margin: { l: 0, r: 0, t: 0, b: 0 },
},
{ responsive: true }
);
})();
return () => {
cancelled = true;
};
}, [rows, x, y, z]);
function onFile(file: File) {
papa.parse(file, {
header: true,
dynamicTyping: true,
skipEmptyLines: true,
complete: (res) => {
const data = res.data as Row[];
setRows(data);
setColumns(Object.keys(data[0]));
},
});
}
async function exportPNG() {
const Plotly = await import("plotly.js-dist-min");
if (!plotRef.current) return;
Plotly.toImage(plotRef.current, {
format: "png",
width: 1600,
height: 900,
}).then((url) => {
const a = document.createElement("a");
a.href = url;
a.download = "3dplot-gen.png";
a.click();
});
}
return (
<div className="h-screen grid grid-cols-[280px_1fr]">
{/* Sidebar */}
<aside className="bg-panel p-4 space-y-4">
<div className="text-lg font-semibold">
3dplot-gen
<div className="text-xs text-muted">
CSV → 3D Plot
</div>
<div className="text-xs text-muted">CSV → 3D Plot</div>
</div>
<div className="space-y-3">
<Control label="CSV file">
<input
type="file"
accept=".csv"
className="w-full text-sm file:bg-bg file:text-slate-300 file:border-0
file:rounded file:px-2 file:py-1"
/>
</Control>
<Control label="CSV file">
<input
type="file"
accept=".csv"
onChange={(e) =>
e.target.files && onFile(e.target.files[0])
}
className="w-full text-sm file:bg-neutral-900 file:text-slate-300 file:border-0
file:rounded file:px-2 file:py-1"
/>
</Control>
<Control label="X axis">
<select className="w-full bg-bg border border-slate-700 rounded px-2 py-1">
<option>-</option>
</select>
</Control>
<Control label="X axis">
<select
className="w-full bg-neutral-900 border border-slate-700 rounded px-2 py-1"
value={x}
onChange={(e) => setX(e.target.value)}
>
<option value="">—</option>
{columns.map(c => (
<option key={c}>{c}</option>
))}
</select>
</Control>
<Control label="Y axis">
<select className="w-full bg-bg border border-slate-700 rounded px-2 py-1">
<option>-</option>
</select>
</Control>
<Control label="Y axis">
<select
className="w-full bg-neutral-900 border border-slate-700 rounded px-2 py-1"
value={y}
onChange={(e) => setY(e.target.value)}
>
<option value="">—</option>
{columns.map(c => (
<option key={c}>{c}</option>
))}
</select>
</Control>
<Control label="Z axis">
<select className="w-full bg-bg border border-slate-700 rounded px-2 py-1">
<option>-</option>
</select>
</Control>
</div>
<Control label="Z axis">
<select
className="w-full bg-neutral-900 border border-slate-700 rounded px-2 py-1"
value={z}
onChange={(e) => setZ(e.target.value)}
>
<option value="">—</option>
{columns.map(c => (
<option key={c}>{c}</option>
))}
</select>
</Control>
<button
onClick={exportPNG}
className="w-full mt-2 bg-accent text-black text-sm rounded px-2 py-1 hover:opacity-90"
>
Export PNG
</button>
</aside>
{/* Plot */}
<main className="bg-plot">
<div
id="plot"
className="w-full h-full"
/>
<div ref={plotRef} className="w-full h-full" />
</main>
</div>
);
}
function Control({ label, children }: { label: string, children: React.ReactNode }) {
function Control({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) {
return (
<div className="space-y-1">
<label className="text-xs text-muted">{label}</label>
{children}
</div>
);
}
}