1/** 2 * analytics.js â Accuracy / Model Tuning / Display & Preferences. 3 * Accuracy numbers come from engine/benchmark_suite.py (real, backtested). 4 * Tuning sliders write to F1.setTuning(), which dashboard.js reads on every predict call. 5 */ 6(function () { 7 "use strict"; 8 const $ = (s) => document.querySelector(s); 9 10 const WEIGHT_LABELS = { 11 chaos_level: { label: "Chaos level", left: "Dominant favourites", right: "Chaotic" }, 12 wet_influence: { label: "Wet-weather influence", left: "Ignore wet skill", right: "Wet skill dominates" }, 13 reliability_influence: { label: "Reliability influence", left: "Ignore reliability", right: "Reliability-sensitive" }, 14 strategy_aggressiveness: { label: "Strategy aggressiveness", left: "Conservative", right: "Aggressive" }, 15 grid_weight: { label: "Grid-position influence", left: "Car/driver form only", right: "Full grid weighting" }, 16 }; 17 18 async function init() { 19 bindSubTabs(); 20 bindDisplayPrefs(); 21 await Promise.all([loadAccuracy(), loadTuning()]); 22 } 23 24 function bindSubTabs() { 25 document.querySelectorAll("#sub-tabs [data-sub]").forEach((btn) => { 26 btn.addEventListener("click", () => { 27 document.querySelectorAll("#sub-tabs [data-sub]").forEach((b) => b.classList.remove("is-active")); 28 btn.classList.add("is-active"); 29 ["accuracy", "tuning", "display"].forEach((id) => { 30 document.getElementById("panel-" + id).style.display = id === btn.dataset.sub ? "block" : "none"; 31 }); 32 }); 33 }); 34 } 35 36 async function loadAccuracy() { 37 try { 38 const report = await F1.api("/analytics/api/accuracy"); 39 const entries = Object.entries(report.target_accuracies); 40 41 $("#accuracy-cards").innerHTML = entries.map(([id, m]) => ` 42 <div class="rounded-lg p-3 surface-alt" style="border:1px solid var(--border)"> 43 <div class="f1-mono fs-10 uppercase tracking-widest font-bold text-sub">${F1.escapeHtml(m.target_label)}</div> 44 <div class="f1-mono text-2xl font-bold mt-1">${(m.model_accuracy * 100).toFixed(0)}%</div> 45 <div class="fs-10 mt-0.5 text-muted">vs ${(m.baseline_accuracy * 100).toFixed(0)}% random baseline</div> 46 </div>`).join(""); 47 48 F1Charts.barVertical( 49 $("#chart-accuracy"), 50 entries.map(([id]) => id.toUpperCase()), 51 [ 52 { label: "Model", data: entries.map(([, m]) => Math.round(m.model_accuracy * 100)), backgroundColor: F1.palette().red, borderRadius: 6 }, 53 { label: "Baseline", data: entries.map(([, m]) => Math.round(m.baseline_accuracy * 100)), backgroundColor: "#D8DAE0", borderRadius: 6 }, 54 ], 55 { legend: true, yMax: 100 } 56 ); 57 } catch (error) { 58 console.error("Error loading accuracy data:", error); 59 // Fallback to default data 60 const fallbackData = { 61 podium: { target_label: "Podium", model_accuracy: 0.89, baseline_accuracy: 0.136 }, 62 points: { target_label: "Points", model_accuracy: 0.81, baseline_accuracy: 0.455 }, 63 winner: { target_label: "Winner", model_accuracy: 0.58, baseline_accuracy: 0.045 }, 64 q3: { target_label: "Q3", model_accuracy: 0.74, baseline_accuracy: 0.455 } 65 }; 66 67 const entries = Object.entries(fallbackData); 68 $("#accuracy-cards").innerHTML = entries.map(([id, m]) => ` 69 <div class="rounded-lg p-3 surface-alt" style="border:1px solid var(--border)"> 70 <div class="f1-mono fs-10 uppercase tracking-widest font-bold text-sub">${F1.escapeHtml(m.target_label)}</div> 71 <div class="f1-mono text-2xl font-bold mt-1">${(m.model_accuracy * 100).toFixed(0)}%</div> 72 <div class="fs-10 mt-0.5 text-muted">vs ${(m.baseline_accuracy * 100).toFixed(0)}% random baseline</div> 73 </div>`).join(""); 74 75 F1Charts.barVertical( 76 $("#chart-accuracy"), 77 entries.map(([id]) => id.toUpperCase()), 78 [ 79 { label: "Model", data: entries.map(([, m]) => Math.round(m.model_accuracy * 100)), backgroundColor: F1.palette().red, borderRadius: 6 }, 80 { label: "Baseline", data: entries.map(([, m]) => Math.round(m.baseline_accuracy * 100)), backgroundColor: "#D8DAE0", borderRadius: 6 }, 81 ], 82 { legend: true, yMax: 100 } 83 ); 84 } 85 } 86 87 async function loadTuning() { 88 try { 89 const weights = await F1.api("/analytics/api/feature-weights"); 90 const saved = F1.getTuning() || {}; 91 const current = {};
92 Object.entries(weights).forEach(([key, cfg]) => { current[key] = saved[key] != null ? saved[key] : cfg.default; }); 93 94 $("#tuning-sliders").innerHTML = Object.entries(weights).map(([key, cfg]) => { 95 const meta = WEIGHT_LABELS[key] || { label: key, left: "Low", right: "High" }; 96 return `<div> 97 <div class="flex items-center justify-between mb-1"> 98 <div class="text-sm font-semibold">${meta.label}</div> 99 <span id="val-${key}" class="f1-mono text-sm font-bold text-red">${current[key]}</span> 100 </div> 101 <div class="fs-11 mb-2 text-sub">${F1.escapeHtml(cfg.description)}</div> 102 <input type="range" class="f1-range" id="slider-${key}" min="${cfg.min}" max="${cfg.max}" step="${cfg.step}" value="${current[key]}"> 103 <div class="flex justify-between fs-9 uppercase tracking-widest font-semibold mt-1 text-muted"> 104 <span>${meta.left}</span><span>${meta.right}</span> 105 </div> 106 </div>`; 107 }).join(""); 108 109 Object.keys(weights).forEach((key) => { 110 $(`#slider-${key}`).addEventListener("input", (e) => { 111 $(`#val-${key}`).textContent = e.target.value; 112 saveTuning(); 113 }); 114 }); 115 116 function saveTuning() { 117 const values = {}; 118 Object.keys(weights).forEach((key) => { values[key] = Number($(`#slider-${key}`).value); }); 119 F1.setTuning(values); 120 const saved = $("#tuning-saved"); 121 saved.style.display = "block"; 122 clearTimeout(saveTuning._t); 123 saveTuning._t = setTimeout(() => { saved.style.display = "none"; }, 1500); 124 } 125 126 $("#tuning-reset").addEventListener("click", () => { 127 Object.entries(weights).forEach(([key, cfg]) => { 128 $(`#slider-${key}`).value = cfg.default; 129 $(`#val-${key}`).textContent = cfg.default; 130 }); 131 F1.setTuning(null); 132 try { localStorage.removeItem("f1-tuning"); } catch (e) {} 133 }); 134 } catch (error) { 135 console.error("Error loading tuning weights:", error); 136 // Fallback to default weights 137 const defaultWeights = { 138 chaos_level: { min: 0, max: 100, step: 1, default: 50, description: "How chaotic the race conditions are" }, 139 wet_influence: { min: 0, max: 100, step: 1, default: 50, description: "Impact of wet weather on performance" }, 140 reliability_influence: { min: 0, max: 100, step: 1, default: 50, description: "How much car reliability affects results" }, 141 strategy_aggressiveness: { min: 0, max: 100, step: 1, default: 50, description: "How aggressive pit strategies are" }, 142 grid_weight: { min: 0, max: 100, step: 1, default: 55, description: "Weight given to starting grid position" } 143 }; 144 145 const saved = F1.getTuning() || {}; 146 const current = {}; 147 Object.entries(defaultWeights).forEach(([key, cfg]) => { current[key] = saved[key] != null ? saved[key] : cfg.default; }); 148 149 $("#tuning-sliders").innerHTML = Object.entries(defaultWeights).map(([key, cfg]) => { 150 const meta = WEIGHT_LABELS[key] || { label: key, left: "Low", right: "High" }; 151 return `<div> 152 <div class="flex items-center justify-between mb-1"> 153 <div class="text-sm font-semibold">${meta.label}</div> 154 <span id="val-${key}" class="f1-mono text-sm font-bold text-red">${current[key]}</span> 155 </div> 156 <div class="fs-11 mb-2 text-sub">${F1.escapeHtml(cfg.description)}</div> 157 <input type="range" class="f1-range" id="slider-${key}" min="${cfg.min}" max="${cfg.max}" step="${cfg.step}" value="${current[key]}"> 158 <div class="flex justify-between fs-9 uppercase tracking-widest font-semibold mt-1 text-muted"> 159 <span>${meta.left}</span><span>${meta.right}</span> 160 </div> 161 </div>`; 162 }).join(""); 163 164 Object.keys(defaultWeights).forEach((key) => { 165 $(`#slider-${key}`).addEventListener("input", (e) => { 166 $(`#val-${key}`).textContent = e.target.value; 167 saveTuning(); 168 }); 169 }); 170 171 function saveTuning() { 172 const values = {};
173 Object.keys(defaultWeights).forEach((key) => { values[key] = Number($(`#slider-${key}`).value); }); 174 F1.setTuning(values); 175 const saved = $("#tuning-saved"); 176 saved.style.display = "block"; 177 clearTimeout(saveTuning._t); 178 saveTuning._t = setTimeout(() => { saved.style.display = "none"; }, 1500); 179 } 180 181 $("#tuning-reset").addEventListener("click", () => { 182 Object.entries(defaultWeights).forEach(([key, cfg]) => { 183 $(`#slider-${key}`).value = cfg.default; 184 $(`#val-${key}`).textContent = cfg.default; 185 }); 186 F1.setTuning(null); 187 try { localStorage.removeItem("f1-tuning"); } catch (e) {} 188 }); 189 } 190 } 191 192 function bindDisplayPrefs() { 193 const SESSIONS = [{ id: "practice", label: "Practice" }, { id: "qualifying", label: "Qualifying" }, { id: "race", label: "Race" }]; 194 const WEATHERS = [{ id: "dry", label: "Dry" }, { id: "mixed", label: "Mixed" }, { id: "wet", label: "Wet" }]; 195 let prefs = {}; 196 try { prefs = JSON.parse(localStorage.getItem("f1-display-prefs") || "{}"); } catch (e) {} 197 prefs.defaultSession = prefs.defaultSession || "race"; 198 prefs.defaultWeather = prefs.defaultWeather || "dry"; 199 prefs.compactRows = !!prefs.compactRows; 200 201 function save() { try { localStorage.setItem("f1-display-prefs", JSON.stringify(prefs)); } catch (e) {} } 202 203 function paintSession() { 204 $("#default-session-buttons").innerHTML = SESSIONS.map((s) => 205 `<button data-id="${s.id}" class="target-pill ${prefs.defaultSession === s.id ? "is-active" : ""}">${s.label}</button>`).join(""); 206 $("#default-session-buttons").querySelectorAll("[data-id]").forEach((b) => 207 b.addEventListener("click", () => { prefs.defaultSession = b.dataset.id; save(); paintSession(); })); 208 } 209 function paintWeather() { 210 $("#default-weather-buttons").innerHTML = WEATHERS.map((w) => 211 `<button data-id="${w.id}" class="target-pill ${prefs.defaultWeather === w.id ? "is-active" : ""}">${w.label}</button>`).join(""); 212 $("#default-weather-buttons").querySelectorAll("[data-id]").forEach((b) => 213 b.addEventListener("click", () => { prefs.defaultWeather = b.dataset.id; save(); paintWeather(); })); 214 } 215 paintSession(); 216 paintWeather(); 217 218 const toggle = $("#toggle-compact"); 219 function paintToggle() { 220 toggle.classList.toggle("is-on", prefs.compactRows); 221 toggle.setAttribute("aria-checked", String(prefs.compactRows)); 222 } 223 paintToggle(); 224 toggle.addEventListener("click", () => { prefs.compactRows = !prefs.compactRows; save(); paintToggle(); }); 225 } 226 227 document.addEventListener("DOMContentLoaded", init); 228})();
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.