spb/qc-election
Public
Python 66.6%
HTML 24.8%
CSS 4.9%
JavaScript 3.6%
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>Le point du jour — QC Élection Forecast</title>14<meta name="description" content="Le forecast du jour de l'élection québécoise 2026 : probabilités datées, ce qui a changé depuis hier, attention web, nouveaux signaux.">15<link rel="icon" type="image/svg+xml" href="/assets/img/favicon.svg">16<link rel="icon" type="image/png" sizes="64x64" href="/assets/img/favicon-64.png">17<link rel="apple-touch-icon" href="/assets/img/apple-touch-icon.png">18<meta property="og:site_name" content="QC Élection Forecast">19<meta property="og:type" content="website">20<meta property="og:title" content="Le point du jour — QC Élection Forecast">21<meta property="og:description" content="Le forecast du jour de l'élection québécoise 2026 : probabilités datées, ce qui a changé depuis hier, attention web, nouveaux signaux.">22<meta property="og:image" content="https://www.qc-election.com/assets/img/og.png">23<meta property="og:image:width" content="1200">24<meta property="og:image:height" content="630">25<meta property="og:locale" content="fr_CA">26<meta name="twitter:card" content="summary_large_image">27<meta name="twitter:title" content="Le point du jour — QC Élection Forecast">28<meta name="twitter:image" content="https://www.qc-election.com/assets/img/og.png">29<meta name="theme-color" content="#0a0d14">30<link rel="stylesheet" href="/assets/style.css">31<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>32<style>33 .att-bar { height: 10px; border-radius: 6px; display: flex; overflow: hidden;34 margin: 8px 0 4px; }35 .mini-spark { height: 34px; }36 .synth-badge { border: 1px dashed var(--muted); border-radius: 8px; padding: 2px 8px;37 font-size: 11px; color: var(--muted); }38 .tick { display: flex; gap: 10px; align-items: baseline; padding: 8px 0;39 border-bottom: 1px solid var(--line); font-size: .88rem; line-height: 1.5; }40 .tick:last-child { border-bottom: 0; }41 .tick .t { color: var(--muted); white-space: nowrap; font-size: 12px; min-width: 86px; }42 #attention-card td { white-space: nowrap; }43 #attention-card td:last-child { min-width: 200px; width: 40%; }44 .day-lede { color: var(--ink-2); font-size: 1.02rem; line-height: 1.65; margin: 0;45 max-width: 860px; font-variant-numeric: tabular-nums; }46</style>47</head>48<body>49<header class="site"></header>50<div class="wrap">51 <div class="hero">52 <div class="day-date" id="day-date">—</div>53 <h1 class="headline" id="headline">Chargement du point du jour…</h1>54 <p class="day-lede sub" id="day-sub"></p>55 <p class="disclaimer">Snapshot daté et archivé — chaque affirmation renvoie au run du modèle56 (<a href="/methodologie">méthodologie</a> · <a href="/signaux">décomposition des couches</a>).</p>57 </div>5859 <h2 class="section">Les chiffres du jour</h2>60 <div class="grid parties" id="day-cards"><div class="loading">Chargement…</div></div>61 <div class="card">62 <div class="kv"><span>Probabilité qu'aucun parti n'obtienne la majorité</span><b id="d-nomaj"></b></div>63 <div class="kv"><span>Ce qui bouge depuis la veille</span><b id="d-move"></b></div>64 </div>6566 <h2 class="section">Ce qui a changé</h2>67 <p class="section-sub">Dernier run vs dernier run de la journée précédente68 (<span id="prev-date">veille</span>). P(1er) = probabilité publiée (ensemble modèle/marchés).</p>69 <div class="grid two">70 <div class="card" style="overflow-x:auto">71 <h3>Variations du forecast</h3>72 <table class="data" id="delta-table"></table>73 </div>74 <div class="card">75 <h3>Le journal des dernières 24 h</h3>76 <div id="ticker"></div>77 </div>78 </div>7980 <h2 class="section">Attention web <span class="chip badge-aux">Wikipédia — recherche d'information</span></h2>81 <p class="section-sub">Pages des partis et des chef·fe·s (fr.wikipédia) : qui les Québécois·es82 recherchent-ils ? La littérature montre que l'attention précède souvent le mouvement — le83 signal ne déplace jamais le forecast, mais un pic global <b>élargit l'incertitude de campagne</b>.</p>84 <div class="card" id="attention-card"><div class="loading">Chargement…</div></div>8586 <h2 class="section">À surveiller</h2>87 <div class="grid two">88 <div class="card">89 <h3>Prochaines échéances</h3>90 <div class="events" id="upcoming"></div>91 <h3 style="margin-top:18px">Les couches du forecast aujourd'hui</h3>92 <div id="layer-state"></div>93 </div>94 <div class="card">95 <h3>Sondage synthétique IA <span class="synth-badge">EXPÉRIMENTAL — poids nul</span></h3>96 <p class="section-sub">« Silicon sampling » : 96 strates démographiques du Québec incarnées97 par un LLM local ancré dans l'actualité réelle de la campagne, poststratifiées. Ce n'est98 <b>pas un sondage</b> (aucune donnée nouvelle) — publié comme objet d'étude, évalué après99 le 5 octobre.</p>100 <div id="synthetic-body"><div class="loading">Chargement…</div></div>101 </div>102 </div>103</div>104<footer class="site"></footer>105106<script src="/assets/app.js"></script>107<script>108(async () => {109 const meta = await initShell("/aujourdhui");110 const parties = Object.keys(meta.parties).filter(p => p !== "AUT");111 const T = await getJSON(`${API}/today`);112113 const dstr = new Date(T.date + "T12:00:00").toLocaleDateString("fr-CA",114 { weekday: "long", year: "numeric", month: "long", day: "numeric" });115 document.getElementById("day-date").textContent =116 `${dstr} · J−${T.days_to_election} avant le scrutin`;117118 const order = [...parties].sort((a, b) => T.prob_most[b] - T.prob_most[a]);119 const lead = order[0];120 document.getElementById("headline").innerHTML =121 `Le <span style="color:${partyColor(lead)}">${partyName(lead)}</span> a122 ${(T.prob_most[lead] * 100).toFixed(0)} % de chances de remporter le plus de sièges`;123 document.getElementById("day-sub").innerHTML =124 `Majorité ${lead} : <b>${(T.seats[lead].prob_majority * 100).toFixed(0)} %</b> ·125 vote projeté ${T.vote[lead].toFixed(1)} % ·126 ${T.seats[lead].mean.toFixed(0)} sièges attendus [${T.seats[lead].p05}–${T.seats[lead].p95}] ·127 run #${T.run_id} (${new Date(T.run_at + "Z").toLocaleTimeString("fr-CA", { hour: "2-digit", minute: "2-digit" })})`;128129 /* variation : flèche + valeur, couleur du parti ou neutre — jamais vert/rouge */130 const dOf = (obj, p) => obj && obj[p] != null ? obj[p] : null;131132 /* ---- cartes du jour (compactes, data-first) ---- */133 document.getElementById("day-cards").innerHTML = order.map(p => {134 const dp = T.delta ? dOf(T.delta.prob_most, p) : null;135 const dv = T.delta ? dOf(T.delta.vote, p) : null;136 return `<div class="card party-card pcard">137 <div class="bar" style="background:${partyColor(p)}"></div>138 <div class="p-top">139 <div class="p-id">140 <span class="avatars-stack">${avatarHTML(p, 30)}</span>141 <div style="min-width:0">142 <div class="p-abbr" title="${partyName(p)}">${p}</div>143 <div class="p-leader">${partyLeader(p)}</div>144 </div>145 </div>146 </div>147 <div class="p-main">148 <span class="v" style="color:${partyColor(p)}">${(T.prob_most[p] * 100).toFixed(0)}<span style="font-size:.42em;font-weight:650;color:var(--ink-2)"> %</span></span>149 <span class="u">chance<br>d'arriver premier ${deltaHTML(dp != null ? dp * 100 : null, { unit: " pt", title: "variation vs la veille" })}</span>150 </div>151 <div class="p-rows">152 <div class="p-row"><span class="l">Vote projeté</span>153 <span class="r">${T.vote[p].toFixed(1)} % ${deltaHTML(dv, { unit: " pp", digits: 2 })}</span></div>154 <div class="p-row"><span class="l">Sièges attendus</span>155 <span class="r">${T.seats[p].mean.toFixed(0)} <span style="color:var(--muted);font-weight:400">[${T.seats[p].p05}–${T.seats[p].p95}]</span></span></div>156 </div>157 </div>`;158 }).join("");159 document.getElementById("d-nomaj").textContent = fmtPct(T.prob_no_majority);160 const movers = T.delta ? order.map(p => [p, T.delta.vote[p]])161 .filter(([, d]) => Math.abs(d) >= 0.05).sort((a, b) => Math.abs(b[1]) - Math.abs(a[1])) : [];162 document.getElementById("d-move").innerHTML = movers.length163 ? movers.slice(0, 3).map(([p, d]) =>164 `<span style="color:${partyColor(p)}">${p}</span> ${d > 0 ? "+" : ""}${d.toFixed(2)} pp`).join(" · ")165 : "journée stable — aucun mouvement notable";166167 /* ---- table des deltas ---- */168 if (T.delta) {169 document.getElementById("prev-date").textContent = fmtDate(T.delta.since);170 document.getElementById("delta-table").innerHTML =171 `<tr><th>Parti</th><th>P(1er)</th><th>Δ</th><th>Vote</th><th>Δ</th><th>Sièges</th><th>Δ</th></tr>` +172 order.map(p => `<tr>173 <td><span class="dot" style="background:${partyColor(p)}"></span>${p}</td>174 <td>${(T.prob_most[p] * 100).toFixed(1)} %</td>175 <td>${deltaHTML((dOf(T.delta.prob_most, p) ?? NaN) * 100, { unit: " pt", color: partyColor(p) })}</td>176 <td>${T.vote[p].toFixed(1)} %</td>177 <td>${deltaHTML(dOf(T.delta.vote, p), { unit: " pp", digits: 2, color: partyColor(p) })}</td>178 <td>${T.seats[p].mean.toFixed(0)}</td>179 <td>${deltaHTML(dOf(T.delta.seats, p), { unit: "", digits: 1, color: partyColor(p) })}</td></tr>`).join("");180 } else {181 document.getElementById("delta-table").innerHTML =182 `<tr><td style="color:var(--muted)">Premier jour de publication — comparaison disponible demain.</td></tr>`;183 }184185 /* ---- journal 24 h ---- */186 const ticks = [];187 for (const p of T.new_polls_24h) {188 const top = Object.entries(p.shares).filter(([k]) => k !== "AUT")189 .sort((a, b) => b[1] - a[1]).slice(0, 2)190 .map(([k, v]) => `${k} ${v.toFixed(0)}`).join(" · ");191 ticks.push([`sondage`, `<b>${p.pollster}</b> intégré (terrain ${fmtDate(p.field_end)}, n=${p.sample_size ?? "?"}) — ${top}`]);192 }193 for (const s of T.new_signals_24h) {194 ticks.push([s.kind === "poll-radar" ? "radar" : s.kind,195 `<a href="${s.url}" target="_blank" rel="noopener">${s.title}</a>${s.pollster ? " · " + s.pollster : ""}`]);196 }197 document.getElementById("ticker").innerHTML = ticks.length198 ? ticks.slice(0, 12).map(([k, h]) =>199 `<div class="tick"><span class="t"><span class="chip badge-aux">${k}</span></span><span>${h}</span></div>`).join("")200 : emptyStateHTML("Aucun nouveau sondage ni signal dans les dernières 24 h.");201202 /* ---- attention web ---- */203 try {204 const A = await getJSON(`${API}/attention`);205 if (!A.available) throw new Error(A.note || "indisponible");206 const ap = A.parties;207 const ordA = parties.filter(p => ap[p]).sort((a, b) => ap[b].share_7j - ap[a].share_7j);208 document.getElementById("attention-card").innerHTML = `209 <div class="att-bar">${ordA.map(p =>210 `<div style="flex:${ap[p].share_7j};background:${partyColor(p)}" title="${p} ${ap[p].share_7j} %"></div>`).join("")}</div>211 <div style="overflow-x:auto"><table class="data">212 <tr><th>Parti</th><th>Part d'attention 7 j</th><th>vs base 180 j</th><th>Pic (z, 3 j)</th><th>28 derniers jours</th></tr>213 ${ordA.map((p, i) => `<tr>214 <td><span class="dot" style="background:${partyColor(p)}"></span>${p}</td>215 <td><b>${ap[p].share_7j.toFixed(1)} %</b> <span style="color:var(--muted);font-size:11px">(${ap[p].views_7j.toLocaleString("fr-CA")} vues)</span></td>216 <td>${deltaHTML(ap[p].delta_share_pp, { unit: " pp", thresh: 0.5, color: partyColor(p) })}</td>217 <td>${ap[p].z_3j >= 2 ? '<span class="chip badge-aux">pic</span> ' : ""}${ap[p].z_3j.toFixed(1)}</td>218 <td><div class="mini-spark" id="spark-${p}"></div></td></tr>`).join("")}219 </table></div>220 <p class="note">${A.note} Turbulence détectée : z max ${A.z_max} → variance de campagne ×${A.drift_multiplier}.221 Données au ${fmtDate(A.as_of)} · Wikimedia REST API.</p>`;222 for (const p of ordA) {223 const c = baseChart(document.getElementById("spark-" + p));224 c.setOption({ grid: { left: 0, right: 0, top: 2, bottom: 2 },225 xAxis: { type: "category", show: false, data: ap[p].sparkline.map((_, i) => i) },226 yAxis: { type: "value", show: false },227 series: [{ type: "line", data: ap[p].sparkline, symbol: "none", smooth: 0.3,228 color: partyColor(p), lineStyle: { width: 1.6, cap: "round" },229 areaStyle: areaGrad(partyColor(p), 0.3) }] });230 }231 } catch (e) {232 document.getElementById("attention-card").innerHTML =233 emptyStateHTML("Signal d'attention en cours de constitution — première fenêtre de 30 jours requise.");234 }235236 /* ---- sondage synthétique ---- */237 try {238 const S = await getJSON(`${API}/synthetic`);239 const ordS = parties.sort((a, b) => (S.shares[b] || 0) - (S.shares[a] || 0));240 document.getElementById("synthetic-body").innerHTML = `241 <table class="data">242 <tr><th>Parti</th><th>Synthétique</th><th>Forecast</th><th>Écart</th></tr>243 ${ordS.map(p => {244 const c = (S.forecast_comparison || {})[p] || {};245 const d = c.synthetique != null && c.forecast != null ? c.synthetique - c.forecast : null;246 return `<tr><td><span class="dot" style="background:${partyColor(p)}"></span>${p}</td>247 <td><b>${S.shares[p] != null ? S.shares[p].toFixed(1) + " %" : "—"}</b></td>248 <td>${c.forecast != null ? c.forecast.toFixed(1) + " %" : "—"}</td>249 <td style="color:var(--muted)">${d != null ? (d >= 0 ? "+" : "") + d.toFixed(1) + " pp" : "—"}</td></tr>`;250 }).join("")}251 </table>252 <p class="note">${S.note} Généré le ${fmtDate(S.as_of)} · modèle <code>${S.model}</code> ·253 ${S.n_strata} strates poststratifiées · ancré dans ${(S.headlines || []).length} manchettes réelles.</p>`;254 } catch (e) {255 document.getElementById("synthetic-body").innerHTML =256 emptyStateHTML("Aucun sondage synthétique généré pour l'instant (LLM local requis).");257 }258259 /* ---- à surveiller + état des couches ---- */260 document.getElementById("upcoming").innerHTML = (T.upcoming_events || []).map(e =>261 `<div class="event"><span class="d">${fmtDate(e.date)}</span><span>${e.title}</span></div>`).join("")262 || emptyStateHTML("Aucun événement à venir enregistré.");263 const B = T.beyond || {};264 const sat = ((B.fundamentals || {}).satisfaction || {});265 const wF = (((B.fundamentals || {}).blend || {}).precision_share || 0) * 100;266 const att = B.web_attention || {};267 const nudgeMax = Math.max(0, ...Object.values((B.media_adjustment || {}).delta_pp || { x: 0 }).map(Math.abs));268 const mkt = B.market_ensemble;269 document.getElementById("layer-state").innerHTML = [270 ["Votes réels", `${B.n_byelections || 0} partielles dans le filtre`],271 ["Fondamentaux", `satisfaction ${sat.value != null ? sat.value.toFixed(0) + " %" : "—"} (${sat.method === "firecrawl" ? "veille live" : "repli"}) · poids ${wF.toFixed(1)} %`],272 ["Attention web", att.available ? `z max ${att.z_max} → variance ×${att.drift_multiplier}` : "en constitution"],273 ["Médias", `nudge ±${nudgeMax.toFixed(2)} pp`],274 ["Marchés", mkt ? `${(mkt.weight_market * 100).toFixed(0)} % dans P(1er)` : "indisponible"],275 ].map(([t, d]) => `<div class="kv"><span>${t}</span><b style="font-weight:500;color:var(--muted)">${d}</b></div>`).join("");276 try {277 const so = await getJSON(`${API}/social?days=7`);278 const tops = Object.entries(so.parties || {}).filter(([, s]) => s.volume > 0)279 .sort((a, b) => b[1].part_de_voix - a[1].part_de_voix).slice(0, 3);280 if (tops.length) document.getElementById("layer-state").insertAdjacentHTML("beforeend",281 `<div class="kv"><span>Pouls social 7 j</span><b style="font-weight:500;color:var(--muted)">${282 tops.map(([p, s]) => `${p} ${s.part_de_voix}%${s.sentiment_pondere != null283 ? " (" + (s.sentiment_pondere > 0 ? "+" : "") + s.sentiment_pondere.toFixed(2) + ")" : ""}`).join(" · ")284 } — <a href="/intelligence">détail</a></b></div>`);285 } catch (e) { /* premier cycle à venir */ }286})();287</script>288</body>289</html>290