RRosternomics

Nationals WSN

GM: Mike DeBartolo

Overview

REBUILD → steady
cheap & losing · year 4 in this regime · payroll 0.43× league · TAB -8.6
-8.6TAB — talent above budget27th/30
-5.2WAB — realized wins (TAB + luck)22nd/30
3.0depth — 2+ WAR regulars25th/30
-8.3dead WAR — sub-replacement drag27th/30
62%dead money %28th/30
0.407win %28th/30
15.5roster fWAR28th/30
-10surplus $M (value vs cost)16th/30
$66Mpayroll $M28th/30

Roster

CFJacob Young 112GRobert Hassell III 48GDylan Crews 15G
LFJames Wood 123GDaylen Lile 22GRobert Hassell III 8G
RFDylan Crews 72GDaylen Lile 54GRobert Hassell III 12G
SSCJ Abrams 142GNasim Nuñez 19GPaul DeJong 10G
2BLuis García Jr. 129GNasim Nuñez 13GPaul DeJong 13G
3BBrady House 72GJosé Tena 46GPaul DeJong 33G
1BJosh Bell 33GAndrés Chaparro 30GLuis García Jr. 2G
STARTERSMacKenzie Gore 2.9WARTrevor Williams 1.2WARBrad Lord 1.2WARMitchell Parker 0.9WARAndrew Alvarez 0.6WAR
CRiley Adams 80GKeibert Ruiz 66GDrew Millas 16G
DHJosh Bell 98GJames Wood 33GDaylen Lile 15G
BULLPEN José A. Ferrer 11svCole Henry 10hClayton Beeter 9hKonnor Pilkington 5hJorge López 5hPJ Poulin 5hJackson Rutledge 3hEduardo Salazar 2h
CRiley Adams 80 · Keibert Ruiz 66 · Drew Millas 16
1BJosh Bell 33 · Andrés Chaparro 30 · Luis García Jr. 2
2BLuis García Jr. 129 · Nasim Nuñez 13 · Paul DeJong 13
3BBrady House 72 · José Tena 46 · Paul DeJong 33
SSCJ Abrams 142 · Nasim Nuñez 19 · Paul DeJong 10
LFJames Wood 123 · Daylen Lile 22 · Robert Hassell III 8
CFJacob Young 112 · Robert Hassell III 48 · Dylan Crews 15
RFDylan Crews 72 · Daylen Lile 54 · Robert Hassell III 12
DHJosh Bell 98 · James Wood 33 · Daylen Lile 15
SPMacKenzie Gore 2.9 · Trevor Williams 1.2 · Brad Lord 1.2 · Mitchell Parker 0.9 · Andrew Alvarez 0.6
BP José A. Ferrer 11 · Cole Henry 10 · Clayton Beeter 9 · Konnor Pilkington 5 · Jorge López 5 · PJ Poulin 5 · Jackson Rutledge 3 · Eduardo Salazar 2

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
James Wood 22 pre-arb 1573.3 0.8 +12.2
CJ Abrams 24 pre-arb 1443.2 0.8 +11.8
MacKenzie Gore 26 arb 302.9 2.9 +8.6
Daylen Lile 22 911.4 0.5 +5.1
José A. Ferrer 25 721.4
Brad Lord 25 481.2 0.8 +4.2
Trevor Williams 33 FA-elig 171.2 7.0 -2.3
Michael Soroka↔ CHC 27 161.2 4.5 +0.1
Jacob Young 25 pre-arb 1201.0 0.8 +3.3
Mitchell Parker 25 pre-arb 330.9 0.8 +2.7
Alex Call↔ LAD 30 720.8 0.3 +3.0
Nasim Nuñez 24 pre-arb 390.8 0.3 +2.7
Luis García Jr. 25 arb 1390.7 4.5 -1.6
Andrew Alvarez 26 50.6 0.1 +2.2
Drew Millas 27 pre-arb 180.6 0.4 +1.9
Kyle Finnegan↔ DET 33 400.6 1.0 +1.3
Clayton Beeter↔ NYY 26 pre-arb 240.5 0.2 +1.6
Cade Cavalli 26 pre-arb 100.5 0.4 +1.4
Jorge López 32 260.3 3.0 -1.9
Dylan Crews 23 pre-arb 850.2 0.8 +0.0
Paul DeJong 31 FA-elig 570.2 1.1 -0.4
PJ Poulin 28 280.2 0.2 +0.4
Andrew Chafin↔ ANA 35 260.2 0.3 +0.3
Amed Rosario↔ NYY 29 470.1 1.0 -0.6
Trey Lipscomb 25 pre-arb 30.1 0.1 +0.2
Luis García↔ ANA 38 arb 100.1 0.8 -0.5
Josh Bell 32 FA-elig 1400.1 6.0 -5.7
José Tena 24 pre-arb 500.0 0.4 -0.3
Sauryn Lao↔ SEA 25 6-0.0 0.0 -0.1
Julian Fernández↔ LAD 29 pre-arb 3-0.0 0.1 -0.3
C.J. Stubbs 28 1-0.1 0.0 -0.3
Mason Thompson 27 arb 14-0.1 0.6 -1.1
Andry Lara 22 9-0.1 0.1 -0.6
Colin Poche↔ NYM 31 13-0.2 1.3 -1.9
Jorge Alfaro 32 arb 14-0.3 0.1 -1.2
Brady House 22 73-0.3 0.4 -1.5
Cole Henry 25 57-0.3 0.7 -1.7
Ryan Loutos↔ LAD 26 pre-arb 10-0.3 0.1 -1.3
Orlando Ribalta 27 pre-arb 22-0.3 0.7 -2.0
Zach Brzykcy 25 pre-arb 26-0.3 0.4 -1.7
Jake Irvin 28 pre-arb 33-0.4 0.8 -2.3
Andrés Chaparro 26 pre-arb 34-0.4 0.6 -2.3
Lucas Sims 31 18-0.5 3.0 -4.8
Robert Hassell III 23 70-0.5 0.3 -2.2
Konnor Pilkington 27 pre-arb 32-0.5 0.4 -2.2
Eduardo Salazar 27 pre-arb 30-0.5 0.3 -2.3
Jackson Rutledge 26 pre-arb 63-0.5 0.7 -2.7
Shinnosuke Ogasawara 27 23-0.5 1.5 -3.6
Riley Adams 29 pre-arb 83-0.6 0.8 -3.2
Nathaniel Lowe↔ BOS 29 119-0.8 8.0 -11.1
Keibert Ruiz 26 arb 68-1.0 5.4 -9.4

Key additions (new vs 2024)

PlayerfWAR
Daylen Lile1.4
Brad Lord1.2
Michael Soroka1.2
Andrew Alvarez0.6
Clayton Beeter0.5
Cade Cavalli0.5

Key subtractions (gone from 2024)

Player2024 fWAR
Patrick Corbin1.7
Robert Garcia1.5
Jesse Winker1.3
Dylan Floro1.0
Lane Thomas1.0
Hunter Harvey0.9

Current & Historical Payroll

2025 payroll
$66.2M
2026 guaranteed
$12.4M

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 (10)

PlayerAgeSvc2025202620272028202920302031
Trevor Williams338.027$7.0M$7.0MFREE AGENT
Josh Bell328.053$6.0MFREE AGENT
Keibert Ruiz263.064$5.4M$5.4M$5.4M$7.4M$9.4M$9.4M$12.0MCLUB
Michael Soroka276.009$4.5MFREE AGENT
Jorge López326.102$3.0MFREE AGENT
Lucas Sims316.014$3.0MFREE AGENT
Paul DeJong317.057$1.1MFREE AGENT
Amed Rosario297.062$987kFREE AGENT
Luis García3812.000$776kFREE AGENT
Andrew Chafin3510.020$303kFREE AGENT

Arbitration-eligible (7)

PlayerAgeSvc2025202620272028202920302031
Nathaniel Lowe294.145$8.0MARB 3FREE AGENT
Luis García Jr.253.142$4.5MARB 2ARB 3FREE AGENT
MacKenzie Gore263.000$2.9MARB 2ARB 3FREE AGENT
Colin Poche315.140$1.3MFREE AGENT
Kyle Finnegan335.000$975kFREE AGENT
Mason Thompson273.046$609kARB 2ARB 3FREE AGENT
Jorge Alfaro325.133$145kFREE AGENT

Pre-arbitration (34)

PlayerAgeSvc2025202620272028202920302031
Shinnosuke Ogasawara270.000$1.5MPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Riley Adams292.171$850kARB 1ARB 2ARB 3FREE AGENT
CJ Abrams242.130$781kARB 1ARB 2ARB 3FREE AGENT
Jake Irvin281.152$775kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jacob Young251.037$769kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mitchell Parker250.168$767kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
James Wood220.091$765kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Dylan Crews230.035$762kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brad Lord250.000$760kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jackson Rutledge260.025$723kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Orlando Ribalta270.013$699kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cole Henry250.000$687kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Andrés Chaparro260.048$554kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Daylen Lile220.000$488kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brady House220.000$427kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
José Tena240.100$420kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cade Cavalli262.041$406kARB 1ARB 2ARB 3FREE AGENT
Zach Brzykcy250.029$386kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Konnor Pilkington270.057$386kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Drew Millas270.116$382kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Nasim Nuñez241.000$350kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Robert Hassell III230.000$345kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Alex Call301.161$344kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Eduardo Salazar270.103$326kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Clayton Beeter260.011$241kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
PJ Poulin280.000$221kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Julian Fernández292.033$135kARB 1ARB 2ARB 3FREE AGENT
Ryan Loutos260.021$133kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Andrew Alvarez260.000$114kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Andry Lara22$106k
Trey Lipscomb250.099$53kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Sauryn Lao25$43k
C.J. Stubbs28$20k
José A. Ferrer252.000ARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Nationals 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, 0.927, 3.09], [1986, 0.944, 0.73], [1987, 0.694, 4.9], [1988, 0.801, 3.6], [1989, 1.029, -1.58], [1990, 0.942, -1.54], [1991, 0.455, 2.76], [1992, 0.53, 10.23], [1993, 0.5, 7.54], [1994, 0.598, 23.78], [1995, 0.402, 5.53], [1996, 0.503, 8.31], [1997, 0.49, 6.76], [1998, 0.242, 7.47], [1999, 0.365, 3.28], [2000, 0.528, -4.45], [2001, 0.547, -11.98], [2002, 0.632, 1.03], [2003, 0.707, -5.0], [2004, 0.597, -12.73], [2005, 0.767, 0.06], [2006, 0.701, -5.96], [2007, 0.385, -5.22], [2008, 0.595, -11.25], [2009, 0.639, -8.42], [2010, 0.681, -3.28], [2011, 0.724, -2.08], [2012, 0.879, 10.48], [2013, 1.136, -0.82], [2014, 1.229, 13.52], [2015, 1.334, 2.41], [2016, 1.176, 9.04], [2017, 1.384, 8.02], [2018, 1.373, 2.81], [2019, 1.57, 10.46], [2021, 1.229, -8.19], [2022, 0.851, -24.23], [2023, 0.433, -8.77], [2024, 0.582, -0.75], [2025, 0.426, -8.56], [2026, 0.315, 10.9]], TEAMCOL = '#AB0003'; 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": ["OF", "IF", "SP", "OF", "RP", "RP", "SP", "SP", "OF", "SP", "OF", "IF", "IF", "SP", "C", "RP", "RP", "SP", "RP", "OF", "IF", "RP", "RP", "IF", "IF", "RP", "IF", "IF", "RP", "RP", "C", "RP", "RP", "RP", "C", "IF", "RP", "RP", "RP", "RP"], "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": ["James Wood", "CJ Abrams", "MacKenzie Gore", "Daylen Lile", "Jos\u00e9 A. Ferrer", "Brad Lord", "Trevor Williams", "Michael Soroka", "Jacob Young", "Mitchell Parker", "Alex Call", "Nasim Nu\u00f1ez", "Luis Garc\u00eda Jr.", "Andrew Alvarez", "Drew Millas", "Kyle Finnegan", "Clayton Beeter", "Cade Cavalli", "Jorge L\u00f3pez", "Dylan Crews", "Paul DeJong", "PJ Poulin", "Andrew Chafin", "Amed Rosario", "Trey Lipscomb", "Luis Garc\u00eda", "Josh Bell", "Jos\u00e9 Tena", "Sauryn Lao", "Julian Fern\u00e1ndez", "C.J. Stubbs", "Mason Thompson", "Andry Lara", "Colin Poche", "Jorge Alfaro", "Brady House", "Cole Henry", "Ryan Loutos", "Orlando Ribalta", "Zach Brzykcy"], "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.25, 3.16, 2.89, 1.4, 1.38, 1.25, 1.17, 1.15, 1.01, 0.87, 0.84, 0.77, 0.73, 0.58, 0.58, 0.56, 0.47, 0.46, 0.29, 0.19, 0.16, 0.16, 0.15, 0.1, 0.07, 0.07, 0.06, 0.03, -0.0, -0.04, -0.06, -0.11, -0.13, -0.16, -0.26, -0.26, -0.27, -0.29, -0.32, -0.34]}; 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:'WSN',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);});