RRosternomics

Reds CIN

President of baseball ops: Dick Williams · GM: Nick Krall

Overview

CONTEND ↗ rising
expensive & winning · year 1 in this regime · payroll 1.1× league · TAB +2.7
+2.7TAB — talent above budget
+0.6WAB — realized wins (TAB + luck)
2.0depth — 2+ WAR regulars5th/30
-3.4dead WAR — sub-replacement drag15th/30
50%dead money %16th/30
0.517win %13th/30
14.1roster fWAR13th/30
-23surplus $M (value vs cost)18th/30
$130Mpayroll $M13th/30

2020: made playoffs · 31–29. Ranks are vs the 30 teams that season.

Roster

CFNick Senzel 23GShogo Akiyama 21GBrian Goodwin 20G
LFShogo Akiyama 36GBrian Goodwin 16GJesse Winker 15G
RFNick Castellanos 57GBrian Goodwin 12GAristides Aquino 4G
SSFreddy Galvis 33GJosé Barrero 21GKyle Farmer 15G
2BMike Moustakas 32GFreddy Galvis 16GKyle Farmer 13G
3BEugenio Suárez 57GKyle Farmer 2GMike Moustakas 2G
1BJoey Votto 50GMike Moustakas 10GJosh VanMeter 3G
STARTERSTrevor Bauer 2.6WARLuis Castillo 2.4WARSonny Gray 1.7WARTyler Mahle 1.1WARAnthony DeSclafani -0.1WAR
CTucker Barnhart 36GCurt Casali 29GTyler Stephenson 4G
DHJesse Winker 37GMatt Davidson 10GJoey Votto 5G
BULLPEN Raisel Iglesias 8svAmir Garrett 6hLucas Sims 5hNate Jones 4hMichael Lorenzen 2hArchie Bradley 2hTejay Antone 1hRobert Stephenson 1h
CTucker Barnhart 36 · Curt Casali 29 · Tyler Stephenson 4
1BJoey Votto 50 · Mike Moustakas 10 · Josh VanMeter 3
2BMike Moustakas 32 · Freddy Galvis 16 · Kyle Farmer 13
3BEugenio Suárez 57 · Kyle Farmer 2 · Mike Moustakas 2
SSFreddy Galvis 33 · José Barrero 21 · Kyle Farmer 15
LFShogo Akiyama 36 · Brian Goodwin 16 · Jesse Winker 15
CFNick Senzel 23 · Shogo Akiyama 21 · Brian Goodwin 20
RFNick Castellanos 57 · Brian Goodwin 12 · Aristides Aquino 4
DHJesse Winker 37 · Matt Davidson 10 · Joey Votto 5
SPTrevor Bauer 2.6 · Luis Castillo 2.4 · Sonny Gray 1.7 · Tyler Mahle 1.1 · Anthony DeSclafani -0.1
BP Raisel Iglesias 8 · Amir Garrett 6 · Lucas Sims 5 · Nate Jones 4 · Michael Lorenzen 2 · Archie Bradley 2 · Tejay Antone 1 · Robert Stephenson 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.

2020 roster — value & surplus

PlayerAgeServiceGfWARSalary $MSurplus $M
Trevor Bauer 29 FA-elig 112.6 17.5 +1.3
Luis Castillo 27 pre-arb 122.4 0.2 +17.7
Sonny Gray 30 FA-elig 111.7 10.2 +2.5
Jesse Winker 26 pre-arb 541.3 0.2 +9.3
Raisel Iglesias 30 arb 221.1 5.7 +2.6
Eugenio Suárez 28 FA-elig 571.1 9.5 -1.3
Tyler Mahle 25 pre-arb 101.1 0.2 +7.7
Tucker Barnhart 29 FA-elig 380.8 3.9 +2.2
Curt Casali 31 FA-elig 310.7 1.5 +3.6
Tejay Antone 26 130.7 0.2 +4.6
Michael Lorenzen 28 arb 230.6 3.7 +0.5
Mike Moustakas 31 FA-elig 440.5 12.0 -8.1
Joey Votto 36 FA-elig 540.5 25.0 -21.5
Lucas Sims 26 pre-arb 200.4 0.2 +3.0
Tyler Stephenson 23 80.3 0.1 +2.1
Nick Castellanos 28 FA-elig 600.3 16.0 -13.8
Shogo Akiyama 32 540.3 2.2 -0.2
Kyle Farmer 29 pre-arb 320.2 0.2 +1.5
Amir Garrett 28 pre-arb 210.2 0.2 +1.1
Tyler Thornburg 31 arb 70.2 0.1 +1.1
Archie Bradley↔ ARI 27 arb 60.1 1.5 -0.6
Wade Miley 33 FA-elig 60.1 6.0 -5.1
Freddy Galvis 30 FA-elig 470.1 2.0 -1.4
Sal Romano 26 pre-arb 20.1 0.0 +0.4
Brooks Raley↔ HOU 32 40.0 0.0 +0.2
Jesse Biddle 28 pre-arb 10.0 0.1 -0.1
Aristides Aquino 26 pre-arb 23-0.0 0.1 -0.4
Anthony DeSclafani 30 FA-elig 9-0.1 5.9 -6.3
Pedro Strop 35 FA-elig 4-0.1 1.8 -2.3
Nick Senzel 25 pre-arb 23-0.1 0.2 -0.9
Joel Kuhnel 25 pre-arb 3-0.1 0.0 -0.7
José De León 27 5-0.1 0.1 -0.8
Mark Payton 28 8-0.1 0.0 -0.8
Cody Reed↔ TBR 27 9-0.1 0.1 -1.0
Nate Jones 34 FA-elig 21-0.1 0.1 -1.0
Travis Jankowski 29 arb 16-0.1 1.1 -2.0
Brian Goodwin↔ ANA 29 arb 20-0.2 0.9 -2.1
Phillip Ervin↔ SEA 27 19-0.3 0.1 -2.0
Christian Colón 31 pre-arb 11-0.3 0.1 -2.1
Matt Davidson 29 pre-arb 22-0.3 0.1 -2.6
Josh VanMeter↔ ARI 25 14-0.4 0.0 -2.8
José Barrero 22 24-0.5
Robert Stephenson 27 pre-arb 10-0.6 0.2 -4.3

Key additions (new vs 2019)

PlayerfWAR
Tejay Antone0.7
Mike Moustakas0.5
Tyler Stephenson0.3
Nick Castellanos0.3
Shogo Akiyama0.3
Tyler Thornburg0.2

Key subtractions (gone from 2019)

Player2019 fWAR
Tanner Roark2.0
Jose Iglesias1.8
Yasiel Puig1.2
Derek Dietrich0.6
Kevin Gausman0.4
David Hernandez0.4

Current & Historical Payroll

2020 payroll
$129.6M
2021 guaranteed
$0M

What the front office was committed to as of 2020, 2020–2026. Guaranteed $ are contract commitments signed by 2020; 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

Guaranteed / signed (8)

PlayerAgeSvc2020202120222023202420252026
Joey Votto3612.027$25.0MFREE AGENT
Nick Castellanos286.029$16.0MFREE AGENT
Mike Moustakas318.111$12.0MFREE AGENT
Sonny Gray306.061$10.2MFREE AGENT
Wade Miley338.025$6.0MFREE AGENT
Freddy Galvis307.021$2.0MFREE AGENT
Pedro Strop3511.000$1.8MFREE AGENT
Nate Jones348.000$100kFREE AGENT

Arbitration-eligible (12)

PlayerAgeSvc2020202120222023202420252026
Trevor Bauer295.158$17.5MFREE AGENT
Eugenio Suárez285.061$9.5MFREE AGENT
Anthony DeSclafani305.062$5.9MFREE AGENT
Raisel Iglesias304.154$5.7MARB 3FREE AGENT
Tucker Barnhart295.075$3.9MFREE AGENT
Michael Lorenzen284.159$3.7MARB 3FREE AGENT
Archie Bradley275.000$1.5MFREE AGENT
Curt Casali314.151$1.5MARB 3FREE AGENT
Travis Jankowski293.148$1.1MARB 2ARB 3FREE AGENT
Brian Goodwin293.019$880kARB 2ARB 3FREE AGENT
Tyler Thornburg315.162$143kFREE AGENT
José De León274.000$87kARB 3FREE AGENT

Pre-arbitration (23)

PlayerAgeSvc2020202120222023202420252026
Shogo Akiyama320.000$2.2MPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Luis Castillo272.101$246kARB 1ARB 2ARB 3FREE AGENT
Tyler Mahle252.018$226kARB 1ARB 2ARB 3FREE AGENT
Jesse Winker262.080$224kARB 1ARB 2ARB 3FREE AGENT
Amir Garrett282.099$220kARB 1ARB 2ARB 3FREE AGENT
Robert Stephenson272.049$219kARB 1ARB 2ARB 3FREE AGENT
Lucas Sims261.014$214kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kyle Farmer291.129$211kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Nick Senzel250.150$211kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tejay Antone260.000$196kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Matt Davidson292.145$149kARB 1ARB 2ARB 3FREE AGENT
Aristides Aquino260.069$125kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jesse Biddle282.166$108kARB 1ARB 2ARB 3FREE AGENT
Cody Reed271.072$83kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Christian Colón312.109$73kARB 1ARB 2ARB 3FREE AGENT
Tyler Stephenson230.000$59kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Phillip Ervin271.118$59kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Sal Romano261.125$48kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Josh VanMeter250.076$44kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mark Payton280.000$31kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brooks Raley320.045$29kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Joel Kuhnel250.046$16kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
José Barrero220.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Reds 2020; 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.902, 2.45], [1986, 1.028, -1.73], [1987, 0.862, 3.35], [1988, 0.805, 7.3], [1989, 0.864, -5.28], [1990, 0.816, 9.71], [1991, 1.049, 5.04], [1992, 1.109, 5.92], [1993, 1.336, -10.65], [1994, 1.206, 13.15], [1995, 1.316, 10.77], [1996, 1.138, -8.19], [1997, 1.005, -10.92], [1998, 0.544, 4.12], [1999, 0.707, 14.14], [2000, 0.77, 7.06], [2001, 0.782, -11.87], [2002, 0.727, -2.5], [2003, 0.827, -15.04], [2004, 0.704, -8.93], [2005, 0.799, -1.32], [2006, 0.807, 2.83], [2007, 0.81, -1.23], [2008, 0.715, -7.55], [2009, 0.852, -5.08], [2010, 0.802, 16.6], [2011, 0.825, 3.63], [2012, 0.771, 10.95], [2013, 1.004, 6.1], [2014, 0.969, -6.36], [2015, 0.882, -5.44], [2016, 0.703, -17.24], [2017, 0.635, -1.64], [2018, 0.746, -4.92], [2019, 0.959, -1.32], [2021, 0.993, 1.18], [2022, 0.714, -15.33], [2023, 0.46, 3.94], [2024, 0.598, -1.93], [2025, 0.717, 2.23], [2026, 0.703, -11.63]], TEAMCOL = '#C6011F'; 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 = 2020; 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", "SP", "SP", "OF", "RP", "IF", "SP", "C", "C", "RP", "RP", "IF", "IF", "RP", "C", "OF", "OF", "IF", "RP", "RP", "RP", "SP", "IF", "RP", "RP", "RP", "OF", "SP", "RP", "OF", "RP", "RP", "OF", "RP", "RP", "OF", "OF", "OF", "IF", "IF"], "league": [2.23, 1.87, 1.55, 1.37, 1.18, 1.03, 0.91, 0.81, 0.68, 0.61, 0.54, 0.47, 0.43, 0.37, 0.33, 0.3, 0.26, 0.22, 0.19, 0.16, 0.13, 0.11, 0.08, 0.06, 0.05, 0.03, 0.01, -0.01, -0.02, -0.04, -0.05, -0.07, -0.09, -0.11, -0.13, -0.14, -0.17, -0.21, -0.24, -0.25], "names": ["Trevor Bauer", "Luis Castillo", "Sonny Gray", "Jesse Winker", "Raisel Iglesias", "Eugenio Su\u00e1rez", "Tyler Mahle", "Tucker Barnhart", "Curt Casali", "Tejay Antone", "Michael Lorenzen", "Mike Moustakas", "Joey Votto", "Lucas Sims", "Tyler Stephenson", "Nick Castellanos", "Shogo Akiyama", "Kyle Farmer", "Amir Garrett", "Tyler Thornburg", "Archie Bradley", "Wade Miley", "Freddy Galvis", "Sal Romano", "Brooks Raley", "Jesse Biddle", "Aristides Aquino", "Anthony DeSclafani", "Pedro Strop", "Nick Senzel", "Joel Kuhnel", "Jos\u00e9 De Le\u00f3n", "Mark Payton", "Cody Reed", "Nate Jones", "Travis Jankowski", "Brian Goodwin", "Phillip Ervin", "Christian Col\u00f3n", "Matt Davidson"], "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.56, 2.44, 1.73, 1.3, 1.14, 1.12, 1.08, 0.83, 0.69, 0.65, 0.58, 0.54, 0.48, 0.44, 0.3, 0.3, 0.27, 0.23, 0.18, 0.17, 0.13, 0.13, 0.09, 0.06, 0.03, 0.0, -0.03, -0.05, -0.07, -0.09, -0.1, -0.1, -0.1, -0.12, -0.13, -0.14, -0.17, -0.26, -0.28, -0.33]}; 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:'CIN',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);});