RRosternomics

Giants SFG

President of baseball ops: Buster Posey · GM: Zack Minasian

Overview

FORCED-SPEND ↘ cooling
expensive & losing · year 1 in this regime · payroll 1.26× league · TAB -11.5
-11.5TAB — talent above budget27th/30
-10.9WAB — realized wins (TAB + luck)29th/30
3.0depth — 2+ WAR regulars19th/30
-4.3dead WAR — sub-replacement drag18th/30
25%dead money %6th/30
0.417win %26th/30
25.5roster fWAR26th/30
-40surplus $M (value vs cost)25th/30
$200Mpayroll $M10th/30luxury tax $186M

Roster

CFDrew Gilbert 40GHarrison Bader 30GJonah Cox 11G
LFHeliot Ramos 44GCasey Schmitt 19GDrew Gilbert 13G
RFJung Hoo Lee 59GVictor Bericoto 11GJerar Encarnacion 8G
SSWilly Adames 67GCasey Schmitt 4GChristian Koss 1G
2BLuis Arraez 64GChristian Koss 4GCasey Schmitt 4G
3BMatt Chapman 67GCasey Schmitt 3GBuddy Kennedy 2G
1BRafael Devers 52GCasey Schmitt 14GBryce Eldridge 5G
STARTERSLanden Roupp 2.3WARLogan Webb 2.1WARTrevor McDonald 0.7WARRobbie Ray 0.7WARTyler Mahle 0.6WAR
CDaniel Susac 24GEric Haase 14GJesus Rodriguez 9G
DHBryce Eldridge 24GCasey Schmitt 23GRafael Devers 17G
BULLPEN Ryan Walker 3svKeaton Winn 11hErik Miller 8hMatt Gage 5hCaleb Kilian 3hJT Brubaker 1hRyan Borucki 1hSam Hentges 1h
CDaniel Susac 24 · Eric Haase 14 · Jesus Rodriguez 9
1BRafael Devers 52 · Casey Schmitt 14 · Bryce Eldridge 5
2BLuis Arraez 64 · Christian Koss 4 · Casey Schmitt 4
3BMatt Chapman 67 · Casey Schmitt 3 · Buddy Kennedy 2
SSWilly Adames 67 · Casey Schmitt 4 · Christian Koss 1
LFHeliot Ramos 44 · Casey Schmitt 19 · Drew Gilbert 13
CFDrew Gilbert 40 · Harrison Bader 30 · Jonah Cox 11
RFJung Hoo Lee 59 · Victor Bericoto 11 · Jerar Encarnacion 8
DHBryce Eldridge 24 · Casey Schmitt 23 · Rafael Devers 17
SPLanden Roupp 2.3 · Logan Webb 2.1 · Trevor McDonald 0.7 · Robbie Ray 0.7 · Tyler Mahle 0.6
BP Ryan Walker 3 · Keaton Winn 11 · Erik Miller 8 · Matt Gage 5 · Caleb Kilian 3 · JT Brubaker 1 · Ryan Borucki 1 · Sam Hentges 1

Each position lists its starter then backups by games (so platoons & bench show). Starters (mound) & bullpen (left, foul territory) are ranked by WAR; marks the closer (most save chances). MLB Stats API.

2026 roster — value & surplus

PlayerAgeServiceGfWARSalary $MSurplus $M
Luis Arraez 29 FA-elig 975.7 12.0 +23.7
Landen Roupp 27 pre-arb 203.6 0.8 +21.9
Logan Webb 29 FA-elig 183.3 23.0 -2.1
Casey Schmitt 27 pre-arb 953.0 0.8 +17.9
Rafael Devers 29 FA-elig 1032.2 27.0 -13.4
Matt Chapman 33 FA-elig 842.1 25.2 -12.2
Jung Hoo Lee 27 951.4 23.2 -14.3
Bryce Eldridge 21 pre-arb 611.3 0.6 +7.9
Daniel Susac 25 411.2 0.8 +6.9
Trevor McDonald 25 pre-arb 141.1 0.6 +6.1
Robbie Ray 34 FA-elig 201.0 25.0 -18.4
Tyler Mahle 31 FA-elig 161.0 10.0 -3.6
Heliot Ramos 26 pre-arb 650.9 0.8 +5.0
Keaton Winn 28 pre-arb 340.7 0.8 +3.7
Drew Gilbert 25 pre-arb 810.6 0.7 +3.3
Caleb Kilian 29 pre-arb 420.4 0.8 +1.9
Jonah Cox 24 240.4 0.5 +2.2
Erik Miller 28 pre-arb 350.4 0.8 +1.8
Sam Hentges 29 arb 270.4 1.4 +0.8
Dylan Smith 26 pre-arb 180.3 0.0 +1.7
Victor Bericoto 24 240.3 0.5 +1.1
JT Brubaker 32 arb 310.2 1.8 -0.2
Willy Adames 30 FA-elig 980.2 13.1 -11.9
Blade Tidwell 25 pre-arb 80.2 0.1 +1.1
Tristan Beck 30 pre-arb 80.1 0.0 +0.4
Spencer Bivens 32 pre-arb 20.0 0.8 -0.5
Joel Peguero 29 pre-arb 110.0 0.8 -0.5
Carson Whisenhunt 25 pre-arb 20.0 0.8 -0.6
Drew Cavanaugh 24 170.0
Logan Porter↔ ANA 30 pre-arb 10.0 0.2 -0.2
Gregory Santos 26 arb 3-0.0 0.1 -0.2
Wilkin Ramos 25 2-0.1 0.3 -0.8
Jesus Rodriguez 24 17-0.1 0.6 -1.1
Jason Foley 30 arb 3-0.1 2.0 -2.5
Jared Oliva 30 pre-arb 7-0.1 0.8 -1.4
Grant McCray 25 pre-arb 6-0.1 0.8 -1.5
Carson Seymour 27 pre-arb 2-0.1 0.8 -1.7
Ryan Borucki 32 FA-elig 21-0.2 1.5 -2.5
Buddy Kennedy↔ SEA 27 7-0.2 0.3 -1.4
Patrick Bailey↔ CLE 27 30-0.2 0.3 -1.8
Eric Haase 33 arb 29-0.3 1.4 -3.1
José Buttó 28 pre-arb 3-0.3 0.8 -2.6
Adrian Houser 33 FA-elig 19-0.3 7.0 -8.8
Christian Koss 28 pre-arb 14-0.3 0.2 -2.1
Reiver Sanmartin 30 pre-arb 2-0.5 0.8 -4.0
Ryan Walker 30 pre-arb 27-0.5 0.2 -3.6
Will Brennan 28 pre-arb 11-0.7 0.8 -5.2
Harrison Bader 32 FA-elig 30-0.7 7.5 -12.0
Jerar Encarnacion 28 17-0.7 0.2 -4.7
Matt Gage 33 pre-arb 34-1.2 0.8 -8.5

Key additions (new vs 2025)

PlayerfWAR
Luis Arraez3.6
Daniel Susac0.8
Tyler Mahle0.6
Caleb Kilian0.3
Sam Hentges0.2
Dylan Smith0.2

Key subtractions (gone from 2025)

Player2025 fWAR
Justin Verlander2.2
Mike Yastrzemski1.0
Tyler Rogers0.9
Jordan Hicks0.7
Camilo Doval0.6
Dominic Smith0.4

Current & Historical Payroll

2026 payroll
$200.3M
luxury tax $186M
2027 guaranteed
$131.8M

What the front office was committed to as of 2026, 2026–2032. Guaranteed $ are contract commitments signed by 2026; arbitration & free-agent years are projected from service time. Hover a figure for its luxury-tax (CBT) & actual-cash values.

current $ guaranteed $ arbitration (proj) pre-arb (proj) free agent club option player option mutual option vesting option

Summary — luxury-tax (CBT) basis

2026202720282029203020312032
Tax threshold$244M$247M$250M$253M$256M$259M$262M
Guaranteed contracts$172M$120M$109M$80M$80M$55M
Proj. arbitration$2M
Proj. pre-arbitration$12M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$204M$138M$127M$98M$98M$73M
Tax space+$40M+$109M+$123M+$155M+$158M+$186M
Est. tax bill

Luxury-tax (CBT) basis: contract AAV + ~$18M benefits/bonus-pool — distinct from the base-salary payroll above. Threshold is the CBA line. Tier-1 payor rates on the overage. Forward years show only guaranteed deals (arb/pre-arb not yet set). Figures are estimates.

Guaranteed / signed (10)

PlayerAgeSvc2026202720282029203020312032
Rafael Devers298.070$27.0M$28.5M$28.5M$28.5M$28.5M$28.5M$28.5M
Matt Chapman338.109$25.2M$25.2M$25.2M$25.2M$25.2MFREE AGENT
Robbie Ray3411.007$25.0MFREE AGENT
Logan Webb296.044$23.0M$23.0M$24.0MFREE AGENT
Willy Adames307.105$13.1M$31.1M$31.1M$31.1M$31.1M$31.1MFREE AGENT
Luis Arraez296.121$12.0MFREE AGENT
Tyler Mahle318.018$10.0MFREE AGENT
Harrison Bader328.030$7.5M$13.0MFREE AGENT
Adrian Houser336.136$7.0M$11.0M$12.5MCLUBFREE AGENT
Ryan Borucki326.168$1.5MFREE AGENT

Arbitration-eligible (5)

PlayerAgeSvc2026202720282029203020312032
Jason Foley303.150$2.0MARB 2ARB 3FREE AGENT
JT Brubaker325.162$1.8MFREE AGENT
Sam Hentges294.157$1.4MARB 3FREE AGENT
Eric Haase334.114$1.4MARB 3FREE AGENT
Gregory Santos263.055$83kARB 2ARB 3FREE AGENT

Pre-arbitration (30)

PlayerAgeSvc2026202720282029203020312032
Jung Hoo Lee272.000$23.2MARB 1ARB 2ARB 3FREE AGENT
Caleb Kilian290.153$850kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Matt Gage330.155$850kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jared Oliva300.050$850kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Spencer Bivens321.077$820kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Grant McCray250.096$820kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Carson Whisenhunt250.054$820kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Carson Seymour270.074$820kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Heliot Ramos262.103$810kARB 1ARB 2ARB 3FREE AGENT
Erik Miller282.000$810kARB 1ARB 2ARB 3FREE AGENT
Casey Schmitt272.035$810kARB 1ARB 2ARB 3FREE AGENT
Landen Roupp271.085$800kPre-ArbARB 1ARB 2ARB 3FREE AGENT
José Buttó281.168$800kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Keaton Winn281.090$800kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Reiver Sanmartin301.148$800kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Joel Peguero290.039$785kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Daniel Susac25$780k
Will Brennan282.155$762kARB 1ARB 2ARB 3FREE AGENT
Drew Gilbert250.052$724kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Bryce Eldridge210.014$609kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Trevor McDonald250.014$596kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Buddy Kennedy271.131$287kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Patrick Bailey272.136$262kARB 1ARB 2ARB 3FREE AGENT
Christian Koss281.000$222kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ryan Walker302.136$204kARB 1ARB 2ARB 3FREE AGENT
Jerar Encarnacion281.112$175kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Logan Porter300.021$152kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Blade Tidwell250.008$113kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tristan Beck302.067$22kARB 1ARB 2ARB 3FREE AGENT
Dylan Smith260.020$4kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Giants 2026; the black line = the league-average player at each slot that season. Staying above the line deep into the roster is the depth edge; dropping below it = holes & dead weight.

What each regime is worth — every settled team-season since 1985

RegimeSeasonsWin %Playoff %Pennant %WS %
WINDOW 28952.230.14.81.0
CONTEND 31156.863.019.310.3
FORCED-SPEND 26047.98.81.21.2
REBUILD 32843.31.20.30.3
const FONT={family:"'Roboto Mono', monospace",size:12}; const ORBIT = [[1985, 0.732, -8.4], [1986, 0.746, 9.11], [1987, 0.749, 11.31], [1988, 1.047, 6.8], [1989, 1.215, 4.56], [1990, 1.22, -5.43], [1991, 1.373, -9.0], [1992, 0.987, -9.14], [1993, 1.112, 12.0], [1994, 1.249, -7.72], [1995, 1.091, -4.25], [1996, 1.104, -11.69], [1997, 1.002, 3.54], [1998, 1.042, 8.19], [1999, 0.984, -1.83], [2000, 0.992, 16.98], [2001, 1.045, 12.76], [2002, 1.2, 17.29], [2003, 1.193, 7.89], [2004, 1.074, 5.07], [2005, 1.28, -12.51], [2006, 1.226, -7.78], [2007, 0.995, -7.63], [2008, 0.82, -6.23], [2009, 0.907, 3.9], [2010, 1.201, 8.01], [2011, 1.361, -2.59], [2012, 1.219, 3.27], [2013, 1.384, -3.96], [2014, 1.39, -3.08], [2015, 1.4, -0.04], [2016, 1.406, -0.55], [2017, 1.443, -18.77], [2018, 1.553, -18.08], [2019, 1.347, -20.49], [2021, 1.291, 17.48], [2022, 1.081, 0.82], [2023, 1.145, -7.19], [2024, 1.186, -6.07], [2025, 0.975, -1.75], [2026, 1.255, -11.46]], TEAMCOL = '#FD5A1E'; function contrastColor(hex,dark){let r=parseInt(hex.slice(1,3),16)/255,g=parseInt(hex.slice(3,5),16)/255,b=parseInt(hex.slice(5,7),16)/255; const mx=Math.max(r,g,b),mn=Math.min(r,g,b);let h=0,s=0,l=(mx+mn)/2; if(mx!==mn){const d=mx-mn;s=l>0.5?d/(2-mx-mn):d/(mx+mn);h=mx===r?(g-b)/d+(g1)t-=1;if(t<1/6)return p+(q-p)*6*t;if(t<0.5)return q;if(t<2/3)return p+(q-p)*(2/3-t)*6;return p;} let R,G,B;if(s===0){R=G=B=L;}else{const q=L<0.5?L*(1+s):L+s-L*s,p=2*L-q;R=h2(p,q,h+1/3);G=h2(p,q,h);B=h2(p,q,h-1/3);} const x=v=>('0'+Math.round(v*255).toString(16)).slice(-2);return '#'+x(R)+x(G)+x(B);} const CURRENT_YEAR = 2026; function drawOrbit(){ const orbEl=document.getElementById('orbit'); if(!ORBIT || ORBIT.length<2){ orbEl.innerHTML='

Not enough history to chart.

'; return; } const f=+document.getElementById('yfrom').value, t=+document.getElementById('yto').value, w=+document.getElementById('win').value; document.getElementById('yflab').textContent=f; document.getElementById('ytlab').textContent=t; document.getElementById('winlab').textContent = w===1?'raw':(w+'-yr'); const dark=document.documentElement.getAttribute('data-theme')==='dark'; const ink=dark?'#cdd4dc':'#1d2630', grid=dark?'#2c313a':'#d6dbe2', refc=dark?'#5a636d':'#9aa3ad', dirc=dark?'#ffffff':'#1d2630'; const col=contrastColor(TEAMCOL,dark); function sm(a,w){const h=(w-1)/2,o=[];for(let i=0;id[0]>=f&&d[0]<=t); if(raw.length<2){ Plotly.purge('orbit'); orbEl.innerHTML='

Pick a wider year range.

'; return; } const s=sm(raw,w),n=s.length,X=s.map(d=>Math.round(d[1]*100)/100),Y=s.map(d=>Math.round(d[2]*10)/10),yr=s.map(d=>d[0]); let xa=Math.min(1,...X),xb=Math.max(1,...X),ya=Math.min(0,...Y),yb=Math.max(0,...Y); const px=(xb-xa)*.12+.04,py=(yb-ya)*.12+1.5;xa-=px;xb+=px;ya-=py;yb+=py; const k=Math.max(0,n-3), ann=[]; [['WINDOW','#15b6a0',xa<1&&yb>0,xa,yb,'left','top'],['CONTEND','#2f7ed8',xb>1&&yb>0,xb,yb,'right','top'], ['FORCED-SPEND','#ec5a3c',xb>1&&ya<0,xb,ya,'right','bottom'],['REBUILD','#f0a91c',xa<1&&ya<0,xa,ya,'left','bottom']].forEach(q=>{ if(q[2]) ann.push({x:q[3],y:q[4],xref:'x',yref:'y',text:q[0],showarrow:false,xanchor:q[5],yanchor:q[6],font:{size:10,color:q[1]},opacity:.85});}); ann.push({x:X[n-1],y:Y[n-1],ax:X[n-1]-0.5*(X[n-1]-X[k]),ay:Y[n-1]-0.5*(Y[n-1]-Y[k]),xref:'x',yref:'y',axref:'x',ayref:'y',showarrow:true,arrowhead:2,arrowsize:1.3,arrowwidth:1.7,arrowcolor:dirc,text:''}); Plotly.newPlot('orbit',[{x:X,y:Y,mode:'lines+markers',line:{color:col,width:2.6,shape:'spline',smoothing:0.7}, marker:{color:col,size:s.map((d,i)=>5+7*(i/Math.max(1,n-1))),line:{color:dark?'#1b1e23':'#fff',width:0.6}}, text:yr,hovertemplate:'%{text}
payroll %{x:.2f}× lg · TAB %{y:+.1f}
'+(w===1?'single season (raw)':w+'-yr smoothed avg')+''}], {margin:{l:50,r:14,t:10,b:40},paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',font:FONT, xaxis:{title:{text:'relative payroll (× lg avg)',font:{size:11}},range:[xa,xb],gridcolor:grid,zeroline:false}, yaxis:{title:{text:'TAB (talent above budget)',font:{size:11}},range:[ya,yb],gridcolor:grid,zeroline:false}, shapes:[{type:'line',x0:1,x1:1,y0:ya,y1:yb,line:{color:refc,width:1,dash:'dot'}},{type:'line',x0:xa,x1:xb,y0:0,y1:0,line:{color:refc,width:1,dash:'dot'}}], annotations:ann},{displayModeBar:false,responsive:true}); } (function initOrbitSliders(){ const yf=document.getElementById('yfrom'), yt=document.getElementById('yto'); if(ORBIT && ORBIT.length>=2){ const yrs=ORBIT.map(d=>d[0]), y0=Math.min(...yrs), y1=Math.max(...yrs); yf.min=y0; yf.max=y1; yf.value=y0; yt.min=y0; yt.max=y1; yt.value=Math.max(y0, Math.min(CURRENT_YEAR, y1)); yf.addEventListener('input',()=>{ if(+yf.value>+yt.value) yt.value=yf.value; drawOrbit(); }); yt.addEventListener('input',()=>{ if(+yt.value<+yf.value) yf.value=yt.value; drawOrbit(); }); document.getElementById('win').addEventListener('input',drawOrbit); } // only paint on load if the orbit pane is the visible default (else it'd draw into a 0-width hidden box) const _op=document.querySelector('.gpane[data-g="orbit"]'); if(!_op || _op.style.display!=='none') drawOrbit(); })(); new MutationObserver(()=>{ const p=document.querySelector('.gpane[data-g="orbit"]'); if(!p || p.style.display!=='none') drawOrbit(); }).observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']}); // ---- roster depth curve (WAR by roster slot vs league-average-by-slot) ---- const WARDIST = {"cats": ["IF", "SP", "SP", "IF", "IF", "IF", "OF", "IF", "C", "SP", "SP", "SP", "OF", "RP", "OF", "RP", "OF", "RP", "RP", "RP", "OF", "RP", "IF", "RP", "RP", "RP", "RP", "RP", "C", "C", "RP", "RP", "C", "RP", "OF", "OF", "RP", "RP", "IF", "C"], "league": [3.73, 2.8, 2.36, 2.09, 1.83, 1.64, 1.41, 1.24, 1.09, 0.96, 0.87, 0.78, 0.68, 0.62, 0.56, 0.49, 0.42, 0.36, 0.3, 0.22, 0.19, 0.16, 0.12, 0.1, 0.08, 0.06, 0.04, 0.03, 0.01, -0.01, -0.03, -0.04, -0.07, -0.09, -0.1, -0.12, -0.15, -0.18, -0.22, -0.25], "names": ["Luis Arraez", "Landen Roupp", "Logan Webb", "Casey Schmitt", "Rafael Devers", "Matt Chapman", "Jung Hoo Lee", "Bryce Eldridge", "Daniel Susac", "Trevor McDonald", "Robbie Ray", "Tyler Mahle", "Heliot Ramos", "Keaton Winn", "Drew Gilbert", "Caleb Kilian", "Jonah Cox", "Erik Miller", "Sam Hentges", "Dylan Smith", "Victor Bericoto", "JT Brubaker", "Willy Adames", "Blade Tidwell", "Tristan Beck", "Spencer Bivens", "Joel Peguero", "Carson Whisenhunt", "Drew Cavanaugh", "Logan Porter", "Gregory Santos", "Wilkin Ramos", "Jesus Rodriguez", "Jason Foley", "Jared Oliva", "Grant McCray", "Carson Seymour", "Ryan Borucki", "Buddy Kennedy", "Patrick Bailey"], "ranks": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40], "team": [3.61, 2.3, 2.11, 1.89, 1.37, 1.31, 0.91, 0.86, 0.78, 0.67, 0.66, 0.65, 0.58, 0.45, 0.4, 0.28, 0.27, 0.27, 0.22, 0.17, 0.16, 0.16, 0.12, 0.12, 0.05, 0.03, 0.03, 0.02, 0.02, 0.0, -0.01, -0.05, -0.05, -0.05, -0.06, -0.07, -0.09, -0.1, -0.11, -0.15]}; let WD_MODE='raw', WC_MODE='team'; const POSCOL={SP:'#2f7ed8',RP:'#6fb1ec',IF:'#f0a91c',OF:'#15b6a0',C:'#ec5a3c'}; const POSLAB={SP:'Starting P',RP:'Bullpen',IF:'Infield',OF:'Outfield',C:'Catcher'}; function buildPosLegend(){ const el=document.getElementById('poslegend'); if(!el) return; el.style.display=WC_MODE==='pos'?'flex':'none'; el.innerHTML=['SP','RP','C','IF','OF'].map(k=>''+POSLAB[k]+'').join(''); } function drawWarDist(){ if(!WARDIST) return; const dark=document.documentElement.getAttribute('data-theme')==='dark'; const col=contrastColor(TEAMCOL,dark), line=dark?'#e8eef0':'#111', grid=dark?'#2c313a':'#d6dbe2', ink=dark?'#cdd4dc':'#1d2630'; let team=WARDIST.team.slice(), league=WARDIST.league.slice(); if(WD_MODE==='prop'){ const ts=team.reduce((a,b)=>a+Math.max(b,0),0)||1, ls=league.reduce((a,b)=>a+Math.max(b,0),0)||1; team=team.map(v=>v/ts); league=league.map(v=>v/ls); } const fmt=WD_MODE==='prop'?'.1%':'.1f'; const names=WARDIST.names||[], cats=WARDIST.cats||[]; const cd=WARDIST.ranks.map((_,i)=>[names[i]||'(empty slot)', WARDIST.team[i], POSLAB[cats[i]]||'']); const barcol=WC_MODE==='pos'?cats.map(c=>POSCOL[c]||'#9aa3ad'):col; const posbit=WC_MODE==='pos'?' · %{customdata[2]}':''; const htmpl=WD_MODE==='prop' ? '%{customdata[0]}'+posbit+'
slot %{x} · %{customdata[1]:.1f} WAR
%{y:.1%} of roster WAR' : '%{customdata[0]}'+posbit+'
slot %{x} · %{y:.1f} WAR'; buildPosLegend(); const _div=document.getElementById('wardist'); const traces=[ {type:'bar',x:WARDIST.ranks,y:team,name:'SFG',marker:{color:barcol},customdata:cd,hovertemplate:htmpl}, {type:'scatter',x:WARDIST.ranks,y:league,name:'league avg',mode:'lines',line:{color:line,width:2,shape:'spline',smoothing:0.6},hovertemplate:'league slot %{x}: %{y:'+fmt+'}'} ]; const layout={margin:{l:48,r:12,t:6,b:38},paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',font:FONT, transition:{duration:360,easing:'cubic-in-out'}, xaxis:{title:{text:'roster slot (ranked by WAR)',font:{size:11}},gridcolor:grid,zeroline:false,color:ink}, yaxis:{title:{text:WD_MODE==='prop'?'share of roster WAR':'WAR',font:{size:11}},gridcolor:grid,zeroline:true,zerolinecolor:grid,color:ink,tickformat:WD_MODE==='prop'?'.0%':undefined}, showlegend:true,legend:{orientation:'h',y:1.13,x:0,font:{size:11}},bargap:0.14}; if(_div.data&&_div.data.length){ Plotly.react(_div,traces,layout,{displayModeBar:false,responsive:true}); } else { Plotly.newPlot(_div,traces,layout,{displayModeBar:false,responsive:true}); } } window.wdMode=function(m){WD_MODE=m;document.getElementById('wd-raw').classList.toggle('on',m==='raw');document.getElementById('wd-prop').classList.toggle('on',m==='prop');drawWarDist();}; window.wcToggle=function(){WC_MODE=WC_MODE==='pos'?'team':'pos';const b=document.getElementById('wc-btn');b.classList.toggle('on',WC_MODE==='pos');b.textContent=WC_MODE==='pos'?'Show team color':'Color by position';drawWarDist();}; // ---- Graphs dropdown switcher (orbit ⇄ roster value distribution) ---- let _wdRendered=false; window.showTeamGraph=function(g){ document.querySelectorAll('.gpane').forEach(p=>{ p.style.display=(p.dataset.g===g)?'':'none'; }); if(g==='orbit'){ drawOrbit(); } else if(g==='roster'){ if(!_wdRendered){ drawWarDist(); _wdRendered=true; } else { try{ Plotly.Plots.resize('wardist'); }catch(e){ drawWarDist(); } } } }; { const _ts=document.getElementById('tgsel'); if(_ts) _ts.addEventListener('change',()=>showTeamGraph(_ts.value)); } // initial paint of the default pane (roster when the team has roster data, else orbit already drawn above) if(WARDIST){ drawWarDist(); _wdRendered=true; } // redraw the roster pane on theme change only if it's been rendered and is visible if(WARDIST){ new MutationObserver(()=>{ const p=document.querySelector('.gpane[data-g="roster"]'); if(_wdRendered && p && p.style.display!=='none') drawWarDist(); }).observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']}); } // ---- TOC scroll-spy ---- (function(){ const links=[...document.querySelectorAll('.toc a')]; const secs=links.map(a=>document.querySelector(a.getAttribute('href'))).filter(Boolean); links.forEach(a=>a.addEventListener('click',e=>{e.preventDefault();document.querySelector(a.getAttribute('href')).scrollIntoView({behavior:'smooth'});})); if('IntersectionObserver' in window && secs.length){ const obs=new IntersectionObserver(es=>{es.forEach(e=>{if(e.isIntersecting) links.forEach(l=>l.classList.toggle('on',l.getAttribute('href')==='#'+e.target.id));});},{rootMargin:'-15% 0px -75% 0px'}); secs.forEach(s=>obs.observe(s)); } })(); // ---- sortable roster table ---- const SVC_RANK={'pre-arb':0,'arb':1,'FA-elig':2,'?':3}; function cellNum(td){const n=parseFloat(td.textContent.replace(/[^0-9.\-]/g,''));return isNaN(n)?null:n;} function scale(v,mn,mx,g){ if(g==='diverge'){ if(v>=0){const t=mx>0?v/mx:0;return `rgba(26,125,90,${(0.10+0.55*t).toFixed(2)})`;} const t=mn<0?v/mn:0;return `rgba(192,73,43,${(0.10+0.55*t).toFixed(2)})`; } if(g==='salmag'){const t=mx>mn?(v-mn)/(mx-mn):0;return `rgba(128,135,143,${(0.05+0.45*t).toFixed(2)})`;} const t=mx>mn?(v-mn)/(mx-mn):0;return `rgba(26,125,90,${(0.05+0.55*t).toFixed(2)})`; } function applyGradients(table){ [...table.tHead.rows[0].cells].forEach((th,ci)=>{ if(!th.dataset.grad) return; const rows=[...table.tBodies[0].rows], vals=rows.map(r=>cellNum(r.cells[ci])).filter(v=>v!==null); if(!vals.length) return; const mn=Math.min(...vals),mx=Math.max(...vals); rows.forEach(r=>{const v=cellNum(r.cells[ci]);if(v!==null)r.cells[ci].style.background=scale(v,mn,mx,th.dataset.grad);}); }); } function makeSortable(table){ const ths=[...table.tHead.rows[0].cells]; ths.forEach((th,ci)=>{ th.dataset.label=th.textContent.trim(); th.addEventListener('click',()=>{ th._dir=(th._dir===-1)?1:-1; const dir=th._dir, tb=table.tBodies[0], svc=th.dataset.sortType==='service'; [...tb.rows].sort((a,b)=>{ let va,vb; if(svc){va=SVC_RANK[a.cells[ci].textContent.trim()]??9;vb=SVC_RANK[b.cells[ci].textContent.trim()]??9;} else{va=cellNum(a.cells[ci]);vb=cellNum(b.cells[ci]); if(va===null||vb===null){va=a.cells[ci].textContent.trim().toLowerCase();vb=b.cells[ci].textContent.trim().toLowerCase();}} return vavb?dir:0; }).forEach(r=>tb.appendChild(r)); ths.forEach(h=>h.innerHTML=h.dataset.label); th.innerHTML=th.dataset.label+' '+(dir===1?'▲':'▼')+''; }); }); } document.querySelectorAll('table.sortable').forEach(t=>{applyGradients(t);makeSortable(t);});