RRosternomics

Cubs CHC

GM: Larry Himes

Overview

FORCED-SPEND → steady
expensive & losing · year 1 in this regime · payroll 1.13× league · TAB -8.6
-8.6TAB — talent above budget19th/28
-9.2WAB — realized wins (TAB + luck)28th/28
1.0depth — 2+ WAR regulars26th/28
-2.1dead WAR — sub-replacement drag5th/28
44%dead money %27th/28
0.434win %26th/28
18.9roster fWAR17th/28
-14surplus $M (value vs cost)23rd/28
$36Mpayroll $M10th/28

Roster

CFKarl Rhodes 67GGlenallen Hill 44GSammy Sosa 15G
LFDerrick May 92GGlenallen Hill 31GKarl Rhodes 15G
RFSammy Sosa 98GEduardo Zambrano 18GKevin Roberson 9G
SSShawon Dunston 84GRey Sanchez 30GJosé Hernández 21G
2BRyne Sandberg 57GRey Sanchez 50GTodd Haney 11G
3BSteve Buechele 99GJosé Hernández 28GRey Sanchez 17G
1BMark Grace 103GEduardo Zambrano 9GSteve Buechele 6G
STARTERSJim Bullinger 1.8WARSteve Trachsel 1.6WARKevin Foster 1.3WARWillie Banks 1.2WARAnthony Young 1.1WAR
CRick Wilkins 95GMark Parent 37GMike Maksudian 2G
BULLPEN Randy Myers 21svJose Bautista 0hDan Plesac 0hChuck Crim 0hDave Otto 0hDonn Pall 0hRandy Veres 0hBlaise Ilsley 0h
CRick Wilkins 95 · Mark Parent 37 · Mike Maksudian 2
1BMark Grace 103 · Eduardo Zambrano 9 · Steve Buechele 6
2BRyne Sandberg 57 · Rey Sanchez 50 · Todd Haney 11
3BSteve Buechele 99 · José Hernández 28 · Rey Sanchez 17
SSShawon Dunston 84 · Rey Sanchez 30 · José Hernández 21
LFDerrick May 92 · Glenallen Hill 31 · Karl Rhodes 15
CFKarl Rhodes 67 · Glenallen Hill 44 · Sammy Sosa 15
RFSammy Sosa 98 · Eduardo Zambrano 18 · Kevin Roberson 9
SPJim Bullinger 1.8 · Steve Trachsel 1.6 · Kevin Foster 1.3 · Willie Banks 1.2 · Anthony Young 1.1
BP Randy Myers 21 · Jose Bautista 0 · Dan Plesac 0 · Chuck Crim 0 · Dave Otto 0 · Donn Pall 0 · Randy Veres 0 · Blaise Ilsley 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.

1994 roster — value & surplus

PlayerAgeServiceGfWARSalary $MSurplus $M
Sammy Sosa 25 arb 1053.8 3.0 +1.5
Mark Grace 30 FA-elig 1061.8 4.4 -2.3
Jim Bullinger 28 pre-arb 331.8 0.1 +1.9
Steve Trachsel 23 pre-arb 221.6 0.1 +1.7
Kevin Foster 25 pre-arb 131.3 0.1 +1.4
Willie Banks 25 arb 231.2 0.2 +1.2
Glenallen Hill 29 arb 891.2 1.0 +0.4
Anthony Young 28 arb 201.1 0.2 +1.1
Rick Wilkins 27 arb 1001.1 0.3 +0.9
Ryne Sandberg 34 FA-elig 570.9 6.0 -4.9
Eduardo Zambrano 28 pre-arb 670.8 0.1 +0.8
Derrick May 25 arb 1000.8 0.3 +0.6
Mark Parent 32 FA-elig 440.7 0.3 +0.6
Rey Sanchez 26 arb 960.7 0.2 +0.6
Randy Myers 31 FA-elig 380.5 3.6 -3.1
Karl Rhodes 25 arb 950.4 0.1 +0.4
Frank Castillo 25 arb 40.3 0.2 +0.2
Dan Plesac 32 FA-elig 540.3 1.7 -1.4
Mike Morgan 34 FA-elig 150.3 3.4 -3.1
Mike Maksudian 28 260.1
Shawn Boskie↔ PHI 27 arb 20.1 0.0 +0.1
Jose Guzman 31 FA-elig 40.1 3.5 -3.4
Kevin Roberson 26 440.0
Jose Bautista 29 FA-elig 58-0.0 0.7 -0.8
Donn Pall↔ NYY 32 FA-elig 2-0.0 0.0 -0.1
José Hernández 24 arb 56-0.1 0.1 -0.2
Blaise Ilsley 30 pre-arb 10-0.1 0.1 -0.3
Chuck Crim 32 49-0.1
Willie Wilson 38 FA-elig 17-0.2 0.7 -0.9
Turk Wendell 27 pre-arb 6-0.2 0.1 -0.3
Dave Otto 29 36-0.2
Steve Buechele 32 FA-elig 104-0.2 2.5 -2.8
Todd Haney 28 pre-arb 17-0.2 0.1 -0.4
Shawon Dunston 31 FA-elig 88-0.3 2.4 -2.7
Randy Veres 28 arb 10-0.4 0.2 -0.6

Key additions (new vs 1993)

PlayerfWAR
Kevin Foster1.3
Willie Banks1.2
Anthony Young1.1
Mark Parent0.7
Mike Maksudian0.1
Donn Pall-0.0

Key subtractions (gone from 1993)

Player1993 fWAR
Greg Hibbard1.9
Jose Vizcaino1.8
Dwight Smith1.7
Mike Harkey1.1
Steve Lake0.4
Bob Scanlan0.3

Current & Historical Payroll

1994 payroll
$35.9M

What the front office was committed to as of 1994, 1994–2000. 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 (14)

PlayerAgeSvc1994199519961997199819992000
Ryne Sandberg3413.000$6.0MFREE AGENT$2.3M$3.2M
Mark Grace306.000$4.4M$4.4M$4.2M$4.5M$4.2M$4.1M$5.3M
Randy Myers319.000$3.6M$4.2MFREE AGENT
Jose Guzman319.000$3.5MFREE AGENT
Mike Morgan3416.000$3.4M$2.7MFREE AGENT$1.0M
Steve Buechele329.000$2.5MFREE AGENT
Shawon Dunston319.000$2.4M$3.8MFREE AGENT$2.0M
Dan Plesac328.000$1.7MFREE AGENT
Jose Bautista296.000$710kFREE AGENT
Willie Wilson3818.000$700kFREE AGENT
Mark Parent328.000$260k$281kFREE AGENT
Donn Pall326.000$36kFREE AGENT
Chuck Crim327.000FREE AGENT
Dave Otto297.000FREE AGENT

Arbitration-eligible (12)

PlayerAgeSvc1994199519961997199819992000
Sammy Sosa255.000$3.0MFREE AGENT
Glenallen Hill295.000$1.0MFREE AGENT
Rick Wilkins273.000$350kARB 2ARB 3FREE AGENT
Derrick May254.000$300kARB 3FREE AGENT
Anthony Young283.000$230kARB 2ARB 3FREE AGENT
Rey Sanchez263.000$225kARB 2ARB 3FREE AGENT
Frank Castillo253.000$225kARB 2ARB 3FREE AGENT
Willie Banks253.000$190kARB 2ARB 3FREE AGENT
Randy Veres285.000$160kFREE AGENT
Karl Rhodes254.000$145kARB 3FREE AGENT
José Hernández243.000$112kARB 2ARB 3FREE AGENT
Shawn Boskie274.000$26kARB 3FREE AGENT

Pre-arbitration (9)

PlayerAgeSvc1994199519961997199819992000
Jim Bullinger282.000$150kARB 1ARB 2ARB 3FREE AGENT
Turk Wendell271.000$112kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Steve Trachsel231.000$112kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Eduardo Zambrano281.000$112kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Todd Haney282.000$109kARB 1ARB 2ARB 3FREE AGENT
Kevin Foster251.000$109kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Blaise Ilsley300.000$109kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mike Maksudian282.000ARB 1ARB 2ARB 3FREE AGENT
Kevin Roberson261.000Pre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Cubs 1994; 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.34, -7.32], [1986, 1.452, -13.12], [1987, 1.247, -10.73], [1988, 1.118, -5.1], [1989, 0.786, 5.14], [1990, 0.869, -9.92], [1991, 1.007, -1.28], [1992, 1.035, -8.39], [1993, 1.197, 3.58], [1994, 1.125, -8.56], [1995, 0.931, -0.06], [1996, 0.888, -1.68], [1997, 0.984, -11.53], [1998, 1.242, -0.6], [1999, 1.274, -17.33], [2000, 1.035, -10.85], [2001, 1.132, 7.09], [2002, 1.119, -1.01], [2003, 1.203, 4.39], [2004, 1.412, 9.26], [2005, 1.151, 0.49], [2006, 1.219, -12.5], [2007, 1.255, 1.89], [2008, 1.466, 11.85], [2009, 1.529, -6.3], [2010, 1.514, -11.95], [2011, 1.297, -12.21], [2012, 0.836, -16.47], [2013, 0.826, -6.52], [2014, 0.579, 8.0], [2015, 1.047, 13.54], [2016, 1.319, 16.52], [2017, 1.313, 6.36], [2018, 1.543, -0.13], [2019, 1.688, 1.13], [2021, 0.991, -11.38], [2022, 1.006, -8.21], [2023, 1.037, 6.72], [2024, 1.329, -1.62], [2025, 1.294, 7.16], [2026, 1.332, 0.41]], TEAMCOL = '#0E3386'; 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 = 1994; 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", "RP", "SP", "SP", "SP", "OF", "SP", "C", "IF", "OF", "OF", "C", "IF", "RP", "OF", "SP", "RP", "SP", "IF", "SP", "SP", "OF", "RP", "RP", "IF", "RP", "RP", "OF", "RP", "RP", "IF", "IF", "IF", "RP", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014"], "league": [4.46, 3.51, 2.81, 2.48, 2.13, 1.89, 1.57, 1.36, 1.16, 0.99, 0.88, 0.76, 0.67, 0.58, 0.49, 0.42, 0.35, 0.29, 0.24, 0.19, 0.11, 0.07, 0.03, -0.01, -0.04, -0.07, -0.11, -0.13, -0.18, -0.22, -0.31, -0.39, -0.38, -0.41, -0.49, -0.55, -0.56, -0.44, -0.58, -0.9], "names": ["Sammy Sosa", "Mark Grace", "Jim Bullinger", "Steve Trachsel", "Kevin Foster", "Willie Banks", "Glenallen Hill", "Anthony Young", "Rick Wilkins", "Ryne Sandberg", "Eduardo Zambrano", "Derrick May", "Mark Parent", "Rey Sanchez", "Randy Myers", "Karl Rhodes", "Frank Castillo", "Dan Plesac", "Mike Morgan", "Mike Maksudian", "Shawn Boskie", "Jose Guzman", "Kevin Roberson", "Jose Bautista", "Donn Pall", "Jos\u00e9 Hern\u00e1ndez", "Blaise Ilsley", "Chuck Crim", "Willie Wilson", "Turk Wendell", "Dave Otto", "Steve Buechele", "Todd Haney", "Shawon Dunston", "Randy Veres", "(empty slot)", "(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": [3.82, 1.82, 1.79, 1.56, 1.35, 1.22, 1.2, 1.11, 1.09, 0.89, 0.8, 0.8, 0.74, 0.69, 0.5, 0.44, 0.34, 0.29, 0.28, 0.13, 0.08, 0.08, 0.04, -0.04, -0.05, -0.07, -0.12, -0.13, -0.17, -0.2, -0.23, -0.23, -0.25, -0.29, -0.38, 0.0, 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:'CHC',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);});