RRosternomics

Red Sox BOS

GM: Lou Gorman

Overview

FORCED-SPEND ↗ rising
expensive & losing · year 1 in this regime · payroll 1.19× league · TAB -9.6
-9.6TAB — talent above budget24th/28
-5.7WAB — realized wins (TAB + luck)22nd/28
5.0depth — 2+ WAR regulars13th/28
-6.2dead WAR — sub-replacement drag25th/28
68%dead money %28th/28
0.470win %14th/28
18.9roster fWAR16th/28
-14surplus $M (value vs cost)24th/28
$38Mpayroll $M9th/28

Roster

CFOtis Nixon 103GLee Tinsley 26G
LFMike Greenwell 84GLee Tinsley 27GTom Brunansky 14G
RFWes Chamberlain 52GTom Brunansky 38GLee Tinsley 11G
SSJohn Valentin 83GCarlos Rodriguez 32GTim Naehring 8G
2BScott Fletcher 53GTim Naehring 49GCarlos Rodriguez 20G
3BScott Cooper 104GTim Naehring 11GStan Royer 8G
1BMo Vaughn 106GStan Royer 12GTim Naehring 8G
STARTERSRoger Clemens 4.2WARAaron Sele 2.4WARJoe Hesketh 2.3WARDanny Darwin 0.8WARNate Minchey 0.4WAR
CDamon Berryhill 67GRich Rowland 39G
DHAndre Dawson 73GWes Chamberlain 9GTim Naehring 7G
BULLPEN Ken Ryan 13svTony Fossas 0hChris Howard 0hSteve Farr 0hScott Bankhead 0hTodd Frohwirth 0hSergio Valdez 0hRicky Trlicek 0h
CDamon Berryhill 67 · Rich Rowland 39
1BMo Vaughn 106 · Stan Royer 12 · Tim Naehring 8
2BScott Fletcher 53 · Tim Naehring 49 · Carlos Rodriguez 20
3BScott Cooper 104 · Tim Naehring 11 · Stan Royer 8
SSJohn Valentin 83 · Carlos Rodriguez 32 · Tim Naehring 8
LFMike Greenwell 84 · Lee Tinsley 27 · Tom Brunansky 14
CFOtis Nixon 103 · Lee Tinsley 26
RFWes Chamberlain 52 · Tom Brunansky 38 · Lee Tinsley 11
DHAndre Dawson 73 · Wes Chamberlain 9 · Tim Naehring 7
SPRoger Clemens 4.2 · Aaron Sele 2.4 · Joe Hesketh 2.3 · Danny Darwin 0.8 · Nate Minchey 0.4
BP Ken Ryan 13 · Tony Fossas 0 · Chris Howard 0 · Steve Farr 0 · Scott Bankhead 0 · Todd Frohwirth 0 · Sergio Valdez 0 · Ricky Trlicek 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
Roger Clemens 31 FA-elig 244.2 5.2 -0.3
John Valentin 27 pre-arb 844.1 0.4 +4.3
Mo Vaughn 26 arb 1112.6 0.7 +2.3
Aaron Sele 24 pre-arb 222.4 0.2 +2.6
Joe Hesketh 35 FA-elig 252.3 1.8 +1.0
Scott Cooper 26 arb 1042.0 0.5 +1.8
Tim Naehring 27 arb 801.7 0.3 +1.6
Ken Ryan 25 pre-arb 421.1 0.2 +1.2
Danny Darwin 38 FA-elig 130.8 2.6 -1.8
Scott Fletcher 35 FA-elig 630.5 0.5 +0.1
Otis Nixon 35 FA-elig 1030.4 3.2 -2.8
Rich Rowland 30 arb 460.4 0.1 +0.4
Lee Tinsley 25 pre-arb 780.4 0.1 +0.4
Nate Minchey 24 pre-arb 60.4 0.1 +0.3
Tom Brunansky↔ MIL 33 FA-elig 480.3 0.8 -0.5
Gar Finnvold 26 80.3
Frank Viola 34 FA-elig 60.3 4.3 -4.0
Scott Bankhead 30 FA-elig 270.2 1.4 -1.2
Mike Greenwell 30 FA-elig 950.2 3.3 -3.1
Chris Howard 28 370.2
Carlos Rodriguez 26 arb 570.1 0.1 +0.0
Jeff Russell↔ CLE 32 FA-elig 290.1 2.1 -1.9
Chris Nabholz↔ CLE 27 arb 80.1 0.5 -0.5
Steve Farr↔ CLE 37 FA-elig 110.0 0.3 -0.3
Damon Berryhill 30 FA-elig 820.0 0.5 -0.5
Andy Tomberlin 27 180.0
Tim Van Egmond 25 pre-arb 7-0.0 0.1 -0.1
Paul Quantrill↔ PHI 25 pre-arb 17-0.0 0.1 -0.1
Tony Fossas 36 FA-elig 44-0.0 0.5 -0.5
Greg Harris↔ NYY 38 FA-elig 35-0.1 1.6 -1.7
Greg Blosser 23 pre-arb 5-0.1 0.1 -0.2
Eric Wedge 26 2-0.1
Stan Royer↔ STL 26 arb 4-0.2 0.0 -0.2
Cory Bailey 23 5-0.2
Jose Melendez 28 10-0.2
Todd Frohwirth 31 22-0.2
Luis Ortiz 24 7-0.3
Sergio Valdez 29 FA-elig 12-0.3 0.2 -0.5
Wes Chamberlain↔ PHI 28 arb 51-0.4 0.2 -0.7
Bob Zupcic↔ CHW 27 4-0.5
Ricky Trlicek 25 pre-arb 12-0.5 0.2 -0.7
Dave Valle↔ MIL 33 FA-elig 30-0.6 0.8 -1.5
Andre Dawson 39 FA-elig 75-0.7 4.4 -5.2
Greg Litton 29 11-0.8
Billy Hatcher↔ PHI 33 FA-elig 44-1.1 0.3 -1.6

Key additions (new vs 1993)

PlayerfWAR
Otis Nixon0.4
Rich Rowland0.4
Lee Tinsley0.4
Tom Brunansky0.3
Gar Finnvold0.3
Chris Howard0.2

Key subtractions (gone from 1993)

Player1993 fWAR
John Dopson1.8
Jeff Richardson0.4
Jeff McNeely0.2
Rob Deer0.1
Ivan Calderon0.0
Jim Byrd0.0

Current & Historical Payroll

1994 payroll
$37.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 (19)

PlayerAgeSvc1994199519961997199819992000
Roger Clemens3110.000$5.2M$5.7M$5.5MFREE AGENT
Andre Dawson3918.000$4.4MFREE AGENT
Frank Viola3412.000$4.3MFREE AGENT
Mike Greenwell309.000$3.3M$3.6M$3.7MFREE AGENT
Otis Nixon3511.000$3.2MFREE AGENT
Danny Darwin3816.000$2.6MFREE AGENT
Jeff Russell3211.000$2.1MFREE AGENT
Joe Hesketh3510.000$1.8MFREE AGENT
Greg Harris3813.000$1.6MFREE AGENT
Scott Bankhead308.000$1.4MFREE AGENT
Tom Brunansky3313.000$825kFREE AGENT
Dave Valle3310.000$783kFREE AGENT
Damon Berryhill307.000$500kFREE AGENT
Scott Fletcher3513.000$500kFREE AGENT
Tony Fossas366.000$475kFREE AGENT
Steve Farr3710.000$330kFREE AGENT
Billy Hatcher3310.000$329kFREE AGENT
Sergio Valdez298.000$170kFREE AGENT
Todd Frohwirth317.000FREE AGENT

Arbitration-eligible (12)

PlayerAgeSvc1994199519961997199819992000
Mo Vaughn263.000$675kARB 2ARB 3FREE AGENT
Chris Nabholz274.000$543kARB 3FREE AGENT
Scott Cooper264.000$500kARB 3FREE AGENT
Tim Naehring274.000$300kARB 3FREE AGENT
Wes Chamberlain284.000$238kARB 3FREE AGENT
Rich Rowland304.000$142kARB 3FREE AGENT
Carlos Rodriguez263.000$109kARB 2ARB 3FREE AGENT
Stan Royer263.000$12kARB 2ARB 3FREE AGENT
Greg Litton295.000FREE AGENT
Jose Melendez284.000ARB 3FREE AGENT
Eric Wedge263.000ARB 2ARB 3FREE AGENT
Bob Zupcic273.000ARB 2ARB 3FREE AGENT

Pre-arbitration (14)

PlayerAgeSvc1994199519961997199819992000
John Valentin272.000$370kARB 1ARB 2ARB 3FREE AGENT
Aaron Sele241.000$190kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ken Ryan252.000$180kARB 1ARB 2ARB 3FREE AGENT
Ricky Trlicek252.000$165kARB 1ARB 2ARB 3FREE AGENT
Nate Minchey241.000$122kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Lee Tinsley251.000$119kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Paul Quantrill252.000$114kARB 1ARB 2ARB 3FREE AGENT
Greg Blosser231.000$109kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tim Van Egmond250.000$109kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cory Bailey231.000Pre-ArbARB 1ARB 2ARB 3FREE AGENT
Gar Finnvold26
Chris Howard28
Luis Ortiz241.000Pre-ArbARB 1ARB 2ARB 3FREE AGENT
Andy Tomberlin271.000Pre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Red Sox 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.054, 10.87], [1986, 1.292, 3.24], [1987, 1.169, 1.08], [1988, 1.293, 13.08], [1989, 1.374, 5.57], [1990, 1.231, 2.11], [1991, 1.488, -1.16], [1992, 1.458, -15.31], [1993, 1.186, 0.61], [1994, 1.186, -9.6], [1995, 1.032, 20.02], [1996, 1.245, -4.96], [1997, 1.075, 6.38], [1998, 1.219, 10.04], [1999, 1.367, 9.76], [2000, 1.424, -1.54], [2001, 1.795, -7.73], [2002, 1.642, 7.11], [2003, 1.556, 14.58], [2004, 1.917, 3.35], [2005, 1.7, 0.86], [2006, 1.597, -10.36], [2007, 1.643, 7.96], [2008, 1.404, 12.69], [2009, 1.427, 3.85], [2010, 1.767, -0.69], [2011, 1.76, 10.08], [2012, 1.477, -12.69], [2013, 1.602, 11.59], [2014, 1.276, -9.14], [2015, 1.406, -8.56], [2016, 1.59, 10.1], [2017, 1.327, 1.92], [2018, 1.629, 11.2], [2019, 1.685, 3.78], [2021, 1.275, 4.95], [2022, 1.381, -9.52], [2023, 1.165, -8.21], [2024, 0.911, 3.57], [2025, 1.145, 7.85], [2026, 1.137, 5.14]], TEAMCOL = '#BD3039'; 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": ["SP", "IF", "IF", "SP", "SP", "IF", "IF", "RP", "SP", "IF", "OF", "C", "OF", "SP", "OF", "SP", "SP", "RP", "OF", "RP", "IF", "RP", "SP", "RP", "C", "OF", "SP", "RP", "RP", "RP", "OF", "IF", "IF", "RP", "RP", "RP", "IF", "RP", "OF", "OF"], "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": ["Roger Clemens", "John Valentin", "Mo Vaughn", "Aaron Sele", "Joe Hesketh", "Scott Cooper", "Tim Naehring", "Ken Ryan", "Danny Darwin", "Scott Fletcher", "Otis Nixon", "Rich Rowland", "Lee Tinsley", "Nate Minchey", "Tom Brunansky", "Gar Finnvold", "Frank Viola", "Scott Bankhead", "Mike Greenwell", "Chris Howard", "Carlos Rodriguez", "Jeff Russell", "Chris Nabholz", "Steve Farr", "Damon Berryhill", "Andy Tomberlin", "Tim Van Egmond", "Paul Quantrill", "Tony Fossas", "Greg Harris", "Greg Blosser", "Eric Wedge", "Stan Royer", "Cory Bailey", "Jose Melendez", "Todd Frohwirth", "Luis Ortiz", "Sergio Valdez", "Wes Chamberlain", "Bob Zupcic"], "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.17, 4.05, 2.58, 2.42, 2.34, 1.96, 1.68, 1.15, 0.77, 0.54, 0.43, 0.43, 0.41, 0.37, 0.31, 0.3, 0.28, 0.22, 0.19, 0.16, 0.13, 0.09, 0.07, 0.05, 0.01, 0.01, -0.01, -0.01, -0.02, -0.05, -0.09, -0.14, -0.15, -0.15, -0.21, -0.21, -0.28, -0.29, -0.36, -0.46]}; 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:'BOS',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);});