diff --git a/embryo-frontend/package-lock.json b/embryo-frontend/package-lock.json index 7aa3520..fa47555 100644 --- a/embryo-frontend/package-lock.json +++ b/embryo-frontend/package-lock.json @@ -17,10 +17,20 @@ "three": "^0.178.0" }, "devDependencies": { + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@types/three": "^0.178.1", "typescript": "~5.8.3", "vite": "^7.0.4" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.6", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", @@ -749,6 +759,13 @@ "win32" ] }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -756,6 +773,63 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.178.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.178.1.tgz", + "integrity": "sha512-WSabew1mgWgRx2RfLfKY+9h4wyg6U94JfLbZEGU245j/WY2kXqU0MUfghS+3AYMV5ET1VlILAgpy77cB6a3Itw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": "*", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.22", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.22.tgz", + "integrity": "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webgpu/types": { + "version": "0.1.64", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.64.tgz", + "integrity": "sha512-84kRIAGV46LJTlJZWxShiOrNL30A+9KokD7RB3dRCIqODFjodS5tCD5yyiZ8kIReGVZSDfA3XkkwyyOIF6K62A==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -819,6 +893,13 @@ "node": ">=18" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -944,6 +1025,13 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -1101,6 +1189,13 @@ "node": ">= 0.4" } }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "dev": true, + "license": "MIT" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", diff --git a/embryo-frontend/package.json b/embryo-frontend/package.json index 17934c2..54f307e 100644 --- a/embryo-frontend/package.json +++ b/embryo-frontend/package.json @@ -9,6 +9,9 @@ "preview": "vite preview" }, "devDependencies": { + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@types/three": "^0.178.1", "typescript": "~5.8.3", "vite": "^7.0.4" }, diff --git a/embryo-frontend/src/components/PointCloud.tsx b/embryo-frontend/src/components/PointCloud.tsx index 9e47d96..2b6552b 100644 --- a/embryo-frontend/src/components/PointCloud.tsx +++ b/embryo-frontend/src/components/PointCloud.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef } from "react"; import * as THREE from "three"; -import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; interface Point { x: number; @@ -15,7 +15,7 @@ interface PointCloudProps { const PointCloud: React.FC = ({ data }) => { const containerRef = useRef(null); - const rendererRef = useRef(); + const rendererRef = useRef(undefined); useEffect(() => { if (!containerRef.current || data.length === 0) return; @@ -106,12 +106,11 @@ const PointCloud: React.FC = ({ data }) => { // 图例(颜色条) const legend = document.createElement("div"); + legend.className = "point-cloud-legend"; legend.innerHTML = ` -
-
-
↑ High
-
↓ Low
-
`; +
+
↑ High
+
↓ Low
`; containerRef.current.appendChild(legend); // 清理 @@ -123,7 +122,7 @@ const PointCloud: React.FC = ({ data }) => { }; }, [data]); - return
; + return
; }; export default PointCloud; diff --git a/embryo-frontend/src/pages/GeneView.tsx b/embryo-frontend/src/pages/GeneView.tsx index 31df47b..570e151 100644 --- a/embryo-frontend/src/pages/GeneView.tsx +++ b/embryo-frontend/src/pages/GeneView.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import axios from "axios"; import PointCloud from "../components/PointCloud"; +import "../style.css"; const GeneView: React.FC = () => { const [gene, setGene] = useState(""); @@ -53,11 +54,11 @@ const GeneView: React.FC = () => { }; return ( -
+

Gene Expression Viewer

{/* 阶段选择 */} -
+
{/* 错误提示 */} - {error &&

{error}

} + {error &&

{error}

} {/* 可视化区域 */} -
+
{data.length > 0 ? ( ) : ( -

+

No data to display.

)} diff --git a/embryo-frontend/src/style.css b/embryo-frontend/src/style.css index 3bcdbd0..c096a9b 100644 --- a/embryo-frontend/src/style.css +++ b/embryo-frontend/src/style.css @@ -82,6 +82,66 @@ button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } +/* GeneView component styles */ +.gene-view-container { + padding: 1rem; +} + +.stage-selector { + margin-bottom: 10px; +} + +.gene-selector { + margin-bottom: 1rem; +} + +.gene-label { + margin-right: 0.5rem; +} + +.gene-select { + margin-right: 1rem; +} + +.error-message { + color: red; +} + +.visualization-container { + width: 100%; + height: 600px; + border: 1px solid #ccc; + position: relative; +} + +.no-data-message { + text-align: center; + padding-top: 2rem; +} + +/* PointCloud component styles */ +.point-cloud-container { + width: 100%; + height: 100%; + position: relative; +} + +.point-cloud-legend { + position: absolute; + top: 10px; + right: 10px; + background: white; + padding: 5px; + border: 1px solid gray; + font-size: 12px; +} + +.legend-gradient { + background: linear-gradient(to top, hsla(180, 100%, 50%, 1.00), hsla(72, 100%, 50%, 1.00), hsla(0, 100%, 50%, 0.99)); + width: 20px; + height: 100px; +} + @media (prefers-color-scheme: light) { :root { color: #213547;