@@ -1655,6 +1655,7 @@ def build_interactive_report(
16551655 const featuresRows = chartData['_features_rows'] || [];
16561656 const teamDevPrs = chartData['_team_dev_prs'] || {{}};
16571657 const cycleTimePrs = chartData['_cycle_time_prs'] || {{}};
1658+ const velocityPrs = chartData['_velocity_prs'] || {{}};
16581659
16591660 tabOrder.forEach((key, i) => {{
16601661 const panel = document.createElement('div');
@@ -2120,6 +2121,14 @@ def build_interactive_report(
21202121 if (prs.length > 0) openCycleTimePrModal(scope, week, prs);
21212122 }});
21222123 }}
2124+ if (c._velocity_scope) {{
2125+ ch.on('click', function(params) {{
2126+ const week = params.name;
2127+ const scope = c._velocity_scope;
2128+ const prs = (velocityPrs[scope] || {{}})[week] || [];
2129+ if (prs.length > 0) openVelocityPrModal(scope, week, prs);
2130+ }});
2131+ }}
21232132 if (c.type === 'scatter' && c._pr_examples) {{
21242133 ch.on('click', function(params) {{
21252134 const linesChanged = params.data[0];
@@ -2434,6 +2443,70 @@ def build_interactive_report(
24342443 ddOverlay.classList.add('open');
24352444 }}
24362445
2446+ function openVelocityPrModal(scope, week, prs) {{
2447+ const ddOverlay = document.getElementById('drilldown-overlay');
2448+ const ddTitle = document.getElementById('dd-title');
2449+ const ddBody = document.getElementById('dd-body');
2450+ const weekDate = new Date(week + 'T00:00:00');
2451+ const weekFmt = weekDate.toLocaleDateString('en-US', {{month: 'short', day: 'numeric', year: 'numeric'}});
2452+ const totalCx = prs.reduce((s, p) => s + (p.complexity || 0), 0);
2453+ const devSet = new Set(prs.map(p => p.developer).filter(Boolean));
2454+ const perCapita = devSet.size > 0 ? (totalCx / devSet.size).toFixed(2) : '0';
2455+ const scopeLabel = scope === '_all' ? 'All Teams' : scope;
2456+ ddTitle.innerHTML = `Velocity Per Capita — ${{scopeLabel}} — week of ${{weekFmt}}<span class="dd-count">${{prs.length}} PR${{prs.length !== 1 ? 's' : ''}} · ${{devSet.size}} dev${{devSet.size !== 1 ? 's' : ''}} · total cx ${{totalCx}} · per-capita ${{perCapita}}</span>`;
2457+
2458+ const cxColor = (v) => v >= 8 ? '#991b1b' : v >= 5 ? '#92400e' : '#065f46';
2459+ const cxBg = (v) => v >= 8 ? '#fee2e2' : v >= 5 ? '#fef3c7' : '#d1fae5';
2460+
2461+ const sorted = prs.slice().sort((a, b) => (b.complexity || 0) - (a.complexity || 0));
2462+
2463+ let tableHtml = `<table class="dd-table">
2464+ <thead><tr>
2465+ <th>PR</th><th>Complexity</th><th>Developer</th><th>Team</th><th>Repo</th><th>Source</th><th>Link</th>
2466+ </tr></thead><tbody>`;
2467+
2468+ sorted.forEach(pr => {{
2469+ const title = pr.title || pr.url || '—';
2470+ const displayTitle = title.length > 60 ? title.slice(0, 60) + '…' : title;
2471+ const cx = pr.complexity || 0;
2472+ const cxBadge = `<span style="display:inline-block;font-size:0.7rem;font-family:'Syne',sans-serif;font-weight:600;padding:0.12rem 0.45rem;border-radius:4px;background:${{cxBg(cx)}};color:${{cxColor(cx)}}">${{cx}}</span>`;
2473+ const link = pr.url ? `<a href="${{pr.url}}" target="_blank" rel="noopener" style="color:var(--accent);font-size:0.85rem">→ Open PR</a>` : '—';
2474+ const dev = pr.developer || '—';
2475+ const team = pr.team || '—';
2476+
2477+ let repoName = '—';
2478+ let source = '—';
2479+ if (pr.url) {{
2480+ try {{
2481+ const url = new URL(pr.url);
2482+ if (url.hostname.includes('github.com')) {{
2483+ source = '<span style="display:inline-block;font-size:0.7rem;font-family:\\ 'Syne\\ ',sans-serif;font-weight:600;padding:0.12rem 0.45rem;border-radius:4px;background:#dbeafe;color:#1e40af">GitHub</span>';
2484+ const pathParts = url.pathname.split('/').filter(p => p);
2485+ if (pathParts.length >= 2) repoName = pathParts[1];
2486+ }} else if (url.hostname.includes('bitbucket.org')) {{
2487+ source = '<span style="display:inline-block;font-size:0.7rem;font-family:\\ 'Syne\\ ',sans-serif;font-weight:600;padding:0.12rem 0.45rem;border-radius:4px;background:#e0e7ff;color:#4338ca">Bitbucket</span>';
2488+ const pathParts = url.pathname.split('/').filter(p => p);
2489+ if (pathParts.length >= 2) repoName = pathParts[1];
2490+ }}
2491+ }} catch (e) {{}}
2492+ }}
2493+
2494+ tableHtml += `<tr>
2495+ <td class="cell-name" title="${{title}}"><span class="name-text">${{displayTitle}}</span></td>
2496+ <td>${{cxBadge}}</td>
2497+ <td style="font-size:0.85rem">${{dev}}</td>
2498+ <td style="font-size:0.85rem">${{team}}</td>
2499+ <td style="font-size:0.85rem">${{repoName}}</td>
2500+ <td>${{source}}</td>
2501+ <td>${{link}}</td>
2502+ </tr>`;
2503+ }});
2504+
2505+ tableHtml += '</tbody></table>';
2506+ ddBody.innerHTML = tableHtml;
2507+ ddOverlay.classList.add('open');
2508+ }}
2509+
24372510 function openScatterPrModal(linesChanged, complexity, prs) {{
24382511 const ddTitle = document.getElementById('dev-drill-title');
24392512 const ddBody = document.getElementById('dev-drill-body');
0 commit comments