RRosternomics

Astros HOU

GM: Dana Brown

Overview

FORCED-SPEND ↘ cooling
expensive & losing · year 1 in this regime · payroll 1.48× league · TAB -12.6
-12.6TAB — talent above budget29th/30
-5.3WAB — realized wins (TAB + luck)24th/30
3.0depth — 2+ WAR regulars19th/30
-4.2dead WAR — sub-replacement drag17th/30
39%dead money %16th/30
0.486win %19th/30
26.3roster fWAR23rd/30
-66surplus $M (value vs cost)28th/30
$237Mpayroll $M6th/30luxury tax $211M

Roster

CFJake Meyers 32GBrice Matthews 26GTaylor Trammell 12G
LFBrice Matthews 24GJoey Loperfido 18GZach Dezenzo 17G
RFCam Smith 69GDustin Harris 6GZach Cole 3G
SSJeremy Peña 32GCarlos Correa 23GNick Allen 17G
2BJose Altuve 46GBrice Matthews 20GNick Allen 16G
3BIsaac Paredes 53GCarlos Correa 14GBraden Shewmake 11G
1BChristian Walker 68GIsaac Paredes 8GShay Whitcomb 3G
STARTERSPeter Lambert 1.5WARSpencer Arrighetti 0.4WARTatsuya Imai 0.4WARKai-Wei Teng 0.2WARLance McCullers Jr. 0.1WAR
CChristian Vázquez 41GYainer Diaz 23GCésar Salazar 9G
DHYordan Alvarez 59GIsaac Paredes 5GChristian Vázquez 4G
BULLPEN Bryan King 6svSteven Okert 7hEnyel De Los Santos 5hBryan Abreu 5hAJ Blubaugh 4hRyan Weiss 0hCody Bolton 0hJayden Murray 0h
CChristian Vázquez 41 · Yainer Diaz 23 · César Salazar 9
1BChristian Walker 68 · Isaac Paredes 8 · Shay Whitcomb 3
2BJose Altuve 46 · Brice Matthews 20 · Nick Allen 16
3BIsaac Paredes 53 · Carlos Correa 14 · Braden Shewmake 11
SSJeremy Peña 32 · Carlos Correa 23 · Nick Allen 17
LFBrice Matthews 24 · Joey Loperfido 18 · Zach Dezenzo 17
CFJake Meyers 32 · Brice Matthews 26 · Taylor Trammell 12
RFCam Smith 69 · Dustin Harris 6 · Zach Cole 3
DHYordan Alvarez 59 · Isaac Paredes 5 · Christian Vázquez 4
SPPeter Lambert 1.5 · Spencer Arrighetti 0.4 · Tatsuya Imai 0.4 · Kai-Wei Teng 0.2 · Lance McCullers Jr. 0.1
BP Bryan King 6 · Steven Okert 7 · Enyel De Los Santos 5 · Bryan Abreu 5 · AJ Blubaugh 4 · Ryan Weiss 0 · Cody Bolton 0 · Jayden Murray 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
Yordan Alvarez 29 FA-elig 1037.8 26.8 +22.6
Isaac Paredes 27 arb 984.1 9.3 +16.4
Jeremy Peña 28 arb 573.4 9.5 +11.7
Peter Lambert 29 arb 172.3 1.3 +13.3
Christian Walker 35 FA-elig 1022.3 20.0 -5.7
Carlos Correa 31 FA-elig 321.7 21.5 -10.6
Cam Smith 23 pre-arb 1031.7 0.8 +10.0
Steven Okert 34 arb 511.7 2.3 +8.2
Josh Hader 32 FA-elig 211.3 19.0 -10.6
Enyel De Los Santos 30 451.3 1.6 +6.3
Jose Altuve 36 FA-elig 801.0 33.0 -26.7
Tatsuya Imai 28 140.6 16.7 -12.8
Spencer Arrighetti 26 pre-arb 170.6 0.7 +3.0
AJ Blubaugh 25 pre-arb 390.4 0.8 +1.8
Hunter Brown 27 arb 80.4 5.7 -3.3
Joey Loperfido 27 pre-arb 390.3 0.8 +1.2
Nick Allen 27 pre-arb 530.3 1.4 +0.3
Logan VanWey 27 pre-arb 50.3 0.0 +1.7
Kai-Wei Teng 27 pre-arb 250.3 0.8 +0.8
Miguel Ullola 24 20.2 0.8 +0.5
Lance McCullers Jr.↔ MIL 32 FA-elig 80.2 15.7 -14.4
Brice Matthews 24 pre-arb 820.2 0.8 +0.4
Taylor Trammell 28 pre-arb 490.2 0.9 +0.1
Zach Dezenzo 26 pre-arb 300.1 0.6 +0.1
Nate Pearson 29 arb 160.1 1.4 -0.7
Alimber Santa 23 100.1 0.5 -0.0
Bennett Sousa 31 pre-arb 70.0 0.9 -0.6
LaMonte Wade Jr. 32 170.0 0.5 -0.4
Cody Bolton 28 pre-arb 8-0.0 0.2 -0.2
Jayden Murray↔ CHC 29 pre-arb 8-0.0 0.1 -0.2
Dustin Harris↔ CHW 26 pre-arb 11-0.0 0.0 -0.1
Braden Shewmake↔ MIL 28 pre-arb 30-0.0 0.6 -0.7
Collin Price 26 7-0.0
Lucas Spence 23 7-0.1
Jason Alexander 33 pre-arb 4-0.1 0.6 -1.4
Ronel Blanco 32 pre-arb 1-0.1 0.8 -1.7
Jake Meyers 30 arb 52-0.1 3.5 -4.5
J.P. France 31 pre-arb 3-0.1 0.0 -1.0
Shay Whitcomb 27 pre-arb 17-0.2 0.2 -1.1
Daniel Johnson 30 8-0.2 0.1 -1.1
Christian Vázquez 35 FA-elig 64-0.2 1.0 -2.3
Yainer Diaz 27 arb 48-0.2 4.5 -5.8
Bryan King 29 pre-arb 42-0.2 0.8 -2.2
Christian Roa 27 7-0.2 0.1 -1.5
Raynel Delgado 26 17-0.2
Zach Cole 25 19-0.2 0.1 -1.6
César Salazar 30 pre-arb 9-0.3 0.6 -2.5
Cristian Javier 29 FA-elig 7-0.4 21.4 -24.0
Mike Burrows 26 18-0.4
Roddery Muñoz 26 pre-arb 3-0.5 0.1 -2.9
Colton Gordon 27 pre-arb 4-0.7 0.1 -4.5
Bryan Abreu 29 arb 38-0.9 5.8 -11.3
Ryan Weiss 29 9-0.9 2.1 -7.8

Key additions (new vs 2025)

PlayerfWAR
Peter Lambert1.5
Tatsuya Imai0.4
Joey Loperfido0.2
Nick Allen0.2
Kai-Wei Teng0.2
Nate Pearson0.1

Key subtractions (gone from 2025)

Player2025 fWAR
Framber Valdez4.0
Mauricio Dubón2.2
Ryan Gusto1.1
Victor Caratini0.8
Ramón Urías0.2
Luis Garcia0.1

Current & Historical Payroll

2026 payroll
$236.9M
luxury tax $211M
2027 guaranteed
$177.7M

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$156M$135M$102M$50M$20M$15M
Proj. arbitration$41M$9M
Proj. pre-arbitration$14M$3M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$229M$165M$120M$68M$38M$33M
Tax space+$15M+$82M+$130M+$185M+$218M+$226M
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 (13)

PlayerAgeSvc2026202720282029203020312032
Jose Altuve3614.072$33.0M$33.0M$13.0M$13.0MFREE AGENT
Yordan Alvarez296.113$26.8M$26.8M$26.8MFREE AGENT
Carlos Correa3110.119$21.5M$20.5M$20.0M$25.0MVEST$20.0MVEST$15.0MVEST$10.0MVEST
Cristian Javier296.000$21.4M$21.4MFREE AGENT
Christian Walker357.124$20.0M$20.0MFREE AGENT
Josh Hader328.115$19.0M$19.0M$19.0MFREE AGENT
Tatsuya Imai28$16.7M$18.7MOPT-OUT$18.7MPLAYER
Lance McCullers Jr.3210.140$15.7MFREE AGENT
Isaac Paredes274.160$9.3M$13.3MCLUBFREE AGENT
Ryan Weiss29$2.1M$5.0MCLUB
Enyel De Los Santos308.000$1.6MFREE AGENT
Christian Vázquez3511.031$1.0MFREE AGENT
LaMonte Wade Jr.327.000$523kFREE AGENT

Arbitration-eligible (8)

PlayerAgeSvc2026202720282029203020312032
Jeremy Peña284.000$9.5MARB 3FREE AGENT
Bryan Abreu295.022$5.8MFREE AGENT
Hunter Brown273.035$5.7MARB 2ARB 3FREE AGENT
Yainer Diaz273.035$4.5MARB 2ARB 3FREE AGENT
Jake Meyers304.044$3.5MARB 3FREE AGENT
Steven Okert345.089$2.3MFREE AGENT
Nate Pearson294.042$1.4MARB 3FREE AGENT
Peter Lambert294.077$1.3MARB 3FREE AGENT

Pre-arbitration (27)

PlayerAgeSvc2026202720282029203020312032
Nick Allen272.164$1.4MARB 1ARB 2ARB 3FREE AGENT
Bennett Sousa312.156$910kARB 1ARB 2ARB 3FREE AGENT
Taylor Trammell282.144$900kARB 1ARB 2ARB 3FREE AGENT
Cam Smith231.000$810kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ronel Blanco322.101$803kARB 1ARB 2ARB 3FREE AGENT
Bryan King291.100$800kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Joey Loperfido271.021$792kPre-ArbARB 1ARB 2ARB 3FREE AGENT
AJ Blubaugh250.038$791kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kai-Wei Teng271.022$787kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brice Matthews240.028$782kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Spencer Arrighetti262.000$724kARB 1ARB 2ARB 3FREE AGENT
Zach Dezenzo261.055$613kPre-ArbARB 1ARB 2ARB 3FREE AGENT
César Salazar301.077$609kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Braden Shewmake280.065$589kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jason Alexander331.134$563kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cody Bolton280.149$209kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Shay Whitcomb270.085$154kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Zach Cole25$100k
Jayden Murray290.026$93kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Christian Roa270.065$84kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Roddery Muñoz260.123$63kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Daniel Johnson301.123$54kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Colton Gordon270.112$50kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Dustin Harris260.061$46kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
J.P. France312.043$25kARB 1ARB 2ARB 3FREE AGENT
Logan VanWey270.056$17kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mike Burrows26

Graphs

Every roster slot ranked best→worst by WAR. Bars = Astros 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.926, -0.98], [1986, 0.923, 10.05], [1987, 1.091, -5.05], [1988, 1.093, -8.77], [1989, 1.179, -5.69], [1990, 1.1, -16.42], [1991, 0.562, -6.26], [1992, 0.507, 2.83], [1993, 0.946, 13.6], [1994, 1.029, 15.38], [1995, 0.979, 7.75], [1996, 0.815, 1.7], [1997, 0.882, 14.84], [1998, 1.078, 18.46], [1999, 1.034, 19.47], [2000, 0.916, -1.51], [2001, 1.067, 2.99], [2002, 1.005, 3.47], [2003, 1.016, 8.4], [2004, 1.101, 9.96], [2005, 1.065, 5.7], [2006, 1.115, 3.33], [2007, 1.063, -10.71], [2008, 1.032, -0.25], [2009, 1.159, -11.88], [2010, 0.892, -11.74], [2011, 0.72, -11.84], [2012, 0.44, -8.41], [2013, 0.215, -11.92], [2014, 0.421, -0.38], [2015, 0.689, 15.15], [2016, 0.732, 12.28], [2017, 1.091, 20.48], [2018, 1.342, 23.68], [2019, 1.346, 29.65], [2021, 1.324, 12.78], [2022, 1.254, 20.36], [2023, 1.266, 5.55], [2024, 1.557, 1.02], [2025, 1.229, 2.04], [2026, 1.485, -12.63]], TEAMCOL = '#EB6E1F'; 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", "IF", "SP", "IF", "IF", "OF", "RP", "RP", "RP", "IF", "SP", "SP", "RP", "SP", "OF", "IF", "RP", "RP", "RP", "SP", "OF", "OF", "OF", "RP", "RP", "RP", "OF", "RP", "RP", "OF", "IF", "C", "OF", "SP", "RP", "OF", "RP", "IF", "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": ["Yordan Alvarez", "Isaac Paredes", "Jeremy Pe\u00f1a", "Peter Lambert", "Christian Walker", "Carlos Correa", "Cam Smith", "Steven Okert", "Josh Hader", "Enyel De Los Santos", "Jose Altuve", "Tatsuya Imai", "Spencer Arrighetti", "AJ Blubaugh", "Hunter Brown", "Joey Loperfido", "Nick Allen", "Logan VanWey", "Kai-Wei Teng", "Miguel Ullola", "Lance McCullers Jr.", "Brice Matthews", "Taylor Trammell", "Zach Dezenzo", "Nate Pearson", "Alimber Santa", "Bennett Sousa", "LaMonte Wade Jr.", "Cody Bolton", "Jayden Murray", "Dustin Harris", "Braden Shewmake", "Collin Price", "Lucas Spence", "Jason Alexander", "Ronel Blanco", "Jake Meyers", "J.P. France", "Shay Whitcomb", "Daniel Johnson"], "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": [5.08, 2.65, 2.18, 1.5, 1.47, 1.12, 1.11, 1.09, 0.86, 0.81, 0.65, 0.4, 0.39, 0.26, 0.24, 0.2, 0.18, 0.18, 0.17, 0.14, 0.14, 0.12, 0.1, 0.07, 0.07, 0.05, 0.03, 0.01, -0.0, -0.01, -0.01, -0.02, -0.02, -0.08, -0.08, -0.09, -0.09, -0.1, -0.1, -0.1]}; 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:'HOU',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);});