新增.hintrc配置文件,更新多个h5ad文件的大小和哈希值,修改FakeEmbryo.py中的细胞层参数,调整前端页面中细胞位置和大小的计算,优化TemporalAnalysis.tsx中的数据连接逻辑以确保细胞类型流动的平衡性。

This commit is contained in:
wjsjwr 2025-08-11 22:08:19 +08:00
parent 801700cd3d
commit c3e965b535
9 changed files with 120 additions and 65 deletions

8
.hintrc Normal file
View File

@ -0,0 +1,8 @@
{
"extends": [
"development"
],
"hints": {
"no-inline-styles": "off"
}
}

BIN
embryo-backend/Data/CS7.5.h5ad (Stored with Git LFS)

Binary file not shown.

BIN
embryo-backend/Data/CS7.h5ad (Stored with Git LFS)

Binary file not shown.

BIN
embryo-backend/Data/CS8.h5ad (Stored with Git LFS)

Binary file not shown.

BIN
embryo-backend/Data/CS9.h5ad (Stored with Git LFS)

Binary file not shown.

View File

@ -6,12 +6,12 @@ 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": 0.1, # 外层
"Mesoderm": 0.85, # 中层 "Mesoderm": 0.085, # 中层
"Endoderm": 0.7, # 内层 "Endoderm": 0.07, # 内层
} }
notochord_ratio = 0.05 # 脊索/原条占总细胞数的比例 notochord_ratio = 0.05 # 脊索/原条占总细胞数的比例
@ -21,17 +21,23 @@ script_path = os.path.dirname(os.path.realpath(__file__))
for stage, total_cells in stages: for stage, total_cells in stages:
n_noto = int(total_cells * notochord_ratio) n_noto = int(total_cells * notochord_ratio)
n_layer = total_cells - n_noto n_layer = total_cells - n_noto
n_per_layer = n_layer // len(layers) num_layers = len(layers)
# 使用 Dirichlet 生成随机且不相等的概率,再用多项分布采样;若计数仍相等则重采样
for _ in range(100):
probs = np.random.dirichlet(np.ones(num_layers))
n_per_layer = np.random.multinomial(n_layer, probs)
if len(set(n_per_layer)) == num_layers:
break
positions = [] positions = []
cell_types = [] cell_types = []
ids = [] ids = []
# 椭球比例参数 # 椭球比例参数
a, b, c = 10, 8, 5 a, b, c = 1, 0.8, 0.5
for i, (layer_name, scale) in enumerate(layers.items()): for i, (layer_name, scale) in enumerate(layers.items()):
N = n_per_layer N = n_per_layer[i]
phi = np.random.uniform(0, np.pi, N) phi = np.random.uniform(0, np.pi, N)
theta = np.random.uniform(0, 2*np.pi, N) theta = np.random.uniform(0, 2*np.pi, N)
r = scale # 层的半径比例 r = scale # 层的半径比例
@ -46,9 +52,9 @@ for stage, total_cells in stages:
ids.append(f"{stage}_{layer_name}_{len(ids)}") ids.append(f"{stage}_{layer_name}_{len(ids)}")
# 添加脊索/原条样结构Z轴中间偏下的细胞 # 添加脊索/原条样结构Z轴中间偏下的细胞
x = np.random.normal(0, 0.5, n_noto) x = np.random.normal(0, 0.01, n_noto)
y = np.random.normal(0, 0.5, n_noto) y = np.random.normal(0, 0.01, n_noto)
z = np.linspace(-3, 3, n_noto) z = np.linspace(-0.1, 0.1, n_noto)
for xi, yi, zi in zip(x, y, z): for xi, yi, zi in zip(x, y, z):
positions.append([xi, yi, zi]) positions.append([xi, yi, zi])

View File

@ -258,10 +258,10 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
// 计算所有点的包围盒,自动居中和缩放 // 计算所有点的包围盒,自动居中和缩放
let allPositions: number[] = []; let allPositions: number[] = [];
cs7Data.forEach(p => { allPositions.push(p.x - 40, p.y, p.z); }); cs7Data.forEach(p => { allPositions.push(p.x - 0.5, p.y, p.z); });
cs8Data.forEach(p => { allPositions.push(p.x + 40, p.y, p.z); }); cs8Data.forEach(p => { allPositions.push(p.x + 0.5,p.y, p.z); });
cs75Data.forEach((p, i) => { cs75Data.forEach((p, i) => {
const fromX = i % 2 === 0 ? p.x - 40 : p.x + 40; const fromX = i % 2 === 0 ? p.x - 0.5 : p.x + 0.5;
allPositions.push(fromX, p.y, p.z); allPositions.push(fromX, p.y, p.z);
allPositions.push(p.x, p.y, p.z); allPositions.push(p.x, p.y, p.z);
}); });
@ -285,7 +285,7 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
const positions: number[] = []; const positions: number[] = [];
const colors: number[] = []; const colors: number[] = [];
cs7Data.forEach((p) => { cs7Data.forEach((p) => {
positions.push(p.x - 40, p.y, p.z); positions.push(p.x - 0.5, p.y, p.z);
const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC'); const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC');
colors.push(c.r, c.g, c.b); colors.push(c.r, c.g, c.b);
}); });
@ -293,7 +293,7 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({ const material = new THREE.PointsMaterial({
vertexColors: true, vertexColors: true,
size: 0.8, size: 0.4,
sizeAttenuation: true, sizeAttenuation: true,
transparent: true, transparent: true,
alphaTest: 0.5, alphaTest: 0.5,
@ -327,7 +327,7 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
const positions: number[] = []; const positions: number[] = [];
const colors: number[] = []; const colors: number[] = [];
cs8Data.forEach((p) => { cs8Data.forEach((p) => {
positions.push(p.x + 40, p.y, p.z); positions.push(p.x + 0.5, p.y, p.z);
const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC'); const c = colorMap.get(p.value as string) || new THREE.Color('#CCCCCC');
colors.push(c.r, c.g, c.b); colors.push(c.r, c.g, c.b);
}); });
@ -335,7 +335,7 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({ const material = new THREE.PointsMaterial({
vertexColors: true, vertexColors: true,
size: 0.8, size: 0.4,
sizeAttenuation: true, sizeAttenuation: true,
transparent: true, transparent: true,
alphaTest: 0.5, alphaTest: 0.5,
@ -408,7 +408,7 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
const sourcePoint = cs7PointsOfType.length > 0 const sourcePoint = cs7PointsOfType.length > 0
? cs7PointsOfType[sourceIndex] ? cs7PointsOfType[sourceIndex]
: cs7Data[i % cs7Data.length]; : cs7Data[i % cs7Data.length];
fromX = sourcePoint.x - 40; fromX = sourcePoint.x - 0.5;
fromY = sourcePoint.y; fromY = sourcePoint.y;
fromZ = sourcePoint.z; fromZ = sourcePoint.z;
} else { } else {
@ -418,7 +418,7 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
const sourcePoint = cs8PointsOfType.length > 0 const sourcePoint = cs8PointsOfType.length > 0
? cs8PointsOfType[sourceIndex] ? cs8PointsOfType[sourceIndex]
: cs8Data[i % cs8Data.length]; : cs8Data[i % cs8Data.length];
fromX = sourcePoint.x + 40; fromX = sourcePoint.x + 0.5;
fromY = sourcePoint.y; fromY = sourcePoint.y;
fromZ = sourcePoint.z; fromZ = sourcePoint.z;
} }
@ -466,7 +466,7 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
vAlpha = alpha; vAlpha = alpha;
vColor = color; vColor = color;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
gl_PointSize = 10.0; gl_PointSize = 5.0;
} }
`; `;

View File

@ -68,7 +68,7 @@ const TemporalAnalysis: React.FC = () => {
// 转换数据为线图格式 // 转换数据为线图格式
const stagesData = response.data.stages_data; const stagesData = response.data.stages_data;
const allCellTypes = new Set<string>(); const allCellTypes = new Set<string>();
// 收集所有细胞类型 // 收集所有细胞类型
stagesData.forEach((stageData: any) => { stagesData.forEach((stageData: any) => {
Object.keys(stageData.cell_types).forEach(cellType => { Object.keys(stageData.cell_types).forEach(cellType => {
@ -78,7 +78,7 @@ const TemporalAnalysis: React.FC = () => {
const cellTypesArray = Array.from(allCellTypes).sort(); const cellTypesArray = Array.from(allCellTypes).sort();
const colorMap = generateCellTypeColors(cellTypesArray); const colorMap = generateCellTypeColors(cellTypesArray);
// 构建线图数据 - 每个细胞类型一条线 // 构建线图数据 - 每个细胞类型一条线
const lineData = cellTypesArray.map(cellType => { const lineData = cellTypesArray.map(cellType => {
const linePoints = stagesData.map((stageData: any) => { const linePoints = stagesData.map((stageData: any) => {
@ -112,7 +112,7 @@ const TemporalAnalysis: React.FC = () => {
// 构建桑基图数据 // 构建桑基图数据
const buildSankeyData = (dataCS7: any[], dataCS8: any[], dataCS9: any[]) => { const buildSankeyData = (dataCS7: any[], dataCS8: any[], dataCS9: any[]) => {
setSankeyLoading(true); setSankeyLoading(true);
try { try {
// 统计各阶段的细胞类型数量 // 统计各阶段的细胞类型数量
const countCellTypes = (data: any[]) => { const countCellTypes = (data: any[]) => {
@ -152,7 +152,7 @@ const TemporalAnalysis: React.FC = () => {
const nodes: any[] = []; const nodes: any[] = [];
cellTypeArray.forEach(cellType => { cellTypeArray.forEach(cellType => {
const color = colorMap.get(cellType) || '#CCCCCC'; const color = colorMap.get(cellType) || '#CCCCCC';
// CS7 节点 // CS7 节点
if (countsCS7[cellType]) { if (countsCS7[cellType]) {
const percentage = ((countsCS7[cellType] / totalCS7) * 100).toFixed(1); const percentage = ((countsCS7[cellType] / totalCS7) * 100).toFixed(1);
@ -184,39 +184,79 @@ const TemporalAnalysis: React.FC = () => {
} }
}); });
// 构建连接 // 构建连接:使用最小百分比作为同类连线,并将剩余部分匹配到有缺口的目标细胞类型,保证各阶段总量守恒
const links: any[] = []; const buildBalancedLinks = (
cellTypeArray.forEach(cellType => { sourceCounts: Record<string, number>,
const color = colorMap.get(cellType) || '#CCCCCC'; sourceTotal: number,
targetCounts: Record<string, number>,
// CS7 -> CS8 连接 targetTotal: number,
if (countsCS7[cellType] && countsCS8[cellType]) { sourceStage: string,
const cs7Percentage = (countsCS7[cellType] / totalCS7) * 100; targetStage: string
const cs8Percentage = (countsCS8[cellType] / totalCS8) * 100; ) => {
const avgPercentage = (cs7Percentage + cs8Percentage) / 2; const types = Array.from(
new Set([
links.push({ ...Object.keys(sourceCounts),
source: `CS7_${cellType}`, ...Object.keys(targetCounts)
target: `CS8_${cellType}`, ])
value: Math.max(avgPercentage, 0.1), // 确保有最小值以显示连接 );
color: color + '80' // 添加透明度
});
}
// CS8 -> CS9 连接 const sourcePct: Record<string, number> = {};
if (countsCS8[cellType] && countsCS9[cellType]) { const targetPct: Record<string, number> = {};
const cs8Percentage = (countsCS8[cellType] / totalCS8) * 100; types.forEach((t) => {
const cs9Percentage = (countsCS9[cellType] / totalCS9) * 100; sourcePct[t] = ((sourceCounts[t] || 0) / sourceTotal) * 100;
const avgPercentage = (cs8Percentage + cs9Percentage) / 2; targetPct[t] = ((targetCounts[t] || 0) / targetTotal) * 100;
});
links.push({
source: `CS8_${cellType}`, const flows: any[] = [];
target: `CS9_${cellType}`,
value: Math.max(avgPercentage, 0.1), // 确保有最小值以显示连接 // 同类对齐:取最小值
color: color + '80' // 添加透明度 const sourceResidual: Record<string, number> = {};
const targetResidual: Record<string, number> = {};
types.forEach((t) => {
const direct = Math.min(sourcePct[t], targetPct[t]);
if (direct > 0) {
const color = (colorMap.get(t) || '#CCCCCC') + '80';
flows.push({
source: `${sourceStage}_${t}`,
target: `${targetStage}_${t}`,
value: direct,
color
});
}
sourceResidual[t] = sourcePct[t] - direct; // >= 0
targetResidual[t] = targetPct[t] - direct; // >= 0
});
// 将源阶段的剩余按目标阶段的缺口比例分配
types.forEach((sType) => {
let remaining = sourceResidual[sType];
if (remaining <= 0) return;
// 计算当前目标缺口总和
let totalDeficit = types.reduce((sum, t) => sum + Math.max(targetResidual[t], 0), 0);
if (totalDeficit <= 0) return;
types.forEach((dType) => {
if (targetResidual[dType] <= 0) return;
const share = (remaining * targetResidual[dType]) / totalDeficit;
if (share > 1e-6) {
const color = (colorMap.get(sType) || '#CCCCCC') + '80';
flows.push({
source: `${sourceStage}_${sType}`,
target: `${targetStage}_${dType}`,
value: share,
color
});
targetResidual[dType] -= share;
}
}); });
} });
});
return flows;
};
const links: any[] = [
...buildBalancedLinks(countsCS7, totalCS7, countsCS8, totalCS8, 'CS7', 'CS8'),
...buildBalancedLinks(countsCS8, totalCS8, countsCS9, totalCS9, 'CS8', 'CS9')
];
setSankeyData({ nodes, links }); setSankeyData({ nodes, links });
} catch (error) { } catch (error) {
@ -326,7 +366,7 @@ const TemporalAnalysis: React.FC = () => {
The three 3D views below show cell type distributions for CS7, CS8, and CS9. The three 3D views below show cell type distributions for CS7, CS8, and CS9.
Compare spatial distributions to observe changes over development. Compare spatial distributions to observe changes over development.
</p> </p>
<div <div
className="three-column-grid visualization-grid" className="three-column-grid visualization-grid"
style={{ style={{
@ -425,7 +465,7 @@ const TemporalAnalysis: React.FC = () => {
The Sankey diagram (left) shows cell type flows across stages; the line chart (right) The Sankey diagram (left) shows cell type flows across stages; the line chart (right)
shows the proportion changes of the selected gene across cell types. shows the proportion changes of the selected gene across cell types.
</p> </p>
<div <div
style={{ style={{
display: "grid", display: "grid",
@ -663,4 +703,4 @@ const TemporalAnalysis: React.FC = () => {
); );
}; };
export default TemporalAnalysis; export default TemporalAnalysis;

View File

@ -6,6 +6,7 @@
"module": "ESNext", "module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"], "lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",