Spaces:
Running
Demo polish: Intro visuals, §3 + §6 charts readability, code-snippet UI
Browse filesIntro tab (§1–§5)
- §1 Atoms: bold green base letters, two-line label (mono A/C/G/T over
full name).
- §2 Helix: redesign à la 3D banner — letters sit *between* the two
strands (helix–base–base–helix), rungs interrupted around them, faded
at crossings instead of culled so there are no bald gaps; thinner
strands, 5'/3' tags symmetric on both ends, tighter top spacing,
more breathing room at the bottom of the card. New base-pair legend:
A=T / G≡C in big green type with H-bond meta + caption.
- §3 + §4 Gene strip: horizontally centred; track-labels now have a
"LEGEND" prefix and §4 gets a duplicated legend.
- §5 Translate / fold / 3D viewer: codon-AA grid + caption centred,
fold-arrow margins trimmed, 3Dmol viewer 340 → 280 px, three-tier
protein caption (sans-serif title 19px / italic desc / mono meta with
clickable PDB link). Slow editorial spin (0.4 → 0.15), wheel events
on the viewer container forwarded to window scroll so the page no
longer traps when the cursor crosses the molecule. Loading retry on
setTimeout in case 3Dmol's deferred CDN script hasn't landed yet.
§3 Variant-effect chart (vep.js)
- viewBox grown ~1.7× (rowH 32 → 56, fonts +50%), variant names + Δ
values readable without zoom.
- Header on two rows: "LOG-LIKELIHOOD DIFFERENCE" centred above
VARIANT / ← LESS LIKELY / MORE LIKELY → so the captions stop
overlapping at column width.
- Bottom caption split into two lines (pathogenic / benign) so it no
longer truncates on the right edge.
§6 Results chart (results.js)
- Replaced the cramped 32-bar grouped-vertical plot with a horizontal
Cleveland layout: 8 task rows × 4 horizontal bars, full-width task
labels, italic-serif category gutter (Generative / Variant effect /
Perturbation / Long-context) with hairline separators on multi-row
groups.
- Two-family palette: Carbon-8B/3B in green, Evo2-7B / GENERator in
warm-neutral grey — "Carbon vs the rest" reads at a glance.
- Numerical labels only on the leader of each row (chevron tinted
Carbon-green when a Carbon model wins, neutral grey otherwise);
faint guideline behind every bar so n/a rows still read as continuous.
- Chart height doubled (viewBox H 638 → 1250) so each bar gets ~24 px
on screen instead of ~12.
- Legend variant `.chart-legend`: sentence-case Inter 12 px with bigger
rounded swatches (was mono uppercase 9 px). Removed the
"higher is better · all scores in %" trailer (the % tick + bar
direction speak for themselves).
- Generic `.legend-swatch` rule (was scoped to `.seq-label`, which
silently broke every other inline legend in the demo).
§2 / §3 Track + completion handles
- track.js: exon/intron rect 6/16 → 12/16, total H 28 → 40, vertically
centred so the track no longer reads as squashed.
- completion.js: triangular drag handles widened (TRI_HW 4 → 9) and
arrows lengthened (TRACK_TOP 8 → 12, ARROW = 12) so the prompt /
generation handles are easy to grab on touch and read clearly.
§6 UMAP default framing (umap.js)
- DEFAULT_VIEW pinned to the bottom-right pan extreme at MIN_SCALE
(with a small Y_EXTRA slack), so visitors land on the densest
framing and only pan up/left into the rest of the manifold rather
than out into empty space.
Code-snippet shared component (code-snippet.js + .css)
- Disclosure header restyled as a pill button: warm-off-white fill,
green `</>` chip on the left, chevron on the right that rotates 90°
when the panel opens. Border + bg shift to green tints on hover
/ open.
- Copy button lifted into the tabs strip (flex-aligned with the
language pills) instead of absolute-positioned.
- Highlight.js Python syntax highlighting wired in (idempotent, no-ops
if the CDN script is blocked).
Tab-lede copy
- Removed parentheses around §X chip anchors in the DNA Lab and
Carbon Recipe tab-ledes for cleaner inline reading.
Co-authored-by: Cursor <cursoragent@cursor.com>
- assets/js/sections/completion.js +12 -9
- assets/js/sections/intro.js +112 -27
- assets/js/sections/results.js +155 -73
- assets/js/sections/track.js +3 -2
- assets/js/sections/umap.js +17 -2
- assets/js/sections/vep.js +48 -22
- assets/js/shared/code-snippet.js +23 -0
- assets/styles/code-snippet.css +124 -30
- assets/styles/section-intro.css +139 -19
- assets/styles/sequence.css +37 -3
- demo.html +66 -36
|
@@ -46,11 +46,14 @@
|
|
| 46 |
}
|
| 47 |
|
| 48 |
function renderTrack() {
|
| 49 |
-
const W = 1000, H =
|
| 50 |
if (!gene) { els.track.innerHTML = ""; return; }
|
| 51 |
const scaleX = (bp) => (bp / gene.length) * W;
|
| 52 |
-
// Track body sits y=
|
| 53 |
-
const TRACK_TOP =
|
|
|
|
|
|
|
|
|
|
| 54 |
let svg = "";
|
| 55 |
// Background line through introns
|
| 56 |
svg += `<line class="intron" x1="0" y1="${INTRON_Y}" x2="${W}" y2="${INTRON_Y}"/>`;
|
|
@@ -70,20 +73,20 @@
|
|
| 70 |
// START handle: vertical line through the track body + downward triangle on top.
|
| 71 |
svg += `<g class="handle${dragging === "start" ? " dragging" : ""}" data-role="start" transform="translate(${xStart.toFixed(1)},0)">`
|
| 72 |
+ `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
|
| 73 |
-
+ `<polygon points="-
|
| 74 |
-
+ `<rect x="-
|
| 75 |
+ `</g>`;
|
| 76 |
// END handle (prompt end / gen start): vertical line + upward triangle on bottom.
|
| 77 |
svg += `<g class="handle${dragging === "end" ? " dragging" : ""}" data-role="end" transform="translate(${xEnd.toFixed(1)},0)">`
|
| 78 |
+ `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
|
| 79 |
-
+ `<polygon points="0,${TRACK_BOT} -
|
| 80 |
-
+ `<rect x="-
|
| 81 |
+ `</g>`;
|
| 82 |
// GEN-END handle: vertical line + downward triangle on top, green.
|
| 83 |
svg += `<g class="handle gen${dragging === "genend" ? " dragging" : ""}" data-role="genend" transform="translate(${xGenEnd.toFixed(1)},0)">`
|
| 84 |
+ `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
|
| 85 |
-
+ `<polygon points="-
|
| 86 |
-
+ `<rect x="-
|
| 87 |
+ `</g>`;
|
| 88 |
els.track.innerHTML = svg;
|
| 89 |
}
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
function renderTrack() {
|
| 49 |
+
const W = 1000, H = 52;
|
| 50 |
if (!gene) { els.track.innerHTML = ""; return; }
|
| 51 |
const scaleX = (bp) => (bp / gene.length) * W;
|
| 52 |
+
// Track body sits y=12..40; arrow tips reach y=0 (start/genend, top) and y=52 (end, bottom).
|
| 53 |
+
const TRACK_TOP = 12, TRACK_BOT = 40, INTRON_Y = 26, EXON_Y = 20, EXON_H = 12;
|
| 54 |
+
// Triangle half-width and arrow vertical run: bumped so the draggable
|
| 55 |
+
// handles read clearly without dominating the timeline body.
|
| 56 |
+
const TRI_HW = 9, ARROW = 12;
|
| 57 |
let svg = "";
|
| 58 |
// Background line through introns
|
| 59 |
svg += `<line class="intron" x1="0" y1="${INTRON_Y}" x2="${W}" y2="${INTRON_Y}"/>`;
|
|
|
|
| 73 |
// START handle: vertical line through the track body + downward triangle on top.
|
| 74 |
svg += `<g class="handle${dragging === "start" ? " dragging" : ""}" data-role="start" transform="translate(${xStart.toFixed(1)},0)">`
|
| 75 |
+ `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
|
| 76 |
+
+ `<polygon points="-${TRI_HW},0 ${TRI_HW},0 0,${ARROW}"/>`
|
| 77 |
+
+ `<rect x="-${TRI_HW + 4}" y="0" width="${(TRI_HW + 4) * 2}" height="${H}" fill="transparent"/>`
|
| 78 |
+ `</g>`;
|
| 79 |
// END handle (prompt end / gen start): vertical line + upward triangle on bottom.
|
| 80 |
svg += `<g class="handle${dragging === "end" ? " dragging" : ""}" data-role="end" transform="translate(${xEnd.toFixed(1)},0)">`
|
| 81 |
+ `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
|
| 82 |
+
+ `<polygon points="0,${TRACK_BOT} -${TRI_HW},${H} ${TRI_HW},${H}"/>`
|
| 83 |
+
+ `<rect x="-${TRI_HW + 4}" y="0" width="${(TRI_HW + 4) * 2}" height="${H}" fill="transparent"/>`
|
| 84 |
+ `</g>`;
|
| 85 |
// GEN-END handle: vertical line + downward triangle on top, green.
|
| 86 |
svg += `<g class="handle gen${dragging === "genend" ? " dragging" : ""}" data-role="genend" transform="translate(${xGenEnd.toFixed(1)},0)">`
|
| 87 |
+ `<line x1="0" y1="${TRACK_TOP}" x2="0" y2="${TRACK_BOT}"/>`
|
| 88 |
+
+ `<polygon points="-${TRI_HW},0 ${TRI_HW},0 0,${ARROW}"/>`
|
| 89 |
+
+ `<rect x="-${TRI_HW + 4}" y="0" width="${(TRI_HW + 4) * 2}" height="${H}" fill="transparent"/>`
|
| 90 |
+ `</g>`;
|
| 91 |
els.track.innerHTML = svg;
|
| 92 |
}
|
|
@@ -31,7 +31,7 @@
|
|
| 31 |
return e;
|
| 32 |
}
|
| 33 |
function text(parent, x, y, str, opts = {}) {
|
| 34 |
-
const
|
| 35 |
x, y,
|
| 36 |
"text-anchor": opts.anchor || "middle",
|
| 37 |
"dominant-baseline": opts.baseline || "central",
|
|
@@ -39,7 +39,9 @@
|
|
| 39 |
"font-size": opts.size || 11,
|
| 40 |
"font-weight": opts.weight || 500,
|
| 41 |
fill: opts.fill || STROKE,
|
| 42 |
-
}
|
|
|
|
|
|
|
| 43 |
t.textContent = str;
|
| 44 |
return t;
|
| 45 |
}
|
|
@@ -78,7 +80,7 @@
|
|
| 78 |
}
|
| 79 |
function drawAtomLabel(parent, a) {
|
| 80 |
el("circle", { cx: a.x, cy: a.y, r: 7, fill: "#fff", stroke: "none" }, parent);
|
| 81 |
-
text(parent, a.x, a.y, a.label, { size: 9.5, weight:
|
| 82 |
}
|
| 83 |
function molecule(spec) {
|
| 84 |
const svg = el("svg", { viewBox: spec.viewBox, xmlns: NS });
|
|
@@ -163,15 +165,19 @@
|
|
| 163 |
// ======================================================================
|
| 164 |
function buildHelix(slot) {
|
| 165 |
// 20 bp over 2 turns at ~10 bp / turn (matches real B-DNA scale).
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
const phaseA = 0, phaseB = Math.PI;
|
| 169 |
const svg = el("svg", { viewBox: `0 0 ${W} ${H}`, xmlns: NS }, slot);
|
| 170 |
|
| 171 |
function yAt(x, phase) { return yc + amp * Math.sin((2 * Math.PI * x) / period + phase); }
|
| 172 |
|
| 173 |
const rungSpacing = 24;
|
| 174 |
-
const minSep = 16;
|
| 175 |
const topSeq = "ATCGGCATCGTAGCCAGTCA";
|
| 176 |
const comp = { A: "T", T: "A", C: "G", G: "C" };
|
| 177 |
const rungs = [];
|
|
@@ -179,24 +185,63 @@
|
|
| 179 |
for (let x = rungSpacing / 2; x < W && seqIdx < topSeq.length; x += rungSpacing) {
|
| 180 |
const yA = yAt(x, phaseA);
|
| 181 |
const yB = yAt(x, phaseB);
|
| 182 |
-
const sep = Math.abs(yA - yB);
|
| 183 |
-
if (sep < minSep) continue;
|
| 184 |
rungs.push({
|
| 185 |
x,
|
| 186 |
yTop: Math.min(yA, yB),
|
| 187 |
yBot: Math.max(yA, yB),
|
|
|
|
| 188 |
top: topSeq[seqIdx],
|
| 189 |
bot: comp[topSeq[seqIdx]],
|
| 190 |
});
|
| 191 |
seqIdx++;
|
| 192 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
-
// 1. Rungs behind strands
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
for (const r of rungs) {
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
}
|
| 201 |
|
| 202 |
// 2. Strand paths
|
|
@@ -208,22 +253,38 @@
|
|
| 208 |
}
|
| 209 |
return d;
|
| 210 |
}
|
| 211 |
-
el("path", { d: pathD(phaseA), fill: "none", stroke: STROKE, "stroke-width":
|
| 212 |
-
el("path", { d: pathD(phaseB), fill: "none", stroke: STROKE, "stroke-width":
|
| 213 |
|
| 214 |
-
// 3. Letter haloes
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
for (const r of rungs) {
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
text(svg, r.x,
|
| 220 |
}
|
| 221 |
|
| 222 |
-
// 5′ / 3′ end tags
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
}
|
| 228 |
|
| 229 |
root.querySelectorAll("[data-helix]").forEach(buildHelix);
|
|
@@ -247,7 +308,15 @@
|
|
| 247 |
function init3DViewer() {
|
| 248 |
if (viewerInitialised) return;
|
| 249 |
const container = root.querySelector("#cd-protein-3d");
|
| 250 |
-
if (!container
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
viewerInitialised = true;
|
| 252 |
|
| 253 |
const loading = container.querySelector(".cd-protein-3d-loading");
|
|
@@ -270,7 +339,11 @@
|
|
| 270 |
viewer.setStyle({ resn: "HEM" }, { stick: { color: "#b8862c", radius: 0.22 } });
|
| 271 |
viewer.addStyle({ resn: "HEM", elem: "FE" }, { sphere: { color: "#cd5c2a", radius: 1.0 } });
|
| 272 |
viewer.zoomTo();
|
| 273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
viewer.render();
|
| 275 |
if (loading) loading.remove();
|
| 276 |
})
|
|
@@ -278,6 +351,18 @@
|
|
| 278 |
console.error("intro: failed to load hemoglobin PDB:", err);
|
| 279 |
if (loading) loading.textContent = "failed to load model";
|
| 280 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
}
|
| 282 |
function maybeInit() {
|
| 283 |
if (root.classList.contains("active")) init3DViewer();
|
|
|
|
| 31 |
return e;
|
| 32 |
}
|
| 33 |
function text(parent, x, y, str, opts = {}) {
|
| 34 |
+
const attrs = {
|
| 35 |
x, y,
|
| 36 |
"text-anchor": opts.anchor || "middle",
|
| 37 |
"dominant-baseline": opts.baseline || "central",
|
|
|
|
| 39 |
"font-size": opts.size || 11,
|
| 40 |
"font-weight": opts.weight || 500,
|
| 41 |
fill: opts.fill || STROKE,
|
| 42 |
+
};
|
| 43 |
+
if (opts.opacity != null) attrs["fill-opacity"] = opts.opacity;
|
| 44 |
+
const t = el("text", attrs, parent);
|
| 45 |
t.textContent = str;
|
| 46 |
return t;
|
| 47 |
}
|
|
|
|
| 80 |
}
|
| 81 |
function drawAtomLabel(parent, a) {
|
| 82 |
el("circle", { cx: a.x, cy: a.y, r: 7, fill: "#fff", stroke: "none" }, parent);
|
| 83 |
+
text(parent, a.x, a.y, a.label, { size: 9.5, weight: 600, fill: GREEN });
|
| 84 |
}
|
| 85 |
function molecule(spec) {
|
| 86 |
const svg = el("svg", { viewBox: spec.viewBox, xmlns: NS });
|
|
|
|
| 165 |
// ======================================================================
|
| 166 |
function buildHelix(slot) {
|
| 167 |
// 20 bp over 2 turns at ~10 bp / turn (matches real B-DNA scale).
|
| 168 |
+
// Vertical layout: brin paths oscillate between y = yc-amp and y = yc+amp
|
| 169 |
+
// (= 22..82). 5'/3' end tags add ySpread+2 = 14px of slack above and
|
| 170 |
+
// below those bounds. H = 96 sits 4px below the lower-most label so the
|
| 171 |
+
// SVG ends almost flush with the legend underneath instead of leaving
|
| 172 |
+
// a fat band of whitespace.
|
| 173 |
+
const W = 480, H = 96;
|
| 174 |
+
const yc = 52, amp = 30, period = 240;
|
| 175 |
const phaseA = 0, phaseB = Math.PI;
|
| 176 |
const svg = el("svg", { viewBox: `0 0 ${W} ${H}`, xmlns: NS }, slot);
|
| 177 |
|
| 178 |
function yAt(x, phase) { return yc + amp * Math.sin((2 * Math.PI * x) / period + phase); }
|
| 179 |
|
| 180 |
const rungSpacing = 24;
|
|
|
|
| 181 |
const topSeq = "ATCGGCATCGTAGCCAGTCA";
|
| 182 |
const comp = { A: "T", T: "A", C: "G", G: "C" };
|
| 183 |
const rungs = [];
|
|
|
|
| 185 |
for (let x = rungSpacing / 2; x < W && seqIdx < topSeq.length; x += rungSpacing) {
|
| 186 |
const yA = yAt(x, phaseA);
|
| 187 |
const yB = yAt(x, phaseB);
|
|
|
|
|
|
|
| 188 |
rungs.push({
|
| 189 |
x,
|
| 190 |
yTop: Math.min(yA, yB),
|
| 191 |
yBot: Math.max(yA, yB),
|
| 192 |
+
sep: Math.abs(yA - yB),
|
| 193 |
top: topSeq[seqIdx],
|
| 194 |
bot: comp[topSeq[seqIdx]],
|
| 195 |
});
|
| 196 |
seqIdx++;
|
| 197 |
}
|
| 198 |
+
// Depth fade: rungs at the helix waist (small sep) tint towards
|
| 199 |
+
// near-transparent so they read as "behind / out of focus" instead
|
| 200 |
+
// of as front-and-centre rungs. This fills the otherwise-empty gaps
|
| 201 |
+
// at the strand crossings (was previously skipping those rungs
|
| 202 |
+
// outright with a hard minSep filter) while keeping the visible
|
| 203 |
+
// letters concentrated where the helix is wide. Smoothstep echoes
|
| 204 |
+
// the depth-tint logic in the banner helix.
|
| 205 |
+
function fadeForSep(sep) {
|
| 206 |
+
const lo = 12, hi = 28;
|
| 207 |
+
const t = Math.max(0, Math.min(1, (sep - lo) / (hi - lo)));
|
| 208 |
+
// smoothstep, then map to [0.18, 1.0]: the floor stays just visible
|
| 209 |
+
// even at full crossing so the rung never reads as a hard gap.
|
| 210 |
+
const ease = t * t * (3 - 2 * t);
|
| 211 |
+
return 0.18 + 0.82 * ease;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
// Letter glyphs sit *between* the strands (not on them), echoing the
|
| 215 |
+
// banner helix's [strand]─letter─letter─[strand] layout. The two letters
|
| 216 |
+
// along each rung are pinned at 25% / 75% of the rung's vertical span so
|
| 217 |
+
// they sit clearly inside the rung rather than on either strand.
|
| 218 |
+
const LETTER_GAP = 6; // half-height of the slot punched out around each glyph
|
| 219 |
+
const STRAND_HALO = 3; // pull rungs back from the strand path so they don't kiss it
|
| 220 |
+
function rungLetterYs(r) {
|
| 221 |
+
const sep = r.yBot - r.yTop;
|
| 222 |
+
return [r.yTop + sep * 0.25, r.yTop + sep * 0.75];
|
| 223 |
+
}
|
| 224 |
|
| 225 |
+
// 1. Rungs behind strands. Each rung becomes up to three short segments,
|
| 226 |
+
// interrupted around each letter so the letter reads as sitting *in*
|
| 227 |
+
// the rung. Rungs at the helix waist still get drawn but with their
|
| 228 |
+
// opacity tapered via fadeForSep so the crossings don't read as
|
| 229 |
+
// bald gaps.
|
| 230 |
for (const r of rungs) {
|
| 231 |
+
const fade = fadeForSep(r.sep);
|
| 232 |
+
const [yA, yB] = rungLetterYs(r);
|
| 233 |
+
const segs = [
|
| 234 |
+
[r.yTop + STRAND_HALO, yA - LETTER_GAP],
|
| 235 |
+
[yA + LETTER_GAP, yB - LETTER_GAP],
|
| 236 |
+
[yB + LETTER_GAP, r.yBot - STRAND_HALO],
|
| 237 |
+
];
|
| 238 |
+
for (const [y1, y2] of segs) {
|
| 239 |
+
if (y2 - y1 < 1) continue;
|
| 240 |
+
el("line", {
|
| 241 |
+
x1: r.x, y1, x2: r.x, y2,
|
| 242 |
+
stroke: GREEN, "stroke-width": 1.4, "stroke-opacity": 0.55 * fade,
|
| 243 |
+
}, svg);
|
| 244 |
+
}
|
| 245 |
}
|
| 246 |
|
| 247 |
// 2. Strand paths
|
|
|
|
| 253 |
}
|
| 254 |
return d;
|
| 255 |
}
|
| 256 |
+
el("path", { d: pathD(phaseA), fill: "none", stroke: STROKE, "stroke-width": 1.5, "stroke-linecap": "round" }, svg);
|
| 257 |
+
el("path", { d: pathD(phaseB), fill: "none", stroke: STROKE, "stroke-width": 1.5, "stroke-linecap": "round" }, svg);
|
| 258 |
|
| 259 |
+
// 3. Letter glyphs themselves (no haloes, since the rung is already
|
| 260 |
+
// interrupted around each letter). fade tracks fadeForSep so the
|
| 261 |
+
// glyphs at the crossings sit as ghosted "out of focus" letters
|
| 262 |
+
// rather than disappearing entirely — fills the visual gap at the
|
| 263 |
+
// helix waist without competing with the rungs at full opacity.
|
| 264 |
for (const r of rungs) {
|
| 265 |
+
const fade = fadeForSep(r.sep);
|
| 266 |
+
const [yA, yB] = rungLetterYs(r);
|
| 267 |
+
text(svg, r.x, yA, r.top, { mono: true, size: 10, weight: 600, fill: GREEN, opacity: fade });
|
| 268 |
+
text(svg, r.x, yB, r.bot, { mono: true, size: 10, weight: 600, fill: GREEN, opacity: fade });
|
| 269 |
}
|
| 270 |
|
| 271 |
+
// 5′ / 3′ end tags. Each label is placed *outside* the higher/lower
|
| 272 |
+
// strand at that x — i.e. min/max over the two strands at the edge —
|
| 273 |
+
// rather than tied to one specific strand. The previous logic pinned
|
| 274 |
+
// 5′ to phaseA (above) and 3′ to phaseB (below), which made the two
|
| 275 |
+
// labels collapse vertically wherever the strands happened to cross
|
| 276 |
+
// close to the SVG edge (left side: ~17px apart) while staying nicely
|
| 277 |
+
// spread on the side where they didn't (right side: ~35px). Using
|
| 278 |
+
// min/max instead gives a symmetric ~35px gap on both ends regardless
|
| 279 |
+
// of where in the helix cycle the edge falls.
|
| 280 |
+
const ySpread = 12;
|
| 281 |
+
const yA0 = yAt(6, phaseA), yB0 = yAt(6, phaseB);
|
| 282 |
+
const yA1 = yAt(W - 6, phaseA), yB1 = yAt(W - 6, phaseB);
|
| 283 |
+
const endTagOpts = { mono: true, size: 9, weight: 400, fill: FAINT, opacity: 0.55 };
|
| 284 |
+
text(svg, 6, Math.min(yA0, yB0) - ySpread, "5′", endTagOpts);
|
| 285 |
+
text(svg, W - 6, Math.min(yA1, yB1) - ySpread, "3′", endTagOpts);
|
| 286 |
+
text(svg, 6, Math.max(yA0, yB0) + ySpread + 2, "3′", endTagOpts);
|
| 287 |
+
text(svg, W - 6, Math.max(yA1, yB1) + ySpread + 2, "5′", endTagOpts);
|
| 288 |
}
|
| 289 |
|
| 290 |
root.querySelectorAll("[data-helix]").forEach(buildHelix);
|
|
|
|
| 308 |
function init3DViewer() {
|
| 309 |
if (viewerInitialised) return;
|
| 310 |
const container = root.querySelector("#cd-protein-3d");
|
| 311 |
+
if (!container) return;
|
| 312 |
+
// 3Dmol is loaded as <script defer> from a CDN, so when intro.js runs
|
| 313 |
+
// synchronously during the body parse the global may not be ready yet.
|
| 314 |
+
// Retry on a short timer rather than giving up, otherwise the viewer
|
| 315 |
+
// never initialises and the panel is stuck on "loading hemoglobin…".
|
| 316 |
+
if (!window.$3Dmol) {
|
| 317 |
+
setTimeout(init3DViewer, 50);
|
| 318 |
+
return;
|
| 319 |
+
}
|
| 320 |
viewerInitialised = true;
|
| 321 |
|
| 322 |
const loading = container.querySelector(".cd-protein-3d-loading");
|
|
|
|
| 339 |
viewer.setStyle({ resn: "HEM" }, { stick: { color: "#b8862c", radius: 0.22 } });
|
| 340 |
viewer.addStyle({ resn: "HEM", elem: "FE" }, { sphere: { color: "#cd5c2a", radius: 1.0 } });
|
| 341 |
viewer.zoomTo();
|
| 342 |
+
// Slow editorial spin: 0.15 deg / tick at 3Dmol's ~30 fps render
|
| 343 |
+
// = ~4.5 deg/sec, a full rotation every ~80 seconds. Slow enough
|
| 344 |
+
// that the molecule reads as "alive" in peripheral vision but
|
| 345 |
+
// never demands attention while the visitor reads the prose.
|
| 346 |
+
viewer.spin("y", 0.15);
|
| 347 |
viewer.render();
|
| 348 |
if (loading) loading.remove();
|
| 349 |
})
|
|
|
|
| 351 |
console.error("intro: failed to load hemoglobin PDB:", err);
|
| 352 |
if (loading) loading.textContent = "failed to load model";
|
| 353 |
});
|
| 354 |
+
|
| 355 |
+
// 3Dmol installs a wheel listener on its internal canvas that zooms
|
| 356 |
+
// the camera AND preventDefaults the page scroll, which traps the
|
| 357 |
+
// page scroll whenever the cursor is over the viewer (the molecule
|
| 358 |
+
// sits mid-page so this hits constantly). Intercept wheel on the
|
| 359 |
+
// container in capture phase and stopImmediatePropagation so 3Dmol
|
| 360 |
+
// never sees the event — no preventDefault → the browser's native
|
| 361 |
+
// scroll (with trackpad momentum etc.) runs untouched. Pan / rotate
|
| 362 |
+
// (mouse-drag) are unaffected. Same pattern as §5 folding.
|
| 363 |
+
container.addEventListener("wheel", (e) => {
|
| 364 |
+
e.stopImmediatePropagation();
|
| 365 |
+
}, { capture: true, passive: true });
|
| 366 |
}
|
| 367 |
function maybeInit() {
|
| 368 |
if (root.classList.contains("active")) init3DViewer();
|
|
@@ -1,114 +1,196 @@
|
|
| 1 |
// =========================================================================
|
| 2 |
-
// §12, Results: training-free benchmark
|
| 3 |
// =========================================================================
|
| 4 |
// Numbers come straight from HuggingFaceBio/plots-release-code/barplot/barplot.py
|
| 5 |
// (the canonical paper figure source). NIAH @ 393 kbp matches the
|
| 6 |
// niah_heatmap CSV (yarn4x · plain · 65,536 tokens).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
(function initDemo12() {
|
| 8 |
const host = document.getElementById("d12-bars");
|
| 9 |
if (!host) return;
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
const MODELS = [
|
| 12 |
-
{ name: "Carbon
|
| 13 |
-
{ name: "Carbon
|
| 14 |
-
{ name: "Evo2-7B", color: "#
|
| 15 |
-
{ name: "GENERator-v2 3B", color: "#
|
| 16 |
];
|
| 17 |
|
| 18 |
-
//
|
| 19 |
-
//
|
|
|
|
| 20 |
const ROWS = [
|
| 21 |
-
{ task: "Sequence
|
| 22 |
-
{ task: "BRCA2",
|
| 23 |
-
{ task: "TraitGym
|
| 24 |
-
{ task: "ClinVar
|
| 25 |
-
{ task: "ClinVar
|
| 26 |
-
{ task: "Triplet
|
| 27 |
-
{ task: "Synonymous
|
| 28 |
-
{ task: "Genome-NIAH
|
| 29 |
];
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
function renderBars() {
|
| 32 |
-
const W = 1280
|
| 33 |
-
const padL =
|
| 34 |
-
const
|
| 35 |
-
const
|
| 36 |
-
const
|
| 37 |
-
const
|
| 38 |
-
const
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
for (let tick = 0; tick <= 100; tick += 20) {
|
| 45 |
-
const
|
| 46 |
-
|
| 47 |
-
|
|
|
|
| 48 |
}
|
| 49 |
-
svg += `<text x="${padL - 10}" y="${y + 4}" font-family="JetBrains Mono" font-size="10" fill="#888" text-anchor="end">${tick}</text>`;
|
| 50 |
}
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
// Group separators + bars + per-task best-star
|
| 56 |
ROWS.forEach((row, ri) => {
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
svg += `<line x1="${groupLeft}" y1="${padT}" x2="${groupLeft}" y2="${padT + innerH}" stroke="#f0f0f0" stroke-width="1"/>`;
|
| 60 |
-
}
|
| 61 |
-
const barsLeft = groupLeft + (groupW - groupBarW) / 2;
|
| 62 |
const present = row.vals.filter(v => v !== null);
|
| 63 |
const best = Math.max(...present);
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
MODELS.forEach((m, mi) => {
|
|
|
|
| 66 |
const v = row.vals[mi];
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
if (v === null) {
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
| 70 |
return;
|
| 71 |
}
|
| 72 |
-
const h = innerH * (v / 100);
|
| 73 |
-
const y = padT + innerH - h;
|
| 74 |
-
svg += `<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${barW.toFixed(1)}" height="${h.toFixed(1)}" fill="${m.color}"/>`;
|
| 75 |
-
// value label on top of bar
|
| 76 |
-
svg += `<text x="${(x + barW / 2).toFixed(1)}" y="${(y - 4).toFixed(1)}" font-family="JetBrains Mono" font-size="9" fill="${m.color}" text-anchor="middle" font-weight="500">${v.toFixed(1)}</text>`;
|
| 77 |
-
// tiny ▲ on the best bar in this group
|
| 78 |
-
if (v === best) {
|
| 79 |
-
svg += `<text x="${(x + barW / 2).toFixed(1)}" y="${(y - 16).toFixed(1)}" font-family="JetBrains Mono" font-size="9" fill="#1f1f1d" text-anchor="middle">▲</text>`;
|
| 80 |
-
}
|
| 81 |
-
});
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
});
|
| 89 |
-
});
|
| 90 |
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
else { cats.push({ cat: row.cat, start: ri, end: ri }); }
|
| 97 |
});
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
const
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
});
|
| 108 |
|
| 109 |
svg += `</svg>`;
|
| 110 |
host.innerHTML = svg;
|
| 111 |
}
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
renderBars();
|
| 114 |
})();
|
|
|
|
| 1 |
// =========================================================================
|
| 2 |
+
// §12, Results: training-free benchmark — horizontal Cleveland bar chart
|
| 3 |
// =========================================================================
|
| 4 |
// Numbers come straight from HuggingFaceBio/plots-release-code/barplot/barplot.py
|
| 5 |
// (the canonical paper figure source). NIAH @ 393 kbp matches the
|
| 6 |
// niah_heatmap CSV (yarn4x · plain · 65,536 tokens).
|
| 7 |
+
//
|
| 8 |
+
// Layout rationale: the previous vertical grouped-bar plot crammed 32 bars
|
| 9 |
+
// into ~750 px of column width; differences were visually crushed and labels
|
| 10 |
+
// were two-line mono at 10 px. The horizontal Cleveland format gives each
|
| 11 |
+
// task its own row with a fully readable label, lets the reader compare
|
| 12 |
+
// models at a glance per row, and keeps the "Carbon vs baselines" message
|
| 13 |
+
// front-and-centre via a two-family palette (greens for Carbon, neutrals
|
| 14 |
+
// for the rest).
|
| 15 |
(function initDemo12() {
|
| 16 |
const host = document.getElementById("d12-bars");
|
| 17 |
if (!host) return;
|
| 18 |
|
| 19 |
+
// Two-family palette: Carbon variants in editorial green (the demo's brand
|
| 20 |
+
// hue), baselines in warm neutral grey. The point is that the reader can
|
| 21 |
+
// tell "Carbon vs the rest" at a glance — the previous brown/beige pair
|
| 22 |
+
// for Evo2 / GENERator was too close in luminance to the green pair for
|
| 23 |
+
// that to register.
|
| 24 |
const MODELS = [
|
| 25 |
+
{ name: "Carbon-8B", color: "#1A7A40", isCarbon: true },
|
| 26 |
+
{ name: "Carbon-3B", color: "#6DBF7E", isCarbon: true },
|
| 27 |
+
{ name: "Evo2-7B", color: "#5A5A56", isCarbon: false },
|
| 28 |
+
{ name: "GENERator-v2 3B", color: "#B5B0A6", isCarbon: false },
|
| 29 |
];
|
| 30 |
|
| 31 |
+
// Rows are ordered by capability axis. Category strings group consecutive
|
| 32 |
+
// rows; the renderer collapses runs of identical category into a single
|
| 33 |
+
// italic gutter label on the left.
|
| 34 |
const ROWS = [
|
| 35 |
+
{ task: "Sequence recovery", cat: "Generative", vals: [64.03, 61.50, 59.83, 55.72] },
|
| 36 |
+
{ task: "BRCA2", cat: "Variant effect", vals: [85.60, 84.64, 83.52, 80.57] },
|
| 37 |
+
{ task: "TraitGym Mendelian", cat: "Variant effect", vals: [36.81, 34.24, 37.36, 20.68] },
|
| 38 |
+
{ task: "ClinVar coding", cat: "Variant effect", vals: [93.43, 93.30, 93.70, 91.98] },
|
| 39 |
+
{ task: "ClinVar non-coding", cat: "Variant effect", vals: [91.98, 91.56, 90.03, 90.61] },
|
| 40 |
+
{ task: "Triplet expansion", cat: "Perturbation", vals: [65.62, 65.94, 63.78, 49.82] },
|
| 41 |
+
{ task: "Synonymous codons", cat: "Perturbation", vals: [92.18, 82.78, 84.90, 74.08] },
|
| 42 |
+
{ task: "Genome-NIAH · 393 kbp", cat: "Long-context", vals: [86.00, 79.00, 80.00, null] },
|
| 43 |
];
|
| 44 |
|
| 45 |
+
// ---- Render ----------------------------------------------------------
|
| 46 |
+
// Layout constants. The SVG renders at viewBox W×H but scales to fit the
|
| 47 |
+
// host width via preserveAspectRatio. Heights are computed from row count
|
| 48 |
+
// so adding/removing tasks doesn't require manual H tweaks.
|
| 49 |
+
//
|
| 50 |
+
// Viewbox aspect ratio: at the previous (1280×638) the rendered chart sat
|
| 51 |
+
// at ~778×388 px in the two-column layout — readable but cramped. We
|
| 52 |
+
// double the internal heights (barH, gaps, fonts) so the viewBox grows
|
| 53 |
+
// to ~1280×1250, which scales to a ~778×760 rendered chart: roughly 2×
|
| 54 |
+
// taller, which makes individual bars and labels comfortable to read.
|
| 55 |
function renderBars() {
|
| 56 |
+
const W = 1280;
|
| 57 |
+
const padL = 24; // outer left padding (svg edge → category gutter)
|
| 58 |
+
const catW = 130; // category gutter width (italic serif label)
|
| 59 |
+
const taskW = 240; // task label column width
|
| 60 |
+
const padR = 90; // room for value label at end of bar
|
| 61 |
+
const padT = 60; // top padding (axis lives here)
|
| 62 |
+
const padB = 36; // bottom padding
|
| 63 |
+
|
| 64 |
+
const barH = 24; // bar thickness
|
| 65 |
+
const barGap = 5; // gap between bars within a task
|
| 66 |
+
const rowH = MODELS.length * barH + (MODELS.length - 1) * barGap; // 4*24 + 3*5 = 111
|
| 67 |
+
const taskGap = 26; // gap between consecutive tasks in same category
|
| 68 |
+
const catGap = 42; // extra gap between category groups
|
| 69 |
+
|
| 70 |
+
// Compute total H by walking rows + inserting catGap when category changes.
|
| 71 |
+
let bodyH = 0;
|
| 72 |
+
ROWS.forEach((row, i) => {
|
| 73 |
+
bodyH += rowH;
|
| 74 |
+
if (i < ROWS.length - 1) {
|
| 75 |
+
bodyH += (ROWS[i + 1].cat === row.cat) ? taskGap : catGap;
|
| 76 |
+
}
|
| 77 |
+
});
|
| 78 |
+
const H = padT + bodyH + padB;
|
| 79 |
+
|
| 80 |
+
const barsX = padL + catW + taskW; // x where bars start
|
| 81 |
+
const barsW = W - barsX - padR; // width of bars zone
|
| 82 |
+
const xForVal = (v) => barsX + barsW * (v / 100);
|
| 83 |
+
|
| 84 |
+
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMinYMin meet" style="display:block;width:100%;height:auto;background:#fff">`;
|
| 85 |
+
|
| 86 |
+
// ---- Axis (top) --------------------------------------------------
|
| 87 |
+
// Light gridlines every 20%, plus a baseline at the left edge. The
|
| 88 |
+
// x-axis ticks sit at the top of the chart so they don't get lost in
|
| 89 |
+
// the row separators below. We skip the "0" label because the strong
|
| 90 |
+
// baseline at x=0 already conveys it (and the label competed visually
|
| 91 |
+
// with the row labels just to its left).
|
| 92 |
+
const axisY = padT - 28;
|
| 93 |
for (let tick = 0; tick <= 100; tick += 20) {
|
| 94 |
+
const x = xForVal(tick);
|
| 95 |
+
svg += `<line x1="${x}" y1="${padT - 10}" x2="${x}" y2="${padT + bodyH}" stroke="#eee" stroke-width="1"/>`;
|
| 96 |
+
if (tick > 0) {
|
| 97 |
+
svg += `<text x="${x}" y="${axisY}" font-family="JetBrains Mono" font-size="14" fill="#888" text-anchor="middle">${tick}</text>`;
|
| 98 |
}
|
|
|
|
| 99 |
}
|
| 100 |
+
svg += `<text x="${xForVal(100) + 12}" y="${axisY}" font-family="JetBrains Mono" font-size="14" fill="#888" text-anchor="start">%</text>`;
|
| 101 |
+
|
| 102 |
+
// Strong baseline at x=0 anchors the bars visually.
|
| 103 |
+
svg += `<line x1="${xForVal(0)}" y1="${padT - 6}" x2="${xForVal(0)}" y2="${padT + bodyH}" stroke="#1f1f1d" stroke-width="1.5"/>`;
|
| 104 |
+
|
| 105 |
+
// ---- Group rows by category for the gutter labels ----------------
|
| 106 |
+
const groups = [];
|
| 107 |
+
ROWS.forEach((row, i) => {
|
| 108 |
+
const last = groups[groups.length - 1];
|
| 109 |
+
if (last && last.cat === row.cat) last.rows.push(i);
|
| 110 |
+
else groups.push({ cat: row.cat, rows: [i] });
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
// ---- Render rows --------------------------------------------------
|
| 114 |
+
let y = padT;
|
| 115 |
+
const rowYs = []; // remember each task's top y for category bracket pass
|
| 116 |
|
|
|
|
| 117 |
ROWS.forEach((row, ri) => {
|
| 118 |
+
rowYs.push(y);
|
| 119 |
+
const rowMid = y + rowH / 2;
|
|
|
|
|
|
|
|
|
|
| 120 |
const present = row.vals.filter(v => v !== null);
|
| 121 |
const best = Math.max(...present);
|
| 122 |
|
| 123 |
+
// Task label (left of bars). Sans-serif, slightly bolder so it reads
|
| 124 |
+
// as the row's title rather than as caption.
|
| 125 |
+
svg += `<text x="${barsX - 16}" y="${rowMid + 5}" font-family="Inter, sans-serif" font-size="18" font-weight="500" fill="#1f1f1d" text-anchor="end">${escapeXml(row.task)}</text>`;
|
| 126 |
+
|
| 127 |
+
// Bars + values for each model in the row.
|
| 128 |
MODELS.forEach((m, mi) => {
|
| 129 |
+
const barY = y + mi * (barH + barGap);
|
| 130 |
const v = row.vals[mi];
|
| 131 |
+
|
| 132 |
+
// Faint guideline behind the bar so the row reads as continuous
|
| 133 |
+
// even when a model has a short bar or null.
|
| 134 |
+
svg += `<line x1="${xForVal(0)}" y1="${barY + barH / 2}" x2="${xForVal(100)}" y2="${barY + barH / 2}" stroke="#f5f5f1" stroke-width="1"/>`;
|
| 135 |
+
|
| 136 |
if (v === null) {
|
| 137 |
+
// n/a marker: dashed segment + faint label, in line with baseline
|
| 138 |
+
// grey so it doesn't pop visually.
|
| 139 |
+
svg += `<line x1="${xForVal(0)}" y1="${barY + barH / 2}" x2="${xForVal(100)}" y2="${barY + barH / 2}" stroke="#d8d4c8" stroke-width="1.5" stroke-dasharray="3 4"/>`;
|
| 140 |
+
svg += `<text x="${xForVal(100) + 12}" y="${barY + barH / 2 + 5}" font-family="JetBrains Mono" font-size="14" fill="#aaa" text-anchor="start">n/a</text>`;
|
| 141 |
return;
|
| 142 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
+
const w = xForVal(v) - xForVal(0);
|
| 145 |
+
svg += `<rect x="${xForVal(0).toFixed(1)}" y="${barY}" width="${w.toFixed(1)}" height="${barH}" fill="${m.color}" rx="2"/>`;
|
| 146 |
+
|
| 147 |
+
// Value label at the end of the bar. Best is bold + dark with a
|
| 148 |
+
// chevron; chevron is tinted Carbon-green when a Carbon model leads
|
| 149 |
+
// (subtle editorial flair appuying the "Carbon leads on 6/8" line)
|
| 150 |
+
// and stays neutral grey when Evo2 wins, to read honestly.
|
| 151 |
+
const isBest = v === best;
|
| 152 |
+
const valColor = isBest ? "#1f1f1d" : "#999";
|
| 153 |
+
const valWeight = isBest ? 600 : 400;
|
| 154 |
+
const chevronColor = isBest ? (m.isCarbon ? "#1A7A40" : "#5A5A56") : null;
|
| 155 |
+
const chevron = chevronColor ? `<tspan fill="${chevronColor}" font-weight="700">▸ </tspan>` : "";
|
| 156 |
+
svg += `<text x="${(xForVal(v) + 10).toFixed(1)}" y="${barY + barH / 2 + 5}" font-family="JetBrains Mono" font-size="15" font-weight="${valWeight}" fill="${valColor}" text-anchor="start">${chevron}${v.toFixed(1)}</text>`;
|
| 157 |
});
|
|
|
|
| 158 |
|
| 159 |
+
// Advance y. catGap if next row belongs to a new category.
|
| 160 |
+
y += rowH;
|
| 161 |
+
if (ri < ROWS.length - 1) {
|
| 162 |
+
y += (ROWS[ri + 1].cat === row.cat) ? taskGap : catGap;
|
| 163 |
+
}
|
|
|
|
| 164 |
});
|
| 165 |
+
|
| 166 |
+
// ---- Category gutter labels -------------------------------------
|
| 167 |
+
// One italic serif label per category, vertically centred on its run
|
| 168 |
+
// of rows. Pure typographic separator — no boxes, no rules — to keep
|
| 169 |
+
// the chart calm.
|
| 170 |
+
groups.forEach(g => {
|
| 171 |
+
const yTop = rowYs[g.rows[0]];
|
| 172 |
+
const yBot = rowYs[g.rows[g.rows.length - 1]] + rowH;
|
| 173 |
+
const yMid = (yTop + yBot) / 2 + 6;
|
| 174 |
+
svg += `<text x="${padL + 4}" y="${yMid}" font-family="Georgia, serif" font-size="18" font-style="italic" fill="#666" text-anchor="start">${escapeXml(g.cat)}</text>`;
|
| 175 |
+
// Subtle vertical hairline to bracket the category run when it has
|
| 176 |
+
// more than one row. Skip for solo-row categories (Generative,
|
| 177 |
+
// Long-context) — a hairline next to a single row reads as noise.
|
| 178 |
+
if (g.rows.length > 1) {
|
| 179 |
+
svg += `<line x1="${padL + catW - 20}" y1="${yTop + 4}" x2="${padL + catW - 20}" y2="${yBot - 4}" stroke="#d8d4c8" stroke-width="1.5"/>`;
|
| 180 |
+
}
|
| 181 |
});
|
| 182 |
|
| 183 |
svg += `</svg>`;
|
| 184 |
host.innerHTML = svg;
|
| 185 |
}
|
| 186 |
|
| 187 |
+
// SVG-safe text. The dataset is hard-coded so this is mostly defensive,
|
| 188 |
+
// but cheap insurance if someone later adds a label with "&" or "<".
|
| 189 |
+
function escapeXml(s) {
|
| 190 |
+
return String(s).replace(/[<>&"']/g, c => ({
|
| 191 |
+
"<": "<", ">": ">", "&": "&", '"': """, "'": "'"
|
| 192 |
+
}[c]));
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
renderBars();
|
| 196 |
})();
|
|
@@ -26,17 +26,18 @@
|
|
| 26 |
const MAX_WINDOW = 24000; // matches scripts/precompute.py TRACK_MAX_BP
|
| 27 |
|
| 28 |
function renderTrack(scoredLen) {
|
| 29 |
-
const W = 1000, H =
|
| 30 |
if (!gene) { els.track.innerHTML = ""; return; }
|
| 31 |
const total = scoredLen || gene.length;
|
| 32 |
const scaleX = (bp) => (bp / total) * W;
|
|
|
|
| 33 |
let svg = "";
|
| 34 |
svg += `<line class="intron" x1="0" y1="${H/2}" x2="${W}" y2="${H/2}"/>`;
|
| 35 |
for (const e of gene.exons) {
|
| 36 |
if (e.start > total) continue;
|
| 37 |
const x = scaleX(e.start);
|
| 38 |
const w = Math.max(1, scaleX(Math.min(e.end, total) - e.start));
|
| 39 |
-
svg += `<rect class="exon" x="${x.toFixed(1)}" y="
|
| 40 |
}
|
| 41 |
els.track.innerHTML = svg;
|
| 42 |
}
|
|
|
|
| 26 |
const MAX_WINDOW = 24000; // matches scripts/precompute.py TRACK_MAX_BP
|
| 27 |
|
| 28 |
function renderTrack(scoredLen) {
|
| 29 |
+
const W = 1000, H = 40;
|
| 30 |
if (!gene) { els.track.innerHTML = ""; return; }
|
| 31 |
const total = scoredLen || gene.length;
|
| 32 |
const scaleX = (bp) => (bp / total) * W;
|
| 33 |
+
const EXON_H = 16, EXON_Y = (H - EXON_H) / 2; // vertically centered
|
| 34 |
let svg = "";
|
| 35 |
svg += `<line class="intron" x1="0" y1="${H/2}" x2="${W}" y2="${H/2}"/>`;
|
| 36 |
for (const e of gene.exons) {
|
| 37 |
if (e.start > total) continue;
|
| 38 |
const x = scaleX(e.start);
|
| 39 |
const w = Math.max(1, scaleX(Math.min(e.end, total) - e.start));
|
| 40 |
+
svg += `<rect class="exon" x="${x.toFixed(1)}" y="${EXON_Y}" width="${w.toFixed(1)}" height="${EXON_H}"/>`;
|
| 41 |
}
|
| 42 |
els.track.innerHTML = svg;
|
| 43 |
}
|
|
@@ -161,7 +161,18 @@
|
|
| 161 |
// zoom-out (scale ≥ MIN_SCALE in the wheel handler) so visitors can pan
|
| 162 |
// around but can never zoom *back* into the empty-margins view.
|
| 163 |
const MIN_SCALE = 1.35;
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
let view = { ...DEFAULT_VIEW };
|
| 166 |
let dpr = Math.max(1, window.devicePixelRatio || 1);
|
| 167 |
let needsRedraw = false;
|
|
@@ -721,7 +732,11 @@
|
|
| 721 |
view.tx = 0; view.ty = 0;
|
| 722 |
} else {
|
| 723 |
view.tx = Math.max(-m, Math.min(m, view.tx));
|
| 724 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 725 |
}
|
| 726 |
}
|
| 727 |
|
|
|
|
| 161 |
// zoom-out (scale ≥ MIN_SCALE in the wheel handler) so visitors can pan
|
| 162 |
// around but can never zoom *back* into the empty-margins view.
|
| 163 |
const MIN_SCALE = 1.35;
|
| 164 |
+
// Default framing = the bottom-right *extreme* reachable by pan at MIN_SCALE.
|
| 165 |
+
// clampPan() allows |tx|, |ty| ≤ 0.92·scale − 1 before whitespace would creep
|
| 166 |
+
// in at an edge; pinning the default to that exact corner means visitors land
|
| 167 |
+
// on the densest framing possible, and from the reset state they can only
|
| 168 |
+
// pan up / left into the rest of the layout — never down / right into empty
|
| 169 |
+
// space they shouldn't be looking at.
|
| 170 |
+
// Y_EXTRA: extra downward pan slack (in NDC) so the editorial framing can sit
|
| 171 |
+
// a touch below the natural bottom clamp — trades a thin sliver of whitespace
|
| 172 |
+
// along the bottom for a heavier anchor on the bottom-right clusters.
|
| 173 |
+
const PAN_EDGE = 0.92 * MIN_SCALE - 1;
|
| 174 |
+
const Y_EXTRA = 0.08;
|
| 175 |
+
const DEFAULT_VIEW = { tx: -PAN_EDGE, ty: PAN_EDGE + Y_EXTRA, scale: MIN_SCALE };
|
| 176 |
let view = { ...DEFAULT_VIEW };
|
| 177 |
let dpr = Math.max(1, window.devicePixelRatio || 1);
|
| 178 |
let needsRedraw = false;
|
|
|
|
| 732 |
view.tx = 0; view.ty = 0;
|
| 733 |
} else {
|
| 734 |
view.tx = Math.max(-m, Math.min(m, view.tx));
|
| 735 |
+
// Asymmetric Y clamp: +Y_EXTRA of slack on the downward side so the
|
| 736 |
+
// editorial default framing (ty = PAN_EDGE + Y_EXTRA) survives a redraw
|
| 737 |
+
// without snapping back to PAN_EDGE. Visitors can also pan that extra
|
| 738 |
+
// sliver down themselves — accepted whitespace cost.
|
| 739 |
+
view.ty = Math.max(-m, Math.min(m + Y_EXTRA, view.ty));
|
| 740 |
}
|
| 741 |
}
|
| 742 |
|
|
@@ -224,7 +224,19 @@
|
|
| 224 |
|
| 225 |
function renderForestBars() {
|
| 226 |
if (!VARIANTS) return;
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
// Sort variants by Δ ascending (most surprising-to-the-model first), but
|
| 229 |
// keep unscored ones at the bottom in their original order.
|
| 230 |
const indexed = VARIANTS.map((v, i) => ({ v, idx: i, d: cache[v.rs] ? cache[v.rs].altSum - cache[v.rs].refSum : null }));
|
|
@@ -260,26 +272,34 @@
|
|
| 260 |
// gray → charcoal
|
| 261 |
return `rgb(${lerp(170, 40, t)},${lerp(170, 40, t)},${lerp(170, 40, t)})`;
|
| 262 |
}
|
| 263 |
-
const VALUE_INSIDE_MIN =
|
|
|
|
| 264 |
|
| 265 |
let svg = "";
|
| 266 |
|
| 267 |
// --- Top axis: directional caption ---
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
|
| 273 |
// Faint shading: pathogenic-expected zone (left of 0)
|
| 274 |
-
svg += `<rect x="${padL.toFixed(1)}" y="${(padT -
|
| 275 |
|
| 276 |
// Center line
|
| 277 |
-
svg += `<line x1="${center}" y1="${padT -
|
| 278 |
// Axis ticks
|
| 279 |
for (const t of [-absMax, -absMax/2, 0, absMax/2, absMax]) {
|
| 280 |
const x = center + t * scale;
|
| 281 |
-
svg += `<line x1="${x.toFixed(1)}" y1="${(H - padB).toFixed(1)}" x2="${x.toFixed(1)}" y2="${(H - padB +
|
| 282 |
-
svg += `<text x="${x.toFixed(1)}" y="${(H - padB +
|
| 283 |
}
|
| 284 |
|
| 285 |
// --- Rows ---
|
|
@@ -287,16 +307,16 @@
|
|
| 287 |
const y = padT + i * rowH + rowH / 2;
|
| 288 |
|
| 289 |
// Curated category dot next to the variant name
|
| 290 |
-
const dotR =
|
| 291 |
-
const dotX = padL -
|
| 292 |
svg += `<circle cx="${dotX.toFixed(1)}" cy="${(y - 0.5).toFixed(1)}" r="${dotR}" fill="${sigColor(v.sig)}"><title>${v.sig}</title></circle>`;
|
| 293 |
|
| 294 |
// Variant name + tiny category label
|
| 295 |
-
svg += `<text x="${(dotX - dotR -
|
| 296 |
-
svg += `<text x="${(dotX - dotR -
|
| 297 |
|
| 298 |
if (d == null) {
|
| 299 |
-
svg += `<text x="${(center +
|
| 300 |
return;
|
| 301 |
}
|
| 302 |
|
|
@@ -304,24 +324,30 @@
|
|
| 304 |
const color = barColor(d);
|
| 305 |
const barX = Math.min(center, x);
|
| 306 |
const barW = Math.max(2, Math.abs(x - center));
|
| 307 |
-
svg += `<rect x="${barX.toFixed(1)}" y="${(y -
|
| 308 |
|
| 309 |
const label = (d >= 0 ? "+" : "") + d.toFixed(2);
|
| 310 |
const insideOK = barW >= VALUE_INSIDE_MIN && Math.abs(d) >= 0.5; // color is dark enough only away from neutral
|
| 311 |
if (insideOK) {
|
| 312 |
-
const tx = x + (d >= 0 ? -
|
| 313 |
const anchor = d >= 0 ? "end" : "start";
|
| 314 |
-
svg += `<text x="${tx.toFixed(1)}" y="${(y +
|
| 315 |
} else {
|
| 316 |
-
const tx = x + (d >= 0 ?
|
| 317 |
const anchor = d >= 0 ? "start" : "end";
|
| 318 |
-
svg += `<text x="${tx.toFixed(1)}" y="${(y +
|
| 319 |
}
|
| 320 |
});
|
| 321 |
|
| 322 |
// --- Bottom caption ---
|
| 323 |
-
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
|
| 326 |
els.bars.innerHTML = svg;
|
| 327 |
}
|
|
|
|
| 224 |
|
| 225 |
function renderForestBars() {
|
| 226 |
if (!VARIANTS) return;
|
| 227 |
+
// Layout sized so the SVG renders comfortably tall in the right-hand
|
| 228 |
+
// column of the §3 two-col layout (~828 px of width). The previous
|
| 229 |
+
// (W=1000, rowH=32) viewBox was ~3.6:1 wide which scaled down to ~230 px
|
| 230 |
+
// tall at column width, making the per-row text and bar values squint-
|
| 231 |
+
// small. Bumping rowH and font sizes here, and padding generously,
|
| 232 |
+
// gets us to roughly a 2.2:1 viewBox → ~380 px rendered, where the
|
| 233 |
+
// variant names and ±Δ values are readable without zoom.
|
| 234 |
+
//
|
| 235 |
+
// padT carries two stacked header lines (axis title above, then
|
| 236 |
+
// VARIANT / ← LESS LIKELY / MORE LIKELY → row), so it has to be
|
| 237 |
+
// a bit taller than a single-line caption would need. padB carries
|
| 238 |
+
// the tick row + a two-line bottom caption.
|
| 239 |
+
const W = 1000, rowH = 56, padL = 320, padR = 80, padT = 72, padB = 90;
|
| 240 |
// Sort variants by Δ ascending (most surprising-to-the-model first), but
|
| 241 |
// keep unscored ones at the bottom in their original order.
|
| 242 |
const indexed = VARIANTS.map((v, i) => ({ v, idx: i, d: cache[v.rs] ? cache[v.rs].altSum - cache[v.rs].refSum : null }));
|
|
|
|
| 272 |
// gray → charcoal
|
| 273 |
return `rgb(${lerp(170, 40, t)},${lerp(170, 40, t)},${lerp(170, 40, t)})`;
|
| 274 |
}
|
| 275 |
+
const VALUE_INSIDE_MIN = 64;
|
| 276 |
+
const BAR_H = 22;
|
| 277 |
|
| 278 |
let svg = "";
|
| 279 |
|
| 280 |
// --- Top axis: directional caption ---
|
| 281 |
+
// Two lines: the axis title sits above on its own row, then VARIANT
|
| 282 |
+
// / ← LESS LIKELY / MORE LIKELY → share the row below. Splitting these
|
| 283 |
+
// avoids the centre title colliding with the two side-arrow captions
|
| 284 |
+
// when the chart renders at column width (~778 px on screen, where
|
| 285 |
+
// viewBox 1000 px squashes everything to ~0.78 of its declared size).
|
| 286 |
+
const capTopY1 = 20; // axis title row
|
| 287 |
+
const capTopY2 = 46; // VARIANT / arrows row
|
| 288 |
+
svg += `<text x="${center.toFixed(1)}" y="${capTopY1}" font-family="JetBrains Mono" font-size="13" fill="#666" text-anchor="middle" letter-spacing="2" font-weight="500">LOG-LIKELIHOOD DIFFERENCE</text>`;
|
| 289 |
+
svg += `<text x="${(padL - 16).toFixed(1)}" y="${capTopY2}" font-family="JetBrains Mono" font-size="12" fill="#888" text-anchor="end" letter-spacing="1">VARIANT</text>`;
|
| 290 |
+
svg += `<text x="${padL.toFixed(1)}" y="${capTopY2}" font-family="JetBrains Mono" font-size="12" fill="#bc2e25" letter-spacing="1">← MUTATION LESS LIKELY</text>`;
|
| 291 |
+
svg += `<text x="${(W - padR).toFixed(1)}" y="${capTopY2}" font-family="JetBrains Mono" font-size="12" fill="#317f3f" letter-spacing="1" text-anchor="end">MUTATION MORE LIKELY →</text>`;
|
| 292 |
|
| 293 |
// Faint shading: pathogenic-expected zone (left of 0)
|
| 294 |
+
svg += `<rect x="${padL.toFixed(1)}" y="${(padT - 6).toFixed(1)}" width="${(center - padL).toFixed(1)}" height="${(ordered.length * rowH + 12).toFixed(1)}" fill="#bc2e25" opacity="0.04"/>`;
|
| 295 |
|
| 296 |
// Center line
|
| 297 |
+
svg += `<line x1="${center}" y1="${padT - 6}" x2="${center}" y2="${H - padB + 6}" stroke="#bbb" stroke-width="1"/>`;
|
| 298 |
// Axis ticks
|
| 299 |
for (const t of [-absMax, -absMax/2, 0, absMax/2, absMax]) {
|
| 300 |
const x = center + t * scale;
|
| 301 |
+
svg += `<line x1="${x.toFixed(1)}" y1="${(H - padB).toFixed(1)}" x2="${x.toFixed(1)}" y2="${(H - padB + 6).toFixed(1)}" stroke="#aaa"/>`;
|
| 302 |
+
svg += `<text x="${x.toFixed(1)}" y="${(H - padB + 20).toFixed(1)}" font-family="JetBrains Mono" font-size="13" fill="#888" text-anchor="middle">${t.toFixed(1)}</text>`;
|
| 303 |
}
|
| 304 |
|
| 305 |
// --- Rows ---
|
|
|
|
| 307 |
const y = padT + i * rowH + rowH / 2;
|
| 308 |
|
| 309 |
// Curated category dot next to the variant name
|
| 310 |
+
const dotR = 6;
|
| 311 |
+
const dotX = padL - 16 - dotR;
|
| 312 |
svg += `<circle cx="${dotX.toFixed(1)}" cy="${(y - 0.5).toFixed(1)}" r="${dotR}" fill="${sigColor(v.sig)}"><title>${v.sig}</title></circle>`;
|
| 313 |
|
| 314 |
// Variant name + tiny category label
|
| 315 |
+
svg += `<text x="${(dotX - dotR - 8).toFixed(1)}" y="${(y - 1).toFixed(1)}" font-family="JetBrains Mono" font-size="16" fill="#222" text-anchor="end">${v.name}</text>`;
|
| 316 |
+
svg += `<text x="${(dotX - dotR - 8).toFixed(1)}" y="${(y + 16).toFixed(1)}" font-family="JetBrains Mono" font-size="12" fill="${sigColor(v.sig)}" text-anchor="end">${v.sig.toLowerCase()}</text>`;
|
| 317 |
|
| 318 |
if (d == null) {
|
| 319 |
+
svg += `<text x="${(center + 8).toFixed(1)}" y="${(y + 5).toFixed(1)}" font-family="JetBrains Mono" font-size="14" fill="#ccc">not scored</text>`;
|
| 320 |
return;
|
| 321 |
}
|
| 322 |
|
|
|
|
| 324 |
const color = barColor(d);
|
| 325 |
const barX = Math.min(center, x);
|
| 326 |
const barW = Math.max(2, Math.abs(x - center));
|
| 327 |
+
svg += `<rect x="${barX.toFixed(1)}" y="${(y - BAR_H / 2).toFixed(1)}" width="${barW.toFixed(1)}" height="${BAR_H}" fill="${color}" stroke="${v === selected ? '#1f1f1d' : 'none'}" stroke-width="${v === selected ? 1 : 0}"/>`;
|
| 328 |
|
| 329 |
const label = (d >= 0 ? "+" : "") + d.toFixed(2);
|
| 330 |
const insideOK = barW >= VALUE_INSIDE_MIN && Math.abs(d) >= 0.5; // color is dark enough only away from neutral
|
| 331 |
if (insideOK) {
|
| 332 |
+
const tx = x + (d >= 0 ? -8 : 8);
|
| 333 |
const anchor = d >= 0 ? "end" : "start";
|
| 334 |
+
svg += `<text x="${tx.toFixed(1)}" y="${(y + 5).toFixed(1)}" font-family="JetBrains Mono" font-size="14" fill="#fff" text-anchor="${anchor}" font-weight="500">${label}</text>`;
|
| 335 |
} else {
|
| 336 |
+
const tx = x + (d >= 0 ? 8 : -8);
|
| 337 |
const anchor = d >= 0 ? "start" : "end";
|
| 338 |
+
svg += `<text x="${tx.toFixed(1)}" y="${(y + 5).toFixed(1)}" font-family="JetBrains Mono" font-size="14" fill="#333" text-anchor="${anchor}">${label}</text>`;
|
| 339 |
}
|
| 340 |
});
|
| 341 |
|
| 342 |
// --- Bottom caption ---
|
| 343 |
+
// Split across two lines so the full sentence fits at column width
|
| 344 |
+
// without truncating on the right edge. Each line is one half of the
|
| 345 |
+
// dichotomy (pathogenic vs benign) so the visual structure mirrors
|
| 346 |
+
// the meaning.
|
| 347 |
+
const capY1 = H - padB + 44;
|
| 348 |
+
const capY2 = H - padB + 60;
|
| 349 |
+
svg += `<text x="${padL.toFixed(1)}" y="${capY1}" font-family="JetBrains Mono" font-size="12" fill="#888" letter-spacing="0.5">pathogenic loss-of-function → mutation much less likely</text>`;
|
| 350 |
+
svg += `<text x="${padL.toFixed(1)}" y="${capY2}" font-family="JetBrains Mono" font-size="12" fill="#888" letter-spacing="0.5">benign / common variants → about as likely as the original</text>`;
|
| 351 |
|
| 352 |
els.bars.innerHTML = svg;
|
| 353 |
}
|
|
@@ -26,11 +26,33 @@
|
|
| 26 |
});
|
| 27 |
}
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
function wire(snippet) {
|
| 30 |
snippet.querySelectorAll(".code-snippet__tab").forEach(tab => {
|
| 31 |
tab.addEventListener("click", () => activate(snippet, tab.dataset.tab));
|
| 32 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
const copyBtn = snippet.querySelector(".code-snippet__copy");
|
|
|
|
|
|
|
|
|
|
| 34 |
if (copyBtn) {
|
| 35 |
copyBtn.addEventListener("click", async () => {
|
| 36 |
const panel = snippet.querySelector(".code-snippet__panel.active");
|
|
@@ -47,6 +69,7 @@
|
|
| 47 |
} catch (e) { /* clipboard blocked; fail quietly */ }
|
| 48 |
});
|
| 49 |
}
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
function init() {
|
|
|
|
| 26 |
});
|
| 27 |
}
|
| 28 |
|
| 29 |
+
// Run every <code> block through highlight.js. Every snippet in the demo
|
| 30 |
+
// is Python, so we hard-tag the language rather than rely on autodetect
|
| 31 |
+
// (which can mis-call short snippets). hljs is loaded via CDN with the
|
| 32 |
+
// same `defer` attribute as this script, but in case CSP / network
|
| 33 |
+
// blocks it we no-op gracefully and the snippets stay plain text.
|
| 34 |
+
function highlight(snippet) {
|
| 35 |
+
if (!window.hljs) return;
|
| 36 |
+
snippet.querySelectorAll(".code-snippet__panel code").forEach(code => {
|
| 37 |
+
if (code.dataset.highlighted) return;
|
| 38 |
+
code.classList.add("language-python");
|
| 39 |
+
try { window.hljs.highlightElement(code); } catch (e) { /* ignore */ }
|
| 40 |
+
code.dataset.highlighted = "1";
|
| 41 |
+
});
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
function wire(snippet) {
|
| 45 |
snippet.querySelectorAll(".code-snippet__tab").forEach(tab => {
|
| 46 |
tab.addEventListener("click", () => activate(snippet, tab.dataset.tab));
|
| 47 |
});
|
| 48 |
+
// Lift the copy button into the tabs strip if the markup still has it
|
| 49 |
+
// as a sibling. Lets the strip's flex layout vertically centre it
|
| 50 |
+
// against the tab pills instead of fighting absolute `top` pixels.
|
| 51 |
+
const tabsRow = snippet.querySelector(".code-snippet__tabs");
|
| 52 |
const copyBtn = snippet.querySelector(".code-snippet__copy");
|
| 53 |
+
if (tabsRow && copyBtn && copyBtn.parentNode !== tabsRow) {
|
| 54 |
+
tabsRow.appendChild(copyBtn);
|
| 55 |
+
}
|
| 56 |
if (copyBtn) {
|
| 57 |
copyBtn.addEventListener("click", async () => {
|
| 58 |
const panel = snippet.querySelector(".code-snippet__panel.active");
|
|
|
|
| 69 |
} catch (e) { /* clipboard blocked; fail quietly */ }
|
| 70 |
});
|
| 71 |
}
|
| 72 |
+
highlight(snippet);
|
| 73 |
}
|
| 74 |
|
| 75 |
function init() {
|
|
@@ -8,39 +8,72 @@
|
|
| 8 |
font-size: 11px;
|
| 9 |
}
|
| 10 |
|
| 11 |
-
/* Disclosure header.
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
| 14 |
.code-snippet > summary {
|
| 15 |
cursor: pointer;
|
| 16 |
-
color: #
|
| 17 |
-
font-weight:
|
| 18 |
letter-spacing: 1.2px;
|
| 19 |
text-transform: uppercase;
|
| 20 |
-
font-size:
|
| 21 |
-
padding:
|
| 22 |
list-style: none;
|
| 23 |
display: inline-flex;
|
| 24 |
align-items: center;
|
| 25 |
gap: 10px;
|
| 26 |
user-select: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
}
|
| 28 |
.code-snippet > summary::-webkit-details-marker { display: none; }
|
| 29 |
-
.code-snippet > summary:
|
| 30 |
-
|
| 31 |
-
display: inline-block;
|
| 32 |
-
width: 14px; height: 14px;
|
| 33 |
-
text-align: center;
|
| 34 |
background: #f4f8f4;
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
border-radius: 2px;
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
font-weight: 700;
|
| 39 |
-
|
| 40 |
-
|
| 41 |
}
|
| 42 |
-
.code-snippet
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
.code-snippet__body {
|
|
@@ -48,13 +81,14 @@
|
|
| 48 |
border: 1px solid #d4dcc9;
|
| 49 |
border-radius: 3px;
|
| 50 |
overflow: hidden;
|
| 51 |
-
background: #
|
| 52 |
position: relative;
|
| 53 |
}
|
| 54 |
|
| 55 |
.code-snippet__tabs {
|
| 56 |
display: flex;
|
| 57 |
-
|
|
|
|
| 58 |
border-bottom: 1px solid #d4dcc9;
|
| 59 |
}
|
| 60 |
.code-snippet__tab {
|
|
@@ -77,7 +111,7 @@
|
|
| 77 |
border-bottom-color: var(--green);
|
| 78 |
}
|
| 79 |
|
| 80 |
-
.code-snippet__panel { display: none; }
|
| 81 |
.code-snippet__panel.active { display: block; }
|
| 82 |
.code-snippet__panel pre {
|
| 83 |
margin: 0;
|
|
@@ -86,18 +120,79 @@
|
|
| 86 |
font-size: 12px;
|
| 87 |
line-height: 1.55;
|
| 88 |
color: #1f1f1d;
|
|
|
|
| 89 |
overflow-x: auto;
|
| 90 |
white-space: pre;
|
| 91 |
tab-size: 4;
|
| 92 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
/* Copy button
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
.code-snippet__copy {
|
| 97 |
-
|
| 98 |
-
|
| 99 |
padding: 4px 10px;
|
| 100 |
-
background:
|
| 101 |
border: 1px solid #d4dcc9;
|
| 102 |
border-radius: 3px;
|
| 103 |
font-family: inherit;
|
|
@@ -107,8 +202,7 @@
|
|
| 107 |
text-transform: uppercase;
|
| 108 |
color: #6b7a6e;
|
| 109 |
cursor: pointer;
|
| 110 |
-
|
| 111 |
-
transition: color 0.12s, border-color 0.12s;
|
| 112 |
}
|
| 113 |
-
.code-snippet__copy:hover { color: #1f1f1d; }
|
| 114 |
-
.code-snippet__copy.copied { color: var(--green); border-color: var(--green); }
|
|
|
|
| 8 |
font-size: 11px;
|
| 9 |
}
|
| 10 |
|
| 11 |
+
/* Disclosure header. Styled as a pill-button so the affordance reads as
|
| 12 |
+
a card-level action rather than a faint inline label: warm-off-white
|
| 13 |
+
fill + outline, a brand-green </> chip on the left to signal "code",
|
| 14 |
+
and a chevron on the right that rotates 90° when the panel is open.
|
| 15 |
+
Border + background shift to brand-green tints on hover / open to
|
| 16 |
+
reinforce state without needing a separate +/− glyph. */
|
| 17 |
.code-snippet > summary {
|
| 18 |
cursor: pointer;
|
| 19 |
+
color: #1f1f1d;
|
| 20 |
+
font-weight: 600;
|
| 21 |
letter-spacing: 1.2px;
|
| 22 |
text-transform: uppercase;
|
| 23 |
+
font-size: 10.5px;
|
| 24 |
+
padding: 7px 14px 7px 8px;
|
| 25 |
list-style: none;
|
| 26 |
display: inline-flex;
|
| 27 |
align-items: center;
|
| 28 |
gap: 10px;
|
| 29 |
user-select: none;
|
| 30 |
+
background: #f7f5ee;
|
| 31 |
+
border: 1px solid #c8c2b3;
|
| 32 |
+
border-radius: 3px;
|
| 33 |
+
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
| 34 |
}
|
| 35 |
.code-snippet > summary::-webkit-details-marker { display: none; }
|
| 36 |
+
.code-snippet > summary:hover {
|
| 37 |
+
border-color: var(--green);
|
|
|
|
|
|
|
|
|
|
| 38 |
background: #f4f8f4;
|
| 39 |
+
}
|
| 40 |
+
.code-snippet[open] > summary {
|
| 41 |
+
border-color: var(--green);
|
| 42 |
+
background: #f4f8f4;
|
| 43 |
+
}
|
| 44 |
+
/* "</>" code chip on the left: solid green pill, white glyph. Same
|
| 45 |
+
visual weight as the brand chips used elsewhere in the page. */
|
| 46 |
+
.code-snippet > summary::before {
|
| 47 |
+
content: "</>";
|
| 48 |
+
display: inline-flex;
|
| 49 |
+
align-items: center;
|
| 50 |
+
justify-content: center;
|
| 51 |
+
height: 18px;
|
| 52 |
+
padding: 0 5px;
|
| 53 |
+
background: var(--green);
|
| 54 |
+
color: #ffffff;
|
| 55 |
+
font-family: "JetBrains Mono", monospace;
|
| 56 |
+
font-weight: 700;
|
| 57 |
+
font-size: 10px;
|
| 58 |
+
letter-spacing: -0.5px;
|
| 59 |
border-radius: 2px;
|
| 60 |
+
}
|
| 61 |
+
/* Chevron on the right, pivots on open via CSS transform. */
|
| 62 |
+
.code-snippet > summary::after {
|
| 63 |
+
content: "›";
|
| 64 |
+
margin-left: 2px;
|
| 65 |
+
color: #6b7a6e;
|
| 66 |
+
font-size: 14px;
|
| 67 |
font-weight: 700;
|
| 68 |
+
line-height: 0;
|
| 69 |
+
transition: transform 0.15s, color 0.15s;
|
| 70 |
}
|
| 71 |
+
.code-snippet > summary:hover::after,
|
| 72 |
+
.code-snippet[open] > summary::after {
|
| 73 |
+
color: var(--green);
|
| 74 |
+
}
|
| 75 |
+
.code-snippet[open] > summary::after {
|
| 76 |
+
transform: rotate(90deg);
|
| 77 |
}
|
| 78 |
|
| 79 |
.code-snippet__body {
|
|
|
|
| 81 |
border: 1px solid #d4dcc9;
|
| 82 |
border-radius: 3px;
|
| 83 |
overflow: hidden;
|
| 84 |
+
background: #ffffff;
|
| 85 |
position: relative;
|
| 86 |
}
|
| 87 |
|
| 88 |
.code-snippet__tabs {
|
| 89 |
display: flex;
|
| 90 |
+
align-items: center;
|
| 91 |
+
background: #f7f5ee;
|
| 92 |
border-bottom: 1px solid #d4dcc9;
|
| 93 |
}
|
| 94 |
.code-snippet__tab {
|
|
|
|
| 111 |
border-bottom-color: var(--green);
|
| 112 |
}
|
| 113 |
|
| 114 |
+
.code-snippet__panel { display: none; background: #ffffff; }
|
| 115 |
.code-snippet__panel.active { display: block; }
|
| 116 |
.code-snippet__panel pre {
|
| 117 |
margin: 0;
|
|
|
|
| 120 |
font-size: 12px;
|
| 121 |
line-height: 1.55;
|
| 122 |
color: #1f1f1d;
|
| 123 |
+
background: #ffffff;
|
| 124 |
overflow-x: auto;
|
| 125 |
white-space: pre;
|
| 126 |
tab-size: 4;
|
| 127 |
}
|
| 128 |
+
/* highlight.js sets `background` on the `.hljs` class once it's done
|
| 129 |
+
processing a block. Force-override to transparent so our pure-white
|
| 130 |
+
panel background shows through regardless of which hljs theme (if any)
|
| 131 |
+
gets loaded. */
|
| 132 |
+
.code-snippet__panel pre code,
|
| 133 |
+
.code-snippet__panel pre code.hljs {
|
| 134 |
+
background: transparent;
|
| 135 |
+
padding: 0;
|
| 136 |
+
color: inherit;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/* --- highlight.js token palette --------------------------------------
|
| 140 |
+
We intentionally don't load a hljs theme stylesheet. Instead, define
|
| 141 |
+
token colours here so the Python snippets stay on-brand: brand green
|
| 142 |
+
for keywords / builtins, warm amber for strings, blue for numbers,
|
| 143 |
+
italic muted gray for comments. Everything else falls through to the
|
| 144 |
+
default ink colour set on <pre>. */
|
| 145 |
+
.code-snippet__panel .hljs-comment,
|
| 146 |
+
.code-snippet__panel .hljs-quote {
|
| 147 |
+
color: #9a958a;
|
| 148 |
+
font-style: italic;
|
| 149 |
+
}
|
| 150 |
+
.code-snippet__panel .hljs-keyword,
|
| 151 |
+
.code-snippet__panel .hljs-built_in,
|
| 152 |
+
.code-snippet__panel .hljs-literal,
|
| 153 |
+
.code-snippet__panel .hljs-type {
|
| 154 |
+
color: #317f3f;
|
| 155 |
+
}
|
| 156 |
+
.code-snippet__panel .hljs-string,
|
| 157 |
+
.code-snippet__panel .hljs-regexp,
|
| 158 |
+
.code-snippet__panel .hljs-doctag {
|
| 159 |
+
color: #a9762f;
|
| 160 |
+
}
|
| 161 |
+
.code-snippet__panel .hljs-number {
|
| 162 |
+
color: #2c5aa0;
|
| 163 |
+
}
|
| 164 |
+
.code-snippet__panel .hljs-meta,
|
| 165 |
+
.code-snippet__panel .hljs-meta .hljs-string {
|
| 166 |
+
color: #7a6242;
|
| 167 |
+
}
|
| 168 |
+
.code-snippet__panel .hljs-title,
|
| 169 |
+
.code-snippet__panel .hljs-title.function_,
|
| 170 |
+
.code-snippet__panel .hljs-title.class_,
|
| 171 |
+
.code-snippet__panel .hljs-class .hljs-title,
|
| 172 |
+
.code-snippet__panel .hljs-section {
|
| 173 |
+
color: #1f1f1d;
|
| 174 |
+
font-weight: 500;
|
| 175 |
+
}
|
| 176 |
+
.code-snippet__panel .hljs-attr,
|
| 177 |
+
.code-snippet__panel .hljs-params,
|
| 178 |
+
.code-snippet__panel .hljs-variable,
|
| 179 |
+
.code-snippet__panel .hljs-symbol,
|
| 180 |
+
.code-snippet__panel .hljs-operator,
|
| 181 |
+
.code-snippet__panel .hljs-punctuation {
|
| 182 |
+
color: #1f1f1d;
|
| 183 |
+
}
|
| 184 |
|
| 185 |
+
/* Copy button. Lives inside .code-snippet__tabs (the JS lifts it there
|
| 186 |
+
on init) so the flex `align-items: center` on the strip handles
|
| 187 |
+
vertical centring against the tab pills. `margin-left: auto` shoves
|
| 188 |
+
it to the right edge. On a pure-white panel background we lean on a
|
| 189 |
+
faint warm fill + border so the chip stays visible without competing
|
| 190 |
+
with the code. */
|
| 191 |
.code-snippet__copy {
|
| 192 |
+
margin-left: auto;
|
| 193 |
+
margin-right: 12px;
|
| 194 |
padding: 4px 10px;
|
| 195 |
+
background: #f7f5ee;
|
| 196 |
border: 1px solid #d4dcc9;
|
| 197 |
border-radius: 3px;
|
| 198 |
font-family: inherit;
|
|
|
|
| 202 |
text-transform: uppercase;
|
| 203 |
color: #6b7a6e;
|
| 204 |
cursor: pointer;
|
| 205 |
+
transition: color 0.12s, border-color 0.12s, background 0.12s;
|
|
|
|
| 206 |
}
|
| 207 |
+
.code-snippet__copy:hover { color: #1f1f1d; background: #f1ece0; }
|
| 208 |
+
.code-snippet__copy.copied { color: var(--green); border-color: var(--green); background: #ffffff; }
|
|
@@ -134,33 +134,87 @@
|
|
| 134 |
}
|
| 135 |
.cd-mol-svg { width: 100%; max-width: 130px; }
|
| 136 |
.cd-mol-svg svg { display: block; width: 100%; height: auto; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
.cd-mol-label {
|
|
|
|
|
|
|
| 138 |
font-family: "JetBrains Mono", monospace;
|
| 139 |
font-size: 11px; letter-spacing: 0.06em;
|
| 140 |
color: var(--ink-soft);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
}
|
| 142 |
-
.cd-mol-label b { color: var(--ink); font-weight: 600; margin-right: 6px; }
|
| 143 |
|
| 144 |
/* --- row 2 : DNA helix ------------------------------------------------- */
|
| 145 |
/* No background/border on the wrapper; the parent .demo card carries
|
| 146 |
the white frame. The wrapper is just here to host the SVG. */
|
| 147 |
.cd-helix-wrap svg { display: block; width: 100%; height: auto; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
.cd-helix-rules {
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
font-family: "JetBrains Mono", monospace;
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
color: var(--ink-soft);
|
|
|
|
| 154 |
}
|
| 155 |
-
.cd-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
}
|
| 159 |
-
.cd-helix-rules .pair b { font-weight: 600; color: var(--green); }
|
| 160 |
|
| 161 |
/* --- rows 3 + 4 : gene strip (per-segment bar + letters) --------------- */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
.cd-gene-strip {
|
| 163 |
-
display: flex; flex-wrap: wrap;
|
|
|
|
|
|
|
| 164 |
font-family: "JetBrains Mono", monospace;
|
| 165 |
font-size: 14px; font-weight: 400;
|
| 166 |
letter-spacing: 0.06em; line-height: 1.9;
|
|
@@ -189,8 +243,11 @@
|
|
| 189 |
.cd-genex:last-child .cd-genex-bar { border-top-right-radius: 2px; border-bottom-right-radius: 2px; }
|
| 190 |
|
| 191 |
.cd-track-labels {
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
| 194 |
font-family: "JetBrains Mono", monospace;
|
| 195 |
font-size: 10px;
|
| 196 |
letter-spacing: 0.14em; text-transform: uppercase;
|
|
@@ -201,24 +258,45 @@
|
|
| 201 |
width: 18px; height: 8px; border-radius: 1px;
|
| 202 |
vertical-align: 1px; margin-right: 6px;
|
| 203 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
/* --- row 4 specific : splicing (top strip → arrows → bottom mRNA) ----- */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
.cd-splice {
|
| 207 |
-
display:
|
| 208 |
-
|
| 209 |
max-width: 100%;
|
|
|
|
| 210 |
}
|
| 211 |
.cd-splice .cd-gene-strip { flex-wrap: nowrap; margin-bottom: 0; }
|
| 212 |
.cd-splice-arrows { display: block; width: 100%; }
|
| 213 |
-
.cd-mrna-strip { justify-content: center; }
|
| 214 |
|
| 215 |
/* --- row 5 : protein, codon → AA translation table -------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
.cd-translate {
|
| 217 |
display: grid;
|
| 218 |
grid-template-columns: max-content repeat(10, max-content);
|
| 219 |
align-items: center; justify-items: center;
|
| 220 |
column-gap: 6px; row-gap: 4px;
|
| 221 |
-
|
|
|
|
| 222 |
}
|
| 223 |
.cd-trow-label {
|
| 224 |
justify-self: end;
|
|
@@ -280,7 +358,7 @@
|
|
| 280 |
.cd-fold-arrow {
|
| 281 |
display: flex; flex-direction: column;
|
| 282 |
align-items: center;
|
| 283 |
-
margin:
|
| 284 |
}
|
| 285 |
.cd-fold-arrow-icon {
|
| 286 |
font-family: "JetBrains Mono", monospace;
|
|
@@ -297,10 +375,12 @@
|
|
| 297 |
|
| 298 |
/* Container for the 3Dmol.js viewer (id="cd-protein-3d"). The .demo
|
| 299 |
parent provides the white card; this just supplies a height for the
|
| 300 |
-
WebGL canvas.
|
|
|
|
|
|
|
| 301 |
.cd-protein-3d {
|
| 302 |
width: 100%;
|
| 303 |
-
height:
|
| 304 |
position: relative;
|
| 305 |
overflow: hidden;
|
| 306 |
}
|
|
@@ -313,13 +393,53 @@
|
|
| 313 |
color: var(--ink-faint);
|
| 314 |
pointer-events: none;
|
| 315 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
.cd-protein-caption {
|
| 317 |
-
margin-top:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
font-family: "JetBrains Mono", monospace;
|
| 319 |
font-size: 10px;
|
| 320 |
letter-spacing: 0.14em; text-transform: uppercase;
|
| 321 |
color: var(--ink-faint);
|
| 322 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
|
| 324 |
@media (max-width: 720px) {
|
| 325 |
.cd-mols { grid-template-columns: repeat(2, 1fr); }
|
|
|
|
| 134 |
}
|
| 135 |
.cd-mol-svg { width: 100%; max-width: 130px; }
|
| 136 |
.cd-mol-svg svg { display: block; width: 100%; height: auto; }
|
| 137 |
+
/* Two-line label: big mono letter (A / C / G / T) sits above the full
|
| 138 |
+
name. The <b> is the only element in the source — the trailing text
|
| 139 |
+
node "adenine"/etc. becomes an anonymous flex item, so flex column +
|
| 140 |
+
align-items: center stacks them automatically without touching the
|
| 141 |
+
HTML. */
|
| 142 |
.cd-mol-label {
|
| 143 |
+
display: flex; flex-direction: column;
|
| 144 |
+
align-items: center; gap: 2px;
|
| 145 |
font-family: "JetBrains Mono", monospace;
|
| 146 |
font-size: 11px; letter-spacing: 0.06em;
|
| 147 |
color: var(--ink-soft);
|
| 148 |
+
text-align: center;
|
| 149 |
+
}
|
| 150 |
+
.cd-mol-label b {
|
| 151 |
+
display: block;
|
| 152 |
+
margin: 0;
|
| 153 |
+
font-size: 22px; font-weight: 700;
|
| 154 |
+
letter-spacing: 0.02em; line-height: 1;
|
| 155 |
+
color: var(--ink);
|
| 156 |
}
|
|
|
|
| 157 |
|
| 158 |
/* --- row 2 : DNA helix ------------------------------------------------- */
|
| 159 |
/* No background/border on the wrapper; the parent .demo card carries
|
| 160 |
the white frame. The wrapper is just here to host the SVG. */
|
| 161 |
.cd-helix-wrap svg { display: block; width: 100%; height: auto; }
|
| 162 |
+
/* Pairing legend under the helix. Two centred A=T / G≡C tiles + an
|
| 163 |
+
H-bond sub-label that turns the typographic difference between
|
| 164 |
+
= and ≡ into actual chemistry. Caption below, mono uppercase, faint,
|
| 165 |
+
matching the .cd-protein-caption / .cd-track-labels treatment so the
|
| 166 |
+
primer reads with one consistent caption voice. */
|
| 167 |
.cd-helix-rules {
|
| 168 |
+
/* Tighter gap to the helix above so the legend reads as a caption to
|
| 169 |
+
the diagram, plus extra slack below to push the whole pair-block
|
| 170 |
+
further from the bottom edge of the .demo card. The .demo's own
|
| 171 |
+
padding-bottom (24px from controls.css) sits underneath this margin
|
| 172 |
+
for ~40px of total breathing room below the caption. */
|
| 173 |
+
margin: 12px 0 16px;
|
| 174 |
+
display: flex; flex-direction: column;
|
| 175 |
+
align-items: center; gap: 12px;
|
| 176 |
font-family: "JetBrains Mono", monospace;
|
| 177 |
+
}
|
| 178 |
+
.cd-helix-rules-pairs {
|
| 179 |
+
display: flex; gap: 56px; align-items: flex-start;
|
| 180 |
+
}
|
| 181 |
+
.cd-pair {
|
| 182 |
+
display: flex; flex-direction: column;
|
| 183 |
+
align-items: center; gap: 8px;
|
| 184 |
+
}
|
| 185 |
+
.cd-pair-formula {
|
| 186 |
+
display: inline-flex; align-items: center; gap: 10px;
|
| 187 |
+
font-size: 24px; font-weight: 700;
|
| 188 |
+
letter-spacing: 0.04em; line-height: 1;
|
| 189 |
+
color: var(--green);
|
| 190 |
+
}
|
| 191 |
+
.cd-pair-bond {
|
| 192 |
color: var(--ink-soft);
|
| 193 |
+
font-weight: 500;
|
| 194 |
}
|
| 195 |
+
.cd-pair-meta {
|
| 196 |
+
font-size: 9.5px; letter-spacing: 0.18em;
|
| 197 |
+
text-transform: uppercase;
|
| 198 |
+
color: var(--ink-faint);
|
| 199 |
+
}
|
| 200 |
+
.cd-pair-caption {
|
| 201 |
+
margin-top: 2px;
|
| 202 |
+
font-size: 10px; letter-spacing: 0.16em;
|
| 203 |
+
text-transform: uppercase;
|
| 204 |
+
color: var(--ink-faint);
|
| 205 |
}
|
|
|
|
| 206 |
|
| 207 |
/* --- rows 3 + 4 : gene strip (per-segment bar + letters) --------------- */
|
| 208 |
+
/* Strip is centred in its parent .demo card, not left-aligned: the gene
|
| 209 |
+
sketch is a self-contained diagram and reads better with breathing room
|
| 210 |
+
on both sides than tucked against the card's left edge. justify-content
|
| 211 |
+
handles the in-flow case (segments fit in one line); margin: 0 auto with
|
| 212 |
+
width: max-content + max-width: 100% covers the inline-block-style
|
| 213 |
+
centring when the strip is shorter than the parent. */
|
| 214 |
.cd-gene-strip {
|
| 215 |
+
display: flex; flex-wrap: wrap;
|
| 216 |
+
align-items: flex-start; justify-content: center;
|
| 217 |
+
width: max-content; max-width: 100%; margin: 0 auto;
|
| 218 |
font-family: "JetBrains Mono", monospace;
|
| 219 |
font-size: 14px; font-weight: 400;
|
| 220 |
letter-spacing: 0.06em; line-height: 1.9;
|
|
|
|
| 243 |
.cd-genex:last-child .cd-genex-bar { border-top-right-radius: 2px; border-bottom-right-radius: 2px; }
|
| 244 |
|
| 245 |
.cd-track-labels {
|
| 246 |
+
/* margin-top bumped 12 → 28 so the legend reads as a separate caption
|
| 247 |
+
row under the gene strip rather than sitting flush against the
|
| 248 |
+
letters of the sequence. */
|
| 249 |
+
display: flex; align-items: center; justify-content: center; gap: 14px;
|
| 250 |
+
margin-top: 28px;
|
| 251 |
font-family: "JetBrains Mono", monospace;
|
| 252 |
font-size: 10px;
|
| 253 |
letter-spacing: 0.14em; text-transform: uppercase;
|
|
|
|
| 258 |
width: 18px; height: 8px; border-radius: 1px;
|
| 259 |
vertical-align: 1px; margin-right: 6px;
|
| 260 |
}
|
| 261 |
+
/* "LEGEND" prefix that labels the row as a key. Slightly darker + tighter
|
| 262 |
+
tracking than the items themselves so the eye groups it as the title
|
| 263 |
+
of the row rather than another item. The margin-right adds a wider
|
| 264 |
+
gap between "LEGEND" and the first swatch on top of the row's gap, so
|
| 265 |
+
the title visually detaches from the items. */
|
| 266 |
+
.cd-track-labels__title {
|
| 267 |
+
color: var(--ink-soft);
|
| 268 |
+
font-weight: 600;
|
| 269 |
+
letter-spacing: 0.2em;
|
| 270 |
+
margin-right: 6px;
|
| 271 |
+
}
|
| 272 |
|
| 273 |
/* --- row 4 specific : splicing (top strip → arrows → bottom mRNA) ----- */
|
| 274 |
+
/* Splice block hosts pre-mRNA + arrow svg + spliced mRNA. We centre the
|
| 275 |
+
whole stack so the three rows line up on a single vertical axis inside
|
| 276 |
+
the parent card. The inner .cd-gene-strip rules above already do the
|
| 277 |
+
centring for the two strips themselves; this block centres the
|
| 278 |
+
transcribe-arrow svg between them too. */
|
| 279 |
.cd-splice {
|
| 280 |
+
display: block;
|
| 281 |
+
width: max-content;
|
| 282 |
max-width: 100%;
|
| 283 |
+
margin: 0 auto;
|
| 284 |
}
|
| 285 |
.cd-splice .cd-gene-strip { flex-wrap: nowrap; margin-bottom: 0; }
|
| 286 |
.cd-splice-arrows { display: block; width: 100%; }
|
|
|
|
| 287 |
|
| 288 |
/* --- row 5 : protein, codon → AA translation table -------------------- */
|
| 289 |
+
/* Grid sized to its content (label + 10 codon columns) and centred in the
|
| 290 |
+
parent card. The grid itself stays a fixed width regardless of the card
|
| 291 |
+
width, so margin: 0 auto picks up the leftover space symmetrically on
|
| 292 |
+
both sides. */
|
| 293 |
.cd-translate {
|
| 294 |
display: grid;
|
| 295 |
grid-template-columns: max-content repeat(10, max-content);
|
| 296 |
align-items: center; justify-items: center;
|
| 297 |
column-gap: 6px; row-gap: 4px;
|
| 298 |
+
width: max-content; max-width: 100%;
|
| 299 |
+
margin: 0 auto 14px;
|
| 300 |
}
|
| 301 |
.cd-trow-label {
|
| 302 |
justify-self: end;
|
|
|
|
| 358 |
.cd-fold-arrow {
|
| 359 |
display: flex; flex-direction: column;
|
| 360 |
align-items: center;
|
| 361 |
+
margin: 8px 0 10px;
|
| 362 |
}
|
| 363 |
.cd-fold-arrow-icon {
|
| 364 |
font-family: "JetBrains Mono", monospace;
|
|
|
|
| 375 |
|
| 376 |
/* Container for the 3Dmol.js viewer (id="cd-protein-3d"). The .demo
|
| 377 |
parent provides the white card; this just supplies a height for the
|
| 378 |
+
WebGL canvas. Height trimmed (340 → 280) so the viewer doesn't dwarf
|
| 379 |
+
the rest of the row; 3Dmol's zoomTo() refits the molecule to whatever
|
| 380 |
+
container size it gets, so the molecule still fills the frame. */
|
| 381 |
.cd-protein-3d {
|
| 382 |
width: 100%;
|
| 383 |
+
height: 280px;
|
| 384 |
position: relative;
|
| 385 |
overflow: hidden;
|
| 386 |
}
|
|
|
|
| 393 |
color: var(--ink-faint);
|
| 394 |
pointer-events: none;
|
| 395 |
}
|
| 396 |
+
/* Three-tier caption under the 3D viewer:
|
| 397 |
+
· title (sans, medium): the protein's common name, page-voice
|
| 398 |
+
· desc (sans, light, italic): one-line plain-language gloss
|
| 399 |
+
· meta (mono, uppercase, faint): chain count + clickable PDB ID
|
| 400 |
+
Mirrors the editorial pattern used by museum / journal captions:
|
| 401 |
+
name → what it is in plain English → the technical reference.
|
| 402 |
+
The mono meta line ties this row back to the rest of the primer's
|
| 403 |
+
captions (cd-pair-caption, cd-track-labels) so the page still has
|
| 404 |
+
one consistent caption voice. */
|
| 405 |
.cd-protein-caption {
|
| 406 |
+
margin-top: 10px;
|
| 407 |
+
text-align: center;
|
| 408 |
+
display: flex; flex-direction: column;
|
| 409 |
+
align-items: center; gap: 4px;
|
| 410 |
+
}
|
| 411 |
+
.cd-protein-caption__title {
|
| 412 |
+
font-family: "Inter", sans-serif;
|
| 413 |
+
font-size: 19px; font-weight: 500;
|
| 414 |
+
letter-spacing: -0.01em; line-height: 1.2;
|
| 415 |
+
color: var(--ink);
|
| 416 |
+
}
|
| 417 |
+
.cd-protein-caption__desc {
|
| 418 |
+
font-family: "Inter", sans-serif;
|
| 419 |
+
font-size: 13px; font-weight: 300;
|
| 420 |
+
line-height: 1.45;
|
| 421 |
+
color: var(--ink-soft);
|
| 422 |
+
}
|
| 423 |
+
.cd-protein-caption__meta {
|
| 424 |
+
margin-top: 6px;
|
| 425 |
font-family: "JetBrains Mono", monospace;
|
| 426 |
font-size: 10px;
|
| 427 |
letter-spacing: 0.14em; text-transform: uppercase;
|
| 428 |
color: var(--ink-faint);
|
| 429 |
}
|
| 430 |
+
.cd-protein-caption__meta a {
|
| 431 |
+
color: inherit;
|
| 432 |
+
text-decoration: underline;
|
| 433 |
+
text-decoration-color: rgba(138, 138, 131, 0.4);
|
| 434 |
+
text-underline-offset: 2px;
|
| 435 |
+
transition: color 0.15s ease, text-decoration-color 0.15s ease;
|
| 436 |
+
}
|
| 437 |
+
.cd-protein-caption__meta a:hover,
|
| 438 |
+
.cd-protein-caption__meta a:focus-visible {
|
| 439 |
+
color: var(--green);
|
| 440 |
+
text-decoration-color: var(--green);
|
| 441 |
+
outline: none;
|
| 442 |
+
}
|
| 443 |
|
| 444 |
@media (max-width: 720px) {
|
| 445 |
.cd-mols { grid-template-columns: repeat(2, 1fr); }
|
|
@@ -13,10 +13,10 @@
|
|
| 13 |
}
|
| 14 |
.gene-info strong { color: #1f1f1d; font-weight: 500; }
|
| 15 |
.gene-track {
|
| 16 |
-
width: 100%; height:
|
| 17 |
margin: 4px 0 8px;
|
| 18 |
}
|
| 19 |
-
.gene-track.draggable { height:
|
| 20 |
.gene-track .exon { fill: #317f3f; }
|
| 21 |
.gene-track .intron { stroke: #aaa; stroke-width: 1; }
|
| 22 |
.gene-track .playhead { stroke: #bc2e25; stroke-width: 2; }
|
|
@@ -85,10 +85,44 @@
|
|
| 85 |
font-size: 9px; color: #888; text-transform: uppercase; letter-spacing: 1.5px;
|
| 86 |
margin-top: 14px; margin-bottom: 4px; display: flex; gap: 12px; align-items: center;
|
| 87 |
}
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
display: inline-block; width: 8px; height: 8px; vertical-align: middle;
|
| 90 |
margin-right: 4px; border-radius: 1px;
|
| 91 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
/* Inline tag chips used in §5 to disambiguate carbon vs reference rows.
|
| 93 |
Same shape/size, different colour band so the eye instantly maps a
|
| 94 |
row of AAs to the correct identity without re-reading the full label. */
|
|
|
|
| 13 |
}
|
| 14 |
.gene-info strong { color: #1f1f1d; font-weight: 500; }
|
| 15 |
.gene-track {
|
| 16 |
+
width: 100%; height: 40px; display: block;
|
| 17 |
margin: 4px 0 8px;
|
| 18 |
}
|
| 19 |
+
.gene-track.draggable { height: 52px; touch-action: none; }
|
| 20 |
.gene-track .exon { fill: #317f3f; }
|
| 21 |
.gene-track .intron { stroke: #aaa; stroke-width: 1; }
|
| 22 |
.gene-track .playhead { stroke: #bc2e25; stroke-width: 2; }
|
|
|
|
| 85 |
font-size: 9px; color: #888; text-transform: uppercase; letter-spacing: 1.5px;
|
| 86 |
margin-top: 14px; margin-bottom: 4px; display: flex; gap: 12px; align-items: center;
|
| 87 |
}
|
| 88 |
+
/* Generic colour-swatch chip used in inline legends across the demo
|
| 89 |
+
(.seq-label, .track-axis-label, etc.). The class is the obvious
|
| 90 |
+
semantic hook, so the selector is intentionally global rather than
|
| 91 |
+
nested under a single parent — having the rule scoped to .seq-label
|
| 92 |
+
broke every other legend that reused the pattern (e.g. the §6 results
|
| 93 |
+
chart legend), which all rendered as zero-width invisible spans. */
|
| 94 |
+
.legend-swatch {
|
| 95 |
display: inline-block; width: 8px; height: 8px; vertical-align: middle;
|
| 96 |
margin-right: 4px; border-radius: 1px;
|
| 97 |
}
|
| 98 |
+
|
| 99 |
+
/* Chart legend variant of .track-axis-label (used by the §6 results
|
| 100 |
+
bars chart). The base class is mono uppercase 9px which suits a thin
|
| 101 |
+
axis caption; for a 4-model legend that pattern was hard to read.
|
| 102 |
+
The variant keeps the demo's typographic system but bumps the legend
|
| 103 |
+
to sentence-case 12px Inter and gives the swatches more visual weight
|
| 104 |
+
so they read as a proper colour key rather than caption text. */
|
| 105 |
+
.chart-legend {
|
| 106 |
+
font-family: "Inter", sans-serif;
|
| 107 |
+
font-size: 12px;
|
| 108 |
+
text-transform: none;
|
| 109 |
+
letter-spacing: 0;
|
| 110 |
+
color: var(--ink, #1f1f1d);
|
| 111 |
+
padding-top: 16px;
|
| 112 |
+
gap: 22px;
|
| 113 |
+
flex-wrap: wrap;
|
| 114 |
+
align-items: center;
|
| 115 |
+
justify-content: flex-start;
|
| 116 |
+
}
|
| 117 |
+
.chart-legend__item {
|
| 118 |
+
display: inline-flex; align-items: center;
|
| 119 |
+
font-weight: 500;
|
| 120 |
+
}
|
| 121 |
+
.chart-legend__item .legend-swatch {
|
| 122 |
+
width: 14px; height: 14px;
|
| 123 |
+
margin-right: 7px;
|
| 124 |
+
border-radius: 2px;
|
| 125 |
+
}
|
| 126 |
/* Inline tag chips used in §5 to disambiguate carbon vs reference rows.
|
| 127 |
Same shape/size, different colour band so the eye instantly maps a
|
| 128 |
row of AAs to the correct identity without re-reading the full label. */
|
|
@@ -10,6 +10,15 @@
|
|
| 10 |
<!-- 3Dmol.js: lightweight WebGL molecular viewer, used by §5 (folding) to
|
| 11 |
render ESMFold-predicted protein cartoons. Pinned for reproducibility. -->
|
| 12 |
<script defer src="https://cdn.jsdelivr.net/npm/3dmol@2.5.1/build/3Dmol-min.js"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
<!-- Modular CSS, served from /assets/styles/. Order matters because
|
| 15 |
several keyframes (pulse) and shared atoms (.seq-block, .seq-label,
|
|
@@ -225,10 +234,22 @@
|
|
| 225 |
<div class="section-body">
|
| 226 |
<div class="demo">
|
| 227 |
<div class="cd-helix-wrap" data-helix></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
<div class="cd-helix-rules">
|
| 229 |
-
<
|
| 230 |
-
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
</div>
|
| 233 |
</div>
|
| 234 |
</div>
|
|
@@ -249,6 +270,7 @@
|
|
| 249 |
<div class="demo">
|
| 250 |
<div class="cd-gene-strip"><span class="cd-genex cd-genex--promoter"><span class="cd-genex-bar"></span><span class="cd-genex-text">TATAAA</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">ATGGCCGAACTG</span></span><span class="cd-genex cd-genex--intron"><span class="cd-genex-bar"></span><span class="cd-genex-text">GTAAGCATATAG</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">CCCGGGTGGTTC</span></span><span class="cd-genex cd-genex--intron"><span class="cd-genex-bar"></span><span class="cd-genex-text">GTACGCCATTAG</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">AGCCGT</span></span></div>
|
| 251 |
<div class="cd-track-labels">
|
|
|
|
| 252 |
<span><span class="sw" style="background: var(--promoter)"></span>promoter</span>
|
| 253 |
<span><span class="sw" style="background: var(--green)"></span>exon</span>
|
| 254 |
<span><span class="sw" style="background: transparent; border-top: 1px solid var(--intron); height: 1px; margin-top: 4px;"></span>intron</span>
|
|
@@ -288,6 +310,12 @@
|
|
| 288 |
</svg>
|
| 289 |
<div class="cd-gene-strip cd-mrna-strip"><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">AUGGCCGAACUG</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">CCCGGGUGGUUC</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">AGCCGU</span></span></div>
|
| 290 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
</div>
|
| 292 |
</div>
|
| 293 |
</div>
|
|
@@ -326,7 +354,11 @@
|
|
| 326 |
<div class="cd-protein-3d" id="cd-protein-3d">
|
| 327 |
<div class="cd-protein-3d-loading">loading hemoglobin…</div>
|
| 328 |
</div>
|
| 329 |
-
<div class="cd-protein-caption">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
</div>
|
| 331 |
</div>
|
| 332 |
</div>
|
|
@@ -349,14 +381,14 @@
|
|
| 349 |
The interesting question is what else falls out of that. We didn't tell Carbon what an
|
| 350 |
exon is. We didn't tell it which mutations are pathogenic. We didn't tell it how genes
|
| 351 |
differ between species. The sections below are ways to read what it picked up
|
| 352 |
-
anyway: autocomplete a gene
|
| 353 |
-
structure emerge in its confidence
|
| 354 |
-
a disease variant against a healthy one
|
| 355 |
-
recognise a gene's species of origin
|
| 356 |
and then push further into folded protein structure
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
<code>HuggingFaceBio/Carbon-3B</code> checkpoint behind a live inference endpoint.
|
| 361 |
</p>
|
| 362 |
</div>
|
|
@@ -376,11 +408,9 @@
|
|
| 376 |
intron just before the 2nd exon plus the first 35 bp of that exon, and ask it to
|
| 377 |
<em>finish the exon</em>. The model streams the remaining bases one 6-base token at a
|
| 378 |
time. Exons are the protein-coding parts of a gene and are under strong evolutionary
|
| 379 |
-
constraint, so they should be the most predictable stretches of DNA.
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
exon/intron annotations on top of the output so you can compare what Carbon produces to
|
| 383 |
-
what's actually there.
|
| 384 |
</p>
|
| 385 |
</div>
|
| 386 |
|
|
@@ -400,7 +430,7 @@
|
|
| 400 |
</div>
|
| 401 |
|
| 402 |
<div class="gene-info" id="d1-info">loading genes…</div>
|
| 403 |
-
<svg class="gene-track draggable" id="d1-track" viewBox="0 0 1000
|
| 404 |
<div class="track-axis-label" style="justify-content:flex-end;gap:20px;align-items:center">
|
| 405 |
<span class="legend-tip"
|
| 406 |
data-tip="Exon: coding segment of the gene. Stays in the mature mRNA and gets translated into protein."
|
|
@@ -460,9 +490,10 @@
|
|
| 460 |
<div class="takeaway">
|
| 461 |
<p>
|
| 462 |
<strong>Try it</strong>
|
| 463 |
-
Drag the
|
| 464 |
-
|
| 465 |
-
|
|
|
|
| 466 |
</p>
|
| 467 |
<p>
|
| 468 |
<strong>What to look for</strong>
|
|
@@ -560,9 +591,9 @@ print(tok.decode(new_ids))</code></pre></div>
|
|
| 560 |
</div>
|
| 561 |
|
| 562 |
<div class="gene-info" id="d3-info">loading genes…</div>
|
| 563 |
-
<svg class="gene-track" id="d3-track" viewBox="0 0 1000
|
| 564 |
<svg id="d3-chart" style="display:block;width:100%;height:140px;background:#fff;border:1px solid #eee;margin-top:6px" preserveAspectRatio="none" viewBox="0 0 1000 140"></svg>
|
| 565 |
-
<div class="track-axis-label">
|
| 566 |
<span><span class="legend-swatch" style="background:#317f3f"></span>exon (shaded)</span>
|
| 567 |
<span style="color:#aaa">y-axis: log P per 6-bp token (higher = more confident)</span>
|
| 568 |
<span id="d3-bp-label" style="color:#888">0 bp</span>
|
|
@@ -787,7 +818,6 @@ print(f"delta = {delta:+.2f} (less likely if negative)")</code></pre></div>
|
|
| 787 |
<div class="demo-toolbar">
|
| 788 |
<span>gene</span>
|
| 789 |
<span id="d4-pills" class="pills"></span>
|
| 790 |
-
<span class="spacer"></span>
|
| 791 |
<span>prefix</span>
|
| 792 |
<span id="d4-prefix-pills" class="pills">
|
| 793 |
<button class="pill" data-prefix="200">200</button>
|
|
@@ -799,6 +829,7 @@ print(f"delta = {delta:+.2f} (less likely if negative)")</code></pre></div>
|
|
| 799 |
<button class="pill active" data-gen="60">60</button>
|
| 800 |
<button class="pill" data-gen="200">200</button>
|
| 801 |
</span>
|
|
|
|
| 802 |
<button id="d4-go" class="action primary">▶ run all</button>
|
| 803 |
<span class="status is-hidden" id="d4-status"><span class="dot"></span><span></span></span>
|
| 804 |
</div>
|
|
@@ -1158,15 +1189,15 @@ for name, ids in zip(species_prefixes, new_ids):
|
|
| 1158 |
</p>
|
| 1159 |
<p class="tab-lede__note">
|
| 1160 |
The sections below walk through each of those choices: how the tokenizer changes
|
| 1161 |
-
what a "token" means in DNA
|
| 1162 |
-
FNS rescues training in the BF16 regime
|
| 1163 |
-
what's in the training corpus
|
| 1164 |
-
what the architecture looks like
|
| 1165 |
how 8k-token pretraining reaches 786 kbp at inference
|
| 1166 |
-
|
| 1167 |
Evo2-7B and GENERator-v2 on the full training-free suite
|
| 1168 |
-
|
| 1169 |
-
|
| 1170 |
</p>
|
| 1171 |
</div>
|
| 1172 |
</div>
|
|
@@ -1425,12 +1456,11 @@ for name, ids in zip(species_prefixes, new_ids):
|
|
| 1425 |
<div class="section-body">
|
| 1426 |
<div class="demo" id="demo12">
|
| 1427 |
<div id="d12-bars"></div>
|
| 1428 |
-
<div class="track-axis-label
|
| 1429 |
-
<span><span class="legend-swatch" style="background:#1A7A40"></span>Carbon 8B</span>
|
| 1430 |
-
<span><span class="legend-swatch" style="background:#6DBF7E"></span>Carbon 3B</span>
|
| 1431 |
-
<span><span class="legend-swatch" style="background:#
|
| 1432 |
-
<span><span class="legend-swatch" style="background:#
|
| 1433 |
-
<span style="color:#888">higher is better · all scores in %</span>
|
| 1434 |
</div>
|
| 1435 |
</div>
|
| 1436 |
|
|
|
|
| 10 |
<!-- 3Dmol.js: lightweight WebGL molecular viewer, used by §5 (folding) to
|
| 11 |
render ESMFold-predicted protein cartoons. Pinned for reproducibility. -->
|
| 12 |
<script defer src="https://cdn.jsdelivr.net/npm/3dmol@2.5.1/build/3Dmol-min.js"></script>
|
| 13 |
+
<!-- highlight.js: syntax-highlights the Python snippets inside every
|
| 14 |
+
<details class="code-snippet"> "Run this from code" block. We load
|
| 15 |
+
the official browser distribution from the `cdn-release` repo (the
|
| 16 |
+
/npm/ path serves CommonJS modules that throw `require is not
|
| 17 |
+
defined` in the browser). Bundle ships Python pre-registered. We
|
| 18 |
+
intentionally do NOT load a hljs theme stylesheet, code-snippet.css
|
| 19 |
+
defines our own token colours so the snippets stay on-brand with
|
| 20 |
+
the editorial palette. -->
|
| 21 |
+
<script defer src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
| 22 |
|
| 23 |
<!-- Modular CSS, served from /assets/styles/. Order matters because
|
| 24 |
several keyframes (pulse) and shared atoms (.seq-block, .seq-label,
|
|
|
|
| 234 |
<div class="section-body">
|
| 235 |
<div class="demo">
|
| 236 |
<div class="cd-helix-wrap" data-helix></div>
|
| 237 |
+
<!-- Pairing legend: two big A=T / G≡C tiles with an H-bond
|
| 238 |
+
sub-label that turns the visual difference between =
|
| 239 |
+
and ≡ into the actual chemistry (2 vs 3 hydrogen bonds).
|
| 240 |
+
Caption sits below the pair row, centred. -->
|
| 241 |
<div class="cd-helix-rules">
|
| 242 |
+
<div class="cd-helix-rules-pairs">
|
| 243 |
+
<div class="cd-pair">
|
| 244 |
+
<div class="cd-pair-formula"><span class="cd-pair-letter">A</span><span class="cd-pair-bond">═</span><span class="cd-pair-letter">T</span></div>
|
| 245 |
+
<div class="cd-pair-meta">2 H bonds</div>
|
| 246 |
+
</div>
|
| 247 |
+
<div class="cd-pair">
|
| 248 |
+
<div class="cd-pair-formula"><span class="cd-pair-letter">G</span><span class="cd-pair-bond">≡</span><span class="cd-pair-letter">C</span></div>
|
| 249 |
+
<div class="cd-pair-meta">3 H bonds</div>
|
| 250 |
+
</div>
|
| 251 |
+
</div>
|
| 252 |
+
<div class="cd-pair-caption">complementary base pairing</div>
|
| 253 |
</div>
|
| 254 |
</div>
|
| 255 |
</div>
|
|
|
|
| 270 |
<div class="demo">
|
| 271 |
<div class="cd-gene-strip"><span class="cd-genex cd-genex--promoter"><span class="cd-genex-bar"></span><span class="cd-genex-text">TATAAA</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">ATGGCCGAACTG</span></span><span class="cd-genex cd-genex--intron"><span class="cd-genex-bar"></span><span class="cd-genex-text">GTAAGCATATAG</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">CCCGGGTGGTTC</span></span><span class="cd-genex cd-genex--intron"><span class="cd-genex-bar"></span><span class="cd-genex-text">GTACGCCATTAG</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">AGCCGT</span></span></div>
|
| 272 |
<div class="cd-track-labels">
|
| 273 |
+
<span class="cd-track-labels__title">Legend</span>
|
| 274 |
<span><span class="sw" style="background: var(--promoter)"></span>promoter</span>
|
| 275 |
<span><span class="sw" style="background: var(--green)"></span>exon</span>
|
| 276 |
<span><span class="sw" style="background: transparent; border-top: 1px solid var(--intron); height: 1px; margin-top: 4px;"></span>intron</span>
|
|
|
|
| 310 |
</svg>
|
| 311 |
<div class="cd-gene-strip cd-mrna-strip"><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">AUGGCCGAACUG</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">CCCGGGUGGUUC</span></span><span class="cd-genex cd-genex--exon"><span class="cd-genex-bar"></span><span class="cd-genex-text">AGCCGU</span></span></div>
|
| 312 |
</div>
|
| 313 |
+
<div class="cd-track-labels">
|
| 314 |
+
<span class="cd-track-labels__title">Legend</span>
|
| 315 |
+
<span><span class="sw" style="background: var(--promoter)"></span>promoter</span>
|
| 316 |
+
<span><span class="sw" style="background: var(--green)"></span>exon</span>
|
| 317 |
+
<span><span class="sw" style="background: transparent; border-top: 1px solid var(--intron); height: 1px; margin-top: 4px;"></span>intron</span>
|
| 318 |
+
</div>
|
| 319 |
</div>
|
| 320 |
</div>
|
| 321 |
</div>
|
|
|
|
| 354 |
<div class="cd-protein-3d" id="cd-protein-3d">
|
| 355 |
<div class="cd-protein-3d-loading">loading hemoglobin…</div>
|
| 356 |
</div>
|
| 357 |
+
<div class="cd-protein-caption">
|
| 358 |
+
<div class="cd-protein-caption__title">Human hemoglobin</div>
|
| 359 |
+
<div class="cd-protein-caption__desc">the molecule that carries oxygen in your blood</div>
|
| 360 |
+
<div class="cd-protein-caption__meta">4 chains · PDB <a href="https://www.rcsb.org/structure/1A3N" target="_blank" rel="noopener">1A3N</a></div>
|
| 361 |
+
</div>
|
| 362 |
</div>
|
| 363 |
</div>
|
| 364 |
</div>
|
|
|
|
| 381 |
The interesting question is what else falls out of that. We didn't tell Carbon what an
|
| 382 |
exon is. We didn't tell it which mutations are pathogenic. We didn't tell it how genes
|
| 383 |
differ between species. The sections below are ways to read what it picked up
|
| 384 |
+
anyway: autocomplete a gene <a class="lede-chip" href="#completion">§1</a>, see
|
| 385 |
+
structure emerge in its confidence <a class="lede-chip" href="#track">§2</a>, score
|
| 386 |
+
a disease variant against a healthy one <a class="lede-chip" href="#vep">§3</a>,
|
| 387 |
+
recognise a gene's species of origin <a class="lede-chip" href="#species">§4</a>,
|
| 388 |
and then push further into folded protein structure
|
| 389 |
+
<a class="lede-chip" href="#folding">§5</a>, the embedding manifold
|
| 390 |
+
<a class="lede-chip" href="#umap">§6</a>, and the species tree
|
| 391 |
+
<a class="lede-chip" href="#speciesTree">§7</a>. Each demo runs against the public
|
| 392 |
<code>HuggingFaceBio/Carbon-3B</code> checkpoint behind a live inference endpoint.
|
| 393 |
</p>
|
| 394 |
</div>
|
|
|
|
| 408 |
intron just before the 2nd exon plus the first 35 bp of that exon, and ask it to
|
| 409 |
<em>finish the exon</em>. The model streams the remaining bases one 6-base token at a
|
| 410 |
time. Exons are the protein-coding parts of a gene and are under strong evolutionary
|
| 411 |
+
constraint, so they should be the most predictable stretches of DNA. We overlay the
|
| 412 |
+
<em>real</em> exon/intron annotations on top of the output so you can compare what
|
| 413 |
+
Carbon produces to what's actually there.
|
|
|
|
|
|
|
| 414 |
</p>
|
| 415 |
</div>
|
| 416 |
|
|
|
|
| 430 |
</div>
|
| 431 |
|
| 432 |
<div class="gene-info" id="d1-info">loading genes…</div>
|
| 433 |
+
<svg class="gene-track draggable" id="d1-track" viewBox="0 0 1000 52" preserveAspectRatio="none"></svg>
|
| 434 |
<div class="track-axis-label" style="justify-content:flex-end;gap:20px;align-items:center">
|
| 435 |
<span class="legend-tip"
|
| 436 |
data-tip="Exon: coding segment of the gene. Stays in the mature mRNA and gets translated into protein."
|
|
|
|
| 490 |
<div class="takeaway">
|
| 491 |
<p>
|
| 492 |
<strong>Try it</strong>
|
| 493 |
+
Drag the dark ▼ ▲ markers to slide the prompt window and the green ▼ to set
|
| 494 |
+
where generation stops, then hit ▶ generate. Land the green-shaded region
|
| 495 |
+
inside an exon (dark green block) and note the count of green-underlined matches;
|
| 496 |
+
repeat with a similar-length window over an intron and compare.
|
| 497 |
</p>
|
| 498 |
<p>
|
| 499 |
<strong>What to look for</strong>
|
|
|
|
| 591 |
</div>
|
| 592 |
|
| 593 |
<div class="gene-info" id="d3-info">loading genes…</div>
|
| 594 |
+
<svg class="gene-track" id="d3-track" viewBox="0 0 1000 40" preserveAspectRatio="none"></svg>
|
| 595 |
<svg id="d3-chart" style="display:block;width:100%;height:140px;background:#fff;border:1px solid #eee;margin-top:6px" preserveAspectRatio="none" viewBox="0 0 1000 140"></svg>
|
| 596 |
+
<div class="track-axis-label" style="padding-top:8px">
|
| 597 |
<span><span class="legend-swatch" style="background:#317f3f"></span>exon (shaded)</span>
|
| 598 |
<span style="color:#aaa">y-axis: log P per 6-bp token (higher = more confident)</span>
|
| 599 |
<span id="d3-bp-label" style="color:#888">0 bp</span>
|
|
|
|
| 818 |
<div class="demo-toolbar">
|
| 819 |
<span>gene</span>
|
| 820 |
<span id="d4-pills" class="pills"></span>
|
|
|
|
| 821 |
<span>prefix</span>
|
| 822 |
<span id="d4-prefix-pills" class="pills">
|
| 823 |
<button class="pill" data-prefix="200">200</button>
|
|
|
|
| 829 |
<button class="pill active" data-gen="60">60</button>
|
| 830 |
<button class="pill" data-gen="200">200</button>
|
| 831 |
</span>
|
| 832 |
+
<span class="spacer"></span>
|
| 833 |
<button id="d4-go" class="action primary">▶ run all</button>
|
| 834 |
<span class="status is-hidden" id="d4-status"><span class="dot"></span><span></span></span>
|
| 835 |
</div>
|
|
|
|
| 1189 |
</p>
|
| 1190 |
<p class="tab-lede__note">
|
| 1191 |
The sections below walk through each of those choices: how the tokenizer changes
|
| 1192 |
+
what a "token" means in DNA <a class="lede-chip" href="#tokenizer">§1</a>, how
|
| 1193 |
+
FNS rescues training in the BF16 regime <a class="lede-chip" href="#loss">§2</a>,
|
| 1194 |
+
what's in the training corpus <a class="lede-chip" href="#data">§3</a>,
|
| 1195 |
+
what the architecture looks like <a class="lede-chip" href="#architecture">§4</a>,
|
| 1196 |
how 8k-token pretraining reaches 786 kbp at inference
|
| 1197 |
+
<a class="lede-chip" href="#longcontext">§5</a>, how Carbon stacks up against
|
| 1198 |
Evo2-7B and GENERator-v2 on the full training-free suite
|
| 1199 |
+
<a class="lede-chip" href="#results">§6</a>, and why the model runs so fast
|
| 1200 |
+
<a class="lede-chip" href="#efficiency">§7</a>.
|
| 1201 |
</p>
|
| 1202 |
</div>
|
| 1203 |
</div>
|
|
|
|
| 1456 |
<div class="section-body">
|
| 1457 |
<div class="demo" id="demo12">
|
| 1458 |
<div id="d12-bars"></div>
|
| 1459 |
+
<div class="track-axis-label chart-legend">
|
| 1460 |
+
<span class="chart-legend__item"><span class="legend-swatch" style="background:#1A7A40"></span>Carbon 8B</span>
|
| 1461 |
+
<span class="chart-legend__item"><span class="legend-swatch" style="background:#6DBF7E"></span>Carbon 3B</span>
|
| 1462 |
+
<span class="chart-legend__item"><span class="legend-swatch" style="background:#5A5A56"></span>Evo2-7B</span>
|
| 1463 |
+
<span class="chart-legend__item"><span class="legend-swatch" style="background:#B5B0A6"></span>GENERator-v2 3B</span>
|
|
|
|
| 1464 |
</div>
|
| 1465 |
</div>
|
| 1466 |
|