lvwerra HF Staff commited on
Commit
22ff2db
·
verified ·
1 Parent(s): 0d06695

Declutter the Database tab

Browse files

The tab opened with an eyebrow, a page title and a subtitle repeating the tab name, then three numbered sections each with a large serif title and its own subtitle, a stats box, a standing hint, an empty table with raw column names, a dropdown repeating that table, and a 380px empty file box.

Now:

* **Two small section titles**, "Find an accession" and "Coding landscape", in place of the page header and the numbered 01/02/03 sections.
* **Search is a single bar** with the example chips under it. No label box, and the button lines up with the input.
* **The results table is hidden until a search returns rows**, uses readable headers (Assembly, Record, Organism, Division, Start, End, Segment), and **a click on a row loads that segment**. That replaces the "Segment to explore" dropdown.
* **The viewer controls sit in one toolbar** with short labels. Download is a small button there: preparing the file reveals a download button instead of an empty file box.
* **The caption under the plot is one line**, and the mean line now has a legend entry.
* **The index statistics and scope note** moved into "Browse the index and its sources", next to the provenance they belong with.
* **The page footer is gone**; its bucket link already lives in that disclosure.

The viewer stays visible before a segment is picked, as asked.

Checked in the browser: search shows the table, clicking the `JBPJTW010000308.1` row moves the region from 0–66,123 to 0–316,820, preparing a download reveals the download button, and the atlas "Show database results" jump still lands on a searched accession. Thirty-six tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Files changed (3) hide show
  1. app.css +18 -15
  2. app.py +73 -77
  3. style.py +0 -6
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. Try an example below."
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, catalog.table(ids), gr.Dropdown(choices=choices, value=str(ids[0]) if ids else None), None
 
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=False)
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 — either strand" if binary else "CDS probability by strand",
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, "Choose a matching segment.", 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), None
116
- return record, start, end, plot, plot_note(step, stats, mode, threshold), None
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
- resolution = (f"**1 = max(P_positive, P_negative) > {threshold:g}; 0 = otherwise.** "
122
- "This is one CDS/background label per base, combining both strands. "
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
- resolution = ("Each point is one base, per strand." if step == 1 else
128
- f"Each column covers up to **{step:,} bases** and shows how their probabilities are "
129
- "distributed, over max(P_positive, P_negative) — the value the threshold tests. "
130
- "Colour is how many bases fall in that band, so a part-coding region shows a high "
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 choose a segment first."
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
- gr.HTML('<header class="database-heading"><p class="database-eyebrow">THE ANNOTATION COLLECTION</p>'
173
- '<h1>Explore the database</h1><p>Find an accession, explore its coding landscape, and download the annotations.</p></header>',
174
- apply_default_css=False)
175
  with gr.Column(elem_classes="atlas-panel"):
176
- gr.HTML(section_header("01", "Find an accession", "Start with an assembly or contig to explore its predicted coding regions."), apply_default_css=False)
177
- gr.Markdown(f"**{len(catalog.records):,} indexed segments** · **{len(catalog.manifest['assemblies']):,} assemblies** · "
178
- f"{catalog.manifest['bases']:,} bases\n\n"
179
- f"Search covers the {scope}. Results are model predictions and assembly coverage may be partial.",
180
- elem_classes=["quiet-note", "scope-note"])
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
- label="Explore an assembly with many segments" if example_labels else "Try an accession",
199
- elem_id="annotation-examples")
200
- status = gr.Markdown("Enter an accession or select an example. IDs are case-insensitive; version suffixes are optional.", elem_classes="quiet-note")
201
- results = gr.Dataframe(value=catalog.table([]), interactive=False, label="Matching segments", elem_id="annotation-results")
202
- segment = gr.Dropdown(choices=[], label="Segment to explore", interactive=True)
203
 
204
  with gr.Column(elem_classes="atlas-panel"):
205
- gr.HTML(section_header("02", "Explore the coding landscape", "View CDS probabilities or apply a threshold to see one label per base."), apply_default_css=False)
206
- with gr.Row():
207
- start = gr.Number(label="Start (0-based, inclusive)", precision=0)
208
- end = gr.Number(label="End (exclusive)", precision=0)
209
- view = gr.Button("Update region", elem_classes="action-button")
210
- with gr.Row():
211
- mode = gr.Radio(["Probabilities", "Binary labels"], value="Probabilities", label="Viewer mode")
212
- threshold = gr.Slider(0, 1, value=0.5, step=0.01, label="CDS threshold (max strand P > threshold)")
213
- plot = gr.Plot(label="CDS tracks", elem_id="annotation-plot")
214
- note = gr.Markdown("Choose a segment above to bring its coding landscape into view.", elem_classes="quiet-note")
 
 
 
215
  with gr.Accordion("Segment metadata and provenance", open=False, elem_classes="atlas-disclosure"):
216
- metadata = gr.JSON(label="Source metadata")
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"Showing the first {len(all_ids):,} indexed segments. Search an accession to find other indexed records. "
 
 
 
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=catalog.table(all_ids), interactive=False)
231
  gr.JSON(value=catalog.manifest, label="Index provenance" if remote_mode else "Sample provenance")
232
- gr.HTML('<footer class="workspace-footer"><span>Hugging Face Bio · Genome Atlas</span>'
233
- '<a href="https://huggingface.co/buckets/HuggingFaceBio/genbank-annotations" target="_blank" rel="noopener noreferrer">Explore the annotation collection ↗</a></footer>', apply_default_css=False)
234
- outputs = [status, results, segment, file]
 
 
 
 
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, outputs) \
244
- .then(select_segment, [segment, mode, threshold], [metadata, start, end, plot, note, file])
245
  for event in (search_button.click, accession.submit):
246
- event(search, accession, outputs).then(select_segment, [segment, mode, threshold], [metadata, start, end, plot, note, file])
247
- segment.input(select_segment, [segment, mode, threshold], [metadata, start, end, plot, note, file])
248
- region_inputs = [segment, start, end, mode, threshold]
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, segment, file)
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