RRosternomics

Twins MIN

President of baseball ops: Derek Falvey · GM: Jeremy Zoll

Overview

WINDOW ↘ cooling
cheap & winning · year 3 in this regime · payroll 0.47× league · TAB +3.8
+3.8TAB — talent above budget12th/30
+4.0WAB — realized wins (TAB + luck)10th/30
3.0depth — 2+ WAR regulars19th/30
-4.5dead WAR — sub-replacement drag20th/30
35%dead money %15th/30
0.486win %19th/30
29.1roster fWAR20th/30
+112surplus $M (value vs cost)9th/30
$75Mpayroll $M25th/30luxury tax $73M

Roster

CFByron Buxton 42GRyan Kreidler 13GKody Clemens 6G
LFTrevor Larnach 48GAustin Martin 24GJames Outman 18G
RFAustin Martin 35GMatt Wallner 33GKody Clemens 6G
SSBrooks Lee 45GTristan Gray 18GOrlando Arcia 9G
2BLuke Keaschall 63GOrlando Arcia 4GKody Clemens 3G
3BRoyce Lewis 31GBrooks Lee 20GTristan Gray 18G
1BKody Clemens 46GJosh Bell 19GVictor Caratini 13G
STARTERSJoe Ryan 2.5WARTaj Bradley 1.6WARConnor Prielipp 1.1WARBailey Ober 1.0WARZebby Matthews 0.2WAR
CVictor Caratini 34GRyan Jeffers 32GAlex Jackson 8G
DHJosh Bell 46GByron Buxton 16GRyan Jeffers 4G
BULLPEN Yoendrys Gómez 5svAnthony Banda 9hAndrew Morris 8hTaylor Rogers 6hEric Orze 6hKody Funderburk 6hJustin Topa 4hJustin Lawrence 2h
CVictor Caratini 34 · Ryan Jeffers 32 · Alex Jackson 8
1BKody Clemens 46 · Josh Bell 19 · Victor Caratini 13
2BLuke Keaschall 63 · Orlando Arcia 4 · Kody Clemens 3
3BRoyce Lewis 31 · Brooks Lee 20 · Tristan Gray 18
SSBrooks Lee 45 · Tristan Gray 18 · Orlando Arcia 9
LFTrevor Larnach 48 · Austin Martin 24 · James Outman 18
CFByron Buxton 42 · Ryan Kreidler 13 · Kody Clemens 6
RFAustin Martin 35 · Matt Wallner 33 · Kody Clemens 6
DHJosh Bell 46 · Byron Buxton 16 · Ryan Jeffers 4
SPJoe Ryan 2.5 · Taj Bradley 1.6 · Connor Prielipp 1.1 · Bailey Ober 1.0 · Zebby Matthews 0.2
BP Yoendrys Gómez 5 · Anthony Banda 9 · Andrew Morris 8 · Taylor Rogers 6 · Eric Orze 6 · Kody Funderburk 6 · Justin Topa 4 · Justin Lawrence 2

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
Byron Buxton 32 FA-elig 794.4 15.1 +12.4
Joe Ryan 30 arb 213.8 6.1 +18.0
Trevor Larnach 29 arb 863.2 4.5 +15.8
Ryan Jeffers 29 arb 453.0 6.7 +12.5
Taj Bradley 25 pre-arb 202.5 0.8 +14.7
Kody Clemens 30 pre-arb 902.4 0.8 +14.1
Brooks Lee 25 pre-arb 1001.9 0.8 +11.4
Luke Keaschall 23 pre-arb 981.8 0.8 +10.8
Connor Prielipp 25 131.7 0.7 +10.2
Bailey Ober 30 arb 151.5 5.2 +4.5
Andrew Morris 24 381.5 0.7 +8.5
Austin Martin 27 pre-arb 921.3 0.8 +7.3
Victor Caratini 32 FA-elig 761.2 4.0 +3.7
Yoendrys Gómez↔ TBR 26 pre-arb 330.9 0.5 +5.2
Mick Abel 24 pre-arb 40.9 0.8 +4.8
Eric Orze 28 pre-arb 360.9 0.8 +4.6
Ryan Kreidler 28 pre-arb 600.5 0.6 +2.7
Royce Lewis 27 arb 700.5 2.9 +0.1
Taylor Rogers 35 FA-elig 430.4 2.0 +0.3
Zebby Matthews 26 pre-arb 130.3 0.6 +1.3
Kendry Rojas 23 90.2 0.6 +0.9
Garrett Acton 28 pre-arb 40.2 0.7 +0.7
Josh Bell 33 FA-elig 1020.2 5.8 -4.6
Jack Anderson↔ BOS 26 30.2 0.0 +1.1
Tommy Nance↔ TOR 35 40.2 0.1 +0.9
Austin Voth↔ TOR 34 10.1 0.2 +0.6
Gabriel Gonzalez 22 10.1 0.0 +0.8
Alex Jackson 30 arb 240.1 1.4 -0.7
Orlando Arcia 31 FA-elig 190.1 0.9 -0.6
Anthony Banda 32 arb 390.0 1.6 -1.5
Travis Adams 26 pre-arb 210.0 0.6 -0.4
Cole Sands 28 arb 12-0.1 1.1 -1.5
Marco Raya 23 3-0.1 0.8 -1.3
Alan Roden 26 pre-arb 11-0.1 0.8 -1.7
Cody Laweryson 28 pre-arb 15-0.2 0.9 -2.1
John Klein 24 3-0.2 0.0 -1.2
Woo-Suk Go 27 4-0.2
Zak Kent↔ WSN 28 2-0.3 0.0 -1.8
James Outman↔ DET 29 pre-arb 49-0.3 0.5 -2.4
Kody Funderburk 29 pre-arb 30-0.3 0.2 -2.1
Tristan Gray 30 pre-arb 63-0.3 0.8 -2.9
Luis García↔ NYM 39 9-0.4 1.1 -3.7
Mike Paredes 25 9-0.4 0.5 -3.2
Kyler Fedko 26 14-0.6
Simeon Woods Richardson↔ TOR 25 12-0.6 0.4 -4.3
Justin Topa 35 23-0.7 1.0 -5.6
Justin Lawrence↔ PIT 31 7-0.8 0.2 -5.4
Matt Wallner 28 pre-arb 34-1.3 0.2 -8.5

Key additions (new vs 2025)

PlayerfWAR
Connor Prielipp1.1
Andrew Morris0.9
Victor Caratini0.8
Yoendrys Gómez0.6
Eric Orze0.6
Ryan Kreidler0.3

Key subtractions (gone from 2025)

Player2025 fWAR
Harrison Bader2.0
Griffin Jax1.5
Jhoan Duran1.2
Chris Paddack1.2
Willi Castro1.1
Carlos Correa1.1

Current & Historical Payroll

2026 payroll
$75.4M
luxury tax $73M
2027 guaranteed
$49.6M

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$32M$28M$14M
Proj. arbitration$30M$6M
Proj. pre-arbitration$12M$7M
Player benefits$18M$18M$18M
Total CBT payroll$91M$52M$39M
Tax space+$153M+$195M+$211M
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 (8)

PlayerAgeSvc2026202720282029203020312032
Byron Buxton329.160$15.1M$15.1M$15.1MFREE AGENT
Joe Ryan304.033$6.1M$13.0MMUTUALFREE AGENT
Josh Bell339.053$5.8M$10.0MMUTUALFREE AGENT
Victor Caratini328.051$4.0M$6.5M$12.0MMUTUALFREE AGENT
Taylor Rogers359.145$2.0MFREE AGENT
Luis García3913.000$1.1MFREE AGENT
Justin Topa355.044$1.0M$5.0MFREE AGENT
Orlando Arcia318.114$876kFREE AGENT

Arbitration-eligible (10)

PlayerAgeSvc2026202720282029203020312032
Ryan Jeffers295.089$6.7MFREE AGENT
Bailey Ober304.093$5.2MARB 3FREE AGENT
Trevor Larnach294.014$4.5MARB 3FREE AGENT
Royce Lewis273.142$2.9MARB 2ARB 3FREE AGENT
Anthony Banda324.135$1.6MARB 3FREE AGENT
Alex Jackson303.036$1.4MARB 2ARB 3FREE AGENT
Cole Sands283.017$1.1MARB 2ARB 3FREE AGENT
Simeon Woods Richardson254.000$395kARB 3FREE AGENT
Justin Lawrence313.167$232kARB 2ARB 3FREE AGENT
Austin Voth345.115$171kFREE AGENT

Pre-arbitration (24)

PlayerAgeSvc2026202720282029203020312032
Cody Laweryson280.017$875kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Alan Roden260.134$820kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kody Clemens302.134$804kARB 1ARB 2ARB 3FREE AGENT
Taj Bradley252.097$802kARB 1ARB 2ARB 3FREE AGENT
Tristan Gray300.123$800kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brooks Lee251.089$793kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Austin Martin271.041$791kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Luke Keaschall230.058$788kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Eric Orze280.116$786kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mick Abel240.050$782kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Garrett Acton280.031$730kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Andrew Morris24$705k
Connor Prielipp25$659k
Ryan Kreidler281.006$645kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kendry Rojas23$596k
Zebby Matthews261.010$596kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Travis Adams260.066$570kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
James Outman292.007$488kARB 1ARB 2ARB 3FREE AGENT
Yoendrys Gómez260.135$474kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Matt Wallner282.043$218kARB 1ARB 2ARB 3FREE AGENT
Kody Funderburk291.122$200kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tommy Nance352.165$90kARB 1ARB 2ARB 3FREE AGENT
Jack Anderson26$19k
Zak Kent280.037$11kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Twins 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.551, 5.75], [1986, 0.799, -6.1], [1987, 0.913, -6.27], [1988, 1.124, 6.51], [1989, 1.175, -5.39], [1990, 0.831, -5.22], [1991, 1.022, 8.14], [1992, 0.928, 10.3], [1993, 0.881, -11.89], [1994, 0.89, -7.43], [1995, 0.611, -8.21], [1996, 0.512, 4.3], [1997, 0.67, -5.0], [1998, 0.651, -19.32], [1999, 0.391, -9.27], [2000, 0.311, -3.57], [2001, 0.427, 9.47], [2002, 0.608, 13.88], [2003, 0.849, 8.46], [2004, 0.715, 9.39], [2005, 0.798, 0.68], [2006, 0.828, 11.35], [2007, 0.831, -4.26], [2008, 0.634, 3.2], [2009, 0.764, 3.76], [2010, 0.981, 10.85], [2011, 1.186, -24.32], [2012, 0.949, -14.89], [2013, 0.717, -8.12], [2014, 0.786, -0.9], [2015, 0.927, -5.23], [2016, 0.82, -6.19], [2017, 0.844, 4.94], [2018, 0.868, -4.72], [2019, 0.833, 23.55], [2021, 0.94, -4.6], [2022, 0.926, -1.3], [2023, 1.01, 9.28], [2024, 0.848, 8.54], [2025, 0.631, 3.36], [2026, 0.473, 3.81]], TEAMCOL = '#0C2C56'; 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": ["OF", "SP", "OF", "C", "SP", "IF", "IF", "IF", "SP", "SP", "RP", "OF", "C", "RP", "SP", "RP", "OF", "IF", "RP", "SP", "RP", "RP", "IF", "RP", "RP", "RP", "OF", "C", "IF", "RP", "RP", "RP", "RP", "OF", "RP", "RP", "RP", "RP", "OF", "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": ["Byron Buxton", "Joe Ryan", "Trevor Larnach", "Ryan Jeffers", "Taj Bradley", "Kody Clemens", "Brooks Lee", "Luke Keaschall", "Connor Prielipp", "Bailey Ober", "Andrew Morris", "Austin Martin", "Victor Caratini", "Yoendrys G\u00f3mez", "Mick Abel", "Eric Orze", "Ryan Kreidler", "Royce Lewis", "Taylor Rogers", "Zebby Matthews", "Kendry Rojas", "Garrett Acton", "Josh Bell", "Jack Anderson", "Tommy Nance", "Austin Voth", "Gabriel Gonzalez", "Alex Jackson", "Orlando Arcia", "Anthony Banda", "Travis Adams", "Cole Sands", "Marco Raya", "Alan Roden", "Cody Laweryson", "John Klein", "Woo-Suk Go", "Zak Kent", "James Outman", "Kody Funderburk"], "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.84, 2.48, 2.09, 1.97, 1.6, 1.53, 1.26, 1.19, 1.12, 1.0, 0.94, 0.83, 0.79, 0.59, 0.57, 0.56, 0.34, 0.3, 0.23, 0.19, 0.15, 0.15, 0.12, 0.11, 0.1, 0.08, 0.08, 0.07, 0.03, 0.01, 0.01, -0.04, -0.05, -0.09, -0.12, -0.12, -0.14, -0.18, -0.19, -0.19]}; 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:'MIN',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);});