/* Standings — season points grid. Drivers & Constructors, season selector. */
const { useState: useSt, useMemo: useStMemo, useEffect: useStEffect } = React;
const CELL_MODE_KEY = "fd.standings.cellMode";
function loadCellMode() {
try { return localStorage.getItem(CELL_MODE_KEY) === "position" ? "position" : "points"; }
catch { return "points"; }
}
const VIEW_KEY = "fd.standings.view";
function loadView() {
try { return localStorage.getItem(VIEW_KEY) === "graph" ? "graph" : "table"; }
catch { return "table"; }
}
function Segmented({ value, options, onChange }) {
return (
{options.map(o => {
const on = o.value === value;
return (
onChange(o.value)} style={{ cursor: "pointer", border: "none",
fontFamily: "var(--mono)", fontSize: 11.5, letterSpacing: ".1em", textTransform: "uppercase", fontWeight: 600,
padding: "8px 14px", borderRadius: 8, transition: "all .15s",
background: on ? "var(--accent)" : "transparent", color: on ? "#fff" : "var(--ink-3)" }}>{o.label}
);
})}
);
}
function Legend() {
const items = [["Win", "var(--accent)"], ["Podium", "color-mix(in srgb, var(--ink) 22%, transparent)"], ["Pole (P)", "var(--warn)"], ["Fastest Lap (F)", "#a855f7"], ["DNF", "transparent"]];
return (
{items.map(([lab, c]) => (
{lab}
))}
);
}
function cellStyle(o, mode) {
// o = {pts, pos, dnf}; mode = "points" | "position" (controls displayed text only)
let bg = "transparent", color = "var(--ink-2)", weight = 500;
if (o.dnf) return { bg, color: "var(--ink-4)", weight, txt: "DNF", small: true };
if (o.pos === 1) { bg = "color-mix(in srgb, var(--accent) 26%, transparent)"; color = "var(--ink)"; weight = 800; }
else if (o.pos <= 3) { bg = "color-mix(in srgb, var(--ink) 9%, transparent)"; color = "var(--ink)"; weight = 700; }
else if (o.pts > 0) { color = "var(--ink)"; weight = 600; }
const txt = mode === "position" ? (o.pos ? "P" + o.pos : "–") : (o.pts > 0 ? o.pts : "–");
return { bg, color, weight, txt, small: false };
}
function DriversGrid({ sid, mode }) {
const FD = window.FD;
const sd = FD.seasonData[sid];
const rounds = sd.byRound;
const grid = {};
rounds.forEach(rd => rd.order.forEach(o => { (grid[o.num] = grid[o.num] || {})[rd.rd] = { pts: o.pts, pos: o.pos, dnf: o.dnf, pole: rd.pole === o.num, fastest: !!o.fastest }; }));
const isS6 = sid === FD.currentSid;
return (
#
Driver
{rounds.map(rd => (
{isS6 && }
R{rd.rd}
))}
Pts
{sd.ranked.map((row, i) => {
const dr = FD.driverByNum[row.num];
const team = FD.teamById[FD.driverTeam(row.num, sid)];
return (
location.href = `driver.html?d=${row.num}`}
onMouseEnter={e => e.currentTarget.style.background = "color-mix(in srgb, var(--ink) 4%, transparent)"}
onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
{i + 1}
{dr.username}
{dr.code} · {team.code} · #{dr.num}
{rounds.map(rd => {
const o = grid[row.num][rd.rd];
if (!o) return · ;
const cs = cellStyle(o, mode);
return (
{cs.txt}
{o.pole && P }
{o.fastest && F }
);
})}
);
})}
);
}
function ConstructorsGrid({ sid }) {
const FD = window.FD;
const sd = FD.seasonData[sid];
const rounds = sd.byRound;
const standings = FD.teamStandings(sid).filter(t => t.roster.length);
// team points per round
const teamRound = (teamId, rd) => rd.order.filter(o => FD.driverTeam(o.num, sid) === teamId).reduce((a, o) => a + o.pts, 0);
const isS6 = sid === FD.currentSid;
return (
#
Constructor
{rounds.map(rd => (
{isS6 && }R{rd.rd}
))}
Pts
{standings.map((t, i) => (
location.href = `team.html?t=${t.id}`}
onMouseEnter={e => e.currentTarget.style.background = "color-mix(in srgb, var(--ink) 4%, transparent)"}
onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
{i + 1}
{t.name}
{t.code}
{rounds.map(rd => {
const pts = teamRound(t.id, rd);
return (
0 ? "var(--ink)" : "var(--ink-4)", fontWeight: pts >= 25 ? 700 : 500 }}>{pts > 0 ? pts : "–"}
);
})}
))}
);
}
const thBase = { fontFamily: "var(--mono)", fontSize: 10.5, letterSpacing: ".1em", textTransform: "uppercase", color: "var(--ink-4)", fontWeight: 500, padding: "13px 8px" };
const tdBase = { padding: "9px 8px", verticalAlign: "middle" };
// Round a max value up to a clean axis bound (1/2/5 × 10ⁿ).
function niceMax(v) {
if (v <= 0) return 10;
const pow = Math.pow(10, Math.floor(Math.log10(v)));
const n = v / pow;
const step = n <= 1 ? 1 : n <= 2 ? 2 : n <= 5 ? 5 : 10;
return step * pow;
}
// Cumulative-points line chart. Pure/dumb: it knows nothing about window.FD.
// series: [{ id, label, color, onClick, cum: [v0, v1, …, vN] }] where cum[0] = 0 (pre-season).
function PointsGraph({ series, roundLabels }) {
const [hover, setHover] = useSt(null);
const n = roundLabels.length;
if (n === 0) {
return (
No rounds completed yet
);
}
const W = 1000, H = 460, padL = 46, padR = 100, padT = 18, padB = 36;
const last = s => s.cum[s.cum.length - 1];
const maxV = niceMax(Math.max(1, ...series.map(last)));
const x = i => padL + (i / n) * (W - padL - padR);
const y = v => (H - padB) - (v / maxV) * (H - padB - padT);
const yTicks = 5;
// Distinguish series that share a color (e.g. teammates): solid line for the
// first of each color, dashed variants for the rest.
const dashPatterns = [null, "7 5", "2 5", "11 5 2 5", "7 5 2 5"];
const dashById = {};
const colorSeen = {};
series.forEach(s => {
const k = colorSeen[s.color] || 0;
dashById[s.id] = dashPatterns[k % dashPatterns.length];
colorSeen[s.color] = k + 1;
});
// Declutter end labels: place top-to-bottom, enforce a minimum vertical gap.
const minGap = 15;
const ends = series.map(s => ({ id: s.id, color: s.color, label: s.label, v: last(s), ly: y(last(s)) }))
.sort((a, b) => a.ly - b.ly);
for (let i = 1; i < ends.length; i++) {
if (ends[i].ly - ends[i - 1].ly < minGap) ends[i].ly = ends[i - 1].ly + minGap;
}
return (
setHover(null)}>
{/* y gridlines + labels */}
{Array.from({ length: yTicks + 1 }, (_, t) => {
const v = (maxV * t) / yTicks, yy = y(v);
return (
{Math.round(v)}
);
})}
{/* x round labels */}
{roundLabels.map((lab, i) => (
{lab}
))}
{/* series lines (hit area + visible line + end dot) */}
{series.map(s => {
const pts = s.cum.map((v, i) => `${x(i)},${y(v)}`).join(" ");
const dim = hover && hover !== s.id;
return (
setHover(s.id)} onClick={s.onClick}>
{s.label}: {last(s)} pts
);
})}
{/* end labels */}
{ends.map(e => {
const dim = hover && hover !== e.id;
return (
setHover(e.id)} onClick={series.find(s => s.id === e.id).onClick}>{e.label}
);
})}
);
}
function driverPointsSeries(FD, sid) {
const sd = FD.seasonData[sid];
return sd.ranked.map(row => {
const dr = FD.driverByNum[row.num];
const team = FD.teamById[FD.driverTeam(row.num, sid)];
let acc = 0; const cum = [0];
sd.byRound.forEach(rd => { const o = rd.order.find(x => x.num === row.num); acc += o ? o.pts : 0; cum.push(acc); });
return { id: row.num, label: dr.code, color: team.color, onClick: () => location.href = `driver.html?d=${row.num}`, cum };
});
}
function teamPointsSeries(FD, sid) {
const sd = FD.seasonData[sid];
return FD.teamStandings(sid).filter(t => t.roster.length).map(t => {
let acc = 0; const cum = [0];
sd.byRound.forEach(rd => { acc += rd.order.filter(o => FD.driverTeam(o.num, sid) === t.id).reduce((a, o) => a + o.pts, 0); cum.push(acc); });
return { id: t.id, label: t.code, color: t.color, onClick: () => location.href = `team.html?t=${t.id}`, cum };
});
}
function StandingsPage() {
const FD = window.FD;
const [sid, setSid] = useSt(FD.currentSid);
const [tab, setTab] = useSt("drivers");
const [cellMode, setCellMode] = useSt(loadCellMode);
const [view, setView] = useSt(loadView);
useStEffect(() => { try { localStorage.setItem(CELL_MODE_KEY, cellMode); } catch {} }, [cellMode]);
useStEffect(() => { try { localStorage.setItem(VIEW_KEY, view); } catch {} }, [view]);
const season = FD.seasons.find(s => s.id === sid);
// Graph shows cumulative points, so it's available on Teams (always points) and on
// Drivers only when the Points cell-mode is selected (per issue #29).
const graphAvailable = tab === "constructors" || (tab === "drivers" && cellMode === "points");
const showGraph = graphAvailable && view === "graph";
const roundLabels = FD.seasonData[sid].byRound.map(rd => "R" + rd.rd);
return (
({ value: s.id, label: s.label.replace("Season ", "S") }))} />
{tab === "drivers" && }
{graphAvailable && }
} />
{showGraph
?
: tab === "drivers" ?
:
}
{!showGraph && }
{showGraph
? <>Cumulative championship points across the season. Hover a line to highlight it; click for the full profile.>
: <>Each cell shows {tab === "drivers" && cellMode === "position" ? "the finishing position" : "points scored"} that round. {season.status === "live" ? `${season.rounds - season.completed} rounds remain this season.` : "Season complete."} Click any row for the full profile.>}
);
}
ReactDOM.createRoot(document.getElementById("root")).render( );