From 73e83da5febfd89bef11491f1a78c745f6549108 Mon Sep 17 00:00:00 2001 From: wjsjwr Date: Sat, 26 Jul 2025 19:30:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=9F=BA=E5=9B=A0=E6=97=B6?= =?UTF-8?q?=E5=BA=8F=E5=88=86=E6=9E=90API=EF=BC=8C=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=97=B6=E9=97=B4=E5=BA=8F=E5=88=97=E5=88=86?= =?UTF-8?q?=E6=9E=90=E9=A1=B5=E9=9D=A2=EF=BC=8C=E6=94=AF=E6=8C=81=E5=9F=BA?= =?UTF-8?q?=E5=9B=A0=E9=80=89=E6=8B=A9=E5=92=8C=E7=BB=86=E8=83=9E=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E6=B5=81=E5=8A=A8=E5=8F=AF=E8=A7=86=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=94=A8=E6=88=B7=E4=BA=A4=E4=BA=92=E4=BD=93?= =?UTF-8?q?=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- embryo-backend/app.py | 78 ++ embryo-frontend/package-lock.json | 185 +++++ embryo-frontend/package.json | 3 + embryo-frontend/src/App.tsx | 2 + embryo-frontend/src/components/Navigation.tsx | 1 + .../src/pages/TemporalAnalysis.tsx | 664 ++++++++++++++++++ 6 files changed, 933 insertions(+) create mode 100644 embryo-frontend/src/pages/TemporalAnalysis.tsx diff --git a/embryo-backend/app.py b/embryo-backend/app.py index 278d21b..23af8da 100644 --- a/embryo-backend/app.py +++ b/embryo-backend/app.py @@ -138,5 +138,83 @@ def get_gene_distribution(): "distribution": distribution }) +@app.route("/api/gene_temporal_analysis") +def get_gene_temporal_analysis(): + gene = request.args.get("gene") + + if not gene: + return jsonify({"error": "Gene parameter is required"}), 400 + + stages = ["CS7", "CS8", "CS9"] + result_data = [] + + for stage in stages: + adata = load_adata(stage) + + if not adata: + return jsonify({"error": f"Stage '{stage}' not found"}), 404 + + if gene not in adata.var_names: + # 如果某个阶段没有该基因,跳过这个阶段 + continue + + # 查找细胞类型列 + 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": f"No cell type information found in stage {stage}"}), 404 + + # 读取基因表达值 + expr = adata[:, gene].X + expr = expr.toarray().flatten() if hasattr(expr, "toarray") else expr.flatten() + + # 读取细胞类型 + cell_types = adata.obs[cell_type_col].values + + # 按细胞类型计算平均表达值和细胞数量 + cell_type_stats = {} + for cell_type, expression in zip(cell_types, expr): + cell_type_str = str(cell_type) + if cell_type_str not in cell_type_stats: + cell_type_stats[cell_type_str] = { + 'expressions': [], + 'count': 0 + } + cell_type_stats[cell_type_str]['expressions'].append(float(expression)) + cell_type_stats[cell_type_str]['count'] += 1 + + # 计算每个细胞类型的平均表达值 + total_cells = len(cell_types) + stage_data = { + 'stage': stage, + 'cell_types': {} + } + + for cell_type, stats in cell_type_stats.items(): + avg_expression = sum(stats['expressions']) / len(stats['expressions']) + proportion = stats['count'] / total_cells * 100 # 转换为百分比 + + stage_data['cell_types'][cell_type] = { + 'avg_expression': avg_expression, + 'proportion': proportion, + 'count': stats['count'] + } + + result_data.append(stage_data) + + if not result_data: + return jsonify({"error": f"Gene '{gene}' not found in any stage"}), 404 + + return jsonify({ + "gene": gene, + "stages_data": result_data + }) + if __name__ == "__main__": app.run(debug=True) diff --git a/embryo-frontend/package-lock.json b/embryo-frontend/package-lock.json index 1e2f2ff..3930bba 100644 --- a/embryo-frontend/package-lock.json +++ b/embryo-frontend/package-lock.json @@ -9,6 +9,9 @@ "version": "0.0.0", "dependencies": { "@nivo/boxplot": "^0.99.0", + "@nivo/line": "^0.99.0", + "@nivo/sankey": "^0.99.0", + "@nivo/stream": "^0.99.0", "axios": "^1.10.0", "chart.js": "^4.5.0", "react": "^19.1.0", @@ -607,6 +610,52 @@ "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, + "node_modules/@nivo/line": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/line/-/line-0.99.0.tgz", + "integrity": "sha512-bAqTXSjpnpcGMs341qWFUi7hJTqQiNoSeJHsYPuPS3icuXPcp3WETQH+zRZACeEF79ZigeOWCW+dzODgne1y9w==", + "license": "MIT", + "dependencies": { + "@nivo/annotations": "0.99.0", + "@nivo/axes": "0.99.0", + "@nivo/colors": "0.99.0", + "@nivo/core": "0.99.0", + "@nivo/legends": "0.99.0", + "@nivo/scales": "0.99.0", + "@nivo/theming": "0.99.0", + "@nivo/tooltip": "0.99.0", + "@nivo/voronoi": "0.99.0", + "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", + "@types/d3-shape": "^3.1.6", + "d3-shape": "^3.2.0" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/sankey": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/sankey/-/sankey-0.99.0.tgz", + "integrity": "sha512-u5hySywsachjo9cHdUxCR9qwD6gfRVPEAcpuIUKiA0WClDjdGbl3vkrQcQcFexJUBThqSSbwGCDWR+2INXSbTw==", + "license": "MIT", + "dependencies": { + "@nivo/colors": "0.99.0", + "@nivo/core": "0.99.0", + "@nivo/legends": "0.99.0", + "@nivo/text": "0.99.0", + "@nivo/theming": "0.99.0", + "@nivo/tooltip": "0.99.0", + "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", + "@types/d3-sankey": "^0.11.2", + "@types/d3-shape": "^3.1.6", + "d3-sankey": "^0.12.3", + "d3-shape": "^3.2.0", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, "node_modules/@nivo/scales": { "version": "0.99.0", "resolved": "https://registry.npmjs.org/@nivo/scales/-/scales-0.99.0.tgz", @@ -630,6 +679,27 @@ "integrity": "sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg==", "license": "MIT" }, + "node_modules/@nivo/stream": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/stream/-/stream-0.99.0.tgz", + "integrity": "sha512-/vXtbU1Nzepgxg0Y3Wrve7Q6BllaQCP9+lLpPCiRWWXIyODcKSauqsYKY4kas9YcIfJbchJclVD2CSHKt+lPxQ==", + "license": "MIT", + "dependencies": { + "@nivo/axes": "0.99.0", + "@nivo/colors": "0.99.0", + "@nivo/core": "0.99.0", + "@nivo/legends": "0.99.0", + "@nivo/scales": "0.99.0", + "@nivo/theming": "0.99.0", + "@nivo/tooltip": "0.99.0", + "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", + "@types/d3-shape": "^3.1.6", + "d3-shape": "^3.2.0" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, "node_modules/@nivo/text": { "version": "0.99.0", "resolved": "https://registry.npmjs.org/@nivo/text/-/text-0.99.0.tgz", @@ -670,6 +740,24 @@ "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, + "node_modules/@nivo/voronoi": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/voronoi/-/voronoi-0.99.0.tgz", + "integrity": "sha512-KfmMdidbYzhiUCki1FG4X4nHEFT4loK8G5bMBnmCl9U+S78W+gvkfrgD2Aoqp/Q9yKQvr3Y8UcZKSFZnn3HgjQ==", + "license": "MIT", + "dependencies": { + "@nivo/core": "0.99.0", + "@nivo/theming": "0.99.0", + "@nivo/tooltip": "0.99.0", + "@types/d3-delaunay": "^6.0.4", + "@types/d3-scale": "^4.0.8", + "d3-delaunay": "^6.0.4", + "d3-scale": "^4.0.2" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, "node_modules/@react-spring/animated": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.1.tgz", @@ -1035,6 +1123,12 @@ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, "node_modules/@types/d3-format": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.5.tgz", @@ -1056,6 +1150,30 @@ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "license": "MIT" }, + "node_modules/@types/d3-sankey": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@types/d3-sankey/-/d3-sankey-0.11.2.tgz", + "integrity": "sha512-U6SrTWUERSlOhnpSrgvMX64WblX1AxX6nEjI2t3mLK2USpQrnbwYYK+AS9SwiE7wgYmOsSSKoSdr8aoKBH0HgQ==", + "license": "MIT", + "dependencies": { + "@types/d3-shape": "^1" + } + }, + "node_modules/@types/d3-sankey/node_modules/@types/d3-path": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", + "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==", + "license": "MIT" + }, + "node_modules/@types/d3-sankey/node_modules/@types/d3-shape": { + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", + "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "^1" + } + }, "node_modules/@types/d3-scale": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", @@ -1247,6 +1365,18 @@ "node": ">=12" } }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-format": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", @@ -1274,6 +1404,46 @@ "node": ">=12" } }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", @@ -1342,6 +1512,15 @@ "d3-time": "1 - 2" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1827,6 +2006,12 @@ "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.45.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", diff --git a/embryo-frontend/package.json b/embryo-frontend/package.json index 7eec8f6..2ae320b 100644 --- a/embryo-frontend/package.json +++ b/embryo-frontend/package.json @@ -17,6 +17,9 @@ }, "dependencies": { "@nivo/boxplot": "^0.99.0", + "@nivo/line": "^0.99.0", + "@nivo/sankey": "^0.99.0", + "@nivo/stream": "^0.99.0", "axios": "^1.10.0", "chart.js": "^4.5.0", "react": "^19.1.0", diff --git a/embryo-frontend/src/App.tsx b/embryo-frontend/src/App.tsx index 68b77cb..39ac189 100644 --- a/embryo-frontend/src/App.tsx +++ b/embryo-frontend/src/App.tsx @@ -6,6 +6,7 @@ import GeneView from "./pages/GeneView"; import SpatialClustering from "./pages/SpatialClustering"; import Resource from "./pages/Resource"; import Download from "./pages/Download"; +import TemporalAnalysis from "./pages/TemporalAnalysis"; import "./style.css"; function App() { @@ -18,6 +19,7 @@ function App() { } /> } /> } /> + } /> } /> } /> diff --git a/embryo-frontend/src/components/Navigation.tsx b/embryo-frontend/src/components/Navigation.tsx index c8fea41..f7e9bd2 100644 --- a/embryo-frontend/src/components/Navigation.tsx +++ b/embryo-frontend/src/components/Navigation.tsx @@ -8,6 +8,7 @@ const Navigation: React.FC = () => { { path: "/", label: "Home", name: "首页" }, { path: "/spatial-clustering", label: "Spatial clustering", name: "空间聚类" }, { path: "/gene-view", label: "Gene Expression", name: "基因表达" }, + { path: "/temporal-analysis", label: "Temporal Analysis", name: "时间序列分析" }, { path: "/resource", label: "Resource", name: "资源" }, { path: "/download", label: "Download", name: "下载" }, ]; diff --git a/embryo-frontend/src/pages/TemporalAnalysis.tsx b/embryo-frontend/src/pages/TemporalAnalysis.tsx new file mode 100644 index 0000000..3ea839d --- /dev/null +++ b/embryo-frontend/src/pages/TemporalAnalysis.tsx @@ -0,0 +1,664 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { ResponsiveSankey } from "@nivo/sankey"; +import { ResponsiveLine } from "@nivo/line"; +import PointCloud from "../components/PointCloud"; +import "../style.css"; + +const TemporalAnalysis: React.FC = () => { + const [cellDataCS7, setCellDataCS7] = useState([]); + const [cellDataCS8, setCellDataCS8] = useState([]); + const [cellDataCS9, setCellDataCS9] = useState([]); + const [loadingCS7, setLoadingCS7] = useState(false); + const [loadingCS8, setLoadingCS8] = useState(false); + const [loadingCS9, setLoadingCS9] = useState(false); + const [errorCS7, setErrorCS7] = useState(""); + const [errorCS8, setErrorCS8] = useState(""); + const [errorCS9, setErrorCS9] = useState(""); + const [sankeyData, setSankeyData] = useState({ nodes: [], links: [] }); + const [sankeyLoading, setSankeyLoading] = useState(false); + + // 第三部分:基因时序分析相关状态 + const [availableGenes, setAvailableGenes] = useState([]); + const [selectedGene, setSelectedGene] = useState(""); + const [temporalData, setTemporalData] = useState([]); + const [temporalLoading, setTemporalLoading] = useState(false); + const [temporalError, setTemporalError] = useState(""); + + // 生成细胞类型颜色映射 + 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) => { + colorMap.set(type, colors[index % colors.length]); + }); + + return colorMap; + }; + + // 获取可用基因列表(从CS7阶段获取) + useEffect(() => { + axios + .get("http://localhost:5000/api/genes", { + params: { stage: "CS7" }, + }) + .then((res) => setAvailableGenes(res.data)) + .catch(() => setAvailableGenes([])); + }, []); + + // 获取基因时序分析数据 + const fetchTemporalAnalysis = async (gene: string) => { + if (!gene) return; + + setTemporalLoading(true); + setTemporalError(""); + setTemporalData([]); + + try { + const response = await axios.get("http://localhost:5000/api/gene_temporal_analysis", { + params: { gene: gene.trim() } + }); + + // 转换数据为线图格式 + const stagesData = response.data.stages_data; + const allCellTypes = new Set(); + + // 收集所有细胞类型 + stagesData.forEach((stageData: any) => { + Object.keys(stageData.cell_types).forEach(cellType => { + allCellTypes.add(cellType); + }); + }); + + const cellTypesArray = Array.from(allCellTypes).sort(); + const colorMap = generateCellTypeColors(cellTypesArray); + + // 构建线图数据 - 每个细胞类型一条线 + const lineData = cellTypesArray.map(cellType => { + const linePoints = stagesData.map((stageData: any) => { + const cellTypeData = stageData.cell_types[cellType]; + return { + x: stageData.stage, + y: cellTypeData ? cellTypeData.proportion : 0 + }; + }); + + return { + id: cellType, + color: colorMap.get(cellType) || '#CCCCCC', + data: linePoints + }; + }); + + setTemporalData(lineData); + console.log('Temporal analysis data:', lineData); + } catch (err: any) { + if (err.response && err.response.data?.error) { + setTemporalError(err.response.data.error); + } else { + setTemporalError("Failed to fetch temporal analysis data."); + } + } finally { + setTemporalLoading(false); + } + }; + + // 构建桑基图数据 + const buildSankeyData = (dataCS7: any[], dataCS8: any[], dataCS9: any[]) => { + setSankeyLoading(true); + + try { + // 统计各阶段的细胞类型数量 + const countCellTypes = (data: any[]) => { + const counts: { [key: string]: number } = {}; + data.forEach(cell => { + const cellType = cell.value || 'Unknown'; // 修正:细胞类型存储在value字段中 + counts[cellType] = (counts[cellType] || 0) + 1; + }); + return counts; + }; + + const countsCS7 = countCellTypes(dataCS7); + const countsCS8 = countCellTypes(dataCS8); + const countsCS9 = countCellTypes(dataCS9); + + // 调试信息 + console.log('Cell type counts CS7:', countsCS7); + console.log('Cell type counts CS8:', countsCS8); + console.log('Cell type counts CS9:', countsCS9); + + // 获取所有细胞类型 + const allCellTypes = new Set([ + ...Object.keys(countsCS7), + ...Object.keys(countsCS8), + ...Object.keys(countsCS9) + ]); + + const cellTypeArray = Array.from(allCellTypes).sort(); + const colorMap = generateCellTypeColors(cellTypeArray); + + // 计算总数 + const totalCS7 = Object.values(countsCS7).reduce((sum, count) => sum + count, 0); + const totalCS8 = Object.values(countsCS8).reduce((sum, count) => sum + count, 0); + const totalCS9 = Object.values(countsCS9).reduce((sum, count) => sum + count, 0); + + // 构建节点 + const nodes: any[] = []; + cellTypeArray.forEach(cellType => { + const color = colorMap.get(cellType) || '#CCCCCC'; + + // CS7 节点 + if (countsCS7[cellType]) { + const percentage = ((countsCS7[cellType] / totalCS7) * 100).toFixed(1); + nodes.push({ + id: `CS7_${cellType}`, + nodeColor: color, + label: `${cellType} (${percentage}%)` + }); + } + + // CS8 节点 + if (countsCS8[cellType]) { + const percentage = ((countsCS8[cellType] / totalCS8) * 100).toFixed(1); + nodes.push({ + id: `CS8_${cellType}`, + nodeColor: color, + label: `${cellType} (${percentage}%)` + }); + } + + // CS9 节点 + if (countsCS9[cellType]) { + const percentage = ((countsCS9[cellType] / totalCS9) * 100).toFixed(1); + nodes.push({ + id: `CS9_${cellType}`, + nodeColor: color, + label: `${cellType} (${percentage}%)` + }); + } + }); + + // 构建连接 + const links: any[] = []; + cellTypeArray.forEach(cellType => { + const color = colorMap.get(cellType) || '#CCCCCC'; + + // CS7 -> CS8 连接 + if (countsCS7[cellType] && countsCS8[cellType]) { + const cs7Percentage = (countsCS7[cellType] / totalCS7) * 100; + const cs8Percentage = (countsCS8[cellType] / totalCS8) * 100; + const avgPercentage = (cs7Percentage + cs8Percentage) / 2; + + links.push({ + source: `CS7_${cellType}`, + target: `CS8_${cellType}`, + value: Math.max(avgPercentage, 0.1), // 确保有最小值以显示连接 + color: color + '80' // 添加透明度 + }); + } + + // CS8 -> CS9 连接 + if (countsCS8[cellType] && countsCS9[cellType]) { + const cs8Percentage = (countsCS8[cellType] / totalCS8) * 100; + const cs9Percentage = (countsCS9[cellType] / totalCS9) * 100; + const avgPercentage = (cs8Percentage + cs9Percentage) / 2; + + links.push({ + source: `CS8_${cellType}`, + target: `CS9_${cellType}`, + value: Math.max(avgPercentage, 0.1), // 确保有最小值以显示连接 + color: color + '80' // 添加透明度 + }); + } + }); + + setSankeyData({ nodes, links }); + } catch (error) { + console.error('Error building Sankey data:', error); + setSankeyData({ nodes: [], links: [] }); + } finally { + setSankeyLoading(false); + } + }; + + // 获取CS7阶段细胞类型数据 + useEffect(() => { + setLoadingCS7(true); + setErrorCS7(""); + setCellDataCS7([]); + + axios + .get("http://localhost:5000/api/cell", { + params: { stage: "CS7" }, + }) + .then((res) => { + setCellDataCS7(res.data.cells); + }) + .catch((err) => { + if (err.response && err.response.data?.error) { + setErrorCS7(err.response.data.error); + } else { + setErrorCS7("Failed to fetch CS7 cell type data."); + } + }) + .finally(() => { + setLoadingCS7(false); + }); + }, []); + + // 获取CS8阶段细胞类型数据 + useEffect(() => { + setLoadingCS8(true); + setErrorCS8(""); + setCellDataCS8([]); + + axios + .get("http://localhost:5000/api/cell", { + params: { stage: "CS8" }, + }) + .then((res) => { + setCellDataCS8(res.data.cells); + }) + .catch((err) => { + if (err.response && err.response.data?.error) { + setErrorCS8(err.response.data.error); + } else { + setErrorCS8("Failed to fetch CS8 cell type data."); + } + }) + .finally(() => { + setLoadingCS8(false); + }); + }, []); + + // 获取CS9阶段细胞类型数据 + useEffect(() => { + setLoadingCS9(true); + setErrorCS9(""); + setCellDataCS9([]); + + axios + .get("http://localhost:5000/api/cell", { + params: { stage: "CS9" }, + }) + .then((res) => { + setCellDataCS9(res.data.cells); + }) + .catch((err) => { + if (err.response && err.response.data?.error) { + setErrorCS9(err.response.data.error); + } else { + setErrorCS9("Failed to fetch CS9 cell type data."); + } + }) + .finally(() => { + setLoadingCS9(false); + }); + }, []); + + // 当所有数据加载完成后构建桑基图数据 + useEffect(() => { + if (cellDataCS7.length > 0 && cellDataCS8.length > 0 && cellDataCS9.length > 0) { + buildSankeyData(cellDataCS7, cellDataCS8, cellDataCS9); + } + }, [cellDataCS7, cellDataCS8, cellDataCS9]); + + return ( +
+
+

时间序列分析

+

+ 通过对比不同发育阶段的细胞类型分布,分析胚胎发育过程中细胞类型的变化和演进模式。 +

+
+ +
+ {/* 第一部分:三个发育阶段的细胞类型分布 */} +
+

发育阶段对比

+

+ 下方展示了CS7、CS8、CS9三个发育阶段的细胞类型3D分布图。通过对比可以观察细胞类型在发育过程中的空间分布变化。 +

+ +
+ {/* CS7阶段细胞类型分布 */} +
+

CS7 - 早期发育阶段

+ {errorCS7 &&
{errorCS7}
} + {cellDataCS7.length > 0 ? ( + + ) : ( +
+ {loadingCS7 ? ( +
+
🔄
+

正在加载CS7数据...

+
+ ) : ( +
+
🧬
+

CS7阶段细胞类型分布

+

+ 早期发育阶段 +

+
+ )} +
+ )} +
+ + {/* CS8阶段细胞类型分布 */} +
+

CS8 - 中期发育阶段

+ {errorCS8 &&
{errorCS8}
} + {cellDataCS8.length > 0 ? ( + + ) : ( +
+ {loadingCS8 ? ( +
+
🔄
+

正在加载CS8数据...

+
+ ) : ( +
+
🧬
+

CS8阶段细胞类型分布

+

+ 中期发育阶段 +

+
+ )} +
+ )} +
+ + {/* CS9阶段细胞类型分布 */} +
+

CS9 - 后期发育阶段

+ {errorCS9 &&
{errorCS9}
} + {cellDataCS9.length > 0 ? ( + + ) : ( +
+ {loadingCS9 ? ( +
+
🔄
+

正在加载CS9数据...

+
+ ) : ( +
+
🧬
+

CS9阶段细胞类型分布

+

+ 后期发育阶段 +

+
+ )} +
+ )} +
+
+
+ + {/* 第二部分和第三部分:并排显示 */} +
+

流动分析与基因时序

+

+ 左侧桑基图展示细胞类型在发育阶段间的流动模式,右侧线图显示选定基因在各细胞类型中的占比变化。 +

+ +
+ {/* 桑基图部分 */} +
+

细胞类型演进流动图

+
+ {sankeyData.nodes.length > 0 && sankeyData.links.length > 0 ? ( + <> + node.nodeColor} + nodeOpacity={0.8} + nodeHoverOpacity={1} + nodeThickness={18} + nodeSpacing={8} + nodeBorderWidth={2} + nodeBorderColor={{ + from: 'color', + modifiers: [['darker', 0.3]] + }} + linkOpacity={0.3} + linkHoverOpacity={0.6} + linkContract={4} + enableLinkGradient={true} + labelPosition="outside" + labelOrientation="horizontal" + labelPadding={12} + labelTextColor={{ + from: 'color', + modifiers: [['darker', 1]] + }} + legends={[]} + motionConfig="gentle" + /> + {/* 发育阶段标签行 */} +
+
+ CS7 - 早期 +
+
+ CS8 - 中期 +
+
+ CS9 - 后期 +
+
+ + ) : ( +
+ {sankeyLoading || loadingCS7 || loadingCS8 || loadingCS9 ? ( +
+
🔄
+

正在构建流动图...

+
+ ) : ( +
+
📊
+

等待数据加载

+
+ )} +
+ )} +
+
+ + {/* 基因时序分析部分 */} +
+
+

基因时序分析

+ +
+ +
+ {temporalLoading ? ( +
+
🔄
+

正在加载时序数据...

+
+ ) : temporalError ? ( +
{temporalError}
+ ) : temporalData.length > 0 ? ( + line.color} + pointSize={8} + pointColor={{ theme: 'background' }} + pointBorderWidth={3} + pointBorderColor={{ from: 'serieColor' }} + pointLabelYOffset={-12} + useMesh={true} + enableGridX={false} + enableGridY={true} + lineWidth={3} + legends={[ + { + anchor: 'bottom-right', + direction: 'column', + justify: false, + translateX: 60, + translateY: 0, + itemsSpacing: 1, + itemWidth: 50, + itemHeight: 14, + itemDirection: 'left-to-right', + itemOpacity: 0.75, + symbolSize: 8, + symbolShape: 'circle', + effects: [ + { + on: 'hover', + style: { + itemOpacity: 1 + } + } + ] + } + ]} + /> + ) : ( +
+
📊
+

请选择基因查看时序变化

+
+ )} +
+
+
+
+ +
+

使用说明

+
+
+

📅 时间序列对比

+

三个3D图分别展示CS7、CS8、CS9三个发育阶段的细胞类型分布,可以直观地观察细胞类型随时间的变化。

+
+
+

🌊 桑基图解读

+

桑基图显示细胞类型的演进流动:节点大小表示占比,连接线显示相同细胞类型在不同阶段间的延续性,颜色保持一致便于追踪。

+
+
+

📊 基因时序分析

+

选择特定基因后,堆叠面积图显示该基因在各细胞类型中的占比变化,帮助理解基因在发育过程中的表达模式演进。

+
+
+

🖱️ 交互操作

+

在3D视图中使用鼠标拖拽旋转视角,滚轮缩放,点击数据点查看详细信息。在桑基图和堆叠图中悬停查看具体数值。

+
+
+

🎨 颜色编码

+

相同颜色代表相同的细胞类型,在所有图表中保持一致,便于追踪特定细胞类型的变化和流动模式。

+
+
+

🔍 发育模式识别

+

通过对比三个阶段和不同基因,可以识别细胞类型的出现、消失、迁移和分化模式,以及基因表达的动态变化。

+
+
+

📈 百分比分析

+

桑基图和堆叠图中的百分比表示各细胞类型在对应发育阶段中的相对丰度,帮助理解细胞类型组成的动态平衡。

+
+
+

🧬 基因选择策略

+

选择不同的发育相关基因(如SOX2、NANOG等),观察其在胚胎发育过程中的表达变化模式和细胞类型特异性。

+
+
+
+
+
+ ); +}; + +export default TemporalAnalysis; \ No newline at end of file