diff --git a/embryo-backend/Data/CS11.h5ad b/embryo-backend/Data/CS11.h5ad new file mode 100644 index 0000000..d3fc817 Binary files /dev/null and b/embryo-backend/Data/CS11.h5ad differ diff --git a/embryo-frontend/src/components/PointCloud.tsx b/embryo-frontend/src/components/PointCloud.tsx index 2e9650b..14a7aa0 100644 --- a/embryo-frontend/src/components/PointCloud.tsx +++ b/embryo-frontend/src/components/PointCloud.tsx @@ -36,12 +36,12 @@ const PointCloud: React.FC = ({ data, isCategorical = false, au '#F8C471', '#82E0AA', '#F1948A', '#85CDCE', '#D7BDE2', '#A9DFBF', '#F9E79F', '#D5A6BD', '#AED6F1', '#E8DAEF' ]; - + cellTypes.forEach((type, index) => { const colorHex = colors[index % colors.length]; colorMap.set(type, new THREE.Color(colorHex)); }); - + return colorMap; }; @@ -56,17 +56,17 @@ const PointCloud: React.FC = ({ data, isCategorical = false, au cancelAnimationFrame(animationIdRef.current); animationIdRef.current = null; } - + if (rendererRef.current) { rendererRef.current.dispose(); rendererRef.current = null; } - + if (geometryRef.current) { geometryRef.current.dispose(); geometryRef.current = null; } - + if (materialRef.current) { materialRef.current.dispose(); materialRef.current = null; @@ -83,12 +83,13 @@ const PointCloud: React.FC = ({ data, isCategorical = false, au // 初始化场景、相机、渲染器 const scene = new THREE.Scene(); sceneRef.current = scene; // 存储scene引用 - const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000); - camera.position.set(-15, 15, -12); // 从左下后方观察,使Z轴指向右上方 - camera.lookAt(0, 0, 0); // 确保相机朝向原点 + const camera = new THREE.PerspectiveCamera(600, width / height, 0.1, 1000); + camera.position.set(0, 0, -500); // 从左下后方观察,使Z轴指向右上方 + camera.lookAt(0, -100, 0); // 确保相机朝向原点 const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(width, height); + renderer.setClearColor(0xFFFFFF, 1); // 设置背景为透明 rendererRef.current = renderer; containerRef.current.appendChild(renderer.domElement); @@ -125,7 +126,7 @@ const PointCloud: React.FC = ({ data, isCategorical = false, au data.forEach((point) => { positions.push(point.x, point.y, point.z); - + if (isCategorical && colorMap) { // 分类数据:按细胞类型着色 const cellColor = colorMap.get(point.value as string) || new THREE.Color('#CCCCCC'); @@ -146,11 +147,32 @@ const PointCloud: React.FC = ({ data, isCategorical = false, au const material = new THREE.PointsMaterial({ vertexColors: true, - size: 0.5, + size: 5, 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); + })() }); materialRef.current = material; - const points = new THREE.Points(geometry, material); scene.add(points); @@ -169,13 +191,13 @@ const PointCloud: React.FC = ({ data, isCategorical = false, au if (intersects.length > 0) { const i = intersects[0].index ?? 0; const p = data[i]; - console.log(`Clicked: x=${p.x}, y=${p.y}, z=${p.z}, value=${p.value}`); - - if (isCategorical) { - alert(`Cell @ (${p.x.toFixed(1)}, ${p.y.toFixed(1)}, ${p.z.toFixed(1)})\nCell Type: ${p.value}`); - } else { - alert(`Cell @ (${p.x.toFixed(1)}, ${p.y.toFixed(1)}, ${p.z.toFixed(1)})\nExpression: ${(p.value as number).toFixed(3)}`); - } + // console.log(`Clicked: x=${p.x}, y=${p.y}, z=${p.z}, value=${p.value}`); + + // if (isCategorical) { + // alert(`Cell @ (${p.x.toFixed(1)}, ${p.y.toFixed(1)}, ${p.z.toFixed(1)})\nCell Type: ${p.value}`); + // } else { + // alert(`Cell @ (${p.x.toFixed(1)}, ${p.y.toFixed(1)}, ${p.z.toFixed(1)})\nExpression: ${(p.value as number).toFixed(3)}`); + // } } }; @@ -184,46 +206,30 @@ const PointCloud: React.FC = ({ data, isCategorical = false, au // 渲染循环 const animate = () => { animationIdRef.current = requestAnimationFrame(animate); - + // 自动旋转逻辑 - 使用 ref 来避免重新创建场景 if (isSpinningRef.current && autoRotate) { - scene.rotation.z += 0.002; // 围绕Z轴更慢速旋转 + scene.rotation.y += 0.002; // 围绕Z轴更慢速旋转 } - + controls.update(); renderer.render(scene, camera); }; animate(); - // 图例 - const legend = document.createElement("div"); - legend.className = "point-cloud-legend"; - - if (isCategorical && colorMap) { - // 分类数据图例 - let legendHTML = '
细胞类型
'; - cellTypes.forEach(type => { - const cellColor = colorMap!.get(type); - if (cellColor) { - const hexColor = `#${cellColor.getHexString()}`; - legendHTML += ` -
-
- ${type} -
- `; - } - }); - legend.innerHTML = legendHTML; - } else { + // 只在非分类数据时在3D图形内显示图例 + if (!isCategorical) { + const legend = document.createElement("div"); + legend.className = "point-cloud-legend"; + // 数值数据图例 legend.innerHTML = `
↑ High
↓ Low
`; + + containerRef.current.appendChild(legend); } - - containerRef.current.appendChild(legend); // 清理 return () => { @@ -232,29 +238,29 @@ const PointCloud: React.FC = ({ data, isCategorical = false, au cancelAnimationFrame(animationIdRef.current); animationIdRef.current = null; } - + // 移除事件监听器 if (rendererRef.current && rendererRef.current.domElement) { rendererRef.current.domElement.removeEventListener("click", onClick); } - + // 清理控制器 if (controlsRef.current) { controlsRef.current.dispose(); controlsRef.current = null; } - + // 释放Three.js资源 if (rendererRef.current) { rendererRef.current.dispose(); rendererRef.current = null; } - + if (geometryRef.current) { geometryRef.current.dispose(); geometryRef.current = null; } - + if (materialRef.current) { materialRef.current.dispose(); materialRef.current = null; @@ -294,56 +300,150 @@ const PointCloud: React.FC = ({ data, isCategorical = false, au }; }, []); - return ( -
-
- {autoRotate === true && ( -
- setIsSpinning(e.target.checked)} - style={{ - cursor: 'pointer', - width: '16px', - height: '16px', - accentColor: '#4ECDC4' - }} - /> - + // 生成图例内容 + const renderLegend = () => { + if (isCategorical) { + // 分类数据图例 + const cellTypes = Array.from(new Set(data.map(p => p.value as string))).sort(); + const colorMap = generateCellTypeColors(cellTypes); + + return ( +
+
细胞类型
+
+ {cellTypes.map(type => { + const cellColor = colorMap.get(type); + if (cellColor) { + const hexColor = `#${cellColor.getHexString()}`; + return ( +
+
+ {type} +
+ ); + } + return null; + })} +
- )} -
- ); + ); + } else { + // 数值数据图例 + return ( +
+
表达强度
+
+
+
+
+
+
+
+
+ ); + } + }; + + // 根据isCategorical返回不同的布局 + if (isCategorical) { + return ( +
+ {/* 左侧:3D图形 */} +
+
+
+ + {/* 右侧:图例 */} +
+ {renderLegend()} +
+
+ ); + } else { + // 原始单列布局 + return ( +
+
+ {autoRotate === true && ( +
+ setIsSpinning(e.target.checked)} + style={{ + cursor: 'pointer', + width: '16px', + height: '16px', + accentColor: '#4ECDC4' + }} + /> + +
+ )} +
+ ); + } }; export default PointCloud; diff --git a/embryo-frontend/src/pages/EmbryoGeneration.tsx b/embryo-frontend/src/pages/EmbryoGeneration.tsx index 79fd777..8ccce3a 100644 --- a/embryo-frontend/src/pages/EmbryoGeneration.tsx +++ b/embryo-frontend/src/pages/EmbryoGeneration.tsx @@ -97,8 +97,8 @@ const EmbryoGeneration: React.FC = () => { borderRadius: "8px", flexWrap: "wrap" }}> - -