Compare commits
3 Commits
93e0eae353
...
2291f59144
| Author | SHA1 | Date | |
|---|---|---|---|
| 2291f59144 | |||
| edb72bc526 | |||
| 73e83da5fe |
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)
|
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"]
|
genes = ["SOX2", "NANOG", "T", "POU5F1", "OTX2", "ZIC2", "FOXA2", "LEFTY1"]
|
||||||
layers = {
|
layers = {
|
||||||
"Ectoderm": 1.0, # 外层
|
"Ectoderm": 1.0, # 外层
|
||||||
|
|||||||
@ -138,5 +138,83 @@ def get_gene_distribution():
|
|||||||
"distribution": 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__":
|
if __name__ == "__main__":
|
||||||
app.run(debug=True)
|
app.run(debug=True)
|
||||||
|
|||||||
185
embryo-frontend/package-lock.json
generated
185
embryo-frontend/package-lock.json
generated
@ -9,6 +9,9 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nivo/boxplot": "^0.99.0",
|
"@nivo/boxplot": "^0.99.0",
|
||||||
|
"@nivo/line": "^0.99.0",
|
||||||
|
"@nivo/sankey": "^0.99.0",
|
||||||
|
"@nivo/stream": "^0.99.0",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
@ -607,6 +610,52 @@
|
|||||||
"react": "^16.14 || ^17.0 || ^18.0 || ^19.0"
|
"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": {
|
"node_modules/@nivo/scales": {
|
||||||
"version": "0.99.0",
|
"version": "0.99.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nivo/scales/-/scales-0.99.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nivo/scales/-/scales-0.99.0.tgz",
|
||||||
@ -630,6 +679,27 @@
|
|||||||
"integrity": "sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg==",
|
"integrity": "sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@nivo/text": {
|
||||||
"version": "0.99.0",
|
"version": "0.99.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nivo/text/-/text-0.99.0.tgz",
|
"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"
|
"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": {
|
"node_modules/@react-spring/animated": {
|
||||||
"version": "10.0.1",
|
"version": "10.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.1.tgz",
|
||||||
@ -1035,6 +1123,12 @@
|
|||||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/d3-format": {
|
||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.5.tgz",
|
||||||
@ -1056,6 +1150,30 @@
|
|||||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/d3-scale": {
|
||||||
"version": "4.0.9",
|
"version": "4.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
@ -1247,6 +1365,18 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/d3-format": {
|
||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz",
|
||||||
@ -1274,6 +1404,46 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/d3-scale": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
@ -1342,6 +1512,15 @@
|
|||||||
"d3-time": "1 - 2"
|
"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": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"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"
|
"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": {
|
"node_modules/rollup": {
|
||||||
"version": "4.45.1",
|
"version": "4.45.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz",
|
||||||
|
|||||||
@ -17,6 +17,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nivo/boxplot": "^0.99.0",
|
"@nivo/boxplot": "^0.99.0",
|
||||||
|
"@nivo/line": "^0.99.0",
|
||||||
|
"@nivo/sankey": "^0.99.0",
|
||||||
|
"@nivo/stream": "^0.99.0",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import GeneView from "./pages/GeneView";
|
|||||||
import SpatialClustering from "./pages/SpatialClustering";
|
import SpatialClustering from "./pages/SpatialClustering";
|
||||||
import Resource from "./pages/Resource";
|
import Resource from "./pages/Resource";
|
||||||
import Download from "./pages/Download";
|
import Download from "./pages/Download";
|
||||||
|
import TemporalAnalysis from "./pages/TemporalAnalysis";
|
||||||
|
import EmbryoGeneration from "./pages/EmbryoGeneration";
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@ -18,6 +20,8 @@ function App() {
|
|||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/gene-view" element={<GeneView />} />
|
<Route path="/gene-view" element={<GeneView />} />
|
||||||
<Route path="/spatial-clustering" element={<SpatialClustering />} />
|
<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="/resource" element={<Resource />} />
|
||||||
<Route path="/download" element={<Download />} />
|
<Route path="/download" element={<Download />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@ -8,6 +8,8 @@ const Navigation: React.FC = () => {
|
|||||||
{ path: "/", label: "Home", name: "首页" },
|
{ path: "/", label: "Home", name: "首页" },
|
||||||
{ path: "/spatial-clustering", label: "Spatial clustering", name: "空间聚类" },
|
{ path: "/spatial-clustering", label: "Spatial clustering", name: "空间聚类" },
|
||||||
{ path: "/gene-view", label: "Gene Expression", 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: "/resource", label: "Resource", name: "资源" },
|
||||||
{ path: "/download", label: "Download", 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;
|
||||||
664
embryo-frontend/src/pages/TemporalAnalysis.tsx
Normal file
664
embryo-frontend/src/pages/TemporalAnalysis.tsx
Normal file
@ -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<any[]>([]);
|
||||||
|
const [cellDataCS8, setCellDataCS8] = useState<any[]>([]);
|
||||||
|
const [cellDataCS9, setCellDataCS9] = useState<any[]>([]);
|
||||||
|
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<any>({ nodes: [], links: [] });
|
||||||
|
const [sankeyLoading, setSankeyLoading] = useState(false);
|
||||||
|
|
||||||
|
// 第三部分:基因时序分析相关状态
|
||||||
|
const [availableGenes, setAvailableGenes] = useState<string[]>([]);
|
||||||
|
const [selectedGene, setSelectedGene] = useState("");
|
||||||
|
const [temporalData, setTemporalData] = useState<any[]>([]);
|
||||||
|
const [temporalLoading, setTemporalLoading] = useState(false);
|
||||||
|
const [temporalError, setTemporalError] = useState("");
|
||||||
|
|
||||||
|
// 生成细胞类型颜色映射
|
||||||
|
const generateCellTypeColors = (cellTypes: string[]) => {
|
||||||
|
const colorMap = new Map<string, string>();
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
// 收集所有细胞类型
|
||||||
|
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 (
|
||||||
|
<div className="page-container">
|
||||||
|
<div className="page-header">
|
||||||
|
<h1>时间序列分析</h1>
|
||||||
|
<p className="page-description">
|
||||||
|
通过对比不同发育阶段的细胞类型分布,分析胚胎发育过程中细胞类型的变化和演进模式。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="page-content">
|
||||||
|
{/* 第一部分:三个发育阶段的细胞类型分布 */}
|
||||||
|
<div className="section">
|
||||||
|
<h2>发育阶段对比</h2>
|
||||||
|
<p className="flat-section-description">
|
||||||
|
下方展示了CS7、CS8、CS9三个发育阶段的细胞类型3D分布图。通过对比可以观察细胞类型在发育过程中的空间分布变化。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="three-column-grid visualization-grid"
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(3, minmax(400px, 1fr))",
|
||||||
|
gap: "2rem",
|
||||||
|
minHeight: "600px",
|
||||||
|
width: "100%",
|
||||||
|
marginBottom: "3rem"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* CS7阶段细胞类型分布 */}
|
||||||
|
<div className="visualization-container">
|
||||||
|
<h3 style={{ marginBottom: "1rem", textAlign: "center" }}>CS7 - 早期发育阶段</h3>
|
||||||
|
{errorCS7 && <div className="error-message">{errorCS7}</div>}
|
||||||
|
{cellDataCS7.length > 0 ? (
|
||||||
|
<PointCloud data={cellDataCS7} isCategorical={true} />
|
||||||
|
) : (
|
||||||
|
<div className="no-data-message">
|
||||||
|
{loadingCS7 ? (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: "2rem", marginBottom: "1rem" }}>🔄</div>
|
||||||
|
<p>正在加载CS7数据...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: "2rem", marginBottom: "1rem" }}>🧬</div>
|
||||||
|
<p>CS7阶段细胞类型分布</p>
|
||||||
|
<p style={{ fontSize: "0.9rem", color: "var(--text-secondary)" }}>
|
||||||
|
早期发育阶段
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CS8阶段细胞类型分布 */}
|
||||||
|
<div className="visualization-container">
|
||||||
|
<h3 style={{ marginBottom: "1rem", textAlign: "center" }}>CS8 - 中期发育阶段</h3>
|
||||||
|
{errorCS8 && <div className="error-message">{errorCS8}</div>}
|
||||||
|
{cellDataCS8.length > 0 ? (
|
||||||
|
<PointCloud data={cellDataCS8} isCategorical={true} />
|
||||||
|
) : (
|
||||||
|
<div className="no-data-message">
|
||||||
|
{loadingCS8 ? (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: "2rem", marginBottom: "1rem" }}>🔄</div>
|
||||||
|
<p>正在加载CS8数据...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: "2rem", marginBottom: "1rem" }}>🧬</div>
|
||||||
|
<p>CS8阶段细胞类型分布</p>
|
||||||
|
<p style={{ fontSize: "0.9rem", color: "var(--text-secondary)" }}>
|
||||||
|
中期发育阶段
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CS9阶段细胞类型分布 */}
|
||||||
|
<div className="visualization-container">
|
||||||
|
<h3 style={{ marginBottom: "1rem", textAlign: "center" }}>CS9 - 后期发育阶段</h3>
|
||||||
|
{errorCS9 && <div className="error-message">{errorCS9}</div>}
|
||||||
|
{cellDataCS9.length > 0 ? (
|
||||||
|
<PointCloud data={cellDataCS9} isCategorical={true} />
|
||||||
|
) : (
|
||||||
|
<div className="no-data-message">
|
||||||
|
{loadingCS9 ? (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: "2rem", marginBottom: "1rem" }}>🔄</div>
|
||||||
|
<p>正在加载CS9数据...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: "2rem", marginBottom: "1rem" }}>🧬</div>
|
||||||
|
<p>CS9阶段细胞类型分布</p>
|
||||||
|
<p style={{ fontSize: "0.9rem", color: "var(--text-secondary)" }}>
|
||||||
|
后期发育阶段
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 第二部分和第三部分:并排显示 */}
|
||||||
|
<div className="section">
|
||||||
|
<h2>流动分析与基因时序</h2>
|
||||||
|
<p className="flat-section-description">
|
||||||
|
左侧桑基图展示细胞类型在发育阶段间的流动模式,右侧线图显示选定基因在各细胞类型中的占比变化。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 1fr",
|
||||||
|
gap: "2rem",
|
||||||
|
marginBottom: "3rem"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 桑基图部分 */}
|
||||||
|
<div className="visualization-container">
|
||||||
|
<h3 style={{ marginBottom: "1rem", textAlign: "center" }}>细胞类型演进流动图</h3>
|
||||||
|
<div style={{ width: "100%", height: "500px", position: "relative" }}>
|
||||||
|
{sankeyData.nodes.length > 0 && sankeyData.links.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<ResponsiveSankey
|
||||||
|
data={sankeyData}
|
||||||
|
margin={{ top: 30, right: 100, bottom: 50, left: 100 }}
|
||||||
|
align="justify"
|
||||||
|
colors={(node: any) => 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"
|
||||||
|
/>
|
||||||
|
{/* 发育阶段标签行 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "15px",
|
||||||
|
left: "100px",
|
||||||
|
right: "100px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: "500",
|
||||||
|
color: "#666"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: "left", flex: 1, paddingLeft: "20px" }}>
|
||||||
|
CS7 - 早期
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: "center", flex: 1 }}>
|
||||||
|
CS8 - 中期
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: "right", flex: 1, paddingRight: "20px" }}>
|
||||||
|
CS9 - 后期
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="no-data-message">
|
||||||
|
{sankeyLoading || loadingCS7 || loadingCS8 || loadingCS9 ? (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: "2rem", marginBottom: "1rem" }}>🔄</div>
|
||||||
|
<p>正在构建流动图...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: "2rem", marginBottom: "1rem" }}>📊</div>
|
||||||
|
<p>等待数据加载</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 基因时序分析部分 */}
|
||||||
|
<div className="visualization-container">
|
||||||
|
<h3 style={{ marginBottom: "1rem", textAlign: "center" }}>基因时序分析</h3>
|
||||||
|
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", marginBottom: "1rem" }}>
|
||||||
|
<select
|
||||||
|
value={selectedGene}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedGene(e.target.value);
|
||||||
|
if (e.target.value) {
|
||||||
|
fetchTemporalAnalysis(e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="选择基因进行时序分析"
|
||||||
|
style={{ padding: "0.5rem", borderRadius: "0.3rem", border: "1px solid #ccc", fontSize: "0.9rem" }}
|
||||||
|
>
|
||||||
|
<option value="">请选择基因</option>
|
||||||
|
{availableGenes.map((gene) => (
|
||||||
|
<option key={gene} value={gene}>
|
||||||
|
{gene}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ width: "100%", height: "450px" }}>
|
||||||
|
{temporalLoading ? (
|
||||||
|
<div className="no-data-message">
|
||||||
|
<div style={{ fontSize: "2rem", marginBottom: "1rem" }}>🔄</div>
|
||||||
|
<p>正在加载时序数据...</p>
|
||||||
|
</div>
|
||||||
|
) : temporalError ? (
|
||||||
|
<div className="error-message">{temporalError}</div>
|
||||||
|
) : temporalData.length > 0 ? (
|
||||||
|
<ResponsiveLine
|
||||||
|
data={temporalData}
|
||||||
|
margin={{ top: 30, right: 70, bottom: 70, left: 70 }}
|
||||||
|
xScale={{ type: 'point' }}
|
||||||
|
yScale={{
|
||||||
|
type: 'linear',
|
||||||
|
min: 0,
|
||||||
|
max: 'auto',
|
||||||
|
stacked: false,
|
||||||
|
reverse: false
|
||||||
|
}}
|
||||||
|
yFormat=" >-.2f"
|
||||||
|
axisTop={null}
|
||||||
|
axisRight={null}
|
||||||
|
axisBottom={{
|
||||||
|
tickSize: 5,
|
||||||
|
tickPadding: 5,
|
||||||
|
tickRotation: 0,
|
||||||
|
legend: '发育阶段',
|
||||||
|
legendPosition: 'middle',
|
||||||
|
legendOffset: 40
|
||||||
|
}}
|
||||||
|
axisLeft={{
|
||||||
|
tickSize: 5,
|
||||||
|
tickPadding: 5,
|
||||||
|
tickRotation: 0,
|
||||||
|
legend: '占比 (%)',
|
||||||
|
legendPosition: 'middle',
|
||||||
|
legendOffset: -50
|
||||||
|
}}
|
||||||
|
colors={(line: any) => line.color}
|
||||||
|
pointSize={8}
|
||||||
|
pointColor={{ theme: 'background' }}
|
||||||
|
pointBorderWidth={3}
|
||||||
|
pointBorderColor={{ from: 'serieColor' }}
|
||||||
|
pointLabelYOffset={-12}
|
||||||
|
useMesh={true}
|
||||||
|
enableGridX={false}
|
||||||
|
enableGridY={true}
|
||||||
|
lineWidth={3}
|
||||||
|
legends={[
|
||||||
|
{
|
||||||
|
anchor: 'top',
|
||||||
|
direction: 'row',
|
||||||
|
justify: false,
|
||||||
|
translateX: 0,
|
||||||
|
translateY: -30,
|
||||||
|
itemsSpacing: 45,
|
||||||
|
itemWidth: 70,
|
||||||
|
itemHeight: 16,
|
||||||
|
itemDirection: 'left-to-right',
|
||||||
|
itemOpacity: 0.75,
|
||||||
|
symbolSize: 8,
|
||||||
|
symbolShape: 'circle',
|
||||||
|
effects: [
|
||||||
|
{
|
||||||
|
on: 'hover',
|
||||||
|
style: {
|
||||||
|
itemOpacity: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="no-data-message">
|
||||||
|
<div style={{ fontSize: "2rem", marginBottom: "1rem" }}>📊</div>
|
||||||
|
<p>请选择基因查看时序变化</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>三个3D图分别展示CS7、CS8、CS9三个发育阶段的细胞类型分布,可以直观地观察细胞类型随时间的变化。</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>🌊 桑基图解读</h3>
|
||||||
|
<p>桑基图显示细胞类型的演进流动:节点大小表示占比,连接线显示相同细胞类型在不同阶段间的延续性,颜色保持一致便于追踪。</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>📊 基因时序分析</h3>
|
||||||
|
<p>选择特定基因后,堆叠面积图显示该基因在各细胞类型中的占比变化,帮助理解基因在发育过程中的表达模式演进。</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>🖱️ 交互操作</h3>
|
||||||
|
<p>在3D视图中使用鼠标拖拽旋转视角,滚轮缩放,点击数据点查看详细信息。在桑基图和堆叠图中悬停查看具体数值。</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>🎨 颜色编码</h3>
|
||||||
|
<p>相同颜色代表相同的细胞类型,在所有图表中保持一致,便于追踪特定细胞类型的变化和流动模式。</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>🔍 发育模式识别</h3>
|
||||||
|
<p>通过对比三个阶段和不同基因,可以识别细胞类型的出现、消失、迁移和分化模式,以及基因表达的动态变化。</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>📈 百分比分析</h3>
|
||||||
|
<p>桑基图和堆叠图中的百分比表示各细胞类型在对应发育阶段中的相对丰度,帮助理解细胞类型组成的动态平衡。</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>🧬 基因选择策略</h3>
|
||||||
|
<p>选择不同的发育相关基因(如SOX2、NANOG等),观察其在胚胎发育过程中的表达变化模式和细胞类型特异性。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemporalAnalysis;
|
||||||
@ -60,8 +60,9 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-container {
|
.nav-container {
|
||||||
max-width: 1200px;
|
/* max-width: 1200px; */
|
||||||
margin: 0 auto;
|
/* margin: 0 auto; */
|
||||||
|
width: 100%;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user