RRosternomics

Astros HOU

GM: Tim Purpura

Overview

CONTEND ↘ cooling
expensive & winning · year 5 in this regime · payroll 1.06× league · TAB +5.7
+5.7TAB — talent above budget8th/30
+6.3WAB — realized wins (TAB + luck)7th/30
9.0depth — 2+ WAR regulars3rd/30
-4.2dead WAR — sub-replacement drag13th/30
30%dead money %15th/30
0.549win %8th/30
40.7roster fWAR9th/30
+3surplus $M (value vs cost)8th/30
$77Mpayroll $M11th/30

2005: won pennant · 89–73. Ranks are vs the 30 teams that season.

Roster

CFWilly Taveras 148GEric Bruntlett 14GJason Lane 6G
LFChris Burke 83GOrlando Palmeiro 47GLance Berkman 39G
RFJason Lane 137GOrlando Palmeiro 26GLance Berkman 11G
SSAdam Everett 150GJose Vizcaino 17GEric Bruntlett 10G
2BCraig Biggio 141GEric Bruntlett 28GJose Vizcaino 23G
3BMorgan Ensberg 148GMike Lamb 15GEric Bruntlett 8G
1BLance Berkman 96GMike Lamb 68GJeff Bagwell 24G
STARTERSRoger Clemens 6.4WARRoy Oswalt 6.1WARAndy Pettitte 5.2WARBrandon Backe 1.6WARWandy Rodriguez 0.4WAR
CBrad Ausmus 134GRaúl Chávez 36GHumberto Quintero 16G
DHCraig Biggio 5GLance Berkman 3GMike Lamb 1G
BULLPEN Brad Lidge 42svChad Qualls 22hDan Wheeler 17hRuss Springer 10hMike Gallo 8hJohn Franco 6hMike Burns 1hBrandon Duckworth 0h
CBrad Ausmus 134 · Raúl Chávez 36 · Humberto Quintero 16
1BLance Berkman 96 · Mike Lamb 68 · Jeff Bagwell 24
2BCraig Biggio 141 · Eric Bruntlett 28 · Jose Vizcaino 23
3BMorgan Ensberg 148 · Mike Lamb 15 · Eric Bruntlett 8
SSAdam Everett 150 · Jose Vizcaino 17 · Eric Bruntlett 10
LFChris Burke 83 · Orlando Palmeiro 47 · Lance Berkman 39
CFWilly Taveras 148 · Eric Bruntlett 14 · Jason Lane 6
RFJason Lane 137 · Orlando Palmeiro 26 · Lance Berkman 11
DHCraig Biggio 5 · Lance Berkman 3 · Mike Lamb 1
SPRoger Clemens 6.4 · Roy Oswalt 6.1 · Andy Pettitte 5.2 · Brandon Backe 1.6 · Wandy Rodriguez 0.4
BP Brad Lidge 42 · Chad Qualls 22 · Dan Wheeler 17 · Russ Springer 10 · Mike Gallo 8 · John Franco 6 · Mike Burns 1 · Brandon Duckworth 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.

2005 roster — value & surplus

PlayerAgeServiceGfWARSalary $MSurplus $M
Roger Clemens 42 FA-elig 326.4 18.0 -5.9
Roy Oswalt 27 arb 356.1 5.9 +5.7
Morgan Ensberg 29 arb 1506.1 0.5 +11.0
Andy Pettitte 33 FA-elig 335.2 8.5 +1.4
Lance Berkman 29 FA-elig 1322.7 10.5 -5.3
Craig Biggio 39 FA-elig 1552.7 3.0 +2.1
Brad Lidge 28 arb 702.5 0.5 +4.3
Adam Everett 28 arb 1522.2 0.4 +3.7
Willy Taveras 23 pre-arb 1522.2 0.3 +3.8
Brandon Backe 27 arb 281.6 0.3 +2.6
Chris Burke 25 pre-arb 1081.3 0.3 +2.1
Dan Wheeler 27 FA-elig 711.1 0.4 +1.8
Jason Lane 28 arb 1451.1 0.3 +1.7
Orlando Palmeiro 36 FA-elig 1140.8 0.8 +0.7
Chad Qualls 26 pre-arb 770.7 0.3 +1.0
Eric Bruntlett 27 pre-arb 910.6 0.3 +0.8
Brad Ausmus 36 FA-elig 1340.6 3.0 -1.9
Wandy Rodriguez 26 250.4 0.1 +0.7
John Franco 44 FA-elig 310.3 0.7 -0.1
Russ Springer 36 FA-elig 620.2 0.6 -0.2
Travis Driskill 33 10.0
Mike Gallo 28 360.0
Jeff Bagwell 37 FA-elig 390.0 18.0 -18.0
Charles Gipson 32 190.0
Charlton Jimerson 25 10.0
Brandon Duckworth 29 arb 7-0.1 0.5 -0.8
Scott Strickland 29 5-0.2
Mike Burns 26 27-0.2
Jose Vizcaino 37 FA-elig 98-0.3 1.2 -1.8
Todd Self 26 21-0.3
Luke Scott 27 pre-arb 34-0.3 0.3 -0.9
Raul Chavez 32 FA-elig 37-0.4 0.4 -1.1
Humberto Quintero 25 18-0.5
Chad Harville↔ BOS 28 FA-elig 37-0.5 0.3 -1.2
Ezequiel Astacio 25 22-0.6
Mike Lamb 29 arb 125-0.8 1.3 -2.7

Key additions (new vs 2004)

PlayerfWAR
Wandy Rodriguez0.4
John Franco0.3
Travis Driskill0.0
Charles Gipson0.0
Charlton Jimerson0.0
Scott Strickland-0.2

Key subtractions (gone from 2004)

Player2004 fWAR
Jeff Kent4.2
Carlos Beltrán4.0
Wade Miller1.3
Dan Miceli1.2
Pete Munro0.9
Octavio Dotel0.7

Current & Historical Payroll

2005 payroll
$76.8M

What the front office was committed to as of 2005, 2005–2011. 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 (15)

PlayerAgeSvc2005200620072008200920102011
Jeff Bagwell3714.000$18.0MFREE AGENT
Roger Clemens4221.000$18.0M$12.2MFREE AGENT
Lance Berkman296.000$10.5M$14.5M$14.5M$14.5M$14.5M$10.1MFREE AGENT
Andy Pettitte3310.000$8.5M$16.4MFREE AGENT
Craig Biggio3917.000$3.0M$4.0M$5.2MFREE AGENT
Brad Ausmus3612.000$3.0MFREE AGENT$4.0M$2.0M
Jose Vizcaino3716.000$1.2MFREE AGENT
Orlando Palmeiro3610.000$800k$950k$950kFREE AGENT
John Franco4421.000$700kFREE AGENT
Russ Springer3613.000$550k$750kFREE AGENT
Dan Wheeler276.000$365k$930k$1.4MFREE AGENT
Raul Chavez329.000$360kFREE AGENT
Chad Harville286.000$300kFREE AGENT
Charles Gipson327.000FREE AGENT
Scott Strickland296.000FREE AGENT

Arbitration-eligible (8)

PlayerAgeSvc2005200620072008200920102011
Roy Oswalt274.000$5.9MARB 3FREE AGENT
Mike Lamb295.000$1.3MFREE AGENT
Brandon Duckworth294.000$500kARB 3FREE AGENT
Brad Lidge283.000$500kARB 2ARB 3FREE AGENT
Morgan Ensberg295.000$450kFREE AGENT
Adam Everett284.000$445kARB 3FREE AGENT
Brandon Backe273.000$350kARB 2ARB 3FREE AGENT
Jason Lane283.000$345kARB 2ARB 3FREE AGENT

Pre-arbitration (13)

PlayerAgeSvc2005200620072008200920102011
Eric Bruntlett272.000$335kARB 1ARB 2ARB 3FREE AGENT
Chad Qualls261.000$316kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Chris Burke251.000$316kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Luke Scott270.000$316kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Willy Taveras231.000$316kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Wandy Rodriguez260.000$104kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Travis Driskill33
Humberto Quintero252.000ARB 1ARB 2ARB 3FREE AGENT
Mike Gallo282.000ARB 1ARB 2ARB 3FREE AGENT
Ezequiel Astacio25
Charlton Jimerson250.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mike Burns260.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Todd Self26

Graphs

Every roster slot ranked best→worst by WAR. Bars = Astros 2005; 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 = 2005; 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", "IF", "SP", "IF", "IF", "RP", "IF", "OF", "SP", "OF", "RP", "OF", "OF", "RP", "IF", "C", "SP", "RP", "RP", "RP", "RP", "IF", "OF", "OF", "RP", "RP", "RP", "IF", "OF", "OF", "C", "C", "RP", "SP", "IF", "\u2014", "\u2014", "\u2014", "\u2014"], "league": [5.57, 4.25, 3.64, 3.25, 2.82, 2.5, 2.26, 1.98, 1.78, 1.52, 1.35, 1.22, 1.02, 0.88, 0.77, 0.66, 0.54, 0.46, 0.39, 0.31, 0.25, 0.2, 0.15, 0.12, 0.09, 0.05, 0.03, 0.0, -0.02, -0.04, -0.07, -0.1, -0.13, -0.16, -0.21, -0.25, -0.3, -0.33, -0.31, -0.41], "names": ["Roger Clemens", "Roy Oswalt", "Morgan Ensberg", "Andy Pettitte", "Lance Berkman", "Craig Biggio", "Brad Lidge", "Adam Everett", "Willy Taveras", "Brandon Backe", "Chris Burke", "Dan Wheeler", "Jason Lane", "Orlando Palmeiro", "Chad Qualls", "Eric Bruntlett", "Brad Ausmus", "Wandy Rodriguez", "John Franco", "Russ Springer", "Travis Driskill", "Mike Gallo", "Jeff Bagwell", "Charles Gipson", "Charlton Jimerson", "Brandon Duckworth", "Scott Strickland", "Mike Burns", "Jose Vizcaino", "Todd Self", "Luke Scott", "Raul Chavez", "Humberto Quintero", "Chad Harville", "Ezequiel Astacio", "Mike Lamb", "(empty slot)", "(empty slot)", "(empty slot)", "(empty slot)"], "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": [6.4, 6.12, 6.06, 5.22, 2.74, 2.72, 2.51, 2.22, 2.18, 1.56, 1.27, 1.13, 1.1, 0.79, 0.68, 0.58, 0.57, 0.41, 0.3, 0.18, 0.04, 0.03, 0.01, 0.01, 0.0, -0.14, -0.16, -0.24, -0.27, -0.32, -0.32, -0.37, -0.46, -0.49, -0.62, -0.76, 0.0, 0.0, 0.0, 0.0]}; 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);});