Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 60 additions & 2 deletions assets/insights.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
}

function load() {
return fetch('assets/insights.json?v=a6a894c6').then(function (r) {
return fetch('assets/insights.json?v=19b59a90').then(function (r) {
if (!r.ok) throw new Error('insights.json ' + r.status);
return r.json();
});
Expand Down Expand Up @@ -226,7 +226,43 @@
return s + '</svg>';
}

var VIZ = { bars: barsSVG, shares: sharesSVG, timeline: timelineSVG, line: lineSVG };
var VIZ = { bars: barsSVG, shares: sharesSVG, timeline: timelineSVG, line: lineSVG, seasons: seasonsSVG };

/* Multi-season cheapest-vs-priciest comparison: one group per season, two
bars each (gold = cheapest wins, terra = priciest). Rows carry
{s, cheap:{label,v,note}, pricey:{label,v,note}}. */
function seasonsSVG(ins) {
var rows = (ins.rows || []).filter(function (r) {
return r && typeof r.s === 'string' &&
r.cheap && typeof r.cheap.label === 'string' && typeof r.cheap.v === 'number' && isFinite(r.cheap.v) &&
r.pricey && typeof r.pricey.label === 'string' && typeof r.pricey.v === 'number' && isFinite(r.pricey.v);
});
if (!rows.length) return '';
var W = 620, headH = 34, groupH = 96, barH = 13, barGap = 28;
var H = headH + rows.length * groupH + 6;
var maxV = Math.max.apply(null, rows.map(function (r) { return r.pricey.v; })) * 1.06 || 1;
function X(v) { return (v / maxV) * W; }
var s = '<svg viewBox="0 0 ' + W + ' ' + H + '" class="vh-ins__viz" role="img" aria-label="' +
esc(ins.viz_label || 'season comparison chart') + '">';
/* Legend: gold = cheapest wins, terra = priciest wins. */
s += '<rect x="0" y="8" width="12" height="12" rx="3" fill="' + GOLD + '"/>' +
'<text x="18" y="18" class="vh-ins__svgtag">cheapest wins</text>' +
'<rect x="152" y="8" width="12" height="12" rx="3" fill="' + TERRA + '"/>' +
'<text x="170" y="18" class="vh-ins__svgtag">priciest wins</text>';
rows.forEach(function (r, i) {
var gy = headH + i * groupH;
s += '<text x="0" y="' + (gy + 15) + '" class="vh-ins__svglabel">' + esc(r.s) + '</text>';
[['cheap', GOLD, gy + 24], ['pricey', TERRA, gy + 24 + barGap]].forEach(function (t) {
var d = r[t[0]], by = t[2];
var bw = Math.max(3, X(d.v));
var lbl = fitText(d.label + ' \u00b7 $' + d.v + 'M/win' + (d.note ? ' (' + d.note + ')' : ''), W - 8, F_TAG);
s += '<text x="0" y="' + (by - 5) + '" class="vh-ins__svgtag">' + esc(lbl) + '</text>';
s += '<rect x="0" y="' + by + '" width="' + W + '" height="' + barH + '" rx="4" fill="' + VIZ_PAPER + '" opacity="0.06"/>';
s += '<rect x="0" y="' + by + '" width="' + bw.toFixed(1) + '" height="' + barH + '" rx="4" fill="' + t[1] + '"/>';
});
});
return s + '</svg>';
}

/* ---------- card + spotlight rendering ---------- */

Expand Down Expand Up @@ -523,6 +559,28 @@
c.fillText(rows4[0].s, X(0), y + h + 34);
var ls = rows4[rows4.length - 1].s;
c.fillText(ls, X(rows4.length - 1) - c.measureText(ls).width, y + h + 34);
} else if (ins.viz === 'seasons') {
/* Multi-season cheapest-vs-priciest: season label, then a gold
(cheapest) and terra (priciest) bar per season. */
var rows5 = (ins.rows || []).filter(function (r) { return r && r.cheap && r.pricey; });
var rh5 = rows5.length ? h / rows5.length : h;
var max5 = rows5.length ?
Math.max.apply(null, rows5.map(function (r) { return r.pricey.v; })) * 1.06 : 1;
rows5.forEach(function (r, i) {
var ry = y + i * rh5;
c.font = '700 26px ' + F; c.fillStyle = '#F9F6F0';
c.fillText(r.s, x, ry + 32);
[['cheap', GOLD], ['pricey', TERRA]].forEach(function (t, j) {
var d = r[t[0]];
var bwy = ry + 46 + j * 46;
c.font = '400 22px ' + F; c.fillStyle = '#F9F6F0';
fitLabel(d.label + ' \u00b7 $' + d.v + 'M/win', x, bwy + 19, 300);
c.fillStyle = t[1];
c.beginPath();
c.roundRect(x + 320, bwy, Math.max(4, (d.v / max5) * (w - 320)), 24, 6);
c.fill();
});
});
}
c.restore();
return y + h;
Expand Down
103 changes: 55 additions & 48 deletions assets/insights.json
Original file line number Diff line number Diff line change
Expand Up @@ -1232,65 +1232,72 @@
"series": "ledger"
},
{
"foot": "Payrolls: Basketball Insiders 2024-25 season snapshot (total cap). Wins: 2024-25 regular season. Cross-checked against Spotrac cap tracker.",
"foot": "Payrolls: Spotrac NBA Team Salary Cap Tracker, 'Total Cap Allocations' (includes dead money and mid-season moves), one table per season. Wins: final regular-season records — 2023-24 wins cross-checked against landofbasketball.com, whose tracker record column was stale that year. League-average cost per win rose $3.84M → $4.83M over the window. The earlier single-season card used a Basketball Insiders snapshot, so 2024-25 figures differ slightly.",
"kicker": "Money layer",
"lede": "Revenue efficiency, team-level: 2024-25 total payroll (Basketball Insiders season snapshot) divided by regular-season wins. A 5.4x spread separates the most efficient checkbook from the least.",
"og_desc": "Oklahoma City spent $159M and won 68 games. Washington spent $228M and won 18. Payroll per win, 2024-25.",
"og_title": "The cheapest NBA wins cost $2.34M — the priciest $12.67M",
"lede": "Revenue efficiency, team-level: Spotrac Total Cap Allocations divided by regular-season wins, 2022-23 through 2025-26. One consistent source across all four seasons.",
"og_desc": "Oklahoma City bought the cheapest wins three of the last four seasons. Washington paid the most three straight — $13.65M per win in 2025-26.",
"og_title": "The cheapest NBA wins cost $2.28M — the priciest $13.65M",
"rows": [
{
"label": "Oklahoma City",
"tag": "68 wins, best",
"value": 2.34
},
{
"label": "Detroit",
"tag": null,
"value": 2.98
},
{
"label": "Cleveland",
"tag": null,
"value": 2.99
},
{
"label": "Boston",
"tag": null,
"value": 3.36
},
{
"label": "Phoenix",
"tag": "$237M for 36 wins",
"value": 6.58
},
{
"label": "Philadelphia",
"tag": null,
"value": 7.43
"s": "2022-23",
"cheap": {
"label": "Memphis",
"v": 2.28,
"note": "56 wins"
},
"pricey": {
"label": "Houston",
"v": 6.91,
"note": "20 wins"
}
},
{
"label": "Utah",
"tag": null,
"value": 8.73
"s": "2023-24",
"cheap": {
"label": "Oklahoma City",
"v": 2.82,
"note": "57 wins"
},
"pricey": {
"label": "Washington",
"v": 12.66,
"note": "15 wins"
}
},
{
"label": "New Orleans",
"tag": null,
"value": 8.84
"s": "2024-25",
"cheap": {
"label": "Oklahoma City",
"v": 2.44,
"note": "68 wins"
},
"pricey": {
"label": "Washington",
"v": 11.76,
"note": "18 wins"
}
},
{
"label": "Washington",
"tag": "18 wins, worst",
"value": 12.67
"s": "2025-26",
"cheap": {
"label": "Oklahoma City",
"v": 2.94,
"note": "64 wins"
},
"pricey": {
"label": "Washington",
"v": 13.65,
"note": "17 wins"
}
}
],
"slug": "payroll-per-win",
"stat": "2.34",
"stat_label": "$M per win — Oklahoma City, best payroll efficiency",
"title": "The cheapest wins in the NBA cost $2.34M — the priciest cost $12.67M",
"tldr": "Team payroll divided by 2024-25 wins: Oklahoma City bought the league's best record for $2.34M per win, while Washington spent $12.67M per win on 18 wins.",
"viz": "bars",
"viz_label": "Payroll $M per regular-season win, 2024-25 (lower = cheaper wins)",
"stat": "6.0x",
"stat_label": "cheapest-to-priciest win across the four-season window",
"title": "The cheapest wins in the NBA cost $2.28M — the priciest cost $13.65M",
"tldr": "Payroll divided by wins, four seasons running: Oklahoma City bought the cheapest wins three of the last four years; Washington paid the most three straight, $13.65M per win last season.",
"viz": "seasons",
"viz_label": "Payroll $M per regular-season win — cheapest vs priciest, by season (lower = cheaper wins)",
"series": "ledger"
},
{
Expand Down Expand Up @@ -1658,4 +1665,4 @@
"series": "ledger"
}
]
}
}
Binary file modified assets/og/insight-payroll-per-win.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion insights.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,6 @@ <h1 class="title-card__title reveal reveal-1">Insights: from the model, and from
<p class="site-footer__attribution">Every number recomputable from stats.nba.com · A <a href="https://dumbmodel.com">dumbmodel</a> project</p>
</footer>
<script src="/assets/site-nav.js?v=1d646996"></script>
<script src="assets/insights.js?v=9474d72b"></script>
<script src="assets/insights.js?v=7d69dfe7"></script>
<script>if('serviceWorker' in navigator){window.addEventListener('load',()=>{navigator.serviceWorker.register('/sw.js').catch(()=>{})})}</script>
</body></html>
16 changes: 8 additions & 8 deletions insights/payroll-per-win.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,26 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The cheapest NBA wins cost $2.34M — the priciest $12.67M — Vector Hoops Insights</title>
<meta name="description" content="Oklahoma City spent $159M and won 68 games. Washington spent $228M and won 18. Payroll per win, 2024-25.">
<title>The cheapest NBA wins cost $2.28M — the priciest $13.65M — Vector Hoops Insights</title>
<meta name="description" content="Oklahoma City bought the cheapest wins three of the last four seasons. Washington paid the most three straight — $13.65M per win in 2025-26.">
<link rel="canonical" href="https://hoops.dumbmodel.com/insights/payroll-per-win">
<meta property="og:type" content="article">
<meta property="og:site_name" content="Vector Hoops">
<meta property="og:title" content="The cheapest NBA wins cost $2.34M — the priciest $12.67M">
<meta property="og:description" content="Oklahoma City spent $159M and won 68 games. Washington spent $228M and won 18. Payroll per win, 2024-25.">
<meta property="og:title" content="The cheapest NBA wins cost $2.28M — the priciest $13.65M">
<meta property="og:description" content="Oklahoma City bought the cheapest wins three of the last four seasons. Washington paid the most three straight — $13.65M per win in 2025-26.">
<meta property="og:url" content="https://hoops.dumbmodel.com/insights/payroll-per-win">
<meta property="og:image" content="https://hoops.dumbmodel.com/assets/og/insight-payroll-per-win.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="The cheapest NBA wins cost $2.34M — the priciest $12.67M">
<meta name="twitter:description" content="Oklahoma City spent $159M and won 68 games. Washington spent $228M and won 18. Payroll per win, 2024-25.">
<meta name="twitter:title" content="The cheapest NBA wins cost $2.28M — the priciest $13.65M">
<meta name="twitter:description" content="Oklahoma City bought the cheapest wins three of the last four seasons. Washington paid the most three straight — $13.65M per win in 2025-26.">
<meta name="twitter:image" content="https://hoops.dumbmodel.com/assets/og/insight-payroll-per-win.png">
<meta http-equiv="refresh" content="0; url=/insights.html#payroll-per-win">
</head>
<body>
<h1>The cheapest NBA wins cost $2.34M — the priciest $12.67M</h1>
<p>Oklahoma City spent $159M and won 68 games. Washington spent $228M and won 18. Payroll per win, 2024-25.</p>
<h1>The cheapest NBA wins cost $2.28M — the priciest $13.65M</h1>
<p>Oklahoma City bought the cheapest wins three of the last four seasons. Washington paid the most three straight — $13.65M per win in 2025-26.</p>
<p><a href="/insights.html#payroll-per-win">View this insight on Vector Hoops</a></p>
<script>location.replace("/insights.html#payroll-per-win");</script>
</body>
Expand Down
62 changes: 60 additions & 2 deletions public/assets/insights.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
}

function load() {
return fetch('assets/insights.json?v=a6a894c6').then(function (r) {
return fetch('assets/insights.json?v=19b59a90').then(function (r) {
if (!r.ok) throw new Error('insights.json ' + r.status);
return r.json();
});
Expand Down Expand Up @@ -226,7 +226,43 @@
return s + '</svg>';
}

var VIZ = { bars: barsSVG, shares: sharesSVG, timeline: timelineSVG, line: lineSVG };
var VIZ = { bars: barsSVG, shares: sharesSVG, timeline: timelineSVG, line: lineSVG, seasons: seasonsSVG };

/* Multi-season cheapest-vs-priciest comparison: one group per season, two
bars each (gold = cheapest wins, terra = priciest). Rows carry
{s, cheap:{label,v,note}, pricey:{label,v,note}}. */
function seasonsSVG(ins) {
var rows = (ins.rows || []).filter(function (r) {
return r && typeof r.s === 'string' &&
r.cheap && typeof r.cheap.label === 'string' && typeof r.cheap.v === 'number' && isFinite(r.cheap.v) &&
r.pricey && typeof r.pricey.label === 'string' && typeof r.pricey.v === 'number' && isFinite(r.pricey.v);
});
if (!rows.length) return '';
var W = 620, headH = 34, groupH = 96, barH = 13, barGap = 28;
var H = headH + rows.length * groupH + 6;
var maxV = Math.max.apply(null, rows.map(function (r) { return r.pricey.v; })) * 1.06 || 1;
function X(v) { return (v / maxV) * W; }
var s = '<svg viewBox="0 0 ' + W + ' ' + H + '" class="vh-ins__viz" role="img" aria-label="' +
esc(ins.viz_label || 'season comparison chart') + '">';
/* Legend: gold = cheapest wins, terra = priciest wins. */
s += '<rect x="0" y="8" width="12" height="12" rx="3" fill="' + GOLD + '"/>' +
'<text x="18" y="18" class="vh-ins__svgtag">cheapest wins</text>' +
'<rect x="152" y="8" width="12" height="12" rx="3" fill="' + TERRA + '"/>' +
'<text x="170" y="18" class="vh-ins__svgtag">priciest wins</text>';
rows.forEach(function (r, i) {
var gy = headH + i * groupH;
s += '<text x="0" y="' + (gy + 15) + '" class="vh-ins__svglabel">' + esc(r.s) + '</text>';
[['cheap', GOLD, gy + 24], ['pricey', TERRA, gy + 24 + barGap]].forEach(function (t) {
var d = r[t[0]], by = t[2];
var bw = Math.max(3, X(d.v));
var lbl = fitText(d.label + ' \u00b7 $' + d.v + 'M/win' + (d.note ? ' (' + d.note + ')' : ''), W - 8, F_TAG);
s += '<text x="0" y="' + (by - 5) + '" class="vh-ins__svgtag">' + esc(lbl) + '</text>';
s += '<rect x="0" y="' + by + '" width="' + W + '" height="' + barH + '" rx="4" fill="' + VIZ_PAPER + '" opacity="0.06"/>';
s += '<rect x="0" y="' + by + '" width="' + bw.toFixed(1) + '" height="' + barH + '" rx="4" fill="' + t[1] + '"/>';
});
});
return s + '</svg>';
}

/* ---------- card + spotlight rendering ---------- */

Expand Down Expand Up @@ -523,6 +559,28 @@
c.fillText(rows4[0].s, X(0), y + h + 34);
var ls = rows4[rows4.length - 1].s;
c.fillText(ls, X(rows4.length - 1) - c.measureText(ls).width, y + h + 34);
} else if (ins.viz === 'seasons') {
/* Multi-season cheapest-vs-priciest: season label, then a gold
(cheapest) and terra (priciest) bar per season. */
var rows5 = (ins.rows || []).filter(function (r) { return r && r.cheap && r.pricey; });
var rh5 = rows5.length ? h / rows5.length : h;
var max5 = rows5.length ?
Math.max.apply(null, rows5.map(function (r) { return r.pricey.v; })) * 1.06 : 1;
rows5.forEach(function (r, i) {
var ry = y + i * rh5;
c.font = '700 26px ' + F; c.fillStyle = '#F9F6F0';
c.fillText(r.s, x, ry + 32);
[['cheap', GOLD], ['pricey', TERRA]].forEach(function (t, j) {
var d = r[t[0]];
var bwy = ry + 46 + j * 46;
c.font = '400 22px ' + F; c.fillStyle = '#F9F6F0';
fitLabel(d.label + ' \u00b7 $' + d.v + 'M/win', x, bwy + 19, 300);
c.fillStyle = t[1];
c.beginPath();
c.roundRect(x + 320, bwy, Math.max(4, (d.v / max5) * (w - 320)), 24, 6);
c.fill();
});
});
}
c.restore();
return y + h;
Expand Down
Loading
Loading