SPB Git

Repository Public

circonscriptions.html 10.4 KB 202 lines html

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>Forecast par circonscription — QC Élection Forecast</title>14<meta name="description" content="Le forecast des 127 circonscriptions du Québec : probabilités de victoire, circonscriptions pivots de la majorité, champs de bataille.">15<link rel="stylesheet" href="/assets/style.css">16<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>17<style>18  .prob-bar { display: flex; height: 12px; border-radius: 6px; overflow: hidden;19    gap: 1.5px; min-width: 130px; }20  .prob-bar div { min-width: 0; }21  .riding-row { cursor: pointer; }22  .riding-row.open td { background: color-mix(in srgb, var(--accent) 10%, transparent); }23  .detail-cell { padding: 0 !important; }24  .detail-inner { padding: 14px 12px 18px; display: grid;25    grid-template-columns: 1fr 1fr; gap: 18px; }26  @media (max-width: 760px) { .detail-inner { grid-template-columns: 1fr; } }27  .cat-chip { font-size: .7rem; }28  .tip-num { font-variant-numeric: tabular-nums; font-weight: 700; }29  #search { min-width: 220px; }30</style>31</head>32<body>33<header class="site"></header>34<div class="wrap">35  <div class="hero">36    <h1>Le forecast, comté par comté</h1>37    <p class="sub">Les <b>127 circonscriptions</b> : probabilités de victoire issues des simulations38      Monte Carlo (erreurs nationale, régionales et locales corrélées), circonscriptions39      <b>pivots de la majorité</b> et champs de bataille.</p>40    <p class="disclaimer">Cliquer une circonscription pour le détail complet (parts attendues,41      IC 95 %, sortant·e, candidatures, référence 2022).</p>42  </div>4344  <div class="grid three" id="kpis"></div>4546  <h2 class="section">Circonscriptions pivots de la majorité <span class="chip badge-aux">méthode des simulations</span></h2>47  <p class="section-sub">Dans chaque simulation où un parti atteint 64 sièges, la 64ᵉ circonscription48    la plus sûre de ce parti est <b>celle qui fait basculer la majorité</b>. P(pivot) = fréquence sur49    les simulations.</p>50  <div class="card" style="overflow-x:auto"><table class="data" id="tips-table"></table></div>5152  <h2 class="section">Les 127 circonscriptions</h2>53  <div class="filters">54    <input type="text" id="search" placeholder="Rechercher une circonscription…">55    <select id="f-region"><option value="">Toutes les régions</option></select>56    <select id="f-cat">57      <option value="">Toutes les catégories</option>58      <option>Chaudement disputé</option><option>Favorisé</option>59      <option>Probable</option><option>Solide</option>60    </select>61    <select id="f-sort">62      <option value="contested">Tri : plus disputées d'abord</option>63      <option value="name">Tri : alphabétique</option>64      <option value="region">Tri : par région</option>65    </select>66    <span style="color:var(--muted);font-size:.8rem" id="count"></span>67  </div>68  <div class="card" style="overflow-x:auto"><table class="data" id="ridings"></table></div>69</div>70<footer class="site"></footer>7172<script src="/assets/app.js"></script>73<script>74(async () => {75  const meta = await initShell("/circonscriptions");76  const parties = Object.keys(meta.parties).filter(p => p !== "AUT");7778  const [dd, bg] = await Promise.all([79    getJSON(`${API}/districts`), getJSON(`${API}/battlegrounds`).catch(() => null)]);80  const ridings = dd.districts;8182  /* ---- KPIs ---- */83  const byCat = {};84  for (const r of ridings) byCat[r.category] = (byCat[r.category] || 0) + 1;85  const flips = ridings.filter(r => r.incumbent_party && r.favorite !== r.incumbent_party).length;86  document.getElementById("kpis").innerHTML = [87    ["Chaudement disputées", byCat["Chaudement disputé"] || 0,88     "aucun parti au-dessus de 60 % de chances"],89    ["Sièges qui changeraient de main", flips, "favori ≠ parti sortant (médiane des simulations)"],90    ["Solides", byCat["Solide"] || 0, "favori à ≥ 95 % de chances"],91  ].map(([t, v, d]) => `<div class="card"><h3>${t}</h3>92    <div class="bignum">${v}</div><p class="note">${d}</p></div>`).join("");9394  /* ---- pivots ---- */95  const tips = (bg && bg.tipping && bg.tipping.majority_tipping) || [];96  const rByName = Object.fromEntries(ridings.map(r => [r.district, r]));97  document.getElementById("tips-table").innerHTML = tips.length98    ? `<tr><th>#</th><th>Circonscription</th><th>P(pivot de la majorité)</th><th>Favori</th><th>Région</th><th>Course</th></tr>` +99      tips.slice(0, 12).map((t, i) => {100        const r = rByName[t.district] || {};101        return `<tr><td style="color:var(--muted)">${i + 1}</td>102          <td><b>${t.district}</b></td>103          <td class="tip-num">${(t.prob_majority_tipping * 100).toFixed(1)} %</td>104          <td>${r.favorite ? `<span class="dot" style="background:${partyColor(r.favorite)}"></span>${r.favorite}` : "—"}</td>105          <td style="color:var(--muted)">${r.region || "—"}</td>106          <td>${probBar(r)}</td></tr>`;107      }).join("")108    : `<tr><td style="color:var(--muted)">Pivots disponibles au prochain run du pipeline.</td></tr>`;109110  /* ---- explorateur ---- */111  const regions = [...new Set(ridings.map(r => r.region))].sort();112  document.getElementById("f-region").innerHTML +=113    regions.map(r => `<option>${r}</option>`).join("");114115  function probBar(r) {116    if (!r.win_probs) return "";117    const entries = parties.map(p => [p, r.win_probs[p] || 0]).filter(([, v]) => v > 0.01);118    return `<div class="prob-bar">${entries.sort((a, b) => b[1] - a[1]).map(([p, v]) =>119      `<div style="flex:${v};background:${partyColor(p)}" title="${p} ${(v * 100).toFixed(0)} %"></div>`).join("")}</div>`;120  }121  const catColor = c => c === "Chaudement disputé" ? "var(--danger)" :122    c === "Favorisé" ? "var(--warn)" : c === "Probable" ? "var(--accent)" : "var(--good)";123124  let open = null;125  function render() {126    const q = document.getElementById("search").value.trim().toLowerCase();127    const fr = document.getElementById("f-region").value;128    const fcat = document.getElementById("f-cat").value;129    const sort = document.getElementById("f-sort").value;130    let rows = ridings.filter(r =>131      (!q || r.district.toLowerCase().includes(q)) &&132      (!fr || r.region === fr) && (!fcat || r.category === fcat));133    if (sort === "name") rows.sort((a, b) => a.district.localeCompare(b.district, "fr"));134    else if (sort === "region") rows.sort((a, b) =>135      a.region.localeCompare(b.region, "fr") || a.district.localeCompare(b.district, "fr"));136    else rows.sort((a, b) =>137      Math.max(...Object.values(a.win_probs)) - Math.max(...Object.values(b.win_probs)));138    document.getElementById("count").textContent = `${rows.length} / ${ridings.length}`;139    document.getElementById("ridings").innerHTML =140      `<tr><th>Circonscription</th><th>Région</th><th>Favori</th><th>P(victoire)</th><th>Course</th><th>Catégorie</th><th>Sortant</th></tr>` +141      rows.map(r => `<tr class="riding-row" data-d="${r.district}">142        <td><b>${r.district}</b>${r.is_new_2026 ? ' <span class="chip badge-aux">nouvelle</span>' : ""}</td>143        <td style="color:var(--muted)">${r.region}</td>144        <td><span class="dot" style="background:${partyColor(r.favorite)}"></span>${r.favorite}</td>145        <td class="tip-num">${(Math.max(...Object.values(r.win_probs)) * 100).toFixed(0)} %</td>146        <td>${probBar(r)}</td>147        <td><span class="chip cat-chip" style="background:color-mix(in srgb, ${catColor(r.category)} 15%, transparent);color:${catColor(r.category)}">${r.category}</span></td>148        <td style="color:var(--muted)">${r.incumbent_party || "—"}${r.incumbent_running === false ? " · retrait" : ""}</td>149      </tr>`).join("");150    document.querySelectorAll(".riding-row").forEach(tr =>151      tr.addEventListener("click", () => toggleDetail(tr)));152  }153154  async function toggleDetail(tr) {155    const name = tr.dataset.d;156    document.querySelectorAll(".detail-row").forEach(e => e.remove());157    document.querySelectorAll(".riding-row.open").forEach(e => e.classList.remove("open"));158    if (open === name) { open = null; return; }159    open = name;160    tr.classList.add("open");161    const det = await getJSON(`${API}/districts/${encodeURIComponent(name)}`);162    const f = det.forecast || {};163    const row = document.createElement("tr");164    row.className = "detail-row";165    const exp = f.expected || {};166    const ordD = parties.filter(p => exp[p] != null).sort((a, b) => exp[b] - exp[a]);167    row.innerHTML = `<td colspan="7" class="detail-cell"><div class="detail-inner">168      <div>169        <h3>Parts attendues — ${name}</h3>170        <table class="data">171          <tr><th>Parti</th><th>Attendu</th><th>IC 95 %</th><th>P(victoire)</th><th>2022</th></tr>172          ${ordD.map(p => `<tr>173            <td><span class="dot" style="background:${partyColor(p)}"></span>${p}</td>174            <td><b>${exp[p].toFixed(1)} %</b></td>175            <td style="color:var(--muted)">${f.lo95 ? f.lo95[p].toFixed(0) + "–" + f.hi95[p].toFixed(0) + " %" : "—"}</td>176            <td>${((f.win_probs || {})[p] * 100 || 0).toFixed(1)} %</td>177            <td style="color:var(--muted)">${det.baseline_2022 && det.baseline_2022[p] != null ? det.baseline_2022[p].toFixed(1) + " %" : "—"}</td>178          </tr>`).join("")}179        </table>180        <p class="note">${det.baseline_source || ""} · participation 2022 : ${det.turnout_2022 != null ? det.turnout_2022.toFixed(0) + " %" : "—"}</p>181      </div>182      <div>183        <h3>Candidatures (DGEQ)</h3>184        ${det.candidates && det.candidates.length ? `<table class="data">185          <tr><th>Candidat·e</th><th>Parti</th><th></th></tr>186          ${det.candidates.map(c => `<tr><td>${c.name}</td>187            <td><span class="dot" style="background:${partyColor(c.party)}"></span>${c.party}</td>188            <td style="color:var(--muted)">${c.is_incumbent ? "sortant·e" : ""}</td></tr>`).join("")}189        </table>` : '<p class="note">Candidatures officielles à venir (registre DGEQ).</p>'}190      </div>191    </div></td>`;192    tr.after(row);193  }194195  ["search", "f-region", "f-cat", "f-sort"].forEach(id =>196    document.getElementById(id).addEventListener(id === "search" ? "input" : "change", render));197  render();198})();199</script>200</body>201</html>202
Files e6325299f4fddaa71fa341f4d44ed851fc90f9ec
Menu

Theme