RRosternomics

Nationals WSN

President of baseball ops: Paul Toboni · GM: Anirudh Kilambi

Overview

WINDOW ↗ rising
cheap & winning · year 1 in this regime · payroll 0.31× league · TAB +10.9
+10.9TAB — talent above budget7th/30
+8.7WAB — realized wins (TAB + luck)3rd/30
3.0depth — 2+ WAR regulars19th/30
-5.1dead WAR — sub-replacement drag25th/30
40%dead money %17th/30
0.500win %15th/30
31.3roster fWAR17th/30
+144surplus $M (value vs cost)5th/30
$50Mpayroll $M30th/30luxury tax $51M

Roster

CFJacob Young 64GJoey Wiemer 11GDylan Crews 8G
LFDaylen Lile 59GJames Wood 13GJosé Tena 3G
RFJames Wood 49GDylan Crews 17GJoey Wiemer 14G
SSCJ Abrams 65GNasim Nuñez 8G
2BNasim Nuñez 58GJorbit Vivas 18GJosé Tena 7G
3BBrady House 37GJorbit Vivas 26GCurtis Mead 19G
1BLuis García Jr. 53GCurtis Mead 31GAndrés Chaparro 15G
STARTERSCade Cavalli 2.7WARFoster Griffin 1.8WARJake Irvin 0.7WARPJ Poulin -0.1WARRichard Lovelady -0.4WAR
CKeibert Ruiz 43GDrew Millas 40G
DHJosé Tena 30GJames Wood 12GDaylen Lile 10G
BULLPEN Gus Varland 5svBrad Lord 5hMitchell Parker 4hOrlando Ribalta 3hClayton Beeter 3hPaxton Schultz 0hCole Henry 0hAndre Granillo 0h
CKeibert Ruiz 43 · Drew Millas 40
1BLuis García Jr. 53 · Curtis Mead 31 · Andrés Chaparro 15
2BNasim Nuñez 58 · Jorbit Vivas 18 · José Tena 7
3BBrady House 37 · Jorbit Vivas 26 · Curtis Mead 19
SSCJ Abrams 65 · Nasim Nuñez 8
LFDaylen Lile 59 · James Wood 13 · José Tena 3
CFJacob Young 64 · Joey Wiemer 11 · Dylan Crews 8
RFJames Wood 49 · Dylan Crews 17 · Joey Wiemer 14
DHJosé Tena 30 · James Wood 12 · Daylen Lile 10
SPCade Cavalli 2.7 · Foster Griffin 1.8 · Jake Irvin 0.7 · PJ Poulin -0.1 · Richard Lovelady -0.4
BP Gus Varland 5 · Brad Lord 5 · Mitchell Parker 4 · Orlando Ribalta 3 · Clayton Beeter 3 · Paxton Schultz 0 · Cole Henry 0 · Andre Granillo 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
James Wood 23 pre-arb 1046.4 0.8 +39.6
CJ Abrams 25 arb 1005.2 4.2 +28.8
Cade Cavalli 27 pre-arb 224.2 0.9 +25.5
Foster Griffin 30 pre-arb 202.9 5.5 +12.5
Luis García Jr. 26 arb 962.6 6.9 +9.7
Curtis Mead 25 pre-arb 862.4 0.8 +14.3
Jacob Young 26 pre-arb 922.1 0.8 +12.2
Andrew Alvarez 27 pre-arb 142.0 0.6 +12.1
Keibert Ruiz 27 arb 672.0 5.4 +7.3
Nasim Nuñez 25 pre-arb 961.8 0.8 +10.3
Daylen Lile 23 pre-arb 1021.4 0.8 +8.2
Joey Wiemer 27 pre-arb 321.2 0.2 +7.0
Jake Irvin 29 pre-arb 111.1 2.8 +3.9
Jorbit Vivas 25 pre-arb 760.8 0.8 +4.0
Dylan Crews 24 pre-arb 530.8 0.6 +4.2
Orlando Ribalta 28 pre-arb 320.6 0.7 +3.2
Andrés Chaparro 27 pre-arb 360.5 0.6 +2.6
Harry Ford 23 pre-arb 40.3 0.8 +1.3
Tom Cosgrove 30 50.3
Carson Palmquist 25 80.3
Brad Lord 26 pre-arb 270.2 0.8 +0.4
Zak Kent↔ MIN 28 pre-arb 80.1 0.0 +0.6
Eddy Yean 25 20.1
Paxton Schultz 28 pre-arb 250.1 0.8 -0.3
Abimelec Ortiz 24 10.1 0.8 -0.4
Jackson Rutledge 27 1-0.0 0.0 -0.0
José Tena 25 pre-arb 77-0.0 0.8 -1.0
Riley Cornelio 26 8-0.0 0.0 -0.3
Julian Fernández 30 pre-arb 3-0.1 0.0 -0.4
Drew Millas 28 pre-arb 50-0.1 0.8 -1.4
Max Kranick 28 arb 3-0.1 1.0 -1.8
Matt Krook↔ OAK 31 5-0.2
PJ Poulin 29 pre-arb 35-0.2 0.8 -2.1
Justin Lawrence↔ MIN 31 7-0.3 0.2 -1.8
Brady House 23 pre-arb 44-0.3 0.2 -2.0
Andre Granillo 26 pre-arb 8-0.4 0.1 -2.4
Cionel Pérez↔ NYM 30 arb 16-0.4 0.2 -2.8
Gus Varland 29 pre-arb 32-0.5 0.8 -3.6
Cole Henry 26 pre-arb 11-0.5 0.8 -3.9
Richard Lovelady↔ NYM 30 arb 27-0.6 0.7 -4.2
Ken Waldichuk 28 pre-arb 5-0.6 0.8 -4.7
Clayton Beeter 27 pre-arb 31-0.7 0.8 -5.2
Mitchell Parker 26 pre-arb 22-0.9 0.7 -6.1
Miles Mikolas 37 FA-elig 21-1.0 2.2 -8.5
Zack Littell 30 FA-elig 21-1.2 3.0 -10.5

Key additions (new vs 2025)

PlayerfWAR
Foster Griffin1.8
Curtis Mead1.5
Joey Wiemer0.7
Jorbit Vivas0.5
Harry Ford0.2
Tom Cosgrove0.2

Key subtractions (gone from 2025)

Player2025 fWAR
MacKenzie Gore2.9
José A. Ferrer1.4
Michael Soroka1.2
Alex Call0.8
Kyle Finnegan0.6
Jorge López0.3

Current & Historical Payroll

2026 payroll
$50.2M
luxury tax $51M
2027 guaranteed
$21.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$26M$14M$6M$6M$6M$6M
Proj. arbitration$7M$1M
Proj. pre-arbitration$18M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$69M$33M$24M$24M$24M$24M
Tax space+$175M+$214M+$226M+$229M+$232M+$235M
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 (4)

PlayerAgeSvc2026202720282029203020312032
Keibert Ruiz274.064$5.4M$5.4M$7.4M$9.4M$9.4M$12.0MCLUB$14.0MCLUB
Zack Littell306.043$3.0M$12.0MMUTUALFREE AGENT
Miles Mikolas379.037$2.2MFREE AGENT
Cade Cavalli272.141$862k$4.0MCLUBARB 2ARB 3FREE AGENT

Arbitration-eligible (6)

PlayerAgeSvc2026202720282029203020312032
Luis García Jr.264.142$6.9MARB 3FREE AGENT
CJ Abrams253.130$4.2MARB 2ARB 3FREE AGENT
Max Kranick283.011$1.0MARB 2ARB 3FREE AGENT
Richard Lovelady303.052$718kARB 2ARB 3FREE AGENT
Cionel Pérez304.151$236kARB 3FREE AGENT
Justin Lawrence313.167$232kARB 2ARB 3FREE AGENT

Pre-arbitration (33)

PlayerAgeSvc2026202720282029203020312032
Foster Griffin301.034$5.5MPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jake Irvin292.152$2.8MARB 1ARB 2ARB 3FREE AGENT
Ken Waldichuk282.150$825kARB 1ARB 2ARB 3FREE AGENT
Harry Ford230.028$820kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
James Wood231.091$807kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jacob Young262.037$794kARB 1ARB 2ARB 3FREE AGENT
Jorbit Vivas250.056$791kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Daylen Lile230.120$786kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Gus Varland290.169$785kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Clayton Beeter270.129$785kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brad Lord261.000$785kPre-ArbARB 1ARB 2ARB 3FREE AGENT
José Tena251.031$784kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cole Henry260.169$784kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Nasim Nuñez251.086$783kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Drew Millas281.038$783kPre-ArbARB 1ARB 2ARB 3FREE AGENT
PJ Poulin290.055$769kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Curtis Mead251.094$763kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Paxton Schultz280.078$752kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mitchell Parker262.000$728kARB 1ARB 2ARB 3FREE AGENT
Orlando Ribalta281.013$662kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Andrés Chaparro271.002$603kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Dylan Crews241.035$574kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Andrew Alvarez270.028$567kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Joey Wiemer271.089$235kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brady House230.105$235kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Andre Granillo260.076$96kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Zak Kent280.037$43kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Julian Fernández302.042$18kARB 1ARB 2ARB 3FREE AGENT
Riley Cornelio26$4k
Jackson Rutledge271.025$4kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Matt Krook311.048Pre-ArbARB 1ARB 2ARB 3FREE AGENT
Tom Cosgrove302.085ARB 1ARB 2ARB 3FREE AGENT
Carson Palmquist25

Graphs

Every roster slot ranked best→worst by WAR. Bars = Nationals 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.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 = 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": ["OF", "IF", "SP", "SP", "IF", "IF", "OF", "RP", "C", "IF", "OF", "OF", "SP", "IF", "OF", "RP", "IF", "C", "RP", "RP", "RP", "RP", "RP", "RP", "IF", "RP", "IF", "RP", "RP", "C", "RP", "RP", "RP", "RP", "IF", "RP", "RP", "RP", "RP", "RP"], "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": ["James Wood", "CJ Abrams", "Cade Cavalli", "Foster Griffin", "Luis Garc\u00eda Jr.", "Curtis Mead", "Jacob Young", "Andrew Alvarez", "Keibert Ruiz", "Nasim Nu\u00f1ez", "Daylen Lile", "Joey Wiemer", "Jake Irvin", "Jorbit Vivas", "Dylan Crews", "Orlando Ribalta", "Andr\u00e9s Chaparro", "Harry Ford", "Tom Cosgrove", "Carson Palmquist", "Brad Lord", "Zak Kent", "Eddy Yean", "Paxton Schultz", "Abimelec Ortiz", "Jackson Rutledge", "Jos\u00e9 Tena", "Riley Cornelio", "Julian Fern\u00e1ndez", "Drew Millas", "Max Kranick", "Matt Krook", "PJ Poulin", "Justin Lawrence", "Brady House", "Andre Granillo", "Cionel P\u00e9rez", "Gus Varland", "Cole Henry", "Richard Lovelady"], "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.12, 3.37, 2.69, 1.84, 1.69, 1.54, 1.33, 1.29, 1.29, 1.13, 0.92, 0.74, 0.68, 0.49, 0.49, 0.4, 0.32, 0.22, 0.17, 0.17, 0.12, 0.07, 0.06, 0.05, 0.04, -0.0, -0.03, -0.03, -0.04, -0.06, -0.09, -0.12, -0.14, -0.16, -0.18, -0.24, -0.26, -0.29, -0.32, -0.36]}; 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);});