RRosternomics

Pirates PIT

GM: Ben Cherington

Overview

WINDOW ↗ rising
cheap & winning · year 2 in this regime · payroll 0.67× league · TAB +14.5
+14.5TAB — talent above budget2nd/30
+3.6WAB — realized wins (TAB + luck)12th/30
5.0depth — 2+ WAR regulars5th/30
-3.1dead WAR — sub-replacement drag11th/30
26%dead money %7th/30
0.510win %12th/30
44.0roster fWAR4th/30
+169surplus $M (value vs cost)6th/30
$107Mpayroll $M21st/30luxury tax $113M

Roster

CFOneil Cruz 57GJake Mangum 10GJhostynxon Garcia 6G
LFBryan Reynolds 51GJake Mangum 29GBilly Cook 11G
RFRyan O'Hearn 37GBryan Reynolds 22GBilly Cook 13G
SSKonnor Griffin 49GJared Triolo 21GNick Gonzales 3G
2BBrandon Lowe 61GNick Gonzales 17GTyler Callihan 1G
3BNick Gonzales 51GNick Yorke 19GJared Triolo 11G
1BSpencer Horwitz 60GRyan O'Hearn 23GJared Triolo 5G
STARTERSPaul Skenes 3.5WARBraxton Ashcraft 2.5WARCarmen Mlodzinski 1.3WARBubba Chandler 1.2WARMitch Keller 1.2WAR
CHenry Davis 44GJoey Bart 20GEndy Rodríguez 16G
DHMarcell Ozuna 48GRyan O'Hearn 7GBryan Reynolds 7G
BULLPEN Gregory Soto 9svMason Montgomery 7hIsaac Mattson 7hDennis Santana 4hYohan Ramírez 3hEvan Sisk 3hWilber Dotel 2hBrandan Bidois 0h
CHenry Davis 44 · Joey Bart 20 · Endy Rodríguez 16
1BSpencer Horwitz 60 · Ryan O'Hearn 23 · Jared Triolo 5
2BBrandon Lowe 61 · Nick Gonzales 17 · Tyler Callihan 1
3BNick Gonzales 51 · Nick Yorke 19 · Jared Triolo 11
SSKonnor Griffin 49 · Jared Triolo 21 · Nick Gonzales 3
LFBryan Reynolds 51 · Jake Mangum 29 · Billy Cook 11
CFOneil Cruz 57 · Jake Mangum 10 · Jhostynxon Garcia 6
RFRyan O'Hearn 37 · Bryan Reynolds 22 · Billy Cook 13
DHMarcell Ozuna 48 · Ryan O'Hearn 7 · Bryan Reynolds 7
SPPaul Skenes 3.5 · Braxton Ashcraft 2.5 · Carmen Mlodzinski 1.3 · Bubba Chandler 1.2 · Mitch Keller 1.2
BP Gregory Soto 9 · Mason Montgomery 7 · Isaac Mattson 7 · Dennis Santana 4 · Yohan Ramírez 3 · Evan Sisk 3 · Wilber Dotel 2 · Brandan Bidois 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
Paul Skenes 24 pre-arb 215.4 1.1 +33.2
Bryan Reynolds 31 FA-elig 1044.2 14.2 +12.0
Brandon Lowe 31 FA-elig 964.0 11.5 +13.6
Braxton Ashcraft 26 pre-arb 204.0 0.8 +24.2
Nick Gonzales 27 pre-arb 983.3 0.8 +20.1
Esmerlyn Valdez 22 352.7 0.5 +16.2
Oneil Cruz 27 arb 642.5 3.3 +12.1
Carmen Mlodzinski 27 pre-arb 222.1 0.8 +12.3
Spencer Horwitz 28 pre-arb 742.1 0.8 +12.2
Bubba Chandler 23 pre-arb 201.9 0.8 +11.1
Jared Jones 24 pre-arb 101.9 0.8 +10.9
Mitch Keller 30 FA-elig 201.8 16.9 -5.3
Jake Mangum 30 pre-arb 821.8 0.8 +10.7
Konnor Griffin 20 591.8 2.3 +8.9
Mason Montgomery 26 pre-arb 421.6 0.8 +9.2
Endy Rodríguez 26 pre-arb 351.3 0.6 +7.9
Ryan O'Hearn 32 FA-elig 891.3 14.0 -6.1
Evan Sisk 29 pre-arb 321.0 0.7 +5.9
Gregory Soto 31 FA-elig 431.0 7.8 -1.7
Jared Triolo 28 pre-arb 690.5 0.8 +2.6
Wilber Dotel 23 110.5 0.6 +2.3
Isaac Mattson 30 pre-arb 340.4 0.2 +2.4
Henry Davis 26 pre-arb 670.4 0.8 +1.8
Jacob Gonzalez 24 70.4 0.5 +1.9
Yohan Ramírez 31 arb 430.3 0.8 +1.4
Rafael Flores Jr. 25 pre-arb 80.2 0.8 +0.2
Khristian Curtis 24 10.1
Joey Bart↔ ATL 29 arb 210.1 1.4 -0.5
Tyler Callihan 26 pre-arb 360.1 0.8 -0.0
Hunter Stratton↔ ATL 29 20.1 0.0 +0.5
José Urquidy 31 FA-elig 50.1 1.5 -1.2
Brandon Eisert↔ CHW 28 50.0 0.1 -0.1
Jack Brannigan 25 1-0.0 0.8 -1.0
Chris Devenski 35 FA-elig 3-0.1 0.7 -1.1
Cam Sanders↔ BAL 29 pre-arb 9-0.1 0.0 -0.5
Billy Cook 27 pre-arb 60-0.1 0.2 -1.1
Davis Wendzel 29 pre-arb 2-0.1 0.8 -1.8
Antwone Kelly 22 2-0.2 0.8 -2.3
Nick Yorke 24 pre-arb 27-0.3 0.0 -1.6
Justin Lawrence↔ MIN 31 arb 23-0.3 0.8 -2.4
Jhostynxon Garcia 23 pre-arb 13-0.5 0.5 -3.6
Brandan Bidois 25 15-0.5 0.6 -3.9
Dennis Santana 30 arb 44-0.5 3.5 -6.8
Hunter Barco 25 pre-arb 5-0.7 0.1 -4.8
Marcell Ozuna 35 FA-elig 66-1.3 10.5 -18.8

Key additions (new vs 2025)

PlayerfWAR
Brandon Lowe2.6
Jared Jones1.2
Jake Mangum1.2
Konnor Griffin1.1
Mason Montgomery1.0
Ryan O'Hearn0.8

Key subtractions (gone from 2025)

Player2025 fWAR
Mike Burrows1.3
David Bednar1.3
Ke'Bryan Hayes0.8
Isiah Kiner-Falefa0.8
Caleb Ferguson0.7
Bailey Falter0.5

Current & Historical Payroll

2026 payroll
$107.3M
luxury tax $113M
2027 guaranteed
$68.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$90M$71M$44M$29M$29M$29M
Proj. arbitration$11M
Proj. pre-arbitration$12M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$131M$89M$62M$47M$47M$47M
Tax space+$113M+$158M+$188M+$206M+$209M+$212M
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 (9)

PlayerAgeSvc2026202720282029203020312032
Mitch Keller306.026$16.9M$18.4M$20.4MFREE AGENT
Bryan Reynolds316.163$14.2M$15.2M$15.2M$15.2M$15.2M$20.0MCLUBFREE AGENT
Ryan O'Hearn327.002$14.0M$15.0MFREE AGENT
Brandon Lowe317.058$11.5MCLUBFREE AGENT
Marcell Ozuna3512.124$10.5M$16.0MMUTUALFREE AGENT
Gregory Soto316.102$7.8MFREE AGENT
Konnor Griffin20$2.3M$3.3M$5.3M$7.3M$13.8M$22.3M$27.8M
José Urquidy316.049$1.5MFREE AGENT
Chris Devenski357.115$686kFREE AGENT

Arbitration-eligible (5)

PlayerAgeSvc2026202720282029203020312032
Dennis Santana305.126$3.5MFREE AGENT
Oneil Cruz273.110$3.3MARB 2ARB 3FREE AGENT
Joey Bart294.020$1.4MARB 3FREE AGENT
Yohan Ramírez313.135$825kARB 2ARB 3FREE AGENT
Justin Lawrence313.167$761kARB 2ARB 3FREE AGENT

Pre-arbitration (25)

PlayerAgeSvc2026202720282029203020312032
Paul Skenes242.000$1.1MARB 1ARB 2ARB 3FREE AGENT
Davis Wendzel290.070$850kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Rafael Flores Jr.250.013$820kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tyler Callihan260.152$820kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jared Triolo282.071$819kARB 1ARB 2ARB 3FREE AGENT
Carmen Mlodzinski272.081$807kARB 1ARB 2ARB 3FREE AGENT
Spencer Horwitz281.152$802kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jared Jones242.000$802kARB 1ARB 2ARB 3FREE AGENT
Nick Gonzales272.018$802kARB 1ARB 2ARB 3FREE AGENT
Henry Davis262.037$800kARB 1ARB 2ARB 3FREE AGENT
Jake Mangum301.000$800kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Braxton Ashcraft260.126$795kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mason Montgomery261.021$793kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Bubba Chandler230.038$787kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Evan Sisk290.060$732kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Endy Rodríguez262.077$605kARB 1ARB 2ARB 3FREE AGENT
Wilber Dotel23$580k
Jhostynxon Garcia230.009$546kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Isaac Mattson300.150$242kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Billy Cook270.025$234kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brandon Eisert281.034$102kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Hunter Barco250.006$88kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Nick Yorke240.042$42kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cam Sanders290.037$34kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Hunter Stratton291.071$6kPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Pirates 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, 0.994, -10.2], [1986, 0.716, 0.52], [1987, 0.559, 12.18], [1988, 0.541, 10.89], [1989, 0.851, -1.23], [1990, 0.833, 13.44], [1991, 1.005, 13.53], [1992, 1.147, 1.31], [1993, 0.665, -1.3], [1994, 0.69, -7.9], [1995, 0.54, -6.8], [1996, 0.666, -7.78], [1997, 0.273, 2.07], [1998, 0.344, 1.55], [1999, 0.486, 3.48], [2000, 0.524, -2.74], [2001, 0.742, -14.99], [2002, 0.565, -7.14], [2003, 0.659, 0.8], [2004, 0.449, -1.06], [2005, 0.504, -5.41], [2006, 0.532, -6.45], [2007, 0.54, -3.47], [2008, 0.535, -9.72], [2009, 0.459, -1.65], [2010, 0.395, -15.14], [2011, 0.561, -11.48], [2012, 0.686, -6.18], [2013, 0.767, 8.23], [2014, 0.721, 9.24], [2015, 0.841, 11.31], [2016, 0.812, -0.52], [2017, 0.75, -3.64], [2018, 0.664, 4.59], [2019, 0.502, -3.88], [2021, 0.371, -9.98], [2022, 0.391, -8.07], [2023, 0.429, 0.89], [2024, 0.604, -7.07], [2025, 0.488, 0.03], [2026, 0.672, 14.53]], TEAMCOL = '#C9A227'; 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", "OF", "IF", "SP", "IF", "OF", "OF", "SP", "IF", "SP", "SP", "SP", "OF", "IF", "RP", "C", "OF", "RP", "RP", "IF", "RP", "RP", "C", "IF", "RP", "C", "RP", "C", "IF", "RP", "RP", "RP", "IF", "RP", "RP", "OF", "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": ["Paul Skenes", "Bryan Reynolds", "Brandon Lowe", "Braxton Ashcraft", "Nick Gonzales", "Esmerlyn Valdez", "Oneil Cruz", "Carmen Mlodzinski", "Spencer Horwitz", "Bubba Chandler", "Jared Jones", "Mitch Keller", "Jake Mangum", "Konnor Griffin", "Mason Montgomery", "Endy Rodr\u00edguez", "Ryan O\u0027Hearn", "Evan Sisk", "Gregory Soto", "Jared Triolo", "Wilber Dotel", "Isaac Mattson", "Henry Davis", "Jacob Gonzalez", "Yohan Ram\u00edrez", "Rafael Flores Jr.", "Khristian Curtis", "Joey Bart", "Tyler Callihan", "Hunter Stratton", "Jos\u00e9 Urquidy", "Brandon Eisert", "Jack Brannigan", "Chris Devenski", "Cam Sanders", "Billy Cook", "Davis Wendzel", "Antwone Kelly", "Nick Yorke", "Justin Lawrence"], "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.49, 2.68, 2.56, 2.55, 2.13, 1.71, 1.57, 1.33, 1.33, 1.21, 1.19, 1.18, 1.17, 1.15, 1.02, 0.86, 0.81, 0.67, 0.62, 0.35, 0.29, 0.27, 0.26, 0.24, 0.22, 0.11, 0.09, 0.09, 0.08, 0.05, 0.03, 0.0, -0.01, -0.04, -0.05, -0.09, -0.09, -0.15, -0.16, -0.17]}; 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:'PIT',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);});