Skip to content

Commit da549d8

Browse files
ihhclaude
andcommitted
Fetch alignment from repo instead of embedding in score JSON
Parse protein.sto client-side from raw.githubusercontent.com on first expand. Auto-expand the top-ranked entry by default. Removes alignment data from score JSON to keep it lightweight. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 40baa9f commit da549d8

6 files changed

Lines changed: 125 additions & 77 deletions

File tree

ci/validate.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -155,17 +155,7 @@ def _build_report(
155155
"functionality": 0.0, "size": 0.0, "total": 0.0,
156156
}
157157

158-
# Embed alignment data for the web viewer
159-
alignment = None
160-
if protein_block is not None:
161-
alignment = {
162-
"sequences": {
163-
sid: seq for sid, seq in protein_block.sequences.items()
164-
},
165-
"catalytic_triad": protein_block.gc.get("catalytic_triad", ""),
166-
}
167-
168-
report = {
158+
return {
169159
"team": team,
170160
"timestamp": datetime.now(timezone.utc).isoformat(),
171161
"commit": commit,
@@ -175,9 +165,6 @@ def _build_report(
175165
"scores": scores,
176166
"per_sequence_issues": per_seq_issues,
177167
}
178-
if alignment:
179-
report["alignment"] = alignment
180-
return report
181168

182169

183170
def update_leaderboard(scores_dir: Path, output_path: Path) -> None:

docs/leaderboard.js

Lines changed: 116 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
(function () {
22
"use strict";
33

4-
const SCORE_COMPONENTS = [
5-
{ key: "diversity", label: "Diversity" },
6-
{ key: "annotation", label: "Annotation" },
7-
{ key: "functionality", label: "Functionality" },
8-
{ key: "size", label: "Size" },
9-
];
4+
// Detect repo owner/name from the Pages URL or fall back
5+
const REPO_RAW_BASE = (function () {
6+
// GitHub Pages URLs: {owner}.github.io/{repo}/
7+
const m = location.hostname.match(/^(.+)\.github\.io$/);
8+
if (m) {
9+
const owner = m[1];
10+
const repo = location.pathname.split("/")[1] || "";
11+
return `https://raw.githubusercontent.com/${owner}/${repo}/main`;
12+
}
13+
// Local dev: assume relative paths work (they won't for .sto, but graceful fail)
14+
return "";
15+
})();
1016

1117
async function fetchJSON(url) {
1218
const resp = await fetch(url);
1319
if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
1420
return resp.json();
1521
}
1622

23+
async function fetchText(url) {
24+
const resp = await fetch(url);
25+
if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
26+
return resp.text();
27+
}
28+
1729
function scoreBar(value, max) {
1830
const pct = Math.min(100, (value / max) * 100);
1931
return `<span class="score-bar"><span class="score-bar-fill" style="width:${pct}%"></span></span>`;
@@ -25,24 +37,34 @@
2537
return `<span class="${cls}">${sym}</span>`;
2638
}
2739

28-
function buildRow(entry, rank) {
29-
const tr = document.createElement("tr");
30-
tr.innerHTML = `
31-
<td class="rank">${rank}</td>
32-
<td class="team"><a data-team="${entry.team}">${entry.team}</a></td>
33-
<td class="score-total">${scoreBar(entry.score, 100)}${entry.score.toFixed(1)}</td>
34-
<td class="score-sub hide-mobile">${(entry.scores?.diversity ?? "").toString().slice(0, 4)}</td>
35-
<td class="score-sub hide-mobile">${(entry.scores?.annotation ?? "").toString().slice(0, 4)}</td>
36-
<td class="score-sub hide-mobile">${(entry.scores?.functionality ?? "").toString().slice(0, 4)}</td>
37-
<td class="score-sub hide-mobile">${(entry.scores?.size ?? "").toString().slice(0, 4)}</td>
38-
<td class="count">${entry.n_sequences}</td>
39-
<td class="count">${entry.n_families}</td>
40-
<td class="score-sub"><code>${(entry.commit || "").slice(0, 7)}</code></td>
41-
`;
42-
return tr;
40+
// ── Stockholm parser (client-side) ──────────────────────────────────
41+
42+
function parseProteinSto(text) {
43+
const sequences = {};
44+
const order = [];
45+
let triad = "";
46+
47+
for (const raw of text.split("\n")) {
48+
const line = raw.trimEnd();
49+
if (!line || line.startsWith("# ") || line === "//") continue;
50+
if (line.startsWith("#=GC catalytic_triad")) {
51+
triad = line.split(/\s+/).pop();
52+
} else if (line.startsWith("#=GC") || line.startsWith("#=GF")) {
53+
continue;
54+
} else {
55+
const parts = line.split(/\s+/);
56+
if (parts.length >= 2) {
57+
const id = parts[0].split("/")[0]; // strip /coord suffix
58+
if (!(id in sequences)) order.push(id);
59+
sequences[id] = (sequences[id] || "") + parts[1];
60+
}
61+
}
62+
}
63+
return { sequences, order, catalytic_triad: triad };
4364
}
4465

45-
// Residue coloring by physicochemical property
66+
// ── Alignment renderer ──────────────────────────────────────────────
67+
4668
const AA_CLASSES = {};
4769
"AILMFWV".split("").forEach(c => AA_CLASSES[c] = "aa-hydrophobic");
4870
"KR".split("").forEach(c => AA_CLASSES[c] = "aa-positive");
@@ -53,12 +75,12 @@
5375
"P".split("").forEach(c => AA_CLASSES[c] = "aa-proline");
5476
"HY".split("").forEach(c => AA_CLASSES[c] = "aa-aromatic");
5577

56-
function renderAlignment(alignment) {
57-
if (!alignment || !alignment.sequences) return "";
78+
function renderAlignment(aln) {
79+
if (!aln || !aln.order.length) return "";
5880

59-
const ids = Object.keys(alignment.sequences);
60-
const seqs = Object.values(alignment.sequences);
61-
const triad = alignment.catalytic_triad || "";
81+
const ids = aln.order;
82+
const seqs = ids.map(id => aln.sequences[id]);
83+
const triad = aln.catalytic_triad || "";
6284
const len = seqs[0].length;
6385
const BLOCK = 80;
6486
const pad = Math.max(...ids.map(s => s.length));
@@ -78,7 +100,6 @@
78100
for (let start = 0; start < len; start += BLOCK) {
79101
const end = Math.min(start + BLOCK, len);
80102

81-
// Sequence rows
82103
for (let s = 0; s < ids.length; s++) {
83104
const label = ids[s].padEnd(pad + 2);
84105
let row = `<span class="aln-label">${label}</span>`;
@@ -91,7 +112,7 @@
91112
html += row + "\n";
92113
}
93114

94-
// Triad annotation row
115+
// Triad row
95116
let triadRow = " ".repeat(pad + 2);
96117
for (let i = start; i < end; i++) {
97118
const ch = triad[i] || ".";
@@ -122,7 +143,26 @@
122143
return html;
123144
}
124145

125-
function buildDetailRow(teamData) {
146+
// ── Detail panel builder ────────────────────────────────────────────
147+
148+
function buildRow(entry, rank) {
149+
const tr = document.createElement("tr");
150+
tr.innerHTML = `
151+
<td class="rank">${rank}</td>
152+
<td class="team"><a data-team="${entry.team}">${entry.team}</a></td>
153+
<td class="score-total">${scoreBar(entry.score, 100)}${entry.score.toFixed(1)}</td>
154+
<td class="score-sub hide-mobile">${(entry.scores?.diversity ?? "").toString().slice(0, 4)}</td>
155+
<td class="score-sub hide-mobile">${(entry.scores?.annotation ?? "").toString().slice(0, 4)}</td>
156+
<td class="score-sub hide-mobile">${(entry.scores?.functionality ?? "").toString().slice(0, 4)}</td>
157+
<td class="score-sub hide-mobile">${(entry.scores?.size ?? "").toString().slice(0, 4)}</td>
158+
<td class="count">${entry.n_sequences}</td>
159+
<td class="count">${entry.n_families}</td>
160+
<td class="score-sub"><code>${(entry.commit || "").slice(0, 7)}</code></td>
161+
`;
162+
return tr;
163+
}
164+
165+
function buildDetailRow(teamData, team) {
126166
const tr = document.createElement("tr");
127167
tr.className = "detail-row";
128168
const td = document.createElement("td");
@@ -155,14 +195,43 @@
155195
}
156196
}
157197

158-
const alignmentHTML = renderAlignment(teamData.alignment);
198+
// Alignment placeholder — loaded on first expand
199+
const alnDiv = document.createElement("div");
200+
alnDiv.className = "alignment-container";
201+
alnDiv.dataset.team = team;
202+
alnDiv.dataset.loaded = "false";
159203

160-
td.innerHTML = `<div class="detail-panel">${checksHTML}${issuesHTML}${alignmentHTML}</div>`;
204+
td.innerHTML = `<div class="detail-panel">${checksHTML}${issuesHTML}</div>`;
205+
td.querySelector(".detail-panel").appendChild(alnDiv);
161206
tr.appendChild(td);
162207
tr.style.display = "none";
163208
return tr;
164209
}
165210

211+
async function loadAlignment(container) {
212+
if (container.dataset.loaded === "true") return;
213+
container.dataset.loaded = "true";
214+
215+
const team = container.dataset.team;
216+
if (!REPO_RAW_BASE) {
217+
container.innerHTML = '<p class="aln-note">Alignment view requires GitHub Pages hosting.</p>';
218+
return;
219+
}
220+
221+
const url = `${REPO_RAW_BASE}/entries/${team}/protein.sto`;
222+
container.innerHTML = '<p class="aln-note">Loading alignment\u2026</p>';
223+
224+
try {
225+
const text = await fetchText(url);
226+
const aln = parseProteinSto(text);
227+
container.innerHTML = renderAlignment(aln);
228+
} catch (e) {
229+
container.innerHTML = `<p class="aln-note">Could not load alignment: ${e.message}</p>`;
230+
}
231+
}
232+
233+
// ── Init ────────────────────────────────────────────────────────────
234+
166235
async function init() {
167236
const tbody = document.getElementById("leaderboard-body");
168237
const updatedEl = document.getElementById("updated");
@@ -186,17 +255,15 @@
186255
return;
187256
}
188257

189-
// Sort by score descending
190258
data.entries.sort((a, b) => b.score - a.score);
191259

192-
// Load all score files in parallel for detail panels
193260
const scoreFiles = {};
194261
await Promise.allSettled(
195262
data.entries.map(async (entry) => {
196263
try {
197264
scoreFiles[entry.team] = await fetchJSON(`scores/${entry.team}.json`);
198265
} catch {
199-
/* score file not copied yet, that's OK */
266+
/* score file not copied yet */
200267
}
201268
})
202269
);
@@ -205,14 +272,24 @@
205272
const row = buildRow(entry, i + 1);
206273
tbody.appendChild(row);
207274

208-
const detailRow = buildDetailRow(scoreFiles[entry.team] || {});
275+
const detailRow = buildDetailRow(scoreFiles[entry.team] || {}, entry.team);
209276
tbody.appendChild(detailRow);
210277

211-
// Toggle detail on team name click
212278
row.querySelector("[data-team]").addEventListener("click", () => {
213-
detailRow.style.display =
214-
detailRow.style.display === "none" ? "table-row" : "none";
279+
const showing = detailRow.style.display === "none";
280+
detailRow.style.display = showing ? "table-row" : "none";
281+
if (showing) {
282+
const container = detailRow.querySelector(".alignment-container");
283+
if (container) loadAlignment(container);
284+
}
215285
});
286+
287+
// Auto-expand the top-ranked entry
288+
if (i === 0) {
289+
detailRow.style.display = "table-row";
290+
const container = detailRow.querySelector(".alignment-container");
291+
if (container) loadAlignment(container);
292+
}
216293
});
217294
}
218295

docs/leaderboard.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
2-
"updated": "2026-03-08T21:47:22.080087+00:00",
2+
"updated": "2026-03-08T22:21:58.337473+00:00",
33
"entries": [
44
{
55
"team": "test-entry",
66
"score": 31.5,
77
"n_sequences": 3,
88
"n_families": 1,
9-
"commit": "676dd42",
9+
"commit": "40baa9f",
1010
"scores": {
1111
"diversity": 0.0554,
1212
"annotation": 1.0,

docs/scores/test-entry.json

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"team": "test-entry",
3-
"timestamp": "2026-03-08T21:47:22.078521+00:00",
4-
"commit": "676dd42",
3+
"timestamp": "2026-03-08T22:21:58.336276+00:00",
4+
"commit": "40baa9f",
55
"n_sequences": 3,
66
"n_families": 1,
77
"checks": {
@@ -41,13 +41,5 @@
4141
"Mos1_Dmaur": [],
4242
"SynMar1_Dpse": [],
4343
"SynMar2_Agam": []
44-
},
45-
"alignment": {
46-
"sequences": {
47-
"Mos1_Dmaur": "MSSFVPNKEQTRTVLIFCFHLKKTAAESHRMLVEAFGEQVPTVKKCERWFQRFKSGDFDVDDKEHGKPPKRYEDAELQALLDEDDAQTQKQLAEQLEVSQQAVSNRLREMGKIQKVGRWVPHELNERQMERRKNTCEILLSRYKRKSFLHRIVTGDEKWIFFVSPKRKKSYVDPGQPATSTARPNRFGKKTMLCVWWDQSGVIYYELLKRGETVNTARYQQQLINLNRALQRKRPEYQKRQHRVIFLHDNAPSHTARAVRDTLETLNWEVLPHAAYSPDLAPSDYHLFASMGHALAEQRFDSYESVKKWLDEWFAAKDDEFYWRGIHKLPERWEKCVASDGKYLE",
48-
"SynMar1_Dpse": "LSSFVPNKEQTRTVLIFCFQLKKTAGESHRMLVEAFAEQVPTIKKCERWFQRFKTGDFDIDDKEHGKPPKRYEDAELQALLDEDEAQTQKQLAEQLEVSQQAVSNRLREMGKIQKVGRWVPHELNERQMERRKNTCEILLSRYKRKSFLHRIVTGDEKWIFFVSPKRKKSYVDPGQPATSTGRPNRFGKKTMLCVWWDQSGVIYFELLKRGETVNTARFQQQLINLNRALQRKRPEYQKRQHRVIFLHDNAPSHTGRAVRDTLETLNWEVLPHAGYSPDLAPSDYHLFASMGHALAEQRFDSYESVKKWLDEWFAAKDDEFYWRGIHKLPERWEKCVASDGKYLE",
49-
"SynMar2_Agam": "MSSFVPNKEQTRTVLIFCFHLKKTGAETHRMLVDGFGEQVPTVKKCERWYQKFKSGDFDVDDKEHGKPPKRYEDGELQALLDEDDAQTQKQLAEQLEISNQAVTNRLREMGKIQKVGRWVPHELQERQMERRKQTCEILLSKYKKKSYIHRIVTGDDKWIFFVSPKRKKSYVDPAQPATSTARPNRFGKKTMLCVWWDQSGVIYYELLKRGETVNTARYQQQLINLNRGIQRKRPEYQKRQHRVIFLHDNAPSHTARAIRDTLETLNWEVLPHAAYSPELAPSDYHLFATMGHALAENRFDSFESVRKWLDEWFAAKDDEFYWRGIHKLPERWEKCVASDGKYLE"
50-
},
51-
"catalytic_triad": "...........................................................................................................................................................D............................................................................................d..................................D............................................................."
5244
}
5345
}

leaderboard.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
2-
"updated": "2026-03-08T21:47:22.080087+00:00",
2+
"updated": "2026-03-08T22:21:58.337473+00:00",
33
"entries": [
44
{
55
"team": "test-entry",
66
"score": 31.5,
77
"n_sequences": 3,
88
"n_families": 1,
9-
"commit": "676dd42",
9+
"commit": "40baa9f",
1010
"scores": {
1111
"diversity": 0.0554,
1212
"annotation": 1.0,

scores/test-entry.json

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"team": "test-entry",
3-
"timestamp": "2026-03-08T21:47:22.078521+00:00",
4-
"commit": "676dd42",
3+
"timestamp": "2026-03-08T22:21:58.336276+00:00",
4+
"commit": "40baa9f",
55
"n_sequences": 3,
66
"n_families": 1,
77
"checks": {
@@ -41,13 +41,5 @@
4141
"Mos1_Dmaur": [],
4242
"SynMar1_Dpse": [],
4343
"SynMar2_Agam": []
44-
},
45-
"alignment": {
46-
"sequences": {
47-
"Mos1_Dmaur": "MSSFVPNKEQTRTVLIFCFHLKKTAAESHRMLVEAFGEQVPTVKKCERWFQRFKSGDFDVDDKEHGKPPKRYEDAELQALLDEDDAQTQKQLAEQLEVSQQAVSNRLREMGKIQKVGRWVPHELNERQMERRKNTCEILLSRYKRKSFLHRIVTGDEKWIFFVSPKRKKSYVDPGQPATSTARPNRFGKKTMLCVWWDQSGVIYYELLKRGETVNTARYQQQLINLNRALQRKRPEYQKRQHRVIFLHDNAPSHTARAVRDTLETLNWEVLPHAAYSPDLAPSDYHLFASMGHALAEQRFDSYESVKKWLDEWFAAKDDEFYWRGIHKLPERWEKCVASDGKYLE",
48-
"SynMar1_Dpse": "LSSFVPNKEQTRTVLIFCFQLKKTAGESHRMLVEAFAEQVPTIKKCERWFQRFKTGDFDIDDKEHGKPPKRYEDAELQALLDEDEAQTQKQLAEQLEVSQQAVSNRLREMGKIQKVGRWVPHELNERQMERRKNTCEILLSRYKRKSFLHRIVTGDEKWIFFVSPKRKKSYVDPGQPATSTGRPNRFGKKTMLCVWWDQSGVIYFELLKRGETVNTARFQQQLINLNRALQRKRPEYQKRQHRVIFLHDNAPSHTGRAVRDTLETLNWEVLPHAGYSPDLAPSDYHLFASMGHALAEQRFDSYESVKKWLDEWFAAKDDEFYWRGIHKLPERWEKCVASDGKYLE",
49-
"SynMar2_Agam": "MSSFVPNKEQTRTVLIFCFHLKKTGAETHRMLVDGFGEQVPTVKKCERWYQKFKSGDFDVDDKEHGKPPKRYEDGELQALLDEDDAQTQKQLAEQLEISNQAVTNRLREMGKIQKVGRWVPHELQERQMERRKQTCEILLSKYKKKSYIHRIVTGDDKWIFFVSPKRKKSYVDPAQPATSTARPNRFGKKTMLCVWWDQSGVIYYELLKRGETVNTARYQQQLINLNRGIQRKRPEYQKRQHRVIFLHDNAPSHTARAIRDTLETLNWEVLPHAAYSPELAPSDYHLFATMGHALAENRFDSFESVRKWLDEWFAAKDDEFYWRGIHKLPERWEKCVASDGKYLE"
50-
},
51-
"catalytic_triad": "...........................................................................................................................................................D............................................................................................d..................................D............................................................."
5244
}
5345
}

0 commit comments

Comments
 (0)