Spaces:
Running
Running
Re-designed UI
Browse files- app.py +114 -51
- assets/interactions.svg +0 -0
- assets/spMetaTME_brand.npy +3 -0
- assets/style.css +3 -2
- requirements.txt +0 -1
- src/backend/flux_analysis.py +0 -40
- src/backend/preprocessing.py +0 -43
- src/ui/components/header.py +28 -41
- src/ui/pages/{visualization.py → analyze.py} +34 -59
- src/ui/pages/flux_analysis.py +0 -31
- src/ui/pages/{overview.py → home.py} +28 -6
- src/ui/pages/preprocessing.py +0 -41
- src/ui/plots/domain_statistics.py +2 -10
- src/ui/plots/metabolic_interactions.py +32 -2
- src/ui/plots/spatial_flux_map.py +0 -12
app.py
CHANGED
|
@@ -1,10 +1,38 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
import logging
|
|
|
|
|
|
|
| 3 |
|
| 4 |
# Configure Logging
|
| 5 |
logging.basicConfig(level=logging.INFO)
|
| 6 |
logger = logging.getLogger(__name__)
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
def init_session_state():
|
| 9 |
"""Initialise global session state."""
|
| 10 |
if "adata" not in st.session_state:
|
|
@@ -34,6 +62,53 @@ def init_session_state():
|
|
| 34 |
if "dev_mode" not in st.session_state:
|
| 35 |
st.session_state.dev_mode = True
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
def render_sidebar_dev():
|
| 38 |
"""Developer shortcuts in sidebar."""
|
| 39 |
with st.sidebar:
|
|
@@ -44,7 +119,6 @@ def render_sidebar_dev():
|
|
| 44 |
st.info("Dev Shortcuts Active")
|
| 45 |
if st.button("Load Breast Cancer Block A", use_container_width=True):
|
| 46 |
with st.spinner("Loading example data..."):
|
| 47 |
-
# Clear interaction cache for new tissue
|
| 48 |
for key in ['interaction_scores', 'interaction_type']:
|
| 49 |
if key in st.session_state:
|
| 50 |
del st.session_state[key]
|
|
@@ -53,66 +127,55 @@ def render_sidebar_dev():
|
|
| 53 |
if adata is not None:
|
| 54 |
st.session_state.metabolic_adata = adata
|
| 55 |
st.session_state.data_type = "metabolic"
|
| 56 |
-
|
| 57 |
if 'domain' not in adata.obs.columns and 'domain_id' in adata.obs.columns:
|
| 58 |
adata.obs['domain'] = adata.obs['domain_id']
|
| 59 |
-
st.success("Loaded Breast Cancer Block A
|
| 60 |
st.rerun()
|
| 61 |
|
| 62 |
-
# Import UI Components and Pages (after session state init)
|
| 63 |
-
from src.ui.components.header import render_header, load_css
|
| 64 |
-
from src.ui.components.footer import render_footer
|
| 65 |
-
|
| 66 |
-
from src.ui.pages.overview import show_overview
|
| 67 |
-
from src.ui.pages.visualization import show_visualization
|
| 68 |
-
from src.ui.pages.preprocessing import show_preprocessing
|
| 69 |
-
from src.ui.pages.flux_analysis import show_flux_analysis
|
| 70 |
-
|
| 71 |
def main():
|
| 72 |
-
|
| 73 |
init_session_state()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
current_page = "overview"
|
| 85 |
-
|
| 86 |
-
# # Set page layout based on current page
|
| 87 |
-
# if current_page == "overview":
|
| 88 |
-
# layout = "wide"
|
| 89 |
-
# else:
|
| 90 |
-
# layout = "centered"
|
| 91 |
-
|
| 92 |
-
layout = "wide"
|
| 93 |
-
# Configure page with appropriate layout
|
| 94 |
-
st.set_page_config(
|
| 95 |
-
page_title="spMetaTME Atlas",
|
| 96 |
-
page_icon=":material/hub:",
|
| 97 |
-
layout=layout,
|
| 98 |
-
initial_sidebar_state="expanded",
|
| 99 |
-
)
|
| 100 |
-
|
| 101 |
-
# render_sidebar_dev()
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
show_visualization()
|
| 106 |
-
elif current_page == "preprocessing":
|
| 107 |
-
show_preprocessing()
|
| 108 |
-
elif current_page == "flux_analysis":
|
| 109 |
-
show_flux_analysis()
|
| 110 |
-
else:
|
| 111 |
render_header()
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
|
|
|
|
|
|
| 116 |
|
| 117 |
if __name__ == "__main__":
|
| 118 |
main()
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
import logging
|
| 3 |
+
import numpy as np
|
| 4 |
+
import os
|
| 5 |
|
| 6 |
# Configure Logging
|
| 7 |
logging.basicConfig(level=logging.INFO)
|
| 8 |
logger = logging.getLogger(__name__)
|
| 9 |
|
| 10 |
+
|
| 11 |
+
# Page configuration
|
| 12 |
+
sidebar_state = "expanded"
|
| 13 |
+
st.set_page_config(
|
| 14 |
+
page_title="spMetaTME-Atlas",
|
| 15 |
+
page_icon=":material/hub:",
|
| 16 |
+
layout="wide",
|
| 17 |
+
initial_sidebar_state=sidebar_state,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# Import UI Components and Pages
|
| 22 |
+
from src.ui.components.header import render_header, load_css
|
| 23 |
+
from src.ui.components.footer import render_footer
|
| 24 |
+
|
| 25 |
+
from src.ui.pages.home import show_overview
|
| 26 |
+
from src.ui.pages.analyze import (
|
| 27 |
+
page_domain_statistics,
|
| 28 |
+
page_spatial_flux,
|
| 29 |
+
page_umap_analysis,
|
| 30 |
+
page_differential_analysis,
|
| 31 |
+
page_metabolic_interactions,
|
| 32 |
+
page_metabolite_balance,
|
| 33 |
+
page_reset
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
def init_session_state():
|
| 37 |
"""Initialise global session state."""
|
| 38 |
if "adata" not in st.session_state:
|
|
|
|
| 62 |
if "dev_mode" not in st.session_state:
|
| 63 |
st.session_state.dev_mode = True
|
| 64 |
|
| 65 |
+
def render_analyze_header():
|
| 66 |
+
"""Display a centered, premium branding header with the white ASCII logo prominently featured."""
|
| 67 |
+
try:
|
| 68 |
+
brand_data = np.load("assets/spMetaTME_brand.npy", allow_pickle=True)
|
| 69 |
+
brand_text = str(brand_data)
|
| 70 |
+
|
| 71 |
+
# We'll use the full brand text including the separators for a professional "terminal" look
|
| 72 |
+
# User requested center alignment and white font color for the logo.
|
| 73 |
+
|
| 74 |
+
subtitle = "A comprehensive atlas for spatial metabolic enrichment and interaction analysis within the Tumor Microenvironment (TME)."
|
| 75 |
+
# f'<h1 style="margin: 0; font-size: 2.0rem; font-weight: 800; color: white; letter-spacing: -1px; text-shadow: 0 2px 10px rgba(0,0,0,0.2);">'
|
| 76 |
+
# f'spMetaTME-Atlas Explorer'
|
| 77 |
+
# f'</h1>'
|
| 78 |
+
# We use a compact HTML string to ensure Streamlit doesn't break the rendering
|
| 79 |
+
header_html = (
|
| 80 |
+
f'<div style="'
|
| 81 |
+
f'background: linear-gradient(135deg, #d32f2f 0%, #6a1b9a 100%), radial-gradient(circle at 2px 2px, rgba(255,255,255,0.05) 1px, transparent 0);'
|
| 82 |
+
f'background-size: 100% 100%, 25px 25px;'
|
| 83 |
+
f'border-radius: 20px;'
|
| 84 |
+
f'margin-bottom: 2rem;'
|
| 85 |
+
f'color: white;'
|
| 86 |
+
f'position: relative;'
|
| 87 |
+
f'overflow: hidden;'
|
| 88 |
+
f'box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);'
|
| 89 |
+
f'border: 1px solid rgba(255, 255, 255, 0.1);'
|
| 90 |
+
f'text-align: center;'
|
| 91 |
+
f'">'
|
| 92 |
+
f'<div style="'
|
| 93 |
+
f'font-family: \'Courier New\', monospace;'
|
| 94 |
+
f'font-size: 12px;'
|
| 95 |
+
f'font-weight: 800;'
|
| 96 |
+
f'line-height: 1.1;'
|
| 97 |
+
f'white-space: pre;'
|
| 98 |
+
f'color: white;'
|
| 99 |
+
f'margin: 1rem 0;'
|
| 100 |
+
f'display: inline-block;'
|
| 101 |
+
f'text-align: left;'
|
| 102 |
+
f'filter: drop-shadow(0 0 1px white);'
|
| 103 |
+
f'">{brand_text}</div>'
|
| 104 |
+
f'<p style="font-size: 1.1rem; line-height: 1.4; opacity: 0.95; max-width: 800px; margin: 0 auto; font-weight: 400; color: #ffebee;">{subtitle}</p>'
|
| 105 |
+
f'</div>'
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
st.markdown(header_html, unsafe_allow_html=True)
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.error(f"Failed to load branding header: {e}")
|
| 111 |
+
|
| 112 |
def render_sidebar_dev():
|
| 113 |
"""Developer shortcuts in sidebar."""
|
| 114 |
with st.sidebar:
|
|
|
|
| 119 |
st.info("Dev Shortcuts Active")
|
| 120 |
if st.button("Load Breast Cancer Block A", use_container_width=True):
|
| 121 |
with st.spinner("Loading example data..."):
|
|
|
|
| 122 |
for key in ['interaction_scores', 'interaction_type']:
|
| 123 |
if key in st.session_state:
|
| 124 |
del st.session_state[key]
|
|
|
|
| 127 |
if adata is not None:
|
| 128 |
st.session_state.metabolic_adata = adata
|
| 129 |
st.session_state.data_type = "metabolic"
|
| 130 |
+
st.session_state.just_loaded = True
|
| 131 |
if 'domain' not in adata.obs.columns and 'domain_id' in adata.obs.columns:
|
| 132 |
adata.obs['domain'] = adata.obs['domain_id']
|
| 133 |
+
st.success("Loaded Breast Cancer Block A")
|
| 134 |
st.rerun()
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
def main():
|
| 137 |
+
# Initial setup
|
| 138 |
init_session_state()
|
| 139 |
+
load_css()
|
| 140 |
+
|
| 141 |
+
# Define available page objects
|
| 142 |
+
home_page = st.Page(show_overview, title="Home", icon=":material/home:", url_path="home")
|
| 143 |
+
domain_stats_page = st.Page(page_domain_statistics, title="Domain Statistics", icon=":material/pie_chart:", url_path="domain_stats")
|
| 144 |
|
| 145 |
+
analysis_pages = [
|
| 146 |
+
st.Page(page_reset, title="Back to Home", icon=":material/home:", url_path="reset_to_home"),
|
| 147 |
+
domain_stats_page,
|
| 148 |
+
st.Page(page_spatial_flux, title="Spatial Flux Distribution", icon=":material/image:", url_path="spatial_flux"),
|
| 149 |
+
st.Page(page_umap_analysis, title="UMAP Analysis", icon=":material/palette:", url_path="umap_analysis"),
|
| 150 |
+
st.Page(page_differential_analysis, title="Differential Reactions", icon=":material/bar_chart:", url_path="differential_analysis"),
|
| 151 |
+
st.Page(page_metabolic_interactions, title="Spatial Metabolic Interactions", icon=":material/link:", url_path="metabolic_interactions"),
|
| 152 |
+
st.Page(page_metabolite_balance, title="Metabolite Balance Analysis", icon=":material/opacity:", url_path="metabolite_balance"),
|
| 153 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
+
if st.session_state.metabolic_adata is None:
|
| 156 |
+
pg = st.navigation([home_page], position="hidden")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
render_header()
|
| 158 |
+
render_sidebar_dev()
|
| 159 |
+
pg.run()
|
| 160 |
+
else:
|
| 161 |
+
# Setup Navigation
|
| 162 |
+
pg = st.navigation({"Metabolic Analysis": analysis_pages}, position="sidebar")
|
| 163 |
+
|
| 164 |
+
# Render Premium Header
|
| 165 |
+
render_analyze_header()
|
| 166 |
+
|
| 167 |
+
# Developer Shortcuts
|
| 168 |
+
render_sidebar_dev()
|
| 169 |
+
|
| 170 |
+
# Handle first-time entry to analysis
|
| 171 |
+
if st.session_state.get('just_loaded', False):
|
| 172 |
+
st.session_state.just_loaded = False
|
| 173 |
+
st.switch_page(domain_stats_page)
|
| 174 |
+
|
| 175 |
+
pg.run()
|
| 176 |
|
| 177 |
+
# Shared Footer
|
| 178 |
+
render_footer()
|
| 179 |
|
| 180 |
if __name__ == "__main__":
|
| 181 |
main()
|
assets/interactions.svg
ADDED
|
|
assets/spMetaTME_brand.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:34d2cc7a52615215df5d42145fbd1826908f5cfc3a9dd0d04e46b02d16184611
|
| 3 |
+
size 2756
|
assets/style.css
CHANGED
|
@@ -21,7 +21,7 @@
|
|
| 21 |
|
| 22 |
/* Main Header */
|
| 23 |
.main-header {
|
| 24 |
-
font-size:
|
| 25 |
color: var(--primary-red);
|
| 26 |
margin-bottom: 1.5rem;
|
| 27 |
font-weight: 700;
|
|
@@ -135,6 +135,7 @@ section[data-testid="stSidebar"] {
|
|
| 135 |
margin: 1.5rem 0;
|
| 136 |
border: 1px solid #f0f4f8;
|
| 137 |
}
|
|
|
|
| 138 |
/* Fix flickering on HuggingFace Spaces with stable selector */
|
| 139 |
/* @media (min-width: calc(736px + 8rem)) {
|
| 140 |
section[data-testid="stMain"] {
|
|
@@ -147,4 +148,4 @@ section[data-testid="stSidebar"] {
|
|
| 147 |
/* div[data-testid="stMainBlockContainer"] {
|
| 148 |
max-width: 75% !important;
|
| 149 |
margin: 0 auto !important;
|
| 150 |
-
} */
|
|
|
|
| 21 |
|
| 22 |
/* Main Header */
|
| 23 |
.main-header {
|
| 24 |
+
font-size: 1.5rem;
|
| 25 |
color: var(--primary-red);
|
| 26 |
margin-bottom: 1.5rem;
|
| 27 |
font-weight: 700;
|
|
|
|
| 135 |
margin: 1.5rem 0;
|
| 136 |
border: 1px solid #f0f4f8;
|
| 137 |
}
|
| 138 |
+
|
| 139 |
/* Fix flickering on HuggingFace Spaces with stable selector */
|
| 140 |
/* @media (min-width: calc(736px + 8rem)) {
|
| 141 |
section[data-testid="stMain"] {
|
|
|
|
| 148 |
/* div[data-testid="stMainBlockContainer"] {
|
| 149 |
max-width: 75% !important;
|
| 150 |
margin: 0 auto !important;
|
| 151 |
+
} */
|
requirements.txt
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
# Core dependencies
|
| 2 |
streamlit>=1.31.0
|
| 3 |
-
streamlit-option-menu
|
| 4 |
huggingface_hub
|
| 5 |
datasets
|
| 6 |
|
|
|
|
| 1 |
# Core dependencies
|
| 2 |
streamlit>=1.31.0
|
|
|
|
| 3 |
huggingface_hub
|
| 4 |
datasets
|
| 5 |
|
src/backend/flux_analysis.py
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
import logging
|
| 2 |
-
import numpy as np
|
| 3 |
-
|
| 4 |
-
logger = logging.getLogger(__name__)
|
| 5 |
-
|
| 6 |
-
def run_smt_inference(adata, model_name, K, batch_size, n_clusters, clustering_method, use_pretrained=True, fine_tune=True, n_epochs=10):
|
| 7 |
-
"""
|
| 8 |
-
Backend logic for running SpMetaTME inference.
|
| 9 |
-
"""
|
| 10 |
-
try:
|
| 11 |
-
from spmetatme.train import SpMetaTME
|
| 12 |
-
from spmetatme.data.dataloader import MetabolicDataLoader
|
| 13 |
-
from spmetatme.data.metabolic_model import get_model_path
|
| 14 |
-
except ImportError:
|
| 15 |
-
logger.error("spMetaTME package not found")
|
| 16 |
-
raise ImportError("spMetaTME package not found. Install with: pip install spmetatme")
|
| 17 |
-
|
| 18 |
-
metabolic_path = get_model_path(model_name)
|
| 19 |
-
data_loader = MetabolicDataLoader(
|
| 20 |
-
adata,
|
| 21 |
-
metabolic_model_path=metabolic_path,
|
| 22 |
-
k=K,
|
| 23 |
-
batch_size=batch_size,
|
| 24 |
-
preprocess=False
|
| 25 |
-
)
|
| 26 |
-
|
| 27 |
-
smt = SpMetaTME()
|
| 28 |
-
if use_pretrained:
|
| 29 |
-
smt.load_pretrained_model("Surajv/spMetaTME-human_64D_v1")
|
| 30 |
-
|
| 31 |
-
if fine_tune:
|
| 32 |
-
smt.fine_tune(data_loader, epochs=n_epochs)
|
| 33 |
-
|
| 34 |
-
metabolic_adata = smt.infer_flux(
|
| 35 |
-
data_loader,
|
| 36 |
-
n_clusters=n_clusters,
|
| 37 |
-
method=clustering_method
|
| 38 |
-
)
|
| 39 |
-
|
| 40 |
-
return metabolic_adata
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/backend/preprocessing.py
DELETED
|
@@ -1,43 +0,0 @@
|
|
| 1 |
-
import scanpy as sc
|
| 2 |
-
import logging
|
| 3 |
-
|
| 4 |
-
logger = logging.getLogger(__name__)
|
| 5 |
-
|
| 6 |
-
def run_preprocessing_pipeline(adata,
|
| 7 |
-
filter_cells_qc=False, min_counts=1000, min_genes=500,
|
| 8 |
-
filter_genes_qc=False, min_cells=10,
|
| 9 |
-
mt_filter=False,
|
| 10 |
-
normalize=True, target_sum=1e4,
|
| 11 |
-
log_transform=True,
|
| 12 |
-
hvg_selection=False, n_hvg=2000):
|
| 13 |
-
"""
|
| 14 |
-
Pure backend logic for preprocessing.
|
| 15 |
-
"""
|
| 16 |
-
adata_processed = adata.copy()
|
| 17 |
-
|
| 18 |
-
if filter_cells_qc:
|
| 19 |
-
sc.pp.calculate_qc_metrics(adata_processed, inplace=True)
|
| 20 |
-
adata_processed = adata_processed[
|
| 21 |
-
(adata_processed.obs['total_counts'] >= min_counts) &
|
| 22 |
-
(adata_processed.obs['n_genes_by_counts'] >= min_genes)
|
| 23 |
-
]
|
| 24 |
-
|
| 25 |
-
if filter_genes_qc:
|
| 26 |
-
sc.pp.filter_genes(adata_processed, min_cells=min_cells)
|
| 27 |
-
|
| 28 |
-
if mt_filter:
|
| 29 |
-
adata_processed = adata_processed[
|
| 30 |
-
:, ~adata_processed.var_names.str.startswith(('MT-', 'mt-', 'MTRNR', 'mtrnr'))
|
| 31 |
-
]
|
| 32 |
-
|
| 33 |
-
if normalize:
|
| 34 |
-
sc.pp.normalize_total(adata_processed, target_sum=target_sum, inplace=True)
|
| 35 |
-
|
| 36 |
-
if log_transform:
|
| 37 |
-
sc.pp.log1p(adata_processed)
|
| 38 |
-
|
| 39 |
-
if hvg_selection:
|
| 40 |
-
sc.pp.highly_variable_genes(adata_processed, n_top_genes=n_hvg, inplace=True)
|
| 41 |
-
adata_processed = adata_processed[:, adata_processed.var['highly_variable']]
|
| 42 |
-
|
| 43 |
-
return adata_processed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/ui/components/header.py
CHANGED
|
@@ -8,66 +8,53 @@ def get_base64_of_bin_file(bin_file):
|
|
| 8 |
return base64.b64encode(data).decode()
|
| 9 |
|
| 10 |
def render_header():
|
| 11 |
-
"""Render application header with
|
| 12 |
logo_path = "assets/Logo.png"
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
<
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
reprogramming in the tumour microenvironment and metabolic interactions.
|
| 34 |
-
</div>
|
| 35 |
-
</div>
|
| 36 |
-
</div>
|
| 37 |
-
""", unsafe_allow_html=True)
|
| 38 |
-
else:
|
| 39 |
-
# Fallback if logo is missing
|
| 40 |
st.markdown("""
|
| 41 |
-
<div style="
|
| 42 |
-
|
| 43 |
-
<
|
| 44 |
-
|
| 45 |
-
</
|
| 46 |
-
<
|
| 47 |
-
Unlike traditional flux estimation approaches, <b>spMetaTME</b> represents the metabolic network as a directed
|
| 48 |
-
hypergraph, where metabolites are represented as nodes and reactions as hyperedges, enabling the modelling of
|
| 49 |
-
directional reactant-to-product flux propagation. By leveraging self-supervised hypergraph learning, <b>spMetaTME</b>
|
| 50 |
-
captures the intrinsic metabolic dependencies and directional flux propagation across spatially adjacent cells or spots.
|
| 51 |
-
</div>
|
| 52 |
</div>
|
| 53 |
""", unsafe_allow_html=True)
|
|
|
|
|
|
|
| 54 |
|
| 55 |
@st.cache_resource(show_spinner=False)
|
| 56 |
def load_css():
|
| 57 |
"""Load and apply CSS - cached to prevent reloading on every rerun."""
|
| 58 |
-
# Load custom CSS file
|
| 59 |
css_path = "assets/style.css"
|
| 60 |
css_content = ""
|
| 61 |
if os.path.exists(css_path):
|
| 62 |
with open(css_path) as f:
|
| 63 |
css_content = f.read()
|
| 64 |
|
| 65 |
-
# Load external assets
|
| 66 |
st.markdown("""
|
| 67 |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
|
| 68 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 69 |
""", unsafe_allow_html=True)
|
| 70 |
|
| 71 |
-
# Apply custom CSS
|
| 72 |
if css_content:
|
| 73 |
st.markdown(f"<style>{css_content}</style>", unsafe_allow_html=True)
|
|
|
|
| 8 |
return base64.b64encode(data).decode()
|
| 9 |
|
| 10 |
def render_header():
|
| 11 |
+
"""Render a professional, clean application header with a modern typography-first layout."""
|
| 12 |
logo_path = "assets/Logo.png"
|
| 13 |
|
| 14 |
+
st.markdown(f"""
|
| 15 |
+
<div style="text-align: center; margin-top: 1rem; margin-bottom: 2.5rem;">
|
| 16 |
+
<h1 style='color: #d32f2f; font-size: 3.0rem; font-weight: 700; margin-bottom: 0.3rem; letter-spacing: -1.5px;'>spMetaTME-Atlas</h1>
|
| 17 |
+
<p style="font-size: 2.0rem; color: #1a1a1a; font-weight: 600; max-width: 850px; margin: 0 auto; line-height: 1.2;">
|
| 18 |
+
A spatial atlas of tumour microenvironment metabolism and metabolic interactions inferred by a pretrained self-supervised metabolic hypergraph
|
| 19 |
+
</p>
|
| 20 |
+
<div style="height: 3px; width: 60px; background: #d32f2f; margin: 1.5rem auto; border-radius: 2px;"></div>
|
| 21 |
+
</div>
|
| 22 |
+
""", unsafe_allow_html=True)
|
| 23 |
+
|
| 24 |
+
col1, col2 = st.columns([0.7, 1.3], gap="large")
|
| 25 |
+
|
| 26 |
+
with col1:
|
| 27 |
+
if os.path.exists(logo_path):
|
| 28 |
+
st.image(logo_path, use_container_width=True)
|
| 29 |
+
else:
|
| 30 |
+
st.info("Technical Diagram Space")
|
| 31 |
+
|
| 32 |
+
with col2:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
st.markdown("""
|
| 34 |
+
<div style="color: #374151; font-size: 1.rem; line-height: 1.8; text-align: justify; padding-top: 0.5rem;">
|
| 35 |
+
Unlike traditional flux estimation approaches, <span style="color: #d32f2f; font-weight: 700;">spMetaTME</span> represents the metabolic network as a
|
| 36 |
+
<b>directed hypergraph</b>, where metabolites are represented as nodes and reactions as hyperedges, enabling the modelling of directional reactant-to-product flux propagation.
|
| 37 |
+
<br><br>
|
| 38 |
+
By leveraging self-supervised hypergraph learning, <b>spMetaTME</b> captures the intrinsic metabolic dependencies and directional flux propagation across spatially adjacent cells or spots.
|
| 39 |
+
We introduce <b>spMetaTME-Atlas</b>, a comprehensive resource of spatial metabolic data designed to uncover metabolic reprogramming and complex metabolic interactions within the TME.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
</div>
|
| 41 |
""", unsafe_allow_html=True)
|
| 42 |
+
|
| 43 |
+
st.markdown("<div style='margin-bottom: 4rem;'></div>", unsafe_allow_html=True)
|
| 44 |
|
| 45 |
@st.cache_resource(show_spinner=False)
|
| 46 |
def load_css():
|
| 47 |
"""Load and apply CSS - cached to prevent reloading on every rerun."""
|
|
|
|
| 48 |
css_path = "assets/style.css"
|
| 49 |
css_content = ""
|
| 50 |
if os.path.exists(css_path):
|
| 51 |
with open(css_path) as f:
|
| 52 |
css_content = f.read()
|
| 53 |
|
|
|
|
| 54 |
st.markdown("""
|
| 55 |
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
|
| 56 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 57 |
""", unsafe_allow_html=True)
|
| 58 |
|
|
|
|
| 59 |
if css_content:
|
| 60 |
st.markdown(f"<style>{css_content}</style>", unsafe_allow_html=True)
|
src/ui/pages/{visualization.py → analyze.py}
RENAMED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
-
from streamlit_option_menu import option_menu
|
| 3 |
import matplotlib.pyplot as plt
|
| 4 |
from src.ui.plots.domain_statistics import render_domain_statistics
|
| 5 |
from src.ui.plots.spatial_flux_map import render_spatial_flux_map
|
|
@@ -23,71 +22,47 @@ def _clear_plot_cache():
|
|
| 23 |
pass
|
| 24 |
|
| 25 |
|
| 26 |
-
def
|
| 27 |
-
"""
|
| 28 |
if st.session_state.metabolic_adata is None:
|
| 29 |
st.error("No flux data available. Please load data first.")
|
| 30 |
-
|
| 31 |
|
| 32 |
metabolic_adata = st.session_state.metabolic_adata
|
| 33 |
if not metabolic_adata.var_names.is_unique:
|
| 34 |
metabolic_adata.var_names_make_unique()
|
| 35 |
-
|
| 36 |
-
viz_options = [
|
| 37 |
-
"Home",
|
| 38 |
-
"Domain Statistics",
|
| 39 |
-
"Spatial Flux Distribution",
|
| 40 |
-
"UMAP Analysis",
|
| 41 |
-
"Differential Analysis",
|
| 42 |
-
"Metabolic Interactions",
|
| 43 |
-
"Metabolite Balance Analysis",
|
| 44 |
-
]
|
| 45 |
-
viz_icons = [
|
| 46 |
-
"house",
|
| 47 |
-
"pie-chart",
|
| 48 |
-
"bi-image-fill",
|
| 49 |
-
"bi-palette2",
|
| 50 |
-
"bi-bar-chart-steps",
|
| 51 |
-
"bi-link",
|
| 52 |
-
"bi-droplet-fill",
|
| 53 |
-
]
|
| 54 |
-
|
| 55 |
-
with st.sidebar:
|
| 56 |
-
selected_viz = option_menu(
|
| 57 |
-
"Metabolic Analysis",
|
| 58 |
-
viz_options,
|
| 59 |
-
icons=viz_icons,
|
| 60 |
-
menu_icon="vial",
|
| 61 |
-
default_index=1,
|
| 62 |
-
key="viz_menu"
|
| 63 |
-
)
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
st.session_state.data_type = None
|
| 78 |
-
st.rerun()
|
| 79 |
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
render_umap_embedding(metabolic_adata)
|
| 89 |
-
elif selected_viz == "Differential Analysis":
|
| 90 |
-
render_differential_reactions(metabolic_adata)
|
| 91 |
-
elif selected_viz == "Metabolic Interactions":
|
| 92 |
-
render_metabolic_interactions(metabolic_adata)
|
| 93 |
|
|
|
|
| 1 |
import streamlit as st
|
|
|
|
| 2 |
import matplotlib.pyplot as plt
|
| 3 |
from src.ui.plots.domain_statistics import render_domain_statistics
|
| 4 |
from src.ui.plots.spatial_flux_map import render_spatial_flux_map
|
|
|
|
| 22 |
pass
|
| 23 |
|
| 24 |
|
| 25 |
+
def _check_data():
|
| 26 |
+
"""Verify data is loaded before rendering."""
|
| 27 |
if st.session_state.metabolic_adata is None:
|
| 28 |
st.error("No flux data available. Please load data first.")
|
| 29 |
+
st.stop()
|
| 30 |
|
| 31 |
metabolic_adata = st.session_state.metabolic_adata
|
| 32 |
if not metabolic_adata.var_names.is_unique:
|
| 33 |
metabolic_adata.var_names_make_unique()
|
| 34 |
+
return metabolic_adata
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
def page_domain_statistics():
|
| 37 |
+
adata = _check_data()
|
| 38 |
+
render_domain_statistics(adata)
|
| 39 |
+
|
| 40 |
+
def page_spatial_flux():
|
| 41 |
+
adata = _check_data()
|
| 42 |
+
render_spatial_flux_map(adata)
|
| 43 |
+
|
| 44 |
+
def page_metabolite_balance():
|
| 45 |
+
adata = _check_data()
|
| 46 |
+
render_metabolite_balance_analysis(adata)
|
| 47 |
+
|
| 48 |
+
def page_umap_analysis():
|
| 49 |
+
adata = _check_data()
|
| 50 |
+
render_umap_embedding(adata)
|
| 51 |
+
|
| 52 |
+
def page_differential_analysis():
|
| 53 |
+
adata = _check_data()
|
| 54 |
+
render_differential_reactions(adata)
|
| 55 |
|
| 56 |
+
def page_metabolic_interactions():
|
| 57 |
+
adata = _check_data()
|
| 58 |
+
render_metabolic_interactions(adata)
|
|
|
|
|
|
|
| 59 |
|
| 60 |
+
def page_reset():
|
| 61 |
+
"""Reset session state and return to overview."""
|
| 62 |
+
st.session_state.metabolic_adata = None
|
| 63 |
+
st.session_state.adata = None
|
| 64 |
+
st.session_state.data_type = None
|
| 65 |
+
st.session_state.preprocessing_done = False
|
| 66 |
+
st.session_state.flux_analysis_done = False
|
| 67 |
+
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
src/ui/pages/flux_analysis.py
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
import streamlit as st
|
| 2 |
-
from src.backend.flux_analysis import run_smt_inference
|
| 3 |
-
|
| 4 |
-
def show_flux_analysis():
|
| 5 |
-
"""Render flux analysis UI."""
|
| 6 |
-
st.markdown("## <i class='fas fa-flask-vial' style='color:#d32f2f'></i> Metabolic Flux Analysis", unsafe_allow_html=True)
|
| 7 |
-
|
| 8 |
-
if st.session_state.adata is None:
|
| 9 |
-
st.error("Please preprocess data first.")
|
| 10 |
-
return
|
| 11 |
-
|
| 12 |
-
col1, col2 = st.columns(2)
|
| 13 |
-
with col1:
|
| 14 |
-
model = st.selectbox("<i class='fas fa-microscope'></i> Model:", ["breast_cancer", "pan_cancer"], help="Select the pre-trained spMetaTME model type.")
|
| 15 |
-
K = st.number_input("K neighbors", value=150, help="Number of neighbors for spatial graph construction.")
|
| 16 |
-
with col2:
|
| 17 |
-
n_clusters = st.number_input("Domains", value=5, help="Number of clusters (metabolic domains) to identify.")
|
| 18 |
-
clustering = st.selectbox("Method", ["kmeans", "leiden"], help="Clustering algorithm for domain identification.")
|
| 19 |
-
|
| 20 |
-
if st.button("Run Analysis", key="run_flux", icon=":material/rocket_launch:"):
|
| 21 |
-
with st.spinner("Running spMetaTME (this may take 5-30 mins)..."):
|
| 22 |
-
try:
|
| 23 |
-
metabolic_adata = run_smt_inference(
|
| 24 |
-
st.session_state.adata, model, K, 80, n_clusters, clustering
|
| 25 |
-
)
|
| 26 |
-
st.session_state.metabolic_adata = metabolic_adata
|
| 27 |
-
st.session_state.flux_analysis_done = True
|
| 28 |
-
st.success("Analysis completed!")
|
| 29 |
-
st.rerun()
|
| 30 |
-
except Exception as e:
|
| 31 |
-
st.error(f"Error: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/ui/pages/{overview.py → home.py}
RENAMED
|
@@ -79,14 +79,34 @@ def render_available_datasets():
|
|
| 79 |
meta_df = get_metadata()
|
| 80 |
if meta_df.empty: return
|
| 81 |
|
| 82 |
-
# Filter
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
st.markdown("---")
|
|
|
|
| 89 |
filtered_df = meta_df.copy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
if selected_species: filtered_df = filtered_df[filtered_df['species'].isin(selected_species)]
|
| 91 |
if selected_organ: filtered_df = filtered_df[filtered_df['organ'].isin(selected_organ)]
|
| 92 |
|
|
@@ -169,6 +189,7 @@ def render_available_datasets():
|
|
| 169 |
if adata:
|
| 170 |
st.session_state.metabolic_adata = adata
|
| 171 |
st.session_state.data_type = "flux"
|
|
|
|
| 172 |
# Clear interaction cache for new tissue
|
| 173 |
for key in ['interaction_scores', 'interaction_type']:
|
| 174 |
if key in st.session_state:
|
|
@@ -197,6 +218,7 @@ def render_upload_fluxes():
|
|
| 197 |
if adata:
|
| 198 |
st.session_state.metabolic_adata = adata
|
| 199 |
st.session_state.data_type = "flux"
|
|
|
|
| 200 |
for key in ['interaction_scores', 'interaction_type']:
|
| 201 |
if key in st.session_state:
|
| 202 |
del st.session_state[key]
|
|
|
|
| 79 |
meta_df = get_metadata()
|
| 80 |
if meta_df.empty: return
|
| 81 |
|
| 82 |
+
# Filter layout - Compact single line
|
| 83 |
+
c1, c2, c3, c4 = st.columns([1.5, 1, 1, 0.5], gap="small")
|
| 84 |
+
|
| 85 |
+
with c1:
|
| 86 |
+
search_query = st.text_input("Search Atlas", placeholder="Search by Dataset, ID...", help="Enter text to search across atlas.")
|
| 87 |
+
with c2:
|
| 88 |
+
selected_species = st.multiselect("Species", options=sorted(meta_df['species'].unique()))
|
| 89 |
+
with c3:
|
| 90 |
+
selected_organ = st.multiselect("Organ", options=sorted(meta_df['organ'].unique()))
|
| 91 |
+
with c4:
|
| 92 |
+
datasets_per_page = st.selectbox("Show", options=[10, 20, 50], index=0)
|
| 93 |
+
|
| 94 |
st.markdown("---")
|
| 95 |
+
|
| 96 |
filtered_df = meta_df.copy()
|
| 97 |
+
|
| 98 |
+
# Text Search Filtering
|
| 99 |
+
if search_query:
|
| 100 |
+
q = search_query.lower()
|
| 101 |
+
search_mask = (
|
| 102 |
+
filtered_df['dataset_title'].str.lower().str.contains(q, na=False) |
|
| 103 |
+
filtered_df['id'].str.lower().str.contains(q, na=False) |
|
| 104 |
+
filtered_df['organ'].str.lower().str.contains(q, na=False) |
|
| 105 |
+
filtered_df['species'].str.lower().str.contains(q, na=False)
|
| 106 |
+
)
|
| 107 |
+
filtered_df = filtered_df[search_mask]
|
| 108 |
+
|
| 109 |
+
# Dropdown Filtering
|
| 110 |
if selected_species: filtered_df = filtered_df[filtered_df['species'].isin(selected_species)]
|
| 111 |
if selected_organ: filtered_df = filtered_df[filtered_df['organ'].isin(selected_organ)]
|
| 112 |
|
|
|
|
| 189 |
if adata:
|
| 190 |
st.session_state.metabolic_adata = adata
|
| 191 |
st.session_state.data_type = "flux"
|
| 192 |
+
st.session_state.just_loaded = True
|
| 193 |
# Clear interaction cache for new tissue
|
| 194 |
for key in ['interaction_scores', 'interaction_type']:
|
| 195 |
if key in st.session_state:
|
|
|
|
| 218 |
if adata:
|
| 219 |
st.session_state.metabolic_adata = adata
|
| 220 |
st.session_state.data_type = "flux"
|
| 221 |
+
st.session_state.just_loaded = True
|
| 222 |
for key in ['interaction_scores', 'interaction_type']:
|
| 223 |
if key in st.session_state:
|
| 224 |
del st.session_state[key]
|
src/ui/pages/preprocessing.py
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
import streamlit as st
|
| 2 |
-
from src.backend.preprocessing import run_preprocessing_pipeline
|
| 3 |
-
|
| 4 |
-
def show_preprocessing():
|
| 5 |
-
"""Render preprocessing UI."""
|
| 6 |
-
st.markdown("## <i class='fas fa-screwdriver-wrench' style='color:#d32f2f'></i> Data Preprocessing", unsafe_allow_html=True)
|
| 7 |
-
|
| 8 |
-
if st.session_state.adata is None:
|
| 9 |
-
st.error("Please upload data first.")
|
| 10 |
-
return
|
| 11 |
-
|
| 12 |
-
adata = st.session_state.adata
|
| 13 |
-
|
| 14 |
-
col1, col2 = st.columns(2)
|
| 15 |
-
with col1:
|
| 16 |
-
st.markdown("#### <i class='fas fa-filter'></i> Filtering Options", unsafe_allow_html=True)
|
| 17 |
-
filter_cells = st.checkbox("Filter cells by quality", value=False)
|
| 18 |
-
min_counts = st.number_input("Min counts", value=1000, help="Minimum library size (total counts) per cell.") if filter_cells else 1000
|
| 19 |
-
min_genes = st.number_input("Min genes", value=500, help="Minimum number of genes detected per cell.") if filter_cells else 500
|
| 20 |
-
|
| 21 |
-
with col2:
|
| 22 |
-
st.markdown("#### <i class='fas fa-wand-magic-sparkles'></i> Normalization", unsafe_allow_html=True)
|
| 23 |
-
normalize = st.checkbox("Normalize library size", value=True)
|
| 24 |
-
log_transform = st.checkbox("Log transform", value=True)
|
| 25 |
-
|
| 26 |
-
if st.button("Run Preprocessing", key="run_pre", icon=":material/play_arrow:"):
|
| 27 |
-
with st.spinner("Processing..."):
|
| 28 |
-
processed = run_preprocessing_pipeline(
|
| 29 |
-
adata,
|
| 30 |
-
filter_cells_qc=filter_cells, min_counts=min_counts, min_genes=min_genes,
|
| 31 |
-
normalize=normalize, log_transform=log_transform
|
| 32 |
-
)
|
| 33 |
-
st.session_state.adata = processed
|
| 34 |
-
st.session_state.preprocessing_done = True
|
| 35 |
-
st.success("Preprocessing completed!")
|
| 36 |
-
st.rerun()
|
| 37 |
-
|
| 38 |
-
if st.session_state.preprocessing_done:
|
| 39 |
-
if st.button("Proceed to Analysis", icon=":material/arrow_forward:"):
|
| 40 |
-
# Redirect logic
|
| 41 |
-
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/ui/plots/domain_statistics.py
CHANGED
|
@@ -12,7 +12,6 @@ from src.backend.flux_distribution import adata_to_long_df, p_to_star
|
|
| 12 |
|
| 13 |
def render_domain_statistics(metabolic_adata):
|
| 14 |
"""Render domain-level statistics and flux distribution."""
|
| 15 |
-
# Clear matplotlib figures to prevent flickering on page transitions
|
| 16 |
plt.close('all')
|
| 17 |
|
| 18 |
st.markdown(
|
|
@@ -28,7 +27,6 @@ def render_domain_statistics(metabolic_adata):
|
|
| 28 |
_render_metabolic_metadata(metabolic_adata)
|
| 29 |
st.markdown("---")
|
| 30 |
|
| 31 |
-
# Three-column layout for Domain-level overview
|
| 32 |
c1, c2, c3 = st.columns(3, gap="small")
|
| 33 |
|
| 34 |
with c1:
|
|
@@ -107,7 +105,7 @@ def render_domain_statistics(metabolic_adata):
|
|
| 107 |
display_plot_with_download(
|
| 108 |
fig,
|
| 109 |
"moranI_kde",
|
| 110 |
-
help_text="Moran's I measures the degree of spatial clustering in flux values. A positive value indicates that similar flux levels are
|
| 111 |
)
|
| 112 |
|
| 113 |
plt.close(fig)
|
|
@@ -119,7 +117,6 @@ def render_domain_statistics(metabolic_adata):
|
|
| 119 |
st.markdown("---")
|
| 120 |
|
| 121 |
st.markdown("<div style='font-size: 1.2rem; font-weight: 600; color: #d32f2f; margin-bottom: 1rem;'><i class='fas fa-box-open'></i> Flux Distribution Across Domains</div>", unsafe_allow_html=True)
|
| 122 |
-
# Horizontal controls for Flux Distribution
|
| 123 |
col_ctrl1, col_ctrl2 = st.columns([1, 2])
|
| 124 |
|
| 125 |
with col_ctrl1:
|
|
@@ -134,7 +131,6 @@ def render_domain_statistics(metabolic_adata):
|
|
| 134 |
with col_ctrl2:
|
| 135 |
if view_mode == "Reactions":
|
| 136 |
if 'rxn_full_names' in metabolic_adata.var.columns:
|
| 137 |
-
# Map full name to ID for user selection
|
| 138 |
unique_names = {}
|
| 139 |
for idx, row in metabolic_adata.var.iterrows():
|
| 140 |
f_name = str(row['rxn_full_names'])
|
|
@@ -188,7 +184,6 @@ def _render_metabolic_metadata(adata):
|
|
| 188 |
domain_counts = adata.obs['domain'].value_counts()
|
| 189 |
domains = sorted(domain_counts.index.tolist())
|
| 190 |
|
| 191 |
-
# Row 1: Global Stats
|
| 192 |
c1, c2, c3 = st.columns(3)
|
| 193 |
with c1:
|
| 194 |
st.markdown(f"""
|
|
@@ -243,7 +238,7 @@ def _render_domain_overall(adata):
|
|
| 243 |
display_plot_with_download(
|
| 244 |
fig,
|
| 245 |
"domain_overall_flux",
|
| 246 |
-
help_text="This boxen plot shows the distribution of per-spot mean metabolic flux across all reactions for each domain. It highlights the overall metabolic activity levels and identifies
|
| 247 |
)
|
| 248 |
|
| 249 |
plt.close(fig)
|
|
@@ -276,7 +271,6 @@ def _render_reactions_mode(adata, selected):
|
|
| 276 |
)
|
| 277 |
add_significance_brackets(ax, sub, domain_order, y_col="flux")
|
| 278 |
|
| 279 |
-
# Use friendly name if available
|
| 280 |
title_text = rxn
|
| 281 |
if 'rxn_full_names' in adata.var.columns and rxn in adata.var_names:
|
| 282 |
title_text = adata.var.loc[rxn, 'rxn_full_names']
|
|
@@ -286,7 +280,6 @@ def _render_reactions_mode(adata, selected):
|
|
| 286 |
ax.set_ylabel("Flux")
|
| 287 |
|
| 288 |
plt.tight_layout()
|
| 289 |
-
# Generate specific reactions help
|
| 290 |
rxn_names = []
|
| 291 |
for rxn in selected:
|
| 292 |
if 'rxn_full_names' in adata.var.columns and rxn in adata.var_names:
|
|
@@ -341,7 +334,6 @@ def _render_pathway_mode(adata, selected_pathways):
|
|
| 341 |
ax.set_ylabel("Flux")
|
| 342 |
|
| 343 |
plt.tight_layout()
|
| 344 |
-
# Generate specific pathway help
|
| 345 |
pathway_str = ", ".join(selected_pathways)
|
| 346 |
display_plot_with_download(
|
| 347 |
fig,
|
|
|
|
| 12 |
|
| 13 |
def render_domain_statistics(metabolic_adata):
|
| 14 |
"""Render domain-level statistics and flux distribution."""
|
|
|
|
| 15 |
plt.close('all')
|
| 16 |
|
| 17 |
st.markdown(
|
|
|
|
| 27 |
_render_metabolic_metadata(metabolic_adata)
|
| 28 |
st.markdown("---")
|
| 29 |
|
|
|
|
| 30 |
c1, c2, c3 = st.columns(3, gap="small")
|
| 31 |
|
| 32 |
with c1:
|
|
|
|
| 105 |
display_plot_with_download(
|
| 106 |
fig,
|
| 107 |
"moranI_kde",
|
| 108 |
+
help_text="Moran's I measures the degree of spatial clustering in flux values. A positive value indicates that similar flux levels are spatially clustered, while values near zero suggest a random distribution. This helps confirm that metabolic patterns are spatially organized."
|
| 109 |
)
|
| 110 |
|
| 111 |
plt.close(fig)
|
|
|
|
| 117 |
st.markdown("---")
|
| 118 |
|
| 119 |
st.markdown("<div style='font-size: 1.2rem; font-weight: 600; color: #d32f2f; margin-bottom: 1rem;'><i class='fas fa-box-open'></i> Flux Distribution Across Domains</div>", unsafe_allow_html=True)
|
|
|
|
| 120 |
col_ctrl1, col_ctrl2 = st.columns([1, 2])
|
| 121 |
|
| 122 |
with col_ctrl1:
|
|
|
|
| 131 |
with col_ctrl2:
|
| 132 |
if view_mode == "Reactions":
|
| 133 |
if 'rxn_full_names' in metabolic_adata.var.columns:
|
|
|
|
| 134 |
unique_names = {}
|
| 135 |
for idx, row in metabolic_adata.var.iterrows():
|
| 136 |
f_name = str(row['rxn_full_names'])
|
|
|
|
| 184 |
domain_counts = adata.obs['domain'].value_counts()
|
| 185 |
domains = sorted(domain_counts.index.tolist())
|
| 186 |
|
|
|
|
| 187 |
c1, c2, c3 = st.columns(3)
|
| 188 |
with c1:
|
| 189 |
st.markdown(f"""
|
|
|
|
| 238 |
display_plot_with_download(
|
| 239 |
fig,
|
| 240 |
"domain_overall_flux",
|
| 241 |
+
help_text="This boxen plot shows the distribution of per-spot mean metabolic flux across all reactions for each domain. It highlights the overall metabolic activity levels and identifies the domains that are significantly more or less active."
|
| 242 |
)
|
| 243 |
|
| 244 |
plt.close(fig)
|
|
|
|
| 271 |
)
|
| 272 |
add_significance_brackets(ax, sub, domain_order, y_col="flux")
|
| 273 |
|
|
|
|
| 274 |
title_text = rxn
|
| 275 |
if 'rxn_full_names' in adata.var.columns and rxn in adata.var_names:
|
| 276 |
title_text = adata.var.loc[rxn, 'rxn_full_names']
|
|
|
|
| 280 |
ax.set_ylabel("Flux")
|
| 281 |
|
| 282 |
plt.tight_layout()
|
|
|
|
| 283 |
rxn_names = []
|
| 284 |
for rxn in selected:
|
| 285 |
if 'rxn_full_names' in adata.var.columns and rxn in adata.var_names:
|
|
|
|
| 334 |
ax.set_ylabel("Flux")
|
| 335 |
|
| 336 |
plt.tight_layout()
|
|
|
|
| 337 |
pathway_str = ", ".join(selected_pathways)
|
| 338 |
display_plot_with_download(
|
| 339 |
fig,
|
src/ui/plots/metabolic_interactions.py
CHANGED
|
@@ -20,7 +20,37 @@ def render_metabolic_interactions(metabolic_adata):
|
|
| 20 |
Investigate metabolic interaction types in the TME using Plotly.
|
| 21 |
"""
|
| 22 |
st.markdown("<h2 style='color: #d32f2f;'><i class='fas fa-project-diagram'></i> Metabolic Interaction Analysis</h2>", unsafe_allow_html=True)
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
if 'interaction_type' not in st.session_state:
|
| 25 |
st.session_state.interaction_type = None
|
| 26 |
if 'interaction_scores' not in st.session_state:
|
|
@@ -43,7 +73,7 @@ def render_metabolic_interactions(metabolic_adata):
|
|
| 43 |
DENSITY_VALS = [99.5, 99, 95, 90, 80, 60, 40, 20, 10, 0]
|
| 44 |
DENSITY_MAP = dict(zip(DENSITY_LABELS, DENSITY_VALS))
|
| 45 |
|
| 46 |
-
tab1, tab2, tab3 = st.tabs(["
|
| 47 |
|
| 48 |
with tab1:
|
| 49 |
st.markdown("#### Distribution of Interaction Types")
|
|
|
|
| 20 |
Investigate metabolic interaction types in the TME using Plotly.
|
| 21 |
"""
|
| 22 |
st.markdown("<h2 style='color: #d32f2f;'><i class='fas fa-project-diagram'></i> Metabolic Interaction Analysis</h2>", unsafe_allow_html=True)
|
| 23 |
+
st.markdown("""
|
| 24 |
+
Metabolic interactions represent the dynamic exchange of metabolites between spatially adjacent spots (cells).
|
| 25 |
+
**spMetaTME** identifies five distinct metabolic interaction types:
|
| 26 |
+
""")
|
| 27 |
+
# Interaction Types Demonstration - Direct Layout
|
| 28 |
+
col_img, col_txt = st.columns([1, 1], gap="large")
|
| 29 |
+
with col_img:
|
| 30 |
+
logo_path = "assets/interactions.svg"
|
| 31 |
+
if os.path.exists(logo_path):
|
| 32 |
+
st.image(logo_path, caption="Spatially resolved metabolic interaction types in the tumor microenvironment", use_container_width=True)
|
| 33 |
+
else:
|
| 34 |
+
st.info("Interaction schema image (interactions.svg) not found in assets.")
|
| 35 |
+
|
| 36 |
+
with col_txt:
|
| 37 |
+
# st.markdown("""
|
| 38 |
+
# Metabolic interactions represent the dynamic exchange of metabolites between spatially adjacent spots (cells).
|
| 39 |
+
# spMetaTME identifies **five distinct metabolic interaction types**:
|
| 40 |
+
|
| 41 |
+
# * **Competition**: Spatially adjacent spots (cells) compete for the same limited nutrients.
|
| 42 |
+
# * **Cooperation**: Spatially adjacent spots (cells) exchange metabolites in a mutually beneficial manner.
|
| 43 |
+
# * **Release**: Spatially adjacent spots (cells) release metabolites to the environment.
|
| 44 |
+
# * **Amensalism**: One cell's metabolic byproducts adversely affect neighbors without direct benefit to the producer.
|
| 45 |
+
# * **Neutralism**: Cells co-exist in the same region without significant metabolic cross-talk or resource interference.
|
| 46 |
+
# """)
|
| 47 |
+
st.markdown("""
|
| 48 |
+
* **Competition**: Spatially adjacent spots (cells) compete for the metabolites available in the microenvironment.
|
| 49 |
+
* **Cooperation**: Spatially adjacent cells engage in metabolite exchange that benefits both, typically where a metabolite secreted by one cell is taken up and utilized by another.
|
| 50 |
+
* **Release**: Cells secrete metabolites into the microenvironment without evidence of uptake by neighboring cells, contributing to the shared metabolite pool.
|
| 51 |
+
* **Amensalism**: One cell either secrets or consumes the metabolite, while spatially adjacent cell do not utilize it.
|
| 52 |
+
* **Neutralism**: Spatially adjacent cells coexist without detectable metabolic interaction, showing no significant exchange or competition for metabolites.
|
| 53 |
+
""")
|
| 54 |
if 'interaction_type' not in st.session_state:
|
| 55 |
st.session_state.interaction_type = None
|
| 56 |
if 'interaction_scores' not in st.session_state:
|
|
|
|
| 73 |
DENSITY_VALS = [99.5, 99, 95, 90, 80, 60, 40, 20, 10, 0]
|
| 74 |
DENSITY_MAP = dict(zip(DENSITY_LABELS, DENSITY_VALS))
|
| 75 |
|
| 76 |
+
tab1, tab2, tab3 = st.tabs(["Metabolic Interaction Distribution", "Interaction Type Investigation", "Communication Score"])
|
| 77 |
|
| 78 |
with tab1:
|
| 79 |
st.markdown("#### Distribution of Interaction Types")
|
src/ui/plots/spatial_flux_map.py
CHANGED
|
@@ -9,7 +9,6 @@ from .utils import display_plot_with_download, display_interactive_spatial_plot,
|
|
| 9 |
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
|
| 12 |
-
# Initialize session state for plot caching
|
| 13 |
def init_plot_state():
|
| 14 |
"""Initialize plot caching state variables."""
|
| 15 |
if "plot_cache" not in st.session_state:
|
|
@@ -27,7 +26,6 @@ def _detect_viz_change_and_clear():
|
|
| 27 |
|
| 28 |
last_params = st.session_state.get('sp_last_params', {})
|
| 29 |
|
| 30 |
-
# Check if any visualization parameter changed
|
| 31 |
if current_params != last_params:
|
| 32 |
st.session_state.sp_last_params = current_params
|
| 33 |
plt.close('all') # Close all matplotlib figures
|
|
@@ -37,15 +35,11 @@ def _detect_viz_change_and_clear():
|
|
| 37 |
|
| 38 |
def render_spatial_flux_map(metabolic_adata):
|
| 39 |
"""Render spatial flux maps with Red theme."""
|
| 40 |
-
# Initialize plot caching state
|
| 41 |
init_plot_state()
|
| 42 |
-
|
| 43 |
-
# Detect visualization changes and clear cache to prevent flickering
|
| 44 |
_detect_viz_change_and_clear()
|
| 45 |
|
| 46 |
st.markdown("<h2 style='color: #d32f2f;'><i class='fas fa-map-location-dot'></i> Spatial Metabolic flux</h2>", unsafe_allow_html=True)
|
| 47 |
|
| 48 |
-
# 1. Determine layout and render primary filters
|
| 49 |
viz_choice = st.session_state.get("sp_viz_choice", "Domains")
|
| 50 |
|
| 51 |
if viz_choice == "Domains":
|
|
@@ -56,7 +50,6 @@ def render_spatial_flux_map(metabolic_adata):
|
|
| 56 |
with c1:
|
| 57 |
viz_choice = st.selectbox("Analysis Type:", options=["Domains", "Reactions", "Pathways"], key="sp_viz_choice")
|
| 58 |
|
| 59 |
-
# Plot mode and spot size are always present, but column varies
|
| 60 |
with (c3 if viz_choice == "Domains" else c4):
|
| 61 |
plot_mode = st.radio("Plot Mode:", ["Static", "Interactive"], horizontal=True, key="sp_mode")
|
| 62 |
|
|
@@ -65,7 +58,6 @@ def render_spatial_flux_map(metabolic_adata):
|
|
| 65 |
|
| 66 |
selected_items = []
|
| 67 |
|
| 68 |
-
# 2. Render selective filters (only for non-domain modes in col2)
|
| 69 |
if viz_choice != "Domains":
|
| 70 |
with c2:
|
| 71 |
if viz_choice == "Reactions":
|
|
@@ -101,7 +93,6 @@ def render_spatial_flux_map(metabolic_adata):
|
|
| 101 |
else:
|
| 102 |
st.warning("No pathway data.")
|
| 103 |
|
| 104 |
-
# 3. Visualization logic
|
| 105 |
try:
|
| 106 |
library_id = next(iter(metabolic_adata.uns["spatial"]))
|
| 107 |
img_key = "hires" if "hires" in metabolic_adata.uns["spatial"][library_id]["images"] else "downscaled_fullres"
|
|
@@ -153,7 +144,6 @@ def render_spatial_flux_map(metabolic_adata):
|
|
| 153 |
|
| 154 |
del metabolic_adata.obs[f'temp_{target}']
|
| 155 |
else:
|
| 156 |
-
# Static grid for pathways
|
| 157 |
per_page = 4
|
| 158 |
total = len(selected_items)
|
| 159 |
pages = (total + per_page - 1) // per_page
|
|
@@ -182,7 +172,6 @@ def render_spatial_flux_map(metabolic_adata):
|
|
| 182 |
|
| 183 |
for j in range(len(curr_items), n_rows*n_cols): axes[j//n_cols, j%n_cols].axis('off')
|
| 184 |
plt.tight_layout()
|
| 185 |
-
# Generate names for help text
|
| 186 |
target_names = ", ".join([str(t) for t in curr_items])
|
| 187 |
display_plot_with_download(
|
| 188 |
fig,
|
|
@@ -246,7 +235,6 @@ def render_spatial_flux_map(metabolic_adata):
|
|
| 246 |
|
| 247 |
for j in range(len(curr_rx), n_rows*n_cols): axes[j//n_cols, j%n_cols].axis('off')
|
| 248 |
plt.tight_layout()
|
| 249 |
-
# Generate names for help text
|
| 250 |
rx_names_list = []
|
| 251 |
for rx in curr_rx:
|
| 252 |
if 'rxn_full_names' in metabolic_adata.var.columns and rx in metabolic_adata.var_names:
|
|
|
|
| 9 |
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
|
|
|
|
| 12 |
def init_plot_state():
|
| 13 |
"""Initialize plot caching state variables."""
|
| 14 |
if "plot_cache" not in st.session_state:
|
|
|
|
| 26 |
|
| 27 |
last_params = st.session_state.get('sp_last_params', {})
|
| 28 |
|
|
|
|
| 29 |
if current_params != last_params:
|
| 30 |
st.session_state.sp_last_params = current_params
|
| 31 |
plt.close('all') # Close all matplotlib figures
|
|
|
|
| 35 |
|
| 36 |
def render_spatial_flux_map(metabolic_adata):
|
| 37 |
"""Render spatial flux maps with Red theme."""
|
|
|
|
| 38 |
init_plot_state()
|
|
|
|
|
|
|
| 39 |
_detect_viz_change_and_clear()
|
| 40 |
|
| 41 |
st.markdown("<h2 style='color: #d32f2f;'><i class='fas fa-map-location-dot'></i> Spatial Metabolic flux</h2>", unsafe_allow_html=True)
|
| 42 |
|
|
|
|
| 43 |
viz_choice = st.session_state.get("sp_viz_choice", "Domains")
|
| 44 |
|
| 45 |
if viz_choice == "Domains":
|
|
|
|
| 50 |
with c1:
|
| 51 |
viz_choice = st.selectbox("Analysis Type:", options=["Domains", "Reactions", "Pathways"], key="sp_viz_choice")
|
| 52 |
|
|
|
|
| 53 |
with (c3 if viz_choice == "Domains" else c4):
|
| 54 |
plot_mode = st.radio("Plot Mode:", ["Static", "Interactive"], horizontal=True, key="sp_mode")
|
| 55 |
|
|
|
|
| 58 |
|
| 59 |
selected_items = []
|
| 60 |
|
|
|
|
| 61 |
if viz_choice != "Domains":
|
| 62 |
with c2:
|
| 63 |
if viz_choice == "Reactions":
|
|
|
|
| 93 |
else:
|
| 94 |
st.warning("No pathway data.")
|
| 95 |
|
|
|
|
| 96 |
try:
|
| 97 |
library_id = next(iter(metabolic_adata.uns["spatial"]))
|
| 98 |
img_key = "hires" if "hires" in metabolic_adata.uns["spatial"][library_id]["images"] else "downscaled_fullres"
|
|
|
|
| 144 |
|
| 145 |
del metabolic_adata.obs[f'temp_{target}']
|
| 146 |
else:
|
|
|
|
| 147 |
per_page = 4
|
| 148 |
total = len(selected_items)
|
| 149 |
pages = (total + per_page - 1) // per_page
|
|
|
|
| 172 |
|
| 173 |
for j in range(len(curr_items), n_rows*n_cols): axes[j//n_cols, j%n_cols].axis('off')
|
| 174 |
plt.tight_layout()
|
|
|
|
| 175 |
target_names = ", ".join([str(t) for t in curr_items])
|
| 176 |
display_plot_with_download(
|
| 177 |
fig,
|
|
|
|
| 235 |
|
| 236 |
for j in range(len(curr_rx), n_rows*n_cols): axes[j//n_cols, j%n_cols].axis('off')
|
| 237 |
plt.tight_layout()
|
|
|
|
| 238 |
rx_names_list = []
|
| 239 |
for rx in curr_rx:
|
| 240 |
if 'rxn_full_names' in metabolic_adata.var.columns and rx in metabolic_adata.var_names:
|