RRosternomics

Rockies COL

GM: Josh Byrnes

Overview

REBUILD ↗ rising
cheap & losing · year 3 in this regime · payroll 0.56× league · TAB -9.1
-9.1TAB — talent above budget25th/30
-6.5WAB — realized wins (TAB + luck)25th/30
2.0depth — 2+ WAR regulars25th/30
-6.5dead WAR — sub-replacement drag28th/30
48%dead money %24th/30
0.400win %29th/30
18.3roster fWAR29th/30
+26surplus $M (value vs cost)10th/30
$90Mpayroll $M23rd/30luxury tax $115M

Roster

CFBrenton Doyle 41GJake McCarthy 34GMickey Moniak 11G
LFJake McCarthy 22GTroy Johnston 19GMickey Moniak 19G
RFTyler Freeman 32GTroy Johnston 31GJordan Beck 11G
SSEzequiel Tovar 66GWilli Castro 10GChad Stevens 2G
2BEdouard Julien 44GWilli Castro 41GChad Stevens 12G
3BKyle Karros 64GWilli Castro 17GChad Stevens 4G
1BTJ Rumfield 52GTroy Johnston 16GEdouard Julien 11G
STARTERSKyle Freeland 0.8WARMichael Lorenzen 0.8WARTomoyuki Sugano 0.3WARJose Quintana 0.3WARRyan Feltner 0.1WAR
CHunter Goodman 47GBrett Sullivan 25GBraxton Fulford 6G
DHHunter Goodman 17GTJ Rumfield 11GMickey Moniak 10G
BULLPEN Antonio Senzatela 3svJaden Hill 11hJuan Mejia 4hBrennan Bernardino 3hJimmy Herget 3hSeth Halvorsen 2hZach Agnos 0hVictor Vodnik 0h
CHunter Goodman 47 · Brett Sullivan 25 · Braxton Fulford 6
1BTJ Rumfield 52 · Troy Johnston 16 · Edouard Julien 11
2BEdouard Julien 44 · Willi Castro 41 · Chad Stevens 12
3BKyle Karros 64 · Willi Castro 17 · Chad Stevens 4
SSEzequiel Tovar 66 · Willi Castro 10 · Chad Stevens 2
LFJake McCarthy 22 · Troy Johnston 19 · Mickey Moniak 19
CFBrenton Doyle 41 · Jake McCarthy 34 · Mickey Moniak 11
RFTyler Freeman 32 · Troy Johnston 31 · Jordan Beck 11
DHHunter Goodman 17 · TJ Rumfield 11 · Mickey Moniak 10
SPKyle Freeland 0.8 · Michael Lorenzen 0.8 · Tomoyuki Sugano 0.3 · Jose Quintana 0.3 · Ryan Feltner 0.1
BP Antonio Senzatela 3 · Jaden Hill 11 · Juan Mejia 4 · Brennan Bernardino 3 · Jimmy Herget 3 · Seth Halvorsen 2 · Zach Agnos 0 · Victor Vodnik 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
Hunter Goodman 26 pre-arb 963.9 0.8 +23.7
TJ Rumfield 26 1023.2 0.8 +19.4
Kyle Karros 23 pre-arb 993.0 0.8 +18.1
Jake McCarthy 28 arb 902.9 1.5 +17.0
Mickey Moniak 28 arb 681.8 4.0 +7.4
Cole Carrigg 24 371.7 0.5 +10.2
Antonio Senzatela 31 FA-elig 321.6 12.0 -1.7
Brennan Bernardino 34 pre-arb 491.3 0.9 +7.4
Michael Lorenzen 34 FA-elig 221.3 7.8 +0.2
Kyle Freeland 33 FA-elig 191.2 16.0 -8.3
Gabriel Hughes 24 41.2 0.8 +6.7
Juan Mejia 25 pre-arb 410.9 0.8 +5.1
Chase Dollander 24 pre-arb 100.7 0.8 +3.6
Willi Castro 29 FA-elig 890.6 5.5 -1.9
Tomoyuki Sugano 36 pre-arb 180.5 5.1 -2.2
Jordan Romano↔ ANA 33 70.5 0.8 +2.1
Troy Johnston 29 pre-arb 930.4 0.8 +2.0
Jose Quintana 37 FA-elig 90.4 6.0 -3.5
Jaden Hill 26 pre-arb 320.3 0.8 +1.4
Keegan Thompson 31 arb 50.3 1.0 +1.0
Ryan Feltner 29 arb 140.2 2.5 -1.2
Jimmy Herget 32 arb 280.1 1.6 -1.0
Tanner Gordon 28 pre-arb 130.1 0.7 -0.3
Welinton Herrera 22 30.0 0.5 -0.2
Eiberson Castellano 25 10.0
Victor Vodnik 26 pre-arb 300.0 0.8 -0.6
Zach Agnos 25 pre-arb 27-0.0 0.8 -1.0
Ryan Ritter 25 pre-arb 6-0.0 0.0 -0.3
Sammy Peralta 28 pre-arb 2-0.1 0.0 -0.6
Sean Sullivan 23 5-0.1
John Brebbia 36 3-0.1
Blas Castaño 27 pre-arb 10-0.1 0.5 -1.5
TJ Shook 28 4-0.2 0.0 -1.3
Seth Halvorsen 26 pre-arb 21-0.3 0.6 -2.5
Jeff Criswell 27 pre-arb 3-0.3 0.8 -2.7
Sterlin Thompson 25 27-0.3 0.6 -2.7
Brett Sullivan 32 pre-arb 52-0.4 0.8 -3.4
Chad Stevens 27 pre-arb 16-0.4 0.5 -3.3
Braxton Fulford 27 pre-arb 38-0.4 0.6 -3.4
Valente Bellozo 26 pre-arb 3-0.8 0.0 -5.0
Brenton Doyle 28 pre-arb 43-0.9 3.1 -8.7
Jordan Beck 25 pre-arb 30-1.1 0.8 -7.4
Edouard Julien 27 pre-arb 82-1.1 0.8 -7.8
Tyler Freeman 27 arb 70-1.4 1.5 -10.2
Ezequiel Tovar 24 arb 97-1.8 5.2 -16.3

Key additions (new vs 2025)

PlayerfWAR
TJ Rumfield2.1
Jake McCarthy1.9
Brennan Bernardino0.9
Michael Lorenzen0.8
Willi Castro0.4
Tomoyuki Sugano0.3

Key subtractions (gone from 2025)

Player2025 fWAR
Ryan McMahon1.3
Jake Bird0.7
Germán Márquez0.3
Tyler Kinley0.3
Blaine Crim0.1
Owen Miller0.0

Current & Historical Payroll

2026 payroll
$90.1M
luxury tax $115M
2027 guaranteed
$56.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$85M$80M$35M$9M$9M$9M
Proj. arbitration$11M
Proj. pre-arbitration$18M
Player benefits$18M$18M$18M$18M$18M$18M
Total CBT payroll$133M$98M$53M$27M$27M$27M
Tax space+$111M+$149M+$197M+$226M+$229M+$232M
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
Kyle Freeland338.144$16.0M$17.0MPLAYERFREE AGENT
Antonio Senzatela318.106$12.0M$14.0MCLUBFREE AGENT
Michael Lorenzen3410.159$7.8M$9.0MCLUBFREE AGENT
Jose Quintana3713.133$6.0M$1.0MFREE AGENT
Willi Castro296.017$5.5M$7.3MFREE AGENT
Ezequiel Tovar243.014$5.2M$8.2M$11.2M$14.2M$16.2M$23.0MCLUBFREE AGENT
Jordan Romano336.051$778kFREE AGENT
John Brebbia368.078FREE AGENT

Arbitration-eligible (6)

PlayerAgeSvc2026202720282029203020312032
Mickey Moniak284.027$4.0MARB 3FREE AGENT
Ryan Feltner293.071$2.5MARB 2ARB 3FREE AGENT
Jimmy Herget324.069$1.6MARB 3FREE AGENT
Tyler Freeman273.046$1.5MARB 2ARB 3FREE AGENT
Jake McCarthy283.124$1.5MARB 2ARB 3FREE AGENT
Keegan Thompson313.006$1.0MARB 2ARB 3FREE AGENT

Pre-arbitration (24)

PlayerAgeSvc2026202720282029203020312032
Tomoyuki Sugano361.000$5.1MPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brenton Doyle282.161$3.1MARB 1ARB 2ARB 3FREE AGENT
Brennan Bernardino342.150$925kARB 1ARB 2ARB 3FREE AGENT
Hunter Goodman262.010$810kARB 1ARB 2ARB 3FREE AGENT
Brett Sullivan320.122$800kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Edouard Julien272.009$793kARB 1ARB 2ARB 3FREE AGENT
Victor Vodnik262.024$790kARB 1ARB 2ARB 3FREE AGENT
Jordan Beck251.153$785kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jeff Criswell271.040$785kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Troy Johnston290.062$782kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kyle Karros230.052$782kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Juan Mejia250.144$782kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jaden Hill260.097$782kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Chase Dollander240.141$782kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
TJ Rumfield26$780k
Zach Agnos250.145$778kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tanner Gordon280.156$684kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Seth Halvorsen261.031$605kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Braxton Fulford270.097$584kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Blas Castaño270.007$546kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Chad Stevens270.005$538kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Valente Bellozo261.050$46kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Ryan Ritter250.115$38kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Sammy Peralta280.970$38kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Rockies 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 = [[1993, 0.399, -8.04], [1994, 0.744, -13.36], [1995, 1.078, -5.41], [1996, 1.066, -8.19], [1997, 1.123, -5.39], [1998, 1.191, -6.85], [1999, 1.16, -26.39], [2000, 1.07, -3.2], [2001, 1.086, 1.72], [2002, 0.84, -16.82], [2003, 0.943, -8.28], [2004, 0.766, -9.69], [2005, 0.558, -6.7], [2006, 0.581, 3.08], [2007, 0.697, 9.42], [2008, 0.812, -3.97], [2009, 0.875, 6.9], [2010, 0.939, 2.38], [2011, 0.973, -7.85], [2012, 0.774, -10.21], [2013, 0.747, -0.43], [2014, 0.878, -8.06], [2015, 0.823, -13.94], [2016, 0.729, 4.03], [2017, 0.82, 1.52], [2018, 1.051, -3.37], [2019, 1.181, -17.95], [2021, 0.688, -6.7], [2022, 0.887, -11.48], [2023, 1.025, -27.06], [2024, 0.852, -23.85], [2025, 0.734, -33.15], [2026, 0.565, -9.12]], TEAMCOL = '#5F259F'; 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", "IF", "OF", "OF", "OF", "RP", "RP", "SP", "SP", "RP", "RP", "RP", "IF", "SP", "RP", "OF", "SP", "RP", "RP", "SP", "RP", "RP", "RP", "RP", "RP", "RP", "IF", "SP", "RP", "RP", "RP", "RP", "RP", "RP", "OF", "C", "IF", "C", "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": ["Hunter Goodman", "TJ Rumfield", "Kyle Karros", "Jake McCarthy", "Mickey Moniak", "Cole Carrigg", "Antonio Senzatela", "Brennan Bernardino", "Michael Lorenzen", "Kyle Freeland", "Gabriel Hughes", "Juan Mejia", "Chase Dollander", "Willi Castro", "Tomoyuki Sugano", "Jordan Romano", "Troy Johnston", "Jose Quintana", "Jaden Hill", "Keegan Thompson", "Ryan Feltner", "Jimmy Herget", "Tanner Gordon", "Welinton Herrera", "Eiberson Castellano", "Victor Vodnik", "Zach Agnos", "Ryan Ritter", "Sammy Peralta", "Sean Sullivan", "John Brebbia", "Blas Casta\u00f1o", "TJ Shook", "Seth Halvorsen", "Jeff Criswell", "Sterlin Thompson", "Brett Sullivan", "Chad Stevens", "Braxton Fulford", "Valente Bellozo"], "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.52, 2.08, 1.95, 1.91, 1.17, 1.1, 1.06, 0.85, 0.82, 0.8, 0.77, 0.6, 0.45, 0.37, 0.3, 0.3, 0.28, 0.26, 0.22, 0.2, 0.13, 0.06, 0.04, 0.03, 0.03, 0.02, -0.02, -0.03, -0.06, -0.08, -0.08, -0.1, -0.14, -0.19, -0.19, -0.22, -0.27, -0.28, -0.28, -0.51]}; 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:'COL',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);});