RRosternomics

Marlins MIA

GM: Dave Dombrowski

Overview

FORCED-SPEND ↘ cooling
expensive & losing · year 1 in this regime · payroll 1.27× league · TAB +0.0
+0.0TAB — talent above budget13th/28
+7.0WAB — realized wins (TAB + luck)7th/28
6.0depth — 2+ WAR regulars13th/28
-4.0dead WAR — sub-replacement drag9th/28
7%dead money %2nd/28
0.568win %4th/28
37.1roster fWAR10th/28
-12surplus $M (value vs cost)18th/28
$50Mpayroll $M7th/28

1997: World Series champions · 92–70. Ranks are vs the 28 teams that season.

Roster

CFDevon White 71GMoises Alou 55GJohn Cangelosi 23G
LFMoises Alou 90GJim Eisenreich 43GJohn Cangelosi 34G
RFGary Sheffield 132GDarren Daulton 73GMoises Alou 22G
SSEdgar Renteria 153GAlex Arias 11GKurt Abbott 7G
2BLuis Castillo 70GKurt Abbott 54GCraig Counsell 51G
3BBobby Bonilla 149GAlex Arias 37GJohn Wehner 6G
1BJeff Conine 145GDarren Daulton 42GJim Eisenreich 29G
STARTERSKevin Brown 6.4WARAlex Fernandez 3.7WARLivan Hernandez 2.0WARAl Leiter 1.1WARTony Saunders 0.9WAR
CCharles Johnson 123GGregg Zaun 50GRob Natal 4G
DHDarren Daulton 7GJim Eisenreich 4GBobby Bonilla 3G
BULLPEN Robb Nen 35svJay Powell 0hEd Vosberg 0hDennis Cook 0hFélix Heredia 0hRob Stanifer 0hAntonio Alfonseca 0hKurt Miller 0h
CCharles Johnson 123 · Gregg Zaun 50 · Rob Natal 4
1BJeff Conine 145 · Darren Daulton 42 · Jim Eisenreich 29
2BLuis Castillo 70 · Kurt Abbott 54 · Craig Counsell 51
3BBobby Bonilla 149 · Alex Arias 37 · John Wehner 6
SSEdgar Renteria 153 · Alex Arias 11 · Kurt Abbott 7
LFMoises Alou 90 · Jim Eisenreich 43 · John Cangelosi 34
CFDevon White 71 · Moises Alou 55 · John Cangelosi 23
RFGary Sheffield 132 · Darren Daulton 73 · Moises Alou 22
DHDarren Daulton 7 · Jim Eisenreich 4 · Bobby Bonilla 3
SPKevin Brown 6.4 · Alex Fernandez 3.7 · Livan Hernandez 2.0 · Al Leiter 1.1 · Tony Saunders 0.9
BP Robb Nen 35 · Jay Powell 0 · Ed Vosberg 0 · Dennis Cook 0 · Félix Heredia 0 · Rob Stanifer 0 · Antonio Alfonseca 0 · Kurt Miller 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.

1997 roster — value & surplus

PlayerAgeServiceGfWARSalary $MSurplus $M
Kevin Brown 32 FA-elig 336.4 4.5 +2.1
Charles Johnson 25 arb 1244.8 0.3 +4.6
Alex Fernandez 27 FA-elig 333.7 7.0 -3.2
Moises Alou 30 FA-elig 1503.3 4.5 -1.1
Bobby Bonilla 34 FA-elig 1532.6 5.6 -2.9
Gary Sheffield 28 FA-elig 1352.4 6.1 -3.7
Livan Hernandez 22 pre-arb 172.0 1.1 +1.0
Jeff Conine 31 FA-elig 1511.7 2.8 -1.1
Devon White 34 FA-elig 741.3 3.4 -2.1
Jay Powell 25 pre-arb 741.3 0.2 +1.1
Gregg Zaun 26 pre-arb 581.2 0.2 +1.0
Craig Counsell 26 pre-arb 511.1 0.1 +1.0
Al Leiter 31 FA-elig 271.1 2.9 -1.7
Dennis Cook 34 FA-elig 611.1 0.8 +0.3
Edgar Renteria 20 pre-arb 1541.0 0.1 +0.9
Tony Saunders 23 pre-arb 220.9 0.1 +0.8
John Cangelosi 34 FA-elig 1030.7 0.5 +0.2
Pat Rapp↔ SFG 29 arb 190.7 0.8 -0.1
Cliff Floyd 24 arb 610.6 0.4 +0.3
Robb Nen 27 arb 730.6 3.1 -2.4
Darren Daulton↔ PHI 35 FA-elig 520.6 1.8 -1.3
Felix Heredia 22 pre-arb 560.5 0.2 +0.4
Kurt Abbott 28 arb 940.3 0.7 -0.3
Jim Eisenreich 38 FA-elig 1200.2 1.4 -1.2
Rob Natal 31 40.2
Todd Dunwoody 22 190.2
Ralph Milliard 23 80.2
Josh Booty 22 40.1
Russ Morman 35 40.1
Mark Kotsay 21 140.1
Ed Vosberg↔ TEX 35 FA-elig 170.1 0.1 -0.1
Matt Whisenant↔ KCR 26 pre-arb 5-0.0 0.0 -0.1
Antonio Alfonseca 25 pre-arb 17-0.1 0.1 -0.2
Alex Arias 29 arb 74-0.1 0.5 -0.6
Donn Pall 35 2-0.2
Kirt Ojala 28 7-0.2
Billy McMillon↔ PHI 25 pre-arb 13-0.3 0.1 -0.3
Kurt Miller 24 arb 7-0.3 0.2 -0.4
Mark Hutton↔ COL 27 arb 32-0.4 0.2 -0.5
John Wehner 30 44-0.4
Rob Stanifer 25 36-0.5
Luis Castillo 21 pre-arb 75-0.7 0.2 -0.9
Rick Helling↔ TEX 26 arb 31-0.9 0.1 -1.0

Key additions (new vs 1996)

PlayerfWAR
Alex Fernandez3.7
Moises Alou3.3
Bobby Bonilla2.6
Craig Counsell1.1
Dennis Cook1.1
Tony Saunders0.9

Key subtractions (gone from 1996)

Player1996 fWAR
John Burkett2.5
Greg Colbrunn1.7
Quilvio Veras1.0
Yorkis Perez0.4
David Weathers0.4
Jerry Brooks0.0

Current & Historical Payroll

1997 payroll
$50.1M

What the front office was committed to as of 1997, 1997–2003. 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 (16)

PlayerAgeSvc1997199819992000200120022003
Alex Fernandez277.000$7.0MFREE AGENT$7.0M$7.0M
Gary Sheffield289.000$6.1M$10.3MFREE AGENT
Bobby Bonilla3411.000$5.6M$4.2MFREE AGENT
Kevin Brown3211.000$4.5MFREE AGENT
Moises Alou307.000$4.5MFREE AGENT
Devon White3412.000$3.4MFREE AGENT
Al Leiter3110.000$2.9MFREE AGENT
Jeff Conine317.000$2.8MFREE AGENT$3.5M
Darren Daulton3514.000$1.8MFREE AGENT
Jim Eisenreich3815.000$1.4M$1.0MFREE AGENT
Dennis Cook349.000$850kFREE AGENT
John Cangelosi3412.000$525k$550kFREE AGENT
Ed Vosberg3511.000$130kFREE AGENT
Russ Morman3511.000FREE AGENT
Donn Pall359.000$200kFREE AGENT
John Wehner306.000$275kFREE AGENT

Arbitration-eligible (10)

PlayerAgeSvc1997199819992000200120022003
Robb Nen274.000$3.1MARB 3FREE AGENT
Pat Rapp295.000$792kFREE AGENT
Kurt Abbott284.000$650kARB 3FREE AGENT
Alex Arias295.000$450kFREE AGENT
Cliff Floyd244.000$390kARB 3FREE AGENT
Charles Johnson253.000$290kARB 2ARB 3FREE AGENT
Mark Hutton274.000$164kARB 3FREE AGENT
Kurt Miller243.000$163kARB 2ARB 3FREE AGENT
Rick Helling263.000$125kARB 2ARB 3FREE AGENT
Rob Natal315.000FREE AGENT

Pre-arbitration (17)

PlayerAgeSvc1997199819992000200120022003
Livan Hernandez221.000$1.1MPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jay Powell252.000$208kARB 1ARB 2ARB 3FREE AGENT
Gregg Zaun262.000$205kARB 1ARB 2ARB 3FREE AGENT
Luis Castillo211.000$154kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Felix Heredia221.000$153kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Antonio Alfonseca250.000$150kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Craig Counsell262.000$150kARB 1ARB 2ARB 3FREE AGENT
Tony Saunders230.000$150kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Edgar Renteria201.000$110kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Billy McMillon251.000$54kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Matt Whisenant260.000$26kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Josh Booty221.000Pre-ArbARB 1ARB 2ARB 3FREE AGENT
Todd Dunwoody220.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mark Kotsay210.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ralph Milliard23
Kirt Ojala280.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Rob Stanifer250.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Marlins 1997; 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 = [[1993, 0.647, -10.58], [1994, 0.676, -4.63], [1995, 0.725, 2.75], [1996, 0.904, 4.83], [1997, 1.273, 0.0], [1998, 0.432, -12.7], [1999, 0.405, -3.55], [2000, 0.388, 4.95], [2001, 0.443, 9.98], [2002, 0.649, 2.52], [2003, 0.766, 13.62], [2004, 0.721, 4.46], [2005, 0.817, 7.6], [2006, 0.207, 17.02], [2007, 0.433, 4.85], [2008, 0.31, 13.82], [2009, 0.443, 9.25], [2010, 0.613, 3.69], [2011, 0.708, -1.02], [2012, 1.146, -11.81], [2013, 0.347, -3.2], [2014, 0.407, 9.68], [2015, 0.557, 0.31], [2016, 0.6, 8.22], [2017, 0.839, -1.27], [2018, 0.632, -14.09], [2019, 0.571, -18.95], [2021, 0.399, 1.3], [2022, 0.557, -3.9], [2023, 0.697, -0.89], [2024, 0.463, -9.31], [2025, 0.309, 5.82], [2026, 0.37, 14.0]], TEAMCOL = '#00A3E0'; 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 = 1997; 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", "C", "SP", "OF", "IF", "OF", "SP", "IF", "OF", "RP", "C", "IF", "SP", "RP", "IF", "SP", "OF", "SP", "OF", "RP", "OF", "RP", "IF", "OF", "C", "OF", "IF", "IF", "OF", "OF", "RP", "RP", "RP", "IF", "RP", "SP", "OF", "RP", "RP", "OF"], "league": [6.5, 4.54, 3.62, 3.26, 2.81, 2.46, 2.13, 1.82, 1.64, 1.38, 1.18, 1.07, 0.93, 0.85, 0.74, 0.64, 0.54, 0.47, 0.41, 0.34, 0.28, 0.23, 0.17, 0.13, 0.1, 0.07, 0.04, 0.01, -0.01, -0.04, -0.06, -0.09, -0.13, -0.16, -0.19, -0.22, -0.27, -0.33, -0.36, -0.42], "names": ["Kevin Brown", "Charles Johnson", "Alex Fernandez", "Moises Alou", "Bobby Bonilla", "Gary Sheffield", "Livan Hernandez", "Jeff Conine", "Devon White", "Jay Powell", "Gregg Zaun", "Craig Counsell", "Al Leiter", "Dennis Cook", "Edgar Renteria", "Tony Saunders", "John Cangelosi", "Pat Rapp", "Cliff Floyd", "Robb Nen", "Darren Daulton", "Felix Heredia", "Kurt Abbott", "Jim Eisenreich", "Rob Natal", "Todd Dunwoody", "Ralph Milliard", "Josh Booty", "Russ Morman", "Mark Kotsay", "Ed Vosberg", "Matt Whisenant", "Antonio Alfonseca", "Alex Arias", "Donn Pall", "Kirt Ojala", "Billy McMillon", "Kurt Miller", "Mark Hutton", "John Wehner"], "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.45, 4.75, 3.68, 3.28, 2.65, 2.39, 1.96, 1.68, 1.27, 1.27, 1.15, 1.14, 1.13, 1.09, 1.01, 0.91, 0.68, 0.65, 0.65, 0.64, 0.57, 0.5, 0.34, 0.23, 0.23, 0.22, 0.19, 0.14, 0.11, 0.09, 0.06, -0.04, -0.06, -0.14, -0.18, -0.18, -0.26, -0.26, -0.37, -0.38]}; 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:'MIA',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);});