RRosternomics

Diamondbacks ARI

GM: Mike Hazen

Overview

REBUILD ↘ cooling
cheap & losing · year 1 in this regime · payroll 0.98× league · TAB -5.6
-5.6TAB — talent above budget21st/30
+2.5WAB — realized wins (TAB + luck)13th/30
4.0depth — 2+ WAR regulars7th/30
-5.0dead WAR — sub-replacement drag23rd/30
50%dead money %25th/30
0.529win %9th/30
28.4roster fWAR22nd/30
+24surplus $M (value vs cost)18th/30
$157Mpayroll $M16th/30luxury tax $193M

Roster

CFRyan Waldschmidt 28GAlek Thomas 28GJorge Barrosa 26G
LFLourdes Gurriel Jr. 24GTim Tawa 20GTommy Troy 16G
RFCorbin Carroll 63GJorge Barrosa 12G
SSGeraldo Perdomo 66GJose Fernandez 5GIldemaro Vargas 1G
2BKetel Marte 55GIldemaro Vargas 17GTim Tawa 1G
3BNolan Arenado 61GJose Fernandez 12GIldemaro Vargas 3G
1BIldemaro Vargas 40GJose Fernandez 17GCarlos Santana 8G
STARTERSMichael Soroka 2.2WAREduardo Rodriguez 1.5WARRyne Nelson -0.1WARZac Gallen -0.1WARMerrill Kelly -0.6WAR
CGabriel Moreno 42GJames McCann 19GAdrian Del Castillo 11G
DHAdrian Del Castillo 25GJose Fernandez 19GKetel Marte 9G
BULLPEN Paul Sewald 15svTaylor Clarke 11hJuan Morillo 8hRyan Thompson 5hBrandyn Garcia 5hKevin Ginkel 4hJonathan Loáisiga 4hBrandon Pfaadt 2h
CGabriel Moreno 42 · James McCann 19 · Adrian Del Castillo 11
1BIldemaro Vargas 40 · Jose Fernandez 17 · Carlos Santana 8
2BKetel Marte 55 · Ildemaro Vargas 17 · Tim Tawa 1
3BNolan Arenado 61 · Jose Fernandez 12 · Ildemaro Vargas 3
SSGeraldo Perdomo 66 · Jose Fernandez 5 · Ildemaro Vargas 1
LFLourdes Gurriel Jr. 24 · Tim Tawa 20 · Tommy Troy 16
CFRyan Waldschmidt 28 · Alek Thomas 28 · Jorge Barrosa 26
RFCorbin Carroll 63 · Jorge Barrosa 12
DHAdrian Del Castillo 25 · Jose Fernandez 19 · Ketel Marte 9
SPMichael Soroka 2.2 · Eduardo Rodriguez 1.5 · Ryne Nelson -0.1 · Zac Gallen -0.1 · Merrill Kelly -0.6
BP Paul Sewald 15 · Taylor Clarke 11 · Juan Morillo 8 · Ryan Thompson 5 · Brandyn Garcia 5 · Kevin Ginkel 4 · Jonathan Loáisiga 4 · Brandon Pfaadt 2

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
Corbin Carroll 25 arb 1005.3 10.6 +22.5
Gabriel Moreno 26 arb 774.6 2.5 +26.3
Geraldo Perdomo 26 arb 1003.8 6.2 +17.7
Michael Soroka 28 FA-elig 153.4 7.0 +14.2
Nolan Arenado 35 FA-elig 983.1 5.0 +14.2
Ketel Marte 32 FA-elig 993.0 16.0 +2.9
Eduardo Rodriguez 33 FA-elig 212.4 21.0 -6.0
Juan Morillo 27 pre-arb 451.6 0.8 +9.3
Brandyn Garcia 26 pre-arb 321.3 0.6 +7.6
Ildemaro Vargas 34 arb 901.2 1.2 +6.0
Jonathan Loáisiga 31 FA-elig 431.1 1.5 +5.3
Kevin Ginkel 32 arb 461.1 2.7 +3.9
Brandon Pfaadt 27 pre-arb 180.8 3.4 +1.8
Jordan Lawlar 23 pre-arb 120.8 0.8 +4.1
Ryan Waldschmidt 23 400.6 0.6 +3.3
Ryan Thompson 34 arb 420.4 4.0 -1.2
Jorge Barrosa 25 pre-arb 720.4 0.8 +1.7
Taylor Clarke 33 arb 460.3 1.6 +0.6
Tyler Locklear 25 pre-arb 50.3 0.2 +1.6
Kohl Drake 25 10.2 0.8 +0.4
Tommy Troy 24 390.2 0.5 +0.6
Max Kepler 33 190.1 0.5 +0.4
Tim Tawa 27 pre-arb 470.1 0.8 -0.1
Juan Burgos 26 pre-arb 20.1 0.8 -0.4
Drey Jameson 28 pre-arb 110.0 0.8 -0.5
Jose Cabrera 24 40.0
Gerardo Carrillo 27 10.0
Kade Strowd 28 pre-arb 1-0.1 0.8 -1.2
James McCann 36 FA-elig 26-0.1 2.9 -3.2
Taylor Rashi 30 pre-arb 3-0.1 0.0 -0.5
Luken Baker 29 pre-arb 3-0.1 0.0 -0.6
Paul Sewald 36 FA-elig 41-0.1 1.5 -2.1
Zac Gallen 30 FA-elig 19-0.1 16.2 -16.9
Yilber Díaz 25 pre-arb 1-0.1 0.8 -1.6
Ryne Nelson 28 arb 15-0.1 3.0 -3.8
Alek Thomas 26 28-0.2 1.4 -2.5
Aramis Garcia 33 arb 8-0.2 0.6 -1.7
Joe Ross↔ TEX 33 3-0.2 0.6 -1.9
LuJames Groover 24 15-0.2 0.5 -1.9
Jose Fernandez 22 55-0.2 0.8 -2.2
Philip Abner 24 pre-arb 5-0.2 0.0 -1.6
Andrew Hoffmann 26 8-0.3
Mitch Bratt 22 3-0.3 0.8 -2.8
Adrian Del Castillo 26 54-0.7 0.8 -5.5
Carlos Santana 40 FA-elig 8-0.9 2.0 -7.4
Merrill Kelly 37 FA-elig 18-1.0 18.0 -24.1
Pavin Smith 30 arb 28-1.1 2.2 -9.4
Lourdes Gurriel Jr. 32 47-1.4 13.0 -22.0

Key additions (new vs 2025)

PlayerfWAR
Michael Soroka2.2
Nolan Arenado2.0
Jonathan Loáisiga0.7
Taylor Clarke0.2
Max Kepler0.1
Kade Strowd-0.0

Key subtractions (gone from 2025)

Player2025 fWAR
Eugenio Suárez3.1
Josh Naylor1.4
Blaze Alexander1.1
Shelby Miller0.6
Jalen Beeks0.2
Nabil Crismatt0.1

Current & Historical Payroll

2026 payroll
$156.8M
luxury tax $193M
2027 guaranteed
$98.3M

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$164M$137M$102M$82M$82M$38M
Proj. arbitration$18M$4M$4M$4M
Proj. pre-arbitration$10M$20M$4M$4M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$211M$159M$144M$104M$104M$60M
Tax space+$33M+$88M+$106M+$149M+$152M+$199M
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 (17)

PlayerAgeSvc2026202720282029203020312032
Eduardo Rodriguez3310.070$21.0M$19.0M$17.0MMUTUALFREE AGENT
Merrill Kelly377.000$18.0M$22.0M$12.0MVESTFREE AGENT
Zac Gallen306.100$16.2MFREE AGENT
Ketel Marte329.162$16.0M$12.0M$20.0M$22.0M$22.0M$11.5MPLAYERFREE AGENT
Lourdes Gurriel Jr.328.000$13.0MPLAYERFREE AGENT
Corbin Carroll253.038$10.6M$12.6M$14.6M$28.6M$28.6M$28.0MCLUBFREE AGENT
Michael Soroka287.009$7.0M$10.0MMUTUALFREE AGENT
Geraldo Perdomo264.015$6.2M$9.2M$14.8M$16.8M$17.5MCLUBFREE AGENT
Nolan Arenado3512.155$5.0M$6.0MFREE AGENT
Brandon Pfaadt272.097$3.4M$5.4M$8.4M$11.4M$15.4M$21.0MCLUB$25.0MMUTUAL
James McCann3610.126$2.9MFREE AGENT
Carlos Santana4015.115$2.0M$2.0MMUTUALFREE AGENT
Jonathan Loáisiga317.022$1.5MFREE AGENT
Paul Sewald367.072$1.5MFREE AGENT
Jose Fernandez227.030$759kFREE AGENT
Joe Ross338.018$562kFREE AGENT
Max Kepler339.152$467kFREE AGENT

Arbitration-eligible (9)

PlayerAgeSvc2026202720282029203020312032
Ryan Thompson345.095$4.0MFREE AGENT
Ryne Nelson283.020$3.0MARB 2ARB 3FREE AGENT
Kevin Ginkel325.033$2.7MFREE AGENT
Gabriel Moreno263.061$2.5MARB 2ARB 3FREE AGENT
Pavin Smith304.015$2.2MARB 3FREE AGENT
Taylor Clarke335.148$1.6MFREE AGENT
Alek Thomas263.103$1.4MARB 2ARB 3FREE AGENT
Ildemaro Vargas345.129$1.2MFREE AGENT
Aramis Garcia333.047$621kARB 2ARB 3FREE AGENT

Pre-arbitration (15)

PlayerAgeSvc2026202720282029203020312032
Drey Jameson282.012$820kARB 1ARB 2ARB 3FREE AGENT
Juan Burgos260.074$820kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Yilber Díaz250.041$820kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kade Strowd280.074$820kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tim Tawa270.112$788kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Adrian Del Castillo26$788k
Juan Morillo270.111$785kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jorge Barrosa250.070$784kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jordan Lawlar230.073$784kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brandyn Garcia260.041$621kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tyler Locklear250.084$189kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Luken Baker290.164$46kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Taylor Rashi300.033$46kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Philip Abner240.011$8kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Andrew Hoffmann26

Graphs

Every roster slot ranked best→worst by WAR. Bars = Diamondbacks 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.761, -11.6], [1999, 1.421, 10.59], [2000, 1.409, -0.64], [2001, 1.266, 10.76], [2002, 1.576, 8.99], [2003, 1.156, -3.62], [2004, 0.997, -23.95], [2005, 0.882, -8.41], [2006, 0.724, -0.57], [2007, 0.665, -3.34], [2008, 0.824, 0.99], [2009, 0.812, -0.21], [2010, 0.55, -1.63], [2011, 0.606, 14.09], [2012, 0.753, 6.13], [2013, 0.857, -3.86], [2014, 0.92, -12.13], [2015, 0.53, 5.51], [2016, 0.711, -2.97], [2017, 0.785, 13.33], [2018, 0.979, -1.78], [2019, 0.911, 3.12], [2021, 0.662, -16.18], [2022, 0.641, -2.91], [2023, 0.775, 1.78], [2024, 1.021, 12.18], [2025, 0.943, 1.98], [2026, 0.983, -5.63]], TEAMCOL = '#1D7C84'; 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", "C", "IF", "SP", "IF", "IF", "SP", "RP", "RP", "IF", "RP", "RP", "RP", "OF", "OF", "RP", "OF", "RP", "IF", "RP", "OF", "OF", "OF", "RP", "RP", "RP", "RP", "RP", "C", "RP", "IF", "RP", "SP", "RP", "SP", "OF", "C", "RP", "IF", "IF"], "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": ["Corbin Carroll", "Gabriel Moreno", "Geraldo Perdomo", "Michael Soroka", "Nolan Arenado", "Ketel Marte", "Eduardo Rodriguez", "Juan Morillo", "Brandyn Garcia", "Ildemaro Vargas", "Jonathan Lo\u00e1isiga", "Kevin Ginkel", "Brandon Pfaadt", "Jordan Lawlar", "Ryan Waldschmidt", "Ryan Thompson", "Jorge Barrosa", "Taylor Clarke", "Tyler Locklear", "Kohl Drake", "Tommy Troy", "Max Kepler", "Tim Tawa", "Juan Burgos", "Drey Jameson", "Jose Cabrera", "Gerardo Carrillo", "Kade Strowd", "James McCann", "Taylor Rashi", "Luken Baker", "Paul Sewald", "Zac Gallen", "Yilber D\u00edaz", "Ryne Nelson", "Alek Thomas", "Aramis Garcia", "Joe Ross", "LuJames Groover", "Jose Fernandez"], "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.38, 2.94, 2.44, 2.16, 1.96, 1.92, 1.53, 1.03, 0.84, 0.74, 0.7, 0.68, 0.53, 0.5, 0.4, 0.28, 0.25, 0.22, 0.18, 0.12, 0.12, 0.09, 0.07, 0.04, 0.03, 0.02, 0.01, -0.03, -0.04, -0.05, -0.05, -0.06, -0.07, -0.07, -0.08, -0.11, -0.11, -0.14, -0.15, -0.15]}; 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:'ARI',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);});