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.