RRosternomics

Cardinals STL

GM: John Mozeliak

Overview

CONTEND ↗ rising
expensive & winning · year 2 in this regime · payroll 1.08× league · TAB +6.9
+6.9TAB — talent above budget7th/30
+3.1WAB — realized wins (TAB + luck)11th/30
9.0depth — 2+ WAR regulars4th/30
-1.8dead WAR — sub-replacement drag1st/30
14%dead money %3rd/30
0.531win %12th/30
42.1roster fWAR7th/30
-0surplus $M (value vs cost)8th/30
$136Mpayroll $M13th/30

Roster

CFRandal Grichuk 115GTommy Pham 34GJeremy Hazelbaker 21G
LFMatt Holliday 85GBrandon Moss 58GJeremy Hazelbaker 52G
RFStephen Piscotty 146GBrandon Moss 21GJeremy Hazelbaker 6G
SSAledmys Díaz 106GGreg Garcia 30GJedd Gyorko 26G
2BKolten Wong 88GJedd Gyorko 46GMatt Carpenter 40G
3BJhonny Peralta 67GMatt Carpenter 54GJedd Gyorko 39G
1BMatt Adams 86GBrandon Moss 64GMatt Carpenter 45G
STARTERSAdam Wainwright 3.7WARCarlos Martinez 3.6WARMike Leake 2.5WARMichael Wacha 1.5WARAlex Reyes 1.2WAR
CYadier Molina 146GAlberto Rosario 17GCarson Kelly 10G
DHMatt Holliday 8GMatt Carpenter 1GJhonny Peralta 1G
BULLPEN Seunghwan Oh 19svZach Duke 26hKevin Siegrist 17hMatt Bowman 13hJonathan Broxton 12hTyler Lyons 4hSeth Maness 1hTrevor Rosenthal 0h
CYadier Molina 146 · Alberto Rosario 17 · Carson Kelly 10
1BMatt Adams 86 · Brandon Moss 64 · Matt Carpenter 45
2BKolten Wong 88 · Jedd Gyorko 46 · Matt Carpenter 40
3BJhonny Peralta 67 · Matt Carpenter 54 · Jedd Gyorko 39
SSAledmys Díaz 106 · Greg Garcia 30 · Jedd Gyorko 26
LFMatt Holliday 85 · Brandon Moss 58 · Jeremy Hazelbaker 52
CFRandal Grichuk 115 · Tommy Pham 34 · Jeremy Hazelbaker 21
RFStephen Piscotty 146 · Brandon Moss 21 · Jeremy Hazelbaker 6
DHMatt Holliday 8 · Matt Carpenter 1 · Jhonny Peralta 1
SPAdam Wainwright 3.7 · Carlos Martinez 3.6 · Mike Leake 2.5 · Michael Wacha 1.5 · Alex Reyes 1.2
BP Seunghwan Oh 19 · Zach Duke 26 · Kevin Siegrist 17 · Matt Bowman 13 · Jonathan Broxton 12 · Tyler Lyons 4 · Seth Maness 1 · Trevor Rosenthal 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.

2016 roster — value & surplus

PlayerAgeServiceGfWARSalary $MSurplus $M
Yadier Molina 33 FA-elig 1474.5 14.0 +0.7
Matt Carpenter 30 arb 1294.0 6.5 +6.5
Adam Wainwright 34 FA-elig 343.7 19.5 -7.6
Carlos Martinez 24 arb 333.6 0.5 +11.0
Aledmys Díaz 25 pre-arb 1113.1 2.5 +7.6
Stephen Piscotty 25 pre-arb 1532.7 0.5 +8.4
Jedd Gyorko 27 arb 1282.6 4.0 +4.6
Seunghwan Oh 33 pre-arb 762.5 2.5 +5.8
Mike Leake 28 FA-elig 342.5 12.0 -3.7
Randal Grichuk 24 pre-arb 1321.7 0.5 +4.9
Michael Wacha 24 arb 271.5 0.5 +4.4
Alex Reyes 21 121.2 0.2 +3.8
Brandon Moss 32 FA-elig 1281.2 8.2 -4.3
Jaime García 29 FA-elig 321.2 11.5 -7.8
Greg Garcia 26 pre-arb 991.1 0.5 +3.1
Kolten Wong 25 arb 1211.0 1.2 +2.1
Matt Adams 27 arb 1180.8 1.6 +0.9
Matt Bowman 25 pre-arb 590.7 0.5 +1.6
Luke Weaver 22 90.6 0.1 +1.9
Jeremy Hazelbaker 28 pre-arb 1140.6 0.5 +1.3
Matt Holliday 36 FA-elig 1100.5 17.0 -15.3
Jhonny Peralta 34 FA-elig 820.5 12.5 -10.8
Zach Duke↔ CHW 33 FA-elig 280.4 1.7 -0.3
Eric Fryer↔ PIT 30 arb 240.3 0.2 +0.8
Tommy Pham 28 pre-arb 780.3 0.5 +0.5
José A. Martínez 27 120.2
Trevor Rosenthal 26 arb 450.2 5.6 -5.0
Miguel Socolovich 29 pre-arb 150.1 0.1 +0.2
Seth Maness 27 arb 290.1 1.4 -1.1
Jonathan Broxton 32 FA-elig 660.1 3.8 -3.5
Kevin Siegrist 26 arb 670.1 0.5 -0.4
Sam Tuivailala 23 pre-arb 12-0.0 0.1 -0.2
Dean Kiekhefer 27 26-0.0 0.2 -0.3
Michael McKenry 31 3-0.0 0.0 -0.2
Carson Kelly 21 10-0.1 0.1 -0.3
Tyler Lyons 28 arb 30-0.1 0.5 -0.8
Alberto Rosario 29 20-0.2 0.2 -0.8
Brayan Peña 34 FA-elig 9-0.2 2.5 -3.1
Jerome Williams 34 FA-elig 11-0.3 0.2 -1.1
Mike Mayers 24 4-0.4 0.1 -1.2
Rubén Tejada↔ SFG 26 FA-elig 23-0.5 1.0 -2.7

Key additions (new vs 2015)

PlayerfWAR
Aledmys Díaz3.1
Jedd Gyorko2.6
Seunghwan Oh2.5
Mike Leake2.5
Alex Reyes1.2
Matt Bowman0.7

Key subtractions (gone from 2015)

Player2015 fWAR
Jason Heyward5.6
John Lackey3.5
Tim Cooney0.6
Randy Choate0.1
Cody Stanley0.1
Matt Belisle0.1

Current & Historical Payroll

2016 payroll
$135.9M
2017 guaranteed
$0M

What the front office was committed to as of 2016, 2016–2022. Guaranteed $ are contract commitments signed by 2016; 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

Guaranteed / signed (10)

PlayerAgeSvc2016201720182019202020212022
Adam Wainwright3410.027$19.5MFREE AGENT
Matt Holliday3612.000$17.0MFREE AGENT
Yadier Molina3311.123$14.0MFREE AGENT
Jhonny Peralta3411.118$12.5MFREE AGENT
Mike Leake286.000$12.0MFREE AGENT
Jaime García297.047$11.5MFREE AGENT
Jonathan Broxton3210.020$3.8MFREE AGENT
Brayan Peña348.081$2.5MFREE AGENT
Zach Duke339.054$1.7MFREE AGENT
Jerome Williams347.043$194kFREE AGENT

Arbitration-eligible (7)

PlayerAgeSvc2016201720182019202020212022
Brandon Moss325.160$8.2MFREE AGENT
Matt Carpenter304.012$6.5MARB 3FREE AGENT
Trevor Rosenthal263.058$5.6MARB 2ARB 3FREE AGENT
Seunghwan Oh333.000$2.5MARB 2ARB 3FREE AGENT
Matt Adams273.033$1.6MARB 2ARB 3FREE AGENT
Rubén Tejada264.171$958kARB 3FREE AGENT
Michael McKenry314.097$31kARB 3FREE AGENT

Pre-arbitration (24)

PlayerAgeSvc2016201720182019202020212022
Jedd Gyorko272.164$4.0MARB 1ARB 2ARB 3FREE AGENT
Aledmys Díaz250.000$2.5MPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Seth Maness272.154$1.4MARB 1ARB 2ARB 3FREE AGENT
Kolten Wong252.045$1.2MARB 1ARB 2ARB 3FREE AGENT
Michael Wacha242.062$539kARB 1ARB 2ARB 3FREE AGENT
Kevin Siegrist262.116$539kARB 1ARB 2ARB 3FREE AGENT
Carlos Martinez242.073$539kARB 1ARB 2ARB 3FREE AGENT
Randal Grichuk241.060$520kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tyler Lyons281.070$512kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Stephen Piscotty250.076$512kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Tommy Pham280.149$510kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jeremy Hazelbaker281.052$507kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Matt Bowman250.000$507kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Greg Garcia260.110$507kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Alberto Rosario290.000$247kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Eric Fryer302.108$230kARB 1ARB 2ARB 3FREE AGENT
Dean Kiekhefer27$227k
Alex Reyes210.000$153kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Luke Weaver220.000$144kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Sam Tuivailala230.075$144kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Miguel Socolovich290.156$125kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Carson Kelly210.000$80kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Mike Mayers240.000$78kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
José A. Martínez270.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Cardinals 2016; 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.069, 16.47], [1986, 0.875, -4.71], [1987, 1.046, 1.67], [1988, 1.247, -7.23], [1989, 1.131, 10.44], [1990, 1.154, 1.97], [1991, 0.867, 1.78], [1992, 0.878, 3.34], [1993, 0.717, 4.95], [1994, 0.905, -2.82], [1995, 0.956, -8.39], [1996, 1.202, 0.25], [1997, 1.131, 5.0], [1998, 1.286, 3.59], [1999, 1.017, -3.03], [2000, 1.215, 9.66], [2001, 1.174, 7.13], [2002, 1.208, 8.04], [2003, 1.243, 4.92], [2004, 1.361, 12.09], [2005, 1.271, 5.55], [2006, 1.249, -5.69], [2007, 1.152, -8.12], [2008, 1.055, 7.7], [2009, 1.17, 5.07], [2010, 1.151, 0.15], [2011, 1.224, 4.54], [2012, 1.192, 8.25], [2013, 0.905, 9.76], [2014, 1.059, -0.95], [2015, 1.068, 9.12], [2016, 1.083, 6.89], [2017, 1.113, 5.24], [2018, 1.183, -3.7], [2019, 1.166, 0.06], [2021, 1.331, 0.51], [2022, 1.154, 9.59], [2023, 1.131, -2.95], [2024, 1.109, -3.12], [2025, 0.777, -0.79], [2026, 0.344, 7.3]], TEAMCOL = '#C41E3A'; 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 = 2016; 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", "IF", "OF", "IF", "RP", "SP", "OF", "SP", "RP", "OF", "SP", "IF", "IF", "IF", "RP", "SP", "OF", "OF", "IF", "RP", "C", "OF", "OF", "RP", "RP", "RP", "RP", "RP", "RP", "RP", "C", "C", "RP", "C", "C", "RP", "RP"], "league": [5.58, 4.15, 3.63, 3.11, 2.71, 2.41, 2.16, 1.91, 1.69, 1.47, 1.27, 1.13, 1.02, 0.92, 0.82, 0.72, 0.63, 0.56, 0.48, 0.41, 0.37, 0.3, 0.27, 0.22, 0.18, 0.15, 0.12, 0.09, 0.06, 0.04, 0.01, -0.01, -0.03, -0.05, -0.07, -0.1, -0.12, -0.15, -0.18, -0.23], "names": ["Yadier Molina", "Matt Carpenter", "Adam Wainwright", "Carlos Martinez", "Aledmys D\u00edaz", "Stephen Piscotty", "Jedd Gyorko", "Seunghwan Oh", "Mike Leake", "Randal Grichuk", "Michael Wacha", "Alex Reyes", "Brandon Moss", "Jaime Garc\u00eda", "Greg Garcia", "Kolten Wong", "Matt Adams", "Matt Bowman", "Luke Weaver", "Jeremy Hazelbaker", "Matt Holliday", "Jhonny Peralta", "Zach Duke", "Eric Fryer", "Tommy Pham", "Jos\u00e9 A. Mart\u00ednez", "Trevor Rosenthal", "Miguel Socolovich", "Seth Maness", "Jonathan Broxton", "Kevin Siegrist", "Sam Tuivailala", "Dean Kiekhefer", "Michael McKenry", "Carson Kelly", "Tyler Lyons", "Alberto Rosario", "Brayan Pe\u00f1a", "Jerome Williams", "Mike Mayers"], "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.54, 4.0, 3.66, 3.57, 3.1, 2.75, 2.64, 2.54, 2.54, 1.67, 1.54, 1.21, 1.2, 1.15, 1.11, 1.03, 0.8, 0.66, 0.64, 0.57, 0.53, 0.52, 0.45, 0.32, 0.32, 0.25, 0.19, 0.09, 0.09, 0.08, 0.05, -0.01, -0.01, -0.05, -0.07, -0.1, -0.17, -0.18, -0.26, -0.35]}; 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:'STL',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);});