File size: 9,828 Bytes
475bc81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
02bd35b
 
475bc81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
"""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)