Skip to content

Commit 40baa9f

Browse files
ihhclaude
andcommitted
Add protein alignment viewer to leaderboard page
Embed alignment data (sequences + catalytic triad) in score JSON files. Render colored alignment in the expandable detail panel on the Pages site with Clustal-style residue coloring and triad highlighting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 676dd42 commit 40baa9f

7 files changed

Lines changed: 152 additions & 10 deletions

File tree

ci/validate.py

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

158-
return {
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 = {
159169
"team": team,
160170
"timestamp": datetime.now(timezone.utc).isoformat(),
161171
"commit": commit,
@@ -165,6 +175,9 @@ def _build_report(
165175
"scores": scores,
166176
"per_sequence_issues": per_seq_issues,
167177
}
178+
if alignment:
179+
report["alignment"] = alignment
180+
return report
168181

169182

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

docs/leaderboard.js

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,86 @@
4242
return tr;
4343
}
4444

45+
// Residue coloring by physicochemical property
46+
const AA_CLASSES = {};
47+
"AILMFWV".split("").forEach(c => AA_CLASSES[c] = "aa-hydrophobic");
48+
"KR".split("").forEach(c => AA_CLASSES[c] = "aa-positive");
49+
"DE".split("").forEach(c => AA_CLASSES[c] = "aa-negative");
50+
"NQST".split("").forEach(c => AA_CLASSES[c] = "aa-polar");
51+
"C".split("").forEach(c => AA_CLASSES[c] = "aa-cysteine");
52+
"G".split("").forEach(c => AA_CLASSES[c] = "aa-glycine");
53+
"P".split("").forEach(c => AA_CLASSES[c] = "aa-proline");
54+
"HY".split("").forEach(c => AA_CLASSES[c] = "aa-aromatic");
55+
56+
function renderAlignment(alignment) {
57+
if (!alignment || !alignment.sequences) return "";
58+
59+
const ids = Object.keys(alignment.sequences);
60+
const seqs = Object.values(alignment.sequences);
61+
const triad = alignment.catalytic_triad || "";
62+
const len = seqs[0].length;
63+
const BLOCK = 80;
64+
const pad = Math.max(...ids.map(s => s.length));
65+
66+
let html = '<div class="alignment"><h3>Protein alignment</h3>';
67+
html += '<div class="aln-legend">';
68+
html += '<span class="aa-hydrophobic">hydrophobic</span> ';
69+
html += '<span class="aa-positive">positive</span> ';
70+
html += '<span class="aa-negative">negative</span> ';
71+
html += '<span class="aa-polar">polar</span> ';
72+
html += '<span class="aa-cysteine">cysteine</span> ';
73+
html += '<span class="aa-glycine">glycine</span> ';
74+
html += '<span class="aa-aromatic">aromatic</span> ';
75+
html += '<span class="aa-triad">triad</span>';
76+
html += '</div><pre class="aln-block">';
77+
78+
for (let start = 0; start < len; start += BLOCK) {
79+
const end = Math.min(start + BLOCK, len);
80+
81+
// Sequence rows
82+
for (let s = 0; s < ids.length; s++) {
83+
const label = ids[s].padEnd(pad + 2);
84+
let row = `<span class="aln-label">${label}</span>`;
85+
for (let i = start; i < end; i++) {
86+
const aa = seqs[s][i];
87+
const isTriad = triad[i] && "DdE".includes(triad[i]);
88+
const cls = isTriad ? "aa-triad" : (AA_CLASSES[aa] || "");
89+
row += cls ? `<span class="${cls}">${aa}</span>` : aa;
90+
}
91+
html += row + "\n";
92+
}
93+
94+
// Triad annotation row
95+
let triadRow = " ".repeat(pad + 2);
96+
for (let i = start; i < end; i++) {
97+
const ch = triad[i] || ".";
98+
if ("DdE".includes(ch)) {
99+
triadRow += `<span class="aa-triad">${ch}</span>`;
100+
} else {
101+
triadRow += " ";
102+
}
103+
}
104+
html += triadRow + "\n";
105+
106+
// Conservation row
107+
let consRow = " ".repeat(pad + 2);
108+
for (let i = start; i < end; i++) {
109+
const col = seqs.map(s => s[i]);
110+
const unique = new Set(col);
111+
if (unique.size === 1) consRow += "*";
112+
else if (col.every(r => "AILMFWV".includes(r))) consRow += ":";
113+
else if (col.every(r => "DE".includes(r))) consRow += ":";
114+
else if (col.every(r => "KR".includes(r))) consRow += ":";
115+
else if (col.every(r => "NQST".includes(r))) consRow += ":";
116+
else consRow += " ";
117+
}
118+
html += consRow + "\n\n";
119+
}
120+
121+
html += "</pre></div>";
122+
return html;
123+
}
124+
45125
function buildDetailRow(teamData) {
46126
const tr = document.createElement("tr");
47127
tr.className = "detail-row";
@@ -75,7 +155,9 @@
75155
}
76156
}
77157

78-
td.innerHTML = `<div class="detail-panel">${checksHTML}${issuesHTML}</div>`;
158+
const alignmentHTML = renderAlignment(teamData.alignment);
159+
160+
td.innerHTML = `<div class="detail-panel">${checksHTML}${issuesHTML}${alignmentHTML}</div>`;
79161
tr.appendChild(td);
80162
tr.style.display = "none";
81163
return tr;

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:38:54.957636+00:00",
2+
"updated": "2026-03-08T21:47:22.080087+00:00",
33
"entries": [
44
{
55
"team": "test-entry",
66
"score": 31.5,
77
"n_sequences": 3,
88
"n_families": 1,
9-
"commit": "954249c",
9+
"commit": "676dd42",
1010
"scores": {
1111
"diversity": 0.0554,
1212
"annotation": 1.0,

docs/scores/test-entry.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"team": "test-entry",
3-
"timestamp": "2026-03-08T21:38:54.956448+00:00",
4-
"commit": "954249c",
3+
"timestamp": "2026-03-08T21:47:22.078521+00:00",
4+
"commit": "676dd42",
55
"n_sequences": 3,
66
"n_families": 1,
77
"checks": {
@@ -41,5 +41,13 @@
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............................................................."
4452
}
4553
}

docs/style.css

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,37 @@ tbody tr:hover { background: var(--accent-subtle); }
115115
.status-fail { color: var(--red); }
116116
.issue-list { list-style: disc; margin-left: 1.5rem; color: var(--text-muted); }
117117

118+
/* Alignment viewer */
119+
.alignment { margin-top: 1rem; }
120+
.alignment h3 { font-size: 0.9rem; margin-bottom: 0.5rem; color: var(--text-muted); }
121+
.aln-legend {
122+
font-size: 0.75rem;
123+
margin-bottom: 0.5rem;
124+
display: flex;
125+
flex-wrap: wrap;
126+
gap: 0.75rem;
127+
}
128+
.aln-block {
129+
font-family: "SF Mono", "Menlo", "Monaco", "Consolas", monospace;
130+
font-size: 0.7rem;
131+
line-height: 1.35;
132+
overflow-x: auto;
133+
padding: 0.75rem;
134+
background: var(--bg);
135+
border: 1px solid var(--border);
136+
border-radius: 4px;
137+
}
138+
.aln-label { color: var(--text-muted); }
139+
.aa-hydrophobic { color: #6cb4ee; }
140+
.aa-positive { color: #f47067; }
141+
.aa-negative { color: #d2a8ff; }
142+
.aa-polar { color: #7ee787; }
143+
.aa-cysteine { color: #e3b341; }
144+
.aa-glycine { color: #56d4dd; }
145+
.aa-proline { color: #f0b72f; font-weight: 700; }
146+
.aa-aromatic { color: #79dafa; font-weight: 700; }
147+
.aa-triad { color: #fff; background: #da3633; border-radius: 2px; padding: 0 1px; font-weight: 700; }
148+
118149
.empty-state {
119150
text-align: center;
120151
padding: 4rem 1rem;

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:38:54.957636+00:00",
2+
"updated": "2026-03-08T21:47:22.080087+00:00",
33
"entries": [
44
{
55
"team": "test-entry",
66
"score": 31.5,
77
"n_sequences": 3,
88
"n_families": 1,
9-
"commit": "954249c",
9+
"commit": "676dd42",
1010
"scores": {
1111
"diversity": 0.0554,
1212
"annotation": 1.0,

scores/test-entry.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"team": "test-entry",
3-
"timestamp": "2026-03-08T21:38:54.956448+00:00",
4-
"commit": "954249c",
3+
"timestamp": "2026-03-08T21:47:22.078521+00:00",
4+
"commit": "676dd42",
55
"n_sequences": 3,
66
"n_families": 1,
77
"checks": {
@@ -41,5 +41,13 @@
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............................................................."
4452
}
4553
}

0 commit comments

Comments
 (0)