Spaces:
Running
Running
designed home page
Browse files- streamlit_hf/.streamlit/config.toml +4 -2
- streamlit_hf/app.py +1 -1
- streamlit_hf/home.py +171 -21
- streamlit_hf/lib/plots.py +73 -14
- streamlit_hf/lib/ui.py +200 -1
- streamlit_hf/pages/4_Gene_expression_analysis.py +5 -5
streamlit_hf/.streamlit/config.toml
CHANGED
|
@@ -1,7 +1,9 @@
|
|
| 1 |
[theme]
|
| 2 |
primaryColor = "#2563eb"
|
| 3 |
-
|
| 4 |
-
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
ui.inject_app_styles()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
if not
|
| 25 |
st.warning(
|
| 26 |
-
"
|
|
|
|
| 27 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
else:
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
| 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("
|
| 37 |
-
with
|
|
|
|
| 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
|
| 41 |
-
with
|
|
|
|
| 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 |
-
|
| 46 |
-
|
| 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
|
| 54 |
|
| 55 |
-
st.markdown("--
|
| 56 |
-
|
| 57 |
-
|
| 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=
|
| 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 min
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
"""
|
| 517 |
-
Global top-N by latent-shift and by attention (min
|
| 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=
|
| 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=
|
| 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=
|
| 585 |
),
|
| 586 |
textinfo="label+percent",
|
| 587 |
textfont_size=12,
|
| 588 |
-
|
|
|
|
| 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=
|
|
|
|
|
|
|
| 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 (Benjamini
|
| 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="Pathway
|
| 1169 |
height=h,
|
| 1170 |
width=w,
|
| 1171 |
margin=dict(l=4, r=168, t=52, b=108),
|
| 1172 |
-
paper_bgcolor=
|
| 1173 |
-
plot_bgcolor=
|
| 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
|
| 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 pathway
|
| 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 (Benjamini
|
| 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
|
| 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
|
| 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 pathway
|
| 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")
|