RRosternomics

Marlins MIA

President of baseball ops: Peter Bendix · GM: Gabe Kapler

Overview

WINDOW ↗ rising
cheap & winning · year 2 in this regime · payroll 0.37× league · TAB +14.0
+14.0TAB — talent above budget3rd/30
+7.4WAB — realized wins (TAB + luck)6th/30
4.0depth — 2+ WAR regulars7th/30
-2.5dead WAR — sub-replacement drag6th/30
46%dead money %21st/30
0.500win %15th/30
36.3roster fWAR11th/30
+171surplus $M (value vs cost)1st/30
$59Mpayroll $M28th/30luxury tax $51M

Roster

CFJakob Marsee 65GEsteury Ruiz 7GJavier Sanoja 1G
LFHeriberto Hernández 40GKyle Stowers 29GEsteury Ruiz 11G
RFOwen Caissie 55GEsteury Ruiz 19GKyle Stowers 18G
SSOtto Lopez 69GJavier Sanoja 4GLeo Jiménez 1G
2BXavier Edwards 68GLeo Jiménez 7GJavier Sanoja 3G
3BJavier Sanoja 44GGraham Pauley 30GLeo Jiménez 13G
1BConnor Norby 40GLiam Hicks 18GChristopher Morel 14G
STARTERSMax Meyer 2.4WARSandy Alcantara 2.2WAREury Pérez 1.4WARJanson Junk 0.8WAR
CJoe Mack 30GLiam Hicks 29GAgustín Ramírez 17G
DHLiam Hicks 23GAgustín Ramírez 13GConnor Norby 12G
BULLPEN Pete Fairbanks 8svMichael Petersen 9hAndrew Nardi 8hAnthony Bender 7hCalvin Faucher 6hJohn King 3hLake Bachar 1hTyler Phillips 0h
CJoe Mack 30 · Liam Hicks 29 · Agustín Ramírez 17
1BConnor Norby 40 · Liam Hicks 18 · Christopher Morel 14
2BXavier Edwards 68 · Leo Jiménez 7 · Javier Sanoja 3
3BJavier Sanoja 44 · Graham Pauley 30 · Leo Jiménez 13
SSOtto Lopez 69 · Javier Sanoja 4 · Leo Jiménez 1
LFHeriberto Hernández 40 · Kyle Stowers 29 · Esteury Ruiz 11
CFJakob Marsee 65 · Esteury Ruiz 7 · Javier Sanoja 1
RFOwen Caissie 55 · Esteury Ruiz 19 · Kyle Stowers 18
DHLiam Hicks 23 · Agustín Ramírez 13 · Connor Norby 12
SPMax Meyer 2.4 · Sandy Alcantara 2.2 · Eury Pérez 1.4 · Janson Junk 0.8
BP Pete Fairbanks 8 · Michael Petersen 9 · Andrew Nardi 8 · Anthony Bender 7 · Calvin Faucher 6 · John King 3 · Lake Bachar 1 · Tyler Phillips 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.

2026 roster — value & surplus

PlayerAgeServiceGfWARSalary $MSurplus $M
Otto Lopez 27 pre-arb 1006.7 0.8 +41.3
Xavier Edwards 26 pre-arb 1044.0 0.8 +24.2
Max Meyer 27 pre-arb 203.8 1.0 +22.7
Sandy Alcantara 30 FA-elig 223.4 17.3 +4.3
Joe Mack 23 602.5 0.6 +15.3
Liam Hicks 27 pre-arb 922.3 0.8 +14.0
Eury Pérez 23 pre-arb 172.2 0.8 +13.0
Heriberto Hernández 26 pre-arb 741.8 0.7 +10.7
Esteury Ruiz 27 pre-arb 641.7 0.8 +9.9
Javier Sanoja 23 pre-arb 861.3 0.8 +7.6
Anthony Bender 31 arb 351.3 2.8 +5.6
Michael Petersen 32 pre-arb 421.3 0.8 +7.5
Janson Junk 30 pre-arb 131.2 0.8 +6.8
Kyle Stowers 28 pre-arb 791.2 0.8 +6.5
Griffin Conine 28 pre-arb 371.1 0.8 +6.2
Calvin Faucher 30 pre-arb 440.9 1.8 +3.8
Ryan Gusto 27 pre-arb 120.9 0.0 +5.4
Lake Bachar 31 pre-arb 350.6 0.8 +3.1
Tyler Phillips 28 pre-arb 260.5 0.8 +2.5
Cade Gibson 28 pre-arb 210.3 0.6 +1.5
John King 31 arb 410.3 1.5 +0.2
Dax Fulton 24 40.3
Connor Norby 26 pre-arb 650.3 0.8 +0.8
William Kempner 25 90.1 0.0 +0.8
Chris Paddack↔ CIN 30 70.1 0.3 +0.5
Jakob Marsee 25 pre-arb 990.1 0.8 -0.4
Brian Navarreto 31 70.0
Deyvison De Los Santos 23 30.0 0.1 +0.1
Zach Brzykcy 26 20.0 0.0 +0.1
Josh Ekness 24 60.0 0.1 +0.0
Agustín Ramírez 24 pre-arb 31-0.0 0.2 -0.4
Braxton Garrett 28 arb 2-0.0 1.5 -1.8
Andrew Nardi 27 arb 25-0.1 0.8 -1.2
Austin Slater↔ NYM 33 12-0.1 0.3 -0.7
Rece Hinds↔ CIN 25 pre-arb 1-0.1 0.1 -0.6
Robby Snelling 22 1-0.1 0.6 -1.4
Josh White 25 1-0.2 0.8 -2.1
Tyler Zuber 31 17-0.3 0.5 -2.6
Owen Caissie 23 pre-arb 80-0.4 0.8 -3.2
Pete Fairbanks 32 FA-elig 33-0.4 13.0 -15.6
Leo Jiménez 25 47-0.4
Graham Pauley 25 pre-arb 38-0.5 0.7 -3.8
Christopher Morel 27 arb 22-1.2 2.0 -9.9

Key additions (new vs 2025)

PlayerfWAR
Esteury Ruiz1.1
John King0.2
Chris Paddack0.1
Deyvison De Los Santos0.0
Zach Brzykcy0.0
Braxton Garrett-0.0

Key subtractions (gone from 2025)

Player2025 fWAR
Edward Cabrera2.0
Jesús Sánchez1.1
Cal Quantrill0.8
Nick Fortes0.6
Derek Hill0.5
Joey Wiemer0.4

Current & Historical Payroll

2026 payroll
$59.1M
luxury tax $51M
2027 guaranteed
$21.0M

What the front office was committed to as of 2026, 2026–2032. Guaranteed $ are contract commitments signed by 2026; arbitration & free-agent years are projected from service time. Hover a figure for its luxury-tax (CBT) & actual-cash values.

current $ guaranteed $ arbitration (proj) pre-arb (proj) free agent club option player option mutual option vesting option

Summary — luxury-tax (CBT) basis

2026202720282029203020312032
Tax threshold$244M$247M$250M$253M$256M$259M$262M
Guaranteed contracts$28M$21M
Proj. arbitration$5M
Proj. pre-arbitration$18M
Player benefits$18M$18M
Total CBT payroll$69M$39M
Tax space+$175M+$208M
Est. tax bill

Luxury-tax (CBT) basis: contract AAV + ~$18M benefits/bonus-pool — distinct from the base-salary payroll above. Threshold is the CBA line. Tier-1 payor rates on the overage. Forward years show only guaranteed deals (arb/pre-arb not yet set). Figures are estimates.

Guaranteed / signed (4)

PlayerAgeSvc2026202720282029203020312032
Sandy Alcantara307.100$17.3M$21.0MCLUBFREE AGENT
Pete Fairbanks326.057$13.0MFREE AGENT
Chris Paddack307.000$286kFREE AGENT
Austin Slater337.147$254kFREE AGENT

Arbitration-eligible (6)

PlayerAgeSvc2026202720282029203020312032
Anthony Bender314.153$2.8MARB 3FREE AGENT
Christopher Morel273.117$2.0MARB 2ARB 3FREE AGENT
Braxton Garrett283.168$1.5MARB 2ARB 3FREE AGENT
John King314.148$1.5MARB 3FREE AGENT
Andrew Nardi273.053$800kARB 2ARB 3FREE AGENT
Tyler Zuber313.104$492kARB 2ARB 3FREE AGENT

Pre-arbitration (27)

PlayerAgeSvc2026202720282029203020312032
Calvin Faucher302.156$1.8MARB 1ARB 2ARB 3FREE AGENT
Max Meyer272.166$980kARB 1ARB 2ARB 3FREE AGENT
Otto Lopez272.109$810kARB 1ARB 2ARB 3FREE AGENT
Kyle Stowers282.012$804kARB 1ARB 2ARB 3FREE AGENT
Xavier Edwards262.034$804kARB 1ARB 2ARB 3FREE AGENT
Eury Pérez232.113$800kARB 1ARB 2ARB 3FREE AGENT
Janson Junk301.045$798kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Connor Norby261.057$794kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tyler Phillips281.048$794kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Michael Petersen320.086$792kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Javier Sanoja231.023$792kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Esteury Ruiz272.064$792kARB 1ARB 2ARB 3FREE AGENT
Lake Bachar311.021$792kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Liam Hicks271.000$790kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jakob Marsee250.059$786kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Griffin Conine281.035$786kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Owen Caissie230.034$782kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Heriberto Hernández260.122$746kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Graham Pauley251.005$730kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cade Gibson280.135$559kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Agustín Ramírez240.161$174kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Deyvison De Los Santos23$66k
Rece Hinds250.089$60kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ryan Gusto270.167$13kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Zach Brzykcy261.029$4kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brian Navarreto311.033Pre-ArbARB 1ARB 2ARB 3FREE AGENT
Leo Jiménez25

Graphs

Every roster slot ranked best→worst by WAR. Bars = Marlins 2026; 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 = 2026; 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", "IF", "SP", "SP", "C", "C", "SP", "OF", "OF", "IF", "RP", "RP", "SP", "OF", "OF", "RP", "SP", "RP", "RP", "RP", "RP", "RP", "IF", "RP", "SP", "OF", "C", "IF", "RP", "RP", "C", "SP", "RP", "OF", "OF", "SP", "RP", "RP", "OF", "RP"], "league": [3.73, 2.8, 2.36, 2.09, 1.83, 1.64, 1.41, 1.24, 1.09, 0.96, 0.87, 0.78, 0.68, 0.62, 0.56, 0.49, 0.42, 0.36, 0.3, 0.22, 0.19, 0.16, 0.12, 0.1, 0.08, 0.06, 0.04, 0.03, 0.01, -0.01, -0.03, -0.04, -0.07, -0.09, -0.1, -0.12, -0.15, -0.18, -0.22, -0.25], "names": ["Otto Lopez", "Xavier Edwards", "Max Meyer", "Sandy Alcantara", "Joe Mack", "Liam Hicks", "Eury P\u00e9rez", "Heriberto Hern\u00e1ndez", "Esteury Ruiz", "Javier Sanoja", "Anthony Bender", "Michael Petersen", "Janson Junk", "Kyle Stowers", "Griffin Conine", "Calvin Faucher", "Ryan Gusto", "Lake Bachar", "Tyler Phillips", "Cade Gibson", "John King", "Dax Fulton", "Connor Norby", "William Kempner", "Chris Paddack", "Jakob Marsee", "Brian Navarreto", "Deyvison De Los Santos", "Zach Brzykcy", "Josh Ekness", "Agust\u00edn Ram\u00edrez", "Braxton Garrett", "Andrew Nardi", "Austin Slater", "Rece Hinds", "Robby Snelling", "Josh White", "Tyler Zuber", "Owen Caissie", "Pete Fairbanks"], "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.29, 2.55, 2.41, 2.2, 1.62, 1.51, 1.41, 1.17, 1.09, 0.86, 0.86, 0.85, 0.77, 0.74, 0.71, 0.57, 0.55, 0.4, 0.33, 0.21, 0.18, 0.17, 0.17, 0.08, 0.08, 0.04, 0.02, 0.02, 0.01, 0.01, -0.02, -0.03, -0.04, -0.04, -0.06, -0.08, -0.13, -0.21, -0.25, -0.27]}; 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);});