Add embryo generation page
This commit is contained in:
parent
73e83da5fe
commit
edb72bc526
BIN
embryo-backend/Data/CS7.5.h5ad
Normal file
BIN
embryo-backend/Data/CS7.5.h5ad
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -6,7 +6,7 @@ import os
|
||||
np.random.seed(42)
|
||||
|
||||
# 参数配置
|
||||
stages = [("CS7", 500), ("CS8", 1000), ("CS9", 1500)]
|
||||
stages = [("CS7", 500), ("CS7.5", 750), ("CS8", 1000), ("CS9", 1500)]
|
||||
genes = ["SOX2", "NANOG", "T", "POU5F1", "OTX2", "ZIC2", "FOXA2", "LEFTY1"]
|
||||
layers = {
|
||||
"Ectoderm": 1.0, # 外层
|
||||
|
||||
@ -7,6 +7,7 @@ import SpatialClustering from "./pages/SpatialClustering";
|
||||
import Resource from "./pages/Resource";
|
||||
import Download from "./pages/Download";
|
||||
import TemporalAnalysis from "./pages/TemporalAnalysis";
|
||||
import EmbryoGeneration from "./pages/EmbryoGeneration";
|
||||
import "./style.css";
|
||||
|
||||
function App() {
|
||||
@ -20,6 +21,7 @@ function App() {
|
||||
<Route path="/gene-view" element={<GeneView />} />
|
||||
<Route path="/spatial-clustering" element={<SpatialClustering />} />
|
||||
<Route path="/temporal-analysis" element={<TemporalAnalysis />} />
|
||||
<Route path="/embryo-generation" element={<EmbryoGeneration />} />
|
||||
<Route path="/resource" element={<Resource />} />
|
||||
<Route path="/download" element={<Download />} />
|
||||
</Routes>
|
||||
|
||||
@ -9,6 +9,7 @@ const Navigation: React.FC = () => {
|
||||
{ path: "/spatial-clustering", label: "Spatial clustering", name: "空间聚类" },
|
||||
{ path: "/gene-view", label: "Gene Expression", name: "基因表达" },
|
||||
{ path: "/temporal-analysis", label: "Temporal Analysis", name: "时间序列分析" },
|
||||
{ path: "/embryo-generation", label: "Embryo Generation", name: "胚胎生成" },
|
||||
{ path: "/resource", label: "Resource", name: "资源" },
|
||||
{ path: "/download", label: "Download", name: "下载" },
|
||||
];
|
||||
|
||||
445
embryo-frontend/src/pages/EmbryoGeneration.tsx
Normal file
445
embryo-frontend/src/pages/EmbryoGeneration.tsx
Normal file
@ -0,0 +1,445 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import axios from "axios";
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import "../style.css";
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
value: number | string;
|
||||
}
|
||||
|
||||
const EmbryoGeneration: React.FC = () => {
|
||||
const [cs7Data, setCs7Data] = useState<Point[]>([]);
|
||||
const [cs8Data, setCs8Data] = useState<Point[]>([]);
|
||||
const [cs75Data, setCs75Data] = useState<Point[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [animationProgress, setAnimationProgress] = useState(0);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const [animationSpeed, setAnimationSpeed] = useState(2);
|
||||
|
||||
// 加载数据
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const [cs7Res, cs8Res, cs75Res] = await Promise.all([
|
||||
axios.get("http://localhost:5000/api/cell", { params: { stage: "CS7" } }),
|
||||
axios.get("http://localhost:5000/api/cell", { params: { stage: "CS8" } }),
|
||||
axios.get("http://localhost:5000/api/cell", { params: { stage: "CS7.5" } })
|
||||
]);
|
||||
setCs7Data(cs7Res.data.cells);
|
||||
setCs8Data(cs8Res.data.cells);
|
||||
setCs75Data(cs75Res.data.cells);
|
||||
} catch (err: any) {
|
||||
if (err.response && err.response.data?.error) {
|
||||
setError(err.response.data.error);
|
||||
} else {
|
||||
setError("Failed to fetch embryo data.");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
// 动画逻辑
|
||||
useEffect(() => {
|
||||
if (!isAnimating || cs7Data.length === 0 || cs8Data.length === 0 || cs75Data.length === 0) {
|
||||
return;
|
||||
}
|
||||
const duration = 5000 / animationSpeed;
|
||||
const startTime = Date.now();
|
||||
const animate = () => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
setAnimationProgress(progress);
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
setIsAnimating(false);
|
||||
setAnimationProgress(1);
|
||||
}
|
||||
};
|
||||
animate();
|
||||
}, [isAnimating, cs7Data.length, cs8Data.length, cs75Data.length, animationSpeed]);
|
||||
|
||||
const startAnimation = () => {
|
||||
setIsAnimating(true);
|
||||
setAnimationProgress(0);
|
||||
};
|
||||
const resetAnimation = () => {
|
||||
setIsAnimating(false);
|
||||
setAnimationProgress(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="page-container">
|
||||
<div className="page-header">
|
||||
<h1>Embryo Generation</h1>
|
||||
<p className="page-description">
|
||||
观察胚胎发育过程中的细胞迁移和融合。CS7、CS8、CS7.5点云在同一3D视图中展示,动画模拟细胞从两侧汇聚到中间。
|
||||
</p>
|
||||
</div>
|
||||
<div className="page-content">
|
||||
<div className="control-panel" style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "2rem",
|
||||
marginBottom: "2rem",
|
||||
padding: "1rem",
|
||||
backgroundColor: "var(--bg-secondary)",
|
||||
borderRadius: "8px",
|
||||
flexWrap: "wrap"
|
||||
}}>
|
||||
<button
|
||||
onClick={startAnimation}
|
||||
disabled={isAnimating || loading}
|
||||
style={{
|
||||
padding: "0.75rem 1.5rem",
|
||||
backgroundColor: "var(--primary-color)",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
cursor: isAnimating || loading ? "not-allowed" : "pointer",
|
||||
opacity: isAnimating || loading ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
{isAnimating ? "动画中..." : "开始动画"}
|
||||
</button>
|
||||
<button
|
||||
onClick={resetAnimation}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: "0.75rem 1.5rem",
|
||||
backgroundColor: "var(--secondary-color)",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
cursor: loading ? "not-allowed" : "pointer",
|
||||
opacity: loading ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
<label style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||
动画速度:
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="3"
|
||||
step="0.1"
|
||||
value={animationSpeed}
|
||||
onChange={(e) => setAnimationSpeed(parseFloat(e.target.value))}
|
||||
style={{ width: "100px" }}
|
||||
/>
|
||||
<span>{animationSpeed}x</span>
|
||||
</label>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||
进度: {Math.round(animationProgress * 100)}%
|
||||
<div style={{
|
||||
width: "100px",
|
||||
height: "8px",
|
||||
backgroundColor: "var(--bg-tertiary)",
|
||||
borderRadius: "4px",
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<div style={{
|
||||
width: `${animationProgress * 100}%`,
|
||||
height: "100%",
|
||||
backgroundColor: "var(--primary-color)",
|
||||
transition: "width 0.1s ease"
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{error && <div className="error-message" style={{ marginBottom: "2rem" }}>{error}</div>}
|
||||
<UnifiedEmbryoAnimation
|
||||
cs7Data={cs7Data}
|
||||
cs8Data={cs8Data}
|
||||
cs75Data={cs75Data}
|
||||
animationProgress={animationProgress}
|
||||
/>
|
||||
<div className="section">
|
||||
<h2>使用说明</h2>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))", gap: "2rem", marginTop: "1.5rem" }}>
|
||||
<div>
|
||||
<h3>🎬 动画控制</h3>
|
||||
<p>点击"开始动画"按钮观看CS7.5阶段的细胞生成过程。可以调节动画速度,使用重置按钮重新开始。</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>🖱️ 交互操作</h3>
|
||||
<p>在3D视图中使用鼠标拖拽旋转视角,滚轮缩放,点击数据点查看详细信息。</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>🎨 颜色编码</h3>
|
||||
<p>相同颜色代表相同细胞类型,便于观察细胞在发育过程中的变化。</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>📊 发育过程</h3>
|
||||
<p>观察从CS7和CS8到CS7.5的胚胎发育过程,了解细胞的空间分布变化和类型分化。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface UnifiedEmbryoAnimationProps {
|
||||
cs7Data: Point[];
|
||||
cs8Data: Point[];
|
||||
cs75Data: Point[];
|
||||
animationProgress: number;
|
||||
}
|
||||
|
||||
const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data, cs8Data, cs75Data, animationProgress }) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
|
||||
const sceneRef = useRef<THREE.Scene | null>(null);
|
||||
const controlsRef = useRef<any>(null);
|
||||
const animationIdRef = useRef<number | null>(null);
|
||||
|
||||
// 颜色映射
|
||||
const getColorMap = (allData: Point[]) => {
|
||||
const cellTypes = Array.from(new Set(allData.map(p => p.value as string))).sort();
|
||||
const colors = [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
|
||||
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9',
|
||||
'#F8C471', '#82E0AA', '#F1948A', '#85CDCE', '#D7BDE2',
|
||||
'#A9DFBF', '#F9E79F', '#D5A6BD', '#AED6F1', '#E8DAEF'
|
||||
];
|
||||
const colorMap = new Map<string, THREE.Color>();
|
||||
cellTypes.forEach((type, idx) => {
|
||||
colorMap.set(type, new THREE.Color(colors[idx % colors.length]));
|
||||
});
|
||||
return colorMap;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
// 清理
|
||||
if (animationIdRef.current !== null) {
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
animationIdRef.current = null;
|
||||
}
|
||||
if (rendererRef.current) {
|
||||
rendererRef.current.dispose();
|
||||
rendererRef.current = null;
|
||||
}
|
||||
while (containerRef.current.firstChild) {
|
||||
containerRef.current.removeChild(containerRef.current.firstChild);
|
||||
}
|
||||
// 初始化
|
||||
const width = containerRef.current.clientWidth;
|
||||
const height = containerRef.current.clientHeight;
|
||||
const scene = new THREE.Scene();
|
||||
sceneRef.current = scene;
|
||||
const camera = new THREE.PerspectiveCamera(30, width / height, 0.1, 1000);
|
||||
camera.position.set(0, 0, 30);
|
||||
camera.lookAt(0, 0, 0);
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setSize(width, height);
|
||||
rendererRef.current = renderer;
|
||||
containerRef.current.appendChild(renderer.domElement);
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controlsRef.current = controls;
|
||||
// 不添加坐标轴
|
||||
|
||||
// 颜色映射
|
||||
const allData = [...cs7Data, ...cs8Data, ...cs75Data];
|
||||
const colorMap = getColorMap(allData);
|
||||
|
||||
// 计算所有点的包围盒,自动居中和缩放
|
||||
let allPositions: number[] = [];
|
||||
cs7Data.forEach(p => { allPositions.push(p.x - 40, p.y, p.z); });
|
||||
cs8Data.forEach(p => { allPositions.push(p.x + 40, p.y, p.z); });
|
||||
cs75Data.forEach((p, i) => {
|
||||
const fromX = i % 2 === 0 ? p.x - 40 : p.x + 40;
|
||||
allPositions.push(fromX, p.y, p.z);
|
||||
allPositions.push(p.x, p.y, p.z);
|
||||
});
|
||||
if (allPositions.length > 0) {
|
||||
const posArr = new Float32Array(allPositions);
|
||||
const box = new THREE.Box3().setFromArray(posArr);
|
||||
const center = new THREE.Vector3();
|
||||
box.getCenter(center);
|
||||
const size = new THREE.Vector3();
|
||||
box.getSize(size);
|
||||
// 让最大边长适配canvas
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
const fitScale = 32 / maxDim; // 32为经验缩放系数
|
||||
scene.position.set(-center.x, -center.y, -center.z);
|
||||
scene.scale.set(fitScale, fitScale, fitScale);
|
||||
}
|
||||
|
||||
// CS7点云(左,静态)
|
||||
if (cs7Data.length > 0) {
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
const positions: number[] = [];
|
||||
const colors: number[] = [];
|
||||
cs7Data.forEach((p) => {
|
||||
positions.push(p.x - 40, p.y, p.z);
|
||||
const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC');
|
||||
colors.push(c.r, c.g, c.b);
|
||||
});
|
||||
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
||||
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
|
||||
const material = new THREE.PointsMaterial({
|
||||
vertexColors: true,
|
||||
size: 0.7,
|
||||
sizeAttenuation: true,
|
||||
transparent: true,
|
||||
alphaTest: 0.5,
|
||||
depthWrite: false,
|
||||
blending: THREE.NormalBlending,
|
||||
map: (() => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.clearRect(0, 0, 64, 64);
|
||||
// 绘制带有alpha渐变的圆形
|
||||
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 30);
|
||||
gradient.addColorStop(0, 'rgba(255,255,255,1)');
|
||||
gradient.addColorStop(0.98, 'rgba(255,255,255,1)');
|
||||
gradient.addColorStop(1, 'rgba(255,255,255,0)');
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 30, 0, 2 * Math.PI);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
return new THREE.CanvasTexture(canvas);
|
||||
})()
|
||||
});
|
||||
const points = new THREE.Points(geometry, material);
|
||||
scene.add(points);
|
||||
}
|
||||
// CS8点云(右,静态)
|
||||
if (cs8Data.length > 0) {
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
const positions: number[] = [];
|
||||
const colors: number[] = [];
|
||||
cs8Data.forEach((p) => {
|
||||
positions.push(p.x + 40, p.y, p.z);
|
||||
const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC');
|
||||
colors.push(c.r, c.g, c.b);
|
||||
});
|
||||
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
||||
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
|
||||
const material = new THREE.PointsMaterial({
|
||||
vertexColors: true,
|
||||
size: 0.7,
|
||||
sizeAttenuation: true,
|
||||
transparent: true,
|
||||
alphaTest: 0.5,
|
||||
depthWrite: false,
|
||||
blending: THREE.NormalBlending,
|
||||
map: (() => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.clearRect(0, 0, 64, 64);
|
||||
// 绘制带有alpha渐变的圆形
|
||||
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 30);
|
||||
gradient.addColorStop(0, 'rgba(255,255,255,1)');
|
||||
gradient.addColorStop(0.98, 'rgba(255,255,255,1)');
|
||||
gradient.addColorStop(1, 'rgba(255,255,255,0)');
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 30, 0, 2 * Math.PI);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
return new THREE.CanvasTexture(canvas);
|
||||
})()
|
||||
});
|
||||
const points = new THREE.Points(geometry, material);
|
||||
scene.add(points);
|
||||
}
|
||||
// CS7.5点云(动画)
|
||||
if (cs75Data.length > 0) {
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
const positions: number[] = [];
|
||||
const colors: number[] = [];
|
||||
const n = cs75Data.length;
|
||||
const moveDuration = 0.4; // 每个点的动画持续时间(占总进度的比例)
|
||||
const delayPerPoint = 0.6 / n; // 剩余0.6进度用于错峰
|
||||
for (let i = 0; i < n; i++) {
|
||||
const to = cs75Data[i];
|
||||
const fromX = i % 2 === 0 ? to.x - 40 : to.x + 40;
|
||||
const fromY = to.y;
|
||||
const fromZ = to.z;
|
||||
const delay = i * delayPerPoint;
|
||||
let pointProgress = (animationProgress - delay) / moveDuration;
|
||||
pointProgress = Math.max(0, Math.min(1, pointProgress));
|
||||
const x = fromX + (to.x - fromX) * pointProgress;
|
||||
const y = fromY + (to.y - fromY) * pointProgress;
|
||||
const z = fromZ + (to.z - fromZ) * pointProgress;
|
||||
positions.push(x, y, z);
|
||||
const c = colorMap.get(to.value as string) || new THREE.Color('#CCCCCC');
|
||||
colors.push(c.r, c.g, c.b);
|
||||
}
|
||||
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
||||
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
|
||||
const material = new THREE.PointsMaterial({
|
||||
vertexColors: true,
|
||||
size: 0.7,
|
||||
sizeAttenuation: true,
|
||||
transparent: true,
|
||||
alphaTest: 0.5,
|
||||
depthWrite: false,
|
||||
blending: THREE.NormalBlending,
|
||||
map: (() => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.clearRect(0, 0, 64, 64);
|
||||
// 绘制带有alpha渐变的圆形
|
||||
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 30);
|
||||
gradient.addColorStop(0, 'rgba(255,255,255,1)');
|
||||
gradient.addColorStop(0.98, 'rgba(255,255,255,1)');
|
||||
gradient.addColorStop(1, 'rgba(255,255,255,0)');
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 30, 0, 2 * Math.PI);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
return new THREE.CanvasTexture(canvas);
|
||||
})()
|
||||
});
|
||||
const points = new THREE.Points(geometry, material);
|
||||
scene.add(points);
|
||||
}
|
||||
// 渲染循环
|
||||
const animate = () => {
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
animate();
|
||||
// 清理
|
||||
return () => {
|
||||
if (animationIdRef.current !== null) {
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
}
|
||||
if (controlsRef.current) {
|
||||
controlsRef.current.dispose();
|
||||
}
|
||||
if (rendererRef.current) {
|
||||
rendererRef.current.dispose();
|
||||
}
|
||||
};
|
||||
}, [cs7Data, cs8Data, cs75Data, animationProgress]);
|
||||
|
||||
return <div ref={containerRef} className="point-cloud-container" style={{ width: "100%", height: "700px", minHeight: 600 }} />;
|
||||
};
|
||||
|
||||
export default EmbryoGeneration;
|
||||
Loading…
Reference in New Issue
Block a user