RRosternomics

Royals KCR

GM: J.J. Picollo

Overview

REBUILD ↘ cooling
cheap & losing · year 1 in this regime · payroll 0.9× league · TAB -10.9
-10.9TAB — talent above budget26th/30
-9.2WAB — realized wins (TAB + luck)27th/30
1.0depth — 2+ WAR regulars30th/30
-6.6dead WAR — sub-replacement drag29th/30
47%dead money %23rd/30
0.410win %28th/30
22.0roster fWAR27th/30
-7surplus $M (value vs cost)19th/30
$143Mpayroll $M17th/30luxury tax $139M

Roster

CFKyle Isbel 56GLane Thomas 25GTyler Tolbert 2G
LFIsaac Collins 57GLane Thomas 13GStarling Marte 4G
RFJac Caglianone 54GLane Thomas 19GStarling Marte 11G
SSBobby Witt Jr. 69GTyler Tolbert 3GNick Loftin 1G
2BMichael Massey 48GNick Loftin 21GJonathan India 17G
3BMaikel Garcia 59GNick Loftin 15GJosh Rojas 2G
1BVinnie Pasquantino 65GSalvador Perez 5GJac Caglianone 3G
STARTERSMichael Wacha 1.9WARNoah Cameron 1.4WARSeth Lugo 1.3WARKris Bubic 0.8WARStephen Kolek 0.5WAR
CCarter Jensen 36GSalvador Perez 30G
DHSalvador Perez 29GCarter Jensen 26GStarling Marte 9G
BULLPEN Lucas Erceg 12svMatt Strahm 9hDaniel Lynch IV 8hNick Mears 5hJohn Schreiber 4hAlex Lange 1hSteven Cruz 0hEli Morgan 0h
CCarter Jensen 36 · Salvador Perez 30
1BVinnie Pasquantino 65 · Salvador Perez 5 · Jac Caglianone 3
2BMichael Massey 48 · Nick Loftin 21 · Jonathan India 17
3BMaikel Garcia 59 · Nick Loftin 15 · Josh Rojas 2
SSBobby Witt Jr. 69 · Tyler Tolbert 3 · Nick Loftin 1
LFIsaac Collins 57 · Lane Thomas 13 · Starling Marte 4
CFKyle Isbel 56 · Lane Thomas 25 · Tyler Tolbert 2
RFJac Caglianone 54 · Lane Thomas 19 · Starling Marte 11
DHSalvador Perez 29 · Carter Jensen 26 · Starling Marte 9
SPMichael Wacha 1.9 · Noah Cameron 1.4 · Seth Lugo 1.3 · Kris Bubic 0.8 · Stephen Kolek 0.5
BP Lucas Erceg 12 · Matt Strahm 9 · Daniel Lynch IV 8 · Nick Mears 5 · John Schreiber 4 · Alex Lange 1 · Steven Cruz 0 · Eli Morgan 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
Bobby Witt Jr. 26 947.3 14.1 +31.7
Michael Wacha 34 FA-elig 202.9 18.0 +0.1
Carter Jensen 22 pre-arb 972.7 0.8 +16.4
Michael Massey 28 arb 852.4 1.6 +13.8
Maikel Garcia 26 pre-arb 692.2 4.3 +9.7
Noah Cameron 26 pre-arb 202.2 0.8 +13.0
Jac Caglianone 23 pre-arb 962.1 0.8 +12.5
Seth Lugo 36 FA-elig 212.0 21.5 -9.1
Lane Thomas 30 FA-elig 901.3 5.2 +2.9
Kris Bubic 28 arb 91.2 6.2 +1.6
Daniel Lynch IV 29 arb 421.2 1.0 +6.3
Isaac Collins 28 pre-arb 940.9 0.8 +4.6
Nick Loftin 27 pre-arb 670.8 0.7 +4.5
Stephen Kolek 29 pre-arb 100.7 0.8 +3.9
Kyle Isbel 29 arb 560.7 2.7 +1.6
Luinder Avila 24 pre-arb 190.7 0.7 +3.4
John Rave 28 90.3
Elias Díaz↔ TEX 35 FA-elig 100.2 0.1 +1.4
Jose Cuas 32 50.2
Tyler Tolbert 28 pre-arb 410.2 0.6 +0.5
Luke Maile 35 3-0.0
Connor Seabold↔ DET 30 5-0.0 0.2 -0.4
Nick Mears 29 arb 21-0.1 1.9 -2.2
Jonathan India 29 arb 17-0.1 8.0 -8.4
Randy Dobnak 31 4-0.1
Andrew Velazquez 31 2-0.1
Josh Rojas 32 20-0.1 0.0 -0.7
Starling Marte 37 FA-elig 47-0.1 1.4 -2.2
Alex Lange 30 arb 40-0.2 0.9 -1.9
Beck Way 26 19-0.2 0.5 -1.6
Vinnie Pasquantino 28 arb 77-0.2 4.2 -5.6
Mason Black 26 pre-arb 12-0.3 0.6 -2.2
Mitch Spence 28 pre-arb 3-0.4 0.0 -2.5
Carlos Estévez 33 FA-elig 1-0.4 10.1 -12.6
Cole Ragans 28 arb 8-0.4 4.6 -7.2
Eric Cerantola 26 4-0.4 0.0 -2.7
Eli Morgan 30 arb 16-0.5 0.8 -3.7
Bailey Falter 29 arb 5-0.5 3.6 -6.9
Kameron Misner 28 18-0.6 0.5 -4.3
Steven Cruz 27 pre-arb 34-0.6 0.6 -4.6
John Schreiber 32 arb 43-0.6 3.7 -7.7
Lucas Erceg 31 pre-arb 40-0.9 0.8 -6.3
Matt Strahm 34 FA-elig 38-1.6 7.5 -17.4
Salvador Perez 36 FA-elig 97-1.9 12.5 -24.4

Key additions (new vs 2025)

PlayerfWAR
Lane Thomas0.8
Isaac Collins0.6
Elias Díaz0.2
Jose Cuas0.1
Connor Seabold-0.0
Nick Mears-0.0

Key subtractions (gone from 2025)

Player2025 fWAR
Mike Yastrzemski1.3
Michael Lorenzen1.2
Ryan Bergert0.7
Adam Frazier0.6
Hunter Harvey0.5
Freddy Fermin0.4

Current & Historical Payroll

2026 payroll
$143.0M
luxury tax $139M
2027 guaranteed
$82.7M

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$101M$85M$48M$12M$12M$12M
Proj. arbitration$29M
Proj. pre-arbitration$10M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$157M$103M$66M$30M$30M$30M
Tax space+$87M+$144M+$184M+$224M+$226M+$230M
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 (12)

PlayerAgeSvc2026202720282029203020312032
Seth Lugo369.082$21.5M$21.5M$17.0MCLUBFREE AGENT
Michael Wacha3412.062$18.0M$14.0M$14.0MCLUBFREE AGENT
Salvador Perez3614.050$12.5M$12.5MFREE AGENT
Carlos Estévez339.022$10.1M$13.0MCLUBFREE AGENT
Matt Strahm349.064$7.5MVESTFREE AGENT
Lane Thomas306.014$5.2MFREE AGENT
Cole Ragans283.029$4.6M$7.6MARB 3FREE AGENT
Maikel Garcia262.168$4.3M$7.3M$10.3M$13.3M$19.3M$21.0MCLUBFREE AGENT
Vinnie Pasquantino283.101$4.2M$6.9MARB 3FREE AGENT
Starling Marte3712.162$1.4MFREE AGENT
Elias Díaz359.087$110kFREE AGENT
Luke Maile358.148FREE AGENT

Arbitration-eligible (15)

PlayerAgeSvc2026202720282029203020312032
Bobby Witt Jr.264.000$14.1MARB 3FREE AGENT
Jonathan India295.000$8.0MFREE AGENT
Kris Bubic285.135$6.2MFREE AGENT
John Schreiber325.027$3.7MFREE AGENT
Bailey Falter293.138$3.6MARB 2ARB 3FREE AGENT
Kyle Isbel294.043$2.7MARB 3FREE AGENT
Nick Mears294.022$1.9MARB 3FREE AGENT
Michael Massey283.068$1.6MARB 2ARB 3FREE AGENT
Daniel Lynch IV293.136$1.0MARB 2ARB 3FREE AGENT
Alex Lange303.145$900kARB 2ARB 3FREE AGENT
Eli Morgan304.091$821kARB 3FREE AGENT
Josh Rojas325.126$23kFREE AGENT
Jose Cuas323.129ARB 2ARB 3FREE AGENT
Andrew Velazquez315.126FREE AGENT
Randy Dobnak315.038FREE AGENT

Pre-arbitration (15)

PlayerAgeSvc2026202720282029203020312032
Lucas Erceg312.136$804kARB 1ARB 2ARB 3FREE AGENT
Noah Cameron260.137$802kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Isaac Collins281.022$800kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Stephen Kolek291.099$785kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jac Caglianone230.118$784kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Carter Jensen220.028$781kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Nick Loftin271.081$728kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Luinder Avila240.033$659kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tyler Tolbert280.144$645kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Steven Cruz271.057$626kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mason Black260.051$580kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kameron Misner281.029$455kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Connor Seabold301.054$190kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mitch Spence281.124$42kPre-ArbARB 1ARB 2ARB 3FREE AGENT
John Rave28

Graphs

Every roster slot ranked best→worst by WAR. Bars = Royals 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.037, 3.24], [1986, 1.145, -4.72], [1987, 1.109, -0.08], [1988, 1.17, 0.35], [1989, 1.214, 2.51], [1990, 1.349, -10.2], [1991, 1.191, -2.15], [1992, 1.09, -12.61], [1993, 1.306, -4.17], [1994, 1.269, -3.14], [1995, 0.914, -4.91], [1996, 0.613, -0.49], [1997, 0.887, -0.24], [1998, 0.893, -14.16], [1999, 0.454, 1.84], [2000, 0.456, -1.07], [2001, 0.514, -7.33], [2002, 0.709, -15.88], [2003, 0.672, -7.23], [2004, 0.613, -18.46], [2005, 0.449, -15.84], [2006, 0.644, -10.15], [2007, 0.798, -5.15], [2008, 0.689, -5.77], [2009, 0.869, -15.08], [2010, 0.708, -11.44], [2011, 0.375, 7.78], [2012, 0.606, -0.62], [2013, 0.775, 5.28], [2014, 0.802, 4.18], [2015, 1.003, 1.35], [2016, 1.042, -9.24], [2017, 1.099, -9.14], [2018, 0.814, -16.88], [2019, 0.675, -14.02], [2021, 0.728, -5.39], [2022, 0.609, -10.79], [2023, 0.602, -7.41], [2024, 0.683, 11.34], [2025, 0.828, 0.42], [2026, 0.896, -10.91]], TEAMCOL = '#1857B8'; 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", "C", "IF", "IF", "SP", "OF", "SP", "OF", "SP", "RP", "OF", "IF", "SP", "OF", "RP", "OF", "C", "RP", "OF", "C", "RP", "RP", "IF", "RP", "IF", "IF", "OF", "RP", "RP", "IF", "RP", "RP", "RP", "SP", "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": ["Bobby Witt Jr.", "Michael Wacha", "Carter Jensen", "Michael Massey", "Maikel Garcia", "Noah Cameron", "Jac Caglianone", "Seth Lugo", "Lane Thomas", "Kris Bubic", "Daniel Lynch IV", "Isaac Collins", "Nick Loftin", "Stephen Kolek", "Kyle Isbel", "Luinder Avila", "John Rave", "Elias D\u00edaz", "Jose Cuas", "Tyler Tolbert", "Luke Maile", "Connor Seabold", "Nick Mears", "Jonathan India", "Randy Dobnak", "Andrew Velazquez", "Josh Rojas", "Starling Marte", "Alex Lange", "Beck Way", "Vinnie Pasquantino", "Mason Black", "Mitch Spence", "Carlos Est\u00e9vez", "Cole Ragans", "Eric Cerantola", "Eli Morgan", "Bailey Falter", "Kameron Misner", "Steven Cruz"], "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.71, 1.86, 1.77, 1.58, 1.44, 1.42, 1.37, 1.27, 0.84, 0.8, 0.75, 0.55, 0.53, 0.48, 0.44, 0.42, 0.22, 0.16, 0.12, 0.12, -0.02, -0.02, -0.03, -0.04, -0.04, -0.05, -0.07, -0.09, -0.1, -0.11, -0.15, -0.17, -0.25, -0.26, -0.27, -0.27, -0.3, -0.34, -0.4, -0.41]}; 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:'KCR',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);});