Add legend
This commit is contained in:
parent
699bf4c62e
commit
537926d8d8
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||||
@ -34,10 +34,23 @@ interface AnimationSectionConfig {
|
|||||||
};
|
};
|
||||||
layout: LayoutConfig;
|
layout: LayoutConfig;
|
||||||
}
|
}
|
||||||
|
const CELL_COLORS = [
|
||||||
|
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
|
||||||
|
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9',
|
||||||
|
'#F8C471', '#82E0AA', '#F1948A', '#85CDCE', '#D7BDE2',
|
||||||
|
'#A9DFBF', '#F9E79F', '#D5A6BD', '#AED6F1', '#E8DAEF',
|
||||||
|
'#FF9A8B', '#FF6CAB', '#C56CF0', '#6C5CE7', '#81ECEC',
|
||||||
|
'#74B9FF', '#A8E6CF', '#DCEDC1', '#FFD3B6', '#FFAAA5',
|
||||||
|
'#FFC3A0', '#FF677D', '#B2EBF2', '#80CBC4', '#C5E1A5',
|
||||||
|
'#FFF176', '#FFD54F', '#FFB347', '#B39DDB', '#9FA8DA',
|
||||||
|
'#90CAF9', '#64B5F6', '#4DD0E1', '#4DB6AC', '#81C784',
|
||||||
|
'#A5D6A7', '#E6EE9C', '#DCE775', '#FF8A65', '#F06292'
|
||||||
|
];
|
||||||
|
|
||||||
const ANIMATION_SECTIONS: AnimationSectionConfig[] = [
|
const ANIMATION_SECTIONS: AnimationSectionConfig[] = [
|
||||||
{
|
{
|
||||||
id: "human-cs",
|
id: "human-cs",
|
||||||
title: "Human Carnegie stages",
|
title: "Human gastrulation",
|
||||||
description: "CS7 and CS8 point clouds converge into CS7.5 to highlight human embryonic development.",
|
description: "CS7 and CS8 point clouds converge into CS7.5 to highlight human embryonic development.",
|
||||||
stages: {
|
stages: {
|
||||||
sourceLeft: "CS7",
|
sourceLeft: "CS7",
|
||||||
@ -59,7 +72,7 @@ const ANIMATION_SECTIONS: AnimationSectionConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "mouse-early",
|
id: "mouse-early",
|
||||||
title: "Mouse early gastrulation",
|
title: "Mouse organogenesis",
|
||||||
description: "Mouse 7.5 and 7.75 datasets merge toward stage 8.5, mirroring the human pipeline.",
|
description: "Mouse 7.5 and 7.75 datasets merge toward stage 8.5, mirroring the human pipeline.",
|
||||||
stages: {
|
stages: {
|
||||||
// sourceLeft: "GSM9046243_Embryo_E7.5_stereo_rep1",
|
// sourceLeft: "GSM9046243_Embryo_E7.5_stereo_rep1",
|
||||||
@ -98,7 +111,7 @@ const EmbryoGeneration: React.FC = () => {
|
|||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h1>Embryo Generation</h1>
|
<h1>Embryo Generation</h1>
|
||||||
<p className="page-description">
|
<p className="page-description">
|
||||||
Observe cell migration and merging across human CS7/CS8/CS7.5 and mouse 7.5/7.75/8.5 datasets with the same control workflow.
|
Observe cell migration and merging across human CS7/CS8/CS7.5 and mouse 7.5/7.75/8.0 datasets with the same control workflow.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="page-content">
|
<div className="page-content">
|
||||||
@ -136,7 +149,7 @@ interface EmbryoAnimationSectionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EmbryoAnimationSection: React.FC<EmbryoAnimationSectionProps> = ({ config }) => {
|
const EmbryoAnimationSection: React.FC<EmbryoAnimationSectionProps> = ({ config }) => {
|
||||||
const { stages, title, description, layout } = config;
|
const { stages, title, layout } = config;
|
||||||
const { sourceLeft, sourceRight, target } = stages;
|
const { sourceLeft, sourceRight, target } = stages;
|
||||||
const [sourceLeftData, setSourceLeftData] = useState<Point[]>([]);
|
const [sourceLeftData, setSourceLeftData] = useState<Point[]>([]);
|
||||||
const [sourceRightData, setSourceRightData] = useState<Point[]>([]);
|
const [sourceRightData, setSourceRightData] = useState<Point[]>([]);
|
||||||
@ -206,15 +219,14 @@ const EmbryoAnimationSection: React.FC<EmbryoAnimationSectionProps> = ({ config
|
|||||||
setAnimationProgress(0);
|
setAnimationProgress(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const datasetLabel = `${sourceLeft} + ${sourceRight} -> ${target}`;
|
|
||||||
const progressPercent = Math.round(animationProgress * 100);
|
const progressPercent = Math.round(animationProgress * 100);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="section" style={{ marginBottom: "3rem" }}>
|
<section className="section" style={{ marginBottom: "3rem" }}>
|
||||||
<div style={{ marginBottom: "1.5rem" }}>
|
<div style={{ marginBottom: "1.5rem" }}>
|
||||||
<h2 style={{ marginBottom: "0.5rem" }}>{title}</h2>
|
<h2 style={{ marginBottom: "0.5rem" }}>{title}</h2>
|
||||||
<p className="page-description" style={{ marginBottom: "0.25rem" }}>{description}</p>
|
{/* <p className="page-description" style={{ marginBottom: "0.25rem" }}>{description}</p>
|
||||||
<p style={{ color: "var(--text-secondary)", margin: 0 }}>Dataset: {datasetLabel}</p>
|
<p style={{ color: "var(--text-secondary)", margin: 0 }}>Dataset: {datasetLabel}</p> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="control-panel" style={{
|
<div className="control-panel" style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -324,7 +336,7 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ sourceL
|
|||||||
const animationIdRef = useRef<number | null>(null);
|
const animationIdRef = useRef<number | null>(null);
|
||||||
const animationStateRef = useRef<TargetAnimationState | null>(null);
|
const animationStateRef = useRef<TargetAnimationState | null>(null);
|
||||||
|
|
||||||
const { leftOffsetX, rightOffsetX, targetOffsetX = 0, cameraParams, transFunc = null } = layout;
|
const { leftOffsetX, rightOffsetX, targetOffsetX = 0, cameraParams, transFunc } = layout;
|
||||||
|
|
||||||
if (transFunc) {
|
if (transFunc) {
|
||||||
for (let i = 0; i < sourceLeftData.length; i++) {
|
for (let i = 0; i < sourceLeftData.length; i++) {
|
||||||
@ -339,17 +351,20 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ sourceL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const legendEntries = useMemo(() => {
|
||||||
|
const allData = [...sourceLeftData, ...sourceRightData, ...targetData];
|
||||||
|
const cellTypes = Array.from(new Set(allData.map(p => p.value as string))).sort();
|
||||||
|
return cellTypes.map((type, idx) => ({
|
||||||
|
type,
|
||||||
|
color: CELL_COLORS[idx % CELL_COLORS.length]
|
||||||
|
}));
|
||||||
|
}, [sourceLeftData, sourceRightData, targetData]);
|
||||||
|
|
||||||
const getColorMap = (allData: Point[]) => {
|
const getColorMap = (allData: Point[]) => {
|
||||||
const cellTypes = Array.from(new Set(allData.map(p => p.value as string))).sort();
|
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>();
|
const colorMap = new Map<string, THREE.Color>();
|
||||||
cellTypes.forEach((type, idx) => {
|
cellTypes.forEach((type, idx) => {
|
||||||
colorMap.set(type, new THREE.Color(colors[idx % colors.length]));
|
colorMap.set(type, new THREE.Color(CELL_COLORS[idx % CELL_COLORS.length]));
|
||||||
});
|
});
|
||||||
return colorMap;
|
return colorMap;
|
||||||
};
|
};
|
||||||
@ -409,7 +424,7 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ sourceL
|
|||||||
sceneRef.current = scene;
|
sceneRef.current = scene;
|
||||||
const camera = new THREE.PerspectiveCamera(cameraParams.fov, width / height, cameraParams.near, cameraParams.far);
|
const camera = new THREE.PerspectiveCamera(cameraParams.fov, width / height, cameraParams.near, cameraParams.far);
|
||||||
camera.position.set(cameraParams.position.x, cameraParams.position.y, cameraParams.position.z);
|
camera.position.set(cameraParams.position.x, cameraParams.position.y, cameraParams.position.z);
|
||||||
camera.lookAt(0, 0, 0);
|
camera.lookAt(cameraParams.lookAt);
|
||||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
renderer.setSize(width, height);
|
renderer.setSize(width, height);
|
||||||
rendererRef.current = renderer;
|
rendererRef.current = renderer;
|
||||||
@ -559,10 +574,6 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ sourceL
|
|||||||
const toBaseY = to.y;
|
const toBaseY = to.y;
|
||||||
const toBaseZ = to.z;
|
const toBaseZ = to.z;
|
||||||
|
|
||||||
if (i < 5) {
|
|
||||||
console.log(`Point ${i}: from (${fromX.toFixed(2)}, ${fromY.toFixed(2)}, ${fromZ.toFixed(2)}) to (${toBaseX.toFixed(2)}, ${toBaseY.toFixed(2)}, ${toBaseZ.toFixed(2)})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
fromPositions[fromBaseIndex] = fromX;
|
fromPositions[fromBaseIndex] = fromX;
|
||||||
fromPositions[fromBaseIndex + 1] = fromY;
|
fromPositions[fromBaseIndex + 1] = fromY;
|
||||||
fromPositions[fromBaseIndex + 2] = fromZ;
|
fromPositions[fromBaseIndex + 2] = fromZ;
|
||||||
@ -700,7 +711,52 @@ const UnifiedEmbryoAnimation: React.FC<UnifiedEmbryoAnimationProps> = ({ sourceL
|
|||||||
applyAnimationProgress(animationProgress);
|
applyAnimationProgress(animationProgress);
|
||||||
}, [animationProgress, applyAnimationProgress]);
|
}, [animationProgress, applyAnimationProgress]);
|
||||||
|
|
||||||
return <div ref={containerRef} className="point-cloud-container" style={{ width: "100%", height: "700px", minHeight: 600 }} />;
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||||
|
<div ref={containerRef} className="point-cloud-container" style={{ width: "100%", height: "700px", minHeight: 600 }} />
|
||||||
|
{legendEntries.length > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--bg-secondary)",
|
||||||
|
color: "var(--text-primary)",
|
||||||
|
padding: "1rem",
|
||||||
|
borderRadius: "8px",
|
||||||
|
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
||||||
|
fontSize: "0.85rem"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontWeight: 600, marginBottom: "0.5rem" }}>Legend</div>
|
||||||
|
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.4rem" }}>
|
||||||
|
{legendEntries.map((entry) => (
|
||||||
|
<span
|
||||||
|
key={entry.type}
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.3rem",
|
||||||
|
padding: "0.3rem 0.5rem",
|
||||||
|
borderRadius: "999px",
|
||||||
|
backgroundColor: "var(--bg-tertiary)",
|
||||||
|
lineHeight: 1.2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: "10px",
|
||||||
|
height: "10px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: entry.color,
|
||||||
|
flexShrink: 0
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>{entry.type}</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建纹理贴图(与PointsMaterial相同)
|
// 创建纹理贴图(与PointsMaterial相同)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user