RRosternomics

Padres SDP

GM: AJ Preller

Overview

FORCED-SPEND ↘ cooling
expensive & losing · year 1 in this regime · payroll 1.11× league · TAB -4.9
-4.9TAB — talent above budget19th/30
-2.4WAB — realized wins (TAB + luck)20th/30
2.0depth — 2+ WAR regulars25th/30
-3.0dead WAR — sub-replacement drag9th/30
34%dead money %13th/30
0.490win %17th/30
30.6roster fWAR19th/30
+11surplus $M (value vs cost)20th/30
$177Mpayroll $M13th/30luxury tax $222M

Roster

CFJackson Merrill 65GBryce Johnson 5G
LFRamón Laureano 45GGavin Sheets 14GBryce Johnson 7G
RFFernando Tatis Jr. 43GNick Castellanos 19GBryce Johnson 9G
SSXander Bogaerts 64GSung-Mun Song 5GJake Cronenworth 1G
2BFernando Tatis Jr. 32GJake Cronenworth 30GSung-Mun Song 15G
3BManny Machado 62GMiguel Andujar 5GTy France 2G
1BTy France 44GGavin Sheets 35GNick Castellanos 3G
STARTERSMichael King 1.9WARWalker Buehler 1.0WARRandy Vásquez 0.4WARGriffin Canning -0.0WARLucas Giolito -0.2WAR
CFreddy Fermin 50GLuis Campusano 18GRodolfo Durán 16G
DHMiguel Andujar 45GGavin Sheets 9GNick Castellanos 7G
BULLPEN Mason Miller 18svJason Adam 14hAdrian Morejon 11hJeremiah Estrada 6hBradgley Rodriguez 3hWandy Peralta 2hRon Marinaccio 1hYuki Matsui 0h
CFreddy Fermin 50 · Luis Campusano 18 · Rodolfo Durán 16
1BTy France 44 · Gavin Sheets 35 · Nick Castellanos 3
2BFernando Tatis Jr. 32 · Jake Cronenworth 30 · Sung-Mun Song 15
3BManny Machado 62 · Miguel Andujar 5 · Ty France 2
SSXander Bogaerts 64 · Sung-Mun Song 5 · Jake Cronenworth 1
LFRamón Laureano 45 · Gavin Sheets 14 · Bryce Johnson 7
CFJackson Merrill 65 · Bryce Johnson 5
RFFernando Tatis Jr. 43 · Nick Castellanos 19 · Bryce Johnson 9
DHMiguel Andujar 45 · Gavin Sheets 9 · Nick Castellanos 7
SPMichael King 1.9 · Walker Buehler 1.0 · Randy Vásquez 0.4 · Griffin Canning -0.0 · Lucas Giolito -0.2
BP Mason Miller 18 · Jason Adam 14 · Adrian Morejon 11 · Jeremiah Estrada 6 · Bradgley Rodriguez 3 · Wandy Peralta 2 · Ron Marinaccio 1 · Yuki Matsui 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
Mason Miller 27 pre-arb 414.1 4.0 +21.8
Fernando Tatis Jr. 27 1033.8 20.7 +3.3
Ty France 31 FA-elig 783.1 1.4 +18.2
Michael King 31 FA-elig 212.9 9.0 +9.6
Adrian Morejon 27 arb 482.9 3.9 +14.1
Xander Bogaerts 33 FA-elig 992.5 25.5 -9.8
Manny Machado 33 FA-elig 1012.4 25.1 -9.8
Jackson Merrill 23 pre-arb 1011.8 2.1 +9.5
Bradgley Rodriguez 22 pre-arb 421.6 0.8 +9.5
Walker Buehler 31 FA-elig 201.6 1.5 +8.3
Luis Campusano 27 arb 321.5 0.9 +8.7
Nick Pivetta 33 FA-elig 41.4 20.5 -11.7
Samad Taylor 27 261.1 0.5 +6.7
Sung-Mun Song 29 480.9
Luis Rengifo↔ MIL 29 70.7 0.2 +4.3
Randy Vásquez 27 pre-arb 190.6 0.8 +3.2
Gavin Sheets 30 arb 980.5 4.5 -1.3
Jeremiah Estrada 27 pre-arb 210.4 0.8 +1.8
Kyle Hart 33 pre-arb 270.4 1.0 +1.6
Freddy Fermin 31 pre-arb 640.3 2.1 -0.0
Ramón Laureano 31 FA-elig 530.3 6.5 -4.6
Wandy Peralta 34 FA-elig 450.1 4.5 -3.8
David Morgan 26 pre-arb 180.1 0.2 +0.3
JP Sears 30 arb 50.1 2.8 -2.4
Yuki Matsui 30 pre-arb 280.0 5.8 -5.6
Griffin Canning 30 FA-elig 15-0.0 2.5 -2.6
Rodolfo Durán 28 37-0.0 0.6 -0.7
Alek Jacob 28 pre-arb 3-0.0 0.0 -0.2
Miguel Andujar 31 FA-elig 74-0.0 1.5 -1.7
Jhony Brito 28 pre-arb 3-0.0 0.8 -1.0
Blake Hunt 27 2-0.1
Nick Solak 31 4-0.1
Will Wagner 27 pre-arb 16-0.1 0.6 -1.3
Jason Adam 34 arb 36-0.2 6.7 -7.6
Matt Waldron 29 pre-arb 9-0.2 0.8 -1.9
Bryce Johnson 30 pre-arb 32-0.2 0.8 -2.0
Lucas Giolito 31 FA-elig 7-0.3 1.5 -3.4
Jake Cronenworth 32 FA-elig 53-0.4 12.3 -14.7
Ron Marinaccio 30 pre-arb 33-0.4 0.8 -3.3
Jase Bowen 25 35-0.6 0.5 -4.1
Germán Márquez 31 FA-elig 11-0.8 1.8 -6.7
Nick Castellanos 34 FA-elig 39-1.3 0.8 -9.2

Key additions (new vs 2025)

PlayerfWAR
Ty France2.0
Walker Buehler1.0
Samad Taylor0.7
Sung-Mun Song0.6
Luis Rengifo0.5
Griffin Canning-0.0

Key subtractions (gone from 2025)

Player2025 fWAR
Dylan Cease3.4
Robert Suarez1.9
Luis Arraez0.9
Stephen Kolek0.8
Ryan O'Hearn0.6
Elias Díaz0.4

Current & Historical Payroll

2026 payroll
$176.7M
luxury tax $222M
2027 guaranteed
$176.1M

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$189M$180M$146M$84M$84M$72M
Proj. arbitration$19M
Proj. pre-arbitration$15M$1M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$240M$199M$164M$102M$102M$90M
Tax space+$4M+$48M+$86M+$151M+$154M+$169M
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 (19)

PlayerAgeSvc2026202720282029203020312032
Xander Bogaerts3312.042$25.5M$25.5M$25.5M$25.5M$25.5M$25.5M$25.5M
Manny Machado3313.056$25.1M$39.1M$39.1M$39.1M$39.1M$39.1M$39.1M
Fernando Tatis Jr.277.000$20.7MFREE AGENT
Nick Pivetta337.166$20.5M$14.0MOPT-OUT$18.0MPLAYERFREE AGENT
Jake Cronenworth326.000$12.3M$12.3M$12.3M$12.3M$12.3MFREE AGENT
Michael King316.004$9.0M$32.0MOPT-OUT$34.0MPLAYERFREE AGENT
Ramón Laureano316.159$6.5MCLUBFREE AGENT
Yuki Matsui302.000$5.8M$6.5MOPT-OUT$7.0MPLAYERARB 3FREE AGENT
Wandy Peralta348.168$4.5MOPT-OUT$4.5MPLAYERFREE AGENT
Griffin Canning306.075$2.5M$5.0MMUTUALFREE AGENT
Jackson Merrill232.000$2.1M$7.1M$9.1M$11.1M$21.1M$21.1M$21.1M
Germán Márquez319.027$1.8M$1.8MFREE AGENT
Walker Buehler317.168$1.5MFREE AGENT
Lucas Giolito318.080$1.5M$8.0MMUTUALFREE AGENT
Miguel Andujar316.053$1.5M$8.0MMUTUALFREE AGENT
Ty France316.089$1.4MFREE AGENT
Kyle Hart331.022$1.0M$2.5MCLUBARB 1ARB 2ARB 3FREE AGENT
Nick Castellanos3412.029$780kFREE AGENT
Luis Rengifo296.043$241k$10.0MMUTUALFREE AGENT

Arbitration-eligible (6)

PlayerAgeSvc2026202720282029203020312032
Jason Adam345.132$6.7MFREE AGENT
Gavin Sheets304.076$4.5MARB 3FREE AGENT
Adrian Morejon275.140$3.9MFREE AGENT
JP Sears303.065$2.8MARB 2ARB 3FREE AGENT
Luis Campusano273.003$900kARB 2ARB 3FREE AGENT
Nick Solak315.126FREE AGENT

Pre-arbitration (14)

PlayerAgeSvc2026202720282029203020312032
Mason Miller272.166$4.0MARB 1ARB 2ARB 3FREE AGENT
Freddy Fermin312.165$2.1MARB 1ARB 2ARB 3FREE AGENT
Jeremiah Estrada272.069$810kARB 1ARB 2ARB 3FREE AGENT
Randy Vásquez271.129$809kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ron Marinaccio302.064$808kARB 1ARB 2ARB 3FREE AGENT
Bryce Johnson301.097$805kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Matt Waldron291.107$797kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jhony Brito282.048$792kARB 1ARB 2ARB 3FREE AGENT
Bradgley Rodriguez220.020$783kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Will Wagner270.151$596kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Samad Taylor271.071$484kPre-ArbARB 1ARB 2ARB 3FREE AGENT
David Morgan260.134$156kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Alek Jacob281.059$18kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Sung-Mun Song29

Graphs

Every roster slot ranked best→worst by WAR. Bars = Padres 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, 1.101, -0.87], [1986, 1.045, -10.29], [1987, 0.949, -4.97], [1988, 0.816, 5.64], [1989, 0.994, -4.69], [1990, 1.025, -5.29], [1991, 0.965, 2.72], [1992, 0.88, 8.84], [1993, 0.579, -13.43], [1994, 0.473, -0.55], [1995, 0.792, -6.32], [1996, 0.909, 7.89], [1997, 0.998, -15.19], [1998, 1.193, 1.97], [1999, 0.843, -2.53], [2000, 0.917, -8.36], [2001, 0.556, -1.44], [2002, 0.631, -6.78], [2003, 0.68, -3.38], [2004, 0.867, 8.89], [2005, 0.933, 4.26], [2006, 0.948, 8.92], [2007, 0.797, 6.78], [2008, 0.725, -3.78], [2009, 0.499, 0.71], [2010, 0.475, 14.63], [2011, 0.468, 5.02], [2012, 0.601, 0.95], [2013, 0.656, -2.58], [2014, 0.622, -4.65], [2015, 1.037, -8.59], [2016, 0.608, -11.52], [2017, 0.271, -1.43], [2018, 0.639, -4.72], [2019, 0.596, -1.23], [2021, 1.394, -2.57], [2022, 1.44, 1.18], [2023, 1.548, 4.34], [2024, 1.047, 11.15], [2025, 1.205, 0.33], [2026, 1.108, -4.9]], TEAMCOL = '#7E5539'; 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": ["RP", "OF", "IF", "SP", "RP", "IF", "IF", "OF", "RP", "SP", "C", "SP", "OF", "IF", "IF", "SP", "IF", "RP", "RP", "C", "OF", "RP", "RP", "RP", "RP", "SP", "C", "RP", "IF", "RP", "C", "IF", "IF", "RP", "SP", "OF", "SP", "IF", "RP", "OF"], "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": ["Mason Miller", "Fernando Tatis Jr.", "Ty France", "Michael King", "Adrian Morejon", "Xander Bogaerts", "Manny Machado", "Jackson Merrill", "Bradgley Rodriguez", "Walker Buehler", "Luis Campusano", "Nick Pivetta", "Samad Taylor", "Sung-Mun Song", "Luis Rengifo", "Randy V\u00e1squez", "Gavin Sheets", "Jeremiah Estrada", "Kyle Hart", "Freddy Fermin", "Ram\u00f3n Laureano", "Wandy Peralta", "David Morgan", "JP Sears", "Yuki Matsui", "Griffin Canning", "Rodolfo Dur\u00e1n", "Alek Jacob", "Miguel Andujar", "Jhony Brito", "Blake Hunt", "Nick Solak", "Will Wagner", "Jason Adam", "Matt Waldron", "Bryce Johnson", "Lucas Giolito", "Jake Cronenworth", "Ron Marinaccio", "Jase Bowen"], "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.64, 2.45, 2.0, 1.89, 1.83, 1.6, 1.56, 1.18, 1.04, 1.0, 0.98, 0.89, 0.73, 0.56, 0.46, 0.41, 0.33, 0.27, 0.27, 0.21, 0.19, 0.06, 0.05, 0.03, 0.02, -0.01, -0.01, -0.01, -0.02, -0.02, -0.06, -0.07, -0.08, -0.1, -0.12, -0.12, -0.19, -0.25, -0.26, -0.37]}; 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:'SDP',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);});