RRosternomics

Phillies PHI

President of baseball ops: Dave Dombrowski · GM: Preston Mattingly

Overview

CONTEND ↗ rising
expensive & winning · year 4 in this regime · payroll 1.87× league · TAB +9.5
+9.5TAB — talent above budget3rd/30
+6.2WAB — realized wins (TAB + luck)9th/30
10.0depth — 2+ WAR regulars2nd/30
-3.2dead WAR — sub-replacement drag3rd/30
18%dead money %3rd/30
0.593win %2nd/30
51.3roster fWAR1st/30
-85surplus $M (value vs cost)25th/30
$290Mpayroll $M3rd/30

2025: made playoffs · 96–66. Ranks are vs the 30 teams that season.

Roster

CFBrandon Marsh 84GHarrison Bader 81GJohan Rojas 65G
LFMax Kepler 101GBrandon Marsh 62GWeston Wilson 27G
RFNick Castellanos 143GMax Kepler 36GBrandon Marsh 2G
SSTrea Turner 139GEdmundo Sosa 19GBryson Stott 13G
2BBryson Stott 135GEdmundo Sosa 33GWeston Wilson 11G
3BAlec Bohm 108GEdmundo Sosa 33GOtto Kemp 31G
1BBryce Harper 130GOtto Kemp 17GAlec Bohm 13G
STARTERSCristopher Sánchez 6.4WARJesús Luzardo 5.3WARZack Wheeler 4.0WARRanger Suarez 4.0WARAaron Nola 0.9WAR
CJ.T. Realmuto 132GRafael Marchán 42GGarrett Stubbs 2G
DHKyle Schwarber 154GNick Castellanos 4GGarrett Stubbs 3G
BULLPEN Jhoan Duran 32svMatt Strahm 22hOrion Kerkering 19hTanner Banks 10hJordan Romano 7hLou Trivino 5hJoe Ross 1hMax Lazar 1h
CJ.T. Realmuto 132 · Rafael Marchán 42 · Garrett Stubbs 2
1BBryce Harper 130 · Otto Kemp 17 · Alec Bohm 13
2BBryson Stott 135 · Edmundo Sosa 33 · Weston Wilson 11
3BAlec Bohm 108 · Edmundo Sosa 33 · Otto Kemp 31
SSTrea Turner 139 · Edmundo Sosa 19 · Bryson Stott 13
LFMax Kepler 101 · Brandon Marsh 62 · Weston Wilson 27
CFBrandon Marsh 84 · Harrison Bader 81 · Johan Rojas 65
RFNick Castellanos 143 · Max Kepler 36 · Brandon Marsh 2
DHKyle Schwarber 154 · Nick Castellanos 4 · Garrett Stubbs 3
SPCristopher Sánchez 6.4 · Jesús Luzardo 5.3 · Zack Wheeler 4.0 · Ranger Suarez 4.0 · Aaron Nola 0.9
BP Jhoan Duran 32 · Matt Strahm 22 · Orion Kerkering 19 · Tanner Banks 10 · Jordan Romano 7 · Lou Trivino 5 · Joe Ross 1 · Max Lazar 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.

2025 roster — value & surplus

PlayerAgeServiceGfWARSalary $MSurplus $M
Trea Turner 32 FA-elig 1416.7 27.3 -0.7
Cristopher Sánchez 28 pre-arb 326.4 2.0 +23.5
Jesús Luzardo 27 arb 325.3 6.6 +14.7
Kyle Schwarber 32 FA-elig 1624.9 20.0 -0.4
Zack Wheeler 35 FA-elig 244.0 42.0 -25.9
Ranger Suarez 29 arb 264.0 8.8 +7.3
Bryce Harper 32 FA-elig 1323.5 27.5 -13.7
Bryson Stott 27 arb 1473.1 3.2 +9.0
Brandon Marsh 27 arb 1332.4 3.0 +6.7
J.T. Realmuto 34 FA-elig 1342.1 23.9 -15.5
Alec Bohm 28 arb 1201.7 7.7 -0.8
Edmundo Sosa 29 arb 891.6 3.0 +3.5
Matt Strahm 33 FA-elig 661.5 7.5 -1.4
Harrison Bader↔ MIN 31 501.2 1.2 +3.6
Aaron Nola 32 FA-elig 170.9 24.6 -20.9
Jhoan Duran↔ MIN 27 arb 230.9 0.9 +2.6
Tanner Banks 33 pre-arb 690.8 0.8 +2.3
Orion Kerkering 24 pre-arb 690.7 0.8 +2.0
Max Kepler 32 FA-elig 1270.6 10.0 -7.6
Taijuan Walker 32 FA-elig 340.5 18.0 -16.0
Johan Rojas 24 pre-arb 710.4 0.6 +1.1
José Alvarado 30 FA-elig 280.3 4.7 -3.3
Walker Buehler↔ BOS 30 FA-elig 30.3 2.3 -0.9
Rafael Marchán 26 pre-arb 420.3 0.8 +0.3
Lou Trivino III↔ LAD 33 FA-elig 100.1 0.2 +0.2
Nolan Hoffman 27 10.0 0.1 -0.0
Alan Rangel 27 pre-arb 5-0.0 0.2 -0.2
Seth Johnson 26 pre-arb 10-0.0 0.3 -0.3
Garrett Stubbs 32 5-0.0
Brett de Geus 27 1-0.0 0.1 -0.3
Tim Mayza↔ PIT 33 arb 8-0.0 0.7 -0.8
Daniel Robert 30 pre-arb 15-0.1 0.3 -0.5
Carlos Hernández↔ CLE 28 25-0.1 0.3 -0.6
Michael Mercado 26 pre-arb 3-0.1 0.2 -0.5
Cal Stevenson 28 5-0.1 0.1 -0.5
Kody Clemens↔ MIN 29 7-0.1 0.0 -0.5
David Robertson 40 FA-elig 20-0.1 6.0 -6.5
Buddy Kennedy↔ LAD 26 4-0.1 0.0 -0.5
Joe Ross 32 37-0.1 4.0 -4.6
Donovan Walton 31 2-0.2
José Ruiz↔ ATL 30 16-0.2 0.7 -1.4
Otto Kemp 25 62-0.2 0.4 -1.2
Max Lazar 26 pre-arb 36-0.2 0.5 -1.3
Mick Abel↔ MIN 23 7-0.2 0.1 -1.1
Jordan Romano 32 arb 49-0.4 8.5 -10.0
Weston Wilson 30 pre-arb 54-0.4 0.6 -2.1
Nick Castellanos 33 FA-elig 147-0.6 20.0 -22.3

Key additions (new vs 2024)

PlayerfWAR
Jesús Luzardo5.3
Harrison Bader1.2
Jhoan Duran0.9
Max Kepler0.6
Walker Buehler0.3
Lou Trivino III0.1

Key subtractions (gone from 2024)

Player2024 fWAR
Jeff Hoffman2.0
Spencer Turnbull0.7
Gregory Soto0.4
Carlos Estévez0.2
Seranthony Domínguez0.1
Nick Nelson0.1

Current & Historical Payroll

2025 payroll
$290.5M
2026 guaranteed
$137.9M

What the front office was committed to as of 2025, 2025–2031. Guaranteed $ are contract commitments signed by 2025; 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

Guaranteed / signed (16)

PlayerAgeSvc2025202620272028202920302031
Zack Wheeler3511.098$42.0M$42.0M$42.0MFREE AGENT
Bryce Harper3212.159$27.5M$27.5M$27.5M$27.5M$23.5M$23.5M$23.5M
Trea Turner328.135$27.3M$27.3M$27.3M$27.3M$27.3M$27.3M$27.3M
Aaron Nola329.076$24.6M$24.6M$24.6M$24.6M$24.6M$24.6MFREE AGENT
J.T. Realmuto3410.038$23.9MFREE AGENT
Nick Castellanos3311.029$20.0MFREE AGENT
Kyle Schwarber329.086$20.0MFREE AGENT
Taijuan Walker3210.142$18.0MFREE AGENT
Max Kepler328.152$10.0MFREE AGENT
Matt Strahm338.064$7.5M$7.5MVESTFREE AGENT
David Robertson4015.119$6.0MFREE AGENT
José Alvarado307.088$4.7M$9.0MCLUBFREE AGENT
Joe Ross327.018$4.0MFREE AGENT
Walker Buehler306.168$2.3MFREE AGENT
Harrison Bader317.030$1.2MFREE AGENT
Lou Trivino III336.163$227kFREE AGENT

Arbitration-eligible (13)

PlayerAgeSvc2025202620272028202920302031
Ranger Suarez295.112$8.8MFREE AGENT
Jordan Romano325.051$8.5MFREE AGENT
Alec Bohm284.106$7.7MARB 3FREE AGENT
Jesús Luzardo274.165$6.6MARB 3FREE AGENT
Bryson Stott273.000$3.2MARB 2ARB 3FREE AGENT
Edmundo Sosa294.140$3.0MARB 3FREE AGENT
Brandon Marsh273.078$3.0MARB 2ARB 3FREE AGENT
Jhoan Duran273.000$893kARB 2ARB 3FREE AGENT
Tim Mayza335.129$698kFREE AGENT
José Ruiz304.148$667kARB 3FREE AGENT
Carlos Hernández283.099$350kARB 2ARB 3FREE AGENT
Brett de Geus274.000$128kARB 3FREE AGENT
Garrett Stubbs324.120ARB 3FREE AGENT

Pre-arbitration (18)

PlayerAgeSvc2025202620272028202920302031
Cristopher Sánchez282.083$2.0MARB 1ARB 2ARB 3FREE AGENT
Tanner Banks332.092$786kARB 1ARB 2ARB 3FREE AGENT
Rafael Marchán262.006$780kARB 1ARB 2ARB 3FREE AGENT
Orion Kerkering241.010$773kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Weston Wilson300.113$641kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Johan Rojas241.080$587kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Max Lazar260.051$521kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Otto Kemp250.000$376kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Daniel Robert300.017$260kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Seth Johnson260.022$254kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Alan Rangel270.012$213kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Michael Mercado260.031$162kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cal Stevenson281.036$140kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mick Abel230.000$115kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Nolan Hoffman270.000$72kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kody Clemens291.134$38kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Buddy Kennedy260.131$29kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Donovan Walton31

Graphs

Every roster slot ranked best→worst by WAR. Bars = Phillies 2025; 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, 1.075, -9.45], [1986, 0.937, -1.33], [1987, 1.094, -7.26], [1988, 1.16, -21.73], [1989, 0.7, -12.61], [1990, 0.816, -2.7], [1991, 0.849, -10.29], [1992, 0.747, -1.01], [1993, 0.93, 12.49], [1994, 0.96, -8.59], [1995, 1.007, -5.74], [1996, 0.997, -9.36], [1997, 0.733, -8.69], [1998, 0.703, -2.26], [1999, 0.644, 2.19], [2000, 0.7, -7.25], [2001, 0.697, 6.57], [2002, 0.856, 4.31], [2003, 1.046, 8.27], [2004, 1.421, 1.64], [2005, 1.31, 7.82], [2006, 1.066, 5.02], [2007, 1.16, 8.84], [2008, 1.119, 11.47], [2009, 1.281, 6.91], [2010, 1.645, -0.75], [2011, 1.865, 6.56], [2012, 1.682, 0.61], [2013, 1.626, -17.76], [2014, 1.611, -12.17], [2015, 0.77, -12.03], [2016, 0.58, -6.46], [2017, 0.592, -2.76], [2018, 0.775, 0.51], [2019, 1.265, -10.39], [2021, 1.578, -3.67], [2022, 1.648, 5.1], [2023, 1.479, 6.92], [2024, 1.505, 8.51], [2025, 1.869, 9.54], [2026, 1.645, -5.52]], TEAMCOL = '#E81828'; 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 = 2025; 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", "OF", "SP", "SP", "IF", "IF", "OF", "C", "IF", "IF", "RP", "OF", "SP", "RP", "RP", "RP", "OF", "SP", "OF", "RP", "SP", "C", "RP", "RP", "RP", "RP", "C", "RP", "RP", "RP", "RP", "RP", "OF", "IF", "RP", "IF", "RP", "IF"], "league": [5.54, 4.03, 3.33, 2.91, 2.51, 2.3, 2.05, 1.86, 1.64, 1.46, 1.31, 1.2, 1.1, 0.97, 0.87, 0.79, 0.71, 0.64, 0.57, 0.51, 0.44, 0.38, 0.32, 0.28, 0.23, 0.19, 0.17, 0.13, 0.1, 0.08, 0.06, 0.04, 0.02, -0.0, -0.02, -0.04, -0.04, -0.06, -0.09, -0.11], "names": ["Trea Turner", "Cristopher S\u00e1nchez", "Jes\u00fas Luzardo", "Kyle Schwarber", "Zack Wheeler", "Ranger Suarez", "Bryce Harper", "Bryson Stott", "Brandon Marsh", "J.T. Realmuto", "Alec Bohm", "Edmundo Sosa", "Matt Strahm", "Harrison Bader", "Aaron Nola", "Jhoan Duran", "Tanner Banks", "Orion Kerkering", "Max Kepler", "Taijuan Walker", "Johan Rojas", "Jos\u00e9 Alvarado", "Walker Buehler", "Rafael March\u00e1n", "Lou Trivino III", "Nolan Hoffman", "Alan Rangel", "Seth Johnson", "Garrett Stubbs", "Brett de Geus", "Tim Mayza", "Daniel Robert", "Carlos Hern\u00e1ndez", "Michael Mercado", "Cal Stevenson", "Kody Clemens", "David Robertson", "Buddy Kennedy", "Joe Ross", "Donovan Walton"], "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": [6.67, 6.4, 5.34, 4.92, 4.04, 4.04, 3.47, 3.07, 2.43, 2.09, 1.73, 1.63, 1.54, 1.2, 0.93, 0.88, 0.78, 0.71, 0.61, 0.51, 0.44, 0.35, 0.34, 0.27, 0.11, 0.01, -0.0, -0.02, -0.03, -0.03, -0.04, -0.05, -0.06, -0.09, -0.09, -0.11, -0.12, -0.13, -0.15, -0.17]}; 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:'PHI',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);});