RRosternomics

Braves ATL

GM: Alex Anthopoulos

Overview

FORCED-SPEND ↘ cooling
expensive & losing · year 2 in this regime · payroll 1.42× league · TAB -2.7
-2.7TAB — talent above budget18th/30
+6.1WAB — realized wins (TAB + luck)9th/30
4.0depth — 2+ WAR regulars7th/30
-4.9dead WAR — sub-replacement drag22nd/30
46%dead money %22nd/30
0.592win %3rd/30
35.7roster fWAR15th/30
-14surplus $M (value vs cost)24th/30
$226Mpayroll $M7th/30luxury tax $165M

Roster

CFMichael Harris II 54GMauricio Dubón 11GEli White 9G
LFMike Yastrzemski 43GMauricio Dubón 26GEli White 14G
RFRonald Acuña Jr. 49GMike Yastrzemski 16GEli White 12G
SSMauricio Dubón 33GJorge Mateo 27GHa-Seong Kim 15G
2BOzzie Albies 68GJorge Mateo 1GLuke Williams 1G
3BAustin Riley 67GMauricio Dubón 3GKyle Farmer 2G
1BMatt Olson 68GDominic Smith 1G
STARTERSChris Sale 3.5WARBryce Elder 1.1WARMartín Pérez 0.6WARGrant Holmes 0.5WARReynaldo López 0.3WAR
CDrake Baldwin 36GAustin Wynns 19GSandy León 18G
DHDominic Smith 39GDrake Baldwin 19GJorge Mateo 5G
BULLPEN Raisel Iglesias 13svDylan Lee 13hRobert Suarez 10hTyler Kinley 9hDidier Fuentes 4hAaron Bummer 4hJoel Payamps 2hDylan Dodd 1h
CDrake Baldwin 36 · Austin Wynns 19 · Sandy León 18
1BMatt Olson 68 · Dominic Smith 1
2BOzzie Albies 68 · Jorge Mateo 1 · Luke Williams 1
3BAustin Riley 67 · Mauricio Dubón 3 · Kyle Farmer 2
SSMauricio Dubón 33 · Jorge Mateo 27 · Ha-Seong Kim 15
LFMike Yastrzemski 43 · Mauricio Dubón 26 · Eli White 14
CFMichael Harris II 54 · Mauricio Dubón 11 · Eli White 9
RFRonald Acuña Jr. 49 · Mike Yastrzemski 16 · Eli White 12
DHDominic Smith 39 · Drake Baldwin 19 · Jorge Mateo 5
SPChris Sale 3.5 · Bryce Elder 1.1 · Martín Pérez 0.6 · Grant Holmes 0.5 · Reynaldo López 0.3
BP Raisel Iglesias 13 · Dylan Lee 13 · Robert Suarez 10 · Tyler Kinley 9 · Didier Fuentes 4 · Aaron Bummer 4 · Joel Payamps 2 · Dylan Dodd 1

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
Chris Sale 37 FA-elig 195.5 18.0 +16.7
Matt Olson 32 FA-elig 1034.8 22.0 +8.2
Michael Harris II 25 974.7 8.0 +21.6
Drake Baldwin 25 pre-arb 803.6 0.8 +21.7
Mauricio Dubón 31 arb 992.9 6.1 +12.4
Dylan Lee 31 arb 482.7 2.2 +14.7
Ozzie Albies 29 FA-elig 1032.6 7.0 +9.4
Didier Fuentes 21 pre-arb 341.9 0.7 +11.4
Bryce Elder 27 pre-arb 191.7 0.8 +9.7
Ronald Acuña Jr. 28 531.3
Raisel Iglesias 36 FA-elig 351.3 16.0 -7.6
Robert Suarez 35 arb 311.3 13.0 -4.7
Martín Pérez 35 FA-elig 191.0 3.1 +3.2
Austin Riley 29 FA-elig 1030.9 22.0 -16.1
Jorge Mateo 31 FA-elig 640.9 1.0 +4.7
Dylan Dodd 28 pre-arb 260.9 0.7 +4.7
Grant Holmes 30 pre-arb 200.7 0.8 +3.7
Eli White 32 arb 620.6 0.9 +2.8
José Suarez↔ OAK 28 80.6 0.1 +3.5
Jim Jarvis 25 180.5 0.0 +3.4
Reynaldo López 32 FA-elig 230.5 14.0 -10.8
Brewer Hicklen 30 60.4
José Azócar 30 110.4 0.0 +2.5
Mike Yastrzemski 35 FA-elig 890.3 9.0 -6.9
Victor Mederos 25 pre-arb 60.3 0.0 +2.1
Jonah Heim↔ OAK 31 120.2 0.2 +0.8
Joey Bart↔ PIT 29 170.1 1.1 -0.2
Owen Murphy 22 30.1
Spencer Strider 27 arb 80.1 20.0 -19.3
Jair Camargo 26 10.1
Ian Hamilton 31 arb 40.1 0.0 +0.3
Luke Williams 29 20.1 0.1 +0.2
James Karinchak 30 arb 90.0 0.8 -0.5
Rowdy Tellez 31 70.0 0.7 -0.4
Elieser Hernández 31 10.0
Hunter Stratton↔ PIT 29 pre-arb 10.0 0.0 +0.2
Anthony Molina 24 pre-arb 3-0.1 0.0 -0.4
Connor Thomas 28 1-0.1
Kyle Farmer 35 FA-elig 12-0.1 1.2 -1.8
Osvaldo Bido↔ CHW 30 6-0.1 0.0 -0.7
Rolddy Muñoz 26 pre-arb 1-0.1 0.0 -0.7
Joel Payamps 32 arb 12-0.2 2.2 -3.4
Carlos Carrasco 39 FA-elig 8-0.3 1.1 -2.7
Danny Young 32 pre-arb 9-0.3 0.9 -2.7
Dominic Smith 31 FA-elig 85-0.3 1.2 -3.2
Chadwick Tromp↔ BAL 31 pre-arb 12-0.3 0.4 -2.5
Tyler Kinley 35 FA-elig 42-0.4 3.0 -5.3
Sean Murphy 31 FA-elig 4-0.4 15.0 -17.3
JR Ritchie 23 13-0.4 0.1 -2.5
Austin Wynns↔ OAK 35 6-0.5 0.1 -3.3
Hurston Waldrep 24 pre-arb 3-0.5 0.8 -4.0
Sandy León 37 FA-elig 21-0.8 0.7 -5.9
Aaron Bummer 32 19-1.2 9.5 -16.9
Ha-Seong Kim 30 arb 27-1.8 20.0 -31.4

Key additions (new vs 2025)

PlayerfWAR
Mauricio Dubón1.9
Robert Suarez0.8
Martín Pérez0.6
Jorge Mateo0.6
Brewer Hicklen0.3
Mike Yastrzemski0.2

Key subtractions (gone from 2025)

Player2025 fWAR
Marcell Ozuna1.2
Nick Allen0.7
Enyel De Los Santos0.5
Pierce Johnson0.3
Nacho Alvarez Jr.0.2
Vidal Bruján0.2

Current & Historical Payroll

2026 payroll
$225.9M
luxury tax $165M
2027 guaranteed
$154.5M

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$130M$119M$93M$55M$42M$21M
Proj. arbitration$12M
Proj. pre-arbitration$23M$27M$27M$12M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$183M$164M$138M$85M$60M$39M
Tax space+$61M+$83M+$112M+$168M+$196M+$220M
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 (21)

PlayerAgeSvc2026202720282029203020312032
Matt Olson328.103$22.0M$22.0M$22.0M$22.0M$20.0MCLUBFREE AGENT
Austin Riley296.138$22.0M$22.0M$22.0M$22.0M$22.0M$22.0M$22.0M
Spencer Strider274.003$20.0M$22.0M$22.0M$22.0MCLUBFREE AGENT
Chris Sale3715.061$18.0MCLUB$27.0M$30.0MCLUBFREE AGENT
Raisel Iglesias3610.154$16.0MFREE AGENT
Sean Murphy316.029$15.0M$15.0M$15.0M$15.0MCLUBFREE AGENT
Reynaldo López328.004$14.0M$8.0MFREE AGENT
Robert Suarez354.000$13.0M$16.0M$16.0MFREE AGENT
Aaron Bummer327.150$9.5MFREE AGENT
Mike Yastrzemski356.128$9.0M$10.0M$7.0MCLUBFREE AGENT
Ozzie Albies298.062$7.0MCLUB$7.0MCLUBFREE AGENT
Martín Pérez3513.038$3.1MFREE AGENT
Tyler Kinley357.014$3.0M$5.5MCLUBFREE AGENT
Dominic Smith316.151$1.2MFREE AGENT
Kyle Farmer357.129$1.2MFREE AGENT
Carlos Carrasco3914.039$1.1MFREE AGENT
Jorge Mateo316.000$1.0MFREE AGENT
Sandy León378.159$738kFREE AGENT
Rowdy Tellez317.004$729kFREE AGENT
Elieser Hernández317.051FREE AGENT
Ronald Acuña Jr.288.000FREE AGENT

Arbitration-eligible (14)

PlayerAgeSvc2026202720282029203020312032
Ha-Seong Kim305.000$20.0MFREE AGENT
Michael Harris II254.000$8.0MARB 3FREE AGENT
Mauricio Dubón315.162$6.1MFREE AGENT
Joel Payamps324.117$2.2MARB 3FREE AGENT
Dylan Lee313.150$2.2MARB 2ARB 3FREE AGENT
Joey Bart294.020$1.1MARB 3FREE AGENT
Eli White323.140$900kARB 2ARB 3FREE AGENT
James Karinchak304.099$840kARB 3FREE AGENT
Jonah Heim315.097$210kFREE AGENT
José Suarez284.064$150kARB 3FREE AGENT
Austin Wynns355.017$149kFREE AGENT
Luke Williams293.101$113kARB 2ARB 3FREE AGENT
José Azócar303.002$50kARB 2ARB 3FREE AGENT
Ian Hamilton313.081$34kARB 2ARB 3FREE AGENT

Pre-arbitration (17)

PlayerAgeSvc2026202720282029203020312032
Danny Young321.160$925kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Bryce Elder272.069$810kARB 1ARB 2ARB 3FREE AGENT
Drake Baldwin251.000$800kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Grant Holmes301.106$800kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Hurston Waldrep240.103$798kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Dylan Dodd280.131$702kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Didier Fuentes210.019$672kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Chadwick Tromp312.120$447kARB 1ARB 2ARB 3FREE AGENT
JR Ritchie23$108k
Osvaldo Bido301.161$47kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Victor Mederos250.101$8kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Rolddy Muñoz260.028$4kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Anthony Molina241.074$4kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Hunter Stratton291.071$3kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jair Camargo262.000ARB 1ARB 2ARB 3FREE AGENT
Connor Thomas28
Brewer Hicklen301.038Pre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Braves 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.431, -23.61], [1986, 1.401, -14.45], [1987, 1.273, -13.43], [1988, 1.03, -21.95], [1989, 0.616, -0.3], [1990, 0.757, -6.68], [1991, 0.705, 2.11], [1992, 1.131, 8.31], [1993, 1.367, 18.33], [1994, 1.367, 16.3], [1995, 1.423, 8.57], [1996, 1.549, 15.1], [1997, 1.317, 22.84], [1998, 1.466, 25.7], [1999, 1.4, 13.27], [2000, 1.486, 7.8], [2001, 1.531, 2.68], [2002, 1.381, 3.37], [2003, 1.507, 11.61], [2004, 1.359, 7.23], [2005, 1.232, 7.29], [2006, 1.02, 0.18], [2007, 0.939, 6.79], [2008, 1.142, -3.51], [2009, 1.101, 6.18], [2010, 1.027, 4.02], [2011, 0.985, -1.77], [2012, 0.885, 5.76], [2013, 0.897, 10.67], [2014, 0.911, -1.0], [2015, 0.791, -14.54], [2016, 0.584, -6.39], [2017, 0.954, -10.45], [2018, 0.699, 9.92], [2019, 1.138, 2.66], [2021, 1.137, 3.69], [2022, 1.274, 14.31], [2023, 1.283, 20.39], [2024, 1.525, 3.14], [2025, 1.218, -5.85], [2026, 1.416, -2.74]], TEAMCOL = '#13274F'; 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": ["SP", "IF", "OF", "C", "OF", "RP", "IF", "RP", "SP", "OF", "RP", "RP", "SP", "IF", "IF", "RP", "SP", "OF", "RP", "IF", "RP", "OF", "OF", "OF", "RP", "C", "C", "RP", "SP", "C", "RP", "OF", "RP", "IF", "RP", "RP", "RP", "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": ["Chris Sale", "Matt Olson", "Michael Harris II", "Drake Baldwin", "Mauricio Dub\u00f3n", "Dylan Lee", "Ozzie Albies", "Didier Fuentes", "Bryce Elder", "Ronald Acu\u00f1a Jr.", "Raisel Iglesias", "Robert Suarez", "Mart\u00edn P\u00e9rez", "Austin Riley", "Jorge Mateo", "Dylan Dodd", "Grant Holmes", "Eli White", "Jos\u00e9 Suarez", "Jim Jarvis", "Reynaldo L\u00f3pez", "Brewer Hicklen", "Jos\u00e9 Az\u00f3car", "Mike Yastrzemski", "Victor Mederos", "Jonah Heim", "Joey Bart", "Owen Murphy", "Spencer Strider", "Jair Camargo", "Ian Hamilton", "Luke Williams", "James Karinchak", "Rowdy Tellez", "Elieser Hern\u00e1ndez", "Hunter Stratton", "Anthony Molina", "Connor Thomas", "Kyle Farmer", "Osvaldo Bido"], "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.5, 3.04, 2.99, 2.27, 1.87, 1.71, 1.65, 1.22, 1.06, 0.85, 0.85, 0.84, 0.63, 0.6, 0.57, 0.54, 0.45, 0.38, 0.36, 0.34, 0.32, 0.27, 0.26, 0.21, 0.21, 0.1, 0.09, 0.08, 0.07, 0.04, 0.03, 0.03, 0.03, 0.03, 0.02, 0.02, -0.04, -0.04, -0.06, -0.06]}; 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:'ATL',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);});