Testing functionality
This commit is contained in:
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
@apply bg-bg text-slate-200;
|
@apply bg-neutral-900 text-slate-200;
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
|||||||
+155
-36
@@ -1,71 +1,190 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import * as papa from "papaparse";
|
||||||
import type { Route } from "./+types/home";
|
import type { Route } from "./+types/home";
|
||||||
|
|
||||||
export function meta({ }: Route.MetaArgs) {
|
export function meta({ }: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
{ title: "New React Router App" },
|
{ title: "3dplot-gen" },
|
||||||
{ name: "description", content: "Welcome to React Router!" },
|
{ name: "description", content: "CSV → 3D Plot generator" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Row = Record<string, number>;
|
||||||
|
|
||||||
export default function Home() {
|
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 (
|
return (
|
||||||
<div className="h-screen grid grid-cols-[280px_1fr]">
|
<div className="h-screen grid grid-cols-[280px_1fr]">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside className="bg-panel p-4 space-y-4">
|
<aside className="bg-panel p-4 space-y-4">
|
||||||
<div className="text-lg font-semibold">
|
<div className="text-lg font-semibold">
|
||||||
3dplot-gen
|
3dplot-gen
|
||||||
<div className="text-xs text-muted">
|
<div className="text-xs text-muted">CSV → 3D Plot</div>
|
||||||
CSV → 3D Plot
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<Control label="CSV file">
|
||||||
<Control label="CSV file">
|
<input
|
||||||
<input
|
type="file"
|
||||||
type="file"
|
accept=".csv"
|
||||||
accept=".csv"
|
onChange={(e) =>
|
||||||
className="w-full text-sm file:bg-bg file:text-slate-300 file:border-0
|
e.target.files && onFile(e.target.files[0])
|
||||||
file:rounded file:px-2 file:py-1"
|
}
|
||||||
/>
|
className="w-full text-sm file:bg-neutral-900 file:text-slate-300 file:border-0
|
||||||
</Control>
|
file:rounded file:px-2 file:py-1"
|
||||||
|
/>
|
||||||
|
</Control>
|
||||||
|
|
||||||
<Control label="X axis">
|
<Control label="X axis">
|
||||||
<select className="w-full bg-bg border border-slate-700 rounded px-2 py-1">
|
<select
|
||||||
<option>-</option>
|
className="w-full bg-neutral-900 border border-slate-700 rounded px-2 py-1"
|
||||||
</select>
|
value={x}
|
||||||
</Control>
|
onChange={(e) => setX(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">—</option>
|
||||||
|
{columns.map(c => (
|
||||||
|
<option key={c}>{c}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Control>
|
||||||
|
|
||||||
<Control label="Y axis">
|
<Control label="Y axis">
|
||||||
<select className="w-full bg-bg border border-slate-700 rounded px-2 py-1">
|
<select
|
||||||
<option>-</option>
|
className="w-full bg-neutral-900 border border-slate-700 rounded px-2 py-1"
|
||||||
</select>
|
value={y}
|
||||||
</Control>
|
onChange={(e) => setY(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">—</option>
|
||||||
|
{columns.map(c => (
|
||||||
|
<option key={c}>{c}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Control>
|
||||||
|
|
||||||
<Control label="Z axis">
|
<Control label="Z axis">
|
||||||
<select className="w-full bg-bg border border-slate-700 rounded px-2 py-1">
|
<select
|
||||||
<option>-</option>
|
className="w-full bg-neutral-900 border border-slate-700 rounded px-2 py-1"
|
||||||
</select>
|
value={z}
|
||||||
</Control>
|
onChange={(e) => setZ(e.target.value)}
|
||||||
</div>
|
>
|
||||||
|
<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>
|
</aside>
|
||||||
|
|
||||||
{/* Plot */}
|
{/* Plot */}
|
||||||
<main className="bg-plot">
|
<main className="bg-plot">
|
||||||
<div
|
<div ref={plotRef} className="w-full h-full" />
|
||||||
id="plot"
|
|
||||||
className="w-full h-full"
|
|
||||||
/>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Control({ label, children }: { label: string, children: React.ReactNode }) {
|
function Control({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-xs text-muted">{label}</label>
|
<label className="text-xs text-muted">{label}</label>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+41
-3
@@ -8,7 +8,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-router/node": "7.12.0",
|
"@react-router/node": "7.12.0",
|
||||||
"@react-router/serve": "7.12.0",
|
"@react-router/serve": "7.12.0",
|
||||||
|
"@types/papaparse": "^5.5.2",
|
||||||
|
"@types/plotly.js-dist-min": "^2.3.4",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
|
"papaparse": "^5.5.3",
|
||||||
|
"plotly.js-dist-min": "^3.3.1",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-router": "7.12.0"
|
"react-router": "7.12.0"
|
||||||
@@ -16,7 +20,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-router/dev": "7.12.0",
|
"@react-router/dev": "7.12.0",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22.19.7",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
@@ -1763,12 +1767,35 @@
|
|||||||
"version": "22.19.7",
|
"version": "22.19.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
|
||||||
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
|
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/papaparse": {
|
||||||
|
"version": "5.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz",
|
||||||
|
"integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/plotly.js": {
|
||||||
|
"version": "3.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/plotly.js/-/plotly.js-3.0.9.tgz",
|
||||||
|
"integrity": "sha512-nHKn7czWIPN7rT5wWI5qhML2O1Prm/Gx0NNe1MVr5GUL1zuzxbvIDmG7hlKMMtDsEtNfNQLMlMwk0CRtd3uqhg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/plotly.js-dist-min": {
|
||||||
|
"version": "2.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/plotly.js-dist-min/-/plotly.js-dist-min-2.3.4.tgz",
|
||||||
|
"integrity": "sha512-ISwLFV6Zs/v3DkaRFLyk2rvYAfVdnYP2VVVy7h+fBDWw52sn7sMUzytkWiN4M75uxr1uz1uiBioePTDpAfoFIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/plotly.js": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.10",
|
"version": "19.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
|
||||||
@@ -3225,6 +3252,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/papaparse": {
|
||||||
|
"version": "5.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
|
||||||
|
"integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -3286,6 +3319,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/plotly.js-dist-min": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-ZxKM9DlEoEF3wBzGRPGHt6gWTJrm5N81J9AgX9UBX/Qjc9L4lRxtPBPq+RmBJWoA71j1X5Z1ouuguLkdoo88tg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
@@ -3828,7 +3867,6 @@
|
|||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
|
|||||||
+6
-2
@@ -11,7 +11,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-router/node": "7.12.0",
|
"@react-router/node": "7.12.0",
|
||||||
"@react-router/serve": "7.12.0",
|
"@react-router/serve": "7.12.0",
|
||||||
|
"@types/papaparse": "^5.5.2",
|
||||||
|
"@types/plotly.js-dist-min": "^2.3.4",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
|
"papaparse": "^5.5.3",
|
||||||
|
"plotly.js-dist-min": "^3.3.1",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-router": "7.12.0"
|
"react-router": "7.12.0"
|
||||||
@@ -19,7 +23,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-router/dev": "7.12.0",
|
"@react-router/dev": "7.12.0",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22.19.7",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
@@ -27,4 +31,4 @@
|
|||||||
"vite": "^7.1.7",
|
"vite": "^7.1.7",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user