sergiopaniego HF Staff
Publish the deck source under source/ so the next deck can start from it
3bc016b verified Download source/src/embeds/d3-posttraining-timeline.html from FineEnvs/multi-harness-rl-slides: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/spaces/FineEnvs/multi-harness-rl-slides/resolve/main/source/src/embeds/d3-posttraining-timeline.html
- Command line
-
hf download hf://spaces/FineEnvs/multi-harness-rl-slides/source/src/embeds/d3-posttraining-timeline.html
-
curl -L -o d3-posttraining-timeline.html https://huggingface.co/spaces/FineEnvs/multi-harness-rl-slides/resolve/main/source/src/embeds/d3-posttraining-timeline.html
10.4 kB
| <div class="posttraining-timeline" style="width:100%;margin:14px 0;"></div> | |
| <style> | |
| .posttraining-timeline { position: relative; } | |
| .posttraining-timeline svg { display:block; width:100%; height:auto; overflow:visible; } | |
| .posttraining-timeline .pt-bar { display:flex; align-items:center; gap:10px; margin-bottom:8px; } | |
| .posttraining-timeline .pt-btn { appearance:none; cursor:pointer; font:inherit; font-size:12px; line-height:1; padding:7px 12px; | |
| border-radius:999px; border:1px solid var(--border-color); background:transparent; color:var(--muted-color); } | |
| .posttraining-timeline .pt-btn:hover { color:var(--text-color); } | |
| .posttraining-timeline .pt-btn:focus-visible { outline:2px solid var(--text-color); outline-offset:2px; } | |
| .posttraining-timeline .pt-cap { min-height:40px; margin-top:6px; font-size:13px; line-height:1.45; color:var(--text-color); } | |
| .posttraining-timeline .pt-cap b { font-weight:600; } | |
| .posttraining-timeline .pt-cap span { color:var(--muted-color); } | |
| .posttraining-timeline .pt-cap .lv { display:inline-block; font-size:11px; padding:1px 7px; border-radius:999px; border:1px solid currentColor; margin-left:6px; vertical-align:1px; } | |
| </style> | |
| <script> | |
| (() => { | |
| const C = 'posttraining-timeline'; | |
| const boot = () => { | |
| const scriptEl = document.currentScript; | |
| let r = scriptEl ? scriptEl.previousElementSibling : null; | |
| while (r && !(r.classList && r.classList.contains(C))) r = r.previousElementSibling; | |
| if (!r) { const n = [...document.querySelectorAll('.'+C)].filter(e => e.dataset.mounted !== 'true'); r = n[n.length-1]; } | |
| if (!r || r.dataset.mounted === 'true') return; | |
| r.dataset.mounted = 'true'; | |
| const V = (k,f) => (getComputedStyle(r).getPropertyValue(k).trim() || f); | |
| const esc = (t) => String(t).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); | |
| const P = (i) => { | |
| if (window.ColorPalettes && window.ColorPalettes.categorical) { | |
| try { return window.ColorPalettes.categorical(4)[i%4]; } catch(e) {} | |
| } | |
| return ['rgb(78,165,183)','rgb(146,176,234)','rgb(206,192,250)','rgb(232,137,171)'][i%4]; | |
| }; | |
| const grab = async (f) => { | |
| for (const u of ['/data/'+f, 'assets/data/'+f, '../assets/data/'+f]) { | |
| try { const x = await fetch(u, {cache:'no-cache'}); if (x.ok) return await x.json(); } catch(e) {} | |
| } | |
| throw new Error('could not load '+f); | |
| }; | |
| const mIndex = (d) => { const [y, m] = d.split('-').map(Number); return y*12 + (m-1); }; | |
| const mLabel = (i) => new Date(Date.UTC(Math.floor(i/12), i%12, 1)).toLocaleString('en', { month:'short', timeZone:'UTC' }); | |
| grab('harness-disclosure.json').then(D => { | |
| const L = D.levels, items = D.items; | |
| const m0 = mIndex(items[0].date) - 1, m1 = mIndex(items[items.length-1].date) + 1; | |
| const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; | |
| r.innerHTML = `<div class="pt-bar"><button type="button" class="pt-btn" data-act="play"></button></div>` | |
| + `<div data-svg></div><div class="pt-cap" aria-live="polite"></div>`; | |
| const svgHost = r.querySelector('[data-svg]'), cap = r.querySelector('.pt-cap'), play = r.querySelector('[data-act="play"]'); | |
| // The playhead moves in "release steps": each release gets the same time on | |
| // screen, however many months separate it from the last one. | |
| const STEP = 1700, HOLD = 3500; | |
| const TOTAL = items.length*STEP + HOLD; | |
| let clock = reduce ? TOTAL - 1 : 0, last = 0, playing = !reduce, visible = false, pinned = -1; | |
| const draw = () => { | |
| const W = Math.max(320, r.clientWidth); | |
| const narrow = W < 560; | |
| const mu = V('--muted-color','rgba(128,128,128,.8)'), bd = V('--border-color','rgba(128,128,128,.3)'); | |
| const tx = V('--text-color','#16181d'), sf = V('--surface-bg','#fff'); | |
| const padL = narrow ? 8 : 112, padR = 10, top = 8, band = narrow ? 34 : 40, H = top + band*L.length + 30; | |
| const x = (m) => padL + (m - m0) / (m1 - m0) * (W - padL - padR); | |
| const y = (lv) => top + band*(L.length - 1 - lv) + band/2; | |
| const t = clock % TOTAL; | |
| const shown = Math.min(items.length, Math.floor(t / STEP) + 1); // releases revealed so far | |
| const cur = pinned >= 0 ? pinned : shown - 1; | |
| const k = Math.min(1, (t % STEP) / 420); // pop-in of the newest dot | |
| let s = `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${items.length} releases from ${esc(items[0].date)} to ${esc(items[items.length-1].date)}, rising from naming no harness to training across several">`; | |
| // level bands | |
| L.forEach((lv, i) => { | |
| const yy = y(i); | |
| s += `<line x1="${padL}" x2="${W-padR}" y1="${yy}" y2="${yy}" stroke="${bd}" stroke-dasharray="2 4"/>`; | |
| if (!narrow) s += `<text x="${padL-10}" y="${yy+4}" text-anchor="end" font-size="11" fill="${i===3 ? P(3) : mu}">${esc(lv)}</text>`; | |
| else s += `<text x="${padL}" y="${yy-7}" font-size="9.5" fill="${mu}">${esc(lv)}</text>`; | |
| }); | |
| // month axis, labelled every quarter, years under January | |
| for (let m = m0 + 1; m < m1; m++) { | |
| const mm = m % 12; | |
| if (mm % 3 !== 0 && m !== m0 + 1) continue; | |
| const xx = x(m); | |
| s += `<line x1="${xx}" x2="${xx}" y1="${H-26}" y2="${H-22}" stroke="${mu}"/>`; | |
| s += `<text x="${xx}" y="${H-10}" text-anchor="middle" font-size="10" fill="${mu}">${mLabel(m)}${mm === 0 || m === m0 + 1 ? ' ' + Math.floor(m/12) : ''}</text>`; | |
| } | |
| // the highest level reached so far, as a rising step line | |
| let sp = '', b = -1, px = null; | |
| items.slice(0, shown).forEach(it => { | |
| const xx = x(mIndex(it.date)); | |
| if (b < 0) { b = it.level; sp = `M ${xx} ${y(b)}`; } | |
| else if (it.level > b) { sp += ` L ${xx} ${y(b)} L ${xx} ${y(it.level)}`; b = it.level; } | |
| px = xx; | |
| }); | |
| const headX = x(m0 + (shown >= items.length ? m1 - m0 : (mIndex(items[Math.min(shown, items.length-1)].date) - m0) * Math.min(1, (t % STEP)/STEP) + (mIndex(items[shown-1].date) - m0) * (1 - Math.min(1, (t % STEP)/STEP)))); | |
| if (b >= 0) sp += ` L ${Math.max(px, Math.min(headX, W-padR))} ${y(b)}`; | |
| s += `<path d="${sp}" fill="none" stroke="${P(3)}" stroke-opacity=".55" stroke-width="2"/>`; | |
| // playhead | |
| if (playing && pinned < 0 && shown < items.length) s += `<line x1="${headX}" x2="${headX}" y1="${top}" y2="${H-26}" stroke="${tx}" stroke-opacity=".25"/>`; | |
| // dots, stacked slightly when two releases share a month and level | |
| const seen = {}; | |
| items.forEach((it, i) => { | |
| if (i >= shown) return; | |
| const key = it.date + '|' + it.level; const n = seen[key] = (seen[key] || 0) + 1; | |
| const xx = x(mIndex(it.date)) + (n - 1) * 9, yy = y(it.level) - (n - 1) * 4; | |
| const isCur = i === cur, grow = i === shown - 1 && pinned < 0 ? (reduce ? 1 : k) : 1; | |
| const c = P(it.level === 3 ? 3 : it.level === 2 ? 2 : it.level === 1 ? 1 : 0); | |
| s += `<circle cx="${xx}" cy="${yy}" r="${(isCur ? 7 : 5) * grow}" fill="${c}" stroke="${sf}" stroke-width="1.5" data-i="${i}" style="cursor:pointer"/>`; | |
| if (isCur) s += `<circle cx="${xx}" cy="${yy}" r="11" fill="none" stroke="${c}" stroke-opacity=".6"/>`; | |
| if (isCur && !narrow) { | |
| const right = xx < W*0.7; | |
| s += `<text x="${xx + (right ? 14 : -14)}" y="${yy+4}" text-anchor="${right ? 'start' : 'end'}" font-size="11.5" font-weight="600" fill="${tx}">${esc(it.label)}</text>`; | |
| } | |
| }); | |
| s += `</svg>`; | |
| svgHost.innerHTML = s; | |
| svgHost.querySelectorAll('circle[data-i]').forEach(el => { | |
| el.addEventListener('pointerenter', () => { const i = +el.dataset.i; if (pinned !== i) { pinned = i; draw(); } }); | |
| el.addEventListener('click', () => { pinned = +el.dataset.i; playing = false; draw(); }); | |
| }); | |
| const it = items[cur], lc = P(it.level === 3 ? 3 : it.level); | |
| cap.innerHTML = `<b>${esc(it.label)}</b> <span>${esc(new Date(it.date + '-01T00:00:00Z').toLocaleString('en', { month:'short', year:'numeric', timeZone:'UTC' }))}</span>` | |
| + `<span class="lv" style="color:${lc}">${esc(L[it.level])}</span><br>${esc(it.detail)}`; | |
| play.textContent = playing ? 'Pause' : (shown >= items.length && pinned < 0 ? 'Replay' : 'Play'); | |
| }; | |
| play.addEventListener('click', () => { | |
| if (!playing && (clock % TOTAL) >= items.length*STEP) clock = 0; | |
| playing = !playing; pinned = -1; draw(); | |
| }); | |
| svgHost.addEventListener('pointerleave', () => { if (pinned >= 0 && playing) { pinned = -1; draw(); } }); | |
| const tick = (ts) => { | |
| if (!last) last = ts; | |
| const dt = Math.min(64, ts - last); last = ts; | |
| if (playing && visible && pinned < 0 && !document.hidden) { | |
| clock += dt; | |
| // Stop on the full picture at the end rather than looping back to empty. | |
| if (clock >= items.length*STEP) { clock = items.length*STEP; playing = false; } | |
| draw(); | |
| } | |
| requestAnimationFrame(tick); | |
| }; | |
| draw(); | |
| if (!reduce) requestAnimationFrame(tick); | |
| if ('IntersectionObserver' in window) { | |
| new IntersectionObserver((es) => { visible = es.some(e => e.isIntersecting); }, { threshold: .35 }).observe(r); | |
| } else visible = true; | |
| // Colours are read when drawing, so redraw when the reader switches theme. | |
| new MutationObserver(draw).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); | |
| if ('ResizeObserver' in window) { let tm; new ResizeObserver(() => { clearTimeout(tm); tm = setTimeout(draw, 100); }).observe(r); } | |
| }).catch(e => { r.innerHTML = `<pre style="color:#c33;font-size:12px">${esc(e.message)}</pre>`; }); | |
| }; | |
| if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, { once: true }); else boot(); | |
| })(); | |
| </script> | |