Surajv commited on
Commit
e872263
·
1 Parent(s): 33aca2a

Re-designed UI

Browse files
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
- # Set metadata if missing
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 (HF local cache)")
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
- load_css()
73
  init_session_state()
 
 
 
 
 
74
 
75
- # Determine which page will be shown
76
- if st.session_state.metabolic_adata is not None:
77
- current_page = "visualization"
78
- elif st.session_state.adata is not None:
79
- if st.session_state.preprocessing_done:
80
- current_page = "flux_analysis"
81
- else:
82
- current_page = "preprocessing"
83
- else:
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
- # Render the appropriate page
104
- if current_page == "visualization":
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
- show_overview()
113
-
114
- render_footer()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: 2.5rem;
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 Logo and Introduction side-by-side in a card."""
12
  logo_path = "assets/Logo.png"
13
 
14
- if os.path.exists(logo_path):
15
- logo_base64 = get_base64_of_bin_file(logo_path)
16
- logo_html = f"data:image/png;base64,{logo_base64}"
17
-
18
- st.markdown(f"""
19
- <div style="display: flex; align-items: center; gap: 0.5rem; padding: 2.5rem; margin-bottom: 2.5rem; border-left: 6px solid #d32f2f; background: #ffffff; border-radius: 12px; border: 1px solid #e0e0e0; border-left: 6px solid #d32f2f;">
20
- <div style="flex: 1; display: flex; justify-content: center; align-items: center;">
21
- <img src="{logo_html}" style="max-width: 100%; height: auto; max-height: 300px; border-radius: 8px;">
22
- </div>
23
- <div style="flex: 2;">
24
- <h1 style='color: #d32f2f; margin: 0 0 0.5rem 0; font-size: 3rem; font-weight: 800; line-height: 1; text-align: center;'>spMetaTME-Atlas</h1>
25
- <p style="font-size: 1.3rem; color: #333; font-weight: 600; margin-bottom: 1.2rem; line-height: 1.3;">
26
- A spatial atlas of tumour microenvironment metabolism and metabolic interactions inferred by a pretrained self-supervised metabolic hypergraph
27
- </p>
28
- <div style="color: #555; font-size: 1.1rem; line-height: 1.6; text-align: justify;">
29
- Unlike traditional flux estimation approaches, <b>spMetaTME</b> represents the metabolic network as a directed hypergraph, where metabolites are
30
- represented as nodes and reactions as hyperedges, enabling the modelling of directional reactant-to-product flux propagation. By leveraging
31
- self-supervised hypergraph learning, <b>spMetaTME</b> captures the intrinsic metabolic dependencies and directional flux propagation across spatially
32
- adjacent cells or spots. Leveraging pretrained spMetaTME, we introduce spMetaTME-Atlas, a comprehensive atlas of spatial metabolic data to cover metabolic
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="padding: 2.5rem; margin-bottom: 2.5rem; border-radius: 12px; border: 1px solid #e0e0e0; border-left: 6px solid #d32f2f; background: #ffffff;">
42
- <h1 class='main-header' style='font-size: 3.5rem; margin-bottom: 0.5rem; text-align: center;'>spMetaTME-Atlas</h1>
43
- <p style="font-size: 1.5rem; color: #333; font-weight: 600; line-height: 1.3;">
44
- A spatial atlas of tumour microenvironment metabolism and metabolic interactions inferred by a pretrained self-supervised metabolic hypergraph
45
- </p>
46
- <div style="color: #444; font-size: 1.15rem; line-height: 1.8; margin-top: 1.5rem; text-align: justify;">
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 show_visualization():
27
- """Visualization module coordinator."""
28
  if st.session_state.metabolic_adata is None:
29
  st.error("No flux data available. Please load data first.")
30
- return
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
- # Initialize last selected visualization tracking
66
- if "last_selected_viz" not in st.session_state:
67
- st.session_state.last_selected_viz = selected_viz
68
-
69
- # Detect visualization change and clear cache
70
- if selected_viz != st.session_state.last_selected_viz:
71
- st.session_state.last_selected_viz = selected_viz
72
- _clear_plot_cache()
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- # Handle Home navigation
75
- if selected_viz == "Home":
76
- st.session_state.metabolic_adata = None
77
- st.session_state.data_type = None
78
- st.rerun()
79
 
80
- # Main content rendering
81
- if selected_viz == "Domain Statistics":
82
- render_domain_statistics(metabolic_adata)
83
- elif selected_viz == "Spatial Flux Distribution":
84
- render_spatial_flux_map(metabolic_adata)
85
- elif selected_viz == "Metabolite Balance Analysis":
86
- render_metabolite_balance_analysis(metabolic_adata)
87
- elif selected_viz == "UMAP Analysis":
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 sidebar style layout inside page
83
- # with st.expander("Filter Results", expanded=False, icon=":material/filter_list:"):
84
- c1, c2, c3 = st.columns(3)
85
- selected_species = c1.multiselect("Species", options=sorted(meta_df['species'].unique()), help="Filter datasets by species.")
86
- selected_organ = c2.multiselect("Organ", options=sorted(meta_df['organ'].unique()), help="Filter datasets by organ.")
87
- datasets_per_page = c3.selectbox("Show", options=[10, 20, 50], index=0, help="Number of datasets to show per page.")
 
 
 
 
 
 
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 geographically clustered, while values near zero suggest a random distribution. This helps confirm that metabolic patterns are spatially organized."
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 which domains are significantly more or less active."
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(["Global Distribution", "Interaction Type Investigation", "Communication Score"])
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: