diff --git "a/index.html" "b/index.html" --- "a/index.html" +++ "b/index.html" @@ -1,1938 +1,1940 @@ - - - - - - Silver Regression Explorer - - - - -
-
-
-
-
Regression explorer
-

SLM Regression Line

-

- Choose a benchmark, filter by org or size, and compare models to the size trend. Furthermore, you can submit specific parameter counts and see the predicted benchmark scores. Enjoy! -

-
-
- - - - -
-
-
- -
- - -
-
-
-
-

Average score vs log parameters

-
- - Hover for details. Click a point to open the model page. -
-
-
0 visible / 0 bins
-
-
- -
-
- -
-
Selected benchmark
Avg
-
Visible models
0
-
Fit bins
0
-
Mean absolute residual
-
Residual spread
-
Visible match rate
-
Visible orgs
-
Plot mode
Silver
-
- - - -
-
-
- - + + + +
+
+
+
+
Regression explorer
+

SLM Regression Line

+

+ Choose a benchmark, filter by org or size, and compare models to the size trend. Furthermore, you can submit specific parameter counts and see the predicted benchmark scores. Enjoy! +

+
+
+ + + + +
+
+
+ +
+ + +
+
+
+
+

Average score vs log parameters

+
+ + Hover for details. Click a point to open the model page. +
+
+
0 visible / 0 bins
+
+
+ +
+
+ +
+
Selected benchmark
Avg
+
Visible models
0
+
Fit bins
0
+
Mean absolute residual
+
Residual spread
+
Visible match rate
+
Visible orgs
+
Plot mode
Silver
+
+ + + +
+
+
+ + - - + .filter(([k, v]) => typeof v === 'number' && Number.isFinite(v) && !META_KEYS.has(k) && !k.startsWith('avg')) + .map(([, v]) => v); + if (!values.length) return null; + return values.reduce((a, b) => a + b, 0) / values.length; + } + return model[key]; + } + + + function median(values) { + if (!values.length) return 0; + const sorted = [...values].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; + } + + function weightedMedian(values, weights) { + const pairs = values + .map((v, i) => [v, Math.max(0, Number.isFinite(weights?.[i]) ? weights[i] : 1)]) + .filter(([v, w]) => Number.isFinite(v) && Number.isFinite(w) && w > 0) + .sort((a, b) => a[0] - b[0]); + + if (!pairs.length) return 0; + + const total = pairs.reduce((acc, [, w]) => acc + w, 0); + let acc = 0; + for (const [value, weight] of pairs) { + acc += weight; + if (acc >= total / 2) return value; + } + return pairs[pairs.length - 1][0]; + } + + function evaluatePolynomial(coefficients, x) { + if (!coefficients?.length) return 0; + let y = 0; + for (let i = 0; i < coefficients.length; i += 1) { + y = (y * x) + coefficients[i]; + } + return y; + } + + function polynomialDerivative(coefficients, x) { + if (!coefficients?.length || coefficients.length < 2) return 0; + const degree = coefficients.length - 1; + let y = 0; + for (let i = 0; i < degree; i += 1) { + const power = degree - i; + y = (y * x) + (coefficients[i] * power); + } + return y; + } + + function solveLinearSystem(matrix, vector) { + const n = vector.length; + const a = matrix.map((row, i) => [...row, vector[i]]); + + for (let col = 0; col < n; col += 1) { + let pivotRow = col; + let pivotAbs = Math.abs(a[col][col]); + for (let row = col + 1; row < n; row += 1) { + const cand = Math.abs(a[row][col]); + if (cand > pivotAbs) { + pivotAbs = cand; + pivotRow = row; + } + } + + if (pivotAbs < 1e-12) { + return null; + } + + if (pivotRow !== col) { + const tmp = a[col]; + a[col] = a[pivotRow]; + a[pivotRow] = tmp; + } + + const pivot = a[col][col]; + for (let j = col; j <= n; j += 1) { + a[col][j] /= pivot; + } + + for (let row = 0; row < n; row += 1) { + if (row === col) continue; + const factor = a[row][col]; + if (Math.abs(factor) < 1e-12) continue; + for (let j = col; j <= n; j += 1) { + a[row][j] -= factor * a[col][j]; + } + } + } + + return a.map(row => row[n]); + } + + function weightedPolynomialRegression(points, degree = 1, weights = null) { + const n = points.length; + if (!n) return { degree, coefficients: [0], mse: 0, rmse: 0, r2: 0, n: 0, weightSum: 0 }; + + const actualDegree = Math.max(0, Math.min(degree, n - 1)); + const size = actualDegree + 1; + const matrix = Array.from({ length: size }, () => Array(size).fill(0)); + const vector = Array(size).fill(0); + + for (let i = 0; i < n; i += 1) { + const p = points[i]; + const w = Math.max(0, Number.isFinite(weights?.[i]) ? weights[i] : 1); + if (!w) continue; + + const basis = []; + for (let d = actualDegree; d >= 0; d -= 1) { + basis.push(p.x ** d); + } + + for (let r = 0; r < size; r += 1) { + vector[r] += w * basis[r] * p.y; + for (let c = 0; c < size; c += 1) { + matrix[r][c] += w * basis[r] * basis[c]; + } + } + } + + let coefficients = solveLinearSystem(matrix, vector); + + if (!coefficients) { + if (actualDegree === 0) { + const avg = points.reduce((acc, p, i) => { + const w = Math.max(0, Number.isFinite(weights?.[i]) ? weights[i] : 1); + return acc + w * p.y; + }, 0); + const sw = points.reduce((acc, p, i) => acc + Math.max(0, Number.isFinite(weights?.[i]) ? weights[i] : 1), 0); + coefficients = [sw ? avg / sw : 0]; + } else { + return weightedPolynomialRegression(points, actualDegree - 1, weights); + } + } + + const predictions = points.map(p => evaluatePolynomial(coefficients, p.x)); + const sw = points.reduce((acc, p, i) => acc + Math.max(0, Number.isFinite(weights?.[i]) ? weights[i] : 1), 0); + const yMean = sw ? points.reduce((acc, p, i) => acc + Math.max(0, Number.isFinite(weights?.[i]) ? weights[i] : 1) * p.y, 0) / sw : 0; + const sse = points.reduce((acc, p, i) => { + const w = Math.max(0, Number.isFinite(weights?.[i]) ? weights[i] : 1); + const resid = p.y - predictions[i]; + return acc + w * resid * resid; + }, 0); + const sst = points.reduce((acc, p, i) => { + const w = Math.max(0, Number.isFinite(weights?.[i]) ? weights[i] : 1); + return acc + w * (p.y - yMean) ** 2; + }, 0); + const mse = sw ? sse / sw : 0; + const rmse = Math.sqrt(mse); + const r2 = sst > 0 ? 1 - (sse / sst) : 0; + return { degree: actualDegree, coefficients, mse, rmse, r2, n, weightSum: sw }; + } + + function weightedLinearRegression(points, weights = null) { + return weightedPolynomialRegression(points, 1, weights); + } + + function linearRegression(points) { + return weightedPolynomialRegression(points, 1); + } + + function enrichLinearFit(fit, points, method = fit?.method || 'linear') { + const clean = (points || []).filter(p => Number.isFinite(p.x) && Number.isFinite(p.y)); + const n = clean.length; + const slope = fit?.coefficients?.[0] ?? 0; + const intercept = fit?.coefficients?.[1] ?? 0; + const xMean = n ? clean.reduce((acc, p) => acc + p.x, 0) / n : 0; + const yMean = n ? clean.reduce((acc, p) => acc + p.y, 0) / n : 0; + const sse = clean.reduce((acc, p) => acc + (p.y - evaluatePolynomial([slope, intercept], p.x)) ** 2, 0); + const sst = clean.reduce((acc, p) => acc + (p.y - yMean) ** 2, 0); + const mse = n ? sse / n : 0; + + return { + ...fit, + degree: 1, + coefficients: [slope, intercept], + slope, + intercept, + xMean, + centerValue: evaluatePolynomial([slope, intercept], xMean), + centerSlope: slope, + curvature: 0, + mse, + rmse: Math.sqrt(mse), + r2: sst > 0 ? 1 - (sse / sst) : 0, + n, + method + }; + } + + function fitError(fit, points) { + const residuals = (points || []) + .map(p => p.y - evaluateFit(fit, p.x)) + .filter(Number.isFinite) + .sort((a, b) => Math.abs(a) - Math.abs(b)); + if (!residuals.length) return Infinity; + const trim = Math.floor(residuals.length * 0.1); + const used = residuals.slice(0, Math.max(1, residuals.length - trim)); + return Math.sqrt(used.reduce((acc, r) => acc + r * r, 0) / used.length); + } + + function bestLinearFit(fitSamples) { + const candidates = [ + enrichLinearFit(robustLinearRegression(fitSamples), fitSamples, 'robust-linear'), + enrichLinearFit(weightedLinearRegression(fitSamples), fitSamples, 'binned-linear') + ]; + candidates.sort((a, b) => fitError(a, fitSamples) - fitError(b, fitSamples)); + return candidates[0]; + } + + + function robustPolynomialRegression(points, degree = 2) { + const n = points.length; + if (n < 2) return weightedPolynomialRegression(points, degree); + + const baseWeights = points.map(p => Math.max(1, Number.isFinite(p.count) ? p.count : 1)); + let weights = [...baseWeights]; + let fit = weightedPolynomialRegression(points, degree, weights); + + for (let iter = 0; iter < 10; iter += 1) { + const residuals = points.map((p, i) => p.y - evaluateFit(fit, p.x)); + const residMedian = median(residuals); + const absDeviations = residuals.map(r => Math.abs(r - residMedian)); + const scale = Math.max(1e-6, 1.4826 * median(absDeviations)); + const huberK = 1.345 * scale; + const xMedian = median(points.map(p => p.x)); + const xScale = Math.max(1e-6, 1.4826 * median(points.map(p => Math.abs(p.x - xMedian)))); + + const nextWeights = points.map((p, i) => { + const resid = Math.abs(residuals[i] - residMedian); + let w = baseWeights[i]; + if (resid > huberK) w *= huberK / resid; + const leverage = Math.abs(p.x - xMedian) / xScale; + w *= 1 / (1 + 0.18 * leverage + 0.02 * leverage * leverage); + return Math.max(w, 1e-6); + }); + + const next = weightedPolynomialRegression(points, degree, nextWeights); + const delta = next.coefficients.reduce((acc, coef, i) => acc + Math.abs((fit.coefficients[i] ?? 0) - coef), 0); + fit = next; + weights = nextWeights; + if (delta < 1e-10) break; + } + + const xMean = points.reduce((acc, p, i) => { + const w = Math.max(0, Number.isFinite(weights?.[i]) ? weights[i] : 1); + return acc + w * p.x; + }, 0) / Math.max(1e-12, weights.reduce((a, b) => a + b, 0)); + fit.xMean = xMean; + fit.centerValue = evaluateFit(fit, xMean); + fit.centerSlope = polynomialDerivative(fit.coefficients, xMean); + fit.curvature = fit.coefficients.length >= 3 ? fit.coefficients[0] : 0; + fit.method = `robust-degree-${fit.degree}`; + fit.effectiveN = weights.reduce((a, b) => a + b, 0); + return fit; + } + + function robustLinearRegression(points) { + const n = points.length; + if (n < 2) return weightedPolynomialRegression(points, 1); + + const baseWeights = points.map(p => Math.max(1, Number.isFinite(p.count) ? p.count : 1)); + + const slopes = []; + const slopeWeights = []; + for (let i = 0; i < n; i += 1) { + for (let j = i + 1; j < n; j += 1) { + const dx = points[j].x - points[i].x; + if (Math.abs(dx) < 1e-12) continue; + slopes.push((points[j].y - points[i].y) / dx); + slopeWeights.push(baseWeights[i] * baseWeights[j]); + } + } + + let slope = slopes.length ? weightedMedian(slopes, slopeWeights) : 0; + if (!Number.isFinite(slope)) { + slope = weightedLinearRegression(points, baseWeights).coefficients[0] ?? 0; + } + + let intercept = weightedMedian( + points.map(p => p.y - slope * p.x), + baseWeights + ); + if (!Number.isFinite(intercept)) intercept = 0; + + let fit = { + degree: 1, + coefficients: [slope, intercept], + mse: 0, + rmse: 0, + r2: 0, + n, + weightSum: baseWeights.reduce((a, b) => a + b, 0), + method: 'theil-sen-seeded-linear' + }; + + let weights = [...baseWeights]; + for (let iter = 0; iter < 6; iter += 1) { + const residuals = points.map(p => p.y - evaluateFit(fit, p.x)); + const residMedian = median(residuals); + const absDeviations = residuals.map(r => Math.abs(r - residMedian)); + const scale = Math.max(1e-6, 1.4826 * median(absDeviations)); + const huberK = 1.345 * scale; + const xMedian = median(points.map(p => p.x)); + const xScale = Math.max(1e-6, 1.4826 * median(points.map(p => Math.abs(p.x - xMedian)))); + + const nextWeights = points.map((p, i) => { + const resid = Math.abs(residuals[i] - residMedian); + let w = baseWeights[i]; + if (resid > huberK) w *= huberK / resid; + const leverage = Math.abs(p.x - xMedian) / xScale; + w *= 1 / (1 + 0.14 * leverage + 0.015 * leverage * leverage); + return Math.max(w, 1e-6); + }); + + const nextFit = weightedPolynomialRegression(points, 1, nextWeights); + fit = nextFit; + weights = nextWeights; + if (iter > 0) { + const delta = Math.abs((fit.coefficients[0] ?? 0) - slope) + Math.abs((fit.coefficients[1] ?? 0) - intercept); + if (delta < 1e-10) break; + } + slope = fit.coefficients[0] ?? slope; + intercept = fit.coefficients[1] ?? intercept; + } + + const xMean = points.reduce((acc, p, i) => { + const w = Math.max(0, Number.isFinite(weights?.[i]) ? weights[i] : 1); + return acc + w * p.x; + }, 0) / Math.max(1e-12, weights.reduce((a, b) => a + b, 0)); + + fit.xMean = xMean; + fit.centerValue = evaluateFit(fit, xMean); + fit.centerSlope = fit.coefficients[0] ?? 0; + fit.slope = fit.coefficients[0] ?? 0; + fit.intercept = fit.coefficients[1] ?? 0; + fit.curvature = 0; + fit.effectiveN = weights.reduce((a, b) => a + b, 0); + fit.method = 'robust-linear'; + return fit; + } + + function evaluateFit(fit, x) { + return evaluatePolynomial(fit?.coefficients || [0], x); + } + + function fitSlopeAt(fit, x) { + return polynomialDerivative(fit?.coefficients || [0], x); + } + + function fitCenter(fit) { + return fit?.xMean ?? 0; + } + + function fitSummaryValue(fit) { + return fit?.centerValue ?? 0; + } + + function formatFitEquation(fit) { + if (!fit?.coefficients?.length) return '—'; + const coeffs = fit.coefficients; + if (fit.degree === 1 && coeffs.length >= 2) { + return `${coeffs[0].toFixed(4)}x + ${coeffs[1].toFixed(2)}`; + } + if (fit.degree >= 2 && coeffs.length >= 3) { + return `${coeffs[0].toFixed(4)}x² + ${coeffs[1].toFixed(4)}x + ${coeffs[2].toFixed(2)}`; + } + return coeffs.map(v => v.toFixed(4)).join(', '); + } + + function getEligibleModels() { + return MODELS.filter(m => { + const score = getMetricValue(m, activeBenchmark); + return Number.isFinite(m.params) && + m.params >= MIN_PLOT_PARAMS && + score !== null && + score !== undefined && + Number.isFinite(score); + }); + } + + function getVisibleModels() { + const q = activeSearch.trim().toLowerCase(); + return getEligibleModels().filter(m => { + if (activeOrg !== 'all' && m.org !== activeOrg) return false; + if (activeParamBucket !== 'all' && bucketForParams(m.params) !== activeParamBucket) return false; + if (!q) return true; + return m.name.toLowerCase().includes(q) || m.org.toLowerCase().includes(q); + }); + } + + function getOrgList() { + return [...new Set(MODELS.map(m => m.org))].sort((a, b) => a.localeCompare(b)); + } + + function setChipState(btn, active) { + btn.classList.toggle('active', active); + btn.setAttribute('aria-pressed', active ? 'true' : 'false'); + } + + function applyTheme(theme) { + activeTheme = theme; + document.body.dataset.theme = theme; + setChipState(document.getElementById('themeLight'), theme === 'light'); + setChipState(document.getElementById('themeDark'), theme === 'dark'); + localStorage.setItem('silverRegressionTheme', theme); + render(); + } + + function renderBenchmarkButtons() { + const box = document.getElementById('benchmarkButtons'); + box.innerHTML = BENCHMARKS.map(b => ` + + `).join(''); + box.querySelectorAll('button').forEach(btn => { + btn.addEventListener('click', () => { + activeBenchmark = btn.dataset.key; + renderOrgUI(); + render(); + }); + }); + } + + function renderOrgUI() { + const allOrgs = getOrgList(); + const orgSearch = activeOrgSearch.trim().toLowerCase(); + const orgs = orgSearch + ? allOrgs.filter(o => o.toLowerCase().includes(orgSearch)) + : allOrgs; + + const chips = document.getElementById('orgChips'); + const summary = document.getElementById('orgSummary'); + summary.textContent = activeOrg === 'all' ? 'All orgs' : `Org: ${activeOrg}`; + + chips.innerHTML = [ + ``, + ...orgs.map(org => ``) + ].join(''); + + chips.querySelectorAll('button').forEach(btn => { + btn.addEventListener('click', () => { + activeOrg = btn.dataset.org; + document.getElementById('orgSummary').textContent = activeOrg === 'all' ? 'All orgs' : `Org: ${activeOrg}`; + renderOrgUI(); + render(); + }); + }); + } + + function renderBucketUI() { + const box = document.getElementById('bucketButtons'); + box.innerHTML = BUCKETS.map(b => ` + + `).join(''); + box.querySelectorAll('button').forEach(btn => { + btn.addEventListener('click', () => { + activeParamBucket = btn.dataset.bucket; + renderBucketUI(); + render(); + }); + }); + } + + function applyMode(mode) { + activeMode = mode; + document.body.dataset.mode = mode; + setChipState(document.getElementById('modeDesktop'), mode === 'desktop'); + setChipState(document.getElementById('modeMobile'), mode === 'mobile'); + localStorage.setItem('silverRegressionMode', mode); + render(); + } + + function updatePredictionPanel() { + const input = document.getElementById('paramInput'); + const summary = document.getElementById('predictionSummary'); + const list = document.getElementById('predictionList'); + const value = parseParamCount(input.value); + + if (value === null) { + summary.textContent = 'Enter a parameter count like 12M, 250K, or 1.5B.'; + list.innerHTML = ''; + return; + } + + summary.textContent = `Parameter count: ${fmtParams(value)} (${value.toLocaleString()})`; + const rows = buildPredictionRows(value); + list.innerHTML = rows.map(row => { + const label = row.label === 'Avg' ? 'Average' : row.label; + return ` +
+
${escapeHtml(label)}
+
${formatScore(row.predicted)}
+
+ `; + }).join(''); + } + + function updateStats(fit, visibleCount, residuals, rawCount, binCount, rawFit) { + document.getElementById('statSlope').textContent = fit.n >= 2 ? (fit.curvature ?? 0).toFixed(6) : '—'; + document.getElementById('statIntercept').textContent = fit.n >= 2 ? (fit.centerSlope ?? fit.slope ?? 0).toFixed(4) : '—'; + document.getElementById('statMSE').textContent = fit.n >= 2 ? `${clampScore(fit.centerValue ?? fitSummaryValue(fit)).toFixed(2)}%` : '—'; + document.getElementById('statRMSE').textContent = fit.n >= 2 ? fit.rmse.toFixed(2) : '—'; + document.getElementById('statR2').textContent = fit.n >= 2 ? fit.r2.toFixed(3) : '—'; + + document.getElementById('countBadge').textContent = `${visibleCount} visible / ${binCount} bins`; + document.getElementById('infoBenchmark').textContent = BENCHMARK_NAMES[activeBenchmark] || activeBenchmark; + document.getElementById('infoCount').textContent = String(visibleCount); + document.getElementById('infoFitCount').textContent = String(binCount); + + const absMean = residuals.length ? residuals.reduce((a, b) => a + Math.abs(b), 0) / residuals.length : 0; + const mean = residuals.length ? residuals.reduce((a, b) => a + b, 0) / residuals.length : 0; + const spread = residuals.length ? Math.sqrt(residuals.reduce((a, b) => a + (b - mean) ** 2, 0) / residuals.length) : 0; + + document.getElementById('infoMAE').textContent = residuals.length ? `${absMean.toFixed(2)} pts` : '—'; + document.getElementById('infoResidualSpread').textContent = residuals.length ? `${spread.toFixed(2)} pts` : '—'; + + const orgCount = new Set(getVisibleModels().map(m => m.org)).size; + document.getElementById('infoOrgs').textContent = `${orgCount} orgs`; + document.getElementById('infoMatchRate').textContent = `${visibleCount}/${rawCount}`; + document.getElementById('infoMode').textContent = activeMode === 'mobile' ? 'Mobile' : 'Computer'; + + document.getElementById('fitNote').textContent = + `Fit uses ${rawCount} eligible models collapsed into ${binCount} size bins for ${BENCHMARK_NAMES[activeBenchmark] || activeBenchmark}. Raw-point RMSE is ${rawFit.n >= 2 ? rawFit.rmse.toFixed(2) : '—'}; binned fit RMSE is ${fit.n >= 2 ? fit.rmse.toFixed(2) : '—'}. Search and org filters only affect visibility.`; + } + + function setText(id, value) { + const target = document.getElementById(id); + if (target) target.textContent = value; + } + + const residualLabelPlugin = { + id: 'residualLabelPlugin', + afterDatasetsDraw(chartInstance) { + if (!showResidualLabels) return; + const datasetIndex = 1; + const meta = chartInstance.getDatasetMeta(datasetIndex); + const dataset = chartInstance.data.datasets[datasetIndex]; + if (!meta || meta.hidden || !dataset?.data?.length) return; + + const { ctx, chartArea } = chartInstance; + ctx.save(); + ctx.font = '700 11px Inter, system-ui, sans-serif'; + ctx.textBaseline = 'middle'; + ctx.lineWidth = 1; + + meta.data.forEach((point, index) => { + const raw = dataset.data[index]; + if (!raw || !Number.isFinite(raw.residual) || Math.abs(raw.residual) < 3) return; + + const label = `${raw.residual >= 0 ? '+' : ''}${raw.residual.toFixed(1)}`; + const metrics = ctx.measureText(label); + const width = metrics.width + 10; + const height = 18; + let x = point.x + 8; + let y = point.y - 12; + + x = Math.min(chartArea.right - width - 2, Math.max(chartArea.left + 2, x)); + y = Math.min(chartArea.bottom - height / 2 - 2, Math.max(chartArea.top + height / 2 + 2, y)); + + ctx.fillStyle = activeTheme === 'light' ? 'rgba(255,255,255,.92)' : 'rgba(15,18,24,.92)'; + ctx.strokeStyle = raw.residual >= 0 ? 'rgba(47,111,237,.42)' : 'rgba(31,157,122,.42)'; + ctx.beginPath(); + ctx.roundRect(x, y - height / 2, width, height, 5); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = activeTheme === 'light' ? '#111318' : '#f2f5f9'; + ctx.fillText(label, x + 5, y); + }); + + ctx.restore(); + } + }; + + function render() { + const fitModels = getEligibleModels(); + const visibleModels = getVisibleModels(); + + const fitData = fitModels.map(m => { + const score = getMetricValue(m, activeBenchmark); + return { + name: m.name, + org: m.org, + params: m.params, + score: toPct(score), + url: m.url, + x: Math.log10(m.params), + y: toPct(score) + }; + }); + + const data = visibleModels.map(m => { + const score = getMetricValue(m, activeBenchmark); + return { + name: m.name, + org: m.org, + params: m.params, + score: toPct(score), + url: m.url, + x: Math.log10(m.params), + y: toPct(score) + }; + }); + + const chartTitleMap = { + avg: 'Average score vs log parameters', + arc_easy: 'ARC-Easy vs log parameters', + arc_challenge: 'ARC-Challenge vs log parameters', + hellaswag: 'HellaSwag vs log parameters', + piqa: 'PIQA vs log parameters' + }; + + setText('chartTitle', chartTitleMap[activeBenchmark] || 'Regression vs log parameters'); + setText('chartSub', + 'Binned linear regression on log10(parameters) for the selected benchmark. Each size bin contributes one equally weighted sample, so the line tracks the average score by parameter region instead of point density.'); + + if (fitData.length < 2) { + setText('chartSub', 'Need at least 2 eligible models to fit a line.'); + document.getElementById('countBadge').textContent = `${visibleModels.length} visible / ${fitData.length} bins`; + document.getElementById('infoBenchmark').textContent = BENCHMARK_NAMES[activeBenchmark] || activeBenchmark; + document.getElementById('infoCount').textContent = String(visibleModels.length); + document.getElementById('infoFitCount').textContent = String(fitData.length); + document.getElementById('infoMAE').textContent = '—'; + document.getElementById('infoResidualSpread').textContent = '—'; + document.getElementById('statSlope').textContent = '—'; + document.getElementById('statIntercept').textContent = '—'; + document.getElementById('statRMSE').textContent = '—'; + document.getElementById('statR2').textContent = '—'; + if (chart) chart.destroy(); + return; + } + + const fitSamples = buildBinnedFitSamples(fitData); + const fit = bestLinearFit(fitSamples); + const rawFit = enrichLinearFit(weightedLinearRegression(fitData), fitData, 'raw-linear'); + + const visibleResiduals = data.map(d => d.y - clampScore(evaluateFit(fit, d.x))); + data.forEach((d, i) => { + d.residual = visibleResiduals[i]; + d.prediction = clampScore(evaluateFit(fit, d.x)); + }); + + updateStats(fit, data.length, visibleResiduals, fitData.length, fitSamples.length, rawFit); + updatePredictionPanel(); + + const xMin = Math.min(...fitData.map(d => d.x)); + const xMax = Math.max(...fitData.map(d => d.x)); + const xPad = Math.max(0.18, (xMax - xMin) * 0.09); + + const lineSteps = 50; + const regressionLine = []; + for (let i = 0; i <= lineSteps; i += 1) { + const t = i / lineSteps; + const rawX = (xMin - xPad) + ((xMax + xPad) - (xMin - xPad)) * t; + regressionLine.push({ x: rawX, y: clampScore(evaluateFit(fit, rawX)) }); + } + + const yMin = Math.min(...fitData.map(d => d.y), ...regressionLine.map(p => p.y)) - 2.2; + const yMax = Math.max(...fitData.map(d => d.y), ...regressionLine.map(p => p.y)) + 2.2; + + if (chart) chart.destroy(); + + const ctx = document.getElementById('scatterChart').getContext('2d'); + chart = new Chart(ctx, { + type: 'scatter', + data: { + datasets: [ + { + type: 'line', + label: 'Regression', + data: regressionLine, + borderColor: activeTheme === 'light' ? 'rgba(47,111,237,0.92)' : 'rgba(127,176,255,0.95)', + borderWidth: lineEmphasis ? 4 : 3, + borderDash: lineEmphasis ? [] : [2, 8], + borderCapStyle: 'round', + pointRadius: 0, + tension: 0, + order: 0 + }, + { + label: 'Models', + data, + parsing: false, + pointRadius: activeMode === 'mobile' ? (lineEmphasis ? 5 : 5) : (lineEmphasis ? 6 : 7), + pointHoverRadius: activeMode === 'mobile' ? 10 : 12, + pointHitRadius: activeMode === 'mobile' ? 18 : 24, + borderWidth: 1.5, + backgroundColor: (ctx) => { + const raw = ctx.raw; + if (!raw) return activeTheme === 'light' ? 'rgba(20,22,27,0.95)' : 'rgba(245,246,248,0.95)'; + const abs = Math.abs(raw.residual ?? 0); + if (showResidualLabels && abs > 3.0) { + return raw.residual >= 0 ? (activeTheme === 'light' ? 'rgba(47,111,237,0.95)' : 'rgba(127,176,255,0.98)') : 'rgba(31,157,122,0.92)'; + } + if (activeTheme === 'light') { + return raw.residual >= 0 ? 'rgba(31,42,58,0.94)' : 'rgba(118,129,146,0.92)'; + } + return raw.residual >= 0 ? 'rgba(232,240,255,0.98)' : 'rgba(127,218,196,0.90)'; + }, + borderColor: (ctx) => { + const raw = ctx.raw; + if (!raw) return activeTheme === 'light' ? 'rgba(255,255,255,0.88)' : 'rgba(255,255,255,0.82)'; + if (activeTheme === 'light') { + return raw.residual >= 0 ? 'rgba(255,255,255,0.98)' : 'rgba(20,22,27,0.50)'; + } + return raw.residual >= 0 ? 'rgba(255,255,255,0.98)' : 'rgba(18,19,23,0.42)'; + }, + hoverBackgroundColor: 'rgba(255,255,255,1)', + hoverBorderColor: 'rgba(16,17,20,1)', + order: 2, + showLine: false + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + animation: { duration: 0 }, + interaction: { mode: 'nearest', intersect: true }, + onHover: (event, elements) => { + const target = event?.native?.target; + if (target && target.style) target.style.cursor = elements.length ? 'pointer' : 'default'; + }, + onClick: (event, elements) => { + if (!elements.length) return; + const hit = elements[0]; + if (hit.datasetIndex !== 1) return; + const item = chart.data.datasets[hit.datasetIndex].data[hit.index]; + if (item?.url) window.open(item.url, '_blank', 'noopener,noreferrer'); + }, + plugins: { + legend: { display: false }, + tooltip: { + enabled: true, + backgroundColor: 'rgba(10,11,14,0.98)', + borderColor: 'rgba(255,255,255,0.18)', + borderWidth: 1, + titleColor: '#ffffff', + bodyColor: '#dbe0e8', + titleFont: { size: 13, weight: '700' }, + bodyFont: { size: 12 }, + padding: 12, + displayColors: false, + caretPadding: 10, + cornerRadius: 12, + callbacks: { + title: (items) => items[0]?.raw?.name || '', + label: (item) => { + const d = item.raw; + const predicted = clampScore(evaluateFit(fit, d.x)); + const resid = d.y - predicted; + return [ + `Org: ${d.org}`, + `Params: ${fmtParams(d.params)} (${d.params.toLocaleString()})`, + `Score: ${d.score.toFixed(2)}%`, + `Residual: ${resid >= 0 ? '+' : ''}${resid.toFixed(2)} pts`, + `Predicted (robust line): ${predicted.toFixed(2)}%` + ]; + }, + afterLabel: (item) => { + const d = item.raw; + return showResidualLabels ? `Residual label: ${d.residual.toFixed(2)} pts` : ''; + } + } + } + }, + scales: { + x: { + type: 'linear', + min: Math.max(3.3, xMin - 0.20), + max: xMax + 0.15, + grid: { color: activeTheme === 'light' ? 'rgba(16,17,20,0.07)' : 'rgba(255,255,255,0.08)' }, + ticks: { + color: activeTheme === 'light' ? '#737a87' : '#aab1bf', + callback: v => `10^${Number(v).toFixed(1)}` + }, + title: { + display: true, + text: 'Log₁₀(parameters)', + color: activeTheme === 'light' ? '#616876' : '#b8bfcb', + font: { weight: '700' } + } + }, + y: { + min: Math.max(0, yMin), + max: yMax, + grid: { color: activeTheme === 'light' ? 'rgba(16,17,20,0.07)' : 'rgba(255,255,255,0.08)' }, + ticks: { + color: activeTheme === 'light' ? '#737a87' : '#aab1bf', + callback: v => `${Number(v).toFixed(0)}%` + }, + title: { + display: true, + text: 'Score (%)', + color: activeTheme === 'light' ? '#616876' : '#b8bfcb', + font: { weight: '700' } + } + } + } + }, + plugins: [residualLabelPlugin] + }); + } + + document.getElementById('searchBox').addEventListener('input', (e) => { + activeSearch = e.target.value || ''; + render(); + }); + + document.getElementById('predictBtn').addEventListener('click', updatePredictionPanel); + document.getElementById('paramInput').addEventListener('input', updatePredictionPanel); + + document.getElementById('modeDesktop').addEventListener('click', () => applyMode('desktop')); + document.getElementById('modeMobile').addEventListener('click', () => applyMode('mobile')); + + document.getElementById('orgSearch').addEventListener('input', (e) => { + activeOrgSearch = e.target.value || ''; + renderOrgUI(); + }); + + document.getElementById('clearOrg').addEventListener('click', () => { + activeOrg = 'all'; + activeOrgSearch = ''; + document.getElementById('orgSearch').value = ''; + renderOrgUI(); + render(); + }); + + const residualBtn = document.getElementById('toggleOutliers'); + residualBtn.addEventListener('click', () => { + showResidualLabels = !showResidualLabels; + setChipState(residualBtn, showResidualLabels); + render(); + }); + + const lineBtn = document.getElementById('toggleLineOnly'); + lineBtn.addEventListener('click', () => { + lineEmphasis = !lineEmphasis; + setChipState(lineBtn, lineEmphasis); + render(); + }); + + document.getElementById('themeLight').addEventListener('click', () => applyTheme('light')); + document.getElementById('themeDark').addEventListener('click', () => applyTheme('dark')); + + const savedTheme = localStorage.getItem('silverRegressionTheme'); + if (savedTheme === 'dark' || savedTheme === 'light') { + activeTheme = savedTheme; + } + document.body.dataset.theme = activeTheme; + setChipState(document.getElementById('themeLight'), activeTheme === 'light'); + setChipState(document.getElementById('themeDark'), activeTheme === 'dark'); + + const savedMode = localStorage.getItem('silverRegressionMode'); + if (savedMode === 'mobile' || savedMode === 'desktop') { + activeMode = savedMode; + } + document.body.dataset.mode = activeMode; + setChipState(document.getElementById('modeDesktop'), activeMode === 'desktop'); + setChipState(document.getElementById('modeMobile'), activeMode === 'mobile'); + + renderBenchmarkButtons(); + renderBucketUI(); + renderOrgUI(); + updatePredictionPanel(); + render(); + + +