RRosternomics

Nationals WSN

GM: Dan Duquette

Overview

WINDOW ↗ rising
cheap & winning · year 4 in this regime · payroll 0.6× league · TAB +23.8
+23.8TAB — talent above budget1st/28
+20.9WAB — realized wins (TAB + luck)1st/28
8.0depth — 2+ WAR regulars1st/28
-1.9dead WAR — sub-replacement drag4th/28
9%dead money %3rd/28
0.649win %1st/28
36.5roster fWAR3rd/28
+23surplus $M (value vs cost)1st/28
$19Mpayroll $M27th/28

Roster

CFMarquis Grissom 109GLou Frazier 5GRondell White 4G
LFMoises Alou 63GLou Frazier 31GRondell White 25G
RFLarry Walker 68GMoises Alou 45GCliff Floyd 9G
SSWil Cordero 109GMike Lansing 12GFreddie Benavides 3G
2BMike Lansing 82GFreddie Benavides 36GJuan Bell 25G
3BSean Berry 100GMike Lansing 27GJeff Gardner 9G
1BCliff Floyd 77GLarry Walker 35GRandy Milligan 33G
STARTERSPedro Martínez 3.2WARJeff Fassero 2.6WARButch Henry 2.3WARKen Hill 2.3WARKirk Rueter 0.9WAR
CDarrin Fletcher 81GTim Spehr 46GLenny Webster 46G
BULLPEN John Wetteland 25svMel Rojas 0hJeff Shaw 0hTim Scott 0hGil Heredia 0hDenis Boucher 0hHeath Haynes 0hRod Henderson 0h
CDarrin Fletcher 81 · Tim Spehr 46 · Lenny Webster 46
1BCliff Floyd 77 · Larry Walker 35 · Randy Milligan 33
2BMike Lansing 82 · Freddie Benavides 36 · Juan Bell 25
3BSean Berry 100 · Mike Lansing 27 · Jeff Gardner 9
SSWil Cordero 109 · Mike Lansing 12 · Freddie Benavides 3
LFMoises Alou 63 · Lou Frazier 31 · Rondell White 25
CFMarquis Grissom 109 · Lou Frazier 5 · Rondell White 4
RFLarry Walker 68 · Moises Alou 45 · Cliff Floyd 9
SPPedro Martínez 3.2 · Jeff Fassero 2.6 · Butch Henry 2.3 · Ken Hill 2.3 · Kirk Rueter 0.9
BP John Wetteland 25 · Mel Rojas 0 · Jeff Shaw 0 · Tim Scott 0 · Gil Heredia 0 · Denis Boucher 0 · Heath Haynes 0 · Rod Henderson 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.

1994 roster — value & surplus

PlayerAgeServiceGfWARSalary $MSurplus $M
Moises Alou 27 arb 1074.7 1.4 +4.0
Larry Walker 27 arb 1034.4 4.0 +1.1
Marquis Grissom 27 arb 1104.1 3.6 +1.2
Pedro Martínez 22 pre-arb 243.2 0.2 +3.5
Jeff Fassero 31 arb 212.6 0.3 +2.7
Butch Henry 25 pre-arb 242.3 0.1 +2.6
Wil Cordero 22 pre-arb 1102.3 0.2 +2.5
Ken Hill 28 FA-elig 242.3 2.6 +0.1
Mike Lansing 26 pre-arb 1061.5 0.2 +1.6
John Wetteland 27 arb 521.4 2.2 -0.6
Gil Heredia 28 arb 391.4 0.1 +1.5
Tim Scott 27 arb 401.1 0.2 +1.1
Lenny Webster 29 arb 571.0 0.2 +0.9
Mel Rojas 27 arb 580.9 0.8 +0.2
Kirk Rueter 23 pre-arb 200.9 0.1 +0.9
Sean Berry 28 arb 1030.9 0.2 +0.8
Darrin Fletcher 27 arb 940.8 0.6 +0.3
Juan Bell 26 380.6
Lou Frazier 29 pre-arb 760.6 0.1 +0.5
Jeff Shaw 27 arb 460.5 0.2 +0.4
Rondell White 22 pre-arb 400.4 0.1 +0.4
Cliff Floyd 21 pre-arb 1000.4 0.1 +0.3
Tim Spehr 27 arb 520.2 0.1 +0.1
Randy Milligan 32 FA-elig 470.0 0.6 -0.6
Joey Eischen 24 pre-arb 1-0.0 0.1 -0.1
Heath Haynes 25 4-0.0
Brian Looney 24 1-0.1
Gabe White 22 7-0.1
Rod Henderson 23 pre-arb 3-0.2 0.1 -0.3
Denis Boucher 26 arb 10-0.3 0.1 -0.5
Jeff Gardner 30 arb 18-0.3 0.1 -0.5
Freddie Benavides 28 arb 47-1.0 0.2 -1.3

Key additions (new vs 1993)

PlayerfWAR
Pedro Martínez3.2
Lenny Webster1.0
Juan Bell0.6
Randy Milligan0.0
Joey Eischen-0.0
Heath Haynes-0.0

Key subtractions (gone from 1993)

Player1993 fWAR
Delino DeShields4.5
Dennis Martinez1.9
Chris Nabholz0.5
Curtis Pride0.3
Jimmy Jones0.2
Brian Barnes0.1

Current & Historical Payroll

1994 payroll
$19.1M

What the front office was committed to as of 1994, 1994–2000. 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 (2)

PlayerAgeSvc1994199519961997199819992000
Ken Hill286.000$2.6MFREE AGENT
Randy Milligan327.000$600kFREE AGENT

Arbitration-eligible (17)

PlayerAgeSvc1994199519961997199819992000
Larry Walker275.000$4.0MFREE AGENT
Marquis Grissom275.000$3.6MFREE AGENT
John Wetteland275.000$2.2MFREE AGENT
Moises Alou274.000$1.4MARB 3FREE AGENT
Mel Rojas274.000$850kARB 3FREE AGENT
Darrin Fletcher275.000$615kFREE AGENT
Jeff Fassero313.000$315kARB 2ARB 3FREE AGENT
Sean Berry284.000$200kARB 3FREE AGENT
Freddie Benavides283.000$195kARB 2ARB 3FREE AGENT
Lenny Webster295.000$180kFREE AGENT
Jeff Shaw274.000$172kARB 3FREE AGENT
Tim Scott273.000$165kARB 2ARB 3FREE AGENT
Gil Heredia283.000$135kARB 2ARB 3FREE AGENT
Denis Boucher263.000$135kARB 2ARB 3FREE AGENT
Tim Spehr273.000$132kARB 2ARB 3FREE AGENT
Jeff Gardner303.000$109kARB 2ARB 3FREE AGENT
Juan Bell265.000FREE AGENT

Pre-arbitration (13)

PlayerAgeSvc1994199519961997199819992000
Pedro Martínez222.000$200kARB 1ARB 2ARB 3FREE AGENT
Wil Cordero222.000$200kARB 1ARB 2ARB 3FREE AGENT
Mike Lansing261.000$200kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Lou Frazier291.000$140kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Kirk Rueter231.000$130kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Butch Henry252.000$130kARB 1ARB 2ARB 3FREE AGENT
Rondell White221.000$112kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Cliff Floyd211.000$110kPre-ArbARB 1ARB 2ARB 3FREE AGENT
Joey Eischen240.000$109kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Rod Henderson230.000$109kPre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT
Heath Haynes25
Brian Looney24
Gabe White220.000Pre-ArbPre-ArbARB 1ARB 2ARB 3FREE AGENT

Graphs

Every roster slot ranked best→worst by WAR. Bars = Nationals 1994; 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.927, 3.09], [1986, 0.944, 0.73], [1987, 0.694, 4.9], [1988, 0.801, 3.6], [1989, 1.029, -1.58], [1990, 0.942, -1.54], [1991, 0.455, 2.76], [1992, 0.53, 10.23], [1993, 0.5, 7.54], [1994, 0.598, 23.78], [1995, 0.402, 5.53], [1996, 0.503, 8.31], [1997, 0.49, 6.76], [1998, 0.242, 7.47], [1999, 0.365, 3.28], [2000, 0.528, -4.45], [2001, 0.547, -11.98], [2002, 0.632, 1.03], [2003, 0.707, -5.0], [2004, 0.597, -12.73], [2005, 0.767, 0.06], [2006, 0.701, -5.96], [2007, 0.385, -5.22], [2008, 0.595, -11.25], [2009, 0.639, -8.42], [2010, 0.681, -3.28], [2011, 0.724, -2.08], [2012, 0.879, 10.48], [2013, 1.136, -0.82], [2014, 1.229, 13.52], [2015, 1.334, 2.41], [2016, 1.176, 9.04], [2017, 1.384, 8.02], [2018, 1.373, 2.81], [2019, 1.57, 10.46], [2021, 1.229, -8.19], [2022, 0.851, -24.23], [2023, 0.433, -8.77], [2024, 0.582, -0.75], [2025, 0.426, -8.56], [2026, 0.315, 10.9]], TEAMCOL = '#AB0003'; 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 = 1994; 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", "OF", "OF", "SP", "SP", "SP", "IF", "SP", "IF", "RP", "RP", "RP", "C", "RP", "SP", "IF", "C", "IF", "OF", "RP", "OF", "IF", "C", "IF", "RP", "RP", "RP", "SP", "SP", "RP", "IF", "IF", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014", "\u2014"], "league": [4.46, 3.51, 2.81, 2.48, 2.13, 1.89, 1.57, 1.36, 1.16, 0.99, 0.88, 0.76, 0.67, 0.58, 0.49, 0.42, 0.35, 0.29, 0.24, 0.19, 0.11, 0.07, 0.03, -0.01, -0.04, -0.07, -0.11, -0.13, -0.18, -0.22, -0.31, -0.39, -0.38, -0.41, -0.49, -0.55, -0.56, -0.44, -0.58, -0.9], "names": ["Moises Alou", "Larry Walker", "Marquis Grissom", "Pedro Mart\u00ednez", "Jeff Fassero", "Butch Henry", "Wil Cordero", "Ken Hill", "Mike Lansing", "John Wetteland", "Gil Heredia", "Tim Scott", "Lenny Webster", "Mel Rojas", "Kirk Rueter", "Sean Berry", "Darrin Fletcher", "Juan Bell", "Lou Frazier", "Jeff Shaw", "Rondell White", "Cliff Floyd", "Tim Spehr", "Randy Milligan", "Joey Eischen", "Heath Haynes", "Brian Looney", "Gabe White", "Rod Henderson", "Denis Boucher", "Jeff Gardner", "Freddie Benavides", "(empty slot)", "(empty slot)", "(empty slot)", "(empty slot)", "(empty slot)", "(empty slot)", "(empty slot)", "(empty slot)"], "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.67, 4.4, 4.1, 3.22, 2.59, 2.32, 2.31, 2.3, 1.52, 1.43, 1.42, 1.09, 0.95, 0.92, 0.89, 0.85, 0.79, 0.6, 0.59, 0.48, 0.45, 0.35, 0.16, 0.02, -0.0, -0.02, -0.08, -0.08, -0.17, -0.28, -0.3, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]}; 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:'WSN',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);});