RRosternomics

Tigers DET

President of baseball ops: Scott Harris · GM: Jeff Greenberg

Overview

FORCED-SPEND ↘ cooling
expensive & losing · year 1 in this regime · payroll 1.31× league · TAB -2.0
-2.0TAB — talent above budget17th/30
-4.7WAB — realized wins (TAB + luck)23rd/30
6.0depth — 2+ WAR regulars2nd/30
-4.5dead WAR — sub-replacement drag19th/30
44%dead money %19th/30
0.481win %21st/30
35.4roster fWAR14th/30
+14surplus $M (value vs cost)22nd/30
$208Mpayroll $M9th/30luxury tax $198M

Roster

CFMatt Vierling 52GJames Outman 20GWenceel Pérez 15G
LFRiley Greene 64GMatt Vierling 5GKerry Carpenter 4G
RFWenceel Pérez 43GKerry Carpenter 29GZach McKinstry 14G
SSKevin McGonigle 47GZack Short 21GJavier Báez 17G
2BGleyber Torres 34GZach McKinstry 25GHao-Yu Lee 21G
3BKevin McGonigle 37GColt Keith 31GHao-Yu Lee 11G
1BSpencer Torkelson 67GColt Keith 6G
STARTERSCasey Mize 2.7WARTarik Skubal 2.7WARKeider Montero 2.2WARJack Flaherty 1.3WARFramber Valdez 1.2WAR
CDillon Dingler 52GJake Rogers 20G
DHJahmai Jones 32GColt Keith 26GKerry Carpenter 14G
BULLPEN Kenley Jansen 7svKyle Finnegan 9hWill Vest 6hTyler Holton 5hDrew Anderson 4hBrenan Hanifee 2hBrant Hurter 1hEnmanuel De Jesus 0h
CDillon Dingler 52 · Jake Rogers 20
1BSpencer Torkelson 67 · Colt Keith 6
2BGleyber Torres 34 · Zach McKinstry 25 · Hao-Yu Lee 21
3BKevin McGonigle 37 · Colt Keith 31 · Hao-Yu Lee 11
SSKevin McGonigle 47 · Zack Short 21 · Javier Báez 17
LFRiley Greene 64 · Matt Vierling 5 · Kerry Carpenter 4
CFMatt Vierling 52 · James Outman 20 · Wenceel Pérez 15
RFWenceel Pérez 43 · Kerry Carpenter 29 · Zach McKinstry 14
DHJahmai Jones 32 · Colt Keith 26 · Kerry Carpenter 14
SPCasey Mize 2.7 · Tarik Skubal 2.7 · Keider Montero 2.2 · Jack Flaherty 1.3 · Framber Valdez 1.2
BP Kenley Jansen 7 · Kyle Finnegan 9 · Will Vest 6 · Tyler Holton 5 · Drew Anderson 4 · Brenan Hanifee 2 · Brant Hurter 1 · Enmanuel De Jesus 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
Dillon Dingler 27 pre-arb 957.5 0.8 +46.7
Kevin McGonigle 21 1006.4 0.8 +39.3
Casey Mize 29 arb 154.3 6.2 +20.7
Tarik Skubal 29 arb 154.1 32.0 -5.9
Riley Greene 25 arb 1034.1 5.0 +21.0
Keider Montero 25 pre-arb 223.4 0.8 +21.0
Jack Flaherty 30 FA-elig 192.0 20.0 -7.4
Framber Valdez 32 FA-elig 201.9 22.8 -11.1
Gleyber Torres 29 FA-elig 471.7 22.0 -11.0
Troy Melton 25 pre-arb 101.3 0.8 +7.3
Colt Keith 24 pre-arb 941.1 4.3 +2.6
Enmanuel De Jesus 29 220.6 1.3 +2.7
Drew Anderson 32 pre-arb 370.6 7.0 -3.0
Ty Madden 26 pre-arb 80.6 0.6 +3.1
Tyler Holton 30 arb 430.5 1.6 +1.8
Will Vest 31 arb 270.5 4.0 -0.7
Eduardo Valencia 26 60.4 0.8 +2.0
Parker Meadows 26 pre-arb 120.4 0.8 +1.4
Brant Hurter 27 pre-arb 210.3 0.8 +1.0
Burch Smith 36 arb 130.1 1.3 -0.5
Drew Sommers 25 pre-arb 200.1 0.0 +0.7
Zach McKinstry 31 arb 790.1 4.2 -3.6
Connor Seabold↔ KCR 30 110.1 0.4 +0.1
Ben Malgeri 26 160.1
Zack Short↔ NYM 31 pre-arb 230.0 0.5 -0.5
Javier Báez 33 FA-elig 24-0.0 24.0 -24.1
Jace Jung 25 pre-arb 3-0.0 0.0 -0.3
Jacob Waguespack 32 12-0.0
Jake Rogers 31 arb 35-0.1 3.0 -3.5
Kenley Jansen 38 FA-elig 31-0.1 9.0 -9.6
Hao-Yu Lee 23 62-0.1 0.7 -1.5
Justin Verlander 43 FA-elig 1-0.1 7.8 -8.6
Beau Brieske 28 arb 8-0.2 1.2 -2.4
Trei Cruz 27 2-0.2 0.8 -2.2
Spencer Torkelson 26 arb 103-0.3 4.1 -5.7
Kyle Finnegan 34 FA-elig 44-0.3 8.8 -10.6
Brenan Hanifee 28 pre-arb 18-0.3 0.6 -2.6
Ricky Vanasco 27 pre-arb 5-0.3 0.6 -2.8
James Outman↔ MIN 29 31-0.4 0.3 -2.6
Matt Vierling 29 arb 89-0.6 3.3 -6.9
Gage Workman 26 16-0.7 0.6 -4.8
Wenceel Pérez 26 pre-arb 53-0.7 0.7 -5.0
Kerry Carpenter 28 arb 79-0.8 3.3 -8.1
Jahmai Jones↔ BOS 28 pre-arb 57-1.8 0.8 -12.1

Key additions (new vs 2025)

PlayerfWAR
Kevin McGonigle4.1
Framber Valdez1.2
Enmanuel De Jesus0.4
Drew Anderson0.4
Ty Madden0.4
Burch Smith0.1

Key subtractions (gone from 2025)

Player2025 fWAR
Andy Ibáñez0.4
Brewer Hicklen0.2
Carlos Hernández0.2
Dietrich Enns0.2
Tomás Nido0.1
Sean Guenther0.1

Current & Historical Payroll

2026 payroll
$208.3M
luxury tax $198M
2027 guaranteed
$103.9M

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$120M$69M$41M$5M$5M$5M
Proj. arbitration$68M
Proj. pre-arbitration$10M$19M$28M$57M$19M$19M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$216M$106M$88M$80M$42M$42M
Tax space+$28M+$141M+$162M+$173M+$214M+$217M
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 (10)

PlayerAgeSvc2026202720282029203020312032
Javier Báez3310.089$24.0M$24.0MFREE AGENT
Framber Valdez326.163$22.8M$42.8M$35.0MPLAYER$40.0MMUTUALFREE AGENT
Gleyber Torres297.162$22.0MFREE AGENT
Jack Flaherty308.006$20.0MPLAYERFREE AGENT
Kenley Jansen3815.073$9.0M$12.0MCLUBFREE AGENT
Kyle Finnegan346.000$8.8M$8.0M$10.0MMUTUALFREE AGENT
Justin Verlander4320.002$7.8MFREE AGENT
Drew Anderson320.114$7.0M$10.0MCLUBPre-ArbARB 1ARB 2ARB 3FREE AGENT
Colt Keith242.000$4.3M$4.3M$5.3M$5.3M$10.0MCLUB$13.0MCLUB$15.0MCLUB
Kevin McGonigle21$780k$2.8M$8.8M$17.8M$22.8M$23.8M

Arbitration-eligible (13)

PlayerAgeSvc2026202720282029203020312032
Tarik Skubal295.114$32.0MFREE AGENT
Casey Mize295.111$6.2MFREE AGENT
Riley Greene253.110$5.0MARB 2ARB 3FREE AGENT
Zach McKinstry314.099$4.2MARB 3FREE AGENT
Spencer Torkelson263.076$4.1MARB 2ARB 3FREE AGENT
Will Vest314.100$4.0MARB 3FREE AGENT
Kerry Carpenter283.057$3.3MARB 2ARB 3FREE AGENT
Matt Vierling294.026$3.3MARB 3FREE AGENT
Jake Rogers315.040$3.0MFREE AGENT
Tyler Holton303.047$1.6MARB 2ARB 3FREE AGENT
Burch Smith365.086$1.3MFREE AGENT
Beau Brieske283.056$1.2MARB 2ARB 3FREE AGENT
Jacob Waguespack323.091ARB 2ARB 3FREE AGENT

Pre-arbitration (18)

PlayerAgeSvc2026202720282029203020312032
Enmanuel De Jesus29$1.3M
Dillon Dingler271.063$835kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Parker Meadows261.169$805kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brant Hurter271.042$796kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Troy Melton250.068$786kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Keider Montero251.011$772kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jahmai Jones281.148$765kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Wenceel Pérez262.000$745kARB 1ARB 2ARB 3FREE AGENT
Hao-Yu Lee23$715k
Ricky Vanasco270.039$632kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brenan Hanifee281.069$617kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ty Madden261.035$609kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Gage Workman26$588k
Zack Short312.070$476kARB 1ARB 2ARB 3FREE AGENT
Connor Seabold301.054$419kPre-ArbARB 1ARB 2ARB 3FREE AGENT
James Outman292.007$309kARB 1ARB 2ARB 3FREE AGENT
Jace Jung250.079$29kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Drew Sommers250.032$4kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Tigers 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.081, 8.7], [1986, 1.087, 7.88], [1987, 1.191, 12.36], [1988, 1.157, 0.88], [1989, 1.142, -20.79], [1990, 1.115, -6.7], [1991, 1.023, 4.65], [1992, 0.902, 0.5], [1993, 1.207, -1.91], [1994, 1.275, -11.73], [1995, 1.029, -22.16], [1996, 0.67, -26.38], [1997, 0.434, -0.32], [1998, 0.528, 2.81], [1999, 0.784, -3.26], [2000, 1.083, 1.82], [2001, 0.834, -14.14], [2002, 0.799, -21.62], [2003, 0.709, -28.46], [2004, 0.694, 2.66], [2005, 0.909, -2.56], [2006, 1.067, 5.33], [2007, 1.092, 6.48], [2008, 1.498, -13.64], [2009, 1.358, -6.17], [2010, 1.289, 4.58], [2011, 1.169, 4.19], [2012, 1.245, 7.96], [2013, 1.473, 15.22], [2014, 1.482, 5.1], [2015, 1.357, -8.96], [2016, 1.567, -0.93], [2017, 1.286, -13.3], [2018, 0.867, -18.49], [2019, 0.78, -20.9], [2021, 0.686, -9.01], [2022, 0.92, -20.15], [2023, 0.806, -5.8], [2024, 0.586, 8.26], [2025, 0.89, 4.26], [2026, 1.305, -2.03]], TEAMCOL = '#0C2340'; 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": ["C", "IF", "SP", "SP", "OF", "SP", "SP", "SP", "IF", "SP", "IF", "RP", "RP", "RP", "RP", "RP", "C", "OF", "RP", "RP", "RP", "IF", "RP", "OF", "IF", "IF", "IF", "RP", "C", "RP", "IF", "SP", "RP", "OF", "IF", "RP", "RP", "RP", "OF", "OF"], "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": ["Dillon Dingler", "Kevin McGonigle", "Casey Mize", "Tarik Skubal", "Riley Greene", "Keider Montero", "Jack Flaherty", "Framber Valdez", "Gleyber Torres", "Troy Melton", "Colt Keith", "Enmanuel De Jesus", "Drew Anderson", "Ty Madden", "Tyler Holton", "Will Vest", "Eduardo Valencia", "Parker Meadows", "Brant Hurter", "Burch Smith", "Drew Sommers", "Zach McKinstry", "Connor Seabold", "Ben Malgeri", "Zack Short", "Javier B\u00e1ez", "Jace Jung", "Jacob Waguespack", "Jake Rogers", "Kenley Jansen", "Hao-Yu Lee", "Justin Verlander", "Beau Brieske", "Trei Cruz", "Spencer Torkelson", "Kyle Finnegan", "Brenan Hanifee", "Ricky Vanasco", "James Outman", "Matt Vierling"], "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": [4.84, 4.09, 2.74, 2.66, 2.65, 2.21, 1.28, 1.2, 1.12, 0.83, 0.71, 0.41, 0.41, 0.37, 0.35, 0.33, 0.29, 0.23, 0.18, 0.08, 0.07, 0.06, 0.06, 0.05, 0.0, -0.01, -0.02, -0.03, -0.05, -0.06, -0.08, -0.08, -0.13, -0.14, -0.17, -0.19, -0.2, -0.22, -0.23, -0.37]}; 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:'DET',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);});