RRosternomics

Astros HOU

GM: Bill Wood

Overview

REBUILD → steady
cheap & losing · year 1 in this regime · payroll 0.56× league · TAB -6.3
-6.3TAB — talent above budget21st/26
-9.7WAB — realized wins (TAB + luck)22nd/26
6.0depth — 2+ WAR regulars14th/26
-8.6dead WAR — sub-replacement drag25th/26
62%dead money %26th/26
0.401win %25th/26
21.1roster fWAR26th/26
-3surplus $M (value vs cost)13th/26
$13Mpayroll $M25th/26

Roster

CFSteve Finley 124GGerald Young 78GKenny Lofton 20G
LFLuis Gonzalez 133GMark Davidson 32GCasey Candaele 18G
RFSteve Finley 72GKarl Rhodes 44GMike Simms 41G
SSEric Yelding 72GAndujar Cedeno 66GRafael Ramirez 45G
2BCasey Candaele 109GRafael Ramirez 27GAndy Mota 27G
3BKen Caminiti 152GCasey Candaele 11GGary Cooper 4G
1BJeff Bagwell 155GKen Oberkfell 13GJose Tolentino 10G
STARTERSPete Harnisch 3.1WARJimmy Jones 1.7WARMark Portugal 1.2WARRyan Bowen 0.8WARXavier Hernandez 0.4WAR
CCraig Biggio 139GCarl Nichols 17GScott Servais 14G
BULLPEN Al Osuna 12svCurt Schilling 0hDwayne Henry 0hJim Corsi 0hMike Capel 0hRob Mallicoat 0hDean Wilkins 0hChris Gardner 0h
CCraig Biggio 139 · Carl Nichols 17 · Scott Servais 14
1BJeff Bagwell 155 · Ken Oberkfell 13 · Jose Tolentino 10
2BCasey Candaele 109 · Rafael Ramirez 27 · Andy Mota 27
3BKen Caminiti 152 · Casey Candaele 11 · Gary Cooper 4
SSEric Yelding 72 · Andujar Cedeno 66 · Rafael Ramirez 45
LFLuis Gonzalez 133 · Mark Davidson 32 · Casey Candaele 18
CFSteve Finley 124 · Gerald Young 78 · Kenny Lofton 20
RFSteve Finley 72 · Karl Rhodes 44 · Mike Simms 41
SPPete Harnisch 3.1 · Jimmy Jones 1.7 · Mark Portugal 1.2 · Ryan Bowen 0.8 · Xavier Hernandez 0.4
BP Al Osuna 12 · Curt Schilling 0 · Dwayne Henry 0 · Jim Corsi 0 · Mike Capel 0 · Rob Mallicoat 0 · Dean Wilkins 0 · Chris Gardner 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.

1991 roster — value & surplus

PlayerAgeServiceGfWARSalary $MSurplus $M
Steve Finley 26 pre-arb 1594.5 0.3 +2.4
Jeff Bagwell 23 1564.3
Craig Biggio 25 arb 1493.5 0.4 +1.6
Luis Gonzalez 23 pre-arb 1373.4 0.1 +1.9
Pete Harnisch 24 arb 333.1 0.2 +1.6
Ken Caminiti 28 arb 1522.5 2.2 -0.7
Jimmy Jones 27 261.7
Casey Candaele 30 arb 1511.4 0.3 +0.5
Mark Portugal 28 FA-elig 331.2 0.7 +0.0
Curt Schilling 24 arb 561.0 0.1 +0.4
Ryan Bowen 23 170.8
Gerald Young 26 arb 1080.8 0.3 +0.1
Javier Ortiz 28 pre-arb 470.6 0.1 +0.2
Xavier Hernandez 25 pre-arb 320.4 0.1 +0.1
Jim Corsi 29 470.3
Carl Nichols 28 200.2
Ken Oberkfell 35 FA-elig 530.1 0.7 -0.6
Jim Deshaies 31 FA-elig 280.1 2.1 -2.1
Jose Tolentino 30 pre-arb 440.0 0.1 -0.1
Scott Servais 24 160.0
Mike Simms 24 49-0.1
Al Osuna 25 pre-arb 71-0.1 0.1 -0.1
Jeff Juden 20 4-0.1
Tony Eusebio 24 10-0.1
Kenny Lofton 24 pre-arb 20-0.1 0.1 -0.2
Mike Capel 29 25-0.1
Gary Cooper 26 9-0.1
Jim Clancy↔ ATL 35 30-0.2
Brian Williams 22 2-0.2
Mark Davidson 30 arb 85-0.2 0.3 -0.4
Rob Mallicoat 26 24-0.2
Mike Scott 36 FA-elig 2-0.3 2.4 -2.6
Dean Wilkins 24 7-0.3
Dave Rohde 27 29-0.4
Mark McLemore 26 arb 21-0.4 0.2 -0.4
Darryl Kile 22 pre-arb 37-0.4 0.1 -0.3
Chris Gardner 22 5-0.5
Andy Mota 25 27-0.5
Eric Anthony 23 pre-arb 39-0.5 0.1 -0.4
Andujar Cedeno 21 67-0.5
Karl Rhodes 22 pre-arb 44-0.6 0.1 -0.4
Dwayne Henry 29 FA-elig 52-0.7 0.1 -0.5
Rafael Ramirez 33 FA-elig 101-1.0 1.2 -1.8
Eric Yelding 26 pre-arb 78-1.2 0.3 -1.0

Key additions (new vs 1990)

PlayerfWAR
Steve Finley4.5
Jeff Bagwell4.3
Pete Harnisch3.1
Jimmy Jones1.7
Curt Schilling1.0
Ryan Bowen0.8

Key subtractions (gone from 1990)

Player1990 fWAR
Danny Darwin2.9
Franklin Stubbs2.7
Bill Doran2.6
Glenn Davis2.2
Larry Andersen1.6
Glenn Wilson1.0

Current & Historical Payroll

1991 payroll
$12.9M

What the front office was committed to as of 1991, 1991–1997. Pre-2015 has no contract-terms data — forward italic $ are realized actuals (hindsight, not commitments known then). Control labels (ARB/FA) are point-in-time.

current $ guaranteed $ actual $ (hindsight) arbitration (proj) pre-arb (proj) free agent

Guaranteed / signed (7)

PlayerAgeSvc1991199219931994199519961997
Mike Scott3612.000$2.4MFREE AGENT
Jim Deshaies317.000$2.1MFREE AGENT
Rafael Ramirez3311.000$1.2M$400kFREE AGENT
Mark Portugal286.000$705k$1.2M$2.1MFREE AGENT
Ken Oberkfell3514.000$675kFREE AGENT
Dwayne Henry297.000$100kFREE AGENT
Jim Clancy3514.000FREE AGENT

Arbitration-eligible (12)

PlayerAgeSvc1991199219931994199519961997
Ken Caminiti284.000$2.2MARB 3FREE AGENT
Craig Biggio253.000$438kARB 2ARB 3FREE AGENT
Casey Candaele305.000$350kFREE AGENT
Gerald Young264.000$340kARB 3FREE AGENT
Mark Davidson305.000$300kFREE AGENT
Pete Harnisch243.000$210kARB 2ARB 3FREE AGENT
Mark McLemore265.000$165kFREE AGENT
Curt Schilling243.000$125kARB 2ARB 3FREE AGENT
Mike Capel293.000ARB 2ARB 3FREE AGENT
Jim Corsi293.000ARB 2ARB 3FREE AGENT
Jimmy Jones275.000FREE AGENT
Carl Nichols285.000FREE AGENT

Pre-arbitration (25)

PlayerAgeSvc1991199219931994199519961997
Steve Finley262.000$280kARB 1ARB 2ARB 3FREE AGENT
Eric Yelding262.000$260kARB 1ARB 2ARB 3FREE AGENT
Xavier Hernandez252.000$122kARB 1ARB 2ARB 3FREE AGENT
Jose Tolentino300.000$118kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Javier Ortiz281.000$115kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Eric Anthony232.000$115kARB 1ARB 2ARB 3FREE AGENT
Karl Rhodes221.000$111kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Al Osuna251.000$106kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Luis Gonzalez231.000$105kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Darryl Kile220.000$100kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kenny Lofton240.000$100kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jeff Bagwell230.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ryan Bowen230.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Andujar Cedeno211.000Pre-ArbARB 1ARB 2ARB 3FREE AGENT
Gary Cooper26
Tony Eusebio240.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Chris Gardner22
Jeff Juden200.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Rob Mallicoat26
Andy Mota25
Dave Rohde271.000Pre-ArbARB 1ARB 2ARB 3FREE AGENT
Scott Servais240.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mike Simms241.000Pre-ArbARB 1ARB 2ARB 3FREE AGENT
Dean Wilkins242.000ARB 1ARB 2ARB 3FREE AGENT
Brian Williams220.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Astros 1991; 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 = 1991; 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", "C", "OF", "SP", "IF", "SP", "IF", "SP", "RP", "SP", "OF", "OF", "RP", "RP", "C", "IF", "SP", "IF", "C", "OF", "RP", "SP", "C", "OF", "RP", "IF", "RP", "SP", "OF", "RP", "SP", "RP", "IF", "IF", "SP", "SP", "IF", "OF", "IF"], "league": [5.94, 4.76, 3.83, 3.3, 2.9, 2.49, 2.19, 1.86, 1.71, 1.54, 1.27, 1.13, 1.02, 0.89, 0.75, 0.65, 0.53, 0.43, 0.33, 0.25, 0.2, 0.14, 0.09, 0.07, 0.03, 0.0, -0.02, -0.05, -0.08, -0.1, -0.14, -0.17, -0.23, -0.28, -0.32, -0.38, -0.43, -0.52, -0.49, -0.58], "names": ["Steve Finley", "Jeff Bagwell", "Craig Biggio", "Luis Gonzalez", "Pete Harnisch", "Ken Caminiti", "Jimmy Jones", "Casey Candaele", "Mark Portugal", "Curt Schilling", "Ryan Bowen", "Gerald Young", "Javier Ortiz", "Xavier Hernandez", "Jim Corsi", "Carl Nichols", "Ken Oberkfell", "Jim Deshaies", "Jose Tolentino", "Scott Servais", "Mike Simms", "Al Osuna", "Jeff Juden", "Tony Eusebio", "Kenny Lofton", "Mike Capel", "Gary Cooper", "Jim Clancy", "Brian Williams", "Mark Davidson", "Rob Mallicoat", "Mike Scott", "Dean Wilkins", "Dave Rohde", "Mark McLemore", "Darryl Kile", "Chris Gardner", "Andy Mota", "Eric Anthony", "Andujar Cedeno"], "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.48, 4.27, 3.45, 3.42, 3.12, 2.51, 1.67, 1.4, 1.24, 0.96, 0.78, 0.77, 0.55, 0.38, 0.26, 0.23, 0.07, 0.06, 0.04, 0.02, -0.06, -0.06, -0.08, -0.09, -0.1, -0.11, -0.12, -0.18, -0.19, -0.22, -0.25, -0.26, -0.32, -0.36, -0.39, -0.4, -0.46, -0.47, -0.49, -0.52]}; 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);});