Skip to content

Commit 4ea8a7c

Browse files
committed
Add multi-season support
1 parent adc8391 commit 4ea8a7c

107 files changed

Lines changed: 64133 additions & 1390 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

observablehq.config.js

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
// See https://observablehq.com/framework/config for documentation.
2+
import { readFileSync } from "fs";
3+
import { csvParse } from "d3-dsv";
4+
import stripBom from "strip-bom";
5+
6+
// Read seasons at config-load time so they can be inlined into every page's HTML
7+
const _seasonsRaw = csvParse(stripBom(readFileSync("src/data/static/seasons.csv", "utf-8")));
8+
const _seasons = _seasonsRaw.map(s => s.season);
9+
const _currentSeason = _seasonsRaw.find(s => s.current === "true")?.season || _seasons[_seasons.length - 1];
210
export default {
311
// The app’s title; used in the sidebar and webpage titles.
4-
title: "2025-26 FHL Dashboard",
12+
title: "FHL Dashboard",
513

614
// The pages and sections in the sidebar. If you don’t specify this option,
715
// all pages will be listed in alphabetical order. Listing pages explicitly
@@ -28,7 +36,78 @@ export default {
2836
],
2937

3038
// Content to add to the head of the page, e.g. for a favicon:
31-
head: '<link rel="icon" href="observable.png" type="image/png" sizes="32x32">',
39+
head: `<link rel="icon" href="observable.png" type="image/png" sizes="32x32">
40+
<script>
41+
(function() {
42+
var seasons = ${JSON.stringify(_seasons)};
43+
var currentSeason = ${JSON.stringify(_currentSeason)};
44+
var STORAGE_KEY = 'fhl-season';
45+
46+
// Resolve active season: URL param → sessionStorage → default
47+
var _p = new URLSearchParams(window.location.search);
48+
var activeSeason = _p.get('season');
49+
if (!activeSeason || seasons.indexOf(activeSeason) === -1) {
50+
activeSeason = sessionStorage.getItem(STORAGE_KEY) || currentSeason;
51+
if (seasons.indexOf(activeSeason) === -1) activeSeason = currentSeason;
52+
}
53+
sessionStorage.setItem(STORAGE_KEY, activeSeason);
54+
// If season wasn't in the URL, add it silently so Observable page cells
55+
// see the correct ?season= when they evaluate
56+
if (!_p.get('season')) {
57+
_p.set('season', activeSeason);
58+
window.history.replaceState(null, '', window.location.pathname + '?' + _p.toString());
59+
}
60+
61+
function init() {
62+
var select = document.getElementById('fhl-season-select');
63+
if (!select) { setTimeout(init, 50); return; }
64+
seasons.forEach(function(s) {
65+
var opt = document.createElement('option');
66+
opt.value = s; opt.textContent = s;
67+
if (s === activeSeason) opt.selected = true;
68+
select.appendChild(opt);
69+
});
70+
select.addEventListener('change', function() {
71+
activeSeason = select.value;
72+
sessionStorage.setItem(STORAGE_KEY, activeSeason);
73+
var p = new URLSearchParams(window.location.search);
74+
p.set('season', activeSeason);
75+
window.location.search = p.toString();
76+
});
77+
// Intercept internal nav links to keep ?season= param across pages
78+
document.addEventListener('click', function(e) {
79+
var link = e.target.closest('a[href]');
80+
if (!link) return;
81+
var href = link.getAttribute('href');
82+
if (!href || href.startsWith('http') || href.startsWith('//') ||
83+
href.startsWith('#') || href.startsWith('mailto:')) return;
84+
try {
85+
var url = new URL(href, window.location.origin);
86+
if (url.origin !== window.location.origin) return;
87+
if (!url.searchParams.get('season')) {
88+
e.preventDefault();
89+
url.searchParams.set('season', activeSeason);
90+
window.location.href = url.toString();
91+
}
92+
} catch (_e) {}
93+
});
94+
}
95+
96+
if (document.readyState === 'loading') {
97+
document.addEventListener('DOMContentLoaded', init);
98+
} else {
99+
init();
100+
}
101+
})();
102+
</script>`,
103+
104+
// Season selector HTML rendered in every page header
105+
header: `<div id="fhl-season-bar" style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 1rem;font-size:0.875rem;border-bottom:1px solid var(--theme-border,#e0e0e0)">
106+
<span style="margin-left:auto;display:flex;align-items:center;gap:0.4rem">
107+
<label for="fhl-season-select" style="font-size:0.8rem">Season:</label>
108+
<select id="fhl-season-select" style="font-size:0.8rem;padding:2px 6px;border:1px solid var(--theme-border,#ccc);border-radius:4px"></select>
109+
</span>
110+
</div>`,
32111

33112
// The path to the source root.
34113
root: "src",

src/CompareTeams.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ toc: false
99
// Load the data files
1010
const rosters = await FileAttachment("./data/rosters.json").json();
1111
const teamInfo = await FileAttachment("./data/teamInfo.json").json();
12+
const _params = new URLSearchParams(window.location.search);
13+
const _season = _params.get("season") || rosters.currentSeason;
14+
const _rsd = rosters.data[_season];
15+
const _tisd = teamInfo.data[_season];
1216

13-
const team1Selector = Inputs.select(rosters.teams, {label: "Select Team 1:"});
17+
const team1Selector = Inputs.select(_rsd.teams, {label: "Select Team 1:"});
1418
const selectedTeam1 = Generators.input(team1Selector);
1519

16-
const team2Selector = Inputs.select(rosters.teams, {label: "Select Team 2:"});
20+
const team2Selector = Inputs.select(_rsd.teams, {label: "Select Team 2:"});
1721
const selectedTeam2 = view(team2Selector);
1822

1923
function createRosterTable(roster) {
@@ -59,10 +63,10 @@ function createRosterTable(roster) {
5963
```
6064

6165
```js
62-
const team1 = teamInfo.teams.find((t) => t.ABBR === selectedTeam1);
63-
const team2 = teamInfo.teams.find((t) => t.ABBR === selectedTeam2);
64-
const team1Roster = rosters.teamData.find((t) => t.ABBR === selectedTeam1)["OVERALL"].ROSTER;
65-
const team2Roster = rosters.teamData.find((t) => t.ABBR === selectedTeam2)["OVERALL"].ROSTER;
66+
const team1 = _tisd.teams.find((t) => t.ABBR === selectedTeam1);
67+
const team2 = _tisd.teams.find((t) => t.ABBR === selectedTeam2);
68+
const team1Roster = _rsd.teamData.find((t) => t.ABBR === selectedTeam1)["OVERALL"].ROSTER;
69+
const team2Roster = _rsd.teamData.find((t) => t.ABBR === selectedTeam2)["OVERALL"].ROSTER;
6670
```
6771

6872
```js
@@ -81,8 +85,8 @@ const team2SalariesOut = team2Selections.map((player) => player.Salary).reduce(f
8185

8286
const team1NewSalaryPerPeriod = (team1.LatestSalary - team1SalariesOut + team2SalariesOut) / 25;
8387
const team2NewSalaryPerPeriod = (team2.LatestSalary - team2SalariesOut + team1SalariesOut) / 25;
84-
const team1NewBudget = team1.CASH - (team1NewSalaryPerPeriod < 13 ? 13 : team1NewSalaryPerPeriod) * (25 - teamInfo.lastPeriodNum);
85-
const team2NewBudget = team2.CASH - (team2NewSalaryPerPeriod < 13 ? 13 : team2NewSalaryPerPeriod) * (25 - teamInfo.lastPeriodNum);
88+
const team1NewBudget = team1.CASH - (team1NewSalaryPerPeriod < 13 ? 13 : team1NewSalaryPerPeriod) * (25 - _tisd.lastPeriodNum);
89+
const team2NewBudget = team2.CASH - (team2NewSalaryPerPeriod < 13 ? 13 : team2NewSalaryPerPeriod) * (25 - _tisd.lastPeriodNum);
8690
```
8791

8892
<pre>

src/Overall.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ toc: false
88
```js
99
// Load the data files
1010
const playerInfo = await FileAttachment("./data/allplayers.json").json();
11+
const _params = new URLSearchParams(window.location.search);
12+
const _season = _params.get("season") || playerInfo.currentSeason;
13+
const _sd = playerInfo.data[_season];
1114

12-
const periodSelector = Inputs.select(playerInfo.availablePeriods, {label: "Select Period:", value: playerInfo.availablePeriods[playerInfo.availablePeriods.length-1]});
15+
const periodSelector = Inputs.select(_sd.availablePeriods, {label: "Select Period:", value: _sd.availablePeriods[_sd.availablePeriods.length-1]});
1316
const selectedPeriod = Generators.input(periodSelector);
1417

15-
const teamSelector = Inputs.select(playerInfo.teams, {label: "Select Team:"});
18+
const teamSelector = Inputs.select(_sd.teams, {label: "Select Team:"});
1619
const selectedTeam = Generators.input(teamSelector);
1720

1821
```
@@ -21,7 +24,7 @@ ${teamSelector}
2124
${searchInput}
2225

2326
```js
24-
const playersToSearch = playerInfo.playerData.map(p => p[selectedPeriod]).filter((p) => (selectedTeam === "All") || (p.FHL === selectedTeam));
27+
const playersToSearch = _sd.playerData.map(p => p[selectedPeriod]).filter((p) => (selectedTeam === "All") || (p.FHL === selectedTeam));
2528
console.log(playersToSearch);
2629
const searchInput = Inputs.search(playersToSearch);
2730
const searchPlayers = Generators.input(searchInput);

src/Players.md

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ toc: false
88
```js
99
// Load the data files
1010
const teamInfo = await FileAttachment("./data/rosters.json").json();
11+
const _params = new URLSearchParams(window.location.search);
12+
const _season = _params.get("season") || teamInfo.currentSeason;
13+
const _sd = teamInfo.data[_season];
1114

12-
const teamSelector = Inputs.select(teamInfo.teams, {label: "Select Team:"});
15+
const teamSelector = Inputs.select(_sd.teams, {label: "Select Team:"});
1316
const selectedTeam = Generators.input(teamSelector);
1417

15-
const periodSelector = Inputs.select(teamInfo.availablePeriods, {label: "Select Period:", value: teamInfo.availablePeriods[teamInfo.availablePeriods.length-2]});
18+
const periodSelector = Inputs.select(_sd.availablePeriods, {label: "Select Period:", value: _sd.availablePeriods[_sd.availablePeriods.length-2]});
1619
const selectedPeriod = Generators.input(periodSelector);
1720
```
1821

@@ -26,7 +29,7 @@ ${periodSelector}
2629
</div>
2730

2831
<div id="contract-tab" class="tab-content">
29-
${Inputs.table(teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ROSTER, {
32+
${Inputs.table(_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ROSTER, {
3033
columns: ["Name", "Position", "Salary", "Contract", "BirthDate", "Age"],
3134
header: {
3235
Name: "Player Name",
@@ -55,22 +58,22 @@ ${periodSelector}
5558
<div class="stats-totals">
5659
<div class="totals-row active-totals">
5760
<strong>Active Totals:</strong>
58-
<span>Goals: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.goals}</span>
59-
<span>Assists: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.assists}</span>
60-
<span>Toughness: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.toughness}</span>
61-
<span>D-Stat: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.dstat.toFixed(2)}</span>
62-
<span>G-Stat: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.gstat.toFixed(2)}</span>
61+
<span>Goals: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.goals}</span>
62+
<span>Assists: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.assists}</span>
63+
<span>Toughness: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.toughness}</span>
64+
<span>D-Stat: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.dstat.toFixed(2)}</span>
65+
<span>G-Stat: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.gstat.toFixed(2)}</span>
6366
</div>
6467
<div class="totals-row reserve-totals">
6568
<strong>Reserve Totals:</strong>
66-
<span>Goals: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.goals}</span>
67-
<span>Assists: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.assists}</span>
68-
<span>Toughness: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.toughness}</span>
69-
<span>D-Stat: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.dstat.toFixed(2)}</span>
70-
<span>G-Stat: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.gstat.toFixed(2)}</span>
69+
<span>Goals: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.goals}</span>
70+
<span>Assists: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.assists}</span>
71+
<span>Toughness: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.toughness}</span>
72+
<span>D-Stat: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.dstat.toFixed(2)}</span>
73+
<span>G-Stat: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.gstat.toFixed(2)}</span>
7174
</div>
7275
</div>
73-
${Inputs.table(teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ROSTER, {
76+
${Inputs.table(_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ROSTER, {
7477
columns: ["Name", "Position", "Reserve", "GamesPlayed", "Goals", "Assists", "Toughness", "DStat", "Rating", "NHLTeam", "Salary", "Contract", "BirthDate", "Age"],
7578
header: {
7679
Name: "Player Name",
@@ -123,22 +126,22 @@ ${periodSelector}
123126
<div class="stats-totals">
124127
<div class="totals-row active-totals">
125128
<strong>Active Totals:</strong>
126-
<span>Goals: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.goals}</span>
127-
<span>Assists: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.assists}</span>
128-
<span>Toughness: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.toughness}</span>
129-
<span>D-Stat: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.dstat.toFixed(2)}</span>
130-
<span>G-Stat: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.gstat.toFixed(2)}</span>
129+
<span>Goals: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.goals}</span>
130+
<span>Assists: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.assists}</span>
131+
<span>Toughness: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.toughness}</span>
132+
<span>D-Stat: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.dstat.toFixed(2)}</span>
133+
<span>G-Stat: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ACTIVE_TOTALS.gstat.toFixed(2)}</span>
131134
</div>
132135
<div class="totals-row reserve-totals">
133136
<strong>Reserve Totals:</strong>
134-
<span>Goals: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.goals}</span>
135-
<span>Assists: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.assists}</span>
136-
<span>Toughness: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.toughness}</span>
137-
<span>D-Stat: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.dstat.toFixed(2)}</span>
138-
<span>G-Stat: ${teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.gstat.toFixed(2)}</span>
137+
<span>Goals: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.goals}</span>
138+
<span>Assists: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.assists}</span>
139+
<span>Toughness: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.toughness}</span>
140+
<span>D-Stat: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.dstat.toFixed(2)}</span>
141+
<span>G-Stat: ${_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].RESERVE_TOTALS.gstat.toFixed(2)}</span>
139142
</div>
140143
</div>
141-
${Inputs.table(teamInfo.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ROSTER, {
144+
${Inputs.table(_sd.teamData.find((t) => t.ABBR === selectedTeam)[selectedPeriod].ROSTER, {
142145
columns: ["Name", "Position", "Reserve", "GamesPlayed", "Goals", "Assists", "PIM", "Hits", "Toughness", "Blocks", "Take", "Give", "TOI", "DStat", "Record", "SO", "GA", "SA", "Rating", "NHLTeam"],
143146
header: {
144147
Name: "Player Name",

0 commit comments

Comments
 (0)