RRosternomics

Rangers TEX

President of baseball ops: Chris Young · GM: Ross Fenstermaker

Overview

CONTEND ↗ rising
expensive & winning · year 1 in this regime · payroll 1.13× league · TAB +3.0
+3.0TAB — talent above budget13th/30
-1.1WAB — realized wins (TAB + luck)18th/30
4.0depth — 2+ WAR regulars7th/30
-3.1dead WAR — sub-replacement drag10th/30
16%dead money %1st/30
0.505win %13th/30
38.7roster fWAR7th/30
+56surplus $M (value vs cost)15th/30
$181Mpayroll $M12th/30luxury tax $176M

Roster

CFEvan Carter 66GMichael Helman 7GAlejandro Osuna 7G
LFAlejandro Osuna 35GSam Haggerty 29GWyatt Langford 25G
RFBrandon Nimmo 61GEzequiel Duran 7GAlejandro Osuna 7G
SSCorey Seager 47GEzequiel Duran 20GMichael Helman 4G
2BJosh Smith 29GEzequiel Duran 22GJustin Foscue 18G
3BJosh Jung 64GEzequiel Duran 10G
1BJake Burger 66GJustin Foscue 4GJoc Pederson 2G
STARTERSJacob deGrom 2.5WARMacKenzie Gore 2.1WARNathan Eovaldi 1.3WARKumar Rocker 1.2WARJack Leiter 0.6WAR
CDanny Jansen 37GKyle Higashioka 32GElias Díaz 13G
DHJoc Pederson 60GAndrew McCutchen 23GBrandon Nimmo 6G
BULLPEN Jacob Latz 10svTyler Alexander 6hJalen Beeks 6hCole Winn 6hJakob Junis 6hPeyton Gray 2hChris Martin 1hGavin Collyer 0h
CDanny Jansen 37 · Kyle Higashioka 32 · Elias Díaz 13
1BJake Burger 66 · Justin Foscue 4 · Joc Pederson 2
2BJosh Smith 29 · Ezequiel Duran 22 · Justin Foscue 18
3BJosh Jung 64 · Ezequiel Duran 10
SSCorey Seager 47 · Ezequiel Duran 20 · Michael Helman 4
LFAlejandro Osuna 35 · Sam Haggerty 29 · Wyatt Langford 25
CFEvan Carter 66 · Michael Helman 7 · Alejandro Osuna 7
RFBrandon Nimmo 61 · Ezequiel Duran 7 · Alejandro Osuna 7
DHJoc Pederson 60 · Andrew McCutchen 23 · Brandon Nimmo 6
SPJacob deGrom 2.5 · MacKenzie Gore 2.1 · Nathan Eovaldi 1.3 · Kumar Rocker 1.2 · Jack Leiter 0.6
BP Jacob Latz 10 · Tyler Alexander 6 · Jalen Beeks 6 · Cole Winn 6 · Jakob Junis 6 · Peyton Gray 2 · Chris Martin 1 · Gavin Collyer 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
Ezequiel Duran 27 arb 944.3 1.4 +25.4
Jacob deGrom 38 FA-elig 193.9 38.0 -13.1
Josh Jung 28 arb 923.9 2.9 +21.8
MacKenzie Gore 27 arb 223.4 5.6 +15.6
Wyatt Langford 24 pre-arb 513.1 0.8 +18.6
Jacob Latz 30 pre-arb 382.9 0.8 +17.3
Brandon Nimmo 33 FA-elig 972.7 15.2 +1.6
Nathan Eovaldi 36 FA-elig 192.0 29.0 -16.6
Kumar Rocker 26 pre-arb 192.0 0.8 +11.6
Jake Burger 30 arb 991.9 3.2 +8.8
Evan Carter 23 pre-arb 841.8 0.8 +10.4
Joc Pederson 34 FA-elig 921.8 18.5 -7.3
Justin Foscue 27 pre-arb 531.6 0.6 +9.7
Tyler Alexander 31 FA-elig 461.5 1.1 +8.0
Jakob Junis 33 341.4
Peyton Gray 31 311.2 0.7 +7.2
Corey Seager 32 FA-elig 511.0 31.5 -25.4
Nicky Lopez↔ CHC 31 FA-elig 460.9 0.5 +5.4
Jack Leiter 26 pre-arb 150.9 0.8 +4.8
Elias Díaz↔ KCR 35 330.5 0.4 +2.6
Cole Winn 26 pre-arb 380.4 0.8 +1.6
Kyle Higashioka 36 FA-elig 580.4 6.8 -4.5
Cam Cauley 23 160.2
Jarred Kelenic↔ CHW 26 70.1 0.0 +0.4
Robert Garcia 30 pre-arb 90.1 0.8 -0.5
Robby Ahlstrom 27 210.0
Marco Gonzales 34 10.0
Emiliano Teodo 25 1-0.0 0.8 -0.8
Cal Quantrill 31 FA-elig 21-0.0 0.9 -1.0
Joe Ross↔ ARI 33 5-0.0 0.9 -1.2
Austin Wynns↔ ATL 35 1-0.1 0.0 -0.4
Gavin Collyer 25 23-0.1 0.7 -1.1
Ben Peoples 25 6-0.1
Chris Paddack↔ CIN 30 1-0.1 0.0 -0.7
Winston Santos 24 1-0.1 0.8 -1.5
Carter Baumler 24 4-0.1 0.8 -1.6
Cody Freeman 25 pre-arb 7-0.2 0.8 -1.8
Michael Helman 30 pre-arb 15-0.3 0.6 -2.3
Josh Smith 28 40-0.3
Luis Curvelo 25 pre-arb 8-0.3 0.2 -2.1
Jose Corniell 23 pre-arb 2-0.3 0.8 -2.8
Chris Martin 40 FA-elig 16-0.4 4.0 -6.3
Sam Haggerty 32 arb 36-0.4 1.2 -3.8
Danny Jansen 31 FA-elig 41-0.4 4.2 -6.9
Jalen Beeks 32 FA-elig 29-0.4 1.9 -4.5
Alejandro Osuna 23 pre-arb 66-0.6 0.7 -4.2
Andrew McCutchen 39 FA-elig 37-0.7 0.4 -4.9

Key additions (new vs 2025)

PlayerfWAR
MacKenzie Gore2.1
Brandon Nimmo1.7
Tyler Alexander0.9
Jakob Junis0.9
Peyton Gray0.8
Nicky Lopez0.6

Key subtractions (gone from 2025)

Player2025 fWAR
Marcus Semien2.1
Tyler Mahle1.9
Patrick Corbin1.9
Shawn Armstrong1.4
Hoby Milner1.0
Merrill Kelly0.8

Current & Historical Payroll

2026 payroll
$180.7M
luxury tax $176M
2027 guaranteed
$146.5M

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$152M$147M$60M$53M$53M$32M
Proj. arbitration$14M
Proj. pre-arbitration$10M$37M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$194M$165M$115M$71M$71M$50M
Tax space+$50M+$82M+$135M+$182M+$185M+$208M
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 (18)

PlayerAgeSvc2026202720282029203020312032
Jacob deGrom3811.139$38.0M$37.0M$20.0MCLUBFREE AGENT
Corey Seager3210.032$31.5M$31.5M$31.5M$31.5M$31.5M$31.5MFREE AGENT
Nathan Eovaldi3614.013$29.0M$24.0MFREE AGENT
Joc Pederson3411.028$18.5MPLAYER$18.5MMUTUALFREE AGENT
Brandon Nimmo339.042$15.2M$20.5M$20.5M$20.5M$20.5MFREE AGENT
Kyle Higashioka367.005$6.8M$7.0MMUTUALFREE AGENT
Danny Jansen317.050$4.2M$8.0M$8.0MMUTUALFREE AGENT
Chris Martin408.133$4.0MFREE AGENT
Jalen Beeks327.003$1.9MFREE AGENT
Tyler Alexander316.011$1.1MFREE AGENT
Joe Ross338.018$938kFREE AGENT
Cal Quantrill316.122$882kFREE AGENT
Nicky Lopez316.021$472kFREE AGENT
Andrew McCutchen3916.123$428kFREE AGENT
Elias Díaz359.087$362kFREE AGENT
Chris Paddack307.000$41kFREE AGENT
Marco Gonzales348.102FREE AGENT
Jakob Junis339.000FREE AGENT

Arbitration-eligible (8)

PlayerAgeSvc2026202720282029203020312032
MacKenzie Gore274.000$5.6MARB 3FREE AGENT
Jake Burger303.127$3.2MARB 2ARB 3FREE AGENT
Josh Jung283.023$2.9MARB 2ARB 3FREE AGENT
Ezequiel Duran273.050$1.4MARB 2ARB 3FREE AGENT
Sam Haggerty325.007$1.2MFREE AGENT
Jarred Kelenic263.035$39kARB 2ARB 3FREE AGENT
Austin Wynns355.017$25kFREE AGENT
Josh Smith284.000ARB 3FREE AGENT

Pre-arbitration (16)

PlayerAgeSvc2026202720282029203020312032
Jose Corniell230.003$820kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Wyatt Langford242.000$805kARB 1ARB 2ARB 3FREE AGENT
Robert Garcia302.086$800kARB 1ARB 2ARB 3FREE AGENT
Jack Leiter261.033$794kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jacob Latz301.170$794kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Evan Carter231.170$792kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kumar Rocker260.137$789kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cole Winn261.090$788kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cody Freeman250.051$785kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Carter Baumler24$780k
Alejandro Osuna230.107$693kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Gavin Collyer25$667k
Peyton Gray31$655k
Justin Foscue270.146$636kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Michael Helman300.095$551kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Luis Curvelo250.061$197kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Rangers 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.761, -10.5], [1986, 0.541, 6.06], [1987, 0.555, -3.68], [1988, 0.539, 0.55], [1989, 0.738, 6.82], [1990, 0.843, 3.23], [1991, 0.813, 3.36], [1992, 0.954, -0.6], [1993, 1.132, -0.48], [1994, 1.015, -10.92], [1995, 1.052, -4.66], [1996, 1.218, 9.85], [1997, 1.279, -4.79], [1998, 1.373, 7.48], [1999, 1.597, 4.83], [2000, 1.206, -15.32], [2001, 1.327, -9.37], [2002, 1.56, -11.06], [2003, 1.218, -7.47], [2004, 0.657, 4.08], [2005, 0.79, 3.13], [2006, 0.841, -0.45], [2007, 0.742, -6.38], [2008, 0.776, 1.96], [2009, 0.735, 3.33], [2010, 0.74, 10.61], [2011, 1.001, 18.64], [2012, 1.343, 7.71], [2013, 1.204, 4.11], [2014, 1.199, -15.5], [2015, 1.381, -5.41], [2016, 1.366, -8.94], [2017, 1.157, -14.69], [2018, 0.931, -12.88], [2019, 0.794, -8.18], [2021, 0.445, -6.87], [2022, 0.895, -6.34], [2023, 1.375, 10.82], [2024, 1.631, -10.98], [2025, 1.429, -4.26], [2026, 1.133, 3.02]], TEAMCOL = '#003278'; 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", "SP", "IF", "SP", "OF", "RP", "OF", "SP", "SP", "IF", "OF", "OF", "IF", "RP", "RP", "RP", "IF", "IF", "SP", "C", "RP", "C", "IF", "OF", "RP", "RP", "RP", "RP", "RP", "RP", "C", "RP", "RP", "SP", "RP", "RP", "IF", "OF", "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": ["Ezequiel Duran", "Jacob deGrom", "Josh Jung", "MacKenzie Gore", "Wyatt Langford", "Jacob Latz", "Brandon Nimmo", "Nathan Eovaldi", "Kumar Rocker", "Jake Burger", "Evan Carter", "Joc Pederson", "Justin Foscue", "Tyler Alexander", "Jakob Junis", "Peyton Gray", "Corey Seager", "Nicky Lopez", "Jack Leiter", "Elias D\u00edaz", "Cole Winn", "Kyle Higashioka", "Cam Cauley", "Jarred Kelenic", "Robert Garcia", "Robby Ahlstrom", "Marco Gonzales", "Emiliano Teodo", "Cal Quantrill", "Joe Ross", "Austin Wynns", "Gavin Collyer", "Ben Peoples", "Chris Paddack", "Winston Santos", "Carter Baumler", "Cody Freeman", "Michael Helman", "Josh Smith", "Luis Curvelo"], "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.71, 2.51, 2.5, 2.14, 1.95, 1.82, 1.7, 1.26, 1.25, 1.22, 1.13, 1.13, 1.04, 0.92, 0.88, 0.79, 0.62, 0.59, 0.57, 0.3, 0.24, 0.22, 0.11, 0.04, 0.03, 0.02, 0.01, -0.0, -0.01, -0.03, -0.03, -0.05, -0.06, -0.06, -0.07, -0.08, -0.1, -0.18, -0.18, -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:'TEX',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);});