RRosternomics

Orioles BAL

GM: Dan Duquette

Overview

WINDOW ↗ rising
cheap & winning · year 2 in this regime · payroll 0.96× league · TAB +6.5
+6.5TAB — talent above budget8th/30
+14.6WAB — realized wins (TAB + luck)1st/30
8.0depth — 2+ WAR regulars8th/30
-1.4dead WAR — sub-replacement drag1st/30
22%dead money %6th/30
0.593win %2nd/30
40.2roster fWAR5th/30
+10surplus $M (value vs cost)9th/30
$108Mpayroll $M13th/30

2014: made playoffs · 96–66. Ranks are vs the 30 teams that season.

Roster

CFAdam Jones 155GAlejandro De Aza 16GDavid Lough 16G
LFAlejandro De Aza 132GDavid Lough 85GNelson Cruz 60G
RFNick Markakis 147GNelson Cruz 11GSteve Pearce 8G
SSJ.J. Hardy 141GRyan Flaherty 29G
2BJonathan Schoop 123GRyan Flaherty 30GSteve Lombardozzi Jr. 20G
3BManny Machado 82GKelly Johnson 60GRyan Flaherty 43G
1BChris Davis 115GSteve Pearce 51GChristian Walker 6G
STARTERSChris Tillman 2.3WARWei-Yin Chen 2.3WARKevin Gausman 1.9WARBud Norris 1.3WARUbaldo Jiménez 0.4WAR
CCaleb Joseph 78GNick Hundley 63GSteve Clevenger 25G
DHNelson Cruz 89GDelmon Young 39GSteve Pearce 8G
BULLPEN Zack Britton 37svDarren O'Day 25hAndrew Miller 22hBrian Matusz 14hTommy Hunter 12hRyan Webb 11hBrad Brach 8hT.J. McFarland 5h
CCaleb Joseph 78 · Nick Hundley 63 · Steve Clevenger 25
1BChris Davis 115 · Steve Pearce 51 · Christian Walker 6
2BJonathan Schoop 123 · Ryan Flaherty 30 · Steve Lombardozzi Jr. 20
3BManny Machado 82 · Kelly Johnson 60 · Ryan Flaherty 43
SSJ.J. Hardy 141 · Ryan Flaherty 29
LFAlejandro De Aza 132 · David Lough 85 · Nelson Cruz 60
CFAdam Jones 155 · Alejandro De Aza 16 · David Lough 16
RFNick Markakis 147 · Nelson Cruz 11 · Steve Pearce 8
DHNelson Cruz 89 · Delmon Young 39 · Steve Pearce 8
SPChris Tillman 2.3 · Wei-Yin Chen 2.3 · Kevin Gausman 1.9 · Bud Norris 1.3 · Ubaldo Jiménez 0.4
BP Zack Britton 37 · Darren O'Day 25 · Andrew Miller 22 · Brian Matusz 14 · Tommy Hunter 12 · Ryan Webb 11 · Brad Brach 8 · T.J. McFarland 5

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.

2014 roster — value & surplus

PlayerAgeServiceGfWARSalary $MSurplus $M
Adam Jones 28 FA-elig 1594.6 13.0 +0.6
Steve Pearce 31 FA-elig 1024.5 0.7 +12.4
Nelson Cruz 33 FA-elig 1593.9 8.0 +3.3
J.J. Hardy 31 FA-elig 1413.0 7.0 +1.7
Nick Markakis 30 FA-elig 1552.3 15.0 -8.2
Wei-Yin Chen 28 pre-arb 312.3 4.1 +2.7
Manny Machado 21 pre-arb 822.3 0.5 +6.2
Chris Tillman 26 arb 342.3 0.5 +6.1
Kevin Gausman 23 pre-arb 201.9 0.5 +5.1
Caleb Joseph 28 821.8 0.4 +4.9
Bud Norris 29 arb 291.3 5.3 -1.4
David Lough 28 pre-arb 1121.2 0.5 +2.9
Zack Britton 26 arb 711.0 0.5 +2.4
Andrew Miller↔ BOS 29 FA-elig 231.0 0.6 +2.2
Chris Davis 28 FA-elig 1270.9 10.3 -7.6
Delmon Young 28 FA-elig 830.9 1.0 +1.6
Darren O'Day 31 FA-elig 680.9 3.2 -0.7
Tommy Hunter 27 FA-elig 600.7 3.0 -0.9
Ryan Flaherty 27 pre-arb 1020.7 0.5 +1.5
Ryan Webb 28 arb 510.7 1.8 +0.2
Matt Wieters 28 arb 260.6 7.7 -6.0
Alejandro De Aza↔ CHW 30 FA-elig 200.5 0.6 +1.0
T.J. McFarland 25 pre-arb 370.5 0.5 +0.9
Ubaldo Jiménez 30 FA-elig 250.4 11.2 -10.1
Jonathan Schoop 22 pre-arb 1370.4 0.5 +0.6
Miguel González 30 pre-arb 270.3 0.5 +0.3
Jimmy Paredes↔ KCR 25 pre-arb 180.2 0.3 +0.3
Brian Matusz 27 arb 630.2 2.4 -1.8
Brad Brach 28 arb 460.1 0.5 -0.1
Kelly Johnson↔ BOS 32 FA-elig 190.1 0.5 -0.2
Jemile Weeks↔ BOS 27 arb 30.1 0.1 +0.1
Ramon E Ramirez 32 10.0
Preston Guilmet 26 pre-arb 10-0.0 0.1 -0.1
Nick Hundley↔ SDP 30 FA-elig 50-0.0 2.4 -2.5
Quintin Berry 29 pre-arb 10-0.0 0.1 -0.2
Steve Lombardozzi Jr. 25 arb 20-0.0 0.5 -0.6
Steve Clevenger 28 arb 35-0.1 0.5 -0.7
Christian Walker 23 6-0.1 0.5 -0.7
Cord Phelps 27 3-0.1
Troy Patton↔ SDP 28 FA-elig 9-0.1 0.7 -1.0
Joe Saunders↔ TEX 33 FA-elig 6-0.2 0.6 -1.1
Josh Stinson 26 arb 8-0.2 0.5 -1.0
Alexi Casilla 29 1-0.2 0.2 -0.9
Evan Meek 31 FA-elig 23-0.4 0.8 -1.9

Key additions (new vs 2013)

PlayerfWAR
Nelson Cruz3.9
Caleb Joseph1.8
David Lough1.2
Andrew Miller1.0
Delmon Young0.9
Ryan Webb0.7

Key subtractions (gone from 2013)

Player2013 fWAR
Nate McLouth2.2
Brian Roberts1.1
Scott Feldman1.0
Danny Valencia0.9
Jim Johnson0.9
Jason Hammel0.6

Current & Historical Payroll

2014 payroll
$108.3M

What the front office was committed to as of 2014, 2014–2020. Pre-2015 has no contract-terms data — forward italic $ are realized actuals (hindsight, not commitments known then). Control labels (ARB/FA) are point-in-time.

current $ guaranteed $ actual $ (hindsight) arbitration (proj) pre-arb (proj) free agent

Guaranteed / signed (13)

PlayerAgeSvc2014201520162017201820192020
Nick Markakis308.000$15.0MFREE AGENT
Adam Jones286.139$13.0M$13.3M$16.3M$16.3M$17.3MFREE AGENT
Ubaldo Jiménez306.087$11.2M$12.2M$13.0M$13.5MFREE AGENT
Nelson Cruz336.081$8.0MFREE AGENT
J.J. Hardy3112.164$7.0M$11.5M$12.5M$14.0MFREE AGENT
Delmon Young287.028$1.0M$2.2MFREE AGENT
Steve Pearce317.000$700k$3.7M$3.4MFREE AGENT
Joe Saunders337.013$643kFREE AGENT
Alejandro De Aza307.000$599k$2.6MFREE AGENT
Kelly Johnson328.127$538kFREE AGENT
Christian Walker237.124$500k$53kFREE AGENT
Alexi Casilla296.038$200kFREE AGENT
Ramon E Ramirez328.000FREE AGENT

Arbitration-eligible (11)

PlayerAgeSvc2014201520162017201820192020
Chris Davis284.061$10.3MARB 3FREE AGENT
Matt Wieters284.129$7.7MARB 3FREE AGENT
Bud Norris294.068$5.3MARB 3FREE AGENT
Darren O'Day315.103$3.2MFREE AGENT
Tommy Hunter274.090$3.0MARB 3FREE AGENT
Nick Hundley305.059$2.4MFREE AGENT
Brian Matusz273.156$2.4MARB 2ARB 3FREE AGENT
Ryan Webb284.029$1.8MARB 3FREE AGENT
Evan Meek313.143$800kARB 2ARB 3FREE AGENT
Troy Patton283.150$675kARB 2ARB 3FREE AGENT
Andrew Miller295.062$599kFREE AGENT

Pre-arbitration (20)

PlayerAgeSvc2014201520162017201820192020
Wei-Yin Chen282.000$4.1MARB 1ARB 2ARB 3FREE AGENT
Chris Tillman262.113$546kARB 1ARB 2ARB 3FREE AGENT
Miguel González300.122$529kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Zack Britton261.158$522kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Manny Machado211.056$519kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Steve Lombardozzi Jr.252.023$518kARB 1ARB 2ARB 3FREE AGENT
Ryan Flaherty272.000$512kARB 1ARB 2ARB 3FREE AGENT
David Lough280.169$510kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Brad Brach281.105$509kPre-ArbARB 1ARB 2ARB 3FREE AGENT
T.J. McFarland251.000$505kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Steve Clevenger282.123$505kARB 1ARB 2ARB 3FREE AGENT
Josh Stinson260.090$504kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kevin Gausman230.071$502kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jonathan Schoop220.027$500kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Caleb Joseph280.000$396kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Jimmy Paredes252.059$333kARB 1ARB 2ARB 3FREE AGENT
Jemile Weeks271.142$91kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Preston Guilmet260.034$85kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Quintin Berry290.163$68kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cord Phelps271.095Pre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Orioles 2014; 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.279, -0.79], [1986, 1.024, -2.88], [1987, 1.257, -14.98], [1988, 1.114, -22.14], [1989, 0.618, 2.87], [1990, 0.543, 1.73], [1991, 0.666, 6.86], [1992, 0.804, 11.11], [1993, 0.919, 8.63], [1994, 1.222, 10.8], [1995, 1.332, 2.94], [1996, 1.566, -1.11], [1997, 1.545, 3.87], [1998, 1.777, -0.01], [1999, 1.587, 0.78], [2000, 1.402, -10.94], [2001, 0.855, -12.99], [2002, 0.789, -9.24], [2003, 0.743, -3.29], [2004, 0.669, 8.32], [2005, 1.054, -3.62], [2006, 0.989, -9.38], [2007, 1.044, -8.69], [2008, 0.682, -6.7], [2009, 0.699, -11.97], [2010, 0.879, -14.0], [2011, 0.911, -16.93], [2012, 0.876, -2.38], [2013, 0.885, 3.67], [2014, 0.957, 6.51], [2015, 0.904, 0.4], [2016, 1.221, 0.6], [2017, 1.34, -17.57], [2018, 1.008, -22.32], [2019, 0.63, -16.8], [2021, 0.211, -1.62], [2022, 0.268, 13.96], [2023, 0.55, 15.82], [2024, 0.736, 14.21], [2025, 0.903, -9.03], [2026, 1.035, 2.53]], TEAMCOL = '#DF4601'; 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 = 2014; 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": ["OF", "IF", "OF", "IF", "OF", "SP", "IF", "SP", "SP", "C", "SP", "OF", "RP", "RP", "IF", "OF", "RP", "RP", "IF", "RP", "C", "OF", "RP", "SP", "IF", "SP", "IF", "RP", "RP", "IF", "IF", "RP", "RP", "C", "OF", "IF", "C", "IF", "IF", "RP"], "league": [5.68, 4.42, 3.76, 3.14, 2.78, 2.45, 2.1, 1.92, 1.69, 1.49, 1.31, 1.16, 1.03, 0.94, 0.79, 0.66, 0.56, 0.49, 0.43, 0.38, 0.32, 0.28, 0.23, 0.18, 0.13, 0.09, 0.07, 0.04, 0.02, -0.0, -0.03, -0.05, -0.07, -0.1, -0.12, -0.15, -0.18, -0.21, -0.28, -0.27], "names": ["Adam Jones", "Steve Pearce", "Nelson Cruz", "J.J. Hardy", "Nick Markakis", "Wei-Yin Chen", "Manny Machado", "Chris Tillman", "Kevin Gausman", "Caleb Joseph", "Bud Norris", "David Lough", "Zack Britton", "Andrew Miller", "Chris Davis", "Delmon Young", "Darren O\u0027Day", "Tommy Hunter", "Ryan Flaherty", "Ryan Webb", "Matt Wieters", "Alejandro De Aza", "T.J. McFarland", "Ubaldo Jim\u00e9nez", "Jonathan Schoop", "Miguel Gonz\u00e1lez", "Jimmy Paredes", "Brian Matusz", "Brad Brach", "Kelly Johnson", "Jemile Weeks", "Ramon E Ramirez", "Preston Guilmet", "Nick Hundley", "Quintin Berry", "Steve Lombardozzi Jr.", "Steve Clevenger", "Christian Walker", "Cord Phelps", "Troy Patton"], "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.65, 4.48, 3.87, 2.96, 2.33, 2.33, 2.29, 2.27, 1.93, 1.81, 1.33, 1.18, 1.01, 0.96, 0.93, 0.9, 0.86, 0.71, 0.7, 0.67, 0.59, 0.55, 0.48, 0.38, 0.38, 0.29, 0.22, 0.21, 0.13, 0.13, 0.06, 0.01, -0.01, -0.01, -0.03, -0.04, -0.07, -0.09, -0.09, -0.11]}; 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:'BAL',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);});