RRosternomics

Mariners SEA

President of baseball ops: Jerry Dipoto · GM: Justin Hollander

Overview

CONTEND ↘ cooling
expensive & winning · year 1 in this regime · payroll 1.0× league · TAB +1.7
+1.7TAB — talent above budget15th/30
-1.6WAB — realized wins (TAB + luck)19th/30
4.0depth — 2+ WAR regulars7th/30
-2.8dead WAR — sub-replacement drag7th/30
25%dead money %5th/30
0.490win %17th/30
36.0roster fWAR13th/30
+65surplus $M (value vs cost)12th/30
$160Mpayroll $M15th/30luxury tax $164M

Roster

CFJulio Rodríguez 69GLuke Raley 1GVictor Robles 1G
LFRandy Arozarena 68GRob Refsnyder 2GLuke Raley 1G
RFLuke Raley 56GConnor Joe 14GRob Refsnyder 13G
SSJ.P. Crawford 53GLeo Rivas 10GColt Emerson 6G
2BCole Young 67GRyan Bliss 2GMiles Mastrobuoni 1G
3BLeo Rivas 26GBrendan Donovan 24GColt Emerson 15G
1BJosh Naylor 65GPatrick Wisdom 3GConnor Joe 3G
STARTERSBryan Woo 2.7WARLogan Gilbert 2.6WARGeorge Kirby 2.5WAREmerson Hancock 1.7WARLuis Castillo 0.8WAR
CCal Raleigh 31GMitch Garver 27GJhonny Pereda 17G
DHDominic Canzone 50GRob Refsnyder 24GCal Raleigh 11G
BULLPEN Andrés Muñoz 10svEduard Bazardo 12hJosé A. Ferrer 8hGabe Speier 8hMatt Brash 6hCooper Criswell 3hAlex Hoppe 1hCole Wilcox 0h
CCal Raleigh 31 · Mitch Garver 27 · Jhonny Pereda 17
1BJosh Naylor 65 · Patrick Wisdom 3 · Connor Joe 3
2BCole Young 67 · Ryan Bliss 2 · Miles Mastrobuoni 1
3BLeo Rivas 26 · Brendan Donovan 24 · Colt Emerson 15
SSJ.P. Crawford 53 · Leo Rivas 10 · Colt Emerson 6
LFRandy Arozarena 68 · Rob Refsnyder 2 · Luke Raley 1
CFJulio Rodríguez 69 · Luke Raley 1 · Victor Robles 1
RFLuke Raley 56 · Connor Joe 14 · Rob Refsnyder 13
DHDominic Canzone 50 · Rob Refsnyder 24 · Cal Raleigh 11
SPBryan Woo 2.7 · Logan Gilbert 2.6 · George Kirby 2.5 · Emerson Hancock 1.7 · Luis Castillo 0.8
BP Andrés Muñoz 10 · Eduard Bazardo 12 · José A. Ferrer 8 · Gabe Speier 8 · Matt Brash 6 · Cooper Criswell 3 · Alex Hoppe 1 · Cole Wilcox 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
Randy Arozarena 31 arb 964.3 15.7 +11.4
Bryan Woo 26 pre-arb 194.2 0.8 +25.4
Logan Gilbert 29 arb 204.1 10.9 +14.9
George Kirby 28 arb 193.9 6.5 +17.9
Julio Rodríguez 25 arb 933.0 20.2 -1.2
Emerson Hancock 27 pre-arb 192.6 0.8 +15.5
Cole Young 22 pre-arb 1042.5 0.8 +15.0
Dominic Canzone 28 pre-arb 892.3 0.8 +13.5
Bryce Miller 27 pre-arb 121.8 2.4 +8.9
J.P. Crawford 31 FA-elig 821.6 12.0 -1.7
Luis Castillo 33 FA-elig 191.3 24.1 -16.3
Andrés Muñoz 27 FA-elig 391.2 7.0 +0.7
Eduard Bazardo 30 pre-arb 480.9 0.9 +5.0
Brendan Donovan 29 arb 250.9 5.8 -0.0
Josh Naylor 29 FA-elig 970.8 11.3 -6.1
Luke Raley 31 arb 890.7 2.0 +2.5
Cal Raleigh 29 arb 720.7 12.7 -8.3
Colt Emerson 20 520.7 2.0 +2.3
José A. Ferrer 26 470.5
Matt Brash 28 arb 200.5 1.6 +1.6
Jhonny Pereda 30 pre-arb 190.5 0.6 +2.3
Alex Hoppe 27 210.4 0.7 +2.0
Mitch Garver 35 FA-elig 460.2 2.2 -0.8
Casey Legumina↔ TBR 29 80.2 0.2 +1.3
Victor Robles 29 FA-elig 460.2 5.1 -3.8
Cooper Criswell 29 pre-arb 260.2 0.8 +0.5
Will Wilson 27 pre-arb 20.2 0.7 +0.4
Michael Rucker 32 16-0.0 0.5 -0.5
José Suarez↔ ATL 28 1-0.0 0.0 -0.1
Weston Wilson↔ BAL 31 15-0.0 0.3 -0.4
Cole Wilcox 26 pre-arb 17-0.0 0.2 -0.3
Gabe Speier 31 arb 32-0.0 2.1 -2.4
Domingo Gonzalez 26 5-0.1 0.0 -0.6
Buddy Kennedy↔ SFG 27 5-0.1 0.2 -1.0
Ryan Bliss 26 pre-arb 3-0.1 0.0 -0.9
Nick Davila 27 19-0.2 0.1 -1.3
Connor Joe 33 arb 21-0.3 0.2 -1.9
Leo Rivas 28 38-0.3
Miles Mastrobuoni 30 pre-arb 13-0.4 0.8 -3.3
Josh Simpson 28 pre-arb 4-0.5 0.0 -3.1
Patrick Wisdom 34 arb 15-0.7 0.9 -5.1
Rob Refsnyder 35 FA-elig 49-1.7 6.2 -16.7

Key additions (new vs 2025)

PlayerfWAR
Brendan Donovan0.6
José A. Ferrer0.3
Jhonny Pereda0.3
Alex Hoppe0.3
Cooper Criswell0.1
Will Wilson0.1

Key subtractions (gone from 2025)

Player2025 fWAR
Jorge Polanco2.6
Eugenio Suárez0.7
Ben Williamson0.4
Collin Snider0.2
Luke Jackson0.2
Caleb Ferguson0.1

Current & Historical Payroll

2026 payroll
$160.3M
luxury tax $164M
2027 guaranteed
$98.4M

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$107M$88M$85M$53M$53M$17M
Proj. arbitration$45M$2M
Proj. pre-arbitration$12M$18M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$182M$108M$103M$71M$71M$53M
Tax space+$62M+$139M+$147M+$182M+$185M+$206M
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 (10)

PlayerAgeSvc2026202720282029203020312032
Luis Castillo338.101$24.1M$24.1M$25.0MVESTFREE AGENT
Julio Rodríguez254.000$20.2M$20.2M$20.2M$20.2M$18.0MCLUB$18.0M$18.0M
Cal Raleigh294.085$12.7M$13.7M$24.7M$24.7M$24.7M$20.0MMUTUALFREE AGENT
J.P. Crawford317.163$12.0MFREE AGENT
Josh Naylor296.127$11.3M$17.3M$19.3M$21.3M$23.3MFREE AGENT
Andrés Muñoz276.080$7.0MCLUB$8.0MCLUB$10.0MCLUBFREE AGENT
Rob Refsnyder357.048$6.2MFREE AGENT
Victor Robles297.033$5.1M$9.0MCLUBFREE AGENT
Bryce Miller272.153$2.4M$6.1MCLUBARB 2ARB 3FREE AGENT
Mitch Garver358.045$2.2MFREE AGENT

Arbitration-eligible (12)

PlayerAgeSvc2026202720282029203020312032
Randy Arozarena315.129$15.7MFREE AGENT
Logan Gilbert294.144$10.9MARB 3FREE AGENT
George Kirby283.151$6.5MARB 2ARB 3FREE AGENT
Brendan Donovan294.000$5.8MARB 3FREE AGENT
Gabe Speier314.000$2.1MARB 3FREE AGENT
Luke Raley313.106$2.0MARB 2ARB 3FREE AGENT
Matt Brash283.121$1.6MARB 2ARB 3FREE AGENT
Patrick Wisdom344.058$888kARB 3FREE AGENT
Michael Rucker323.156$450kARB 2ARB 3FREE AGENT
Connor Joe334.068$200kARB 3FREE AGENT
José Suarez284.064$19kARB 3FREE AGENT
José A. Ferrer263.000ARB 2ARB 3FREE AGENT

Pre-arbitration (17)

PlayerAgeSvc2026202720282029203020312032
Eduard Bazardo302.118$923kARB 1ARB 2ARB 3FREE AGENT
Bryan Woo262.121$832kARB 1ARB 2ARB 3FREE AGENT
Cooper Criswell291.143$800kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Dominic Canzone282.014$797kARB 1ARB 2ARB 3FREE AGENT
Emerson Hancock271.059$793kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Miles Mastrobuoni302.043$792kARB 1ARB 2ARB 3FREE AGENT
Cole Young220.121$786kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Will Wilson270.074$667kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Alex Hoppe27$663k
Jhonny Pereda300.140$592kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Weston Wilson311.097$290kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Buddy Kennedy271.131$205kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cole Wilcox260.012$171kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Casey Legumina291.111$155kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Josh Simpson281.099$46kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ryan Bliss261.061$38kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Leo Rivas282.000ARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Mariners 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 = [[1985, 0.443, 6.1], [1986, 0.536, -7.33], [1987, 0.438, 9.95], [1988, 0.648, 2.45], [1989, 0.611, 4.26], [1990, 0.645, 11.14], [1991, 0.686, -3.33], [1992, 0.814, -7.23], [1993, 1.052, 4.91], [1994, 0.904, 3.43], [1995, 1.037, 10.41], [1996, 1.172, 12.26], [1997, 1.092, 13.89], [1998, 1.239, 5.25], [1999, 1.052, -7.44], [2000, 1.121, 8.63], [2001, 1.171, 27.16], [2002, 1.262, 13.33], [2003, 1.274, 9.99], [2004, 1.049, -8.58], [2005, 1.155, -12.96], [2006, 1.132, -2.62], [2007, 1.294, -7.82], [2008, 1.266, -19.91], [2009, 1.106, -0.66], [2010, 0.894, -13.99], [2011, 0.903, -11.17], [2012, 0.737, -3.27], [2013, 0.758, -5.07], [2014, 0.869, -2.55], [2015, 1.023, -8.04], [2016, 1.126, 2.14], [2017, 1.175, -4.38], [2018, 1.237, -0.83], [2019, 0.906, -11.33], [2021, 0.634, -4.04], [2022, 0.77, 6.17], [2023, 0.869, 13.26], [2024, 0.873, 5.41], [2025, 0.909, 7.6], [2026, 1.004, 1.73]], TEAMCOL = '#005C5C'; 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": ["OF", "SP", "SP", "SP", "OF", "SP", "IF", "OF", "SP", "IF", "SP", "RP", "RP", "IF", "IF", "OF", "C", "IF", "RP", "RP", "C", "RP", "C", "RP", "OF", "RP", "IF", "RP", "RP", "IF", "RP", "RP", "RP", "IF", "IF", "RP", "OF", "IF", "IF", "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": ["Randy Arozarena", "Bryan Woo", "Logan Gilbert", "George Kirby", "Julio Rodr\u00edguez", "Emerson Hancock", "Cole Young", "Dominic Canzone", "Bryce Miller", "J.P. Crawford", "Luis Castillo", "Andr\u00e9s Mu\u00f1oz", "Eduard Bazardo", "Brendan Donovan", "Josh Naylor", "Luke Raley", "Cal Raleigh", "Colt Emerson", "Jos\u00e9 A. Ferrer", "Matt Brash", "Jhonny Pereda", "Alex Hoppe", "Mitch Garver", "Casey Legumina", "Victor Robles", "Cooper Criswell", "Will Wilson", "Michael Rucker", "Jos\u00e9 Suarez", "Weston Wilson", "Cole Wilcox", "Gabe Speier", "Domingo Gonzalez", "Buddy Kennedy", "Ryan Bliss", "Nick Davila", "Connor Joe", "Leo Rivas", "Miles Mastrobuoni", "Josh Simpson"], "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": [2.76, 2.67, 2.63, 2.49, 1.94, 1.66, 1.61, 1.46, 1.16, 1.05, 0.8, 0.79, 0.61, 0.59, 0.53, 0.46, 0.45, 0.43, 0.33, 0.32, 0.29, 0.27, 0.15, 0.14, 0.13, 0.13, 0.1, -0.0, -0.01, -0.01, -0.02, -0.02, -0.06, -0.08, -0.09, -0.12, -0.18, -0.2, -0.25, -0.32]}; 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:'SEA',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);});