RRosternomics

Cardinals STL

President of baseball ops: Chaim Bloom · GM: Mike Girsch

Overview

WINDOW ↗ rising
cheap & winning · year 1 in this regime · payroll 0.34× league · TAB +7.3
+7.3TAB — talent above budget9th/30
+8.4WAB — realized wins (TAB + luck)4th/30
3.0depth — 2+ WAR regulars19th/30
-3.1dead WAR — sub-replacement drag12th/30
45%dead money %20th/30
0.505win %13th/30
28.8roster fWAR21st/30
+127surplus $M (value vs cost)8th/30
$55Mpayroll $M29th/30luxury tax $52M

Roster

CFVictor Scott II 60GNathan Church 18GJosé Fermín 5G
LFNathan Church 36GJosé Fermín 18GThomas Saggese 15G
RFJordan Walker 63GNathan Church 3GJosé Fermín 2G
SSMasyn Winn 59GThomas Saggese 8GCésar Prieto 2G
2BJJ Wetherholt 62GThomas Saggese 3GBryan Torres 2G
3BNolan Gorman 47GRamón Urías 21GJosé Fermín 6G
1BAlec Burleson 65GRamón Urías 2GJosé Fermín 1G
STARTERSDustin May 2.5WARAndre Pallante 1.5WARMichael McGreevy 1.3WARKyle Leahy 1.3WARMatthew Liberatore 0.5WAR
CPedro Pagés 38GIván Herrera 25GYohel Pozo 6G
DHIván Herrera 41GNolan Gorman 14GYohel Pozo 6G
BULLPEN Riley O'Brien 17svJoJo Romero 16hRyne Stanek 11hGeorge Soriano 9hJustin Bruihl 6hGordon Graceffo 3hMatt Svanson 1hRyan Fernandez 0h
CPedro Pagés 38 · Iván Herrera 25 · Yohel Pozo 6
1BAlec Burleson 65 · Ramón Urías 2 · José Fermín 1
2BJJ Wetherholt 62 · Thomas Saggese 3 · Bryan Torres 2
3BNolan Gorman 47 · Ramón Urías 21 · José Fermín 6
SSMasyn Winn 59 · Thomas Saggese 8 · César Prieto 2
LFNathan Church 36 · José Fermín 18 · Thomas Saggese 15
CFVictor Scott II 60 · Nathan Church 18 · José Fermín 5
RFJordan Walker 63 · Nathan Church 3 · José Fermín 2
DHIván Herrera 41 · Nolan Gorman 14 · Yohel Pozo 6
SPDustin May 2.5 · Andre Pallante 1.5 · Michael McGreevy 1.3 · Kyle Leahy 1.3 · Matthew Liberatore 0.5
BP Riley O'Brien 17 · JoJo Romero 16 · Ryne Stanek 11 · George Soriano 9 · Justin Bruihl 6 · Gordon Graceffo 3 · Matt Svanson 1 · Ryan Fernandez 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
JJ Wetherholt 23 986.0 0.8 +36.7
Jordan Walker 24 pre-arb 1013.9 0.8 +23.9
Dustin May 28 FA-elig 203.9 12.0 +12.7
Alec Burleson 27 arb 1022.9 3.3 +15.3
Andre Pallante 27 arb 192.4 4.0 +11.3
Masyn Winn 24 pre-arb 952.2 0.8 +12.8
Kyle Leahy 29 pre-arb 192.0 0.8 +12.1
Michael McGreevy 25 pre-arb 202.0 0.8 +11.9
Iván Herrera 26 pre-arb 1021.5 0.8 +8.7
Lars Nootbaar 28 arb 391.1 5.2 +1.6
Riley O'Brien 31 pre-arb 420.9 0.8 +4.7
Matthew Liberatore 26 pre-arb 200.8 2.3 +2.5
Pedro Pagés 27 pre-arb 590.7 0.8 +3.5
Hunter Dobbins 26 pre-arb 60.6 0.0 +4.1
Bryan Torres 28 350.6 0.5 +2.9
José Fermín 27 pre-arb 670.4 0.8 +2.0
George Soriano 27 pre-arb 460.4 0.8 +1.9
Ryan Fernandez 28 pre-arb 110.3 0.6 +1.6
Justin Bruihl 29 pre-arb 440.3 0.8 +0.8
JoJo Romero 29 arb 440.2 4.3 -3.0
Blaze Jordan 23 300.2
Bruce Zimmermann 31 10.1
Jimmy Crooks 24 310.1 0.5 +0.2
Matt Pushard 28 60.0 0.8 -0.5
Matt Svanson 27 pre-arb 390.0 0.3 -0.1
Brycen Mautz 24 10.0 0.0 +0.1
Max Rajcic 24 60.0
Scott Blewett 30 10.0
Nathan Church 25 pre-arb 85-0.1 0.8 -1.2
Jared Shuster 27 pre-arb 5-0.1 0.1 -0.8
Ryne Stanek 34 FA-elig 44-0.2 3.5 -4.7
Nelson Velázquez 27 29-0.2 0.5 -1.7
Yohel Pozo 29 pre-arb 18-0.3 0.8 -2.5
Victor Scott II 25 61-0.4 0.3 -2.8
Ramón Urías 32 arb 25-0.4 1.5 -4.1
Gordon Graceffo 26 pre-arb 42-0.4 0.8 -3.5
Nolan Gorman 26 arb 62-0.4 2.7 -5.4
Luis Gastelum 24 7-0.5
Thomas Saggese 24 pre-arb 32-0.6 0.8 -4.7
Chris Roycroft↔ TBR 29 pre-arb 7-0.6 0.0 -4.1
César Prieto 27 pre-arb 6-0.7 0.6 -4.8

Key additions (new vs 2025)

PlayerfWAR
JJ Wetherholt3.8
Dustin May2.5
Hunter Dobbins0.4
George Soriano0.3
Justin Bruihl0.2
Bruce Zimmermann0.1

Key subtractions (gone from 2025)

Player2025 fWAR
Sonny Gray3.6
Brendan Donovan2.9
Willson Contreras2.8
Steven Matz1.2
Phil Maton1.0
Nolan Arenado0.9

Current & Historical Payroll

2026 payroll
$54.9M
luxury tax $52M
2027 guaranteed
$30.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$18M$16M
Proj. arbitration$19M
Proj. pre-arbitration$14M$2M
Player benefits$18M$18M
Total CBT payroll$70M$36M
Tax space+$174M+$211M
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 (3)

PlayerAgeSvc2026202720282029203020312032
Dustin May286.059$12.0M$20.0MMUTUALFREE AGENT
Ryne Stanek348.038$3.5M$6.0MCLUBFREE AGENT
Ramón Urías325.025$1.5M$4.0MMUTUALFREE AGENT

Arbitration-eligible (6)

PlayerAgeSvc2026202720282029203020312032
Lars Nootbaar284.076$5.2MARB 3FREE AGENT
JoJo Romero295.045$4.3MFREE AGENT
Andre Pallante273.145$4.0MARB 2ARB 3FREE AGENT
Alec Burleson273.029$3.3MARB 2ARB 3FREE AGENT
Nolan Gorman263.114$2.7MARB 2ARB 3FREE AGENT
Nelson Velázquez273.031$505kARB 2ARB 3FREE AGENT

Pre-arbitration (27)

PlayerAgeSvc2026202720282029203020312032
Matthew Liberatore262.144$2.3MARB 1ARB 2ARB 3FREE AGENT
Justin Bruihl291.112$850kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Yohel Pozo291.052$800kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Masyn Winn242.045$799kARB 1ARB 2ARB 3FREE AGENT
Jordan Walker242.044$799kARB 1ARB 2ARB 3FREE AGENT
Iván Herrera262.034$799kARB 1ARB 2ARB 3FREE AGENT
Pedro Pagés271.166$797kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kyle Leahy291.140$796kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Riley O'Brien311.129$795kPre-ArbARB 1ARB 2ARB 3FREE AGENT
George Soriano271.095$793kPre-ArbARB 1ARB 2ARB 3FREE AGENT
José Fermín271.075$792kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Michael McGreevy250.091$785kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Thomas Saggese240.144$783kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Nathan Church250.043$782kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
JJ Wetherholt23$780k
Matt Pushard28$780k
Gordon Graceffo260.091$755kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
César Prieto270.031$609kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ryan Fernandez281.090$606kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jimmy Crooks24$505k
Victor Scott II252.000$326kARB 1ARB 2ARB 3FREE AGENT
Matt Svanson270.116$256kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jared Shuster271.148$105kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Chris Roycroft290.150$27kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Hunter Dobbins260.152$4kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Scott Blewett301.102Pre-ArbARB 1ARB 2ARB 3FREE AGENT
Bruce Zimmermann312.129ARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Cardinals 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.069, 16.47], [1986, 0.875, -4.71], [1987, 1.046, 1.67], [1988, 1.247, -7.23], [1989, 1.131, 10.44], [1990, 1.154, 1.97], [1991, 0.867, 1.78], [1992, 0.878, 3.34], [1993, 0.717, 4.95], [1994, 0.905, -2.82], [1995, 0.956, -8.39], [1996, 1.202, 0.25], [1997, 1.131, 5.0], [1998, 1.286, 3.59], [1999, 1.017, -3.03], [2000, 1.215, 9.66], [2001, 1.174, 7.13], [2002, 1.208, 8.04], [2003, 1.243, 4.92], [2004, 1.361, 12.09], [2005, 1.271, 5.55], [2006, 1.249, -5.69], [2007, 1.152, -8.12], [2008, 1.055, 7.7], [2009, 1.17, 5.07], [2010, 1.151, 0.15], [2011, 1.224, 4.54], [2012, 1.192, 8.25], [2013, 0.905, 9.76], [2014, 1.059, -0.95], [2015, 1.068, 9.12], [2016, 1.083, 6.89], [2017, 1.113, 5.24], [2018, 1.183, -3.7], [2019, 1.166, 0.06], [2021, 1.331, 0.51], [2022, 1.154, 9.59], [2023, 1.131, -2.95], [2024, 1.109, -3.12], [2025, 0.777, -0.79], [2026, 0.344, 7.3]], TEAMCOL = '#C41E3A'; 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", "OF", "SP", "IF", "SP", "IF", "SP", "SP", "C", "OF", "RP", "SP", "C", "SP", "OF", "OF", "RP", "RP", "RP", "RP", "IF", "RP", "C", "RP", "RP", "RP", "RP", "RP", "OF", "RP", "RP", "OF", "C", "OF", "IF", "RP", "IF", "RP", "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": ["JJ Wetherholt", "Jordan Walker", "Dustin May", "Alec Burleson", "Andre Pallante", "Masyn Winn", "Kyle Leahy", "Michael McGreevy", "Iv\u00e1n Herrera", "Lars Nootbaar", "Riley O\u0027Brien", "Matthew Liberatore", "Pedro Pag\u00e9s", "Hunter Dobbins", "Bryan Torres", "Jos\u00e9 Ferm\u00edn", "George Soriano", "Ryan Fernandez", "Justin Bruihl", "JoJo Romero", "Blaze Jordan", "Bruce Zimmermann", "Jimmy Crooks", "Matt Pushard", "Matt Svanson", "Brycen Mautz", "Max Rajcic", "Scott Blewett", "Nathan Church", "Jared Shuster", "Ryne Stanek", "Nelson Vel\u00e1zquez", "Yohel Pozo", "Victor Scott II", "Ram\u00f3n Ur\u00edas", "Gordon Graceffo", "Nolan Gorman", "Luis Gastelum", "Thomas Saggese", "Chris Roycroft"], "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.79, 2.49, 2.49, 1.87, 1.54, 1.37, 1.3, 1.28, 0.96, 0.69, 0.56, 0.48, 0.44, 0.41, 0.35, 0.28, 0.27, 0.22, 0.17, 0.12, 0.11, 0.09, 0.07, 0.03, 0.02, 0.01, 0.01, 0.0, -0.05, -0.07, -0.12, -0.12, -0.18, -0.25, -0.27, -0.27, -0.27, -0.29, -0.4, -0.41]}; 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:'STL',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);});