Spaces:
Running
Running
| <!-- Convergence Curves: Small-multiples sparkline cards with convergence gauges --> | |
| <div class="convergence-curves"></div> | |
| <style> | |
| .convergence-curves { position: relative; width: 100%; } | |
| .convergence-curves .cc-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); | |
| gap: 12px; | |
| } | |
| .convergence-curves .cc-card { | |
| border: 1px solid var(--border-color); | |
| border-radius: 12px; | |
| padding: 16px 18px 14px; | |
| background: var(--surface-bg); | |
| position: relative; | |
| overflow: hidden; | |
| transition: border-color 0.2s ease, box-shadow 0.2s ease; | |
| cursor: default; | |
| } | |
| .convergence-curves .cc-card:hover { | |
| border-color: var(--text-color); | |
| box-shadow: 0 2px 12px rgba(0,0,0,0.06); | |
| } | |
| .convergence-curves .cc-name { | |
| font-size: 12px; font-weight: 600; color: var(--text-color); | |
| margin-bottom: 2px; letter-spacing: 0.01em; | |
| } | |
| .convergence-curves .cc-meta { | |
| font-size: 10px; color: var(--text-color); opacity: 0.4; | |
| font-variant-numeric: tabular-nums; margin-bottom: 10px; | |
| } | |
| .convergence-curves .cc-big { | |
| font-size: 28px; font-weight: 800; line-height: 1; | |
| font-variant-numeric: tabular-nums; | |
| margin-bottom: 2px; | |
| } | |
| .convergence-curves .cc-sub { | |
| font-size: 10px; color: var(--text-color); opacity: 0.45; | |
| margin-bottom: 10px; | |
| } | |
| .convergence-curves .cc-spark { display: block; width: 100%; } | |
| .convergence-curves .cc-bar-track { | |
| height: 4px; border-radius: 2px; background: var(--border-color); | |
| opacity: 0.3; margin-top: 10px; overflow: hidden; | |
| } | |
| .convergence-curves .cc-bar-fill { | |
| height: 100%; border-radius: 2px; | |
| transition: width 1.2s cubic-bezier(0.22, 1, 0.36, 1); | |
| } | |
| .convergence-curves .cc-fitness { | |
| font-size: 10px; color: var(--text-color); opacity: 0.5; | |
| font-variant-numeric: tabular-nums; margin-top: 6px; | |
| text-align: right; | |
| } | |
| .convergence-curves .tooltip { | |
| position: absolute; top: 0; left: 0; pointer-events: none; padding: 10px 14px; border-radius: 8px; | |
| font-size: 12px; line-height: 1.6; border: 1px solid var(--border-color); | |
| background: var(--surface-bg); color: var(--text-color); | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.12); | |
| backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); | |
| opacity: 0; transition: opacity 0.15s ease; | |
| z-index: 100; max-width: 220px; | |
| font-variant-numeric: tabular-nums; | |
| } | |
| @media (max-width: 600px) { | |
| .convergence-curves .cc-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; } | |
| .convergence-curves .cc-card { padding: 12px 14px 10px; } | |
| .convergence-curves .cc-big { font-size: 22px; } | |
| } | |
| </style> | |
| <script> | |
| (() => { | |
| const ensureD3 = (cb) => { | |
| if (window.d3 && typeof window.d3.select === 'function') return cb(); | |
| let s = document.getElementById('d3-cdn-script'); | |
| if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); } | |
| s.addEventListener('load', () => cb(), { once: true }); | |
| }; | |
| const bootstrap = () => { | |
| const container = document.querySelector('.convergence-curves:not([data-mounted])'); | |
| if (!container) return; | |
| container.dataset.mounted = 'true'; | |
| const d3 = window.d3; | |
| // Real incremental-convergence runs (experiments/convergence-rate/convergence-results.json). | |
| // convPct = smallest % of training data at which replay fitness first reaches 0.95. | |
| // curve = the experiment's actual fitnessTrajectory: [pctData, fitness]. | |
| const datasets = [ | |
| { name: 'SWE-agent', traces: '2,000', states: 25, finalFitness: 0.996, | |
| convPct: 1, color: '#3d5a80', | |
| curve: [[0.01,0.9851],[0.05,0.9851],[0.10,0.9961],[0.20,0.9961],[0.50,0.9961],[1.0,0.9961]] }, | |
| { name: 'SWE-smith', traces: '500', states: 10, finalFitness: 1.000, | |
| convPct: 1, color: '#9e5e5a', | |
| curve: [[0.01,0.9996],[0.05,0.9996],[0.10,0.9996],[0.20,0.9996],[0.50,1.000],[1.0,1.000]] }, | |
| { name: 'Mind2Web', traces: '500', states: 8, finalFitness: 0.999, | |
| convPct: 5, color: '#4d6278', | |
| curve: [[0.01,0.7921],[0.05,0.9503],[0.10,0.9503],[0.20,0.9665],[0.50,0.999],[1.0,0.999]] }, | |
| { name: 'Who & When', traces: '184', states: 9, finalFitness: 1.000, | |
| convPct: 10, color: '#8fa6c4', | |
| curve: [[0.01,0.1709],[0.05,0.7945],[0.10,0.9776],[0.20,0.9797],[0.50,0.9797],[1.0,1.000]] }, | |
| ]; | |
| // Build grid | |
| const grid = document.createElement('div'); | |
| grid.className = 'cc-grid'; | |
| container.appendChild(grid); | |
| const tip = document.createElement('div'); | |
| tip.className = 'tooltip'; | |
| container.appendChild(tip); | |
| datasets.forEach((ds, idx) => { | |
| const card = document.createElement('div'); | |
| card.className = 'cc-card'; | |
| card.innerHTML = ` | |
| <div class="cc-name">${ds.name}</div> | |
| <div class="cc-meta">${ds.traces} traces · ${ds.states} states</div> | |
| <div class="cc-big" style="color:${ds.color}">${ds.convPct}%</div> | |
| <div class="cc-sub">of training data to reach 0.95 fitness</div> | |
| <svg class="cc-spark" data-idx="${idx}"></svg> | |
| <div class="cc-bar-track"><div class="cc-bar-fill" style="background:${ds.color};width:0%"></div></div> | |
| <div class="cc-fitness">final fitness ${ds.finalFitness.toFixed(3)}</div> | |
| `; | |
| grid.appendChild(card); | |
| // Hover tooltip | |
| card.addEventListener('mouseenter', (ev) => { | |
| const convTraces = Math.round(ds.convPct / 100 * parseInt(ds.traces.replace(/,/g, ''))); | |
| tip.innerHTML = `<strong>${ds.name}</strong><br/> | |
| Converges at <strong>${ds.convPct}%</strong> (${convTraces.toLocaleString()} traces)<br/> | |
| Final fitness: <strong>${ds.finalFitness.toFixed(3)}</strong><br/> | |
| FSM states: <strong>${ds.states}</strong>`; | |
| tip.style.opacity = '1'; | |
| }); | |
| card.addEventListener('mousemove', (ev) => { | |
| const cr = container.getBoundingClientRect(); | |
| const mx = ev.clientX - cr.left; | |
| const my = ev.clientY - cr.top; | |
| const tw = tip.offsetWidth || 200; | |
| let tx = mx + 14; | |
| if (tx + tw > cr.width) tx = mx - tw - 14; | |
| tip.style.transform = `translate(${tx}px, ${my - 14}px)`; | |
| }); | |
| card.addEventListener('mouseleave', () => { tip.style.opacity = '0'; }); | |
| }); | |
| // Draw sparklines | |
| function drawSparklines() { | |
| container.querySelectorAll('.cc-spark').forEach(svgEl => { | |
| const idx = +svgEl.dataset.idx; | |
| const ds = datasets[idx]; | |
| const rect = svgEl.parentElement.getBoundingClientRect(); | |
| const W = Math.max(100, Math.round(rect.width - 36)); | |
| const H = 48; | |
| svgEl.setAttribute('width', W); | |
| svgEl.setAttribute('height', H); | |
| svgEl.setAttribute('viewBox', `0 0 ${W} ${H}`); | |
| svgEl.innerHTML = ''; | |
| const svg = d3.select(svgEl); | |
| // Full trajectory on x (0-100% of data); y adapts to each curve's real floor. | |
| const fitVals = ds.curve.map(p => p[1]); | |
| const yFloor = Math.max(0, Math.min(...fitVals) - 0.03); | |
| const x = d3.scaleLinear().domain([0, 1]).range([0, W]); | |
| const y = d3.scaleLinear().domain([yFloor, 1.005]).range([H, 0]); | |
| const clipped = ds.curve; | |
| // Area fill | |
| const areaGen = d3.area() | |
| .x(d => x(d[0])).y0(H).y1(d => y(d[1])) | |
| .curve(d3.curveMonotoneX); | |
| svg.append('path') | |
| .datum(clipped) | |
| .attr('d', areaGen) | |
| .attr('fill', ds.color) | |
| .attr('opacity', 0.08); | |
| // Line | |
| const lineGen = d3.line() | |
| .x(d => x(d[0])).y(d => y(d[1])) | |
| .curve(d3.curveMonotoneX); | |
| svg.append('path') | |
| .datum(clipped) | |
| .attr('fill', 'none') | |
| .attr('stroke', ds.color) | |
| .attr('stroke-width', 2) | |
| .attr('opacity', 0.7) | |
| .attr('d', lineGen); | |
| // 0.999 reference line | |
| if (y(0.999) > 2 && y(0.999) < H - 2) { | |
| svg.append('line') | |
| .attr('x1', 0).attr('x2', W) | |
| .attr('y1', y(0.999)).attr('y2', y(0.999)) | |
| .attr('stroke', 'var(--text-color)') | |
| .attr('stroke-width', 0.5) | |
| .attr('stroke-dasharray', '2,3') | |
| .attr('opacity', 0.2); | |
| } | |
| // Convergence marker | |
| const convX = x(ds.convPct / 100); | |
| if (convX >= 0 && convX <= W) { | |
| // Vertical line at convergence | |
| svg.append('line') | |
| .attr('x1', convX).attr('x2', convX) | |
| .attr('y1', 0).attr('y2', H) | |
| .attr('stroke', ds.color) | |
| .attr('stroke-width', 1) | |
| .attr('stroke-dasharray', '2,2') | |
| .attr('opacity', 0.35); | |
| // Dot | |
| const convY = y(ds.finalFitness); | |
| svg.append('circle') | |
| .attr('cx', convX).attr('cy', convY) | |
| .attr('r', 3.5) | |
| .attr('fill', ds.color) | |
| .attr('stroke', 'var(--surface-bg)') | |
| .attr('stroke-width', 1.5); | |
| } | |
| }); | |
| } | |
| drawSparklines(); | |
| // Animate progress bars on scroll into view | |
| let animated = false; | |
| const animateBars = () => { | |
| if (animated) return; | |
| const rect = container.getBoundingClientRect(); | |
| if (rect.top < window.innerHeight * 0.85) { | |
| animated = true; | |
| container.querySelectorAll('.cc-bar-fill').forEach((bar, i) => { | |
| setTimeout(() => { | |
| bar.style.width = datasets[i].convPct + '%'; | |
| }, i * 120); | |
| }); | |
| } | |
| }; | |
| animateBars(); | |
| window.addEventListener('scroll', animateBars, { passive: true }); | |
| if (window.ResizeObserver) new ResizeObserver(() => drawSparklines()).observe(container); | |
| }; | |
| if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); | |
| else ensureD3(bootstrap); | |
| })(); | |
| </script> | |