RRosternomics

Orioles BAL

GM: Mike Elias

Overview

CONTEND ↘ cooling
expensive & winning · year 1 in this regime · payroll 1.03× league · TAB +2.5
+2.5TAB — talent above budget14th/30
-2.9WAB — realized wins (TAB + luck)21st/30
4.0depth — 2+ WAR regulars7th/30
-1.8dead WAR — sub-replacement drag3rd/30
56%dead money %26th/30
0.481win %21st/30
37.2roster fWAR9th/30
+69surplus $M (value vs cost)11th/30
$165Mpayroll $M14th/30luxury tax $193M

Roster

CFLeody Taveras 51GColton Cowser 20GBlaze Alexander 8G
LFTaylor Ward 58GColton Cowser 7GBlaze Alexander 6G
RFTyler O'Neill 38GColton Cowser 26GDylan Beavers 17G
SSGunnar Henderson 65GBlaze Alexander 10GJackson Holliday 1G
2BJeremiah Jackson 51GJackson Holliday 20GBlaze Alexander 14G
3BCoby Mayo 47GBlaze Alexander 31GWeston Wilson 17G
1BPete Alonso 66GCoby Mayo 6GSamuel Basallo 4G
STARTERSShane Baz 2.4WARTrevor Rogers 2.2WARKyle Bradish 1.7WARBrandon Young 1.6WARChris Bassitt 0.3WAR
CAdley Rutschman 40GSamuel Basallo 30GSam Huff 7G
DHSamuel Basallo 21GTaylor Ward 12GAdley Rutschman 10G
BULLPEN Ryan Helsley 7svRico Garcia 9hTyler Wells 8hGrant Wolfram 7hAnthony Nunez 6hYennier Cano 4hAndrew Kittredge 2hKeegan Akin 0h
CAdley Rutschman 40 · Samuel Basallo 30 · Sam Huff 7
1BPete Alonso 66 · Coby Mayo 6 · Samuel Basallo 4
2BJeremiah Jackson 51 · Jackson Holliday 20 · Blaze Alexander 14
3BCoby Mayo 47 · Blaze Alexander 31 · Weston Wilson 17
SSGunnar Henderson 65 · Blaze Alexander 10 · Jackson Holliday 1
LFTaylor Ward 58 · Colton Cowser 7 · Blaze Alexander 6
CFLeody Taveras 51 · Colton Cowser 20 · Blaze Alexander 8
RFTyler O'Neill 38 · Colton Cowser 26 · Dylan Beavers 17
DHSamuel Basallo 21 · Taylor Ward 12 · Adley Rutschman 10
SPShane Baz 2.4 · Trevor Rogers 2.2 · Kyle Bradish 1.7 · Brandon Young 1.6 · Chris Bassitt 0.3
BP Ryan Helsley 7 · Rico Garcia 9 · Tyler Wells 8 · Grant Wolfram 7 · Anthony Nunez 6 · Yennier Cano 4 · Andrew Kittredge 2 · Keegan Akin 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
Pete Alonso 31 FA-elig 1043.9 21.0 +3.8
Shane Baz 27 arb 203.8 1.8 +22.1
Adley Rutschman 28 arb 673.5 7.2 +14.9
Trevor Rogers 28 arb 193.4 6.2 +15.1
Gunnar Henderson 25 arb 1033.0 8.5 +10.6
Taylor Ward 32 arb 1032.7 12.2 +4.7
Kyle Bradish 29 arb 202.6 3.5 +12.8
Brandon Young 27 pre-arb 162.6 0.7 +15.4
Blaze Alexander 27 pre-arb 822.3 0.8 +13.5
Colton Cowser 26 pre-arb 911.6 0.8 +9.4
Samuel Basallo 21 pre-arb 861.3 1.6 +6.9
Tyler Wells 31 arb 401.3 2.5 +5.9
Grant Wolfram 29 pre-arb 381.1 0.8 +5.9
Leody Taveras 27 arb 881.0 2.0 +4.3
Yennier Cano 32 arb 451.0 1.6 +4.4
Jackson Holliday 22 pre-arb 520.8 0.8 +4.3
Christian Encarnacion-Strand 26 pre-arb 50.7 0.8 +3.8
Rico Garcia 32 pre-arb 470.7 0.9 +3.2
Chris Bassitt 37 FA-elig 120.5 18.5 -15.6
Coby Mayo 24 pre-arb 800.4 0.8 +1.8
Jeremiah Jackson 26 pre-arb 740.3 0.8 +1.2
Anthony Nunez 24 360.3 0.8 +1.2
Tyler O'Neill 31 FA-elig 640.3 16.5 -14.7
Cam Sanders↔ PIT 29 20.3 0.0 +1.6
Dietrich Enns 35 pre-arb 130.2 2.5 -1.1
Keegan Akin 31 arb 240.2 3.0 -1.6
Andrew Kittredge 36 FA-elig 330.1 9.0 -8.3
Ryan Mountcastle 29 arb 80.0 6.8 -6.5
Dylan Beavers 24 pre-arb 500.0 0.8 -0.6
Zach Eflin 32 FA-elig 10.0 8.0 -7.8
Josh Walker 31 pre-arb 50.0 0.0 +0.2
Jose Espada 29 pre-arb 10.0 0.0 +0.0
Cameron Weston 25 10.0 0.0 +0.0
Lou Trivino III↔ PHI 34 20.0 0.0 -0.0
Chadwick Tromp↔ ATL 31 4-0.0 0.1 -0.2
Maverick Handley 28 1-0.0 0.8 -1.0
Michael Siani 26 1-0.0
Cameron Foster 27 6-0.1 0.1 -0.4
Cade Povich 26 pre-arb 4-0.1 0.7 -1.5
Ryan Helsley 31 FA-elig 17-0.1 14.0 -14.8
Albert Suárez 36 arb 16-0.2 0.1 -1.2
Nick Raquet 30 pre-arb 4-0.2 0.6 -1.7
Johnathan Rodríguez 26 pre-arb 7-0.2 0.1 -1.2
Sam Huff 28 13-0.2 0.5 -2.0
Dean Kremer 30 arb 6-0.3 5.8 -7.4
Weston Wilson↔ SEA 31 pre-arb 21-0.3 0.4 -2.6
Yaramil Hiraldo 30 pre-arb 3-0.5 0.8 -3.8
Trey Gibson 24 8-0.6 0.0 -3.7

Key additions (new vs 2025)

PlayerfWAR
Pete Alonso2.5
Shane Baz2.4
Taylor Ward1.7
Blaze Alexander1.5
Leody Taveras0.6
Christian Encarnacion-Strand0.5

Key subtractions (gone from 2025)

Player2025 fWAR
Ryan O'Hearn2.4
Ramón Laureano2.3
Cedric Mullins1.1
Alex Jackson1.0
Ramón Urías0.9
Seranthony Domínguez0.6

Current & Historical Payroll

2026 payroll
$165.1M
luxury tax $193M
2027 guaranteed
$109.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$122M$93M$53M$53M$53M$8M
Proj. arbitration$56M
Proj. pre-arbitration$14M$9M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$211M$121M$71M$71M$71M$26M
Tax space+$33M+$126M+$179M+$182M+$185M+$233M
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 (11)

PlayerAgeSvc2026202720282029203020312032
Pete Alonso317.000$21.0M$33.5M$33.5M$33.5M$33.5MFREE AGENT
Chris Bassitt379.130$18.5MFREE AGENT
Tyler O'Neill317.059$16.5MOPT-OUT$16.5MFREE AGENT
Ryan Helsley316.105$14.0M$14.0MPLAYERFREE AGENT
Andrew Kittredge367.070$9.0MCLUBFREE AGENT
Zach Eflin329.018$8.0M$25.0MMUTUALFREE AGENT
Ryan Mountcastle295.105$6.8M$7.5MCLUBFREE AGENT
Dietrich Enns350.162$2.5M$3.5MCLUBPre-ArbARB 1ARB 2ARB 3FREE AGENT
Shane Baz273.158$1.8M$7.8M$10.8M$21.8M$25.8MFREE AGENT
Samuel Basallo210.043$1.6M$1.6M$1.6M$4.6M$7.6M$11.6M$15.6M
Lou Trivino III347.163$10kFREE AGENT

Arbitration-eligible (12)

PlayerAgeSvc2026202720282029203020312032
Taylor Ward325.164$12.2MFREE AGENT
Gunnar Henderson253.036$8.5MARB 2ARB 3FREE AGENT
Adley Rutschman284.000$7.2MARB 3FREE AGENT
Trevor Rogers285.047$6.2MFREE AGENT
Dean Kremer304.112$5.8MARB 3FREE AGENT
Kyle Bradish293.160$3.5MARB 2ARB 3FREE AGENT
Keegan Akin315.083$3.0MFREE AGENT
Tyler Wells314.132$2.5MARB 3FREE AGENT
Leody Taveras274.031$2.0MARB 3FREE AGENT
Yennier Cano323.065$1.6MARB 2ARB 3FREE AGENT
Albert Suárez363.019$145kARB 2ARB 3FREE AGENT
Michael Siani264.000ARB 3FREE AGENT

Pre-arbitration (23)

PlayerAgeSvc2026202720282029203020312032
Rico Garcia321.127$900kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Christian Encarnacion-Strand262.012$820kARB 1ARB 2ARB 3FREE AGENT
Jackson Holliday221.078$802kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Colton Cowser262.043$802kARB 1ARB 2ARB 3FREE AGENT
Grant Wolfram290.084$800kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Blaze Alexander271.053$793kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Coby Mayo240.170$791kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jeremiah Jackson260.061$789kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Dylan Beavers240.044$787kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Yaramil Hiraldo300.053$786kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Anthony Nunez24$780k
Maverick Handley280.108$780kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brandon Young270.095$693kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cade Povich261.080$688kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Nick Raquet300.021$556kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Sam Huff282.156$471kARB 1ARB 2ARB 3FREE AGENT
Weston Wilson311.097$406kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Chadwick Tromp312.120$149kARB 1ARB 2ARB 3FREE AGENT
Cameron Foster27$58k
Johnathan Rodríguez260.097$50kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Josh Walker310.122$25kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jose Espada290.020$21kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cam Sanders290.037$8kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Orioles 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.279, -0.79], [1986, 1.024, -2.88], [1987, 1.257, -14.98], [1988, 1.114, -22.14], [1989, 0.618, 2.87], [1990, 0.543, 1.73], [1991, 0.666, 6.86], [1992, 0.804, 11.11], [1993, 0.919, 8.63], [1994, 1.222, 10.8], [1995, 1.332, 2.94], [1996, 1.566, -1.11], [1997, 1.545, 3.87], [1998, 1.777, -0.01], [1999, 1.587, 0.78], [2000, 1.402, -10.94], [2001, 0.855, -12.99], [2002, 0.789, -9.24], [2003, 0.743, -3.29], [2004, 0.669, 8.32], [2005, 1.054, -3.62], [2006, 0.989, -9.38], [2007, 1.044, -8.69], [2008, 0.682, -6.7], [2009, 0.699, -11.97], [2010, 0.879, -14.0], [2011, 0.911, -16.93], [2012, 0.876, -2.38], [2013, 0.885, 3.67], [2014, 0.957, 6.51], [2015, 0.904, 0.4], [2016, 1.221, 0.6], [2017, 1.34, -17.57], [2018, 1.008, -22.32], [2019, 0.63, -16.8], [2021, 0.211, -1.62], [2022, 0.268, 13.96], [2023, 0.55, 15.82], [2024, 0.736, 14.21], [2025, 0.903, -9.03], [2026, 1.035, 2.53]], TEAMCOL = '#DF4601'; 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", "C", "SP", "IF", "OF", "SP", "SP", "IF", "OF", "C", "RP", "RP", "OF", "RP", "IF", "IF", "RP", "SP", "IF", "IF", "RP", "OF", "RP", "RP", "RP", "RP", "IF", "OF", "SP", "RP", "RP", "RP", "RP", "C", "C", "OF", "RP", "SP", "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": ["Pete Alonso", "Shane Baz", "Adley Rutschman", "Trevor Rogers", "Gunnar Henderson", "Taylor Ward", "Kyle Bradish", "Brandon Young", "Blaze Alexander", "Colton Cowser", "Samuel Basallo", "Tyler Wells", "Grant Wolfram", "Leody Taveras", "Yennier Cano", "Jackson Holliday", "Christian Encarnacion-Strand", "Rico Garcia", "Chris Bassitt", "Coby Mayo", "Jeremiah Jackson", "Anthony Nunez", "Tyler O\u0027Neill", "Cam Sanders", "Dietrich Enns", "Keegan Akin", "Andrew Kittredge", "Ryan Mountcastle", "Dylan Beavers", "Zach Eflin", "Josh Walker", "Jose Espada", "Cameron Weston", "Lou Trivino III", "Chadwick Tromp", "Maverick Handley", "Michael Siani", "Cameron Foster", "Cade Povich", "Ryan Helsley"], "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.53, 2.44, 2.26, 2.17, 1.94, 1.72, 1.67, 1.64, 1.46, 1.04, 0.87, 0.85, 0.68, 0.64, 0.61, 0.52, 0.47, 0.42, 0.29, 0.26, 0.2, 0.2, 0.18, 0.16, 0.15, 0.14, 0.07, 0.03, 0.02, 0.02, 0.02, 0.01, 0.0, 0.0, -0.0, -0.02, -0.02, -0.04, -0.08, -0.08]}; 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:'BAL',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);});