import type { Metadata } from "next"; import Link from "next/link"; import { apiSafe, type Forecast, type Meta, type RidingListItem } from "@/lib/api"; import { ComponentsCompare, MajorityPanel, PopularVoteTable, SeatProjectionTable, WhatChanged } from "@/components/forecast-blocks"; import { Legend, SeatDistribution, SeatMeter, SeatWaffle } from "@/components/charts"; import { TrendChartInteractive } from "@/components/trend-chart"; import { SignalsOverview } from "@/components/signals"; import { PartyLogo } from "@/components/party"; import { Badge, Card, Dual, Empty, KV, PageTitle, SectionHeader } from "@/components/ui"; import { ForecastHistory } from "@/components/forecast-history"; import { dateFr, dateTimeFr, int, prob } from "@/lib/format"; import { PARTY_IDS, partyLabel, partyVar, sortParties } from "@/lib/parties"; export const metadata: Metadata = { title: "Prévision — sièges, vote populaire et probabilités", description: "Projection QC26 de l'élection québécoise 2026 : sièges par parti, intervalles, probabilité de majorité, composantes (sondages, marchés, fondamentaux, momentum), historique du modèle." }; export const revalidate = 60; export default async function Page() { const [meta, f, ridings] = await Promise.all([apiSafe("/api/meta"), apiSafe("/api/forecast/latest"), apiSafe<{ ridings: RidingListItem[] }>("/api/ridings")]); if (!meta || !f) return ; const seats = Object.fromEntries(Object.entries(f.parties).map(([k, v]) => [k, v.seatsMedian])); const ridingNames = Object.fromEntries((ridings?.ridings ?? []).map((r) => [r.code, { name: r.name, slug: r.slug }])); const rows = sortParties(f.parties, (v) => v.seatsMean); const comp = f.summary.components; const leader = f.summary.leader; return (
Projection probabiliste de l'Assemblée nationale au 5 octobre 2026, recalculée à chaque nouveau sondage et à chaque mise à jour des signaux. Données arrêtées au {dateFr(f.dataCutoff)} · {f.pollCount} sondages · {int(f.nSimulations)} simulations · mise à jour {dateTimeFr(f.completedAt)}.} />
Assemblée nationale · médiane
{rows.slice(0, 3).map(([id, v]) => (
P(majorité)
{prob(v.pMajority)}
{partyLabel(id)}
))}
{comp && (
Détail des signaux →} />
{(["pollsOnly", "modelNoMarkets"] as const).map((k) => { const c = comp[k]; return (
{k === "pollsOnly" ? "Sondages seuls" : "Modèle sans marchés"}
{PARTY_IDS.map((id) =>
{partyLabel(id)}{c.parties[id]?.seats} {c.parties[id]?.seats80?.join("–")} · {prob(c.parties[id]?.p_majority)}
)}
minoritaire {prob(c.p_minority)}
); })}
Ensemble QC26
{PARTY_IDS.map((id) =>
{partyLabel(id)}{f.parties[id]?.seatsMedian} {f.parties[id]?.seats80.join("–")} · {prob(f.parties[id]?.pMajority)}
)}
minoritaire {prob(f.summary.p_minority)} · ESS {int(comp.ess)} / {int(comp.nSimulations)}
)}
{rows.map(([id, v]) => (
{partyLabel(id)}médiane {v.seatsMedian} · 80 % {v.seats80[0]}–{v.seats80[1]} · 95 % {v.seats95[0]}–{v.seats95[1]}
{v.seatDistribution && } wide={} />}
Majorité
{prob(v.pMajority)}
Pluralité
{prob(v.pMostSeats)}
Vote
{v.voteMean.toFixed(1).replace(".", ",")} % ± {v.voteSd.toFixed(1).replace(".", ",")}
))}
{f.trend && }
Fiche du run
Lire la prévision
  • Une projection n'est pas une prédiction : un parti à 70 % de chances de majorité perd cette majorité dans 3 simulations sur 10.
  • Les intervalles 80 % couvrent le résultat 8 fois sur 10 si le modèle est bien calibré ; les erreurs des sondages sont corrélées et c'est intégré (σ industrie ≈ 2,2 pt).
  • L'ensemble QC26 ({partyLabel(leader)} favori) tempère les sondages par les marchés, les fondamentaux et le momentum ; la vue « sondages seuls » reste publiée ci-dessus pour comparaison.
  • Chaque chiffre est audité : sondages, signaux, méthodologie, changelog.
Projection QC26
); }