From 37692d5775350ee060336431827e0bf32fa671b1 Mon Sep 17 00:00:00 2001 From: wjsjwr Date: Sat, 26 Jul 2025 12:47:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=BB=86=E8=83=9E=E7=B1=BB?= =?UTF-8?q?=E5=9E=8BAPI=EF=BC=8C=E6=9B=B4=E6=96=B0=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E7=BB=86=E8=83=9E=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=92=8C=E5=9F=BA=E5=9B=A0=E8=A1=A8=E8=BE=BE=E7=9A=84=E5=8F=8C?= =?UTF-8?q?=E9=87=8D=E5=8F=AF=E8=A7=86=E5=8C=96=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E5=92=8C=E4=BA=A4=E4=BA=92=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- embryo-backend/app.py | 37 ++++++ embryo-frontend/src/components/PointCloud.tsx | 93 +++++++++++++-- embryo-frontend/src/pages/GeneView.tsx | 111 ++++++++++++++---- embryo-frontend/src/style.css | 35 ++++++ 4 files changed, 239 insertions(+), 37 deletions(-) diff --git a/embryo-backend/app.py b/embryo-backend/app.py index 6bd81e4..684d14b 100644 --- a/embryo-backend/app.py +++ b/embryo-backend/app.py @@ -57,5 +57,42 @@ def get_gene_expression(): ] return jsonify({"gene": gene, "expression": result}) +@app.route("/api/cell") +def get_cell_types(): + stage = request.args.get("stage") + adata = load_adata(stage) + + if not adata: + return jsonify({"error": f"Stage '{stage}' not found"}), 404 + + # 查找细胞类型列,按常见的列名优先级查找 + cell_type_columns = ['cell_type', 'celltype', 'cluster', 'annotation', 'cell_types', 'clusters'] + cell_type_col = None + + for col in cell_type_columns: + if col in adata.obs.columns: + cell_type_col = col + break + + if cell_type_col is None: + return jsonify({"error": "No cell type information found"}), 404 + + # 读取细胞类型 + cell_types = adata.obs[cell_type_col].values + + # 读取三维坐标 + if "spatial" in adata.obsm: + coords = adata.obsm["spatial"] + elif all(k in adata.obs for k in ("x", "y", "z")): + coords = adata.obs[["x", "y", "z"]].values + else: + return jsonify({"error": "No spatial coordinates found"}), 500 + + result = [ + {"x": float(x), "y": float(y), "z": float(z), "value": str(ct)} + for (x, y, z), ct in zip(coords, cell_types) + ] + return jsonify({"stage": stage, "cells": result}) + if __name__ == "__main__": app.run(debug=True) diff --git a/embryo-frontend/src/components/PointCloud.tsx b/embryo-frontend/src/components/PointCloud.tsx index 2b6552b..f842c1e 100644 --- a/embryo-frontend/src/components/PointCloud.tsx +++ b/embryo-frontend/src/components/PointCloud.tsx @@ -6,17 +6,36 @@ interface Point { x: number; y: number; z: number; - value: number; // 表达强度 + value: number | string; // 表达强度或细胞类型 } interface PointCloudProps { data: Point[]; + isCategorical?: boolean; // 是否为分类数据(细胞类型) } -const PointCloud: React.FC = ({ data }) => { +const PointCloud: React.FC = ({ data, isCategorical = false }) => { const containerRef = useRef(null); const rendererRef = useRef(undefined); + // 为细胞类型生成颜色映射 + const generateCellTypeColors = (cellTypes: string[]) => { + const colorMap = new Map(); + const colors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', + '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9', + '#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; + }; + useEffect(() => { if (!containerRef.current || data.length === 0) return; @@ -51,14 +70,36 @@ const PointCloud: React.FC = ({ data }) => { const colors: number[] = []; const sizes: number[] = []; + let colorMap: Map | null = null; + let cellTypes: string[] = []; + let maxValue = 0; + + if (isCategorical) { + // 分类数据处理 + cellTypes = Array.from(new Set(data.map(p => p.value as string))).sort(); + colorMap = generateCellTypeColors(cellTypes); + } else { + // 数值数据处理 + maxValue = Math.max(...data.map((p) => p.value as number)); + } + const color = new THREE.Color(); - const maxValue = Math.max(...data.map((p) => p.value)); data.forEach((point) => { positions.push(point.x, point.y, point.z); - color.setHSL(0.6 * (1 - point.value / maxValue), 1.0, 0.5); - colors.push(color.r, color.g, color.b); - sizes.push(0.3 + 1.5 * point.value / maxValue); // 点大小映射 + + if (isCategorical && colorMap) { + // 分类数据:按细胞类型着色 + const cellColor = colorMap.get(point.value as string) || new THREE.Color('#CCCCCC'); + colors.push(cellColor.r, cellColor.g, cellColor.b); + sizes.push(0.8); // 统一大小 + } else { + // 数值数据:按表达强度着色 + const value = point.value as number; + color.setHSL(0.6 * (1 - value / maxValue), 1.0, 0.5); + colors.push(color.r, color.g, color.b); + sizes.push(0.3 + 1.5 * value / maxValue); + } }); geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); @@ -90,7 +131,12 @@ const PointCloud: React.FC = ({ data }) => { 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}`); - alert(`Cell @ (${p.x.toFixed(1)}, ${p.y.toFixed(1)}, ${p.z.toFixed(1)})\nExpression: ${p.value.toFixed(3)}`); + + 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)}`); + } } }; @@ -104,13 +150,34 @@ const PointCloud: React.FC = ({ data }) => { }; animate(); - // 图例(颜色条) + // 图例 const legend = document.createElement("div"); legend.className = "point-cloud-legend"; - legend.innerHTML = ` -
-
↑ High
-
↓ Low
`; + + 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 { + // 数值数据图例 + legend.innerHTML = ` +
+
↑ High
+
↓ Low
`; + } + containerRef.current.appendChild(legend); // 清理 @@ -120,7 +187,7 @@ const PointCloud: React.FC = ({ data }) => { material.dispose(); renderer.domElement.removeEventListener("click", onClick); }; - }, [data]); + }, [data, isCategorical]); return
; }; diff --git a/embryo-frontend/src/pages/GeneView.tsx b/embryo-frontend/src/pages/GeneView.tsx index d03d947..2fc27fa 100644 --- a/embryo-frontend/src/pages/GeneView.tsx +++ b/embryo-frontend/src/pages/GeneView.tsx @@ -6,8 +6,11 @@ import "../style.css"; const GeneView: React.FC = () => { const [gene, setGene] = useState(""); const [data, setData] = useState([]); + const [cellData, setCellData] = useState([]); const [loading, setLoading] = useState(false); + const [cellLoading, setCellLoading] = useState(false); const [error, setError] = useState(""); + const [cellError, setCellError] = useState(""); const [availableGenes, setAvailableGenes] = useState([]); const [selectedStage, setSelectedStage] = useState("CS7"); @@ -22,6 +25,33 @@ const GeneView: React.FC = () => { .catch(() => setAvailableGenes([])); }, [selectedStage]); + // 获取细胞类型数据 + useEffect(() => { + if (!selectedStage) return; + + setCellLoading(true); + setCellError(""); + setCellData([]); + + axios + .get("http://localhost:5000/api/cell", { + params: { stage: selectedStage }, + }) + .then((res) => { + setCellData(res.data.cells); + }) + .catch((err) => { + if (err.response && err.response.data?.error) { + setCellError(err.response.data.error); + } else { + setCellError("Failed to fetch cell type data."); + } + }) + .finally(() => { + setCellLoading(false); + }); + }, [selectedStage]); + const handleSearch = async () => { if (!gene) return; @@ -113,31 +143,64 @@ const GeneView: React.FC = () => {
-

3D 表达可视化

+

3D 可视化

- 下方的3D可视化展示了所选基因在{selectedStage}发育阶段的空间表达模式。 - 点击任意数据点可查看详细的表达信息。 + 下方展示了{selectedStage}发育阶段的细胞类型分布(左)和所选基因的表达模式(右)。 + 点击任意数据点可查看详细信息。

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

正在加载基因表达数据...

-
- ) : ( -
-
📊
-

请选择发育阶段和目标基因以查看表达数据

-
- )} -
- )} + {/* 双可视化区域 */} +
+ {/* 细胞类型可视化 */} +
+

细胞类型分布

+ {cellError &&
{cellError}
} + {cellData.length > 0 ? ( + + ) : ( +
+ {cellLoading ? ( +
+
🔄
+

正在加载细胞类型数据...

+
+ ) : ( +
+
🧬
+

细胞类型分布

+

+ {selectedStage} 阶段 +

+
+ )} +
+ )} +
+ + {/* 基因表达可视化 */} +
+

基因表达模式

+ {data.length > 0 ? ( + + ) : ( +
+ {loading ? ( +
+
🔄
+

正在加载基因表达数据...

+
+ ) : ( +
+
📊
+

请选择目标基因以查看表达数据

+

+ 当前阶段:{selectedStage} +

+
+ )} +
+ )} +
@@ -146,7 +209,7 @@ const GeneView: React.FC = () => {

🎯 选择目标

-

首先选择感兴趣的发育阶段,然后从下拉菜单中选择要查看的基因。

+

首先选择感兴趣的发育阶段,系统将自动显示该阶段的细胞类型分布,然后选择基因查看表达模式。

🖱️ 交互操作

@@ -154,7 +217,7 @@ const GeneView: React.FC = () => {

🎨 颜色编码

-

颜色代表基因表达强度:红色表示高表达,蓝色表示低表达。

+

左侧:不同颜色代表不同细胞类型。右侧:颜色代表基因表达强度,红色表示高表达,蓝色表示低表达。

diff --git a/embryo-frontend/src/style.css b/embryo-frontend/src/style.css index 0a5b1c8..bcadac0 100644 --- a/embryo-frontend/src/style.css +++ b/embryo-frontend/src/style.css @@ -470,6 +470,9 @@ button:disabled { box-shadow: var(--shadow-md); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); + max-width: 200px; + max-height: 400px; + overflow-y: auto; } .legend-gradient { @@ -483,6 +486,38 @@ button:disabled { margin-bottom: 0.5rem; } +.legend-title { + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.75rem; + text-align: center; + border-bottom: 1px solid var(--border-light); + padding-bottom: 0.5rem; +} + +.legend-item { + display: flex; + align-items: center; + margin-bottom: 0.5rem; + padding: 0.25rem 0; +} + +.legend-color { + width: 14px; + height: 14px; + border-radius: 50%; + margin-right: 0.75rem; + border: 1px solid rgba(0, 0, 0, 0.1); + flex-shrink: 0; +} + +.legend-label { + font-size: 0.8rem; + color: var(--text-primary); + line-height: 1.2; + word-break: break-word; +} + /* ===== 其他页面样式 ===== */ .clustering-features, .resource-grid, .software-grid { display: grid;