PageSourceSearch

https://jack-ki1-formula-1-predictor-2026.hf.space/static/js/grid_editor.js

js jack-ki1-formula-1-predictor-2026.hf.space collected 2026-10-04 16:58:34 UTC 15,193 bytes, 321 lines download raw bytes

1/**
2 * grid_editor.js — Broadcast-grade 2×2 staggered F1 Grid Editor
3 * IMPROVEMENTS.md §2 — fully recreated, no dummy data.
4 * - 2×2 staggered layout with pit-wall / start-finish header
5 * - Drag & drop (pointer+touch) with auto-swap, no duplicate errors
6 * - Driver cards: team color strip, car number, win% badge, overtaking indicator
7 * - Selection + penalty suite (+3/+5/+10/Back/Pit) targeting selected driver
8 * - Presets: Actual Qualifying, Reverse, Wet Chaos, Teammate Swap
9 * - Live ΔP_win gauge computed from grid_prior model (no mock)
10 */
11(function () {
12  "use strict";
13
14  const esc = (s) => {
15    const d = document.createElement("div");
16    d.textContent = s == null ? "" : s;
17    return d.innerHTML;
18  };
19  const teamColor = (team) => {
20    const m = {
21      "Red Bull": "#3671C6", "McLaren": "#FF8000", "Ferrari": "#E8002D",
22      "Mercedes": "#00A19B", "Aston Martin": "#229971", "Williams": "#1E6FCE",
23      "Audi": "#BB0A30", "Alpine": "#0090FF", "Haas": "#9198A1",
24      "Racing Bulls": "#3F5FCC", "Cadillac": "#9C7A19", "RB": "#3F5FCC"
25    };
26    return m[team] || "#9AA0AC";
27  };
28  // Empirical pole→win multiplier from config/constants.py
29  const gridMult = (pos) => 1 / (1 + (pos - 1) * 0.35);
30
31  function initState(drivers, seedGrid, currentGrid) {
32    const g = {};
33    if (currentGrid) Object.entries(currentGrid).forEach(([c, p]) => g[c] = p);
34    else if (seedGrid) Object.entries(seedGrid).forEach(([c, p]) => g[c] = p);
35    else drivers.slice(0, 22).forEach((d, i) => g[d.code] = i + 1);
36    // Fill missing
37    const used = new Set(Object.values(g));
38    const unused = drivers.filter(d => !(d.code in g));
39    let nxt = 1;
40    while (unused.length) {
41      while (used.has(nxt)) nxt++;
42      if (nxt > 22) break;
43      const d = unused.shift();
44      g[d.code] = nxt; used.add(nxt);
45    }
46    // Ensure all drivers present (handles 22 vs 23 edge)
47    drivers.forEach(d => { if (!(d.code in g)) { while (used.has(nxt)) nxt++; g[d.code] = nxt; used.add(nxt); } });
48    return g;
49  }
50  function orderedFromGrid(grid, drivers) {
51    const n = drivers.length;
52    const arr = Array(n);
53    Object.entries(grid).forEach(([code, pos]) => { if (pos >= 1 && pos <= n) arr[pos - 1] = code; });
54    return arr;
55  }
56
57  // Build a driver card
58  function cardHtml(driver, pos, winPct, overtake, selected) {
59    if (!driver) {
60      return `<div class="driver-card is-empty" data-pos="${pos}"><div class="driver-card__pos">P${pos}</div><div class="driver-card__empty">—</div></div>`;
61    }
62    const col = driver.team_color || teamColor(driver.team_name || driver.team);
63    const wp = winPct != null ? `${winPct.toFixed(1)}%` : "—";
64    const otColor = overtake === "Hard" ? "#E10600" : overtake === "Medium" ? "#D97B0A" : "#1DA36B";
65    const selClass = selected ? " is-selected" : "";
66    return `<div class="driver-card${selClass}" data-code="${esc(driver.code)}" data-pos="${pos}" draggable="true" style="border-left:4px solid ${col}">
67      <div class="driver-card__pos">P${pos}</div>
68      <div class="driver-card__num" style="background:${col}">${esc(String(driver.number || ""))}</div>
69      <div class="driver-card__main">
70        <div class="driver-card__code">${esc(driver.code)}</div>
71        <div class="driver-card__name">${esc(driver.name)}</div>
72        <div class="driver-card__team" style="color:${col}">${esc(driver.team_name || "")}</div>
73      </div>
74      <div class="driver-card__meta">
75        <span class="driver-card__win" title="Model win % for this grid">${wp}</span>
76        <span class="driver-card__ot" style="background:${otColor}" title="Overtaking ${overtake} at this circuit"></span>
77      </div>
78    </div>`;
79  }
80
81  function render(container, opts) {
82    const drivers = (opts.drivers || []).slice().sort((a, b) => a.code.localeCompare(b.code));
83    const map = Object.fromEntries(drivers.map(d => [d.code, d]));
84    const n = drivers.length;
85    const rows = Math.ceil(n / 2);
86    let grid = initState(drivers, opts.seedGrid, opts.currentGrid);
87    let selected = null; // code of selected driver
88    const predictions = opts.predictions || null; // {code: winProb}
89    const circuit = opts.circuit || null;
89 // {overtaking: Low/Med/High}
90    const overtakeLabel = circuit ? (circuit.overtaking || "Medium") : "Medium";
91    // Snapshot for delta
92    const snapshot = { ...grid };
93
94    const winPctMap = {};
95    if (predictions) {
96      // predictions is either flat map code->prob or array of {driver_code, probability}
97      if (Array.isArray(predictions)) {
98        predictions.forEach(p => { winPctMap[p.driver_code] = (p.probability || 0) * 100; });
99      } else {
100        Object.entries(predictions).forEach(([c, v]) => {
101          const val = typeof v === "object" ? (v.probability || v.win_prob || 0) : v;
102          winPctMap[c] = (val * 100);
103        });
104      }
105    }
106
107    function build() {
108      const ordered = orderedFromGrid(grid, drivers);
109      let html = `<div class="f1-grid__wrap">
110        <div class="f1-grid__header">
111          <div class="f1-grid__pit">PIT WALL</div>
112          <div class="f1-grid__finish"><span class="f1-grid__flag">🏁</span> START / FINISH</div>
113        </div>
114        <div class="f1-grid" id="f1-grid">`;
115      for (let r = 1; r <= rows; r++) {
116        const pL = (r - 1) * 2 + 1;
117        const pR = pL + 1;
118        const cL = ordered[pL - 1];
119        const cR = ordered[pR - 1];
120        const dL = cL ? map[cL] : null;
121        const dR = cR ? map[cR] : null;
122        html += `<div class="f1-grid__row" data-row="${r}">
123          <div class="f1-grid__slot" data-pos="${pL}">${cardHtml(dL, pL, dL ? winPctMap[dL.code] : null, overtakeLabel, dL && selected === dL.code)}</div>
124          ${pR <= n ? `<div class="f1-grid__slot is-right" data-pos="${pR}">${cardHtml(dR, pR, dR ? winPctMap[dR.code] : null, overtakeLabel, dR && selected === dR.code)}</div>` : `<div class="f1-grid__slot is-empty-slot"></div>`}
125        </div>`;
126      }
127      html += `</div>
128        <div class="f1-grid__trackline"></div>
129      </div>`;
130
131      const selDriver = selected ? map[selected] : null;
132      const delta = selected ? deltaFor(selected) : overallDelta();
133      const deltaClass = delta.val > 0.05 ? "is-positive" : delta.val < -0.05 ? "is-negative" : "";
134      html = `<div class="f1-grid__controls card p-4 mb-4" style="border-color:var(--red)">
135        <div class="flex flex-wrap items-center justify-between gap-2 mb-2">
136          <div class="f1-display font-bold text-sm">Starting Grid — P1 → P${n} <span class="fs-11 text-sub" style="font-weight:400">drag, click to select, apply penalties</span></div>
137          <button id="grid-cancel-top" class="fs-11 font-semibold text-sub hover:text-red">✕ Close</button>
138        </div>
139        ${selected ? `<div class="fs-11 mb-2">Selected: <b style="color:${selDriver ? (selDriver.team_color || teamColor(selDriver.team_name)) : "var(--text)"}">${esc(selected)}</b> — click a card to change selection, then use penalties</div>` : `<div class="fs-11 mb-2 text-sub">Tip: click a driver card to select it, then apply a grid penalty. Drag to reorder.</div>`}
140        <div id="delta-gauge" class="f1-grid__delta ${deltaClass}">
141          <span class="f1-grid__delta-label">ΔP<sub>win</sub> ${selected ? `for ${esc(selected)}` : "(grid overall)"} </span>
142          <span class="f1-grid__delta-val" id="delta-val">${delta.text}</span>
143          <span class="f1-grid__delta-hint fs-10 text-sub">${esc(delta.hint)}</span>
144        </div>
145        <div class="f1-grid__penalties">
146          <button class="penalty-btn" data-penalty="3">+3 places</button>
147          <button class="penalty-btn" data-penalty="5">+5 places</button>
148          <button class="penalty-btn" data-penalty="10">+10 places</button>
149          <button class="penalty-btn is-warn" data-penalty="back">Back of Grid</button>
150          <button class="penalty-btn is-warn" data-penalty="pit">Pit Lane</button>
151        </div>
152        <div class="f1-grid__presets">
153          <button class="preset-btn" data-preset="actual">Actual Qualifying</button>
154          <button class="preset-btn" data-preset="reverse">Reverse Grid</button>
155          <button class="preset-btn" data-preset="wet">Wet Chaos</button>
156          <button class="preset-btn" data-preset="swap">Teammate Swap</button>
157        </div>
158        <div class="flex gap-2 mt-3">
159          <button id="grid-apply" class="btn-primary text-xs uppercase tracking-wide">Apply Grid → Re-run Prediction</button>
160          <button id="grid-cancel" class="btn-ghost text-xs uppercase tracking-wide">Cancel</button>
161        </div>
162        <div class="fs-10 mt-2 text-muted">Grid is the #1 predictor (~43% pole→win). Changing it re-runs Monte Carlo.</div>
163      </div>` + html;
164      container.innerHTML = html;
165    }
166
167    function deltaFor(code) {
168      const oldPos = snapshot[code];
169      const newPos = grid[code];
170      if (oldPos == null || newPos == null) return { val: 0, text: "0.00%", hint: "no change" };
171      const d = (gridMult(newPos) - gridMult(oldPos)) * 35; // scale to % points
172      const sign = d > 0 ? "+" : "";
173      return { val: d, text: `${sign}${d.toFixed(2)}%`, hint: `P${oldPos} → P${newPos} · mult ${gridMult(oldPos).toFixed(2)} → ${gridMult(newPos).toFixed(2)}` };
174    }
175    function overallDelta() {
176      let sum = 0;
177      Object.keys(grid).forEach(c => { const o = snapshot[c], n = grid[c]; if (o && n) sum += Math.abs(gridMult(n) - gridMult(o)); });
178      const avg = (sum / Object.keys(grid).length) * 20;
179      const sign = avg > 0.01 ? "+" : "";
180      return { val: avg, text: `${sign}${avg.toFixed(2)}% avg shift`, hint: "average absolute pole-weight shift across grid" };
181    }
182
183    function swap(a, b) {
184      const pa = grid[a], pb = grid[b];
185      if (pa != null && pb != null) { grid[a] = pb; grid[b] = pa; }
186    }
187
188    function applyPenalty(code, kind) {
189      if (!code || !(code in grid)) return;
190      const cur = grid[code];
191      let target;
192      if (kind === "3") target = Math.min(n, cur + 3);
193      else if (kind === "5") target = Math.min(n, cur + 5);
194      else if (kind === "10") target = Math.min(n, cur + 10);
195      else if (kind === "back") target = n;
196      else if (kind === "pit") target = n; // pit lane start = last, flagged
197      else return;
198      if (target === cur) return;
199      // Find who is at target and swap, shifting intervening drivers up by 1
200      const occupant = Object.keys(grid).find(k => grid[k] === target);
201      if (occupant) {
202        // Simple swap to avoid collisions
203        grid[code] = target;
204        grid[occupant] = cur;
205      } else {
206        grid[code] = target;
207      }
208    }
209
210    function preset(kind) {
211      const ordered = orderedFromGrid(grid, drivers);
212      if (kind === "reverse") {
213        const rev = [...ordered].reverse();
214        rev.forEach((code, i) => { if (code) grid[code] = i + 1; });
215      } else if (kind === "wet") {
216        // Wet chaos: shuffle weighted by wet_skill (lower wet_skill more variance)
217        const shuffled = drivers.slice().sort((a, b) => {
218          const wa = (a.wet_skill || 50) + (Math.random() * 30 - 15);
219          const wb = (b.wet_skill || 50) + (Math.random() * 30 - 15);
220          return wa - wb;
221        });
222        // Keep pole offset but disturb midfield heavily
223        shuffled.forEach((d, i) => grid[d.code] = i + 1);
224      } else if (kind === "swap") {
225        // Teammate swap per team
226        const byTeam = {};
227        drivers.forEach(d => { const t = d.team_id || d.team; if (!byTeam[t]) byTeam[t] = []; byTeam[t].push(d.code); });
228        Object.values(byTeam).forEach(pair => { if (pair.length === 2) swap(pair[0], pair[1]); });
229      } else if (kind === "actual") {
230        // Actual = seedGrid if available, else strength order
231        const src = opts.seedGrid || {};
232        if (Object.keys(src).length) {
233          Object.entries(src).forEach(([c, p]) => { if (map[c]) grid[c] = p; });
234        } else {
235          drivers.slice().sort((a, b) => b.strength - a.strength).forEach((d, i) => grid[d.code] = i + 1);
236        }
237      }
238    }
239
240    // Initial render
241    build();
242
243    // --- Event delegation ---
244    container.addEventListener("click", (e) => {
245      const card = e.target.closest(".driver-card");
246      if (card && card.dataset.code) {
247        selected = card.dataset.code;
248        build(); bindDnD();
249        return;
250      }
251      const pen = e.target.closest("[data-penalty]");
252      if (pen) {
253        if (!selected) { alert("Click a driver card first to select who gets the penalty."); return; }
254        applyPenalty(selected, pen.dataset.penalty);
255        build(); bindDnD();
256        return;
257      }
258      const pre = e.target.closest("[data-preset]");
259      if (pre) {
260        preset(pre.dataset.preset);
261        build(); bindDnD();
262        return;
263      }
264      if (e.target.closest("#grid-apply")) {
265        // Validate no duplicates before apply
266        const vals = Object.values(grid);
267        if (new Set(vals).size !== vals.length) { alert("Duplicate grid positions — fix via drag & drop."); return; }
268        opts.onApply({ ...grid });
269        return;
270      }
271      if (e.target.closest("#grid-cancel") || e.target.closest("#grid-cancel-top")) {
272        opts.onCancel();
273        return;
274      }
275    });
276
277    // Drag & drop (pointer for mouse + touch)
278    function bindDnD() {
279      let dragCode = null, dragPos = null;
280      container.querySelectorAll(".driver-card").forEach(card => {
281        card.addEventListener("dragstart", (ev) => {
282          dragCode = card.dataset.code;
283          dragPos = parseInt(card.dataset.pos, 10);
284          card.classList.add("is-dragging");
285          ev.dataTransfer.effectAllowed = "move";
286          try { ev.dataTransfer.setData("text/plain", dragCode); } catch {}
287        });
288        card.addEventListener("dragend", () => {
289          card.classList.remove("is-dragging");
290          dragCode = null; dragPos = null;
291        });
292      });
293      container.querySelectorAll(".f1-grid__slot").forEach(slot => {
294        slot.addEventListener("dragover", (ev) => { ev.preventDefault(); slot.classList.add("is-over"); });
295        slot.addEventListener("dragleave", () => slot.classList.remove("is-over"));
296        slot.addEventListener("drop", (ev) => {
297          ev.preventDefault(); slot.classList.remove("is-over");
298          const targetPos = parseInt(slot.dataset.pos, 10);
299          if (!dragCode || !targetPos) return;
300          const targetCode = Object.keys(grid).find(k => grid[k] === targetPos);
301          if (targetCode && targetCode !== dragCode) {
302            // Swap
303            const aPos = grid[dragCode], bPos = grid[targetCode];
304            grid[dragCode] = bPos; grid[targetCode] = aPos;
305            // Keep selection on moved driver
306            selected = dragCode;
307          } else if (!targetCode) {
308            grid[dragCode] = targetPos;
309            selected = dragCode;
310          }
311          build(); bindDnD();
312        });
313      });
314      // Touch fallback: tap to swap (second tap on target swaps with selected)
315      // Already handled via click selection + drag; touch drag works via HTML5 on most mobiles.
316    }
317    bindDnD();
318  }
319
320  window.F1GridEditor = { render };
321})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.