RRosternomics

Rays TBR

President of baseball ops: Erik Neander

Overview

WINDOW ↗ rising
cheap & winning · year 19 in this regime · payroll 0.46× league · TAB +13.9
+13.9TAB — talent above budget4th/30
+14.0WAB — realized wins (TAB + luck)2nd/30
4.0depth — 2+ WAR regulars7th/30
-2.3dead WAR — sub-replacement drag4th/30
28%dead money %9th/30
0.583win %4th/30
38.9roster fWAR6th/30
+170surplus $M (value vs cost)2nd/30
$74Mpayroll $M26th/30luxury tax $103M

Roster

CFCedric Mullins 57GJonny DeLuca 11GVictor Mesa Jr. 1G
LFChandler Simpson 62GRyan Vilade 8GAustin Slater 5G
RFJonny DeLuca 29GJake Fraley 27GRyan Vilade 19G
SSTaylor Walls 50GBen Williamson 11GCarson Williams 8G
2BRichie Palacios 44GBen Williamson 35GCarson Williams 2G
3BJunior Caminero 64GBen Williamson 6GOliver Dunn 3G
1BJonathan Aranda 61GRyan Vilade 10GJunior Caminero 1G
STARTERSDrew Rasmussen 2.5WARShane McClanahan 2.2WARNick Martinez 1.9WARGriffin Jax 0.7WARSteven Matz -0.0WAR
CNick Fortes 52GHunter Feduccia 35G
DHYandy Díaz 62GJonathan Aranda 3GJonny DeLuca 2G
BULLPEN Bryan Baker 18svKevin Kelly 14hIan Seymour 10hGarrett Cleavinger 8hHunter Bigge 5hCole Sulser 4hCasey Legumina 3hCraig Kimbrel 1h
CNick Fortes 52 · Hunter Feduccia 35
1BJonathan Aranda 61 · Ryan Vilade 10 · Junior Caminero 1
2BRichie Palacios 44 · Ben Williamson 35 · Carson Williams 2
3BJunior Caminero 64 · Ben Williamson 6 · Oliver Dunn 3
SSTaylor Walls 50 · Ben Williamson 11 · Carson Williams 8
LFChandler Simpson 62 · Ryan Vilade 8 · Austin Slater 5
CFCedric Mullins 57 · Jonny DeLuca 11 · Victor Mesa Jr. 1
RFJonny DeLuca 29 · Jake Fraley 27 · Ryan Vilade 19
DHYandy Díaz 62 · Jonathan Aranda 3 · Jonny DeLuca 2
SPDrew Rasmussen 2.5 · Shane McClanahan 2.2 · Nick Martinez 1.9 · Griffin Jax 0.7 · Steven Matz -0.0
BP Bryan Baker 18 · Kevin Kelly 14 · Ian Seymour 10 · Garrett Cleavinger 8 · Hunter Bigge 5 · Cole Sulser 4 · Casey Legumina 3 · Craig Kimbrel 1

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
Junior Caminero 22 pre-arb 1035.4 0.8 +33.5
Drew Rasmussen 30 arb 193.9 5.8 +19.1
Shane McClanahan 29 arb 193.5 3.6 +18.3
Yandy Díaz 34 FA-elig 993.4 12.0 +9.4
Jonathan Aranda 28 pre-arb 1023.1 0.8 +19.0
Nick Martinez 35 FA-elig 193.0 9.0 +10.1
Chandler Simpson 25 pre-arb 972.4 0.8 +14.1
Jonny DeLuca 27 pre-arb 631.9 0.8 +11.0
Nick Fortes 29 arb 781.6 2.5 +7.8
Bryan Baker 31 arb 401.6 1.3 +9.0
Cedric Mullins 31 FA-elig 891.4 6.5 +2.6
Victor Mesa Jr. 24 391.3 0.5 +7.4
Ian Seymour 27 pre-arb 351.2 0.8 +6.5
Ryan Vilade 27 pre-arb 561.1 0.9 +5.8
Kevin Kelly 28 pre-arb 431.1 0.9 +5.7
Griffin Jax 31 arb 271.0 3.6 +3.0
Taylor Walls 29 arb 820.9 2.5 +3.5
Joe Boyle 26 pre-arb 30.8 0.2 +4.8
Cole Sulser 36 arb 400.8 1.1 +3.9
Garrett Cleavinger 32 arb 330.6 2.4 +1.2
Ben Williamson 25 pre-arb 750.5 0.8 +2.2
Cam Booser 34 190.4 0.5 +2.0
Hunter Feduccia 29 pre-arb 560.3 0.8 +1.4
Richie Palacios 29 760.3
Michael Grove 29 arb 10.3 0.8 +0.9
Craig Kimbrel↔ NYM 38 FA-elig 160.2 0.3 +1.0
Jake Fraley 31 arb 280.2 3.0 -1.8
Austin Slater↔ MIA 33 70.2 0.1 +1.0
Hunter Bigge 28 pre-arb 180.1 0.8 -0.3
Casey Legumina↔ SEA 29 pre-arb 260.0 0.5 -0.3
Chase Solesky 28 1-0.0 0.0 -0.2
Steven Matz 35 FA-elig 15-0.0 7.5 -7.7
Jonathan Heasley 29 1-0.1
Andrew Wantz 30 1-0.2 0.0 -1.0
Oliver Dunn 28 pre-arb 9-0.2 0.5 -1.6
Chris Roycroft↔ STL 29 6-0.2 0.0 -1.4
Mason Englert 26 pre-arb 14-0.2 0.2 -1.6
Jesse Scholtens 32 pre-arb 9-0.3 0.7 -2.7
Trevor Martin 25 6-0.5 0.1 -3.0
Aaron Brooks 36 pre-arb 1-0.5 0.0 -3.4
Yoendrys Gómez↔ MIN 26 9-0.5 0.1 -3.5
Carson Williams 23 pre-arb 12-0.8 0.6 -5.4

Key additions (new vs 2025)

PlayerfWAR
Shane McClanahan2.2
Nick Martinez1.9
Cedric Mullins0.9
Victor Mesa Jr.0.8
Ryan Vilade0.7
Ben Williamson0.3

Key subtractions (gone from 2025)

Player2025 fWAR
Shane Baz2.0
Jake Mangum1.8
Brandon Lowe1.7
Taj Bradley1.5
Pete Fairbanks1.0
José Caballero0.9

Current & Historical Payroll

2026 payroll
$74.1M
luxury tax $103M
2027 guaranteed
$59.9M

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$65M$60M$17M$17M$17M$17M
Proj. arbitration$27M$1M
Proj. pre-arbitration$12M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$121M$80M$35M$35M$35M$35M
Tax space+$123M+$167M+$215M+$218M+$221M+$224M
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 (8)

PlayerAgeSvc2026202720282029203020312032
Yandy Díaz347.122$12.0MCLUB$10.0MCLUBFREE AGENT
Nick Martinez357.002$9.0M$20.0MMUTUALFREE AGENT
Steven Matz3510.099$7.5M$7.5MFREE AGENT
Cedric Mullins316.078$6.5M$10.0MMUTUALFREE AGENT
Drew Rasmussen305.111$5.8M$9.2MCLUBFREE AGENT
Taylor Walls294.092$2.5MCLUB$3.1MCLUBFREE AGENT
Craig Kimbrel3814.031$276kFREE AGENT
Austin Slater337.147$148kFREE AGENT

Arbitration-eligible (11)

PlayerAgeSvc2026202720282029203020312032
Shane McClanahan294.158$3.6MARB 3FREE AGENT
Griffin Jax314.091$3.6MARB 3FREE AGENT
Jake Fraley315.097$3.0MFREE AGENT
Nick Fortes293.149$2.5MARB 2ARB 3FREE AGENT
Garrett Cleavinger324.060$2.4MARB 3FREE AGENT
Bryan Baker313.049$1.3MARB 2ARB 3FREE AGENT
Cole Sulser364.096$1.1MARB 3FREE AGENT
Michael Grove293.031$755kARB 2ARB 3FREE AGENT
Andrew Wantz303.148$8kARB 2ARB 3FREE AGENT
Jonathan Heasley295.000FREE AGENT
Richie Palacios294.000ARB 3FREE AGENT

Pre-arbitration (22)

PlayerAgeSvc2026202720282029203020312032
Ryan Vilade270.076$950kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kevin Kelly282.156$925kARB 1ARB 2ARB 3FREE AGENT
Jonathan Aranda282.030$796kARB 1ARB 2ARB 3FREE AGENT
Junior Caminero221.058$795kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jonny DeLuca272.085$789kARB 1ARB 2ARB 3FREE AGENT
Ben Williamson250.110$786kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Chandler Simpson250.140$785kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ian Seymour270.085$784kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Hunter Feduccia290.095$782kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Hunter Bigge281.049$776kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jesse Scholtens322.091$745kARB 1ARB 2ARB 3FREE AGENT
Carson Williams230.039$610kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Oliver Dunn281.021$542kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Victor Mesa Jr.24$526k
Casey Legumina291.111$503kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cam Booser341.150$496kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Joe Boyle261.005$231kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mason Englert261.168$194kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Yoendrys Gómez260.135$129kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Trevor Martin25$75k
Aaron Brooks362.157$25kARB 1ARB 2ARB 3FREE AGENT
Chris Roycroft290.150$23kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Rays 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 = [[1998, 0.649, -4.67], [1999, 0.794, -11.73], [2000, 0.895, -11.8], [2001, 0.536, -13.91], [2002, 0.517, -11.68], [2003, 0.302, 0.77], [2004, 0.439, -0.79], [2005, 0.428, 1.39], [2006, 0.386, -3.74], [2007, 0.316, 6.38], [2008, 0.533, 21.83], [2009, 0.724, 17.31], [2010, 0.75, 16.32], [2011, 0.48, 18.85], [2012, 0.691, 14.61], [2013, 0.591, 14.91], [2014, 0.674, 5.17], [2015, 0.597, 6.22], [2016, 0.462, 3.07], [2017, 0.628, 5.9], [2018, 0.389, 20.31], [2019, 0.457, 24.78], [2021, 0.501, 23.17], [2022, 0.584, 7.88], [2023, 0.529, 26.37], [2024, 0.513, 2.34], [2025, 0.393, 8.01], [2026, 0.464, 13.89]], TEAMCOL = '#5DA9E0'; 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", "SP", "SP", "IF", "IF", "SP", "OF", "OF", "C", "RP", "OF", "OF", "RP", "OF", "RP", "RP", "IF", "SP", "RP", "RP", "IF", "RP", "C", "IF", "RP", "RP", "OF", "OF", "RP", "RP", "RP", "SP", "RP", "RP", "IF", "RP", "RP", "RP", "RP", "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": ["Junior Caminero", "Drew Rasmussen", "Shane McClanahan", "Yandy D\u00edaz", "Jonathan Aranda", "Nick Martinez", "Chandler Simpson", "Jonny DeLuca", "Nick Fortes", "Bryan Baker", "Cedric Mullins", "Victor Mesa Jr.", "Ian Seymour", "Ryan Vilade", "Kevin Kelly", "Griffin Jax", "Taylor Walls", "Joe Boyle", "Cole Sulser", "Garrett Cleavinger", "Ben Williamson", "Cam Booser", "Hunter Feduccia", "Richie Palacios", "Michael Grove", "Craig Kimbrel", "Jake Fraley", "Austin Slater", "Hunter Bigge", "Casey Legumina", "Chase Solesky", "Steven Matz", "Jonathan Heasley", "Andrew Wantz", "Oliver Dunn", "Chris Roycroft", "Mason Englert", "Jesse Scholtens", "Trevor Martin", "Aaron Brooks"], "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.46, 2.51, 2.21, 2.16, 2.0, 1.93, 1.5, 1.19, 1.04, 1.04, 0.92, 0.8, 0.74, 0.68, 0.67, 0.66, 0.6, 0.51, 0.5, 0.37, 0.3, 0.25, 0.22, 0.18, 0.17, 0.13, 0.12, 0.11, 0.05, 0.02, -0.01, -0.02, -0.09, -0.1, -0.11, -0.14, -0.15, -0.2, -0.29, -0.34]}; 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:'TBR',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);});