Download app.js from TheFinAI/HERCULEAN: direct link, hf CLI and curl.
- Browser
- Download file 88.5 kB
-
https://huggingface.co/spaces/TheFinAI/HERCULEAN/resolve/main/app.js
- Command line
-
hf download hf://spaces/TheFinAI/HERCULEAN/app.js
-
curl -L -o app.js https://huggingface.co/spaces/TheFinAI/HERCULEAN/resolve/main/app.js
88.5 kB
| /* HERCULEAN leaderboard. | |
| Reads window.HERC from ./data.js. No libraries. */ | |
| (() => { | |
| const H = window.HERC, M = H.meta; | |
| const $ = (s, el = document) => el.querySelector(s); | |
| const $$ = (s, el = document) => [...el.querySelectorAll(s)]; | |
| const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; | |
| const WF = H.workflows; // Trading, Hedging, Market Insights, Auditing | |
| const WC = { 'Trading': '#d6304a', 'Hedging': '#8a3ce0', 'Market Insights': '#2f6fd6', 'Auditing': '#11949f' }; | |
| const SHORT = { 'Market Insights': 'Insights' }; | |
| const AC = { 'ReAct': '#2a78d6', 'Claude Code': '#eb6834', 'Codex': '#1baf7a', 'Hermes': '#eda100', 'OpenClaw': '#e87ba4' }; | |
| const INI = { 'ReAct': 'RA', 'Claude Code': 'CC', 'Codex': 'CX', 'Hermes': 'HM', 'OpenClaw': 'OC' }; | |
| const INI_FG = { 'ReAct': '#fff' }; // dark text on the lighter badges | |
| const ini = (a, cls = 'ini') => `<span class="${cls}" style="--c:${AC[a]};--fg:${INI_FG[a] || '#0f1528'}">${INI[a]}</span>`; | |
| /* ---------- agent systems: Overall = mean of the 4 workflow scores, Floor = min ---------- */ | |
| const rows = H.scores.map(s => { | |
| const v = WF.map(w => s[w]); | |
| return { ...s, key: `${s.agent}|${s.model}`, name: `${s.agent} · ${s.model}`, | |
| Overall: v.reduce((a, b) => a + b, 0) / v.length, Floor: Math.min(...v) }; | |
| }).sort((a, b) => b.Overall - a.Overall); | |
| rows.forEach((r, i) => r.rank = i + 1); | |
| const top = rows[0], N = rows.length, S = H.offlineStd; | |
| // paper rounding (half up), so 96.55 shows as 96.6 | |
| const f1 = v => (Math.round(v * 10 + 1e-9) / 10).toFixed(1); | |
| const fmtRho = v => (v < 0 ? '−' : '') + Math.abs(v).toFixed(2); | |
| const pct = v => v == null ? '—' : (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v).toFixed(2) + '%'; | |
| const num = (v, d = 2) => v == null ? '—' : (v < 0 ? '−' : '') + Math.abs(v).toFixed(d); | |
| const pm = (txt, sd, d = 2) => sd == null ? txt : `${txt}<span class="sd">±${Math.abs(sd).toFixed(d)}</span>`; | |
| const mean = a => { const x = a.filter(v => v != null); return x.length ? x.reduce((p, q) => p + q, 0) / x.length : null; }; | |
| /* best score per workflow (ties count for every tied system) */ | |
| const best = Object.fromEntries(WF.map(w => [w, Math.max(...rows.map(r => r[w]))])); | |
| const isBest = (r, w) => Math.abs(r[w] - best[w]) < 1e-6; | |
| const leaders = Object.fromEntries(WF.map(w => [w, rows.filter(r => isBest(r, w))])); | |
| const leadCount = {}; WF.forEach(w => leaders[w].forEach(r => leadCount[r.key] = (leadCount[r.key] || 0) + 1)); | |
| const maxLeads = Math.max(...Object.values(leadCount)); | |
| /* Spearman ρ between workflow rankings (average ranks for ties) */ | |
| function ranks(v) { | |
| const idx = v.map((x, i) => [x, i]).sort((a, b) => a[0] - b[0]), r = new Array(v.length); | |
| for (let i = 0; i < idx.length;) { let j = i; while (j + 1 < idx.length && idx[j + 1][0] === idx[i][0]) j++; | |
| for (let k = i; k <= j; k++) r[idx[k][1]] = (i + j) / 2 + 1; i = j + 1; } | |
| return r; | |
| } | |
| function spearman(a, b) { | |
| const ra = ranks(a), rb = ranks(b), n = a.length, m = (n + 1) / 2; | |
| let sab = 0, saa = 0, sbb = 0; | |
| for (let i = 0; i < n; i++) { sab += (ra[i] - m) * (rb[i] - m); saa += (ra[i] - m) ** 2; sbb += (rb[i] - m) ** 2; } | |
| return sab / Math.sqrt(saa * sbb); | |
| } | |
| const rhos = []; WF.forEach((a, i) => WF.slice(i + 1).forEach(b => rhos.push(spearman(rows.map(r => r[a]), rows.map(r => r[b]))))); | |
| const meanRho = rhos.reduce((p, q) => p + q, 0) / rhos.length; | |
| /* score magnitude: one purple hue, light → dark */ | |
| const RAMP = [[244, 240, 251], [201, 182, 234], [142, 106, 208], [75, 42, 143]]; | |
| function shade(v) { | |
| const t = Math.max(0, Math.min(1, v / 100)) * (RAMP.length - 1), i = Math.min(RAMP.length - 2, Math.floor(t)), f = t - i; | |
| const c = RAMP[i].map((x, k) => Math.round(x + (RAMP[i + 1][k] - x) * f)); | |
| return { bg: `rgb(${c})`, fg: v > 58 ? '#fff' : '#0f1528' }; | |
| } | |
| /* ---------- tooltip ---------- */ | |
| const tip = $('#tip'); | |
| document.addEventListener('pointerover', e => { const t = e.target.closest('[data-tip]'); if (!t) return; tip.innerHTML = t.dataset.tip; tip.classList.add('on'); }); | |
| document.addEventListener('pointermove', e => { | |
| if (!tip.classList.contains('on')) return; | |
| const x = Math.min(innerWidth - tip.offsetWidth - 10, e.clientX + 14), y = e.clientY - tip.offsetHeight - 12; | |
| tip.style.left = x + 'px'; tip.style.top = (y < 6 ? e.clientY + 18 : y) + 'px'; | |
| }); | |
| document.addEventListener('pointerout', e => { if (e.target.closest('[data-tip]')) tip.classList.remove('on'); }); | |
| /* ================= HERO ================= */ | |
| // small lion for the "New benchmark" pill: scalloped mane, face, eyes, nose | |
| const LION = (() => { | |
| // spiky mane (16-point star), ears, face, eyes, nose and muzzle | |
| const pts = Array.from({ length: 32 }, (_, i) => { const a = i / 32 * 2 * Math.PI - Math.PI / 2, r = i % 2 ? 8.6 : 11.4; | |
| return `${(12 + r * Math.cos(a)).toFixed(2)},${(12.6 + r * Math.sin(a)).toFixed(2)}`; }).join(' '); | |
| const face = '#6b1590'; | |
| return `<svg viewBox="0 0 24 24" aria-hidden="true"><polygon class="mane" points="${pts}" fill="#fff"/> | |
| <circle cx="8.1" cy="8.2" r="1.9" fill="${face}"/><circle cx="15.9" cy="8.2" r="1.9" fill="${face}"/> | |
| <circle cx="12" cy="12.9" r="6.4" fill="${face}"/> | |
| <circle cx="9.6" cy="11.6" r=".95" fill="#fff"/><circle cx="14.4" cy="11.6" r=".95" fill="#fff"/> | |
| <ellipse cx="12" cy="15.4" rx="2.6" ry="1.9" fill="#fff" opacity=".22"/> | |
| <path d="M10.6 13.6h2.8L12 15.2z" fill="#fff"/> | |
| <path d="M12 15.2v.8M12 16c-.6.7-1.5.8-2.1.3M12 16c.6.7 1.5.8 2.1.3" stroke="#fff" stroke-width=".75" fill="none" stroke-linecap="round"/></svg>`; | |
| })(); | |
| $('#lion').innerHTML = LION; | |
| // the same lion beside "Cite HERCULEAN" | |
| if ($('#citeLion')) $('#citeLion').innerHTML = LION; | |
| // hovering the lion or the citation panel speeds the mane up; playbackRate keeps the current angle (no jump) | |
| (() => { | |
| const lion = $('#citeLion'), bib = $('#resources .bib'); if (!lion) return; | |
| const rate = r => { const m = $('.mane', lion); if (m) m.getAnimations().forEach(x => (x.playbackRate = r)); }; | |
| [lion, bib].filter(Boolean).forEach(el => { el.addEventListener('pointerenter', () => rate(14 / 3)); el.addEventListener('pointerleave', () => rate(1)); }); | |
| })(); | |
| // rotating subject: one architecture at a time, in its own colour | |
| (() => { | |
| const r = $('#rotator'); | |
| r.innerHTML = H.agents.map((a, i) => `<span class="${i === 0 ? 'on' : ''}" style="--c:${AC[a]}">${a}</span>`).join(''); | |
| const sp = $$('span', r); let i = 0; | |
| const MS_ROT = 1600; // one name every 1.6 s | |
| r.style.setProperty('--rot', MS_ROT + 'ms'); | |
| // width follows the current name; the underline takes its colour | |
| const fit = () => { r.style.width = sp[i].offsetWidth + 'px'; r.style.setProperty('--uc', AC[H.agents[i]]); }; | |
| fit(); document.fonts && document.fonts.ready.then(fit); | |
| if (reduce) return; | |
| setInterval(() => { | |
| const prev = sp[i]; prev.classList.remove('on'); prev.classList.add('out'); | |
| i = (i + 1) % sp.length; sp[i].classList.remove('out'); sp[i].classList.add('on'); fit(); | |
| setTimeout(() => prev.classList.remove('out'), 600); | |
| }, MS_ROT); | |
| })(); | |
| $('#awardN').textContent = `${H.agents.length} architectures × ${H.models.length} backbones`; | |
| $('#stats').innerHTML = [[N, 'agent systems', `${H.agents.length} architectures × ${H.models.length} backbones`], [WF.length, 'workflows', WF.join(' · ')], [M.offlineDays, 'offline days', `trading days, ${M.offlineWindow[0]} → ${M.offlineWindow[1]}`], [M.liveDays, 'live days', `trading days, ${M.liveWindow[0]} → ${M.liveWindow[1]}; Trading, Hedging and Market Insights, 3 selected systems per workflow`], [M.runs, 'runs', `every agent system is run ${M.runs} times independently`, '×']] | |
| .map(([n, l, t, suf]) => `<div${t ? ` data-tip="<b>${l}</b><br>${t}"` : ''}><b data-count="${n}" data-dec="0"${suf ? ` data-suf="${suf}"` : ''}>0</b><span>${l}</span></div>`).join(''); | |
| // "Top 8 · Overall score" card: bars coloured by architecture, initials badge on top | |
| const MS = { 'Claude Sonnet 5': 'Sonnet 5', 'DeepSeek-V3.2': 'DS-V3.2', 'DeepSeek-V4-Pro': 'DS-V4-Pro', 'Qwen3.5-9B': 'Qwen-9B', 'Qwen3.5-4B': 'Qwen-4B' }; | |
| // jump into the Rankings section on a given view, optionally with a selection (set by the Rankings block below) | |
| let rkGo = null; | |
| const goRank = (view, sel) => { if (rkGo) rkGo(view, sel); document.getElementById('rankings').scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block: 'start' }); }; | |
| $('#award').addEventListener('click', e => { e.preventDefault(); goRank('flow'); }); | |
| const pod = $('#podium'), TOPN = 8; | |
| pod.innerHTML = rows.slice(0, TOPN).map((r, i) => `<div class="col${i === 0 ? ' first' : ''}${i < 3 ? ` podium-${i + 1}` : ''}" data-key="${r.key}" style="--h:${r.Overall / top.Overall * 100}%;--d:${i * 90}ms;--c:${AC[r.agent]};--fg:${INI_FG[r.agent] || '#0f1528'}" | |
| data-tip="<b>#${r.rank} ${r.name}</b><br>Overall ${f1(r.Overall)}<span> · floor ${f1(r.Floor)}</span><br>${WF.map(w => `${SHORT[w] || w} ${f1(r[w])}`).join('<span> · </span>')}"> | |
| <div class="bar"><span class="val">${f1(r.Overall)}</span><span class="rk-badge">${r.rank}</span></div><span class="nm">${r.agent}<small>${MS[r.model] || r.model}</small></span></div>`).join(''); | |
| // ticker: every agent system by Overall rank; the list is doubled so the loop is seamless | |
| const tickItem = r => `<span class="tk${r.rank === 1 ? ' first' : ''}" data-key="${r.key}" data-tip="<b>#${r.rank} ${r.name}</b><br>${WF.map(w => `${SHORT[w] || w} ${f1(r[w])}`).join('<span> · </span>')}"><span class="p">#${String(r.rank).padStart(2, '0')}</span>${ini(r.agent, 'ini sm')}<span class="n">${r.agent} <em>·</em> ${MS[r.model] || r.model}</span><span class="v">${f1(r.Overall)}</span></span>`; | |
| $('#tickerTrack').innerHTML = rows.map(tickItem).join('').repeat(2); | |
| // constant speed (~70 px/s, like a ticker tape) whatever the item widths | |
| const setTickerSpeed = () => { const t = $('#tickerTrack'); t.style.setProperty('--dur', (t.scrollWidth / 2 / 70).toFixed(1) + 's'); }; | |
| setTickerSpeed(); document.fonts && document.fonts.ready.then(setTickerSpeed); | |
| $('#ticker').addEventListener('click', e => { | |
| const t = e.target.closest('.tk'); if (!t) return; | |
| const i = rows.findIndex(r => r.key === t.dataset.key); | |
| if (openKey !== rows[i].key) toggle(i); | |
| document.getElementById('leaderboard').scrollIntoView({ behavior: reduce ? 'auto' : 'smooth' }); | |
| }); | |
| $('#heroLegend').innerHTML = H.agents.map(a => `<span><i style="background:${AC[a]}"></i>${a}</span>`).join(''); | |
| // architecture and backbone rankings: mean Overall over the systems that share the architecture (6 backbones) | |
| // or the backbone (5 architectures) — a page-only aggregate of the leaderboard | |
| { | |
| const agg = (keys, by) => keys.map(k => { const rs = rows.filter(r => r[by] === k), v = rs.map(r => r.Overall); | |
| return { k, m: v.reduce((p, q) => p + q, 0) / v.length, lo: Math.min(...v), hi: Math.max(...v), best: rs[0] }; }).sort((p, q) => q.m - p.m); | |
| const A = agg(H.agents, 'agent'), B = agg(H.models, 'model'), mx = Math.max(...A.map(x => x.m), ...B.map(x => x.m)); | |
| const row = (x, i, kind) => `<div class="gr" data-kind="${kind}" data-k="${x.k}" data-tip="<b>${kind === 'agent' ? x.k : (MS[x.k] || x.k)}</b> · mean Overall ${f1(x.m)}<br><span>range ${f1(x.lo)}–${f1(x.hi)} over ${kind === 'agent' ? H.models.length + ' backbones' : H.agents.length + ' architectures'}</span><br>best: ${x.best.name} (${f1(x.best.Overall)})"> | |
| <span class="gr-p">${i + 1}</span>${kind === 'agent' ? ini(x.k, 'ini sm') : ''}<span class="gr-n">${kind === 'agent' ? x.k : (MS[x.k] || x.k)}</span> | |
| <span class="gr-t"><i style="width:${x.m / mx * 100}%;${kind === 'agent' ? `background:${AC[x.k]}` : ''}"></i></span><b>${f1(x.m)}</b></div>`; | |
| $('#heroLeaders').innerHTML = `<div class="gr-col"><p class="hl-cap">Architectures <span>mean Overall</span></p>${A.map((x, i) => row(x, i, 'agent')).join('')}</div> | |
| <div class="gr-col"><p class="hl-cap">Backbones <span>mean Overall</span></p>${B.map((x, i) => row(x, i, 'model')).join('')}</div>`; | |
| } | |
| $('#vizCap').textContent = `Top ${TOPN} · Overall score`; | |
| // clicking a bar opens Rankings on the Floor → Overall view with that system highlighted; | |
| // clicking an architecture / backbone row opens the Field view with that group highlighted | |
| pod.addEventListener('click', e => { const c = e.target.closest('.col'); if (c) goRank('floor', { mode: 'system', key: c.dataset.key }); }); | |
| $('#heroLeaders').addEventListener('click', e => { const g = e.target.closest('.gr'); if (g) goRank('field', { mode: g.dataset.kind, key: g.dataset.k }); }); | |
| function countUp(el) { | |
| const to = +el.dataset.count, dec = el.dataset.dec != null ? +el.dataset.dec : 1, suf = el.dataset.suf || '', d = 1200, t0 = performance.now(); | |
| const show = v => (el.textContent = v.toFixed(dec) + suf); | |
| if (reduce) { show(to); return; } | |
| setTimeout(() => show(to), d + 300); // always land on the value | |
| // rAF time can precede t0 on the first frame; clamp so the count never starts below zero | |
| const step = t => { const p = Math.max(0, Math.min(1, (t - t0) / d)), e = 1 - Math.pow(1 - p, 3); | |
| show(to * e); if (p < 1) requestAnimationFrame(step); }; | |
| requestAnimationFrame(step); | |
| } | |
| /* ================= 1 · WORKFLOW CARDS ================= */ | |
| const ICONS = { | |
| env: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5.5" rx="7.5" ry="2.5"/><path d="M4.5 5.5v6c0 1.4 3.4 2.5 7.5 2.5s7.5-1.1 7.5-2.5v-6"/><path d="M4.5 11.5v6c0 1.4 3.4 2.5 7.5 2.5s7.5-1.1 7.5-2.5v-6"/></svg>', | |
| proto: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h12l-3-3M20 17H8l3 3"/></svg>', | |
| eval: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 20h18"/><path d="M5 16l4-5 4 3 6-8"/><circle cx="19" cy="6" r="1.5"/></svg>', | |
| }; | |
| // each workflow = Environment + Interaction Protocol + Trajectory Evaluation (short; the paper has the details) | |
| const RECIPE = { | |
| 'Trading': [`Daily prices, news and 10-K / 10-Q filings for ${H.tradeAssets.join(', ')}`, `One BUY, SELL or HOLD per day for ${M.offlineDays} trading days`, ['CR', 'SR', 'MDD', 'vs Buy & Hold']], | |
| 'Hedging': ['The same data across eight large-cap stocks', 'Pick a pair on day 1, then LONG_SHORT, SHORT_LONG, HOLD or CLOSE daily', ['CR', 'SR', 'MDD', 'dollar-neutral']], | |
| 'Market Insights': ['Weekly market context with a fixed peer mapping', 'A weekly eight-section report and rating, about 13 per asset', ['Quality 0–10', 'CR', 'SR', 'MDD']], | |
| 'Auditing': [`SEC XBRL filings with the US-GAAP taxonomy, ${M.auditInstances} instances`, 'Commit the reported and the recalculated value of one fact', ['ACC', 'SER', 'EER', 'CER']], | |
| }; | |
| // one line on what each workflow is for (directional vs market-neutral; forward- vs backward-looking) | |
| const PURPOSE = { | |
| 'Trading': 'Seek profit as the market evolves, one directional decision a day.', | |
| 'Hedging': 'Seek profit with market risk hedged out, through a market-neutral pair.', | |
| 'Market Insights': 'Look forward: turn evidence into a view on an asset’s future.', | |
| 'Auditing': 'Look back: verify what a filing reports against its own numbers.', | |
| }; | |
| const PARTS = [['Environment', 'env'], ['Interaction Protocol', 'proto'], ['Trajectory Evaluation', 'eval']]; | |
| $('#wfGrid').innerHTML = WF.map((w, i) => { | |
| const lead = leaders[w], [env, proto, ev] = RECIPE[w]; | |
| const who = lead.length === 1 ? `<span>${lead[0].agent} <em>·</em></span><span>${lead[0].model}</span>` | |
| : `<span data-tip="<b>${lead.length}-way tie</b><br>${lead.map(r => r.name).join('<br>')}"><span>${lead[0].agent} <em>·</em></span><span>${lead[0].model} <em>+${lead.length - 1} tied</em></span></span>`; | |
| const item = (j, body) => `<li><span class="ico">${ICONS[PARTS[j][1]]}</span><div><small>${PARTS[j][0]}</small>${body}</div></li>`; | |
| return `<article class="wf spot" data-reveal style="--i:${i};--c:${WC[w]}"><span class="wf-bar"></span> | |
| <div class="wf-top"><span class="lbl"><span class="wdot" style="--c:${WC[w]}"></span>Workflow</span><span class="num">0${i + 1}</span></div> | |
| <h3>${w}</h3> | |
| <p class="purpose">${PURPOSE[w]}</p> | |
| <ul class="recipe">${item(0, `<p>${env}</p>`)}${item(1, `<p>${proto}</p>`)}${item(2, `<div class="chips">${ev.map(m => `<span>${m}</span>`).join('')}</div>`)}</ul> | |
| <div class="wf-best"><div><small>Best agent system</small><div class="who">${who}</div></div><b>${f1(best[w])}</b></div> | |
| </article>`; | |
| }).join(''); | |
| /* ================= 2 · LEADERBOARD ================= */ | |
| const score = (r, v) => v === 'Overall' ? r.Overall : v === 'Workflow floor' ? r.Floor : r[v]; | |
| let view = 'Overall', openKey = null, showAll = false; | |
| const LIMIT = 10; // rows shown before "show all" | |
| const hidden = new Set(), hiddenM = new Set(); | |
| $('#lbKicker').textContent = `Leaderboard · offline regime · ${M.offlineWindow[0]} → ${M.offlineWindow[1]}`; | |
| $('#lbSub').textContent = `Click a column to re-rank all ${N} agent systems. Click a row for its details.`; | |
| $('#lbNote').innerHTML = `Each metric (mean of ${M.runs} runs) is rank-normalised across the ${N} agent systems to 0–100. <b>Overall</b> is the mean of the four workflow scores; the <b>workflow floor</b> is the lowest.`; | |
| // ranking is chosen from the column headers (Overall or one workflow); the bar always shows Overall | |
| $('#archChips').innerHTML = H.agents.map(a => `<button type="button" data-a="${a}" aria-pressed="true">${ini(a, 'ini sm')}${a}</button>`).join(''); | |
| $('#archChips').addEventListener('click', e => { | |
| const b = e.target.closest('button'); if (!b) return; | |
| const a = b.dataset.a; | |
| if (hidden.has(a)) hidden.delete(a); else if (hidden.size < H.agents.length - 1) hidden.add(a); | |
| $$('button', $('#archChips')).forEach(x => { x.classList.toggle('off', hidden.has(x.dataset.a)); x.setAttribute('aria-pressed', !hidden.has(x.dataset.a)); }); | |
| layout(); | |
| }); | |
| $('#modelChips').innerHTML = H.models.map(m => `<button type="button" data-m="${m}" aria-pressed="true">${MS[m] || m}</button>`).join(''); | |
| $('#modelChips').addEventListener('click', e => { | |
| const b = e.target.closest('button'); if (!b) return; | |
| const m = b.dataset.m; | |
| if (hiddenM.has(m)) hiddenM.delete(m); else if (hiddenM.size < H.models.length - 1) hiddenM.add(m); | |
| $$('button', $('#modelChips')).forEach(x => { x.classList.toggle('off', hiddenM.has(x.dataset.m)); x.setAttribute('aria-pressed', !hiddenM.has(x.dataset.m)); }); | |
| layout(); | |
| }); | |
| $('#colCells').innerHTML = WF.map(w => `<button type="button" class="sort-h" data-v="${w}" title="Rank by ${w}"><span class="wdot" style="--c:${WC[w]}"></span>${SHORT[w] || w}<i class="arr">↓</i></button>`).join(''); | |
| $('#board .lb-cols').addEventListener('click', e => { const b = e.target.closest('.sort-h'); if (!b || b.dataset.v === view) return; view = b.dataset.v; layout(); }); | |
| // one DOM row per system, created once and moved with transforms | |
| const box = $('#lbRows'); | |
| box.innerHTML = rows.map((r, i) => `<div class="lb-row" data-i="${i}"> | |
| <div class="in" tabindex="0" role="button" aria-expanded="false"> | |
| <span class="lb-pos"></span> | |
| <span class="lb-name">${ini(r.agent)}<b>${r.agent}</b><span>${r.model}</span></span> | |
| <span class="lb-score"><span class="lb-track"><span class="lb-bar" style="--c:${AC[r.agent]}"></span></span><span class="lb-val"></span></span> | |
| <span class="cells">${WF.map(w => { const s = shade(r[w]); | |
| return `<span class="cell${isBest(r, w) ? ' best' : ''}" data-w="${w}" style="background:${s.bg};color:${s.fg}" data-tip="<b>${r.name}</b><br>${w}: ${f1(r[w])}<span> / 100${isBest(r, w) ? ' · best in workflow' : ''}</span>">${f1(r[w])}</span>`; }).join('')}</span> | |
| <span class="chev">›</span> | |
| </div></div>`).join('') + '<div class="lb-detail" id="lbDetail"></div>'; | |
| const els = $$('.lb-row', box), detail = $('#lbDetail'); | |
| function fitNames() { | |
| const w = Math.max(...$$('.lb-name', box).map(n => [...n.children].reduce((s, c) => s + Math.max(c.getBoundingClientRect().width, c.scrollWidth), 0) + 22)); | |
| $('#board').style.setProperty('--namew', Math.ceil(w) + 'px'); | |
| } | |
| const ROW = 40; | |
| let revealed = false; | |
| function layout() { | |
| const vis = rows.map((r, i) => ({ r, i })).filter(x => !hidden.has(x.r.agent) && !hiddenM.has(x.r.model)) | |
| .sort((a, b) => score(b.r, view) - score(a.r, view) || b.r.Overall - a.r.Overall); | |
| if (openKey && (hidden.has(openKey.split('|')[0]) || hiddenM.has(openKey.split('|')[1]))) openKey = null; | |
| // open row beyond the first LIMIT rows (e.g. from the ticker): show everything | |
| if (openKey && !showAll && vis.findIndex(x => x.r.key === openKey) >= LIMIT) showAll = true; | |
| const shown = showAll ? vis : vis.slice(0, LIMIT); | |
| els.forEach(el => el.classList.add('gone')); | |
| let y = 0; | |
| shown.forEach((x, pos) => { | |
| const el = els[x.i], open = openKey === x.r.key; | |
| el.classList.remove('gone'); el.classList.toggle('open', open); el.classList.toggle('top', pos === 0); | |
| $('.in', el).setAttribute('aria-expanded', open); | |
| el.style.transform = `translateY(${y}px)`; | |
| $('.lb-pos', el).textContent = String(pos + 1).padStart(2, '0'); | |
| $('.lb-bar', el).style.width = (revealed ? x.r.Overall : 0) + '%'; | |
| $('.lb-val', el).textContent = f1(x.r.Overall); | |
| $('.cells', el).classList.toggle('dim', WF.includes(view)); | |
| $$('.cell', el).forEach(c => c.classList.toggle('on', c.dataset.w === view)); | |
| y += ROW; | |
| if (open) { detail.style.transform = `translateY(${y}px)`; y += detail.offsetHeight; } | |
| }); | |
| if (openKey && !shown.some(x => x.r.key === openKey)) openKey = null; | |
| detail.classList.toggle('show', !!openKey); | |
| const more = $('#lbMore'), rest = vis.length - LIMIT; | |
| more.hidden = rest <= 0; | |
| more.setAttribute('aria-expanded', showAll); | |
| more.innerHTML = showAll ? `Show top ${LIMIT} only <span class="chev2">↓</span>` : `Show all ${vis.length} agent systems <em style="font-style:normal;color:var(--muted)">(+${rest})</em> <span class="chev2">↓</span>`; | |
| box.style.height = y + 'px'; | |
| $$('#board .sort-h').forEach(b => b.classList.toggle('on', b.dataset.v === view)); | |
| } | |
| $('#lbMore').addEventListener('click', () => { | |
| showAll = !showAll; | |
| if (!showAll) openKey = null; // otherwise an open row below #15 would re-expand the list | |
| layout(); | |
| if (!showAll) document.getElementById('board').scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block: 'start' }); | |
| }); | |
| // expanded row: a one-line summary, then four small cards, one chart each | |
| const wfRank = (r, w) => 1 + rows.filter(x => x[w] > r[w] + 1e-9).length; | |
| const isLive = (w, k) => (M.liveConfigs[w] || []).includes(k); | |
| const pc0 = v => v == null ? '—' : (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v).toFixed(1) + '%'; | |
| const sdS = (v, d = 1) => v == null ? '' : `<span class="sd">±${Math.abs(v).toFixed(d)}</span>`; | |
| // shared CR axis across every system and Buy & Hold, so trading dots are comparable between rows | |
| const crAll = H.tradeAssets.flatMap(a => [...rows.map(r => H.offline.Trading[a].CR[r.key]), H.baseline[`Trading|${a}|CR`]]).filter(v => v != null); | |
| const crMin = Math.min(0, ...crAll), crMax = Math.max(0, ...crAll), xPos = v => (v - crMin) / (crMax - crMin) * 100; | |
| const ACTION = { B: 'BUY', S: 'SELL', H: 'HOLD', L: 'LONG_SHORT', R: 'SHORT_LONG', C: 'CLOSE', '-': 'no committed record (scored as HOLD)' }; | |
| function breakdown(r) { | |
| const k = r.key, O = H.offline, A = H.tradeAssets; | |
| const fw = WF.find(w => r[w] === r.Floor), liveWf = Object.keys(M.liveConfigs).filter(w => isLive(w, k)); | |
| const best = WF.reduce((p, w) => r[w] > r[p] ? w : p, WF[0]); | |
| const liveTip = M.liveRule.replace(/"/g, '"'); | |
| const tag = w => (w === best ? '<span class="d1-tag best" data-tip="this system’s strongest workflow">best</span>' : w === fw ? '<span class="d1-tag floor" data-tip="this system’s weakest workflow (workflow floor)">floor</span>' : '') | |
| + (liveWf.includes(w) ? `<span class="d1-tag live" data-tip="<b>Run live</b> ${M.liveWindow[0]} → ${M.liveWindow[1]}<br><span>${liveTip}</span>">live</span>` : ''); | |
| const rpl = w => (w === 'Trading' || w === 'Hedging') ? `<button type="button" class="d1-replay" data-replay="${k}"><svg class="play-ico" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="7.5" fill="currentColor"/><path d="M6.4 4.9v6.2l4.9-3.1z" fill="#fff"/></svg>Replay</button>` : ''; | |
| const card = (w, sub, body) => `<div class="d1-card" style="--c:${WC[w]}"><h4><span class="wdot" style="--c:${WC[w]}"></span>${w}${tag(w)}<span class="ws" data-tip="${w} workflow score, rank-normalised 0–100"><b>${f1(r[w])}</b><span class="rk" data-tip="rank in ${w} among ${N} agent systems">#${wfRank(r, w)}</span></span></h4><p class="sub">${sub}${rpl(w)}</p>${body}</div>`; | |
| // Trading: CR per asset (dot) against Buy & Hold (tick) | |
| const tr = A.map(a => { const v = O.Trading[a].CR[k], sd = S.Trading[a].CR[k], bh = H.baseline[`Trading|${a}|CR`]; | |
| const sr = O.Trading[a].SR[k], mdd = O.Trading[a].MDD[k]; | |
| return `<div class="d1-cr" data-tip="<b>${a}</b><br>Agent CR ${pct(v)} <span>±${(sd ?? 0).toFixed(2)}</span><br>SR ${num(sr)} <span>±${(S.Trading[a].SR[k] ?? 0).toFixed(2)}</span><br>MDD ${mdd == null ? '— <span>never held a position</span>' : mdd.toFixed(2) + '% <span>±' + (S.Trading[a].MDD[k] ?? 0).toFixed(2) + '</span>'}<br><span>Buy & Hold CR ${pct(bh)}</span>"><span class="d1-t">${a}</span> | |
| <span class="track"><i class="zero" style="left:${xPos(0)}%"></i><i class="bh" style="left:${xPos(bh)}%"></i>${v == null ? '' : `<i class="ag" style="left:${xPos(v)}%"></i>`}</span> | |
| <span class="val">${pct(v)}${sdS(sd)}</span></div>`; }).join(''); | |
| const beats = A.filter(t => O.Trading[t].CR[k] >= H.baseline[`Trading|${t}|CR`]).length; | |
| const trading = card('Trading', 'Cumulative return (CR) per asset · hover a row for SR and MDD', tr + `<p class="d1-beat">Beats Buy & Hold on <b>${beats} of ${A.length}</b> tickers</p><div class="d1-legend"><span><i class="l-ag"></i>agent CR</span><span><i class="l-bh"></i>Buy & Hold CR</span><span><i class="l-z"></i>0%</span></div>`); | |
| // Hedging: pair, CR / SR / MDD, 61-day action strip | |
| const hp = O.Hedging.PAIR, hs = S.Hedging.PAIR, pair = H.pairs[k], pr = H.pairRuns[k], acts = H.hedgingActions[k] || ''; | |
| const [pl, prt] = pair ? pair.split('/') : []; | |
| const hedging = card('Hedging', 'Pair chosen on day 1 (most frequent over runs)', | |
| (pair ? `<div class="d1-pair" data-tip="LONG_SHORT = long ${pl}, short ${prt}">${pl}<span class="s">/</span>${prt}${pr ? `<span class="runs">${pr.runs}/${pr.of} runs</span>` : ''}</div>` : '<div class="d1-pair na">no pair</div>') + | |
| `<div class="d1-kv"><span>CR</span><span>${pct(hp.CR[k])}${sdS(hs.CR[k])}</span></div><div class="d1-kv"><span>SR</span><span>${num(hp.SR[k])}${sdS(hs.SR[k], 2)}</span></div> | |
| <div class="d1-kv"><span>MDD</span><span>${hp.MDD[k] == null ? '<span data-tip="never held a position">—</span>' : hp.MDD[k].toFixed(2) + '%' + sdS(hs.MDD[k])}</span></div>` + | |
| (acts ? (() => { | |
| // same encoding as the paper's Figure 3b: position state per day (HOLD and days without a committed record keep | |
| // the previous position, as in the evaluator); CLOSE days are marked with a diamond | |
| let p = 'F'; | |
| const days = [...acts].map((c, i) => { const cl = c === 'C'; if (c === 'L' || c === 'R') p = c; else if (cl) p = 'F'; return { s: p, cl, c, i }; }); | |
| const pn = { L: `Long-Short (long ${pl}, short ${prt})`, R: `Short-Long (short ${pl}, long ${prt})`, F: 'Flat' }; | |
| return `<div class="d1-cap">Position each day · run ${pr ? pr.shownRun : 1} of ${M.runs}</div><div class="d1-strip">${days.map(d => `<i class="${d.s}${d.cl ? ' cl' : ''}" data-tip="<b>${H.dates[d.i]}</b><br>${pn[d.s]}<br><span>action: ${d.c === '-' ? 'no committed record (kept as HOLD)' : ACTION[d.c]}</span>"></i>`).join('')}</div>`; | |
| })() : '') + | |
| `<div class="d1-legend"><span><i class="sw L"></i>Long-Short</span><span><i class="sw R"></i>Short-Long</span><span><i class="sw F"></i>Flat</span><span><i class="dia"></i>Close</span></div>`); | |
| // Market Insights: report quality per asset, rating-strategy CR vs Buy & Hold | |
| const qs = A.map(a => O['Market Insights'][a].Score[k]), qsd = A.map(a => S['Market Insights'][a].Score[k]); | |
| const insights = card('Market Insights', `Report quality (0–10), rubric over ${M.insightsRubric.length} dimensions`, | |
| `<div class="d1-big">${num(mean(qs))}<small>mean / 10</small></div>` + | |
| `<div class="d1-q hd"><span></span><span></span><span>score</span><span data-tip="CR of the weekly ratings replayed as a strategy">CR</span><span data-tip="Buy & Hold CR">B&H</span></div>` + | |
| A.map((a, i) => { const cr = O['Market Insights'][a].CR[k], bh = H.baseline[`Market Insights|${a}|CR`]; | |
| return `<div class="d1-q" data-tip="<b>${a}</b><br>report quality ${num(qs[i])} ±${(qsd[i] ?? 0).toFixed(2)} / 10<br>CR of ratings ${pct(cr)} ±${(S['Market Insights'][a].CR[k] ?? 0).toFixed(2)}<br><span>Buy & Hold CR ${pct(bh)}</span>"><span class="d1-t">${a}</span><span class="q-bar"><i style="width:${(qs[i] || 0) * 10}%"></i></span><span class="val">${f1(qs[i])}</span><span class="val ${cr >= bh ? 'up' : 'dn'}">${pc0(cr)}</span><span class="val bh">${pc0(bh)}</span></div>`; }).join('')); | |
| // Auditing: ACC and the error split (ACC + SER + EER + CER = 100%) | |
| const MS4 = ['ACC', 'SER', 'EER', 'CER'], au = MS4.map(m => O.Auditing.AUDIT[m][k]), asd = MS4.map(m => S.Auditing.AUDIT[m][k]); | |
| const NAMES = { ACC: 'Accuracy', SER: 'Schema error', EER: 'Extraction error', CER: 'Calculation error' }; | |
| const auditing = card('Auditing', `${M.auditInstances} XBRL instances · ACC + SER + EER + CER = 100%`, | |
| `<div class="d1-big">${f1(au[0])}<small>% ACC${asd[0] == null ? '' : ' ±' + asd[0].toFixed(1)}</small></div>` + | |
| `<div class="d1-stack">${MS4.map((m, i) => au[i] ? `<i class="s-${m.toLowerCase()}" style="width:${au[i]}%" data-tip="${NAMES[m]} (${m}): ${f1(au[i])}%"></i>` : '').join('')}</div>` + | |
| MS4.map((m, i) => `<div class="d1-kv"><span><i class="s-${m.toLowerCase()} sq"></i>${NAMES[m]} <em class="ab">${m}</em></span><span>${f1(au[i])}%${sdS(asd[i])}</span></div>`).join('')); | |
| return `<div class="d1"><div class="d1-cards">${trading}${hedging}${insights}${auditing}</div><p class="d1-foot">Raw metrics: mean of ${M.runs} runs <span class="sd">±std</span></p></div>`; | |
| } | |
| function toggle(i) { | |
| const r = rows[i]; | |
| openKey = openKey === r.key ? null : r.key; | |
| if (openKey) detail.innerHTML = breakdown(r); | |
| layout(); | |
| } | |
| detail.addEventListener('click', e => { const b = e.target.closest('[data-replay]'); if (b) { e.stopPropagation(); replayTo(b.dataset.replay); } }); | |
| els.forEach((el, i) => { | |
| const t = $('.in', el); | |
| t.addEventListener('click', () => toggle(i)); | |
| t.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(i); } }); | |
| }); | |
| fitNames(); document.fonts && document.fonts.ready.then(fitNames); | |
| layout(); | |
| /* ================= 2b · RANKINGS ACROSS WORKFLOWS ================= */ | |
| // bump chart with linked highlighting: pick one system, a whole architecture, or a whole backbone | |
| (() => { | |
| const NS = 'http://www.w3.org/2000/svg'; | |
| const mk = (tag, attrs = {}, parent) => { const e = document.createElementNS(NS, tag); for (const k in attrs) e.setAttribute(k, attrs[k]); if (parent) parent.appendChild(e); return e; }; | |
| const n = rows.length; | |
| // y position = order within a workflow (ties broken by Overall); label = competition rank, "=" marks a tie | |
| const pos = {}, lab = {}, rnum = {}; | |
| WF.forEach(w => { | |
| [...rows].sort((x, y) => y[w] - x[w] || y.Overall - x.Overall).forEach((r, i) => { | |
| (pos[r.key] ||= {})[w] = i + 1; | |
| const c = 1 + rows.filter(x => x[w] > r[w] + 1e-9).length, tie = rows.filter(x => Math.abs(x[w] - r[w]) < 1e-9).length > 1; | |
| (lab[r.key] ||= {})[w] = (tie ? '=' : '') + c; (rnum[r.key] ||= {})[w] = c; | |
| }); | |
| }); | |
| const topIn = w => [...rows].sort((x, y) => y[w] - x[w] || y.Overall - x.Overall); | |
| const topBy = s => s === 'Overall' ? rows : s === 'Floor' ? [...rows].sort((x, y) => y.Floor - x.Floor || y.Overall - x.Overall) : topIn(s); | |
| const ordinal = k => k + (k % 100 >= 11 && k % 100 <= 13 ? 'th' : ({ 1: 'st', 2: 'nd', 3: 'rd' }[k % 10] || 'th')); | |
| const short = m => MS[m] || m; | |
| /* ---------- chart (built once; selection only toggles classes) ---------- */ | |
| // side margins just wide enough for the longest labels: rank ticks + architecture on the left, backbone + rank on the right | |
| const textW = (txt, size, weight = 500) => { const c = textW.c || (textW.c = document.createElement('canvas').getContext('2d')); c.font = `${weight} ${size}px Inter, system-ui, sans-serif`; return c.measureText(txt).width; }; | |
| const longAgent = Math.max(...H.agents.map(ag => textW(ag, 11.5))), longModel = Math.max(...rows.map(r => textW(`${short(r.model)} #${r.rank}`, 11.5, 600))); | |
| const W = 1116, T = 52, rowH = 18, Hh = T + n * rowH + 10; | |
| const L = Math.ceil(4 + textW('#30', 10.5) + 18 + longAgent + 20 + 6), R = Math.ceil(20 + longModel + 10); | |
| const cx = i => L + i * (W - L - R) / (WF.length - 1), cy = p => T + (p - 1) * rowH + rowH / 2; | |
| const svg = mk('svg', { viewBox: `0 0 ${W} ${Hh}`, role: 'img', 'aria-label': 'Rank of every agent system in each workflow' }); | |
| const defs = mk('defs', {}, svg), gr = mk('linearGradient', { id: 'wfGrad', gradientUnits: 'userSpaceOnUse', x1: cx(0), x2: cx(WF.length - 1), y1: 0, y2: 0 }, defs); | |
| WF.forEach((w, i) => mk('stop', { offset: i / (WF.length - 1), 'stop-color': WC[w] }, gr)); | |
| WF.forEach((w, i) => { | |
| mk('line', { x1: cx(i), x2: cx(i), y1: T - 10, y2: Hh - 4, class: 'lane' }, svg); | |
| // header = button: follow this workflow's leader | |
| const ld = leaders[w][0], hg = mk('g', { class: 'col-btn', 'data-k': ld.key, 'data-w': w, style: `--c:${WC[w]}`, tabindex: 0, role: 'button', | |
| 'data-tip': `<b>${w} · top 3</b><br>${[...rows].sort((x, y) => y[w] - x[w] || y.Overall - x.Overall).slice(0, 3).map((r, j) => `${j + 1}. ${r.name}`).join('<br>')}<br><span>click to highlight</span>` }, svg); | |
| const tw = w.length * 7.6 + 34; | |
| mk('rect', { x: cx(i) - tw / 2, y: 2, width: tw, height: 26, rx: 13, class: 'cb-bg' }, hg); | |
| mk('circle', { cx: cx(i) - tw / 2 + 13, cy: 15, r: 3.5, class: 'cb-dot' }, hg); | |
| mk('text', { x: cx(i) + 6, y: 19.5, 'text-anchor': 'middle', class: 'col-h' }, hg).textContent = w; | |
| }); | |
| [1, 5, 10, 15, 20, 25, 30].filter(p => p <= n).forEach(p => (mk('text', { x: 4, y: cy(p) + 3.5, class: 'tick' }, svg).textContent = `#${p}`)); | |
| const lines = {}, layer = mk('g', {}, svg), lbls = mk('g', { class: 'lbls' }, svg); | |
| rows.forEach(r => { | |
| const pts = WF.map((w, i) => [cx(i), cy(pos[r.key][w])]); | |
| let d = `M${pts[0][0]},${pts[0][1]}`; | |
| for (let i = 1; i < pts.length; i++) { const mx = (pts[i - 1][0] + pts[i][0]) / 2; d += ` C${mx},${pts[i - 1][1]} ${mx},${pts[i][1]} ${pts[i][0]},${pts[i][1]}`; } | |
| const g = mk('g', { class: 'ln', 'data-key': r.key }, layer); | |
| const tipAll = `<b>${r.name}</b><br>${WF.map(w => `${SHORT[w] || w} #${lab[r.key][w]}`).join('<span> · </span>')}<br><span>#${r.rank} Overall · click to follow</span>`; | |
| mk('path', { d, class: 'line' }, g); | |
| mk('path', { d, class: 'hit', 'data-tip': tipAll }, g); | |
| WF.forEach((w, i) => { | |
| mk('circle', { cx: pts[i][0], cy: pts[i][1], r: 2.6, style: `--wc:${WC[w]}`, 'data-tip': `<b>${r.name}</b><br>${w}: ${ordinal(rnum[r.key][w])}${lab[r.key][w][0] === '=' ? ' (tied)' : ''} of ${n}<span> · score ${f1(r[w])}</span>` }, g); | |
| const t = mk('text', { x: pts[i][0], y: pts[i][1] + 4, class: 'rkl' }, g); t.textContent = lab[r.key][w]; | |
| if (lab[r.key][w].length > 2) t.style.fontSize = '9px'; | |
| }); | |
| lines[r.key] = g; | |
| }); | |
| $('#rkChart').appendChild(svg); | |
| svg.classList.add('rk-flow'); | |
| /* ---------- second view: workflow floor → Overall ---------- */ | |
| // one row per system (Overall order): its four workflow scores as coloured dots, floor (open ring) joined to Overall (filled dot) | |
| const DW = 1116, DT = 58, DH = 17, DL = 250, DR = 70, dH = DT + n * DH + 34; | |
| const dx = v => DL + v / 100 * (DW - DL - DR); | |
| const db = mk('svg', { viewBox: `0 0 ${DW} ${dH}`, class: 'rk-db', role: 'img', 'aria-label': 'Workflow floor, workflow scores and Overall score of every agent system' }); | |
| // user-space gradient: a horizontal line has a zero-height bounding box, so a bbox gradient would not paint | |
| const dgr = mk('linearGradient', { id: 'brandGrad', gradientUnits: 'userSpaceOnUse', x1: dx(0), x2: dx(100), y1: 0, y2: 0 }, mk('defs', {}, db)); | |
| mk('stop', { offset: 0, 'stop-color': '#b40028' }, dgr); mk('stop', { offset: 1, 'stop-color': '#7800f0' }, dgr); | |
| // sort chips: Overall, workflow floor, then the four workflows (re-orders the rows; the highlight is unchanged) | |
| // same pills as the other two views' headers: centred, tinted, filled when chosen | |
| const DSORT = [['Overall', 'Overall', '#4a1a8c'], ['Floor', 'Workflow floor', '#4a1a8c'], ...WF.map(w => [w, w, WC[w]])]; | |
| const dpw = l => l.length * 7.6 + 34, dGap = 10; | |
| let lx = (DW - DSORT.reduce((s, [, l]) => s + dpw(l), 0) - dGap * (DSORT.length - 1)) / 2, dbSort = 'Overall'; | |
| DSORT.forEach(([k, lbl, c]) => { | |
| const tw = dpw(lbl), g = mk('g', { class: 'db-sort' + (k === dbSort ? ' on' : ''), 'data-s': k, style: `--c:${c}`, tabindex: 0, role: 'button', | |
| 'data-tip': `sort by ${lbl}${k === 'Floor' ? '<br><span>ring = the weakest workflow’s dot</span>' : k === 'Overall' ? '<br><span>black dot = Overall</span>' : ''}` }, db); | |
| mk('rect', { x: lx, y: 2, width: tw, height: 26, rx: 13, class: 'cb-bg' }, g); | |
| if (k === 'Floor') { mk('circle', { cx: lx + 13, cy: 15, r: 2.2, class: 'cb-dot' }, g); mk('circle', { cx: lx + 13, cy: 15, r: 4.6, class: 'cb-ring' }, g); } | |
| else mk('circle', { cx: lx + 13, cy: 15, r: 3.5, class: 'cb-dot' }, g); | |
| mk('text', { x: lx + tw / 2 + 6, y: 19.5, 'text-anchor': 'middle', class: 'col-h' }, g).textContent = lbl; | |
| lx += tw + dGap; | |
| }); | |
| [0, 25, 50, 75, 100].forEach(v => { | |
| mk('line', { x1: dx(v), x2: dx(v), y1: DT - 8, y2: DT + n * DH, class: 'lane' }, db); | |
| mk('text', { x: dx(v), y: DT + n * DH + 18, 'text-anchor': 'middle', class: 'tick' }, db).textContent = v; | |
| }); | |
| const dbRows = {}, dbLayer = mk('g', {}, db); | |
| rows.forEach((r, i) => { | |
| const y = DT + i * DH + DH / 2, vals = WF.map(w => r[w]), lo = Math.min(...vals), hi = Math.max(...vals); | |
| const g = mk('g', { class: 'db', 'data-key': r.key }, dbLayer); | |
| mk('rect', { x: 0, y: y - DH / 2, width: DW, height: DH, rx: 6, class: 'db-band', 'data-tip': `<b>#${r.rank} ${r.name}</b><br>Overall ${f1(r.Overall)} · floor ${f1(r.Floor)}<br>${WF.map(w => `${SHORT[w] || w} ${f1(r[w])}`).join('<span> · </span>')}` }, g); | |
| mk('text', { x: DL - 46, y: y + 4, class: 'db-name', 'text-anchor': 'end' }, g).textContent = `${r.agent} · ${short(r.model)}`; | |
| g.dataset.i = i; g.dataset.y = y; | |
| mk('text', { x: DL - 14, y: y + 4, class: 'db-rank', 'text-anchor': 'end' }, g).textContent = `#${r.rank}`; | |
| mk('line', { x1: dx(lo), x2: dx(hi), y1: y, y2: y, class: 'db-range' }, g); | |
| mk('line', { x1: dx(r.Floor), x2: dx(r.Overall), y1: y, y2: y, class: 'db-gap' }, g); | |
| WF.forEach(w => mk('circle', { cx: dx(r[w]), cy: y, r: 3.2, class: 'db-wf', style: `fill:${WC[w]}` }, g)); | |
| // floor = a transparent ring drawn around the weakest workflow's own dot (the dot shows through) | |
| const fw = WF.find(w => r[w] === r.Floor); | |
| mk('circle', { cx: dx(r.Floor), cy: y, r: 6, class: 'db-floor', 'data-tip': `<b>${r.name}</b><br>workflow floor: ${fw} ${f1(r.Floor)}` }, g); | |
| mk('circle', { cx: dx(r.Overall), cy: y, r: 5.5, class: 'db-ov', 'data-dx': dx(r.Floor) - dx(r.Overall) }, g); | |
| mk('text', { x: DW - DR + 18, y: y + 4, class: 'db-val' }, g).textContent = f1(r.Overall); | |
| dbRows[r.key] = g; | |
| }); | |
| // re-order rows (slide) by the chosen key; the rank and value columns follow that key | |
| const keyOf = (r, s) => s === 'Overall' ? r.Overall : s === 'Floor' ? r.Floor : r[s]; | |
| function sortDb(s) { | |
| dbSort = s; | |
| [...rows].sort((p, q) => keyOf(q, s) - keyOf(p, s) || q.Overall - p.Overall).forEach((r, p) => { | |
| const g = dbRows[r.key]; | |
| g.style.transform = `translateY(${(p - +g.dataset.i) * DH}px)`; | |
| $('.db-rank', g).textContent = `#${1 + rows.filter(x => keyOf(x, s) > keyOf(r, s) + 1e-9).length}`; | |
| $('.db-val', g).textContent = f1(keyOf(r, s)); | |
| }); | |
| $$('.db-sort', db).forEach(b => b.classList.toggle('on', b.dataset.s === s)); | |
| } | |
| db.addEventListener('click', e => { const b = e.target.closest('.db-sort'); if (b) { sortDb(b.dataset.s); choose({ mode: 'sort', key: b.dataset.s }, true); } }); | |
| db.addEventListener('keydown', e => { const b = e.target.closest('.db-sort'); if (b && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); sortDb(b.dataset.s); choose({ mode: 'sort', key: b.dataset.s }, true); } }); | |
| db.style.display = 'none'; | |
| $('#rkChart').appendChild(db); | |
| /* ---------- field view ("staff"): one line per architecture, one note per backbone, x = score ---------- */ | |
| // score = the chosen workflow (header pills), else Overall; follows the same selection state as the other views | |
| const BB = { 'Claude Sonnet 5': 'Son5', 'GPT-5.4': 'GPT', 'DeepSeek-V3.2': 'DS3.2', 'DeepSeek-V4-Pro': 'DS4P', 'Qwen3.5-9B': 'Q9B', 'Qwen3.5-4B': 'Q4B' }; | |
| // same canvas height as the other two views (the card never changes size); the spare height is split above and below | |
| const FW = W, FLANE = 92, FH = Hh, FT = 64 + Math.max(0, (FH - (64 + H.agents.length * FLANE + 70)) / 2), FL = 150, FR = 86, NR = 15.5, KR = 13.5; | |
| const fx = v => FL + v / 100 * (FW - FL - FR); | |
| const fv = mk('svg', { viewBox: `0 0 ${FW} ${FH}`, role: 'img', 'aria-label': 'Every agent system on one score axis, one line per architecture' }); | |
| // score pills: Overall + the four workflows (workflow pills reuse the column-header buttons) | |
| const pills = ['Overall', ...WF], pw = p => p.length * 7.6 + 34, gapP = 10, totP = pills.reduce((s, p) => s + pw(p), 0) + gapP * (pills.length - 1); | |
| let px0 = (FW - totP) / 2; | |
| pills.forEach(p => { | |
| const g = mk('g', p === 'Overall' ? { class: 'col-btn ov-btn', style: '--c:#4a1a8c', tabindex: 0, role: 'button', 'data-tip': 'Overall = mean of the four workflow scores' } | |
| : { class: 'col-btn', 'data-w': p, style: `--c:${WC[p]}`, tabindex: 0, role: 'button', 'data-tip': `<b>${p}</b><br><span>place every system by its ${p} score</span>` }, fv); | |
| mk('rect', { x: px0, y: 2, width: pw(p), height: 26, rx: 13, class: 'cb-bg' }, g); | |
| mk('circle', { cx: px0 + 13, cy: 15, r: 3.5, class: 'cb-dot' }, g); | |
| mk('text', { x: px0 + pw(p) / 2 + 6, y: 19.5, 'text-anchor': 'middle', class: 'col-h' }, g).textContent = p; | |
| px0 += pw(p) + gapP; | |
| }); | |
| const fGrid = mk('g', {}, fv), fLanes = mk('g', {}, fv), fTraces = mk('g', {}, fv), fNotes = mk('g', {}, fv); | |
| const axY = FT + H.agents.length * FLANE + 6; | |
| [0, 25, 50, 75, 100].forEach(v => { mk('line', { x1: fx(v), x2: fx(v), y1: FT - 8, y2: axY, class: 'f-grid' }, fGrid); mk('text', { x: fx(v), y: axY + 18, class: 'tick', 'text-anchor': 'middle' }, fGrid).textContent = v; }); | |
| mk('text', { x: FW - FR + 26, y: FT - 14, class: 'tick' }, fGrid).textContent = 'mean'; | |
| // key: note code -> backbone | |
| // key: a small note with the code, then the backbone's full name | |
| // key: a small dark note with the code, then the backbone's full name | |
| const kW = H.models.map(m => 2 * KR + 8 + textW(m, 11.5)), kGap = 26, kTot = kW.reduce((s, w) => s + w, 0) + kGap * (H.models.length - 1); | |
| let kx = (FW - kTot) / 2; | |
| H.models.forEach((m, i) => { | |
| const g = mk('g', { class: 'f-key', transform: `translate(${kx + KR},${axY + 42})` }, fGrid); | |
| mk('circle', { r: KR, class: 'f-kdot' }, g); | |
| mk('text', { y: 3.6, 'text-anchor': 'middle', class: 'f-kbb' }, g).textContent = BB[m]; | |
| mk('text', { x: KR + 8, y: 4, class: 'f-kname' }, g).textContent = m; | |
| kx += kW[i] + kGap; | |
| }); | |
| const fTrace = Object.fromEntries(H.models.map(m => [m, mk('path', { class: 'f-trace' }, fTraces)])); | |
| const fLane = Object.fromEntries(H.agents.map(ag => { | |
| const g = mk('g', { class: 'f-lane', 'data-a': ag }, fLanes); | |
| mk('line', { x1: FL, x2: FW - FR, y1: 0, y2: 0, class: 'f-line' }, g); | |
| mk('text', { x: FL - 22, y: 5, 'text-anchor': 'end', class: 'f-name', style: `fill:${AC[ag]}` }, g).textContent = ag; | |
| mk('line', { y1: -NR - 8, y2: NR + 8, class: 'f-mean', style: `stroke:${AC[ag]}` }, g); | |
| mk('text', { x: FW - FR + 26, y: 5, class: 'f-mv' }, g); | |
| return [ag, g]; | |
| })); | |
| const fNote = Object.fromEntries(rows.map(r => { | |
| const g = mk('g', { class: 'f-note', 'data-key': r.key }, fNotes); | |
| mk('circle', { r: NR, style: `fill:${AC[r.agent]}` }, g); | |
| mk('text', { y: 3.4, 'text-anchor': 'middle', class: 'f-bb' }, g).textContent = BB[r.model]; | |
| return [r.key, g]; | |
| })); | |
| let fPos = {}; | |
| function fieldLayout() { | |
| const sc = st.mode === 'workflow' ? st.key : 'Overall', val = r => sc === 'Overall' ? r.Overall : r[sc]; | |
| $$('.col-btn', fv).forEach(b => b.classList.toggle('on', b.classList.contains('ov-btn') ? sc === 'Overall' : b.dataset.w === sc)); | |
| const mean = ag => { const v = rows.filter(r => r.agent === ag).map(val); return v.reduce((p, q) => p + q, 0) / v.length; }; | |
| const order = H.agents, ly = ag => FT + (order.indexOf(ag) + .5) * FLANE; // fixed: same row order as the picker | |
| order.forEach(ag => { | |
| const g = fLane[ag], m = mean(ag); | |
| g.style.transform = `translateY(${ly(ag)}px)`; | |
| const mk2 = $('.f-mean', g); mk2.setAttribute('x1', fx(m)); mk2.setAttribute('x2', fx(m)); | |
| $('.f-mv', g).textContent = f1(m); | |
| g.dataset.tip = `<b>${ag}</b><br>mean ${sc}: ${f1(m)}<br><span>click to highlight its ${H.models.length} systems</span>`; | |
| }); | |
| // within a lane, notes that would overlap are nudged up / down (small beeswarm) | |
| fPos = {}; | |
| H.agents.forEach(ag => { | |
| const placed = []; | |
| rows.filter(r => r.agent === ag).sort((p, q) => val(p) - val(q)).forEach(r => { | |
| const x = fx(val(r)); let dy = 0; | |
| let best = -1; // first free slot; if none is free, the slot with the least overlap | |
| for (const c of [0, -1, 1, -2, 2]) { const md = Math.min(1e9, ...placed.map(p => Math.hypot(p.x - x, p.dy - c * NR * 1.1))); | |
| if (md >= NR * 1.9) { dy = c * NR * 1.1; break; } if (md > best) { best = md; dy = c * NR * 1.1; } } | |
| placed.push({ x, dy }); fPos[r.key] = { x, y: ly(ag) + dy }; | |
| }); | |
| }); | |
| rows.forEach(r => { | |
| const g = fNote[r.key], p = fPos[r.key]; | |
| g.style.transform = `translate(${p.x}px,${p.y}px)`; | |
| g.dataset.tip = `<b>${r.name}</b><br>${sc}: ${f1(val(r))}<span> · #${1 + rows.filter(x => val(x) > val(r) + 1e-9).length} of ${n}</span>`; | |
| }); | |
| } | |
| // classes follow the shared selection; the dashed trace links one backbone across the five lines | |
| function fieldApply(selKey, litKeys) { | |
| fieldLayout(); | |
| rows.forEach(r => { const g = fNote[r.key]; g.classList.toggle('sel', r.key === selKey); g.classList.toggle('lit', litKeys.has(r.key)); g.classList.toggle('pv', r.key === pv); }); | |
| fv.classList.toggle('all', st.mode === 'none'); | |
| // the dashed trace links one backbone across the five lines: the hovered note's, the chosen column's, | |
| // else the highlighted system's (by default the #1 system's) | |
| const one = pv ? rows.find(r => r.key === pv).model : st.mode === 'model' ? st.key : rows.find(r => r.key === selKey).model; | |
| H.models.forEach(m => { | |
| const pts = rows.filter(r => r.model === m).map(r => fPos[r.key]).sort((p, q) => p.y - q.y); | |
| fTrace[m].setAttribute('d', pts.map((p, i) => (i ? 'L' : 'M') + p.x.toFixed(1) + ',' + p.y.toFixed(1)).join('')); | |
| fTrace[m].classList.toggle('on', m === one); | |
| }); | |
| } | |
| fNotes.addEventListener('click', e => { const g = e.target.closest('.f-note'); if (g) choose({ mode: 'system', key: g.dataset.key }); }); | |
| fNotes.addEventListener('pointerover', e => { const g = e.target.closest('.f-note'); const k = g ? g.dataset.key : null; if (k && k !== pv) { pv = k; apply(); } }); | |
| fNotes.addEventListener('pointerleave', () => { if (pv) { pv = null; apply(); } }); | |
| fLanes.addEventListener('click', e => { const g = e.target.closest('.f-lane'); if (g) choose({ mode: 'agent', key: g.dataset.a }); }); | |
| fv.addEventListener('click', e => { if (e.target.closest('.ov-btn')) choose({ mode: 'none' }, true); }); | |
| fv.addEventListener('keydown', e => { if (e.target.closest('.ov-btn') && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); choose({ mode: 'none' }, true); } }); | |
| fv.style.display = 'none'; | |
| $('#rkChart').appendChild(fv); | |
| function growField() { | |
| rows.forEach((r, i) => { const g = fNote[r.key]; g.animate([{ opacity: 0, transform: `translate(${fx(0)}px,${fPos[r.key].y}px)` }, { opacity: 1, transform: g.style.transform }], { duration: 900, delay: i * 18, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'backwards' }); }); | |
| } | |
| let view = 'flow'; | |
| /* ---------- picker: architecture rows × backbone columns ---------- */ | |
| const pk = $('#picker'); | |
| pk.innerHTML = '<span></span>' + H.models.map(m => `<button class="pk-col" data-m="${m}" title="All ${H.agents.length} architectures on ${m}">${short(m)}</button>`).join('') + | |
| H.agents.map(ag => `<button class="pk-row" data-a="${ag}" title="All ${H.models.length} ${ag} systems">${ini(ag, 'ini sm')}${ag}</button>` + | |
| H.models.map(m => { const r = rows.find(x => x.agent === ag && x.model === m); | |
| return `<button class="pk-cell" data-k="${r.key}" style="--c:${AC[ag]}" data-tip="<b>${r.name}</b><br>#${r.rank} Overall · ${f1(r.Overall)}">${r.rank}</button>`; }).join('')).join('') + | |
| '<p class="pk-note">Block = one agent system (number = Overall rank) · row = architecture · column = backbone</p>'; | |
| /* ---------- state ---------- */ | |
| let st = { mode: 'none' }, pv = null; | |
| // one highlighted line (sel) plus secondary lines (lit) for every choice: | |
| // none -> #1 Overall highlighted, every other line secondary | |
| // workflow -> its #1 highlighted, its #2 and #3 secondary | |
| // system -> that system highlighted, the same architecture on other backbones secondary | |
| // agent / model -> the group's best Overall system highlighted, the rest of the row / column secondary | |
| // rows are already in Overall order, so the first match is the group's best system | |
| const selOf = s => s.mode === 'system' ? s.key : s.mode === 'workflow' ? topIn(s.key)[0].key : s.mode === 'sort' ? topBy(s.key)[0].key : s.mode === 'none' ? top.key | |
| : rows.find(r => r[s.mode] === s.key).key; | |
| const group = s => { | |
| const k = selOf(s); | |
| if (s.mode === 'agent' || s.mode === 'model') return rows.filter(r => r[s.mode] === s.key && r.key !== k); | |
| if (s.mode === 'workflow') return topIn(s.key).slice(1, 3); | |
| if (s.mode === 'sort') return topBy(s.key).slice(1, 3); | |
| if (s.mode === 'system') { const ag = rows.find(x => x.key === k).agent; return rows.filter(r => r.agent === ag && r.key !== k); } | |
| return []; | |
| }; | |
| // labels are static text, the same for every highlighted line: architecture on the left, backbone + Overall rank on the right | |
| function labels(shown) { | |
| lbls.innerHTML = ''; | |
| const spread = arr => { arr.sort((p, q) => p.y - q.y); for (let i = 1; i < arr.length; i++) if (arr[i].y - arr[i - 1].y < 13) arr[i].y = arr[i - 1].y + 13; return arr; }; | |
| spread(shown.map(r => ({ y: cy(pos[r.key][WF[0]]), r }))).forEach(({ y, r }) => | |
| (mk('text', { x: cx(0) - 20, y: y + 4, class: 'end end-l', style: `fill:${AC[r.agent]}` }, lbls).textContent = r.agent)); | |
| spread(shown.map(r => ({ y: cy(pos[r.key][WF[WF.length - 1]]), r }))).forEach(({ y, r }) => { | |
| const t = mk('text', { x: cx(WF.length - 1) + 20, y: y + 4, class: 'end' }, lbls); | |
| t.textContent = short(r.model) + ' '; | |
| mk('tspan', { class: 'rk-tag' }, t).textContent = `#${r.rank}`; | |
| }); | |
| } | |
| function apply() { | |
| const lit = group(st), litKeys = new Set(lit.map(r => r.key)), selKey = selOf(st); | |
| Object.entries(lines).forEach(([k, g]) => { g.classList.toggle('sel', k === selKey); g.classList.toggle('lit', litKeys.has(k)); g.classList.toggle('pv', k === pv); }); | |
| // draw order: faint lines, then the lit group, then the preview, then the selected line on top | |
| [...lit.map(r => r.key), pv, selKey].filter(Boolean).forEach(k => layer.appendChild(lines[k])); | |
| labels([...lit, ...(selKey ? [rows.find(x => x.key === selKey)] : [])]); | |
| svg.classList.toggle('all', st.mode === 'none'); | |
| $$('.pk-cell', pk).forEach(c => { c.classList.toggle('sel', c.dataset.k === selKey); c.classList.toggle('lit', litKeys.has(c.dataset.k)); c.classList.toggle('pv', c.dataset.k === pv); }); | |
| $$('.pk-row', pk).forEach(b => b.classList.toggle('on', st.mode === 'agent' && b.dataset.a === st.key)); | |
| $$('.pk-col', pk).forEach(b => b.classList.toggle('on', st.mode === 'model' && b.dataset.m === st.key)); | |
| $$('#rkChart .col-btn').forEach(b => b.classList.toggle('on', st.mode === 'workflow' && b.dataset.w === st.key)); | |
| Object.entries(dbRows).forEach(([k, g]) => { g.classList.toggle('sel', k === selKey); g.classList.toggle('lit', litKeys.has(k)); g.classList.toggle('pv', k === pv); }); | |
| db.classList.toggle('all', st.mode === 'none'); | |
| fieldApply(selKey, litKeys); | |
| } | |
| // dots appear as the growing line reaches their column (the ease-out curve gets there early) | |
| const DOT_AT = [0, 70, 160, 300]; | |
| const pop = (c, delay) => c.animate([{ opacity: 0, transform: 'scale(.3)' }, { opacity: 1, transform: 'scale(1)' }], { duration: 260, delay, easing: 'cubic-bezier(.34,1.56,.64,1)', fill: 'backwards' }); | |
| // the lit and selected lines grow in again after every click (same effect as on first view) | |
| function grow(force) { | |
| if (reduce) return; | |
| if (view === 'floor') return growDb(force); | |
| if (view === 'field') return force && growField(); | |
| const keys = force || st.mode === 'none' ? rows.map(r => r.key) : [...group(st).map(r => r.key), selOf(st)].filter(Boolean); | |
| keys.forEach((k, i) => { | |
| const p = $('path.line', lines[k]), l = p.getTotalLength(); | |
| p.getAnimations().forEach(x => x.cancel()); | |
| p.animate([{ strokeDasharray: l, strokeDashoffset: l }, { strokeDasharray: l, strokeDashoffset: 0 }], { duration: 900, delay: i * 45, easing: 'cubic-bezier(.22,1,.36,1)' }); | |
| $$('circle', lines[k]).forEach((c, j) => pop(c, i * 45 + DOT_AT[j])); | |
| }); | |
| } | |
| // clicking the current choice again clears it | |
| // floor → Overall view: the Overall dot slides out from the floor, the gap line stretches, workflow dots pop in | |
| function growDb(all) { | |
| const keys = all || st.mode === 'none' ? rows.map(r => r.key) : [...group(st).map(r => r.key), selOf(st)].filter(Boolean); | |
| keys.forEach((k, i) => { | |
| const g = dbRows[k], d = i * (all ? 22 : 50), ov = $('.db-ov', g); | |
| ov.animate([{ transform: `translateX(${ov.dataset.dx}px)` }, { transform: 'none' }], { duration: 800, delay: d, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'backwards' }); | |
| $('.db-gap', g).animate([{ transform: 'scaleX(0)' }, { transform: 'none' }], { duration: 800, delay: d, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'backwards' }); | |
| $('.db-range', g).animate([{ opacity: 0 }, { opacity: 1 }], { duration: 500, delay: d + 150, fill: 'backwards' }); | |
| $$('.db-wf', g).forEach((c, j) => pop(c, d + 120 + j * 60)); | |
| }); | |
| } | |
| const choose = (s, keep) => { st = (!keep && s.mode === st.mode && s.key === st.key) ? { mode: 'none' } : s; pv = null; apply(); grow(); }; | |
| pk.addEventListener('click', e => { | |
| const c = e.target.closest('.pk-cell'), rw = e.target.closest('.pk-row'), cl = e.target.closest('.pk-col'); | |
| if (c) choose({ mode: 'system', key: c.dataset.k }); else if (rw) choose({ mode: 'agent', key: rw.dataset.a }); else if (cl) choose({ mode: 'model', key: cl.dataset.m }); | |
| }); | |
| pk.addEventListener('pointerover', e => { const c = e.target.closest('.pk-cell'); const k = c ? c.dataset.k : null; if (k !== pv) { pv = k; apply(); } }); | |
| pk.addEventListener('pointerleave', () => { if (pv) { pv = null; apply(); } }); | |
| layer.addEventListener('pointerover', e => { const g = e.target.closest('.ln'); const k = g ? g.dataset.key : null; if (k && k !== pv) { pv = k; apply(); } }); | |
| layer.addEventListener('pointerleave', () => { if (pv) { pv = null; apply(); } }); | |
| layer.addEventListener('click', e => { const g = e.target.closest('.ln'); if (g) choose({ mode: 'system', key: g.dataset.key }); }); | |
| $('#rkChart').addEventListener('click', e => { const b = e.target.closest('.col-btn[data-w]'); if (b) choose({ mode: 'workflow', key: b.dataset.w }); }); | |
| dbLayer.addEventListener('click', e => { const g = e.target.closest('.db'); if (g) choose({ mode: 'system', key: g.dataset.key }); }); | |
| dbLayer.addEventListener('pointerover', e => { const g = e.target.closest('.db'); const k = g ? g.dataset.key : null; if (k && k !== pv) { pv = k; apply(); } }); | |
| dbLayer.addEventListener('pointerleave', () => { if (pv) { pv = null; apply(); } }); | |
| // view switch | |
| const vs = $('#rkView'), vPill = $('.seg-pill', vs); | |
| const placeV = () => { const b = $('button[aria-selected=true]', vs); vPill.style.width = b.offsetWidth + 'px'; vPill.style.transform = `translateX(${b.offsetLeft}px)`; }; | |
| vs.addEventListener('click', e => { | |
| const b = e.target.closest('button'); if (!b || b.dataset.v === view) return; | |
| view = b.dataset.v; $$('button', vs).forEach(x => x.setAttribute('aria-selected', x === b)); placeV(); | |
| svg.style.display = view === 'flow' ? '' : 'none'; db.style.display = view === 'floor' ? '' : 'none'; fv.style.display = view === 'field' ? '' : 'none'; | |
| grow(true); | |
| }); | |
| requestAnimationFrame(placeV); document.fonts && document.fonts.ready.then(placeV); addEventListener("resize", placeV); | |
| $('#rkChart').addEventListener('keydown', e => { const b = e.target.closest('.col-btn[data-w]'); if (b && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); choose({ mode: 'workflow', key: b.dataset.w }); } }); | |
| apply(); | |
| rkGo = (v, s) => { const b = $(`button[data-v="${v}"]`, vs); if (b && v !== view) b.click(); if (s) choose(s, true); }; | |
| /* ---------- draw-in when the chart scrolls into view ---------- */ | |
| if (reduce || !('IntersectionObserver' in window) || /[?&]all\b/.test(location.search)) return; | |
| const paths = rows.map(r => $('path.line', lines[r.key])); | |
| paths.forEach(p => { const l = p.getTotalLength(); p.style.strokeDasharray = l; p.style.strokeDashoffset = l; }); | |
| $$('circle', layer).forEach(c => (c.style.opacity = 0)); | |
| new IntersectionObserver((es, ob) => es.forEach(e => { | |
| if (!e.isIntersecting) return; ob.disconnect(); | |
| paths.forEach((p, i) => { | |
| const l = +p.style.strokeDasharray; | |
| p.animate([{ strokeDashoffset: l }, { strokeDashoffset: 0 }], { duration: 1100, delay: 150 + i * 28, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'forwards' }) | |
| .onfinish = () => { p.style.strokeDasharray = ''; p.style.strokeDashoffset = ''; }; | |
| }); | |
| rows.forEach((r, i) => $$('circle', lines[r.key]).forEach((c, j) => pop(c, 150 + i * 28 + DOT_AT[j]).onfinish = () => (c.style.opacity = ''))); | |
| }), { threshold: .2 }).observe($('#rkChart')); | |
| })(); | |
| /* ================= 2c · REPLAY: one system's Trading and Hedging trajectories, drawn day by day ================= */ | |
| const replayTo = (() => { | |
| const NS = 'http://www.w3.org/2000/svg', D = H.dates, ND = D.length; | |
| const mk = (tag, attrs = {}, parent) => { const e = document.createElementNS(NS, tag); for (const k in attrs) e.setAttribute(k, attrs[k]); if (parent) parent.appendChild(e); return e; }; | |
| const ACOL = { B: '#1593b5', S: '#e0475b', H: 'rgba(185,181,207,.6)' }, PCOL = { L: '#3567d8', R: '#e0475b', F: 'rgba(185,181,207,.45)' }; | |
| const ANAME = { B: 'BUY', S: 'SELL', H: 'HOLD', L: 'LONG_SHORT', R: 'SHORT_LONG', C: 'CLOSE', '-': 'no committed record (scored as HOLD)' }; | |
| const date = d => new Date(d + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); | |
| const W = 560, PH = 150, GAP = 8, SH = 14, L = 40, R = 34, T = 18, Hh = T + PH + GAP + SH + 20; | |
| const X = i => L + (i + .5) * (W - L - R) / ND, cw = (W - L - R) / ND; | |
| let sys = top, tk = H.tradeAssets[0], run = 1, raf = null; | |
| const RN = H.runs; | |
| // one chart: price line(s) with a purple wash underneath, a day-by-day strip, a dot riding the | |
| // first line and a pill that names each day's action / position; returns handles for the animation | |
| function chart(host, series, cells, labels, info) { | |
| host.innerHTML = ''; | |
| const svg = mk('svg', { viewBox: `0 0 ${W} ${Hh}` }, host), defs = mk('defs', {}, svg), id = host.id; | |
| const fg = mk('linearGradient', { id: id + 'Fill', x1: 0, x2: 0, y1: 0, y2: 1 }, defs); | |
| mk('stop', { offset: 0, 'stop-color': '#7800f0', 'stop-opacity': .14 }, fg); mk('stop', { offset: 1, 'stop-color': '#7800f0', 'stop-opacity': 0 }, fg); | |
| const all = series.flat(), lo = Math.min(...all), hi = Math.max(...all), pad = (hi - lo) * .08 || 1; | |
| const Y = v => T + (1 - (v - lo + pad) / (hi - lo + 2 * pad)) * PH; | |
| [0, .5, 1].forEach(f => { const v = lo - pad + f * (hi - lo + 2 * pad); | |
| mk('line', { x1: L, x2: W - R, y1: Y(v), y2: Y(v), class: 'rp-gridline' }, svg); | |
| mk('text', { x: L - 6, y: Y(v) + 3, 'text-anchor': 'end', class: 'rp-ax' }, svg).textContent = v.toFixed(0); }); | |
| [0, Math.floor(ND / 2), ND - 1].forEach(i => mk('text', { x: X(i), y: Hh - 4, 'text-anchor': 'middle', class: 'rp-ax' }, svg).textContent = date(D[i])); | |
| const cr = mk('rect', { x: 0, y: 0, width: L, height: Hh }, mk('clipPath', { id: id + 'Clip' }, defs)); | |
| const g = mk('g', { 'clip-path': `url(#${id}Clip)` }, svg); | |
| const pts = s => series[s].map((v, i) => `${X(i).toFixed(1)},${Y(v).toFixed(1)}`).join(' '); | |
| mk('polygon', { points: `${X(0)},${T + PH} ${pts(0)} ${X(ND - 1)},${T + PH}`, fill: `url(#${id}Fill)` }, g); | |
| series.forEach((p, s) => mk('polyline', { points: pts(s), class: 'rp-line' + (s ? ' b' : '') }, g)); | |
| // end labels, nudged apart when the two legs finish close together | |
| const ly = (labels || []).map((t, s) => ({ t, s, y: Y(series[s][ND - 1]) + 3 })).sort((p, q) => p.y - q.y); | |
| for (let k = 1; k < ly.length; k++) if (ly[k].y - ly[k - 1].y < 11) { const m = (ly[k].y + ly[k - 1].y) / 2; ly[k - 1].y = m - 5.5; ly[k].y = m + 5.5; } | |
| ly.forEach(({ t, s, y }) => mk('text', { x: W - R + 4, y, class: 'rp-lab', fill: s ? '#8a93a3' : '#0f1528' }, svg).textContent = t); | |
| const y0 = T + PH + GAP; | |
| const cellEls = cells.map((c, i) => mk('rect', { x: (L + i * cw + .4).toFixed(2), y: y0, width: (cw - .8).toFixed(2), height: SH, rx: 1.5, class: 'rp-cell pending', fill: c.fill, 'data-tip': c.tip }, svg)); | |
| const dias = cells.map((c, i) => c.diamond ? mk('rect', { x: L + i * cw + cw / 2 - 3, y: y0 + SH / 2 - 3, width: 6, height: 6, fill: '#8b3fd9', transform: `rotate(45 ${L + i * cw + cw / 2} ${y0 + SH / 2})`, class: 'rp-cell pending' }, svg) : null); | |
| const vl = mk('line', { x1: L, x2: L, y1: T - 4, y2: y0 + SH, stroke: '#0f1528', 'stroke-opacity': .18, 'stroke-dasharray': '2 3' }, svg); | |
| const dot = mk('circle', { r: 4.5, stroke: '#fff', 'stroke-width': 2, fill: '#0f1528', class: 'rp-dot' }, svg); | |
| const status = mk('text', { x: L, y: 10, class: 'rp-status' }, svg); | |
| const pill = mk('rect', { x: 0, y: 0, width: 60, height: 15, rx: 7.5, class: 'rp-pill' }, svg), ptx = mk('text', { x: 0, y: 10.8, 'text-anchor': 'middle', class: 'rp-pilltext' }, svg); | |
| // hover: a vertical line follows the pointer; the tooltip lists that day's values | |
| if (info) { | |
| const hv = mk('line', { y1: T - 4, y2: y0 + SH, class: 'rp-hover', opacity: 0 }, svg); | |
| const hd = series.map((p, s) => mk('circle', { r: 3, class: 'rp-hdot' + (s ? ' b' : ''), opacity: 0 }, svg)); | |
| const hit = mk('rect', { x: L, y: 0, width: W - L - R, height: y0 + SH, fill: 'transparent' }, svg); | |
| hit.addEventListener('pointermove', e => { | |
| const b = svg.getBoundingClientRect(), sx = (e.clientX - b.left) / b.width * W; | |
| const i = Math.max(0, Math.min(ND - 1, Math.floor((sx - L) / cw))), x = X(i); | |
| hv.setAttribute('x1', x); hv.setAttribute('x2', x); hv.setAttribute('opacity', 1); | |
| hd.forEach((d, s) => { d.setAttribute('cx', x); d.setAttribute('cy', Y(series[s][i])); d.setAttribute('opacity', 1); }); | |
| tip.innerHTML = info(i); tip.classList.add('on'); | |
| }); | |
| hit.addEventListener('pointerleave', () => { hv.setAttribute('opacity', 0); hd.forEach(d => d.setAttribute('opacity', 0)); tip.classList.remove('on'); }); | |
| } | |
| return { svg, cr, cellEls, dias, vl, dot, status, pill, ptx, cells, Y, line: series[0] }; | |
| } | |
| const hatch = 'url(#rpHatch)'; | |
| const trow = (k, v) => `<div class="tr"><span>${k}</span><b>${v}</b></div>`; | |
| function hatchDef(svg) { const p = mk('pattern', { id: 'rpHatch', width: 4, height: 4, patternUnits: 'userSpaceOnUse', patternTransform: 'rotate(45)' }, mk('defs', {}, svg)); mk('rect', { width: 4, height: 4, fill: '#fff' }, p); mk('line', { x1: 0, y1: 0, x2: 0, y2: 4, stroke: '#d8d6e0', 'stroke-width': 1.6 }, p); } | |
| function render() { | |
| const k = sys.key, O = H.offline, Sd = H.offlineStd; | |
| $$('#rpArch button').forEach(b => b.classList.toggle('sel', b.dataset.a === sys.agent)); | |
| $$('#rpModel button').forEach(b => b.classList.toggle('sel', b.dataset.m === sys.model)); | |
| $$('#rpRun button').forEach(b => b.classList.toggle('on', +b.dataset.r === run)); | |
| $('#rpSub').textContent = `Every committed decision of one agent system over the ${ND} offline trading days. Pick an architecture, a backbone and a run, or open a system from the leaderboard.`; | |
| $$('#rpTk button').forEach(b => b.classList.toggle('on', b.dataset.t === tk)); | |
| // Trading: price + BUY / SELL / HOLD strip | |
| const acts = [...((RN.trading[`${k}|${tk}`] || [])[run - 1] || '-'.repeat(ND))], px = H.prices[tk]; | |
| const tr = chart($('#rpTrade'), [px], acts.map((c, i) => ({ fill: c === '-' ? hatch : ACOL[c], pill: [c === '-' ? 'NO RECORD' : ANAME[c], c === '-' ? hatch : c === 'H' ? '#d9d7e3' : ACOL[c], c === '-' || c === 'H'], dot: c === 'B' || c === 'S' ? ACOL[c] : '#0f1528', tip: `<b>${tk} · ${date(D[i])}</b><br>close $${px[i].toFixed(2)}<br>${ANAME[c]}` })), null, i => `<div class="tt">${tk} · ${date(D[i])}</div>${trow('Close', '$' + px[i].toFixed(2))}${trow('Action', ANAME[acts[i]])}${trow('Run', run + ' of ' + M.runs)}`); | |
| hatchDef(tr.svg); | |
| const [cr, sr, mdd] = (RN.tradingMetrics[`${k}|${tk}`] || [])[run - 1] || [], bh = H.baseline[`Trading|${tk}|CR`]; | |
| $('#rpTradeFoot').innerHTML = `<div><b class="${cr >= bh ? 'up' : 'dn'}">${pct(cr)}</b><span>CR · run ${run}</span></div><em class="vs">vs.</em><div><b>${pct(bh)}</b><span>Buy & Hold</span></div><div class="dim"><b>${num(sr)}</b><span>SR</span></div><div class="dim"><b>${mdd == null ? '—' : mdd.toFixed(2) + '%'}</b><span>MDD</span></div>`; | |
| // Hedging: both legs indexed to 100 + position strip (paper Figure 3b encoding) | |
| const pair = (RN.pairs[k] || [])[run - 1], pr = H.pairRuns[k], hs = (RN.hedging[k] || [])[run - 1]; | |
| let hc = null; | |
| if (pair && hs) { | |
| const [pl, prt] = pair.split('/'), ix = p => p.map(v => v / p[0] * 100); | |
| let p = 'F'; | |
| const cells = [...hs].map((c, i) => { const cl = c === 'C'; if (c === 'L' || c === 'R') p = c; else if (cl) p = 'F'; | |
| const pn = cl ? 'CLOSE' : { L: 'LONG-SHORT', R: 'SHORT-LONG', F: 'FLAT' }[p], pc = cl ? '#8b3fd9' : p === 'F' ? '#d9d7e3' : PCOL[p]; | |
| return { fill: PCOL[p], diamond: cl, pill: [pn, pc, p === 'F' && !cl], dot: p === 'F' && !cl ? '#0f1528' : pc, tip: `<b>${date(D[i])}</b><br>${{ L: 'Long-Short', R: 'Short-Long', F: 'Flat' }[p]}<br><span>action: ${ANAME[c]}</span>` }; }); | |
| const li = ix(H.prices[pl]), ri = ix(H.prices[prt]), posN = { L: `long ${pl} / short ${prt}`, R: `short ${pl} / long ${prt}`, F: 'flat' }; | |
| let q = 'F'; const posArr = [...hs].map(c => { if (c === 'L' || c === 'R') q = c; else if (c === 'C') q = 'F'; return q; }); | |
| hc = chart($('#rpHedge'), [li, ri], cells, [pl, prt], i => `<div class="tt">${pl} / ${prt} · ${date(D[i])}</div>${trow(pl + ' (idx)', li[i].toFixed(1))}${trow(prt + ' (idx)', ri[i].toFixed(1))}${trow('Action', ANAME[hs[i]])}${trow('Position', posN[posArr[i]])}`); | |
| const same = (RN.pairs[k] || []).filter(x => x === pair).length; | |
| $('#rpPair').innerHTML = `${pl} / ${prt}<small>chosen in ${same}/${M.runs} runs · Long-Short = long ${pl}</small>`; | |
| } else { $('#rpHedge').innerHTML = '<p class="sub">No pair selected in this run.</p>'; $('#rpPair').textContent = ''; } | |
| const [hcr, hsr, hmdd] = (RN.hedgingMetrics[k] || [])[run - 1] || []; | |
| $('#rpHedgeFoot').innerHTML = `<div><b class="${hcr >= 0 ? 'up' : 'dn'}">${pct(hcr)}</b><span>CR · run ${run}</span></div><div class="dim"><b>${num(hsr)}</b><span>SR</span></div><div class="dim"><b>${hmdd == null ? '—' : hmdd.toFixed(2) + '%'}</b><span>MDD</span></div>`; | |
| play([tr, hc].filter(Boolean)); | |
| } | |
| // draw both charts left to right over ~4 s (eased); strips light up, the dot rides the line, the pill names the day | |
| let timer = null; | |
| function play(charts) { | |
| cancelAnimationFrame(raf); clearTimeout(timer); | |
| const feet = [$('#rpTradeFoot'), $('#rpHedgeFoot')], shown = charts.map(() => -1); | |
| function frame(p) { | |
| const f = p * (ND - 1), i = Math.min(ND - 1, Math.floor(f + 1e-6)), fr = f - i, x = X(f); | |
| charts.forEach((c, ci) => { | |
| const v = i < ND - 1 ? c.line[i] + (c.line[i + 1] - c.line[i]) * fr : c.line[ND - 1]; | |
| c.cr.setAttribute('width', x + 1); | |
| c.dot.setAttribute('cx', x); c.dot.setAttribute('cy', c.Y(v)); | |
| c.vl.setAttribute('x1', x); c.vl.setAttribute('x2', x); | |
| if (i === shown[ci]) return; | |
| for (let k = shown[ci] + 1; k <= i; k++) { c.cellEls[k].classList.remove('pending'); if (c.dias[k]) c.dias[k].classList.remove('pending'); } | |
| shown[ci] = i; | |
| const [txt, fill, dark] = c.cells[i].pill; | |
| c.status.textContent = `Day ${String(i + 1).padStart(2, '0')}/${ND} · ${date(D[i])}`; | |
| const bw = c.status.getComputedTextLength(), pw = txt.length * 6.4 + 16; | |
| c.pill.setAttribute('x', L + bw + 10); c.pill.setAttribute('width', pw); c.pill.setAttribute('fill', fill); | |
| c.ptx.setAttribute('x', L + bw + 10 + pw / 2); c.ptx.textContent = txt; c.ptx.classList.toggle('dark', dark); | |
| c.dot.setAttribute('fill', c.cells[i].dot); | |
| }); | |
| } | |
| const done = () => { frame(1); charts.forEach(c => c.vl.setAttribute('stroke-opacity', 0)); feet.forEach(f => f.classList.remove('pending')); }; | |
| if (reduce || document.hidden) { done(); return; } | |
| feet.forEach(f => f.classList.add('pending')); | |
| const t0 = performance.now(), dur = 4000; | |
| const step = t => { const p = Math.max(0, Math.min(1, (t - t0) / dur)); frame(1 - Math.pow(1 - p, 1.6)); if (p < 1) raf = requestAnimationFrame(step); else done(); }; | |
| frame(0); raf = requestAnimationFrame(step); | |
| timer = setTimeout(done, dur + 600); // fallback if rAF is throttled | |
| } | |
| // controls | |
| const pickSys = (ag, m) => { sys = rows.find(r => r.agent === ag && r.model === m); render(); }; | |
| $('#rpArch').innerHTML = H.agents.map(ag => `<button type="button" data-a="${ag}">${ini(ag, 'ini sm')}${ag}</button>`).join(''); | |
| $('#rpModel').innerHTML = H.models.map(m => `<button type="button" data-m="${m}">${MS[m] || m}</button>`).join(''); | |
| $('#rpArch').addEventListener('click', e => { const b = e.target.closest('button'); if (b) pickSys(b.dataset.a, sys.model); }); | |
| $('#rpModel').addEventListener('click', e => { const b = e.target.closest('button'); if (b) pickSys(sys.agent, b.dataset.m); }); | |
| $('#rpRun').innerHTML = Array.from({ length: M.runs }, (_, i) => `<button type="button" data-r="${i + 1}">Run ${i + 1}</button>`).join(''); | |
| $('#rpRun').addEventListener('click', e => { const b = e.target.closest('button'); if (b) { run = +b.dataset.r; render(); } }); | |
| $('#rpTk').innerHTML = H.tradeAssets.map(t => `<button type="button" data-t="${t}">${t}</button>`).join(''); | |
| $('#rpTk').addEventListener('click', e => { const b = e.target.closest('button'); if (b) { tk = b.dataset.t; render(); } }); | |
| $('#rpAgain').addEventListener('click', render); | |
| // first draw when the section scrolls into view | |
| let drawn = false; | |
| const go = () => { if (!drawn) { drawn = true; render(); } }; | |
| if (reduce || !('IntersectionObserver' in window) || /[?&]all\b/.test(location.search)) go(); | |
| else new IntersectionObserver((es, ob) => es.forEach(e => { if (e.isIntersecting) { ob.disconnect(); go(); } }), { threshold: .3 }).observe($('#replay')); | |
| // entry point used by the leaderboard rows | |
| return key => { sys = rows.find(r => r.key === key); drawn = true; document.getElementById('replay').scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block: 'start' }); setTimeout(render, reduce ? 0 : 450); }; | |
| })(); | |
| /* ================= 3 · PARTNERS ================= */ | |
| // collaborator institutions from the arXiv paper's affiliations (names only until logos are collected) | |
| const INSTITUTIONS = ['Columbia University', 'Stevens Institute of Technology', 'New York University', 'Georgia Institute of Technology', | |
| 'Université de Montréal', 'University of Minnesota', 'University of Massachusetts Boston', 'AIST', 'University of Liverpool', | |
| 'Vrije Universiteit Amsterdam', 'National University of Singapore', 'Halmstad University', 'Cardiff University', 'Mila – Quebec AI Institute']; | |
| $('#pNames').innerHTML = INSTITUTIONS.map(n => `<span>${n}</span>`).join(' <i>·</i> '); // spaces = line-break points | |
| $('#copyBib').addEventListener('click', () => { | |
| const b = $('#copyBib'); | |
| (navigator.clipboard ? navigator.clipboard.writeText($('#bib').textContent) : Promise.reject()).then(() => b.textContent = 'Copied', () => b.textContent = 'Select & copy'); | |
| setTimeout(() => b.textContent = 'Copy BibTeX', 1800); | |
| }); | |
| /* ================= hero background: real price paths as thin drifting lines ================= */ | |
| // Each line is one stock's 61 offline closes. To loop seamlessly the path runs forward then mirrored | |
| // (period = 2 × width), and the group drifts left by one period every 160 s. | |
| function drawFlow() { | |
| const svg = $('#flow'), W = Math.max(800, svg.clientWidth), Hh = Math.max(500, svg.clientHeight); | |
| svg.setAttribute('viewBox', `0 0 ${W} ${Hh}`); | |
| const syms = ['ADBE', 'META', 'AMZN', 'GOOGL', 'TSLA', 'AAPL', 'NVDA', 'MSFT'].filter(s => H.prices[s]); | |
| // Catmull-Rom through the points, as cubic Bézier segments [start, c1, c2, end] (rounded like the path string) | |
| const r1 = v => Math.round(v * 10) / 10; | |
| const segsOf = pts => pts.slice(0, -1).map((p1, i) => { const p0 = pts[Math.max(0, i - 1)], p2 = pts[i + 1], p3 = pts[Math.min(pts.length - 1, i + 2)]; | |
| return [p1, [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6], [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6], p2].map(q => [r1(q[0]), r1(q[1])]); }); | |
| const f1p = q => `${q[0].toFixed(1)},${q[1].toFixed(1)}`; | |
| const smooth = segs => `M${f1p(segs[0][0])}` + segs.map(([, c1, c2, e]) => ` C${f1p(c1)} ${f1p(c2)} ${f1p(e)}`).join(''); | |
| // ridge: the curve's y at every integer x over one 2W period. Same stepping as SVG getPointAtLength every | |
| // 2 units of arc length (the heroes step along it the way they always did), measured on the segments in JS. | |
| const ridgeOf = segs => { const r = new Float32Array(Math.ceil(2 * W) + 1); | |
| let px = segs[0][0][0], py = segs[0][0][1], acc = 0, s = 2, last = 0, ly = py, done = false; | |
| const put = (x, y) => { if (x > 2 * W) { done = true; return; } for (let i = last; i <= Math.floor(x); i++) r[i] = y; last = Math.floor(x) + 1; ly = y; }; | |
| put(px, py); | |
| for (const [a, b, c, d] of segs) { if (done) break; | |
| for (let j = 1; j <= 64 && !done; j++) { const t = j / 64, u = 1 - t; | |
| const x = u * u * u * a[0] + 3 * u * u * t * b[0] + 3 * u * t * t * c[0] + t * t * t * d[0], y = u * u * u * a[1] + 3 * u * u * t * b[1] + 3 * u * t * t * c[1] + t * t * t * d[1]; | |
| const dl = Math.hypot(x - px, y - py); | |
| while (!done && acc + dl >= s) { const f = (s - acc) / dl; put(px + (x - px) * f, py + (y - py) * f); s += 2; } | |
| acc += dl; px = x; py = y; } } | |
| for (let i = last; i < r.length; i++) r[i] = ly; // the period's last pixels keep the last height | |
| return r; }; | |
| const ridges = []; | |
| const labels = []; | |
| let out = `<defs><linearGradient id="lg" gradientUnits="userSpaceOnUse" x1="0" x2="${W}" y1="0" y2="0" spreadMethod="reflect"><stop offset="0" stop-color="#b40028"/><stop offset="1" stop-color="#7800f0"/></linearGradient></defs><g>`; | |
| syms.forEach((s, li) => { | |
| const p = H.prices[s], n = p.length, lo = Math.min(...p), hi = Math.max(...p), depth = li / (syms.length - 1); | |
| const base = Hh * (.16 + depth * .74), amp = 60 + depth * 60; | |
| const y = v => base - (v - lo) / (hi - lo) * amp; | |
| const loopv = [...p, ...p.slice(0, -1).reverse()], m = loopv.length; // forward then mirrored, ends where it starts | |
| const pts = [0, 1, 2].flatMap(k => loopv.map((v, i) => [k * 2 * W + i / (m - 1) * 2 * W, y(v)])); // three periods, enough for the drift | |
| // ticker name, fixed at the right edge at the middle of this line's band (does not drift) | |
| labels.push(`<text x="${W - 60}" y="${(base - amp / 2 + 3).toFixed(1)}" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="9" fill="#7a1fb4" fill-opacity=".14">${s}</text>`); | |
| const bright = li % 3 === 1; | |
| const segs = segsOf(pts); ridges.push(segs); | |
| out += `<path d="${smooth(segs)}" fill="none" stroke="url(#lg)" stroke-width="${bright ? 1.3 : .9}" opacity="${bright ? .34 : .12 + depth * .1}"/>`; | |
| for (let k = 0; k < 3; k++) { | |
| if (bright) [.18, .55, .83].forEach(f => { const i = Math.round(f * (n - 1)), xi = k * 2 * W + i / (m - 1) * 2 * W; | |
| out += `<circle cx="${xi}" cy="${y(p[i])}" r="2.2" fill="#7800f0" opacity=".5"/><circle class="node" cx="${xi}" cy="${y(p[i])}" r="2.4" fill="none" stroke="#7800f0" stroke-width=".8" style="animation-delay:${(f * 3).toFixed(2)}s"/>`; }); | |
| } | |
| }); | |
| out += '</g>' + labels.join(''); | |
| svg.innerHTML = out; | |
| flowState = { W, Hh, ridge: ridgeOf(ridges[ridges.length - 1]), ridgeTop: ridgeOf(ridges[0]), g: svg.querySelector('g'), period: 2 * W }; | |
| } | |
| let flowState = null, flowT0 = null; | |
| // drift the lines left (one 2W period every 160 s) and stand the pixel hero / lion on the front ridge | |
| function flowTick(ts) { | |
| if (!flowState) return requestAnimationFrame(flowTick); | |
| if (flowT0 === null) flowT0 = ts; | |
| const { W, Hh, ridge, ridgeTop, g, period } = flowState, off = reduce ? 0 : ((ts - flowT0) / 160000 * period) % period; | |
| g.setAttribute('transform', `translate(${-off} 0)`); | |
| const k = $('#flow').getBoundingClientRect().width / W || 1; // svg is stretched to the hero box | |
| const ky = $('#flow').getBoundingClientRect().height / Hh; | |
| const yAt = (r, px) => r[Math.max(0, Math.min(r.length - 1, Math.round(px / k + off) % period))] * ky; | |
| const stand = (sel, r) => $$(sel).forEach(el => { | |
| const sw = el.parentNode.clientWidth, l = el.style.left, w = el.offsetWidth || 18; | |
| const x = l.endsWith('%') ? parseFloat(l) / 100 * sw : parseFloat(l) || 0; // the return walker moves in % | |
| el.style.top = (yAt(r, x + w / 2) - (el.offsetHeight || 24) + 2) + 'px'; | |
| }); | |
| stand('#herc-top .walker, #herc-top .lion, #herc-top .flash', ridgeTop); // the fight, on the top line (ADBE) | |
| stand('#herc-bot .walker', ridge); // the walk back, on the bottom line (MSFT) | |
| requestAnimationFrame(flowTick); | |
| } | |
| requestAnimationFrame(flowTick); | |
| drawFlow(); | |
| /* ================= header: scrolled state, progress bar, sliding nav pill ================= */ | |
| const header = $('#header'), prog = $('#progress'), nav = $('#navLinks'); | |
| function onScroll() { | |
| const y = scrollY, max = document.documentElement.scrollHeight - innerHeight; | |
| header.classList.toggle('scrolled', y > 10); | |
| prog.style.transform = `scaleX(${max > 0 ? y / max : 0})`; | |
| let cur = null; | |
| for (const a of $$('a', nav)) { const sec = document.getElementById(a.dataset.sec); if (sec && sec.getBoundingClientRect().top < innerHeight * .4) cur = a.dataset.sec; } | |
| $$('a', nav).forEach(a => a.classList.toggle('active', a.dataset.sec === cur)); | |
| } | |
| addEventListener('scroll', onScroll, { passive: true }); onScroll(); | |
| let rz; addEventListener('resize', () => { clearTimeout(rz); rz = setTimeout(() => { drawFlow(); layout(); }, 200); }); | |
| /* ================= micro-interactions (thefin.ai): spotlight cards, button glow, magnetic CTAs, tilt ================= */ | |
| let lastBtn = null; | |
| document.addEventListener('pointermove', e => { | |
| const b = e.target.closest && e.target.closest('.btn'); | |
| if (lastBtn && lastBtn !== b) lastBtn.style.transform = ''; | |
| lastBtn = b; | |
| if (b) { | |
| const r = b.getBoundingClientRect(), x = e.clientX - r.left, y = e.clientY - r.top; | |
| b.style.setProperty('--mx', x + 'px'); b.style.setProperty('--my', y + 'px'); | |
| if (!reduce && b.classList.contains('magnetic')) b.style.transform = `translate(${(x - r.width / 2) * .18}px,${(y - r.height / 2) * .3}px)`; | |
| } | |
| const c = e.target.closest && e.target.closest('.spot'); | |
| if (c) { const r = c.getBoundingClientRect(); c.style.setProperty('--x', e.clientX - r.left + 'px'); c.style.setProperty('--y', e.clientY - r.top + 'px'); } | |
| }, { passive: true }); | |
| if (!reduce && matchMedia('(hover:hover)').matches) $$('.tilt').forEach(card => { | |
| card.addEventListener('pointermove', e => { | |
| const r = card.getBoundingClientRect(), px = (e.clientX - r.left) / r.width, py = (e.clientY - r.top) / r.height; | |
| card.style.transition = 'transform .1s'; | |
| card.style.transform = `perspective(1000px) rotateY(${(px - .5) * 8}deg) rotateX(${(.5 - py) * 8}deg)`; | |
| card.style.setProperty('--gx', px * 100 + '%'); card.style.setProperty('--gy', py * 100 + '%'); | |
| }); | |
| card.addEventListener('pointerleave', () => { card.style.transition = ''; card.style.transform = ''; }); | |
| }); | |
| /* ================= reveal on scroll ================= */ | |
| const io = 'IntersectionObserver' in window ? new IntersectionObserver(es => es.forEach(e => { | |
| if (!e.isIntersecting) return; | |
| e.target.classList.add('in'); io.unobserve(e.target); | |
| if (e.target.id === 'board') { revealed = true; setTimeout(layout, 120); } | |
| }), { threshold: .08 }) : null; | |
| $$('[data-reveal]').forEach(el => io ? io.observe(el) : el.classList.add('in')); | |
| if (!io || reduce || /[?&]all\b/.test(location.search)) { revealed = true; pod.classList.add('in'); $$('[data-reveal]').forEach(el => el.classList.add('in')); layout(); } | |
| requestAnimationFrame(() => { document.body.classList.add('loaded'); setTimeout(() => { $$('[data-count]').forEach(countUp); pod.classList.add('in'); }, 500); }); | |
| })(); | |
| /* ---------- game card: mini K-line of the real window (TSLA), with one system's calls ---------- */ | |
| (() => { | |
| const H = window.HERC, svg = document.getElementById('gameK'); if (!svg || !H.ohlc) return; | |
| const tk = 'TSLA', O = H.ohlc, bars = O.bars[tk].slice(O.start), acts = (H.runs.trading[`Hermes|DeepSeek-V4-Pro|${tk}`] || [''])[0] || ''; | |
| const lo = Math.min(...bars.map(b => b[2])), hi = Math.max(...bars.map(b => b[1])), W = svg.clientWidth || 360, Hh = svg.clientHeight || 96, top = 10, ph = Hh - 20, cw = W / bars.length, w = cw * .6; | |
| const Y = v => top + (1 - (v - lo) / (hi - lo)) * ph; | |
| svg.setAttribute('viewBox', `0 0 ${W} ${Hh}`); | |
| svg.innerHTML = bars.map((b, i) => { | |
| const [o, h, l, c] = b, up = c >= o, col = up ? '#3fd68f' : '#ff5a72', x = (i + .5) * cw, d = `style="transition-delay:${(i * 18) | 0}ms"`; | |
| const mk = acts[i] === 'B' ? `<path d="M${x},${Y(l) + 3} l3,5 h-6z" fill="#3fd68f"/>` : acts[i] === 'S' ? `<path d="M${x},${Y(h) - 3} l3,-5 h-6z" fill="#ff5a72"/>` : ''; | |
| return `<g class="gk" ${d}><line x1="${x}" x2="${x}" y1="${Y(h)}" y2="${Y(l)}" stroke="${col}" stroke-width=".9"/><rect x="${x - w / 2}" y="${Y(Math.max(o, c))}" width="${w}" height="${Math.max(.8, Math.abs(Y(o) - Y(c)))}" fill="${up ? 'none' : col}" stroke="${col}" stroke-width=".8"/>${mk}</g>`; | |
| }).join(''); | |
| })(); | |
| /* ---------- deep links (play.html → the Leaderboard section): the target travels in the #hash, ?to=, window.name and | |
| localStorage, because some viewers drop or rewrite the URL; scroll once the JS-built sections exist, and again | |
| while the layout settles, unless the reader has started scrolling */ | |
| (() => { | |
| let want = location.hash.slice(1) || new URLSearchParams(location.search).get('to') || ''; | |
| const wn = /^herc-goto:(\w[\w-]*)$/.exec(window.name || ''); if (wn) { want = want || wn[1]; window.name = ''; } | |
| try { const g = JSON.parse(localStorage.getItem('herc-goto') || 'null'); localStorage.removeItem('herc-goto'); if (!want && g && Date.now() - g.t < 15000) want = g.id; } catch (_) {} | |
| if (!want || !document.getElementById(want)) return; | |
| let user = false; const stop = () => (user = true); | |
| ['wheel', 'touchstart', 'keydown', 'mousedown'].forEach(t => addEventListener(t, stop, { once: true, passive: true })); | |
| const go = () => { | |
| if (user) return; | |
| const el = document.getElementById(want), root = document.documentElement, sb = root.style.scrollBehavior; | |
| root.style.scrollBehavior = 'auto'; // html{scroll-behavior:smooth} would animate and can be cut short | |
| scrollTo(0, el.getBoundingClientRect().top + scrollY - (parseFloat(getComputedStyle(root).scrollPaddingTop) || 0)); | |
| root.style.scrollBehavior = sb; | |
| }; | |
| if ('scrollRestoration' in history) history.scrollRestoration = 'manual'; | |
| go(); requestAnimationFrame(go); | |
| [150, 400, 900, 1600, 2600].forEach(t => setTimeout(go, t)); | |
| addEventListener('load', go); | |
| addEventListener('pageshow', e => e.persisted && go()); | |
| (document.fonts && document.fonts.ready || Promise.resolve()).then(go); | |
| })(); | |
| /* ---------- coins popping out of the $ badge on the two "Trade" buttons: slow and faint at rest, fast and bright on hover ---------- */ | |
| (() => { | |
| if (matchMedia('(prefers-reduced-motion: reduce)').matches) return; | |
| const btns = [...document.querySelectorAll('.btn--trade')]; if (!btns.length) return; | |
| const layer = document.createElement('div'); layer.className = 'coin-layer'; document.body.appendChild(layer); | |
| const coin = () => { const c = document.createElement('i'); c.className = 'coin'; c.textContent = '$'; return c; }; | |
| btns.forEach(b => { | |
| const host = b.closest('.game-card') || b, dol = b.querySelector('.dol') || b; | |
| let hot = false, vis = true, t = 0, raf = 0; | |
| host.addEventListener('pointerenter', () => (hot = true)); host.addEventListener('pointerleave', () => (hot = false)); | |
| if ('IntersectionObserver' in window) new IntersectionObserver(es => es.forEach(e => (vis = e.isIntersecting))).observe(b); | |
| const spawn = () => { | |
| const r = dol.getBoundingClientRect(), x = r.left + r.width / 2 + scrollX, y = r.top + r.height / 2 + scrollY; | |
| const big = hot, c = coin(), dx = (Math.random() - .5) * (big ? 70 : 34), up = (big ? 46 : 26) + Math.random() * (big ? 30 : 14); | |
| const s = big ? .9 + Math.random() * .35 : .7 + Math.random() * .2, op = big ? .95 : .38, dur = big ? 900 + Math.random() * 300 : 1300 + Math.random() * 400; | |
| c.style.left = x + 'px'; c.style.top = y + 'px'; layer.appendChild(c); | |
| // flat coin: a small in-plane turn only | |
| const spin = (Math.random() < .5 ? -1 : 1) * (10 + Math.random() * 20); | |
| c.animate([ | |
| { transform: `translate(-50%,-50%) translate(0,0) scale(${s * .4}) rotate(0deg)`, opacity: 0 }, | |
| { transform: `translate(-50%,-50%) translate(${dx * .5}px,${-up}px) scale(${s}) rotate(${spin * .6}deg)`, opacity: op, offset: .45 }, | |
| { transform: `translate(-50%,-50%) translate(${dx}px,${-up * .55}px) scale(${s * .85}) rotate(${spin}deg)`, opacity: 0 } | |
| ], { duration: dur, easing: 'cubic-bezier(.25,.7,.35,1)' }).onfinish = () => c.remove(); | |
| }; | |
| const tick = now => { | |
| const gap = hot ? 190 : 900; | |
| if (vis && !document.hidden && now - t > gap) { t = now; spawn(); } | |
| raf = requestAnimationFrame(tick); | |
| }; | |
| raf = requestAnimationFrame(tick); | |
| }); | |
| })(); | |