ai-research-survey

Systematic scan of agentic development research. What's signal, what's noise.
git clone https://git.shiptheloop.com/ai-research-survey.git
Log | Files | Refs

commit 4b1edc22cbf261dbc4f797132d00a2067c22d276
parent 8378a8226cb32ffa456ceaab94abc9b42ccb619b
Author: Brian Graham <brian@buildingbetterteams.de>
Date:   Mon, 23 Mar 2026 11:19:23 +0100

Replace jittered scatter with bubble grid for two cultures

Scatter with jitter was dishonest — data is discrete (4-7 questions
per category = quantized scores). Replaced with bubble grid: each
grid intersection shows a circle sized by paper count, colored by
mean score, with count label. Honestly represents the discrete data
while clearly showing the negative correlation pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Diffstat:
Mexplorer/src/views/findings.ts | 98+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
1 file changed, 56 insertions(+), 42 deletions(-)

diff --git a/explorer/src/views/findings.ts b/explorer/src/views/findings.ts @@ -580,73 +580,87 @@ function renderTwoCultures(f: Findings): string { const papers = (f as any).two_cultures as { human_studies: number; artifacts: number; id: string; score: number }[]; if (!papers || papers.length < 10) return ''; - // Compute quadrant counts - const q = { hh: 0, hl: 0, lh: 0, ll: 0 }; + // Bucket into grid cells (scores are quantized to ~25% steps) + const steps = [0, 25, 50, 75, 100]; + function snap(v: number): number { + let best = 0; + for (const s of steps) { + if (Math.abs(v - s) < Math.abs(v - best)) best = s; + } + return best; + } + + const grid = new Map<string, { count: number; totalScore: number }>(); for (const p of papers) { - const hs = p.human_studies >= 50; - const ar = p.artifacts >= 50; - if (hs && ar) q.hh++; - else if (hs && !ar) q.hl++; - else if (!hs && ar) q.lh++; - else q.ll++; + const ax = snap(p.artifacts); + const hy = snap(p.human_studies); + const key = `${ax},${hy}`; + const cell = grid.get(key) || { count: 0, totalScore: 0 }; + cell.count++; + cell.totalScore += p.score; + grid.set(key, cell); } - // SVG scatter - const w = 500, h = 400; - const pad = { l: 60, r: 20, t: 20, b: 50 }; + const maxCount = Math.max(...Array.from(grid.values()).map(c => c.count)); + + // SVG bubble grid + const w = 420, h = 420; + const pad = { l: 70, r: 30, t: 30, b: 60 }; const cw = w - pad.l - pad.r, ch = h - pad.t - pad.b; const xScale = (v: number) => pad.l + (v / 100) * cw; const yScale = (v: number) => pad.t + ch - (v / 100) * ch; - // Deterministic jitter to spread overlapping dots on the quantized grid - function jitter(idx: number, axis: number): number { - // Simple hash-like spread: use index to distribute dots in a circle around the grid point - const angle = ((idx * 137.5 + axis * 73) % 360) * Math.PI / 180; - const radius = 6 + (idx * 3.7 % 5); - return Math.cos(angle) * radius; + // Grid lines + let svgGrid = ''; + for (const v of steps) { + svgGrid += `<line x1="${xScale(v)}" y1="${pad.t}" x2="${xScale(v)}" y2="${h - pad.b}" stroke="var(--border)" stroke-width="0.5"/>`; + svgGrid += `<line x1="${pad.l}" y1="${yScale(v)}" x2="${w - pad.r}" y2="${yScale(v)}" stroke="var(--border)" stroke-width="0.5"/>`; + svgGrid += `<text x="${pad.l - 8}" y="${yScale(v) + 4}" text-anchor="end" font-size="10">${v}%</text>`; + svgGrid += `<text x="${xScale(v)}" y="${h - pad.b + 16}" text-anchor="middle" font-size="10">${v}%</text>`; } - let dots = ''; - for (let i = 0; i < papers.length; i++) { - const p = papers[i]; - const cx = xScale(p.artifacts) + jitter(i, 0); - const cy = yScale(p.human_studies) + jitter(i, 1); - const color = p.score < 40 ? '#f06565' : p.score < 55 ? '#f0c050' : '#3dd68c'; - dots += `<circle cx="${cx}" cy="${cy}" r="5" fill="${color}" opacity="0.6"> - <title>${p.id}: artifacts ${p.artifacts}%, human_studies ${p.human_studies}%, score ${p.score}%</title> + // Bubbles + let bubbles = ''; + for (const [key, cell] of grid.entries()) { + const [ax, hy] = key.split(',').map(Number); + const r = Math.max(4, Math.sqrt(cell.count / maxCount) * 22); + const meanScore = cell.totalScore / cell.count; + const color = meanScore < 40 ? '#f06565' : meanScore < 55 ? '#f0c050' : '#3dd68c'; + bubbles += `<circle cx="${xScale(ax)}" cy="${yScale(hy)}" r="${r}" fill="${color}" opacity="0.7"> + <title>Artifacts ${ax}%, Human Studies ${hy}%\n${cell.count} papers, mean score ${meanScore.toFixed(1)}%</title> </circle>`; + if (cell.count > 1) { + bubbles += `<text x="${xScale(ax)}" y="${yScale(hy) + 4}" text-anchor="middle" font-size="9" fill="#fff" font-weight="600" style="pointer-events:none">${cell.count}</text>`; + } } - // Quadrant labels + // Quadrant shading const midX = xScale(50), midY = yScale(50); - const quadrants = ` - <line x1="${midX}" y1="${pad.t}" x2="${midX}" y2="${h - pad.b}" stroke="var(--border)" stroke-dasharray="4"/> - <line x1="${pad.l}" y1="${midY}" x2="${w - pad.r}" y2="${midY}" stroke="var(--border)" stroke-dasharray="4"/> - <text x="${xScale(25)}" y="${yScale(80)}" text-anchor="middle" font-size="10" fill="var(--text-dim)">CS tradition only (${q.hl})</text> - <text x="${xScale(75)}" y="${yScale(80)}" text-anchor="middle" font-size="10" fill="var(--green)">Both traditions (${q.hh})</text> - <text x="${xScale(25)}" y="${yScale(20)}" text-anchor="middle" font-size="10" fill="var(--red)">Neither (${q.ll})</text> - <text x="${xScale(75)}" y="${yScale(20)}" text-anchor="middle" font-size="10" fill="var(--text-dim)">Psych tradition only (${q.lh})</text> + const quadLabels = ` + <line x1="${midX}" y1="${pad.t}" x2="${midX}" y2="${h - pad.b}" stroke="var(--text-dim)" stroke-dasharray="4" opacity="0.4"/> + <line x1="${pad.l}" y1="${midY}" x2="${w - pad.r}" y2="${midY}" stroke="var(--text-dim)" stroke-dasharray="4" opacity="0.4"/> `; // Axes const axes = ` - <text x="${w / 2}" y="${h - 5}" text-anchor="middle" fill="var(--text-dim)" font-size="11">Artifacts Score \u2192</text> - <text x="12" y="${h / 2}" text-anchor="middle" fill="var(--text-dim)" font-size="11" transform="rotate(-90, 12, ${h / 2})">Human Studies Score \u2192</text> + <text x="${(pad.l + w - pad.r) / 2}" y="${h - 5}" text-anchor="middle" fill="var(--text-dim)" font-size="11">Artifacts Score \u2192</text> + <text x="14" y="${(pad.t + h - pad.b) / 2}" text-anchor="middle" fill="var(--text-dim)" font-size="11" transform="rotate(-90, 14, ${(pad.t + h - pad.b) / 2})">Human Studies Score \u2192</text> `; - // Grid - let grid = ''; - for (let v = 0; v <= 100; v += 25) { - grid += `<text x="${pad.l - 8}" y="${yScale(v) + 4}" text-anchor="end" font-size="10">${v}%</text>`; - grid += `<text x="${xScale(v)}" y="${h - pad.b + 15}" text-anchor="middle" font-size="10">${v}%</text>`; + // Quadrant summary + let qHigh = 0, qLow = 0; + for (const p of papers) { + if (p.human_studies >= 50 && p.artifacts >= 50) qHigh++; + if (p.human_studies < 50 && p.artifacts < 50) qLow++; } return `<div class="section"> <h2>Two Cultures</h2> - <p style="font-size:0.85rem;color:var(--text-dim);margin-bottom:0.5rem">Papers with human subjects (n=${papers.length}): human_studies score vs artifacts score. These two dimensions are <strong>negatively correlated</strong> (r=\u22120.24). CS-trained researchers release code but skip IRB; psychology-trained researchers do ethics review but don't release data.</p> + <p style="font-size:0.85rem;color:var(--text-dim);margin-bottom:0.5rem">Papers with human subjects (n=${papers.length}). Bubble size = paper count at that grid position. Color = mean score. These two dimensions are <strong>negatively correlated</strong> (r=\u22120.24).</p> <svg viewBox="0 0 ${w} ${h}" style="width:100%;max-width:${w}px"> - ${grid}${quadrants}${dots}${axes} + ${svgGrid}${quadLabels}${bubbles}${axes} </svg> + <p style="font-size:0.82rem;color:var(--text-dim);margin-top:0.5rem">Only <strong>${qHigh}</strong> papers score well on both dimensions. <strong>${qLow}</strong> score poorly on both. CS researchers release code but skip IRB; psychology-trained researchers do ethics review but don't release data.</p> </div>`; }

Impressum · Datenschutz