RRosternomics

Athletics OAK

GM: David Forst

Overview

REBUILD ↘ cooling
cheap & losing · year 1 in this regime · payroll 0.58× league · TAB -8.4
-8.4TAB — talent above budget24th/30
-3.7WAB — realized wins (TAB + luck)22nd/30
2.0depth — 2+ WAR regulars25th/30
-7.7dead WAR — sub-replacement drag30th/30
56%dead money %27th/30
0.427win %25th/30
19.3roster fWAR28th/30
+27surplus $M (value vs cost)13th/30
$92Mpayroll $M22nd/30luxury tax $123M

Roster

CFLawrence Butler 26GHenry Bolte 26GDenzel Clarke 22G
LFTyler Soderstrom 64GCarlos Cortes 7GColby Thomas 2G
RFLawrence Butler 47GCarlos Cortes 37GColby Thomas 18G
SSJacob Wilson 39GDarell Hernaiz 27GAlika Williams 15G
2BJeff McNeil 59GDarell Hernaiz 11GZack Gelof 8G
3BZack Gelof 32GMax Muncy 26GDarell Hernaiz 12G
1BNick Kurtz 67GJonah Heim 1GAndy Ibáñez 1G
STARTERSJ.T. Ginn 1.2WARLuis Severino 0.6WARAaron Civale 0.2WARJacob Lopez -0.0WARJeffrey Springs -0.5WAR
CShea Langeliers 50GJonah Heim 21G
DHBrent Rooker 42GShea Langeliers 14GCarlos Cortes 9G
BULLPEN Hogan Harris 5svJustin Sterner 11hScott Barlow 9hMark Leiter Jr. 4hElvis Alvarado 4hLuis Medina 3hJack Perkins 2hJosé Suarez 1h
CShea Langeliers 50 · Jonah Heim 21
1BNick Kurtz 67 · Jonah Heim 1 · Andy Ibáñez 1
2BJeff McNeil 59 · Darell Hernaiz 11 · Zack Gelof 8
3BZack Gelof 32 · Max Muncy 26 · Darell Hernaiz 12
SSJacob Wilson 39 · Darell Hernaiz 27 · Alika Williams 15
LFTyler Soderstrom 64 · Carlos Cortes 7 · Colby Thomas 2
CFLawrence Butler 26 · Henry Bolte 26 · Denzel Clarke 22
RFLawrence Butler 47 · Carlos Cortes 37 · Colby Thomas 18
DHBrent Rooker 42 · Shea Langeliers 14 · Carlos Cortes 9
SPJ.T. Ginn 1.2 · Luis Severino 0.6 · Aaron Civale 0.2 · Jacob Lopez -0.0 · Jeffrey Springs -0.5
BP Hogan Harris 5 · Justin Sterner 11 · Scott Barlow 9 · Mark Leiter Jr. 4 · Elvis Alvarado 4 · Luis Medina 3 · Jack Perkins 2 · José Suarez 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
Nick Kurtz 23 pre-arb 924.7 0.8 +28.9
Shea Langeliers 28 arb 934.4 5.2 +22.6
Jacob Wilson 24 pre-arb 632.9 1.4 +16.6
Tyler Soderstrom 24 pre-arb 912.7 1.4 +15.7
Zack Gelof 26 pre-arb 672.7 0.7 +16.2
J.T. Ginn 27 pre-arb 221.9 0.8 +11.3
Gage Jump 23 111.5 0.5 +8.7
Joshua Kuroda-Grauer 23 161.5
Henry Bolte 22 601.1 0.6 +6.1
Luis Severino 32 FA-elig 121.0 25.0 -19.0
Mark Leiter Jr. 35 360.9 2.9 +3.0
Jonah Heim↔ ATL 31 arb 440.9 0.8 +4.7
Luis Medina 27 pre-arb 340.7 0.8 +3.7
Jack Perkins 26 pre-arb 250.6 0.7 +3.4
José Suarez↔ ATL 28 arb 260.6 0.5 +3.4
Darell Hernaiz 24 pre-arb 470.5 0.8 +2.6
Alika Williams 27 pre-arb 420.5 0.6 +2.7
Hogan Harris 29 pre-arb 490.5 0.8 +2.3
Joel Kuhnel↔ MIL 31 pre-arb 250.4 0.5 +1.9
Carlos Cortes 29 pre-arb 790.3 0.8 +1.3
Donovan Walton↔ ANA 32 70.3
Aaron Civale↔ CHC 31 FA-elig 160.3 5.6 -4.0
Tommy White 23 60.2
Brooks Kriske 32 pre-arb 20.1 0.6 +0.2
Brady Basso 28 pre-arb 70.1 0.1 +0.6
Michael Stefanic 30 20.1 0.0 +0.4
Hayden Juenger 25 10.1 0.0 +0.3
Brian Serven 31 20.0
Joey Estes 24 pre-arb 10.0 0.8 -0.7
Brett Harris↔ BOS 28 pre-arb 5-0.0 0.1 -0.1
Jacob Lopez 28 pre-arb 16-0.0 0.8 -0.9
Tyler Ferguson↔ CHC 32 1-0.2 0.1 -1.3
Brent Rooker 31 arb 48-0.2 8.0 -9.4
Geoff Hartlieb 32 7-0.2
Michael Kelly 33 pre-arb 5-0.2 0.1 -1.5
Matt Krook↔ WSN 31 5-0.2
Max Muncy 23 pre-arb 48-0.3 0.8 -2.5
Mason Barnett 25 pre-arb 13-0.4 0.0 -2.3
Yunior Tur 26 2-0.4
Jeff McNeil 34 FA-elig 91-0.4 10.0 -12.7
Denzel Clarke 26 pre-arb 22-0.5 0.8 -3.6
Andy Ibáñez↔ NYM 33 11-0.5 0.8 -3.8
Elvis Alvarado 27 pre-arb 28-0.5 0.1 -3.2
Kade Morris 24 3-0.5 0.0 -3.1
Colby Thomas 25 pre-arb 50-0.6 0.8 -4.4
Joey Meneses 34 9-0.7
Jeffrey Springs 33 FA-elig 20-0.8 10.5 -15.3
Austin Wynns↔ ATL 35 14-0.8 0.3 -5.6
Luis Morales 23 pre-arb 3-0.8 0.1 -5.4
Justin Sterner 29 pre-arb 46-0.9 0.8 -6.1
Scott Barlow 33 FA-elig 36-1.1 2.0 -9.2
Lawrence Butler 25 pre-arb 95-1.9 3.7 -15.7

Key additions (new vs 2025)

PlayerfWAR
Mark Leiter Jr.0.6
Jonah Heim0.6
Luis Medina0.5
José Suarez0.4
Alika Williams0.3
Joel Kuhnel0.2

Key subtractions (gone from 2025)

Player2025 fWAR
Sean Newcomb1.0
Mason Miller1.0
JP Sears0.7
Max Schuemann0.6
Miguel Andujar0.4
Luis Urías0.1

Current & Historical Payroll

2026 payroll
$92.4M
luxury tax $123M
2027 guaranteed
$81.5M

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$96M$81M$44M$44M$44M$32M
Proj. arbitration$6M
Proj. pre-arbitration$21M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$141M$99M$62M$62M$62M$50M
Tax space+$103M+$148M+$188M+$191M+$194M+$209M
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
Luis Severino329.170$25.0M$22.0MPLAYERFREE AGENT
Jeffrey Springs337.055$10.5M$15.0MCLUBFREE AGENT
Jeff McNeil347.069$10.0M$15.8MCLUBFREE AGENT
Brent Rooker314.059$8.0M$14.0M$15.0M$19.0M$22.0MVESTFREE AGENT
Aaron Civale316.058$5.6MFREE AGENT
Lawrence Butler252.032$3.7M$5.4M$8.4M$10.4M$14.4M$16.4M$20.0MCLUB
Mark Leiter Jr.359.000$2.9MFREE AGENT
Scott Barlow337.026$2.0MFREE AGENT
Tyler Soderstrom242.053$1.4M$6.4M$10.4M$12.4M$16.4M$17.4M$19.4M
Jacob Wilson241.073$1.4M$2.9M$4.9M$9.4M$12.4M$17.4M$19.4M

Arbitration-eligible (7)

PlayerAgeSvc2026202720282029203020312032
Shea Langeliers283.051$5.2MARB 2ARB 3FREE AGENT
Jonah Heim315.097$771kFREE AGENT
Andy Ibáñez333.133$756kARB 2ARB 3FREE AGENT
José Suarez284.064$486kARB 3FREE AGENT
Austin Wynns355.017$348kFREE AGENT
Joey Meneses343.065ARB 2ARB 3FREE AGENT
Brian Serven313.012ARB 2ARB 3FREE AGENT

Pre-arbitration (28)

PlayerAgeSvc2026202720282029203020312032
Luis Medina272.146$835kARB 1ARB 2ARB 3FREE AGENT
Colby Thomas250.079$820kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Joey Estes241.031$820kPre-ArbARB 1ARB 2ARB 3FREE AGENT
J.T. Ginn271.039$785kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Darell Hernaiz241.040$785kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Nick Kurtz231.000$785kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Hogan Harris291.146$785kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Max Muncy23$780k
Jacob Lopez280.160$780kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Justin Sterner290.169$780kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Carlos Cortes290.069$780kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Denzel Clarke260.129$780kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Zack Gelof262.051$745kARB 1ARB 2ARB 3FREE AGENT
Jack Perkins260.099$713kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brooks Kriske321.019$601kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Alika Williams271.032$559kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Joel Kuhnel311.127$515kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Elvis Alvarado270.101$104kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Michael Kelly331.124$67kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tyler Ferguson321.089$60kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Luis Morales230.059$58kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brady Basso281.029$53kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brett Harris280.105$52kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Michael Stefanic302.093$18kARB 1ARB 2ARB 3FREE AGENT
Mason Barnett250.034$17kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Donovan Walton32
Matt Krook311.048Pre-ArbARB 1ARB 2ARB 3FREE AGENT
Geoff Hartlieb322.158ARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Athletics 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.928, -4.87], [1986, 0.86, -1.23], [1987, 1.162, 0.82], [1988, 0.904, 15.97], [1989, 1.274, 6.73], [1990, 1.204, 14.46], [1991, 1.676, -11.14], [1992, 1.401, 2.31], [1993, 1.114, -13.82], [1994, 1.05, -8.77], [1995, 1.083, -7.01], [1996, 0.637, -2.39], [1997, 0.506, -9.28], [1998, 0.538, -6.42], [1999, 0.502, 12.26], [2000, 0.618, 11.59], [2001, 0.569, 24.42], [2002, 0.632, 18.4], [2003, 0.742, 4.39], [2004, 0.907, 5.21], [2005, 0.781, 10.02], [2006, 0.866, 1.2], [2007, 0.795, 2.71], [2008, 0.617, 0.54], [2009, 0.597, 4.15], [2010, 0.606, 5.41], [2011, 0.701, -4.74], [2012, 0.611, 10.56], [2013, 0.608, 14.15], [2014, 0.75, 8.25], [2015, 0.584, -3.44], [2016, 0.563, -8.23], [2017, 0.487, 0.94], [2018, 0.586, 15.16], [2019, 0.758, 13.03], [2021, 0.744, 7.56], [2022, 0.342, -13.06], [2023, 0.395, -15.79], [2024, 0.394, 1.7], [2025, 0.47, 4.2], [2026, 0.579, -8.38]], TEAMCOL = '#006341'; 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", "C", "IF", "OF", "IF", "SP", "SP", "IF", "OF", "SP", "RP", "C", "RP", "RP", "RP", "IF", "IF", "RP", "RP", "OF", "IF", "SP", "IF", "RP", "RP", "IF", "RP", "C", "RP", "IF", "SP", "RP", "OF", "RP", "RP", "RP", "IF", "RP", "RP", "IF"], "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": ["Nick Kurtz", "Shea Langeliers", "Jacob Wilson", "Tyler Soderstrom", "Zack Gelof", "J.T. Ginn", "Gage Jump", "Joshua Kuroda-Grauer", "Henry Bolte", "Luis Severino", "Mark Leiter Jr.", "Jonah Heim", "Luis Medina", "Jack Perkins", "Jos\u00e9 Suarez", "Darell Hernaiz", "Alika Williams", "Hogan Harris", "Joel Kuhnel", "Carlos Cortes", "Donovan Walton", "Aaron Civale", "Tommy White", "Brooks Kriske", "Brady Basso", "Michael Stefanic", "Hayden Juenger", "Brian Serven", "Joey Estes", "Brett Harris", "Jacob Lopez", "Tyler Ferguson", "Brent Rooker", "Geoff Hartlieb", "Michael Kelly", "Matt Krook", "Max Muncy", "Mason Barnett", "Yunior Tur", "Jeff McNeil"], "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.0, 2.81, 1.82, 1.73, 1.71, 1.22, 0.94, 0.93, 0.67, 0.61, 0.59, 0.55, 0.46, 0.41, 0.4, 0.35, 0.33, 0.32, 0.25, 0.21, 0.18, 0.16, 0.13, 0.08, 0.06, 0.05, 0.03, 0.02, 0.02, -0.01, -0.01, -0.12, -0.14, -0.14, -0.14, -0.15, -0.17, -0.23, -0.25, -0.27]}; 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:'OAK',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);});