RRosternomics

Phillies PHI

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

Overview

FORCED-SPEND ↘ cooling
expensive & losing · year 1 in this regime · payroll 1.65× league · TAB -5.5
-5.5TAB — talent above budget20th/30
-0.6WAB — realized wins (TAB + luck)16th/30
4.0depth — 2+ WAR regulars7th/30
-3.9dead WAR — sub-replacement drag15th/30
24%dead money %4th/30
0.538win %7th/30
34.7roster fWAR16th/30
-42surplus $M (value vs cost)26th/30
$263Mpayroll $M4th/30luxury tax $234M

Roster

CFJustin Crawford 63GBrandon Marsh 13GSteward Berroa 3G
LFBrandon Marsh 58GOtto Kemp 12GEdmundo Sosa 9G
RFAdolis García 66GBrandon Marsh 6GSteward Berroa 2G
SSTrea Turner 66GEdmundo Sosa 8G
2BBryson Stott 63GEdmundo Sosa 14GDylan Moore 1G
3BAlec Bohm 62GEdmundo Sosa 8GFelix Reyes 1G
1BBryce Harper 66GFelix Reyes 4GDylan Moore 3G
STARTERSCristopher Sánchez 4.2WARJesús Luzardo 3.5WARZack Wheeler 3.0WARAaron Nola 0.6WARAndrew Painter -0.0WAR
CJ.T. Realmuto 47GRafael Marchán 23GGarrett Stubbs 7G
DHKyle Schwarber 64GBryce Harper 4GDylan Moore 2G
BULLPEN Jhoan Duran 17svBrad Keller 13hJosé Alvarado 8hOrion Kerkering 5hTanner Banks 2hTim Mayza 1hJonathan Bowlan 1hChase Shugart 0h
CJ.T. Realmuto 47 · Rafael Marchán 23 · Garrett Stubbs 7
1BBryce Harper 66 · Felix Reyes 4 · Dylan Moore 3
2BBryson Stott 63 · Edmundo Sosa 14 · Dylan Moore 1
3BAlec Bohm 62 · Edmundo Sosa 8 · Felix Reyes 1
SSTrea Turner 66 · Edmundo Sosa 8
LFBrandon Marsh 58 · Otto Kemp 12 · Edmundo Sosa 9
CFJustin Crawford 63 · Brandon Marsh 13 · Steward Berroa 3
RFAdolis García 66 · Brandon Marsh 6 · Steward Berroa 2
DHKyle Schwarber 64 · Bryce Harper 4 · Dylan Moore 2
SPCristopher Sánchez 4.2 · Jesús Luzardo 3.5 · Zack Wheeler 3.0 · Aaron Nola 0.6 · Andrew Painter -0.0
BP Jhoan Duran 17 · Brad Keller 13 · José Alvarado 8 · Orion Kerkering 5 · Tanner Banks 2 · Tim Mayza 1 · Jonathan Bowlan 1 · Chase Shugart 0

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
Cristopher Sánchez 29 arb 216.6 3.5 +38.0
Jesús Luzardo 28 arb 215.5 11.0 +23.7
Zack Wheeler 36 FA-elig 164.7 42.0 -12.4
Kyle Schwarber 33 FA-elig 1003.7 18.0 +5.3
Jhoan Duran 28 arb 363.0 7.5 +11.1
Bryson Stott 28 arb 982.7 5.9 +11.2
Bryce Harper 33 FA-elig 1042.6 27.5 -10.9
Brandon Marsh 28 arb 992.1 5.2 +7.9
J.T. Realmuto 35 FA-elig 751.5 15.0 -5.8
Derek Hill↔ CHW 30 281.3 0.3 +8.2
Jonathan Bowlan 29 pre-arb 371.2 0.8 +6.7
Trea Turner 33 FA-elig 1011.0 27.3 -20.7
José Alvarado 31 FA-elig 431.0 9.0 -2.8
Tim Mayza 34 FA-elig 390.9 1.8 +4.0
Aaron Nola 33 FA-elig 210.9 24.6 -19.2
Orion Kerkering 25 pre-arb 420.6 0.8 +3.2
Bryan De La Cruz 29 10.3
Brad Keller 30 FA-elig 320.3 9.0 -7.2
Trevor Richards↔ CHW 33 20.2 0.1 +1.4
Zach Pop 29 arb 70.2 0.9 +0.1
Alan Rangel 28 pre-arb 60.1 0.0 +0.9
Seth Johnson 27 pre-arb 140.1 0.0 +0.6
Justin Crawford 22 940.1 0.8 -0.4
Nolan Hoffman 28 pre-arb 30.1 0.0 +0.3
Adolis García 33 arb 670.0 10.0 -9.8
Kyle Backhus 28 pre-arb 170.0 0.8 -0.6
Alex McFarlane 25 10.0
Bryse Wilson↔ CHC 28 1-0.0
Andrew Painter 23 14-0.0 0.8 -0.8
Steward Berroa 27 pre-arb 6-0.1 0.8 -1.2
Lou Trivino III↔ BAL 34 3-0.1 0.0 -0.9
Garrett Stubbs 33 arb 20-0.2 0.9 -2.0
Edmundo Sosa 30 arb 57-0.2 4.4 -5.7
Max Lazar 27 pre-arb 6-0.2 0.8 -2.3
Dylan Moore 33 FA-elig 15-0.3 1.4 -3.3
Chase Shugart 29 pre-arb 28-0.3 0.7 -2.9
Tanner Banks 34 arb 28-0.4 1.2 -3.4
Alec Bohm 29 arb 99-0.4 10.2 -12.7
Rafael Marchán 27 arb 32-0.6 0.9 -4.6
Felix Reyes 25 12-0.6 0.1 -3.9
Gabriel Rincones Jr. 25 26-0.7
Taijuan Walker 33 5-0.8 18.0 -23.0
Otto Kemp 26 pre-arb 13-1.1 0.7 -7.7

Key additions (new vs 2025)

PlayerfWAR
Derek Hill0.9
Jonathan Bowlan0.8
Bryan De La Cruz0.2
Brad Keller0.2
Trevor Richards0.2
Zach Pop0.1

Key subtractions (gone from 2025)

Player2025 fWAR
Ranger Suarez4.0
Matt Strahm1.5
Harrison Bader1.2
Max Kepler0.6
Walker Buehler0.3
Brett de Geus-0.0

Current & Historical Payroll

2026 payroll
$262.6M
luxury tax $234M
2027 guaranteed
$216.4M

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$185M$175M$122M$107M$107M$53M
Proj. arbitration$41M$14M$14M
Proj. pre-arbitration$8M$16M$16M$16M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$252M$207M$154M$142M$142M$87M
Tax space−$-8M+$40M+$96M+$111M+$114M+$172M
Est. tax bill$4M

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

Guaranteed / signed (15)

PlayerAgeSvc2026202720282029203020312032
Zack Wheeler3612.098$42.0M$42.0MFREE AGENT
Bryce Harper3313.159$27.5M$27.5M$27.5M$23.5M$23.5M$23.5MFREE AGENT
Trea Turner339.135$27.3M$27.3M$27.3M$27.3M$27.3M$27.3M$27.3M
Aaron Nola3310.076$24.6M$24.6M$24.6M$24.6M$24.6MFREE AGENT
Taijuan Walker3311.142$18.0MFREE AGENT
Kyle Schwarber3310.086$18.0M$33.0M$33.0M$33.0M$33.0MFREE AGENT
J.T. Realmuto3511.038$15.0M$15.0M$15.0MFREE AGENT
Jesús Luzardo285.165$11.0M$27.0M$27.0M$27.0M$27.0M$27.0MFREE AGENT
José Alvarado318.009$9.0MCLUBFREE AGENT
Brad Keller307.062$9.0M$13.0MFREE AGENT
Cristopher Sánchez293.083$3.5M$7.0M$10.0M$15.0M$16.0M$28.0MFREE AGENT
Tim Mayza346.129$1.8MFREE AGENT
Dylan Moore337.000$1.4MFREE AGENT
Trevor Richards336.088$71kFREE AGENT
Lou Trivino III347.163$15kFREE AGENT

Arbitration-eligible (13)

PlayerAgeSvc2026202720282029203020312032
Alec Bohm295.106$10.2MFREE AGENT
Adolis García335.095$10.0MFREE AGENT
Jhoan Duran284.000$7.5MARB 3FREE AGENT
Bryson Stott284.000$5.9MARB 3FREE AGENT
Brandon Marsh284.078$5.2MARB 3FREE AGENT
Edmundo Sosa305.140$4.4MFREE AGENT
Tanner Banks343.092$1.2MARB 2ARB 3FREE AGENT
Zach Pop293.032$900kARB 2ARB 3FREE AGENT
Garrett Stubbs334.148$871kARB 3FREE AGENT
Rafael Marchán273.006$860kARB 2ARB 3FREE AGENT
Derek Hill303.040$323kARB 2ARB 3FREE AGENT
Bryan De La Cruz295.000FREE AGENT
Bryse Wilson285.036FREE AGENT

Pre-arbitration (13)

PlayerAgeSvc2026202720282029203020312032
Steward Berroa270.077$820kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Orion Kerkering252.010$806kARB 1ARB 2ARB 3FREE AGENT
Max Lazar270.170$785kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jonathan Bowlan290.116$785kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Justin Crawford22$780k
Andrew Painter23$780k
Kyle Backhus280.113$751kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Chase Shugart291.001$692kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Otto Kemp260.092$660kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Felix Reyes25$125k
Nolan Hoffman280.002$25kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Seth Johnson270.061$17kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Alan Rangel280.039$8kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Phillies 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, 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 = 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": ["SP", "SP", "SP", "OF", "RP", "IF", "IF", "OF", "C", "OF", "RP", "IF", "RP", "RP", "SP", "RP", "OF", "RP", "RP", "RP", "RP", "RP", "OF", "RP", "OF", "RP", "RP", "RP", "SP", "OF", "RP", "C", "IF", "RP", "IF", "RP", "RP", "IF", "C", "OF"], "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": ["Cristopher S\u00e1nchez", "Jes\u00fas Luzardo", "Zack Wheeler", "Kyle Schwarber", "Jhoan Duran", "Bryson Stott", "Bryce Harper", "Brandon Marsh", "J.T. Realmuto", "Derek Hill", "Jonathan Bowlan", "Trea Turner", "Jos\u00e9 Alvarado", "Tim Mayza", "Aaron Nola", "Orion Kerkering", "Bryan De La Cruz", "Brad Keller", "Trevor Richards", "Zach Pop", "Alan Rangel", "Seth Johnson", "Justin Crawford", "Nolan Hoffman", "Adolis Garc\u00eda", "Kyle Backhus", "Alex McFarlane", "Bryse Wilson", "Andrew Painter", "Steward Berroa", "Lou Trivino III", "Garrett Stubbs", "Edmundo Sosa", "Max Lazar", "Dylan Moore", "Chase Shugart", "Tanner Banks", "Alec Bohm", "Rafael March\u00e1n", "Felix Reyes"], "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": [4.23, 3.54, 3.02, 2.37, 1.9, 1.74, 1.7, 1.33, 0.94, 0.87, 0.76, 0.67, 0.63, 0.59, 0.55, 0.41, 0.19, 0.18, 0.15, 0.1, 0.09, 0.06, 0.04, 0.03, 0.02, 0.01, 0.01, -0.0, -0.01, -0.04, -0.09, -0.11, -0.13, -0.16, -0.19, -0.22, -0.23, -0.25, -0.38, -0.39]}; 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);});