Declutter the Database tab
#12
by lvwerra HF Staff - opened
app.css
CHANGED
|
@@ -3,14 +3,8 @@
|
|
| 3 |
#eukaryotic-atlas { padding:0!important; border:0!important; background:transparent!important; }
|
| 4 |
.gradio-container .atlas-panel { background:transparent; border:0; border-radius:0; padding:22px 20px; box-shadow:none; gap:16px; margin-top:0; }
|
| 5 |
.gradio-container .atlas-panel + .atlas-panel { border-top:1px solid #e3e9dc; }
|
| 6 |
-
.workspace-heading { display:flex; align-items:flex-start; gap:14px; padding:0 0 4px; }
|
| 7 |
-
.workspace-step { width:32px; height:32px; border:1px solid #cdddc8; background:#eaf1e2; border-radius:50%; display:flex; align-items:center; justify-content:center; flex-shrink:0; font:500 11px Arial,sans-serif; color:#527261; margin-top:4px; }
|
| 8 |
-
.workspace-heading h2 { margin:0 0 8px; font:400 32px/1.15 Georgia,'Times New Roman',serif; letter-spacing:-.7px; color:#173c30; }
|
| 9 |
-
.workspace-heading p { margin:0; color:#647467; font:13px/1.6 Arial,Helvetica,sans-serif; }
|
| 10 |
.gradio-container .quiet-note { color:#647467; font-size:12px; line-height:1.7; }
|
| 11 |
.gradio-container .quiet-note p { color:#647467; font-size:12px; line-height:1.7; }
|
| 12 |
-
.gradio-container .scope-note { padding:11px 15px; border:1px solid #e0e6d8; border-radius:12px; background:#edf3e7; }
|
| 13 |
-
.gradio-container .scope-note p { margin:0; }
|
| 14 |
.gradio-container .action-button { align-self:flex-end; min-height:46px; }
|
| 15 |
/* The jump out of the atlas should read as the one action on the page, not as
|
| 16 |
another quiet control among the disclosures. */
|
|
@@ -39,15 +33,10 @@
|
|
| 39 |
#annotation-examples { font-size:12px; }
|
| 40 |
#annotation-examples .label { color:#647467; }
|
| 41 |
#annotation-results { border-radius:14px; }
|
| 42 |
-
.workspace-footer { border-top:1px solid #e0e6d8; padding-top:18px; margin-top:6px; display:flex; justify-content:space-between; flex-wrap:wrap; gap:8px; font:11px/1.6 Arial,sans-serif; color:#75836f; }
|
| 43 |
-
.workspace-footer a { color:#315641; text-decoration:none; }
|
| 44 |
-
.workspace-footer a:hover { text-decoration:underline; }
|
| 45 |
@media(max-width:700px) {
|
| 46 |
.gradio-container { padding:12px 10px 24px!important; }
|
| 47 |
.gradio-container .atlas-panel { padding:18px 12px; }
|
| 48 |
#atlas-navigation { border-radius:18px; padding:8px 8px 0; }
|
| 49 |
-
.workspace-heading h2 { font-size:28px; }
|
| 50 |
-
.workspace-heading { gap:10px; }
|
| 51 |
.gradio-container .action-button { width:100%; }
|
| 52 |
}
|
| 53 |
@media(prefers-reduced-motion:reduce) { .gradio-container .atlas-panel button { transition:none; } }
|
|
@@ -67,11 +56,25 @@
|
|
| 67 |
#atlas-navigation > .tab-wrapper > .tab-container > button::before, #atlas-navigation > .tab-wrapper > .tab-container > button::after { content:none!important; display:none!important; }
|
| 68 |
#atlas-navigation > .tab-wrapper > .overflow-menu { display:none!important; }
|
| 69 |
#atlas-overview, #atlas-database { border:0; background:transparent; padding:0; min-width:0; max-width:100%; }
|
| 70 |
-
.database-heading { padding:14px 4px 12px; }
|
| 71 |
-
.database-heading .database-eyebrow { color:#647467; font:11px/1.4 Arial,Helvetica,sans-serif; letter-spacing:2px; margin:0 0 12px; }
|
| 72 |
-
.database-heading h1 { color:#173c30; font:400 clamp(32px,5vw,48px)/1.1 Georgia,'Times New Roman',serif; letter-spacing:-1px; margin:0 0 12px; }
|
| 73 |
-
.database-heading p { color:#647467; font:14px/1.6 Arial,Helvetica,sans-serif; margin:0; }
|
| 74 |
@media(max-width:700px) {
|
| 75 |
#atlas-navigation > .tab-wrapper > .tab-container { gap:18px; }
|
| 76 |
#atlas-navigation > .tab-wrapper > .tab-container > button { font-size:13px; padding:0 1px 9px; }
|
| 77 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
#eukaryotic-atlas { padding:0!important; border:0!important; background:transparent!important; }
|
| 4 |
.gradio-container .atlas-panel { background:transparent; border:0; border-radius:0; padding:22px 20px; box-shadow:none; gap:16px; margin-top:0; }
|
| 5 |
.gradio-container .atlas-panel + .atlas-panel { border-top:1px solid #e3e9dc; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
.gradio-container .quiet-note { color:#647467; font-size:12px; line-height:1.7; }
|
| 7 |
.gradio-container .quiet-note p { color:#647467; font-size:12px; line-height:1.7; }
|
|
|
|
|
|
|
| 8 |
.gradio-container .action-button { align-self:flex-end; min-height:46px; }
|
| 9 |
/* The jump out of the atlas should read as the one action on the page, not as
|
| 10 |
another quiet control among the disclosures. */
|
|
|
|
| 33 |
#annotation-examples { font-size:12px; }
|
| 34 |
#annotation-examples .label { color:#647467; }
|
| 35 |
#annotation-results { border-radius:14px; }
|
|
|
|
|
|
|
|
|
|
| 36 |
@media(max-width:700px) {
|
| 37 |
.gradio-container { padding:12px 10px 24px!important; }
|
| 38 |
.gradio-container .atlas-panel { padding:18px 12px; }
|
| 39 |
#atlas-navigation { border-radius:18px; padding:8px 8px 0; }
|
|
|
|
|
|
|
| 40 |
.gradio-container .action-button { width:100%; }
|
| 41 |
}
|
| 42 |
@media(prefers-reduced-motion:reduce) { .gradio-container .atlas-panel button { transition:none; } }
|
|
|
|
| 56 |
#atlas-navigation > .tab-wrapper > .tab-container > button::before, #atlas-navigation > .tab-wrapper > .tab-container > button::after { content:none!important; display:none!important; }
|
| 57 |
#atlas-navigation > .tab-wrapper > .overflow-menu { display:none!important; }
|
| 58 |
#atlas-overview, #atlas-database { border:0; background:transparent; padding:0; min-width:0; max-width:100%; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
@media(max-width:700px) {
|
| 60 |
#atlas-navigation > .tab-wrapper > .tab-container { gap:18px; }
|
| 61 |
#atlas-navigation > .tab-wrapper > .tab-container > button { font-size:13px; padding:0 1px 9px; }
|
| 62 |
}
|
| 63 |
+
|
| 64 |
+
/* Database tab: one small title per section, a single search bar, and one
|
| 65 |
+
toolbar for the viewer, in place of numbered headers and nested boxes. */
|
| 66 |
+
#atlas-database .db-section { font:600 15px/1.3 Arial,Helvetica,sans-serif; color:#1f3d2e; margin:0; padding:0; }
|
| 67 |
+
#atlas-database .atlas-panel { gap:12px; padding:20px 20px 18px; }
|
| 68 |
+
#atlas-database .db-search { gap:10px; align-items:stretch; }
|
| 69 |
+
#atlas-database .db-search textarea, #atlas-database .db-search input { min-height:46px; font-size:14px; }
|
| 70 |
+
#atlas-database .db-search button { min-height:46px; }
|
| 71 |
+
#atlas-database .db-toolbar { gap:10px; }
|
| 72 |
+
#atlas-database .db-actions { gap:8px; justify-content:flex-start; }
|
| 73 |
+
#atlas-database .db-actions button { min-height:36px; }
|
| 74 |
+
#annotation-examples { margin-top:-2px; }
|
| 75 |
+
#annotation-examples .label { font-size:12px; }
|
| 76 |
+
#atlas-database .db-heading { padding:0!important; margin:0; min-height:0; border:0; background:transparent; }
|
| 77 |
+
#atlas-database .db-heading > * { padding:0!important; margin:0!important; }
|
| 78 |
+
#atlas-database .db-actions > * { flex:0 0 auto!important; width:auto!important; min-width:0!important; padding:0 18px; }
|
| 79 |
+
#atlas-database .db-toolbar fieldset .wrap { flex-wrap:nowrap; }
|
| 80 |
+
#atlas-database .db-toolbar fieldset label { white-space:nowrap; }
|
app.py
CHANGED
|
@@ -16,12 +16,15 @@ import pyarrow.parquet as pq
|
|
| 16 |
import plotly.graph_objects as go
|
| 17 |
|
| 18 |
from taxonomy import build_taxonomy_tab
|
| 19 |
-
|
| 20 |
-
HIST_ROWS = 40
|
| 21 |
-
from style import APP_CSS, atlas_theme, section_header
|
| 22 |
from catalog import Catalog
|
| 23 |
from remote_catalog import RemoteCatalog, RemoteReadError
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
def build_app(catalog=None):
|
| 27 |
if catalog is None:
|
|
@@ -32,19 +35,23 @@ def build_app(catalog=None):
|
|
| 32 |
full_snapshot = remote_mode and catalog.manifest.get("full_snapshot", False)
|
| 33 |
scope = "published annotation snapshot" if full_snapshot else "indexed subset" if remote_mode else "sample"
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
def search(accession):
|
| 36 |
began = time.perf_counter()
|
| 37 |
ids, total = catalog.find(accession)
|
| 38 |
elapsed = time.perf_counter() - began
|
| 39 |
-
choices = [(f"{catalog.records[i]['record_name']} · {catalog.records[i]['assembly_accession']} · "
|
| 40 |
-
f"[{catalog.records[i]['segment_start_bp']:,}, {catalog.records[i]['segment_end_bp']:,})", str(i)) for i in ids]
|
| 41 |
if not str(accession or "").strip():
|
| 42 |
-
message = "Enter an assembly or contig accession
|
| 43 |
elif not ids:
|
| 44 |
message = f"No match in this {scope}. Newer bucket publications may not be indexed yet." if full_snapshot else f"No match in this {scope}. This does not mean the accession is absent from the full bucket."
|
| 45 |
else:
|
| 46 |
message = f"Found **{total:,} indexed segment(s)** in {elapsed * 1000:.1f} ms. Showing {len(ids):,}. Assembly coverage may be partial."
|
| 47 |
-
return message,
|
|
|
|
| 48 |
|
| 49 |
def make_plot(frame, mode, threshold):
|
| 50 |
binary = mode == "Binary labels"
|
|
@@ -64,7 +71,7 @@ def build_app(catalog=None):
|
|
| 64 |
tickvals=[0, 1, 2, 3, 4], ticktext=["1", "10", "100", "1k", "10k"]),
|
| 65 |
hovertemplate="%{customdata:,} bases near P=%{y:.2f}<br>from %{x:,}<extra></extra>"))
|
| 66 |
figure.add_trace(go.Scatter(
|
| 67 |
-
x=positions, y=frame["Mean P"].to_numpy()[::HIST_ROWS], mode="lines", name="mean",
|
| 68 |
line=dict(color="#c98b5b", width=1), hovertemplate="mean %{y:.3f}<extra></extra>"))
|
| 69 |
figure.add_hline(y=threshold, line_dash="dot", line_color="#8b9b7b",
|
| 70 |
annotation_text=f"Threshold {threshold:g}")
|
|
@@ -74,7 +81,7 @@ def build_app(catalog=None):
|
|
| 74 |
font=dict(family="Arial, Helvetica, sans-serif", color="#315641", size=12),
|
| 75 |
title_font=dict(family="Georgia, Times New Roman, serif", size=22, color="#173c30"),
|
| 76 |
hoverlabel=dict(bgcolor="#173c30", font_color="#ffffff", bordercolor="#173c30"),
|
| 77 |
-
showlegend=
|
| 78 |
figure.update_xaxes(gridcolor="#e9edde", zerolinecolor="#dbe3d3", linecolor="#dbe3d3")
|
| 79 |
figure.update_yaxes(range=[0, 1], gridcolor="#e9edde", zerolinecolor="#dbe3d3", linecolor="#dbe3d3")
|
| 80 |
return figure
|
|
@@ -88,7 +95,7 @@ def build_app(catalog=None):
|
|
| 88 |
if not binary:
|
| 89 |
figure.add_hline(y=threshold, line_dash="dot", line_color="#8b9b7b",
|
| 90 |
annotation_text=f"Threshold {threshold:g}")
|
| 91 |
-
figure.update_layout(title="Predicted CDS
|
| 92 |
xaxis_title="Position (bp; 0-based)", yaxis_title=column,
|
| 93 |
height=380, margin=dict(l=60, r=25, t=75, b=50), template="plotly_white",
|
| 94 |
paper_bgcolor="#fffefa", plot_bgcolor="#fffefa",
|
|
@@ -102,8 +109,9 @@ def build_app(catalog=None):
|
|
| 102 |
return figure
|
| 103 |
|
| 104 |
def select_segment(index, mode="Probabilities", threshold=0.5):
|
|
|
|
| 105 |
if index is None:
|
| 106 |
-
return {}, None, None, None, "
|
| 107 |
record = catalog.records[int(index)]
|
| 108 |
start = record["segment_start_bp"]
|
| 109 |
end = record["segment_end_bp"]
|
|
@@ -112,33 +120,23 @@ def build_app(catalog=None):
|
|
| 112 |
frame, step = catalog.window(index, start, end, table=table, mode=mode, threshold=threshold)
|
| 113 |
plot = make_plot(frame, mode, threshold)
|
| 114 |
except (ValueError, TypeError, OverflowError) as exc:
|
| 115 |
-
return record, start, end, None, str(exc),
|
| 116 |
-
return record, start, end, plot, plot_note(step, stats, mode, threshold),
|
| 117 |
|
| 118 |
def plot_note(step, stats, mode="Probabilities", threshold=0.5):
|
| 119 |
origin = "local sample" if stats.get("local") else "cache" if stats["cache_hit"] else "bucket"
|
| 120 |
if mode == "Binary labels":
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
+ ("The stepped track preserves every base label in this region." if step == 1 else
|
| 124 |
-
f"**Binned overview:** each interval spans up to {step:,} bases and is 1 if **any** base exceeds the threshold. "
|
| 125 |
-
"This does not mean every base in that interval is CDS. Narrow the region for exact labels."))
|
| 126 |
else:
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
"band and a low one rather than an average between them. The thin line is the mean. "
|
| 132 |
-
"Narrow the region for exact per-strand values.")
|
| 133 |
-
return ("Coordinates are **0-based, end-exclusive**. "
|
| 134 |
-
+ resolution
|
| 135 |
-
+ " Download the segment for the original per-base probabilities.\n\n"
|
| 136 |
-
+ f"Loaded from **{origin}** in **{stats['seconds']:.2f} s**"
|
| 137 |
-
+ (f" · {stats['bytes_read'] / 1_000_000:.2f} MB fetched." if origin == "bucket" else "."))
|
| 138 |
|
| 139 |
def update_window(index, start, end, mode="Probabilities", threshold=0.5):
|
| 140 |
if index is None:
|
| 141 |
-
return None, "Look up an accession and
|
| 142 |
try:
|
| 143 |
table, stats = catalog.fetch(index)
|
| 144 |
frame, step = catalog.window(index, start, end, table=table, mode=mode, threshold=threshold)
|
|
@@ -162,25 +160,21 @@ def build_app(catalog=None):
|
|
| 162 |
filename = f"{assembly}__{record}__{start}-{end}.parquet"
|
| 163 |
target = Path(tempfile.mkdtemp(prefix="genbank-export-")) / filename
|
| 164 |
pq.write_table(table, target, compression="zstd")
|
| 165 |
-
return str(target)
|
| 166 |
|
| 167 |
with gr.Blocks(title="GenBank Annotation Explorer", delete_cache=(3600, 3600)) as demo:
|
| 168 |
with gr.Tabs(selected="atlas", elem_id="atlas-navigation") as navigation:
|
| 169 |
with gr.Tab("Genome Atlas", id="atlas", elem_id="atlas-overview"):
|
| 170 |
atlas = build_taxonomy_tab()
|
| 171 |
with gr.Tab("Database", id="database", elem_id="atlas-database"):
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
apply_default_css=False)
|
| 175 |
with gr.Column(elem_classes="atlas-panel"):
|
| 176 |
-
gr.HTML(
|
| 177 |
-
gr.
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
with gr.Row():
|
| 182 |
-
accession = gr.Textbox(label="Accession ID", placeholder="Assembly (GCA_…) or contig accession", scale=5)
|
| 183 |
-
search_button = gr.Button("Find annotations", variant="primary", scale=1, elem_classes="action-button")
|
| 184 |
examples = [first["assembly_accession"], first["record_name"]]
|
| 185 |
example_labels = None
|
| 186 |
if remote_mode:
|
|
@@ -194,44 +188,46 @@ def build_app(catalog=None):
|
|
| 194 |
examples = [entry["accession"] for entry in suggestions["examples"]]
|
| 195 |
example_labels = [f"{entry['organism']} · {entry['segments']:,} segments" for entry in suggestions["examples"]]
|
| 196 |
gr.Examples(examples=[[e] for e in dict.fromkeys(examples)], inputs=accession,
|
| 197 |
-
example_labels=example_labels,
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
segment = gr.Dropdown(choices=[], label="Segment to explore", interactive=True)
|
| 203 |
|
| 204 |
with gr.Column(elem_classes="atlas-panel"):
|
| 205 |
-
gr.HTML(
|
| 206 |
-
with gr.Row():
|
| 207 |
-
start = gr.Number(label="Start
|
| 208 |
-
end = gr.Number(label="End
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
| 215 |
with gr.Accordion("Segment metadata and provenance", open=False, elem_classes="atlas-disclosure"):
|
| 216 |
-
metadata = gr.JSON(
|
| 217 |
-
|
| 218 |
-
with gr.Column(elem_classes="atlas-panel"):
|
| 219 |
-
gr.HTML(section_header("03", "Take the annotations with you", "Download the original segment, with its accession and record name in the filename."), apply_default_css=False)
|
| 220 |
-
with gr.Row():
|
| 221 |
-
download = gr.Button("Prepare segment download", variant="primary", scale=1, elem_classes="action-button")
|
| 222 |
-
file = gr.File(label="Original segment annotations (Parquet)", interactive=False, scale=3)
|
| 223 |
-
gr.Markdown("Full per-base probabilities are preserved in the download, including when the viewer shows a binned overview.", elem_classes="quiet-note")
|
| 224 |
with gr.Accordion("Browse the index and its sources", open=False, elem_classes="atlas-disclosure"):
|
| 225 |
-
gr.Markdown(f"
|
|
|
|
|
|
|
|
|
|
| 226 |
"An indexed file does not imply complete coverage of its assembly.\n\n"
|
| 227 |
"Source: [HuggingFaceBio/genbank-annotations](https://huggingface.co/buckets/HuggingFaceBio/genbank-annotations). "
|
| 228 |
+ (f"Annotations load on demand from {catalog.manifest.get('source_count', len(catalog.manifest['sources']))} bucket files. "
|
| 229 |
f"Index updated {catalog.manifest['created_at'][:10]}." if remote_mode else "Offline sample."), elem_classes="quiet-note")
|
| 230 |
-
gr.Dataframe(value=
|
| 231 |
gr.JSON(value=catalog.manifest, label="Index provenance" if remote_mode else "Sample provenance")
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
if atlas:
|
| 236 |
# The atlas names a group; the Database tab searches accessions. The
|
| 237 |
# jump hands over one annotated assembly from the selected group and
|
|
@@ -240,16 +236,16 @@ def build_app(catalog=None):
|
|
| 240 |
return gr.Tabs(selected="database"), atlas["accession_for"](path)
|
| 241 |
|
| 242 |
atlas["button"].click(open_database, atlas["route"], [navigation, accession]) \
|
| 243 |
-
.then(search, accession,
|
| 244 |
-
.then(select_segment, [
|
| 245 |
for event in (search_button.click, accession.submit):
|
| 246 |
-
event(search, accession,
|
| 247 |
-
|
| 248 |
-
region_inputs = [
|
| 249 |
view.click(update_window, region_inputs, [plot, note])
|
| 250 |
mode.input(update_window, region_inputs, [plot, note])
|
| 251 |
threshold.release(update_window, region_inputs, [plot, note])
|
| 252 |
-
download.click(export,
|
| 253 |
return demo
|
| 254 |
|
| 255 |
|
|
|
|
| 16 |
import plotly.graph_objects as go
|
| 17 |
|
| 18 |
from taxonomy import build_taxonomy_tab
|
| 19 |
+
from style import APP_CSS, atlas_theme
|
|
|
|
|
|
|
| 20 |
from catalog import Catalog
|
| 21 |
from remote_catalog import RemoteCatalog, RemoteReadError
|
| 22 |
|
| 23 |
+
HIST_ROWS = 40
|
| 24 |
+
# Readable headers for the results table; the catalog keeps its own names.
|
| 25 |
+
HEADERS = {"assembly_accession": "Assembly", "record_name": "Record", "organism_name": "Organism",
|
| 26 |
+
"division": "Division", "segment_start_bp": "Start", "segment_end_bp": "End"}
|
| 27 |
+
|
| 28 |
|
| 29 |
def build_app(catalog=None):
|
| 30 |
if catalog is None:
|
|
|
|
| 35 |
full_snapshot = remote_mode and catalog.manifest.get("full_snapshot", False)
|
| 36 |
scope = "published annotation snapshot" if full_snapshot else "indexed subset" if remote_mode else "sample"
|
| 37 |
|
| 38 |
+
def display_table(ids):
|
| 39 |
+
frame = catalog.table(ids)
|
| 40 |
+
frame["Segment"] = [f"{i + 1} of {n}" for i, n in zip(frame.pop("segment_index"), frame.pop("segment_count"))]
|
| 41 |
+
return frame.rename(columns=HEADERS)
|
| 42 |
+
|
| 43 |
def search(accession):
|
| 44 |
began = time.perf_counter()
|
| 45 |
ids, total = catalog.find(accession)
|
| 46 |
elapsed = time.perf_counter() - began
|
|
|
|
|
|
|
| 47 |
if not str(accession or "").strip():
|
| 48 |
+
message = "Enter an assembly or contig accession, or try an example."
|
| 49 |
elif not ids:
|
| 50 |
message = f"No match in this {scope}. Newer bucket publications may not be indexed yet." if full_snapshot else f"No match in this {scope}. This does not mean the accession is absent from the full bucket."
|
| 51 |
else:
|
| 52 |
message = f"Found **{total:,} indexed segment(s)** in {elapsed * 1000:.1f} ms. Showing {len(ids):,}. Assembly coverage may be partial."
|
| 53 |
+
return (gr.Markdown(message, visible=True), gr.Dataframe(value=display_table(ids), visible=bool(ids)),
|
| 54 |
+
ids, ids[0] if ids else None, gr.DownloadButton(visible=False))
|
| 55 |
|
| 56 |
def make_plot(frame, mode, threshold):
|
| 57 |
binary = mode == "Binary labels"
|
|
|
|
| 71 |
tickvals=[0, 1, 2, 3, 4], ticktext=["1", "10", "100", "1k", "10k"]),
|
| 72 |
hovertemplate="%{customdata:,} bases near P=%{y:.2f}<br>from %{x:,}<extra></extra>"))
|
| 73 |
figure.add_trace(go.Scatter(
|
| 74 |
+
x=positions, y=frame["Mean P"].to_numpy()[::HIST_ROWS], mode="lines", name="mean per column",
|
| 75 |
line=dict(color="#c98b5b", width=1), hovertemplate="mean %{y:.3f}<extra></extra>"))
|
| 76 |
figure.add_hline(y=threshold, line_dash="dot", line_color="#8b9b7b",
|
| 77 |
annotation_text=f"Threshold {threshold:g}")
|
|
|
|
| 81 |
font=dict(family="Arial, Helvetica, sans-serif", color="#315641", size=12),
|
| 82 |
title_font=dict(family="Georgia, Times New Roman, serif", size=22, color="#173c30"),
|
| 83 |
hoverlabel=dict(bgcolor="#173c30", font_color="#ffffff", bordercolor="#173c30"),
|
| 84 |
+
showlegend=True, legend=dict(orientation="h", y=1.12, x=1, xanchor="right"))
|
| 85 |
figure.update_xaxes(gridcolor="#e9edde", zerolinecolor="#dbe3d3", linecolor="#dbe3d3")
|
| 86 |
figure.update_yaxes(range=[0, 1], gridcolor="#e9edde", zerolinecolor="#dbe3d3", linecolor="#dbe3d3")
|
| 87 |
return figure
|
|
|
|
| 95 |
if not binary:
|
| 96 |
figure.add_hline(y=threshold, line_dash="dot", line_color="#8b9b7b",
|
| 97 |
annotation_text=f"Threshold {threshold:g}")
|
| 98 |
+
figure.update_layout(title="Predicted CDS, either strand" if binary else "CDS probability by strand",
|
| 99 |
xaxis_title="Position (bp; 0-based)", yaxis_title=column,
|
| 100 |
height=380, margin=dict(l=60, r=25, t=75, b=50), template="plotly_white",
|
| 101 |
paper_bgcolor="#fffefa", plot_bgcolor="#fffefa",
|
|
|
|
| 109 |
return figure
|
| 110 |
|
| 111 |
def select_segment(index, mode="Probabilities", threshold=0.5):
|
| 112 |
+
hide = gr.DownloadButton(visible=False)
|
| 113 |
if index is None:
|
| 114 |
+
return {}, None, None, None, "Pick a segment above to see its coding landscape.", hide
|
| 115 |
record = catalog.records[int(index)]
|
| 116 |
start = record["segment_start_bp"]
|
| 117 |
end = record["segment_end_bp"]
|
|
|
|
| 120 |
frame, step = catalog.window(index, start, end, table=table, mode=mode, threshold=threshold)
|
| 121 |
plot = make_plot(frame, mode, threshold)
|
| 122 |
except (ValueError, TypeError, OverflowError) as exc:
|
| 123 |
+
return record, start, end, None, str(exc), hide
|
| 124 |
+
return record, start, end, plot, plot_note(step, stats, mode, threshold), hide
|
| 125 |
|
| 126 |
def plot_note(step, stats, mode="Probabilities", threshold=0.5):
|
| 127 |
origin = "local sample" if stats.get("local") else "cache" if stats["cache_hit"] else "bucket"
|
| 128 |
if mode == "Binary labels":
|
| 129 |
+
detail = (f"1 where the higher strand exceeds {threshold:g}" if step == 1 else
|
| 130 |
+
f"each step covers up to {step:,} bases and is 1 if any of them exceeds {threshold:g}; zoom in for exact labels")
|
|
|
|
|
|
|
|
|
|
| 131 |
else:
|
| 132 |
+
detail = ("one point per base, per strand" if step == 1 else
|
| 133 |
+
f"each column covers up to {step:,} bases; colour counts how many sit at each probability of the higher strand")
|
| 134 |
+
fetched = f", {stats['bytes_read'] / 1_000_000:.2f} MB" if origin == "bucket" else ""
|
| 135 |
+
return f"0-based, end-exclusive · {detail} · loaded from {origin} in {stats['seconds']:.2f} s{fetched}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
def update_window(index, start, end, mode="Probabilities", threshold=0.5):
|
| 138 |
if index is None:
|
| 139 |
+
return None, "Look up an accession and pick a segment first."
|
| 140 |
try:
|
| 141 |
table, stats = catalog.fetch(index)
|
| 142 |
frame, step = catalog.window(index, start, end, table=table, mode=mode, threshold=threshold)
|
|
|
|
| 160 |
filename = f"{assembly}__{record}__{start}-{end}.parquet"
|
| 161 |
target = Path(tempfile.mkdtemp(prefix="genbank-export-")) / filename
|
| 162 |
pq.write_table(table, target, compression="zstd")
|
| 163 |
+
return gr.DownloadButton(value=str(target), visible=True)
|
| 164 |
|
| 165 |
with gr.Blocks(title="GenBank Annotation Explorer", delete_cache=(3600, 3600)) as demo:
|
| 166 |
with gr.Tabs(selected="atlas", elem_id="atlas-navigation") as navigation:
|
| 167 |
with gr.Tab("Genome Atlas", id="atlas", elem_id="atlas-overview"):
|
| 168 |
atlas = build_taxonomy_tab()
|
| 169 |
with gr.Tab("Database", id="database", elem_id="atlas-database"):
|
| 170 |
+
hits = gr.State([])
|
| 171 |
+
selected = gr.State(None)
|
|
|
|
| 172 |
with gr.Column(elem_classes="atlas-panel"):
|
| 173 |
+
gr.HTML('<h2 class="db-section">Find an accession</h2>', apply_default_css=False, elem_classes="db-heading")
|
| 174 |
+
with gr.Row(equal_height=True, elem_classes="db-search"):
|
| 175 |
+
accession = gr.Textbox(show_label=False, container=False, scale=5,
|
| 176 |
+
placeholder="Assembly (GCA_…) or contig accession")
|
| 177 |
+
search_button = gr.Button("Find annotations", variant="primary", scale=1, min_width=170)
|
|
|
|
|
|
|
|
|
|
| 178 |
examples = [first["assembly_accession"], first["record_name"]]
|
| 179 |
example_labels = None
|
| 180 |
if remote_mode:
|
|
|
|
| 188 |
examples = [entry["accession"] for entry in suggestions["examples"]]
|
| 189 |
example_labels = [f"{entry['organism']} · {entry['segments']:,} segments" for entry in suggestions["examples"]]
|
| 190 |
gr.Examples(examples=[[e] for e in dict.fromkeys(examples)], inputs=accession,
|
| 191 |
+
example_labels=example_labels, label="Try", elem_id="annotation-examples")
|
| 192 |
+
status = gr.Markdown(visible=False, elem_classes="quiet-note")
|
| 193 |
+
# Hidden until a search returns rows; a click on a row loads that segment.
|
| 194 |
+
results = gr.Dataframe(value=display_table([]), interactive=False, show_label=False,
|
| 195 |
+
visible=False, elem_id="annotation-results")
|
|
|
|
| 196 |
|
| 197 |
with gr.Column(elem_classes="atlas-panel"):
|
| 198 |
+
gr.HTML('<h2 class="db-section">Coding landscape</h2>', apply_default_css=False, elem_classes="db-heading")
|
| 199 |
+
with gr.Row(equal_height=True, elem_classes="db-toolbar"):
|
| 200 |
+
start = gr.Number(label="Start", precision=0, min_width=110, scale=2)
|
| 201 |
+
end = gr.Number(label="End", precision=0, min_width=110, scale=2)
|
| 202 |
+
mode = gr.Radio(["Probabilities", "Binary labels"], value="Probabilities", label="View", min_width=320, scale=3)
|
| 203 |
+
threshold = gr.Slider(0, 1, value=0.5, step=0.01, label="Threshold", min_width=200, scale=3)
|
| 204 |
+
with gr.Row(elem_classes="db-actions"):
|
| 205 |
+
view = gr.Button("Update region", variant="primary", size="sm", min_width=140, scale=0)
|
| 206 |
+
download = gr.Button("Prepare download", size="sm", min_width=150, scale=0)
|
| 207 |
+
file = gr.DownloadButton("Download per-base data (.parquet)", visible=False,
|
| 208 |
+
size="sm", min_width=240, scale=0)
|
| 209 |
+
plot = gr.Plot(show_label=False, elem_id="annotation-plot")
|
| 210 |
+
note = gr.Markdown("Pick a segment above to see its coding landscape.", elem_classes="quiet-note")
|
| 211 |
with gr.Accordion("Segment metadata and provenance", open=False, elem_classes="atlas-disclosure"):
|
| 212 |
+
metadata = gr.JSON(show_label=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
with gr.Accordion("Browse the index and its sources", open=False, elem_classes="atlas-disclosure"):
|
| 214 |
+
gr.Markdown(f"**{len(catalog.records):,} indexed segments** · **{len(catalog.manifest['assemblies']):,} assemblies** · "
|
| 215 |
+
f"{catalog.manifest['bases']:,} bases. Search covers the {scope}; results are model predictions "
|
| 216 |
+
"and assembly coverage may be partial.\n\n"
|
| 217 |
+
f"Showing the first {len(all_ids):,} indexed segments. Search an accession to find other indexed records. "
|
| 218 |
"An indexed file does not imply complete coverage of its assembly.\n\n"
|
| 219 |
"Source: [HuggingFaceBio/genbank-annotations](https://huggingface.co/buckets/HuggingFaceBio/genbank-annotations). "
|
| 220 |
+ (f"Annotations load on demand from {catalog.manifest.get('source_count', len(catalog.manifest['sources']))} bucket files. "
|
| 221 |
f"Index updated {catalog.manifest['created_at'][:10]}." if remote_mode else "Offline sample."), elem_classes="quiet-note")
|
| 222 |
+
gr.Dataframe(value=display_table(all_ids), interactive=False, show_label=False)
|
| 223 |
gr.JSON(value=catalog.manifest, label="Index provenance" if remote_mode else "Sample provenance")
|
| 224 |
+
|
| 225 |
+
found = [status, results, hits, selected, file]
|
| 226 |
+
shown = [metadata, start, end, plot, note, file]
|
| 227 |
+
|
| 228 |
+
def pick_row(rows, evt: gr.SelectData):
|
| 229 |
+
return rows[evt.index[0]] if rows and evt.index and evt.index[0] < len(rows) else None
|
| 230 |
+
|
| 231 |
if atlas:
|
| 232 |
# The atlas names a group; the Database tab searches accessions. The
|
| 233 |
# jump hands over one annotated assembly from the selected group and
|
|
|
|
| 236 |
return gr.Tabs(selected="database"), atlas["accession_for"](path)
|
| 237 |
|
| 238 |
atlas["button"].click(open_database, atlas["route"], [navigation, accession]) \
|
| 239 |
+
.then(search, accession, found) \
|
| 240 |
+
.then(select_segment, [selected, mode, threshold], shown)
|
| 241 |
for event in (search_button.click, accession.submit):
|
| 242 |
+
event(search, accession, found).then(select_segment, [selected, mode, threshold], shown)
|
| 243 |
+
results.select(pick_row, hits, selected).then(select_segment, [selected, mode, threshold], shown)
|
| 244 |
+
region_inputs = [selected, start, end, mode, threshold]
|
| 245 |
view.click(update_window, region_inputs, [plot, note])
|
| 246 |
mode.input(update_window, region_inputs, [plot, note])
|
| 247 |
threshold.release(update_window, region_inputs, [plot, note])
|
| 248 |
+
download.click(export, selected, file)
|
| 249 |
return demo
|
| 250 |
|
| 251 |
|
style.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
"""Shared visual language for the atlas and annotation workspace."""
|
| 2 |
-
from html import escape
|
| 3 |
from pathlib import Path
|
| 4 |
|
| 5 |
import gradio as gr
|
|
@@ -55,8 +54,3 @@ def atlas_theme():
|
|
| 55 |
if name.endswith("_dark") and hasattr(theme, name[:-5]):
|
| 56 |
setattr(theme, name, getattr(theme, name[:-5]))
|
| 57 |
return theme
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def section_header(number, title, description):
|
| 61 |
-
return (f'<header class="workspace-heading"><span class="workspace-step">{escape(number)}</span>'
|
| 62 |
-
f'<div><h2>{escape(title)}</h2><p>{escape(description)}</p></div></header>')
|
|
|
|
| 1 |
"""Shared visual language for the atlas and annotation workspace."""
|
|
|
|
| 2 |
from pathlib import Path
|
| 3 |
|
| 4 |
import gradio as gr
|
|
|
|
| 54 |
if name.endswith("_dark") and hasattr(theme, name[:-5]):
|
| 55 |
setattr(theme, name, getattr(theme, name[:-5]))
|
| 56 |
return theme
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|