RRosternomics

Red Sox BOS

President of baseball ops: Craig Breslow

Overview

CONTEND ↗ rising
expensive & winning · year 2 in this regime · payroll 1.14× league · TAB +5.1
+5.1TAB — talent above budget11th/30
+0.4WAB — realized wins (TAB + luck)15th/30
5.0depth — 2+ WAR regulars5th/30
-1.6dead WAR — sub-replacement drag1st/30
62%dead money %28th/30
0.520win %10th/30
40.9roster fWAR5th/30
+70surplus $M (value vs cost)14th/30
$182Mpayroll $M11th/30luxury tax $232M

Roster

CFCeddanne Rafaela 63GJarren Duran 8G
LFJarren Duran 51GRoman Anthony 16GMasataka Yoshida 6G
RFWilyer Abreu 66GRoman Anthony 1G
SSTrevor Story 39GIsiah Kiner-Falefa 15GMarcelo Mayer 14G
2BMarcelo Mayer 45GIsiah Kiner-Falefa 24GAndruw Monasterio 9G
3BCaleb Durbin 60GIsiah Kiner-Falefa 7GNick Sogard 6G
1BWillson Contreras 64GAndruw Monasterio 5GMickey Gasper 2G
STARTERSRanger Suarez 3.0WARPayton Tolle 2.3WARSonny Gray 1.8WARConnelly Early 0.7WARGarrett Crochet 0.3WAR
CCarlos Narváez 38GConnor Wong 24GMickey Gasper 10G
DHMasataka Yoshida 30GRoman Anthony 13GAndruw Monasterio 9G
BULLPEN Aroldis Chapman 13svGarrett Whitlock 9hDanny Coulombe 4hGreg Weissert 4hZack Kelly 3hRyan Watson 1hJovani Morán 1hTyler Samaniego 1h
CCarlos Narváez 38 · Connor Wong 24 · Mickey Gasper 10
1BWillson Contreras 64 · Andruw Monasterio 5 · Mickey Gasper 2
2BMarcelo Mayer 45 · Isiah Kiner-Falefa 24 · Andruw Monasterio 9
3BCaleb Durbin 60 · Isiah Kiner-Falefa 7 · Nick Sogard 6
SSTrevor Story 39 · Isiah Kiner-Falefa 15 · Marcelo Mayer 14
LFJarren Duran 51 · Roman Anthony 16 · Masataka Yoshida 6
CFCeddanne Rafaela 63 · Jarren Duran 8
RFWilyer Abreu 66 · Roman Anthony 1
DHMasataka Yoshida 30 · Roman Anthony 13 · Andruw Monasterio 9
SPRanger Suarez 3.0 · Payton Tolle 2.3 · Sonny Gray 1.8 · Connelly Early 0.7 · Garrett Crochet 0.3
BP Aroldis Chapman 13 · Garrett Whitlock 9 · Danny Coulombe 4 · Greg Weissert 4 · Zack Kelly 3 · Ryan Watson 1 · Jovani Morán 1 · Tyler Samaniego 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
Willson Contreras 34 FA-elig 955.3 18.0 +15.2
Ceddanne Rafaela 25 pre-arb 985.2 2.2 +30.4
Ranger Suarez 30 FA-elig 174.7 7.6 +22.2
Wilyer Abreu 27 pre-arb 1003.7 0.8 +22.3
Payton Tolle 23 pre-arb 163.6 0.7 +22.2
Sonny Gray 36 FA-elig 182.9 11.0 +7.1
Jake Bennett 25 102.5 0.0 +15.6
Caleb Durbin 26 pre-arb 952.4 0.8 +14.2
Garrett Whitlock 30 arb 391.9 7.5 +4.3
Isiah Kiner-Falefa 31 FA-elig 471.8 6.0 +5.2
Aroldis Chapman 38 FA-elig 351.6 13.0 -2.9
Connelly Early 24 pre-arb 171.1 0.8 +6.0
Tyron Guerrero 35 220.9
Patrick Sandoval 29 arb 30.8 12.8 -7.8
Anthony Seigler 27 pre-arb 280.8 0.8 +4.0
Justin Slaten 28 pre-arb 280.6 0.8 +3.1
Connor Wong 30 arb 480.6 1.4 +2.4
Garrett Crochet 27 arb 60.5 24.8 -21.5
Roman Anthony 22 pre-arb 300.5 2.6 +0.4
Nick Sogard 28 pre-arb 120.4 0.6 +1.9
Jovani Morán 29 pre-arb 280.4 0.8 +1.5
Masataka Yoshida 32 arb 680.3 18.6 -16.5
Tyler Samaniego 27 200.3 0.2 +1.7
Zack Kelly 31 pre-arb 170.2 0.2 +1.2
Carlos Narváez 27 pre-arb 570.2 0.8 +0.2
Brayan Bello 27 arb 140.1 6.2 -5.3
Eduardo Rivera 23 40.1 0.0 +0.7
Nate Eaton 29 pre-arb 140.1 0.0 +0.5
Tsung-Che Cheng 24 pre-arb 140.1 0.8 -0.4
Jack Anderson↔ MIN 26 30.0 0.0 +0.3
Andruw Monasterio 29 pre-arb 62-0.0 0.8 -0.8
Jahmai Jones↔ DET 28 5-0.0 0.1 -0.1
Brett Harris↔ OAK 28 1-0.0 0.0 -0.1
Alec Gamboa 29 6-0.0 0.0 -0.1
Jarren Duran 29 arb 95-0.0 7.7 -7.7
Ryan Watson 28 33-0.0 0.8 -0.9
Romy Gonzalez 29 arb 16-0.1 1.6 -2.4
Tyler Uberstine 27 1-0.1 0.0 -0.8
Joe La Sorsa 28 1-0.1 0.0 -0.8
Greg Weissert 31 pre-arb 42-0.1 0.8 -1.6
Marcelo Mayer 23 pre-arb 70-0.2 0.8 -1.9
Tommy Kahnle 36 8-0.2 0.5 -1.7
Johan Oviedo 28 arb 1-0.2 1.6 -2.8
Danny Coulombe 36 FA-elig 29-0.2 1.5 -2.8
Mickey Gasper 30 pre-arb 34-0.5 0.6 -3.7
Trevor Story 33 FA-elig 41-0.7 25.0 -29.7

Key additions (new vs 2025)

PlayerfWAR
Willson Contreras3.3
Ranger Suarez3.0
Sonny Gray1.8
Caleb Durbin1.5
Isiah Kiner-Falefa1.1
Tyron Guerrero0.6

Key subtractions (gone from 2025)

Player2025 fWAR
Alex Bregman3.5
Rafael Devers2.0
Lucas Giolito2.0
Justin Wilson1.0
Rob Refsnyder1.0
Hunter Dobbins1.0

Current & Historical Payroll

2026 payroll
$181.5M
luxury tax $232M
2027 guaranteed
$173.3M

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$194M$191M$120M$86M$86M$51M
Proj. arbitration$21M$8M$8M$8M
Proj. pre-arbitration$17M$8M$8M$26M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$250M$217M$145M$112M$112M$102M
Tax space−$-6M+$30M+$105M+$142M+$144M+$157M
Est. tax bill$1M

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 (15)

PlayerAgeSvc2026202720282029203020312032
Trevor Story3310.000$25.0MOPT-OUT$25.0M$25.0MCLUBFREE AGENT
Garrett Crochet275.028$24.8M$28.8M$28.8M$28.8M$28.8M$30.0MOPT-OUTFREE AGENT
Masataka Yoshida323.000$18.6M$18.6MARB 3FREE AGENT
Willson Contreras349.108$18.0M$17.0M$20.0MCLUBFREE AGENT
Aroldis Chapman3815.009$13.0M$13.0MMUTUALFREE AGENT
Sonny Gray3612.061$11.0M$30.0MMUTUALFREE AGENT
Ranger Suarez306.112$7.6M$15.6M$30.6M$30.6M$35.6M$35.0MMUTUALFREE AGENT
Garrett Whitlock305.000$7.5M$8.2MCLUB$10.5MCLUBFREE AGENT
Brayan Bello273.082$6.2M$8.7M$16.2M$19.2M$21.0MCLUBFREE AGENT
Isiah Kiner-Falefa318.000$6.0MFREE AGENT
Roman Anthony220.112$2.6M$4.6M$8.6M$15.6M$19.6M$23.6M$25.6M
Ceddanne Rafaela252.035$2.2M$3.8M$5.8M$7.8M$10.8M$13.2M$16.0MCLUB
Danny Coulombe367.008$1.5MFREE AGENT
Tommy Kahnle3610.131$523kFREE AGENT
Tyron Guerrero3510.000FREE AGENT

Arbitration-eligible (5)

PlayerAgeSvc2026202720282029203020312032
Patrick Sandoval295.149$12.8MFREE AGENT
Jarren Duran293.155$7.7MARB 2ARB 3FREE AGENT
Romy Gonzalez293.083$1.6MARB 2ARB 3FREE AGENT
Johan Oviedo284.078$1.6MARB 3FREE AGENT
Connor Wong303.079$1.4MARB 2ARB 3FREE AGENT

Pre-arbitration (24)

PlayerAgeSvc2026202720282029203020312032
Tsung-Che Cheng240.010$820kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Wilyer Abreu272.041$810kARB 1ARB 2ARB 3FREE AGENT
Greg Weissert312.079$808kARB 1ARB 2ARB 3FREE AGENT
Andruw Monasterio292.099$808kARB 1ARB 2ARB 3FREE AGENT
Carlos Narváez271.038$800kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Caleb Durbin260.164$796kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Justin Slaten282.000$792kARB 1ARB 2ARB 3FREE AGENT
Jovani Morán291.100$790kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Connelly Early240.020$790kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Marcelo Mayer230.128$784kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Anthony Seigler270.090$782kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ryan Watson28$780k
Payton Tolle230.031$655kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mickey Gasper300.149$596kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Nick Sogard280.119$559kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Zack Kelly312.134$215kARB 1ARB 2ARB 3FREE AGENT
Tyler Samaniego27$163k
Jahmai Jones281.148$67kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jack Anderson26$19k
Tyler Uberstine27$13k
Joe La Sorsa28$13k
Brett Harris280.105$10kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Nate Eaton291.020$9kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Eduardo Rivera23$8k

Graphs

Every roster slot ranked best→worst by WAR. Bars = Red Sox 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.054, 10.87], [1986, 1.292, 3.24], [1987, 1.169, 1.08], [1988, 1.293, 13.08], [1989, 1.374, 5.57], [1990, 1.231, 2.11], [1991, 1.488, -1.16], [1992, 1.458, -15.31], [1993, 1.186, 0.61], [1994, 1.186, -9.6], [1995, 1.032, 20.02], [1996, 1.245, -4.96], [1997, 1.075, 6.38], [1998, 1.219, 10.04], [1999, 1.367, 9.76], [2000, 1.424, -1.54], [2001, 1.795, -7.73], [2002, 1.642, 7.11], [2003, 1.556, 14.58], [2004, 1.917, 3.35], [2005, 1.7, 0.86], [2006, 1.597, -10.36], [2007, 1.643, 7.96], [2008, 1.404, 12.69], [2009, 1.427, 3.85], [2010, 1.767, -0.69], [2011, 1.76, 10.08], [2012, 1.477, -12.69], [2013, 1.602, 11.59], [2014, 1.276, -9.14], [2015, 1.406, -8.56], [2016, 1.59, 10.1], [2017, 1.327, 1.92], [2018, 1.629, 11.2], [2019, 1.685, 3.78], [2021, 1.275, 4.95], [2022, 1.381, -9.52], [2023, 1.165, -8.21], [2024, 0.911, 3.57], [2025, 1.145, 7.85], [2026, 1.137, 5.14]], TEAMCOL = '#BD3039'; 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", "OF", "SP", "SP", "SP", "IF", "RP", "IF", "RP", "SP", "RP", "RP", "IF", "RP", "C", "SP", "OF", "IF", "RP", "OF", "RP", "RP", "C", "SP", "RP", "OF", "IF", "RP", "IF", "OF", "IF", "RP", "OF", "RP", "IF", "RP", "RP", "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": ["Willson Contreras", "Ceddanne Rafaela", "Ranger Suarez", "Wilyer Abreu", "Payton Tolle", "Sonny Gray", "Jake Bennett", "Caleb Durbin", "Garrett Whitlock", "Isiah Kiner-Falefa", "Aroldis Chapman", "Connelly Early", "Tyron Guerrero", "Patrick Sandoval", "Anthony Seigler", "Justin Slaten", "Connor Wong", "Garrett Crochet", "Roman Anthony", "Nick Sogard", "Jovani Mor\u00e1n", "Masataka Yoshida", "Tyler Samaniego", "Zack Kelly", "Carlos Narv\u00e1ez", "Brayan Bello", "Eduardo Rivera", "Nate Eaton", "Tsung-Che Cheng", "Jack Anderson", "Andruw Monasterio", "Jahmai Jones", "Brett Harris", "Alec Gamboa", "Jarren Duran", "Ryan Watson", "Romy Gonzalez", "Tyler Uberstine", "Joe La Sorsa", "Greg Weissert"], "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.32, 3.27, 2.98, 2.31, 2.28, 1.81, 1.57, 1.5, 1.18, 1.12, 1.01, 0.68, 0.58, 0.49, 0.48, 0.39, 0.38, 0.33, 0.3, 0.25, 0.23, 0.21, 0.18, 0.15, 0.1, 0.09, 0.07, 0.05, 0.04, 0.03, -0.0, -0.0, -0.0, -0.0, -0.0, -0.01, -0.08, -0.08, -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:'BOS',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);});