新增.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)
# 参数配置
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"]
layers = {
"Ectoderm": 1.0, # 外层
"Mesoderm": 0.85, # 中层
"Endoderm": 0.7, # 内层
"Ectoderm": 0.1, # 外层
"Mesoderm": 0.085, # 中层
"Endoderm": 0.07, # 内层
}
notochord_ratio = 0.05 # 脊索/原条占总细胞数的比例
@ -21,17 +21,23 @@ script_path = os.path.dirname(os.path.realpath(__file__))
for stage, total_cells in stages:
n_noto = int(total_cells * notochord_ratio)
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 = []
cell_types = []
ids = []
# 椭球比例参数
a, b, c = 10, 8, 5
a, b, c = 1, 0.8, 0.5
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)
theta = np.random.uniform(0, 2*np.pi, N)
r = scale # 层的半径比例
@ -46,9 +52,9 @@ for stage, total_cells in stages:
ids.append(f"{stage}_{layer_name}_{len(ids)}")
# 添加脊索/原条样结构Z轴中间偏下的细胞
x = np.random.normal(0, 0.5, n_noto)
y = np.random.normal(0, 0.5, n_noto)
z = np.linspace(-3, 3, n_noto)
x = np.random.normal(0, 0.01, n_noto)
y = np.random.normal(0, 0.01, n_noto)
z = np.linspace(-0.1, 0.1, n_noto)
for xi, yi, zi in zip(x, y, z):
positions.append([xi, yi, zi])

View File

@ -258,10 +258,10 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
// 计算所有点的包围盒,自动居中和缩放
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); });
cs7Data.forEach(p => { allPositions.push(p.x - 0.5, p.y, p.z); });
cs8Data.forEach(p => { allPositions.push(p.x + 0.5,p.y, p.z); });
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(p.x, p.y, p.z);
});
@ -285,7 +285,7 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
const positions: number[] = [];
const colors: number[] = [];
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');
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));
const material = new THREE.PointsMaterial({
vertexColors: true,
size: 0.8,
size: 0.4,
sizeAttenuation: true,
transparent: true,
alphaTest: 0.5,
@ -327,7 +327,7 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
const positions: number[] = [];
const colors: number[] = [];
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');
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));
const material = new THREE.PointsMaterial({
vertexColors: true,
size: 0.8,
size: 0.4,
sizeAttenuation: true,
transparent: true,
alphaTest: 0.5,
@ -408,7 +408,7 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
const sourcePoint = cs7PointsOfType.length > 0
? cs7PointsOfType[sourceIndex]
: cs7Data[i % cs7Data.length];
fromX = sourcePoint.x - 40;
fromX = sourcePoint.x - 0.5;
fromY = sourcePoint.y;
fromZ = sourcePoint.z;
} else {
@ -418,7 +418,7 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
const sourcePoint = cs8PointsOfType.length > 0
? cs8PointsOfType[sourceIndex]
: cs8Data[i % cs8Data.length];
fromX = sourcePoint.x + 40;
fromX = sourcePoint.x + 0.5;
fromY = sourcePoint.y;
fromZ = sourcePoint.z;
}
@ -466,7 +466,7 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ cs7Data
vAlpha = alpha;
vColor = color;
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 allCellTypes = new Set<string>();
// 收集所有细胞类型
stagesData.forEach((stageData: any) => {
Object.keys(stageData.cell_types).forEach(cellType => {
@ -78,7 +78,7 @@ const TemporalAnalysis: React.FC = () => {
const cellTypesArray = Array.from(allCellTypes).sort();
const colorMap = generateCellTypeColors(cellTypesArray);
// 构建线图数据 - 每个细胞类型一条线
const lineData = cellTypesArray.map(cellType => {
const linePoints = stagesData.map((stageData: any) => {
@ -112,7 +112,7 @@ const TemporalAnalysis: React.FC = () => {
// 构建桑基图数据
const buildSankeyData = (dataCS7: any[], dataCS8: any[], dataCS9: any[]) => {
setSankeyLoading(true);
try {
// 统计各阶段的细胞类型数量
const countCellTypes = (data: any[]) => {
@ -152,7 +152,7 @@ const TemporalAnalysis: React.FC = () => {
const nodes: any[] = [];
cellTypeArray.forEach(cellType => {
const color = colorMap.get(cellType) || '#CCCCCC';
// CS7 节点
if (countsCS7[cellType]) {
const percentage = ((countsCS7[cellType] / totalCS7) * 100).toFixed(1);
@ -184,39 +184,79 @@ const TemporalAnalysis: React.FC = () => {
}
});
// 构建连接
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' // 添加透明度
});
}
// 构建连接:使用最小百分比作为同类连线,并将剩余部分匹配到有缺口的目标细胞类型,保证各阶段总量守恒
const buildBalancedLinks = (
sourceCounts: Record<string, number>,
sourceTotal: number,
targetCounts: Record<string, number>,
targetTotal: number,
sourceStage: string,
targetStage: string
) => {
const types = Array.from(
new Set([
...Object.keys(sourceCounts),
...Object.keys(targetCounts)
])
);
// 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' // 添加透明度
const sourcePct: Record<string, number> = {};
const targetPct: Record<string, number> = {};
types.forEach((t) => {
sourcePct[t] = ((sourceCounts[t] || 0) / sourceTotal) * 100;
targetPct[t] = ((targetCounts[t] || 0) / targetTotal) * 100;
});
const flows: any[] = [];
// 同类对齐:取最小值
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 });
} catch (error) {
@ -326,7 +366,7 @@ const TemporalAnalysis: React.FC = () => {
The three 3D views below show cell type distributions for CS7, CS8, and CS9.
Compare spatial distributions to observe changes over development.
</p>
<div
className="three-column-grid visualization-grid"
style={{
@ -425,7 +465,7 @@ const TemporalAnalysis: React.FC = () => {
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.
</p>
<div
style={{
display: "grid",
@ -663,4 +703,4 @@ const TemporalAnalysis: React.FC = () => {
);
};
export default TemporalAnalysis;
export default TemporalAnalysis;

View File

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