RRosternomics

Braves ATL

GM: John Schuerholz

Overview

CONTEND ↘ cooling
expensive & winning · year 11 in this regime · payroll 1.38× league · TAB +3.4
+3.4TAB — talent above budget13th/30
+16.1WAB — realized wins (TAB + luck)4th/30
8.0depth — 2+ WAR regulars9th/30
-3.8dead WAR — sub-replacement drag14th/30
13%dead money %7th/30
0.631win %3rd/30
41.0roster fWAR10th/30
-20surplus $M (value vs cost)21st/30
$91Mpayroll $M5th/30

2002: made playoffs · 101–59. Ranks are vs the 30 teams that season.

Roster

CFAndruw Jones 154GDarren Bragg 18G
LFChipper Jones 152GDarren Bragg 12GWes Helms 5G
RFGary Sheffield 127GDarren Bragg 36GB.J. Surhoff 9G
SSRafael Furcal 150GMark DeRosa 19GJesse Garcia 5G
2BKeith Lockhart 89GMarcus Giles 52GMark DeRosa 32G
3BVinny Castilla 139GWes Helms 24GMarcus Giles 8G
1BJulio Franco 95GMatt Franco 51GWes Helms 45G
STARTERSKevin Millwood 5.0WARGreg Maddux 4.1WARTom Glavine 2.5WARDamian Moss 0.9WARJason Marquis 0.2WAR
CJavy Lopez 103GHenry Blanco 79GSteve Torrealba 12G
DHGary Sheffield 4GDarren Bragg 2GJulio Franco 2G
BULLPEN John Smoltz 55svMike Remlinger 30hChris Hammond 16hKevin Gryboski 11hTim Spooneybarger 11hDarren Holmes 7hKerry Ligtenberg 3hAlbie Lopez 1h
CJavy Lopez 103 · Henry Blanco 79 · Steve Torrealba 12
1BJulio Franco 95 · Matt Franco 51 · Wes Helms 45
2BKeith Lockhart 89 · Marcus Giles 52 · Mark DeRosa 32
3BVinny Castilla 139 · Wes Helms 24 · Marcus Giles 8
SSRafael Furcal 150 · Mark DeRosa 19 · Jesse Garcia 5
LFChipper Jones 152 · Darren Bragg 12 · Wes Helms 5
CFAndruw Jones 154 · Darren Bragg 18
RFGary Sheffield 127 · Darren Bragg 36 · B.J. Surhoff 9
DHGary Sheffield 4 · Darren Bragg 2 · Julio Franco 2
SPKevin Millwood 5.0 · Greg Maddux 4.1 · Tom Glavine 2.5 · Damian Moss 0.9 · Jason Marquis 0.2
BP John Smoltz 55 · Mike Remlinger 30 · Chris Hammond 16 · Kevin Gryboski 11 · Tim Spooneybarger 11 · Darren Holmes 7 · Kerry Ligtenberg 3 · Albie Lopez 1

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.

2002 roster — value & surplus

PlayerAgeServiceGfWARSalary $MSurplus $M
Andruw Jones 25 FA-elig 1546.3 10.0 +0.8
Chipper Jones Jr. 30 FA-elig 1585.9 11.3 -1.2
Kevin Millwood 27 arb 355.0 3.9 +4.7
Gary Sheffield 33 FA-elig 1354.8 9.9 -1.7
Greg Maddux 36 FA-elig 354.1 13.1 -6.1
Rafael Furcal 24 pre-arb 1542.7 0.4 +4.1
Tom Glavine 36 FA-elig 372.5 8.6 -4.3
John Smoltz 35 FA-elig 752.5 7.7 -3.4
Chris Hammond 36 FA-elig 631.8 0.5 +2.7
Mike Remlinger 36 FA-elig 731.5 2.0 +0.6
Darren Holmes 36 FA-elig 551.0 0.3 +1.4
Matt Franco 32 811.0
Damian Moss 25 pre-arb 330.9 0.2 +1.4
Marcus Giles 24 pre-arb 680.8 0.2 +1.2
Albie Lopez 30 FA-elig 300.8 4.0 -2.6
Julio Franco 43 FA-elig 1250.7 0.6 +0.6
Javy Lopez 31 FA-elig 1090.6 6.0 -5.0
Mark DeRosa 27 arb 720.6 0.2 +0.7
B.J. Surhoff 37 FA-elig 250.3 4.5 -3.9
Kerry Ligtenberg 31 arb 520.3 1.7 -1.2
Darren Bragg 32 1090.3
Jason Marquis 23 pre-arb 280.2 0.2 +0.1
Jung Keun Bong 21 20.1
Wes Helms 26 arb 850.0 0.2 -0.2
Joe Dawley 30 10.0
John Ennis 22 10.0
Aaron Small 30 1-0.0
Henry Blanco 30 arb 81-0.0 1.5 -1.6
Tim Spooneybarger 22 pre-arb 51-0.0 0.2 -0.3
Andy Pratt 22 1-0.1
Ryan Langerhans 22 1-0.1
Trey Hodges 24 4-0.1
John Foster 24 5-0.3
Steve Torrealba 24 13-0.3
Keith Lockhart 37 FA-elig 128-0.3 0.6 -1.1
Jesse Garcia 28 39-0.3
Kevin Gryboski 28 57-0.8
Vinny Castilla 34 FA-elig 143-1.5 3.0 -5.5

Key additions (new vs 2001)

PlayerfWAR
Gary Sheffield4.8
Chris Hammond1.8
Darren Holmes1.0
Matt Franco1.0
Albie Lopez0.8
Darren Bragg0.3

Key subtractions (gone from 2001)

Player2001 fWAR
John Burkett4.8
Brian Jordan4.2
Odalis Perez1.8
Quilvio Veras1.1
John Rocker0.7
Steve Karsay0.5

Current & Historical Payroll

2002 payroll
$91.0M

What the front office was committed to as of 2002, 2002–2008. 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 (18)

PlayerAgeSvc2002200320042005200620072008
Greg Maddux3616.000$13.1M$14.8MFREE AGENT
Chipper Jones Jr.309.000$11.3M$13.3M$15.3M$16.1M$12.3M$12.3M$12.3M
Andruw Jones256.000$10.0M$12.0M$12.5M$13.0M$13.5M$14.0MFREE AGENT
Gary Sheffield3314.000$9.9M$11.4MFREE AGENT
Tom Glavine3615.000$8.6MFREE AGENT$8.0M
John Smoltz3514.000$7.7M$10.7M$11.7M$9.0M$11.0M$8.0M$14.0M
Javy Lopez3110.000$6.0M$7.0MFREE AGENT
B.J. Surhoff3715.000$4.5MFREE AGENT
Albie Lopez309.000$4.0MFREE AGENT
Vinny Castilla3411.000$3.0M$5.0MFREE AGENT
Mike Remlinger3611.000$2.0MFREE AGENT$700k
Julio Franco4320.000$600k$600k$750k$1.0MFREE AGENT$836k
Keith Lockhart378.000$600kFREE AGENT
Chris Hammond3612.000$450kFREE AGENT
Darren Holmes3612.000$325k$700kFREE AGENT
Darren Bragg328.000$450kFREE AGENT
Matt Franco327.000$800kFREE AGENT
Aaron Small308.000FREE AGENT

Arbitration-eligible (6)

PlayerAgeSvc2002200320042005200620072008
Kevin Millwood275.000$3.9MFREE AGENT
Kerry Ligtenberg315.000$1.7MFREE AGENT
Henry Blanco305.000$1.5MFREE AGENT
Wes Helms264.000$250kARB 3FREE AGENT
Mark DeRosa274.000$222kARB 3FREE AGENT
Jesse Garcia283.000ARB 2ARB 3FREE AGENT

Pre-arbitration (14)

PlayerAgeSvc2002200320042005200620072008
Rafael Furcal242.000$405kARB 1ARB 2ARB 3FREE AGENT
Jason Marquis232.000$230kARB 1ARB 2ARB 3FREE AGENT
Damian Moss251.000$215kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Marcus Giles241.000$210kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tim Spooneybarger221.000$200kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Steve Torrealba24
Jung Keun Bong210.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
John Ennis22
John Foster240.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ryan Langerhans220.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kevin Gryboski280.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Andy Pratt220.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Trey Hodges240.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Joe Dawley300.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Braves 2002; 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, 1.431, -23.61], [1986, 1.401, -14.45], [1987, 1.273, -13.43], [1988, 1.03, -21.95], [1989, 0.616, -0.3], [1990, 0.757, -6.68], [1991, 0.705, 2.11], [1992, 1.131, 8.31], [1993, 1.367, 18.33], [1994, 1.367, 16.3], [1995, 1.423, 8.57], [1996, 1.549, 15.1], [1997, 1.317, 22.84], [1998, 1.466, 25.7], [1999, 1.4, 13.27], [2000, 1.486, 7.8], [2001, 1.531, 2.68], [2002, 1.381, 3.37], [2003, 1.507, 11.61], [2004, 1.359, 7.23], [2005, 1.232, 7.29], [2006, 1.02, 0.18], [2007, 0.939, 6.79], [2008, 1.142, -3.51], [2009, 1.101, 6.18], [2010, 1.027, 4.02], [2011, 0.985, -1.77], [2012, 0.885, 5.76], [2013, 0.897, 10.67], [2014, 0.911, -1.0], [2015, 0.791, -14.54], [2016, 0.584, -6.39], [2017, 0.954, -10.45], [2018, 0.699, 9.92], [2019, 1.138, 2.66], [2021, 1.137, 3.69], [2022, 1.274, 14.31], [2023, 1.283, 20.39], [2024, 1.525, 3.14], [2025, 1.218, -5.85], [2026, 1.416, -2.74]], TEAMCOL = '#13274F'; 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 = 2002; 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", "OF", "SP", "OF", "SP", "IF", "SP", "RP", "RP", "RP", "RP", "IF", "SP", "IF", "RP", "IF", "C", "IF", "IF", "RP", "OF", "SP", "SP", "IF", "RP", "SP", "RP", "C", "RP", "RP", "OF", "RP", "RP", "C", "IF", "IF", "RP", "IF", "\u2014", "\u2014"], "league": [6.03, 4.57, 3.76, 3.19, 2.68, 2.32, 2.09, 1.88, 1.64, 1.49, 1.27, 1.11, 0.95, 0.86, 0.76, 0.68, 0.59, 0.48, 0.42, 0.34, 0.3, 0.25, 0.19, 0.15, 0.1, 0.07, 0.03, 0.0, -0.02, -0.05, -0.08, -0.11, -0.14, -0.18, -0.22, -0.23, -0.33, -0.42, -0.38, -0.39], "names": ["Andruw Jones", "Chipper Jones Jr.", "Kevin Millwood", "Gary Sheffield", "Greg Maddux", "Rafael Furcal", "Tom Glavine", "John Smoltz", "Chris Hammond", "Mike Remlinger", "Darren Holmes", "Matt Franco", "Damian Moss", "Marcus Giles", "Albie Lopez", "Julio Franco", "Javy Lopez", "Mark DeRosa", "B.J. Surhoff", "Kerry Ligtenberg", "Darren Bragg", "Jason Marquis", "Jung Keun Bong", "Wes Helms", "Joe Dawley", "John Ennis", "Aaron Small", "Henry Blanco", "Tim Spooneybarger", "Andy Pratt", "Ryan Langerhans", "Trey Hodges", "John Foster", "Steve Torrealba", "Keith Lockhart", "Jesse Garcia", "Kevin Gryboski", "Vinny Castilla", "(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.3, 5.89, 4.99, 4.81, 4.05, 2.65, 2.54, 2.5, 1.83, 1.52, 0.99, 0.96, 0.94, 0.83, 0.81, 0.68, 0.58, 0.56, 0.35, 0.31, 0.28, 0.21, 0.14, 0.02, 0.01, 0.0, -0.02, -0.03, -0.05, -0.08, -0.09, -0.1, -0.27, -0.28, -0.29, -0.31, -0.8, -1.48, 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:'ATL',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);});