kaveh commited on
Commit
a0a18a3
·
1 Parent(s): 093f57c

designed home page

Browse files
streamlit_hf/.streamlit/config.toml CHANGED
@@ -1,7 +1,9 @@
1
  [theme]
2
  primaryColor = "#2563eb"
3
- backgroundColor = "#f8fafc"
4
- secondaryBackgroundColor = "#ffffff"
 
 
5
  textColor = "#0f172a"
6
  font = "sans-serif"
7
 
 
1
  [theme]
2
  primaryColor = "#2563eb"
3
+ # Match Plotly plotly_white paper (#fff) and CSS shell; avoids grey main area behind charts.
4
+ backgroundColor = "#ffffff"
5
+ # Sidebar / secondary surfaces: slightly tinted vs main canvas
6
+ secondaryBackgroundColor = "#f1f5fb"
7
  textColor = "#0f172a"
8
  font = "sans-serif"
9
 
streamlit_hf/app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- FateFormer: interactive analysis explorer.
3
  Run from repository root: PYTHONPATH=. streamlit run streamlit_hf/app.py
4
  """
5
 
 
1
  """
2
+ FateFormer Explorer: interactive analysis hub.
3
  Run from repository root: PYTHONPATH=. streamlit run streamlit_hf/app.py
4
  """
5
 
streamlit_hf/home.py CHANGED
@@ -1,58 +1,208 @@
1
- """Landing content for the FateFormer Streamlit hub."""
2
 
3
  from __future__ import annotations
4
 
 
5
  import sys
6
  from pathlib import Path
7
 
 
8
  import streamlit as st
9
 
10
  _REPO = Path(__file__).resolve().parents[1]
11
  if str(_REPO) not in sys.path:
12
  sys.path.insert(0, str(_REPO))
13
 
 
 
14
  from streamlit_hf.lib import ui
15
 
16
  _CACHE = Path(__file__).resolve().parent / "cache"
17
- _HAS_CACHE = (_CACHE / "latent_umap.pkl").is_file() and (_CACHE / "df_features.parquet").is_file()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  ui.inject_app_styles()
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
- st.title("FateFormer: interactive analysis")
22
- st.caption("Choose a workspace below or use the sidebar. All views use the same precomputed validation results.")
 
 
23
 
24
- if not _HAS_CACHE:
25
  st.warning(
26
- "This deployment does not have precomputed results yet. Ask the maintainer to publish data, then reload."
 
27
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  else:
29
- st.success("Precomputed results are available. After a server-side update, refresh the browser to load new plots.")
 
 
 
30
 
31
- st.subheader("Open a page")
32
- r1a, r1b, r1c = st.columns(3)
33
- with r1a:
 
 
34
  with st.container(border=True):
35
  st.page_link("pages/1_Single_Cell_Explorer.py", label="Single-Cell Explorer", icon=":material/scatter_plot:")
36
- st.caption("Latent UMAP: colour by fate, prediction, fold, batch, modalities, or dominant fate emphasis.")
37
- with r1b:
 
38
  with st.container(border=True):
39
  st.page_link("pages/2_Feature_insights.py", label="Feature Insights", icon=":material/analytics:")
40
- st.caption("Shift and attention rankings, cohort comparisons, and full feature tables.")
41
- with r1c:
 
42
  with st.container(border=True):
43
  st.page_link("pages/3_Flux_analysis.py", label="Flux Analysis", icon=":material/account_tree:")
44
  st.caption("Reaction pathways, differential flux, rankings, and model metadata.")
45
- r2a, _, _ = st.columns(3)
46
- with r2a:
47
  with st.container(border=True):
48
  st.page_link(
49
  "pages/4_Gene_expression_analysis.py",
50
  label="Gene Expression & TF Activity",
51
  icon=":material/genetics:",
52
  )
53
- st.caption("Pathway enrichment, motif activity, and gene / motif tables.")
54
 
55
- st.markdown("---")
56
- st.markdown(
57
- "**Tips:** use chart toolbars for pan/zoom and lasso selection where offered. Tables support search and column sort from the header row."
58
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Landing page for the FateFormer Explorer Streamlit hub."""
2
 
3
  from __future__ import annotations
4
 
5
+ import html
6
  import sys
7
  from pathlib import Path
8
 
9
+ import numpy as np
10
  import streamlit as st
11
 
12
  _REPO = Path(__file__).resolve().parents[1]
13
  if str(_REPO) not in sys.path:
14
  sys.path.insert(0, str(_REPO))
15
 
16
+ from streamlit_hf.lib import io
17
+ from streamlit_hf.lib import plots
18
  from streamlit_hf.lib import ui
19
 
20
  _CACHE = Path(__file__).resolve().parent / "cache"
21
+
22
+ _APP_NAME = "FateFormer Explorer"
23
+ _HERO_EMOJI = "\U0001f9ec" # DNA (matches HF Space card tone)
24
+ _HOME_PIE_TOP_N = 100
25
+ _HOME_RANK_TOP_N = 15
26
+
27
+ _VALIDATION_ROC_AUC = 0.93
28
+
29
+ _UMAP_HOME_TITLE = "Validation latent space (UMAP)"
30
+
31
+ _APP_SUBTITLE = (
32
+ "A multimodal transformer-based model that jointly encodes RNA, chromatin accessibility, and metabolic flux "
33
+ "to predict single-cell fate, with interpretable attention and latent-shift rankings across omics layers."
34
+ )
35
+
36
+ _BIOLOGY_CONTEXT_MARKDOWN = """
37
+ **At a glance**
38
+
39
+ - **Biological setting:** **FateFormer** models **direct reprogramming** from mouse embryonic fibroblasts (**MEFs**) to induced endoderm progenitors (**iEPs**), combining **transcriptome (scRNA-seq)**, **chromatin (scATAC-seq)**, and **genome-scale metabolic flux** so fate is not inferred from RNA alone; epigenetic and metabolic context matter.
40
+ - **Data & labels:** Trained on a **large sparse-modality** atlas (**>150,000** cells); **2,110** early cells carry **CellTag-Multi** clonal fate tags, the same experimental labels used to colour validation cells in **UMAP** views here.
41
+ - **Model design:** A **transformer** learns **shared representations** across modalities, handles **missing modalities** and **scarce fate labels**, and ties early transcription, chromatin accessibility, and metabolic activity to **later lineage outcomes**, going beyond RNA-only views of reprogramming.
42
+ """
43
+
44
+
45
+ def _cache_ok() -> bool:
46
+ return (_CACHE / "latent_umap.pkl").is_file() and (_CACHE / "df_features.parquet").is_file()
47
+
48
+
49
+ def _downsample_latent_df(df, max_points: int = 6500, seed: int = 42):
50
+ if len(df) <= max_points:
51
+ return df
52
+ return df.sample(n=max_points, random_state=seed)
53
+
54
 
55
  ui.inject_app_styles()
56
+ ui.inject_home_landing_styles()
57
+
58
+ st.markdown(
59
+ f"""<div class="ff-hero"><div class="ff-hero-inner"><div class="ff-hero-text">
60
+ <div class="ff-hero-title-row">
61
+ <span class="ff-hero-emoji" aria-hidden="true">{_HERO_EMOJI}</span>
62
+ <h1>{html.escape(_APP_NAME)}</h1>
63
+ </div>
64
+ <p class="ff-hero-sub">{html.escape(_APP_SUBTITLE)}</p>
65
+ </div></div></div>""",
66
+ unsafe_allow_html=True,
67
+ )
68
 
69
+ bundle = io.load_latent_bundle()
70
+ df_features = io.load_df_features()
71
+ samples = io.load_samples_df()
72
+ ready = _cache_ok()
73
 
74
+ if not ready:
75
  st.warning(
76
+ "Precomputed validation caches are incomplete or missing. "
77
+ "Publish `latent_umap.pkl` and `df_features.parquet` under `streamlit_hf/cache/`, then reload."
78
  )
79
+
80
+ # --- Metrics strip ---
81
+ mcols = st.columns(4)
82
+ if bundle is not None:
83
+ n_cells = len(bundle["umap_x"])
84
+ with mcols[0]:
85
+ st.metric("Validation cells", f"{n_cells:,}")
86
+ with mcols[1]:
87
+ st.metric("Validation ROC-AUC", f"{_VALIDATION_ROC_AUC:.2f}")
88
+ else:
89
+ with mcols[0]:
90
+ st.metric("Validation cells", "n/a")
91
+ with mcols[1]:
92
+ st.metric("Validation ROC-AUC", "n/a")
93
+
94
+ if df_features is not None:
95
+ nf = len(df_features)
96
+ n_mod = df_features["modality"].nunique() if "modality" in df_features.columns else 0
97
+ with mcols[2]:
98
+ st.metric("Ranked features", f"{nf:,}")
99
+ with mcols[3]:
100
+ st.metric("Modalities", str(n_mod) if n_mod else "n/a")
101
  else:
102
+ with mcols[2]:
103
+ st.metric("Ranked features", "n/a")
104
+ with mcols[3]:
105
+ st.metric("Modalities", "n/a")
106
 
107
+ # --- Workspace cards (directly under metrics); hidden spans pair with CSS for per-card colours ---
108
+ _NAV_SLOT = '<span id="ff-nav-slot-{}" class="ff-nav-slot-marker" aria-hidden="true"></span>'
109
+ c1, c2, c3, c4 = st.columns(4, gap="small")
110
+ with c1:
111
+ st.markdown(_NAV_SLOT.format(1), unsafe_allow_html=True)
112
  with st.container(border=True):
113
  st.page_link("pages/1_Single_Cell_Explorer.py", label="Single-Cell Explorer", icon=":material/scatter_plot:")
114
+ st.caption("UMAP, filters, and per-cell inspection: fate, prediction, fold, batch, modalities.")
115
+ with c2:
116
+ st.markdown(_NAV_SLOT.format(2), unsafe_allow_html=True)
117
  with st.container(border=True):
118
  st.page_link("pages/2_Feature_insights.py", label="Feature Insights", icon=":material/analytics:")
119
+ st.caption("Shift probes, attention rollout, cohort views, and full multimodal tables.")
120
+ with c3:
121
+ st.markdown(_NAV_SLOT.format(3), unsafe_allow_html=True)
122
  with st.container(border=True):
123
  st.page_link("pages/3_Flux_analysis.py", label="Flux Analysis", icon=":material/account_tree:")
124
  st.caption("Reaction pathways, differential flux, rankings, and model metadata.")
125
+ with c4:
126
+ st.markdown(_NAV_SLOT.format(4), unsafe_allow_html=True)
127
  with st.container(border=True):
128
  st.page_link(
129
  "pages/4_Gene_expression_analysis.py",
130
  label="Gene Expression & TF Activity",
131
  icon=":material/genetics:",
132
  )
133
+ st.caption("Pathway enrichment, motif activity, and searchable gene tables.")
134
 
135
+ st.markdown('<p class="ff-section-label">Overview</p>', unsafe_allow_html=True)
136
+
137
+ # --- Snapshot charts ---
138
+ if bundle is not None and df_features is not None:
139
+ latent_df = io.latent_join_samples(bundle, samples)
140
+ plot_umap = _downsample_latent_df(latent_df)
141
+ row1_story, row1_umap = st.columns([0.38, 0.62], gap="large")
142
+ with row1_story:
143
+ st.markdown(_BIOLOGY_CONTEXT_MARKDOWN)
144
+ with row1_umap:
145
+ st.caption("Each point is a cell · colours = experimental fate labels · validation split")
146
+ fig_u = plots.latent_scatter(
147
+ plot_umap,
148
+ "label",
149
+ title=_UMAP_HOME_TITLE,
150
+ width=780,
151
+ height=440,
152
+ marker_size=5.2,
153
+ marker_opacity=0.72,
154
+ )
155
+ fig_u.update_layout(margin=dict(l=20, r=8, t=52, b=20), title_font_size=15)
156
+ st.plotly_chart(
157
+ fig_u,
158
+ width="stretch",
159
+ config={"displayModeBar": True, "displaylogo": False, "modeBarButtonsToRemove": ["lasso2d", "select2d"]},
160
+ )
161
+
162
+ st.caption("Global shift and attention · top features by importance (min-max scaled within each bar chart) · modality mix as donut (top by mean rank).")
163
+ fig_g = plots.global_rank_triple_panel(
164
+ df_features,
165
+ top_n=_HOME_RANK_TOP_N,
166
+ top_n_pie=_HOME_PIE_TOP_N,
167
+ chart_outline=False,
168
+ modality_mix_hole=0.66,
169
+ )
170
+ fig_g.update_layout(title_text="", margin=dict(l=36, r=36, t=48, b=100))
171
+ fig_g.update_annotations(font_size=12)
172
+ st.plotly_chart(
173
+ fig_g,
174
+ width="stretch",
175
+ config={"displayModeBar": False, "displaylogo": False},
176
+ )
177
+ elif bundle is not None:
178
+ latent_df = io.latent_join_samples(bundle, samples)
179
+ plot_umap = _downsample_latent_df(latent_df)
180
+ u_story, u_map = st.columns([0.38, 0.62], gap="large")
181
+ with u_story:
182
+ st.markdown(_BIOLOGY_CONTEXT_MARKDOWN)
183
+ with u_map:
184
+ st.caption("Feature ranking cache unavailable · UMAP only")
185
+ fig_u = plots.latent_scatter(
186
+ plot_umap,
187
+ "label",
188
+ title=_UMAP_HOME_TITLE,
189
+ width=820,
190
+ height=480,
191
+ marker_size=5.5,
192
+ marker_opacity=0.72,
193
+ )
194
+ fig_u.update_layout(margin=dict(l=24, r=12, t=52, b=24), title_font_size=15)
195
+ st.plotly_chart(fig_u, width="stretch", config={"displayModeBar": True, "displaylogo": False})
196
+ elif df_features is not None:
197
+ st.caption("Feature ranking overview · latent UMAP unavailable")
198
+ fig_g = plots.global_rank_triple_panel(
199
+ df_features,
200
+ top_n=_HOME_RANK_TOP_N,
201
+ top_n_pie=_HOME_PIE_TOP_N,
202
+ chart_outline=False,
203
+ modality_mix_hole=0.66,
204
+ )
205
+ fig_g.update_layout(title_text="", margin=dict(l=36, r=36, t=48, b=100))
206
+ st.plotly_chart(fig_g, width="stretch", config={"displayModeBar": False, "displaylogo": False})
207
+ else:
208
+ st.info("Charts will appear here once latent and feature caches are available.")
streamlit_hf/lib/plots.py CHANGED
@@ -14,6 +14,8 @@ from streamlit_hf.lib.reactions import normalize_reaction_key
14
 
15
  # Matches Streamlit theme primary + slate text; used across Plotly layouts.
16
  PLOT_FONT = dict(family="Inter, system-ui, sans-serif", size=12)
 
 
17
 
18
  PALETTE = (
19
  "#2563eb",
@@ -148,15 +150,17 @@ def latent_scatter(
148
  else:
149
  color_arg = color_col
150
 
 
151
  common = dict(
152
  x="umap_x",
153
  y="umap_y",
154
  hover_data=hover_data,
155
  labels=labels_map,
156
- title=title,
157
  width=width,
158
  height=height,
159
  )
 
 
160
  if continuous:
161
  fig = px.scatter(
162
  d,
@@ -174,15 +178,20 @@ def latent_scatter(
174
  fig.update_traces(
175
  marker=dict(size=marker_size, opacity=marker_opacity, line=dict(width=0.25, color="rgba(255,255,255,0.4)"))
176
  )
 
177
  fig.update_layout(
178
  template="plotly_white",
179
  font=PLOT_FONT,
180
  title_font_size=16,
181
- margin=dict(l=28, r=20, t=56, b=28),
182
  legend_title_text="",
183
  xaxis_title="",
184
  yaxis_title="",
 
 
185
  )
 
 
186
  fig.update_xaxes(showticklabels=False, showgrid=True, gridcolor="rgba(0,0,0,0.06)", zeroline=False)
187
  fig.update_yaxes(showticklabels=False, showgrid=True, gridcolor="rgba(0,0,0,0.06)", zeroline=False)
188
  return fig
@@ -262,7 +271,7 @@ def _truncate_label(s: str, max_len: int = 36) -> str:
262
  def joint_shift_attention_top_features(df_mod, modality: str, top_n: int):
263
  """
264
  Top features by mean_rank (lowest = strongest joint shift+attention ranking).
265
- Shift and attention importances are minmax scaled within this top-N slice for side-by-side comparison.
266
  """
267
  need = ("mean_rank", "importance_shift", "importance_att", "feature")
268
  if not all(c in df_mod.columns for c in need):
@@ -512,10 +521,20 @@ def attention_cohort_view(
512
  return fig
513
 
514
 
515
- def global_rank_triple_panel(df_features, top_n: int = 20, top_n_pie: int = 100):
 
 
 
 
 
 
 
516
  """
517
- Global top-N by latent-shift and by attention (minmax scaled), plus pie of modality mix
518
  among the top `top_n_pie` features by mean rank.
 
 
 
519
  """
520
  d = df_features.copy()
521
  for col in ("importance_shift", "importance_att"):
@@ -542,13 +561,16 @@ def global_rank_triple_panel(df_features, top_n: int = 20, top_n_pie: int = 100)
542
  horizontal_spacing=0.06,
543
  )
544
 
 
 
 
545
  fig.add_trace(
546
  go.Bar(
547
  x=shift_top["importance_shift_norm"],
548
  y=shift_top["feature"],
549
  orientation="h",
550
  marker_color=[MODALITY_COLOR.get(m, "#64748b") for m in shift_top["modality"]],
551
- marker_line=dict(color="rgba(15,23,42,0.12)", width=1),
552
  showlegend=False,
553
  hovertemplate="%{y}<br>scaled shift: %{x:.3f}<extra></extra>",
554
  ),
@@ -561,7 +583,7 @@ def global_rank_triple_panel(df_features, top_n: int = 20, top_n_pie: int = 100)
561
  y=att_top["feature"],
562
  orientation="h",
563
  marker_color=[MODALITY_COLOR.get(m, "#64748b") for m in att_top["modality"]],
564
- marker_line=dict(color="rgba(15,23,42,0.12)", width=1),
565
  showlegend=False,
566
  hovertemplate="%{y}<br>scaled attention: %{x:.3f}<extra></extra>",
567
  ),
@@ -575,23 +597,47 @@ def global_rank_triple_panel(df_features, top_n: int = 20, top_n_pie: int = 100)
575
  if sum(pie_vals) == 0:
576
  pie_vals = [1, 1, 1]
577
 
 
 
 
578
  fig.add_trace(
579
  go.Pie(
580
  labels=pie_labels,
581
  values=pie_vals,
582
  marker=dict(
583
  colors=[MODALITY_PIE_COLOR.get(l, "#64748b") for l in pie_labels],
584
- line=dict(color="#1e293b", width=1.2),
585
  ),
586
  textinfo="label+percent",
587
  textfont_size=12,
588
- hole=0.0,
 
589
  showlegend=False,
590
  ),
591
  row=1,
592
  col=3,
593
  )
594
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  fig.update_xaxes(title_text="Min-max scaled shift", row=1, col=1)
596
  fig.update_xaxes(title_text="Min-max scaled attention", row=1, col=2)
597
  fig.update_yaxes(autorange="reversed", row=1, col=1)
@@ -603,9 +649,22 @@ def global_rank_triple_panel(df_features, top_n: int = 20, top_n_pie: int = 100)
603
  font=PLOT_FONT,
604
  height=h,
605
  width=min(1280, 400 + top_n * 14),
606
- margin=dict(l=40, r=40, t=80, b=40),
 
 
607
  title_text="Global feature ranking (all modalities)",
608
  title_x=0.5,
 
 
 
 
 
 
 
 
 
 
 
609
  )
610
  return fig
611
 
@@ -900,7 +959,7 @@ def pathway_enrichment_bubble_panel(
900
  title=dict(text=title, x=0.5, xanchor="center"),
901
  annotations=[
902
  dict(
903
- text="No significant pathways (BenjaminiHochberg q < 0.05)",
904
  xref="paper",
905
  yref="paper",
906
  x=0.5,
@@ -1165,12 +1224,12 @@ def pathway_gene_membership_heatmap(
1165
  fig.update_layout(
1166
  template="plotly_white",
1167
  font=PLOT_FONT,
1168
- title=dict(text="Pathwaygene membership", x=0.5, xanchor="center"),
1169
  height=h,
1170
  width=w,
1171
  margin=dict(l=4, r=168, t=52, b=108),
1172
- paper_bgcolor="rgba(0,0,0,0)",
1173
- plot_bgcolor="#f4f6f9",
1174
  )
1175
 
1176
  if use_spine:
 
14
 
15
  # Matches Streamlit theme primary + slate text; used across Plotly layouts.
16
  PLOT_FONT = dict(family="Inter, system-ui, sans-serif", size=12)
17
+ # Same as app / plotly_white paper so figures are not tinted vs the page.
18
+ PAGE_BG = "#ffffff"
19
 
20
  PALETTE = (
21
  "#2563eb",
 
150
  else:
151
  color_arg = color_col
152
 
153
+ # Plotly Express turns title="" into a visible "undefined" title in some versions; omit when empty.
154
  common = dict(
155
  x="umap_x",
156
  y="umap_y",
157
  hover_data=hover_data,
158
  labels=labels_map,
 
159
  width=width,
160
  height=height,
161
  )
162
+ if title:
163
+ common["title"] = title
164
  if continuous:
165
  fig = px.scatter(
166
  d,
 
178
  fig.update_traces(
179
  marker=dict(size=marker_size, opacity=marker_opacity, line=dict(width=0.25, color="rgba(255,255,255,0.4)"))
180
  )
181
+ top_margin = 56 if title else 28
182
  fig.update_layout(
183
  template="plotly_white",
184
  font=PLOT_FONT,
185
  title_font_size=16,
186
+ margin=dict(l=28, r=20, t=top_margin, b=28),
187
  legend_title_text="",
188
  xaxis_title="",
189
  yaxis_title="",
190
+ paper_bgcolor=PAGE_BG,
191
+ plot_bgcolor=PAGE_BG,
192
  )
193
+ if not title:
194
+ fig.update_layout(title=None)
195
  fig.update_xaxes(showticklabels=False, showgrid=True, gridcolor="rgba(0,0,0,0.06)", zeroline=False)
196
  fig.update_yaxes(showticklabels=False, showgrid=True, gridcolor="rgba(0,0,0,0.06)", zeroline=False)
197
  return fig
 
271
  def joint_shift_attention_top_features(df_mod, modality: str, top_n: int):
272
  """
273
  Top features by mean_rank (lowest = strongest joint shift+attention ranking).
274
+ Shift and attention importances are min-max scaled within this top-N slice for side-by-side comparison.
275
  """
276
  need = ("mean_rank", "importance_shift", "importance_att", "feature")
277
  if not all(c in df_mod.columns for c in need):
 
521
  return fig
522
 
523
 
524
+ def global_rank_triple_panel(
525
+ df_features,
526
+ top_n: int = 20,
527
+ top_n_pie: int = 100,
528
+ *,
529
+ chart_outline: bool = True,
530
+ modality_mix_hole: float = 0.0,
531
+ ):
532
  """
533
+ Global top-N by latent-shift and by attention (min-max scaled), plus pie or donut of modality mix
534
  among the top `top_n_pie` features by mean rank.
535
+
536
+ Set ``chart_outline=False`` for a flatter look (e.g. home page); Feature Insights keeps outlines by default.
537
+ Set ``modality_mix_hole`` in (0, 1), e.g. ``0.66``, for a donut instead of a full pie (e.g. home page).
538
  """
539
  d = df_features.copy()
540
  for col in ("importance_shift", "importance_att"):
 
561
  horizontal_spacing=0.06,
562
  )
563
 
564
+ bar_outline = dict(color="#1e293b", width=1.2) if chart_outline else dict(width=0)
565
+ pie_line = dict(color="#1e293b", width=1.2) if chart_outline else dict(width=0)
566
+ leg_line = dict(width=1.2, color="#1e293b") if chart_outline else dict(width=0)
567
  fig.add_trace(
568
  go.Bar(
569
  x=shift_top["importance_shift_norm"],
570
  y=shift_top["feature"],
571
  orientation="h",
572
  marker_color=[MODALITY_COLOR.get(m, "#64748b") for m in shift_top["modality"]],
573
+ marker_line=bar_outline,
574
  showlegend=False,
575
  hovertemplate="%{y}<br>scaled shift: %{x:.3f}<extra></extra>",
576
  ),
 
583
  y=att_top["feature"],
584
  orientation="h",
585
  marker_color=[MODALITY_COLOR.get(m, "#64748b") for m in att_top["modality"]],
586
+ marker_line=bar_outline,
587
  showlegend=False,
588
  hovertemplate="%{y}<br>scaled attention: %{x:.3f}<extra></extra>",
589
  ),
 
597
  if sum(pie_vals) == 0:
598
  pie_vals = [1, 1, 1]
599
 
600
+ _hole = float(modality_mix_hole) if modality_mix_hole and modality_mix_hole > 0 else 0.0
601
+ # Narrow third subplot: "auto" avoids clipped outside labels on donuts.
602
+ _pie_textpos = "auto"
603
  fig.add_trace(
604
  go.Pie(
605
  labels=pie_labels,
606
  values=pie_vals,
607
  marker=dict(
608
  colors=[MODALITY_PIE_COLOR.get(l, "#64748b") for l in pie_labels],
609
+ line=pie_line,
610
  ),
611
  textinfo="label+percent",
612
  textfont_size=12,
613
+ textposition=_pie_textpos,
614
+ hole=_hole,
615
  showlegend=False,
616
  ),
617
  row=1,
618
  col=3,
619
  )
620
 
621
+ # Modality legend (bar colours + pie segment colours): invisible markers in first subplot only.
622
+ for _name, _col in (
623
+ ("RNA (transcriptome)", MODALITY_PIE_COLOR["RNA"]),
624
+ ("ATAC (chromatin)", MODALITY_PIE_COLOR["ATAC"]),
625
+ ("Flux (metabolism)", MODALITY_PIE_COLOR["Flux"]),
626
+ ):
627
+ fig.add_trace(
628
+ go.Scatter(
629
+ x=[None],
630
+ y=[None],
631
+ mode="markers",
632
+ marker=dict(size=12, color=_col, symbol="square", line=leg_line),
633
+ name=_name,
634
+ showlegend=True,
635
+ hoverinfo="skip",
636
+ ),
637
+ row=1,
638
+ col=1,
639
+ )
640
+
641
  fig.update_xaxes(title_text="Min-max scaled shift", row=1, col=1)
642
  fig.update_xaxes(title_text="Min-max scaled attention", row=1, col=2)
643
  fig.update_yaxes(autorange="reversed", row=1, col=1)
 
649
  font=PLOT_FONT,
650
  height=h,
651
  width=min(1280, 400 + top_n * 14),
652
+ margin=dict(l=40, r=40, t=80, b=108),
653
+ paper_bgcolor=PAGE_BG,
654
+ plot_bgcolor=PAGE_BG,
655
  title_text="Global feature ranking (all modalities)",
656
  title_x=0.5,
657
+ legend=dict(
658
+ title=dict(text="Modality colour key", font=dict(size=11, family=PLOT_FONT["family"])),
659
+ orientation="h",
660
+ yanchor="top",
661
+ y=-0.14,
662
+ xanchor="center",
663
+ x=0.5,
664
+ font=dict(size=11, family=PLOT_FONT["family"]),
665
+ traceorder="normal",
666
+ itemsizing="constant",
667
+ ),
668
  )
669
  return fig
670
 
 
959
  title=dict(text=title, x=0.5, xanchor="center"),
960
  annotations=[
961
  dict(
962
+ text="No significant pathways (Benjamini-Hochberg q < 0.05)",
963
  xref="paper",
964
  yref="paper",
965
  x=0.5,
 
1224
  fig.update_layout(
1225
  template="plotly_white",
1226
  font=PLOT_FONT,
1227
+ title=dict(text="Pathway-gene membership", x=0.5, xanchor="center"),
1228
  height=h,
1229
  width=w,
1230
  margin=dict(l=4, r=168, t=52, b=108),
1231
+ paper_bgcolor=PAGE_BG,
1232
+ plot_bgcolor=PAGE_BG,
1233
  )
1234
 
1235
  if use_spine:
streamlit_hf/lib/ui.py CHANGED
@@ -6,10 +6,55 @@ import streamlit as st
6
 
7
 
8
  def inject_app_styles() -> None:
9
- """Panel labels and home cards; safe to call on every rerun (small CSS block)."""
10
  st.markdown(
11
  """
12
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  .latent-panel-title {
14
  font-size: 0.82rem;
15
  font-weight: 600;
@@ -22,3 +67,157 @@ def inject_app_styles() -> None:
22
  """,
23
  unsafe_allow_html=True,
24
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
 
8
  def inject_app_styles() -> None:
9
+ """Panel labels, page background, and shared chrome (all pages)."""
10
  st.markdown(
11
  """
12
  <style>
13
+ /*
14
+ * Full page: white (#fff, same as Plotly plotly_white paper) + subtle dot texture only.
15
+ * Line grid is reserved for the home banner (.ff-hero), not the app shell.
16
+ */
17
+ .stApp {
18
+ background-color: #ffffff !important;
19
+ background-image: radial-gradient(rgba(15, 23, 42, 0.055) 1px, transparent 1px) !important;
20
+ background-size: 20px 20px !important;
21
+ background-attachment: fixed !important;
22
+ }
23
+ [data-testid="stAppViewContainer"] .block-container {
24
+ background-color: transparent !important;
25
+ }
26
+ [data-testid="stHeader"] {
27
+ background-color: #ffffff !important;
28
+ background-image: radial-gradient(rgba(15, 23, 42, 0.055) 1px, transparent 1px) !important;
29
+ background-size: 20px 20px !important;
30
+ border-bottom: 1px solid rgba(226, 232, 240, 0.95);
31
+ backdrop-filter: none;
32
+ }
33
+ /* Plotly embed: match page paper colour (avoids grey Streamlit chrome around charts) */
34
+ [data-testid="stPlotlyChart"],
35
+ [data-testid="stPlotlyChart"] > div,
36
+ [data-testid="stPlotlyChart"] .js-plotly-plot,
37
+ [data-testid="stPlotlyChart"] .plotly-graph-div {
38
+ background-color: #ffffff !important;
39
+ }
40
+ /* Sidebar: distinct from main white canvas (theme secondary + light edge) */
41
+ [data-testid="stSidebar"] {
42
+ background-color: #f1f5fb !important;
43
+ background-image: radial-gradient(rgba(15, 23, 42, 0.045) 1px, transparent 1px) !important;
44
+ background-size: 18px 18px !important;
45
+ border-right: 1px solid rgba(148, 163, 184, 0.35) !important;
46
+ }
47
+ [data-testid="stSidebar"] [data-testid="stSidebarNavLink"] p,
48
+ [data-testid="stSidebar"] [data-testid="stSidebarNavLink"] span {
49
+ font-size: 1.05rem !important;
50
+ line-height: 1.35 !important;
51
+ }
52
+ /* st.title() headings in main column (hero title keeps its own rule below on home) */
53
+ section[data-testid="stMain"] h1 {
54
+ font-size: clamp(1.95rem, 3.2vw, 2.35rem) !important;
55
+ font-weight: 700 !important;
56
+ letter-spacing: -0.02em !important;
57
+ }
58
  .latent-panel-title {
59
  font-size: 0.82rem;
60
  font-weight: 600;
 
67
  """,
68
  unsafe_allow_html=True,
69
  )
70
+
71
+
72
+ def inject_home_landing_styles() -> None:
73
+ """Hero, nav cards, and section labels (home page only)."""
74
+ st.markdown(
75
+ """
76
+ <style>
77
+ .ff-hero {
78
+ position: relative;
79
+ overflow: hidden;
80
+ border-radius: 16px;
81
+ padding: 1.5rem 1.85rem 1.45rem;
82
+ margin-bottom: 1.35rem;
83
+ border: 1px solid rgba(129, 140, 248, 0.45);
84
+ box-shadow:
85
+ 0 4px 24px rgba(30, 27, 75, 0.18),
86
+ inset 0 1px 0 rgba(255, 255, 255, 0.12);
87
+ background:
88
+ linear-gradient(118deg, rgba(15, 23, 42, 0.97) 0%, rgba(49, 46, 129, 0.94) 38%, rgba(67, 56, 202, 0.92) 72%, rgba(79, 70, 229, 0.88) 100%);
89
+ }
90
+ /* Banner: visible line grid (Shape2Force-style) over the gradient */
91
+ .ff-hero::before {
92
+ content: "";
93
+ position: absolute;
94
+ inset: 0;
95
+ opacity: 0.55;
96
+ pointer-events: none;
97
+ background-image:
98
+ linear-gradient(rgba(255, 255, 255, 0.11) 1px, transparent 1px),
99
+ linear-gradient(90deg, rgba(255, 255, 255, 0.11) 1px, transparent 1px);
100
+ background-size: 20px 20px;
101
+ }
102
+ .ff-hero::after {
103
+ content: "";
104
+ position: absolute;
105
+ inset: 0;
106
+ pointer-events: none;
107
+ background: radial-gradient(ellipse 85% 65% at 18% 0%, rgba(129, 140, 248, 0.35) 0%, transparent 55%);
108
+ }
109
+ .ff-hero-inner {
110
+ position: relative;
111
+ z-index: 1;
112
+ }
113
+ .ff-hero-title-row {
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 0.55rem;
117
+ flex-wrap: wrap;
118
+ margin-bottom: 0.4rem;
119
+ }
120
+ .ff-hero-emoji {
121
+ font-size: clamp(1.75rem, 4.5vw, 2.35rem);
122
+ line-height: 1;
123
+ filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.2));
124
+ user-select: none;
125
+ }
126
+ /* Narrower hero title so it does not inherit the large st.title size above */
127
+ section[data-testid="stMain"] .ff-hero .ff-hero-text h1 {
128
+ font-size: clamp(1.65rem, 4vw, 2.1rem) !important;
129
+ font-weight: 700 !important;
130
+ margin: 0 !important;
131
+ color: #f8fafc !important;
132
+ letter-spacing: -0.03em !important;
133
+ line-height: 1.15 !important;
134
+ text-shadow: 0 1px 18px rgba(0, 0, 0, 0.25) !important;
135
+ }
136
+ .ff-hero-sub {
137
+ margin: 0;
138
+ max-width: 52rem;
139
+ font-size: 0.98rem;
140
+ line-height: 1.55;
141
+ color: rgba(226, 232, 240, 0.95);
142
+ font-weight: 400;
143
+ }
144
+ .ff-section-label {
145
+ font-size: 0.72rem;
146
+ font-weight: 600;
147
+ text-transform: uppercase;
148
+ letter-spacing: 0.12em;
149
+ color: #64748b;
150
+ margin: 0 0 0.35rem 0;
151
+ }
152
+ .ff-nav-slot-marker {
153
+ display: block !important;
154
+ width: 0 !important;
155
+ height: 0 !important;
156
+ margin: 0 !important;
157
+ padding: 0 !important;
158
+ overflow: hidden !important;
159
+ clip-path: inset(50%) !important;
160
+ }
161
+ /*
162
+ * Home workspace nav cards: each column renders span#ff-nav-slot-N, then the bordered tile.
163
+ * Target the bordered wrapper as the next sibling block after the markdown that contains the span.
164
+ */
165
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-1) + * [data-testid="stVerticalBlockBorderWrapper"],
166
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-1) + * + * [data-testid="stVerticalBlockBorderWrapper"],
167
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-2) + * [data-testid="stVerticalBlockBorderWrapper"],
168
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-2) + * + * [data-testid="stVerticalBlockBorderWrapper"],
169
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-3) + * [data-testid="stVerticalBlockBorderWrapper"],
170
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-3) + * + * [data-testid="stVerticalBlockBorderWrapper"],
171
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-4) + * [data-testid="stVerticalBlockBorderWrapper"],
172
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-4) + * + * [data-testid="stVerticalBlockBorderWrapper"] {
173
+ border-radius: 14px !important;
174
+ transition: box-shadow 0.15s ease, border-color 0.15s ease !important;
175
+ }
176
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-1) + * [data-testid="stVerticalBlockBorderWrapper"],
177
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-1) + * + * [data-testid="stVerticalBlockBorderWrapper"] {
178
+ background: linear-gradient(152deg, #eff6ff 0%, #ffffff 52%, #dbeafe 100%) !important;
179
+ border: 1px solid rgba(37, 99, 235, 0.38) !important;
180
+ box-shadow: 0 2px 14px rgba(37, 99, 235, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.92) !important;
181
+ }
182
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-1) + * [data-testid="stVerticalBlockBorderWrapper"]:hover,
183
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-1) + * + * [data-testid="stVerticalBlockBorderWrapper"]:hover {
184
+ border-color: rgba(29, 78, 216, 0.55) !important;
185
+ box-shadow: 0 6px 22px rgba(37, 99, 235, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.95) !important;
186
+ }
187
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-2) + * [data-testid="stVerticalBlockBorderWrapper"],
188
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-2) + * + * [data-testid="stVerticalBlockBorderWrapper"] {
189
+ background: linear-gradient(152deg, #fff7ed 0%, #ffffff 52%, #ffedd5 100%) !important;
190
+ border: 1px solid rgba(234, 88, 12, 0.35) !important;
191
+ box-shadow: 0 2px 14px rgba(234, 88, 12, 0.09), inset 0 1px 0 rgba(255, 255, 255, 0.92) !important;
192
+ }
193
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-2) + * [data-testid="stVerticalBlockBorderWrapper"]:hover,
194
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-2) + * + * [data-testid="stVerticalBlockBorderWrapper"]:hover {
195
+ border-color: rgba(194, 65, 12, 0.5) !important;
196
+ box-shadow: 0 6px 22px rgba(234, 88, 12, 0.14), inset 0 1px 0 rgba(255, 255, 255, 0.95) !important;
197
+ }
198
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-3) + * [data-testid="stVerticalBlockBorderWrapper"],
199
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-3) + * + * [data-testid="stVerticalBlockBorderWrapper"] {
200
+ background: linear-gradient(152deg, #ecfdf5 0%, #ffffff 52%, #d1fae5 100%) !important;
201
+ border: 1px solid rgba(5, 150, 105, 0.36) !important;
202
+ box-shadow: 0 2px 14px rgba(5, 150, 105, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.92) !important;
203
+ }
204
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-3) + * [data-testid="stVerticalBlockBorderWrapper"]:hover,
205
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-3) + * + * [data-testid="stVerticalBlockBorderWrapper"]:hover {
206
+ border-color: rgba(4, 120, 87, 0.52) !important;
207
+ box-shadow: 0 6px 22px rgba(5, 150, 105, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.95) !important;
208
+ }
209
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-4) + * [data-testid="stVerticalBlockBorderWrapper"],
210
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-4) + * + * [data-testid="stVerticalBlockBorderWrapper"] {
211
+ background: linear-gradient(152deg, #f5f3ff 0%, #ffffff 52%, #ede9fe 100%) !important;
212
+ border: 1px solid rgba(124, 58, 237, 0.34) !important;
213
+ box-shadow: 0 2px 14px rgba(124, 58, 237, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.92) !important;
214
+ }
215
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-4) + * [data-testid="stVerticalBlockBorderWrapper"]:hover,
216
+ section[data-testid="stMain"] *:has(span#ff-nav-slot-4) + * + * [data-testid="stVerticalBlockBorderWrapper"]:hover {
217
+ border-color: rgba(109, 40, 217, 0.5) !important;
218
+ box-shadow: 0 6px 22px rgba(124, 58, 237, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.95) !important;
219
+ }
220
+ </style>
221
+ """,
222
+ unsafe_allow_html=True,
223
+ )
streamlit_hf/pages/4_Gene_expression_analysis.py CHANGED
@@ -33,7 +33,7 @@ if rna.empty and atac.empty:
33
  st.stop()
34
 
35
  st.caption(
36
- "Pathway enrichment (Reactome / KEGG) and a pathwaygene map; chromVAR-style motif deviations and activity by "
37
  "fate; sortable gene and motif tables. Use **Feature Insights** for global shift and attention rankings across modalities."
38
  )
39
 
@@ -68,7 +68,7 @@ tab_path, tab_motif, tab_gene_tbl, tab_motif_tbl = st.tabs(
68
 
69
  with tab_path:
70
  st.caption(
71
- "Over-representation of Reactome and KEGG pathways (BenjaminiHochberg *q* < 0.05). "
72
  "The lower panel maps leading genes to pathways; empty grid positions are left clear."
73
  )
74
  raw = pathway_data.load_de_re_tsv()
@@ -86,7 +86,7 @@ with tab_path:
86
  st.plotly_chart(
87
  plots.pathway_enrichment_bubble_panel(
88
  mde,
89
- "Pathway enrichment dead-end",
90
  show_colorbar=True,
91
  layout_height=bubble_h,
92
  ),
@@ -96,7 +96,7 @@ with tab_path:
96
  st.plotly_chart(
97
  plots.pathway_enrichment_bubble_panel(
98
  mre,
99
- "Pathway enrichment reprogramming",
100
  show_colorbar=True,
101
  layout_height=bubble_h,
102
  ),
@@ -104,7 +104,7 @@ with tab_path:
104
  )
105
  hm = pathway_data.build_merged_pathway_membership(de_all, re_all)
106
  if hm is None:
107
- st.info("No pathwaygene matrix could be built from the current enrichment results.")
108
  else:
109
  z, ylabs, xlabs = hm
110
  st.plotly_chart(plots.pathway_gene_membership_heatmap(z, ylabs, xlabs), width="stretch")
 
33
  st.stop()
34
 
35
  st.caption(
36
+ "Pathway enrichment (Reactome / KEGG) and a pathway-gene map; chromVAR-style motif deviations and activity by "
37
  "fate; sortable gene and motif tables. Use **Feature Insights** for global shift and attention rankings across modalities."
38
  )
39
 
 
68
 
69
  with tab_path:
70
  st.caption(
71
+ "Over-representation of Reactome and KEGG pathways (Benjamini-Hochberg *q* < 0.05). "
72
  "The lower panel maps leading genes to pathways; empty grid positions are left clear."
73
  )
74
  raw = pathway_data.load_de_re_tsv()
 
86
  st.plotly_chart(
87
  plots.pathway_enrichment_bubble_panel(
88
  mde,
89
+ "Pathway enrichment: dead-end",
90
  show_colorbar=True,
91
  layout_height=bubble_h,
92
  ),
 
96
  st.plotly_chart(
97
  plots.pathway_enrichment_bubble_panel(
98
  mre,
99
+ "Pathway enrichment: reprogramming",
100
  show_colorbar=True,
101
  layout_height=bubble_h,
102
  ),
 
104
  )
105
  hm = pathway_data.build_merged_pathway_membership(de_all, re_all)
106
  if hm is None:
107
+ st.info("No pathway-gene matrix could be built from the current enrichment results.")
108
  else:
109
  z, ylabs, xlabs = hm
110
  st.plotly_chart(plots.pathway_gene_membership_heatmap(z, ylabs, xlabs), width="stretch")