新增基因时序分析API,前端实现时间序列分析页面,支持基因选择和细胞类型流动可视化,优化用户交互体验。

This commit is contained in:
wjsjwr 2025-07-26 19:30:04 +08:00
parent 93e0eae353
commit 73e83da5fe
6 changed files with 933 additions and 0 deletions

View File

@ -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)

View File

@ -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",

View File

@ -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",

View File

@ -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() {
<Route path="/" element={<HomePage />} />
<Route path="/gene-view" element={<GeneView />} />
<Route path="/spatial-clustering" element={<SpatialClustering />} />
<Route path="/temporal-analysis" element={<TemporalAnalysis />} />
<Route path="/resource" element={<Resource />} />
<Route path="/download" element={<Download />} />
</Routes>

View File

@ -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: "下载" },
];

View 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">
CS7CS8CS9三个发育阶段的细胞类型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">
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "1rem" }}>
<h3 style={{ margin: 0 }}></h3>
<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: '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
}
}
]
}
]}
/>
) : (
<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图分别展示CS7CS8CS9三个发育阶段的细胞类型分布</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>SOX2NANOG等</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default TemporalAnalysis;