spb/qc-election
Public
1<!DOCTYPE html>2<!--3 QC Élection Forecast — Plateforme de prévision électorale du Québec 20264 Auteur : Simon-Pierre Boucher5 Contact : contact@spboucher.ai6 https://www.qc-election.com7-->8<html lang="fr">9<head>10<script>document.documentElement.dataset.theme=localStorage.getItem("qce-theme")||"dark";</script>11<meta charset="utf-8">12<meta name="viewport" content="width=device-width, initial-scale=1">13<title>Explorateur de sondages — QC Élection Forecast</title>14<link rel="stylesheet" href="/assets/style.css">15</head>16<body>17<header class="site"></header>18<div class="wrap">19 <div class="hero">20 <h1>Explorateur de sondages</h1>21 <p class="sub">Chaque sondage avec sa valeur brute, sa valeur ajustée (effets maison) et son poids dans le modèle — transparence totale.</p>22 </div>2324 <div class="filters">25 <label>Du</label><input type="date" id="f-from">26 <label>au</label><input type="date" id="f-to">27 <label>Maison</label><select id="f-pollster"><option value="">Toutes</option></select>28 <label>Affichage</label>29 <select id="f-view">30 <option value="raw">Valeurs brutes</option>31 <option value="adjusted">Valeurs ajustées</option>32 </select>33 <span id="count" style="color:var(--muted);font-size:.82rem"></span>34 </div>3536 <div class="card" style="overflow-x:auto">37 <table class="data" id="polls-table"></table>38 </div>3940 <h2 class="section">Cotes des maisons de sondage</h2>41 <p class="section-sub">Effets maison estimés sur les sondages finaux des élections passées (valeur positive = la maison a surestimé le parti), rétrécis de 50 % vers zéro et bornés à ±3 pp.</p>42 <div class="card" style="overflow-x:auto"><table class="data" id="pollsters-table"></table></div>43</div>44<footer class="site"></footer>4546<script src="/assets/app.js"></script>47<script>48(async () => {49 const meta = await initShell("/sondages");50 const parties = Object.keys(meta.parties).filter(p => p !== "AUT");51 let polls = await getJSON(`${API}/polls?limit=2000`);5253 const names = [...new Set(polls.map(p => p.pollster))].sort();54 document.getElementById("f-pollster").innerHTML +=55 names.map(n => `<option>${n}</option>`).join("");5657 function render() {58 const from = document.getElementById("f-from").value;59 const to = document.getElementById("f-to").value;60 const ps = document.getElementById("f-pollster").value;61 const view = document.getElementById("f-view").value;62 const rows = polls.filter(x =>63 (!from || x.field_end >= from) && (!to || x.field_end <= to) &&64 (!ps || x.pollster === ps));65 document.getElementById("count").textContent = `${rows.length} sondages`;66 document.getElementById("polls-table").innerHTML =67 `<tr><th>Maison</th><th>Terrain</th><th>n</th><th>MoE</th><th>Population</th>68 ${parties.map(p => `<th>${p}</th>`).join("")}<th>Poids modèle</th><th>Source</th></tr>` +69 rows.map(x => {70 const vals = view === "adjusted" ? x.adjusted : x.raw;71 return `<tr style="${x.excluded ? "opacity:.4" : ""}">72 <td>${x.pollster}${x.excluded ? ' <span class="chip badge-aux">exclu</span>' : ""}</td>73 <td>${fmtDate(x.field_end)}</td>74 <td>${x.sample_size ?? "—"}</td>75 <td>${x.moe ? "±" + x.moe : "—"}</td>76 <td>${x.population}</td>77 ${parties.map(p => `<td>${vals[p] != null ? (+vals[p]).toFixed(1) : "—"}</td>`).join("")}78 <td><div style="background:var(--card-2);border-radius:4px;height:8px;width:70px">79 <div style="background:var(--accent);height:8px;border-radius:4px;width:${(x.weight*100).toFixed(0)}%"></div></div></td>80 <td>${x.source_url ? `<a href="${x.source_url}" target="_blank" rel="noopener">${x.source_name || "lien"}</a>` : (x.source_name || "—")}</td>81 </tr>`;82 }).join("");83 }84 ["f-from", "f-to", "f-pollster", "f-view"].forEach(id =>85 document.getElementById(id).onchange = render);86 render();8788 const raters = await getJSON(`${API}/pollsters`);89 document.getElementById("pollsters-table").innerHTML =90 `<tr><th>Maison</th><th>Sondages en base</th><th>Sondages finaux évalués</th><th>EAM (pp)</th>91 ${parties.map(p => `<th>Biais ${p}</th>`).join("")}<th>Multiplicateur</th></tr>` +92 raters.map(r => {93 const rt = r.rating;94 const he = rt ? rt.house_effects : {};95 return `<tr><td>${r.name}</td><td>${r.polls_in_db}</td>96 <td>${rt ? rt.n_final_polls : "—"}</td>97 <td>${rt && rt.mae_pp != null ? rt.mae_pp : "—"}</td>98 ${parties.map(p => {99 const v = he[p];100 return `<td style="color:${v > 0.5 ? "var(--warn)" : v < -0.5 ? "var(--accent)" : "var(--muted)"}">${v != null ? (v > 0 ? "+" : "") + v.toFixed(1) : "—"}</td>`;101 }).join("")}102 <td>${rt ? rt.weight_multiplier.toFixed(2) : "0.90 (sans historique)"}</td></tr>`;103 }).join("");104})();105</script>106</body>107</html>108