RRosternomics

Cardinals STL

GM: Walt Jocketty

Overview

FORCED-SPEND ↘ cooling
expensive & losing · year 1 in this regime · payroll 1.02× league · TAB -3.0
-3.0TAB — talent above budget20th/30
-6.6WAB — realized wins (TAB + luck)22nd/30
6.0depth — 2+ WAR regulars17th/30
-6.0dead WAR — sub-replacement drag20th/30
38%dead money %23rd/30
0.466win %18th/30
31.2roster fWAR15th/30
-10surplus $M (value vs cost)18th/30
$50Mpayroll $M15th/30

Roster

CFJ.D. Drew 97GDarren Bragg 43GJoe McEwing 23G
LFRay Lankford 105GJoe McEwing 32GWillie McGee 30G
RFEric Davis 50GThomas Howard 45GWillie McGee 43G
SSEdgar Renteria 151GDavid Howard 13GPlacido Polanco 9G
2BJoe McEwing 96GPlacido Polanco 66GAdam Kennedy 29G
3BFernando Tatis 147GCraig Paquette 10GPlacido Polanco 9G
1BMark McGwire 151GEli Marrero 20GDavid Howard 9G
STARTERSDarren Oliver 4.1WARKent Bottenfield 2.2WARJose Jimenez 2.0WARRick Ankiel 0.9WARGarrett Stephenson 0.8WAR
CEli Marrero 96GAlberto Castillo 91GMarcus Jensen 14G
DHEric Davis 2GShawon Dunston 2GThomas Howard 1G
BULLPEN Ricky Bottalico 20svRick Croushore 14hManny Aybar 11hScott Radinsky 11hLance Painter 9hMike Mohler 6hHeathcliff Slocumb 5hMike Busby 4h
CEli Marrero 96 · Alberto Castillo 91 · Marcus Jensen 14
1BMark McGwire 151 · Eli Marrero 20 · David Howard 9
2BJoe McEwing 96 · Placido Polanco 66 · Adam Kennedy 29
3BFernando Tatis 147 · Craig Paquette 10 · Placido Polanco 9
SSEdgar Renteria 151 · David Howard 13 · Placido Polanco 9
LFRay Lankford 105 · Joe McEwing 32 · Willie McGee 30
CFJ.D. Drew 97 · Darren Bragg 43 · Joe McEwing 23
RFEric Davis 50 · Thomas Howard 45 · Willie McGee 43
DHEric Davis 2 · Shawon Dunston 2 · Thomas Howard 1
SPDarren Oliver 4.1 · Kent Bottenfield 2.2 · Jose Jimenez 2.0 · Rick Ankiel 0.9 · Garrett Stephenson 0.8
BP Ricky Bottalico 20 · Rick Croushore 14 · Manny Aybar 11 · Scott Radinsky 11 · Lance Painter 9 · Mike Mohler 6 · Heathcliff Slocumb 5 · Mike Busby 4

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.

1999 roster — value & surplus

PlayerAgeServiceGfWARSalary $MSurplus $M
Mark McGwire 35 FA-elig 1536.0 9.4 -1.8
Darren Oliver 28 FA-elig 354.1 3.5 +1.6
Ray Lankford 32 FA-elig 1223.8 6.6 -1.8
Fernando Tatis 24 pre-arb 1493.2 0.3 +3.8
J.D. Drew 23 pre-arb 1042.5 1.8 +1.3
Kent Bottenfield 30 FA-elig 312.2 0.8 +1.9
Jose Jimenez 25 pre-arb 312.0 0.2 +2.3
Joe McEwing 26 pre-arb 1521.7 0.2 +1.9
Edgar Renteria 22 arb 1541.2 2.0 -0.5
Darren Bragg 29 arb 931.1 0.8 +0.6
Alberto Castillo 29 arb 931.1 0.2 +1.1
Lance Painter 31 FA-elig 560.9 0.8 +0.3
Rick Ankiel 19 pre-arb 90.9 0.2 +0.9
Garrett Stephenson 27 arb 180.8 0.2 +0.8
Rick Croushore 28 pre-arb 590.8 0.2 +0.8
Craig Paquette 30 FA-elig 480.8 0.3 +0.7
Heathcliff Slocumb↔ BAL 33 FA-elig 400.7 0.9 +0.0
Kent Mercker↔ BOS 31 FA-elig 260.5 2.1 -1.4
Mark Thompson 28 50.5
Eduardo Perez 29 210.5
Thomas Howard 34 FA-elig 980.4 0.3 +0.2
Mike Mohler 30 FA-elig 480.4 0.5 -0.0
Donovan Osborne 30 FA-elig 60.3 5.1 -4.7
Manny Aybar 27 pre-arb 670.3 0.2 +0.2
Marcus Jensen 26 160.2
Shawon Dunston↔ NYM 36 FA-elig 620.2 0.4 -0.1
Eric Davis 37 FA-elig 580.1 3.4 -3.3
Ricky Bottalico 29 arb 680.1 2.3 -2.2
Alan Benes 27 arb 20.0 0.6 -0.5
Curtis King 28 pre-arb 20.0 0.2 -0.2
Scott Radinsky 31 FA-elig 430.0 2.5 -2.5
Larry Luebbers 29 FA-elig 8-0.0 0.2 -0.2
Luis Ordaz 23 pre-arb 10-0.1 0.2 -0.3
Clint Sodowsky 26 3-0.1
David Howard 32 FA-elig 52-0.2 0.9 -1.2
Mike Busby 26 arb 15-0.2 0.2 -0.5
Rick Heiserman 26 3-0.3
Adam Kennedy 23 pre-arb 33-0.3 0.2 -0.6
Juan Acevedo 29 arb 50-0.5 0.5 -1.1
Placido Polanco 23 88-0.7
Eli Marrero 25 pre-arb 114-0.9 0.2 -1.3
Willie McGee 40 FA-elig 132-2.6 1.0 -4.2

Key additions (new vs 1998)

PlayerfWAR
Edgar Renteria1.2
Darren Bragg1.1
Alberto Castillo1.1
Rick Ankiel0.9
Garrett Stephenson0.8
Craig Paquette0.8

Key subtractions (gone from 1998)

Player1998 fWAR
Brian Jordan6.9
Todd Stottlemyre3.1
Delino DeShields2.1
Matt Morris2.1
Ron Gant1.9
Gary Gaetti1.6

Current & Historical Payroll

1999 payroll
$49.5M

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

PlayerAgeSvc1999200020012002200320042005
Mark McGwire3513.000$9.4M$9.3M$11.0MFREE AGENT
Ray Lankford329.000$6.6M$7.6M$5.6MFREE AGENT$650k
Donovan Osborne307.000$5.1MFREE AGENT
Darren Oliver286.000$3.5MFREE AGENT
Eric Davis3715.000$3.4M$4.4MFREE AGENT
Scott Radinsky319.000$2.5M$2.5MFREE AGENT
Kent Mercker3110.000$2.1MFREE AGENT
Willie McGee4017.000$1.0MFREE AGENT
David Howard328.000$950kFREE AGENT
Heathcliff Slocumb338.000$880k$1.1MFREE AGENT
Kent Bottenfield307.000$850kFREE AGENT
Lance Painter316.000$800kFREE AGENT$575k
Mike Mohler306.000$490k$632kFREE AGENT
Shawon Dunston3614.000$364k$600kFREE AGENT
Thomas Howard349.000$325k$650kFREE AGENT
Craig Paquette306.000$300k$595k$1.5MFREE AGENT
Larry Luebbers296.000$212kFREE AGENT
Eduardo Perez296.000FREE AGENT$450k$500k

Arbitration-eligible (11)

PlayerAgeSvc1999200020012002200320042005
Ricky Bottalico295.000$2.3MFREE AGENT
Edgar Renteria223.000$2.0MARB 2ARB 3FREE AGENT
Darren Bragg295.000$800kFREE AGENT
Alan Benes274.000$550kARB 3FREE AGENT
Juan Acevedo294.000$475kARB 3FREE AGENT
Alberto Castillo294.000$225kARB 3FREE AGENT
Mike Busby263.000$220kARB 2ARB 3FREE AGENT
Garrett Stephenson273.000$215kARB 2ARB 3FREE AGENT
Marcus Jensen263.000ARB 2ARB 3FREE AGENT
Clint Sodowsky264.000ARB 3FREE AGENT
Mark Thompson285.000FREE AGENT

Pre-arbitration (13)

PlayerAgeSvc1999200020012002200320042005
J.D. Drew231.000$1.8MPre-ArbARB 1ARB 2ARB 3FREE AGENT
Fernando Tatis242.000$270kARB 1ARB 2ARB 3FREE AGENT
Manny Aybar272.000$210kARB 1ARB 2ARB 3FREE AGENT
Curtis King282.000$210kARB 1ARB 2ARB 3FREE AGENT
Eli Marrero252.000$210kARB 1ARB 2ARB 3FREE AGENT
Luis Ordaz232.000$210kARB 1ARB 2ARB 3FREE AGENT
Rick Croushore281.000$205kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Joe McEwing261.000$200kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Rick Ankiel190.000$200kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jose Jimenez251.000$200kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Adam Kennedy230.000$200kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Placido Polanco231.000Pre-ArbARB 1ARB 2ARB 3FREE AGENT
Rick Heiserman26

Graphs

Every roster slot ranked best→worst by WAR. Bars = Cardinals 1999; 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.069, 16.47], [1986, 0.875, -4.71], [1987, 1.046, 1.67], [1988, 1.247, -7.23], [1989, 1.131, 10.44], [1990, 1.154, 1.97], [1991, 0.867, 1.78], [1992, 0.878, 3.34], [1993, 0.717, 4.95], [1994, 0.905, -2.82], [1995, 0.956, -8.39], [1996, 1.202, 0.25], [1997, 1.131, 5.0], [1998, 1.286, 3.59], [1999, 1.017, -3.03], [2000, 1.215, 9.66], [2001, 1.174, 7.13], [2002, 1.208, 8.04], [2003, 1.243, 4.92], [2004, 1.361, 12.09], [2005, 1.271, 5.55], [2006, 1.249, -5.69], [2007, 1.152, -8.12], [2008, 1.055, 7.7], [2009, 1.17, 5.07], [2010, 1.151, 0.15], [2011, 1.224, 4.54], [2012, 1.192, 8.25], [2013, 0.905, 9.76], [2014, 1.059, -0.95], [2015, 1.068, 9.12], [2016, 1.083, 6.89], [2017, 1.113, 5.24], [2018, 1.183, -3.7], [2019, 1.166, 0.06], [2021, 1.331, 0.51], [2022, 1.154, 9.59], [2023, 1.131, -2.95], [2024, 1.109, -3.12], [2025, 0.777, -0.79], [2026, 0.344, 7.3]], TEAMCOL = '#C41E3A'; 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 = 1999; 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": ["IF", "SP", "OF", "IF", "OF", "SP", "SP", "IF", "IF", "OF", "C", "RP", "SP", "SP", "RP", "OF", "RP", "SP", "SP", "OF", "OF", "RP", "SP", "RP", "C", "OF", "OF", "RP", "RP", "RP", "RP", "SP", "IF", "RP", "IF", "RP", "RP", "IF", "RP", "IF"], "league": [5.79, 4.64, 3.89, 3.49, 2.91, 2.58, 2.16, 1.93, 1.7, 1.4, 1.25, 1.06, 0.93, 0.8, 0.71, 0.61, 0.53, 0.45, 0.38, 0.34, 0.27, 0.21, 0.17, 0.14, 0.09, 0.05, 0.02, -0.0, -0.03, -0.07, -0.09, -0.12, -0.15, -0.21, -0.25, -0.25, -0.36, -0.32, -0.36, -0.51], "names": ["Mark McGwire", "Darren Oliver", "Ray Lankford", "Fernando Tatis", "J.D. Drew", "Kent Bottenfield", "Jose Jimenez", "Joe McEwing", "Edgar Renteria", "Darren Bragg", "Alberto Castillo", "Lance Painter", "Rick Ankiel", "Garrett Stephenson", "Rick Croushore", "Craig Paquette", "Heathcliff Slocumb", "Kent Mercker", "Mark Thompson", "Eduardo Perez", "Thomas Howard", "Mike Mohler", "Donovan Osborne", "Manny Aybar", "Marcus Jensen", "Shawon Dunston", "Eric Davis", "Ricky Bottalico", "Alan Benes", "Curtis King", "Scott Radinsky", "Larry Luebbers", "Luis Ordaz", "Clint Sodowsky", "David Howard", "Mike Busby", "Rick Heiserman", "Adam Kennedy", "Juan Acevedo", "Placido Polanco"], "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.05, 4.06, 3.82, 3.24, 2.49, 2.2, 1.96, 1.66, 1.2, 1.1, 1.07, 0.87, 0.86, 0.79, 0.78, 0.77, 0.72, 0.53, 0.45, 0.45, 0.42, 0.37, 0.32, 0.32, 0.2, 0.2, 0.11, 0.06, 0.04, 0.03, 0.02, -0.03, -0.09, -0.12, -0.18, -0.23, -0.25, -0.31, -0.52, -0.73]}; 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:'STL',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);});