ProtHGT / ui.py
ervau's picture
implement ec prediction in the app and interface
02bd35b
Raw History Blame Contribute Delete
9.83 kB
"""Shared look-and-feel for the ProtHGT Streamlit pages (header, CSS, stepper)."""
import streamlit as st
ACCENT = "#3B4BD8"
GITHUB_URL = "https://github.com/HUBioDataLab/ProtHGT"
PAPER_URL = "https://www.biorxiv.org/content/10.1101/2025.04.19.649272"
# Minimum score behind each strictness preset (used by the app and the user guide).
# Replace "Balanced" with your validation-set Fmax threshold(s) to match the paper exactly.
STRICTNESS = {
"Precise": (0.70, "Fewer, high-confidence terms"),
"Balanced": (0.50, "Recommended default"),
"Exploratory": (0.30, "More candidates to screen"),
}
# How to read the knowledge graph view. Shown in the User guide and in the "How to read this graph"
# expander of the Knowledge graph tab, so the two stay in sync.
KG_VIEW_GUIDE = """
The view shows the part of the ProtHGT knowledge graph around one query protein, drawn at the centre
with a dark ring. Direct neighbours sit on an inner ring, grouped by kind (confirmed predictions, novel
predictions, known annotations, then other node types); second-degree neighbours sit on an outer ring
next to the neighbour they connect to. Choose the protein, then click **Show graph**.
- **Predicted GO terms** – how many of the highest-scoring predictions are shown for each predicted
GO category.
- **Neighbours per edge type** – how many knowledge-graph neighbours are shown for each relationship
type (domains, interactions, pathways, …), including known GO annotations outside the top predictions.
- **Second-degree neighbours** – set above 0 to also expand the neighbours' own connections, excluding
edges back to the query protein, up to that many per edge type. Keep it low: the graph quickly
becomes cluttered. Second-degree edges always come from the knowledge graph, never from predictions.
Node colours mark the entity type (see the legend at the top of the graph). Predictions are drawn for the
selected protein only; select another protein in **Protein** to see its predictions.
Edge colours mark the kind of link:
- **Purple** – predicted GO term that is also annotated in the knowledge graph. Known annotations that
the model predicted are purple even when they rank below the top terms set by **Predicted GO terms**.
- **Red** – novel prediction (one of the top predicted terms) not annotated in the knowledge graph.
- **Blue** – GO annotation in the knowledge graph that the model did not predict in this run.
- **Grey** – other relationships, such as protein–protein interactions, domains, drug targets, pathways,
domain–GO mappings and GO hierarchy links.
Interacting with the graph:
- **Hover** a node to highlight its connections and show their relationship names, with a card giving the
node's name, type and a link to the source database (UniProt, QuickGO, InterPro, …). Hover an edge to see
the relationship and, for GO terms, the score.
- **Drag** nodes to rearrange them; **scroll** to zoom and drag the background to pan.
- **Click a legend item** to hide that node type or edge kind; nodes left without visible edges are hidden
too. Click it again to bring it back.
- Use the icons at the top right to **reset** the view, save it as a **PNG**, or download the edges currently
shown as **JSON** (with their scores and edge kind; hidden edge kinds and node types are left out).
For exploration beyond second-degree connections, download the complete knowledge graph from the link at
the top of the Knowledge graph tab.
"""
_CSS = """
<style>
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=DM+Mono:wght@400;500&display=swap');
/* Material icons (incl. st.status's stExpanderIcon*) must keep their icon font */
.stApp :is(p, h1, h2, h3, h4, h5, label, li, a, button, input, textarea, select, div, span):not([data-testid^="stIcon"]):not([data-testid^="stExpanderIcon"]) {
font-family: 'Manrope', sans-serif;
}
code, .mono { font-family: 'DM Mono', monospace !important; }
/* Hide the sidebar entirely: navigation lives in the header */
[data-testid="stSidebar"], [data-testid="stSidebarCollapsedControl"] { display: none; }
/* Streamlit's fixed top bar (3.75rem) overlaps the page: make it transparent and start content below it */
header[data-testid="stHeader"] { background: transparent; }
.block-container { max-width: 1100px; padding-top: 4.25rem; padding-bottom: 4rem; }
/* Header */
.phgt-brand { font-weight: 800; font-size: 1.35rem; letter-spacing: -0.01em; margin: 0 !important; line-height: 2.4rem; }
div[data-testid="stPageLink"] a { justify-content: flex-end; }
div[data-testid="stPageLink"] p { font-weight: 500; color: #4A4E5C; }
/* Hero */
.phgt-hero h1 { font-size: 2.1rem; font-weight: 800; letter-spacing: -0.02em; line-height: 1.15; margin: 1.6rem 0 0.4rem; padding: 0; }
.phgt-hero p { font-size: 1.05rem; color: #4A4E5C; margin: 0 0 1.4rem; } /* full width, like the stepper */
.phgt-hero-spacer { height: 1.5rem; } /* step 3 has no hero: keep the stepper clear of the header */
/* Stepper */
.phgt-steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; list-style: none; margin: 0 0 1.5rem; padding: 0; }
.phgt-step { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-radius: 12px; background: #FFFFFF; border: 1px solid #E4E2DC; }
.phgt-step.done { background: #EEF0FC; border-color: #EEF0FC; }
.phgt-step.current { border: 2px solid ACCENT; }
.phgt-dot { flex: none; width: 30px; height: 30px; border-radius: 999px; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 14px; background: #EDECE7; color: #4A4E5C; box-sizing: border-box; }
.phgt-step.done .phgt-dot { background: ACCENT; color: #FFFFFF; }
.phgt-step.current .phgt-dot { background: #1E288A; color: #FFFFFF; }
.phgt-step small { display: block; font-size: 12px; color: #4A4E5C; }
.phgt-step span.t { font-weight: 700; font-size: 15px; }
@media (max-width: 640px) {
/* keep the header links on one wrapped row instead of stacking */
div[data-testid="stHorizontalBlock"]:has(.phgt-brand) { flex-direction: row !important; flex-wrap: wrap !important; gap: 0 14px !important; }
div[data-testid="stHorizontalBlock"]:has(.phgt-brand) > div[data-testid="stColumn"] { width: max-content !important; flex: 0 0 max-content !important; min-width: 0 !important; }
div[data-testid="stHorizontalBlock"]:has(.phgt-brand) > div[data-testid="stColumn"]:first-child { flex: 1 0 100% !important; }
div[data-testid="stHorizontalBlock"]:has(.phgt-brand) div[data-testid="stPageLink"] { width: auto !important; }
.phgt-step small { display: none; }
.phgt-step span.t { font-size: 13px; }
.phgt-hero h1 { font-size: 1.8rem; }
}
/* Cards & chips */
div[data-testid="stVerticalBlockBorderWrapper"] { border-radius: 16px; }
/* Dataframes use the same frame as the cards (Streamlit sets the border inline, hence !important) */
div[data-testid="stDataFrameResizable"] { border: 1px solid #E4E2DC !important; border-radius: 16px !important; overflow: hidden; }
div[data-testid="stDataFrame"] .stDataFrameGlideDataEditor { border-radius: 16px; }
.phgt-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 10px; }
.phgt-chip { padding: 4px 9px; border-radius: 8px; background: #F3F2EE; font: 500 13px 'DM Mono', monospace; }
.phgt-muted { color: #4A4E5C; font-size: 0.9rem; }
/* Small category label above a card title (e.g. "Gene Ontology" over "Molecular function") */
.phgt-eyebrow { display: block; font-size: 0.68rem; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: ACCENT; margin-bottom: 2px; }
.phgt-meter { height: 6px; border-radius: 3px; background: #EDECE7; margin: 6px 0 4px; }
.phgt-meter > div { height: 6px; border-radius: 3px; background: ACCENT; }
.phgt-count { display: block; padding-bottom: 0.6rem; } /* breathing room below the protein count */
/* Multiselect chips use the accent tint instead of Streamlit red */
span[data-baseweb="tag"] { background-color: #E2E5FB !important; color: #1E288A !important; font-family: 'DM Mono', monospace; }
span[data-baseweb="tag"] svg { fill: #1E288A !important; }
/* Primary buttons */
.stButton > button, .stDownloadButton > button { border-radius: 12px; min-height: 44px; font-weight: 600; }
.stButton > button[kind="primary"] { font-weight: 700; }
/* Metrics */
div[data-testid="stMetric"] { background: #FFFFFF; border: 1px solid #E4E2DC; border-radius: 14px; padding: 14px 16px; }
</style>
""".replace("ACCENT", ACCENT)
def apply_style():
st.markdown(_CSS, unsafe_allow_html=True)
def render_header(current="predict"):
apply_style()
cols = st.columns([3, 1.1, 1.1, 0.9, 1.1], vertical_alignment="center")
with cols[0]:
st.markdown('<p class="phgt-brand">ProtHGT</p>', unsafe_allow_html=True)
with cols[1]:
st.page_link("ProtHGT_app.py", label="Predict")
with cols[2]:
st.page_link("pages/User_Guide.py", label="User guide")
with cols[3]:
st.page_link("pages/About.py", label="About")
with cols[4]:
st.page_link(GITHUB_URL, label="GitHub")
def render_stepper(step):
labels = ["Add proteins", "Choose what to predict", "Review results"]
items = []
for i, label in enumerate(labels, start=1):
if i < step:
cls, mark, state = "done", "&#10003;", ""
elif i == step:
cls, mark, state = "current", str(i), ' aria-current="step"'
else:
cls, mark, state = "", str(i), ""
items.append(
f'<li class="phgt-step {cls}"{state}><span class="phgt-dot">{mark}</span>'
f'<span><small>Step {i}</small><span class="t">{label}</span></span></li>'
)
st.markdown(f'<ol class="phgt-steps" aria-label="Progress">{"".join(items)}</ol>', unsafe_allow_html=True)