Les résultats du second tour des élections municipales, qui s’est tenu dimanche 22 mars, sont désormais complets. Ils permettent de mesurer l’écart entre les projections théoriques et la réalité des bulletins glissés dans les urnes. Et de constater que les fusions, même lorsqu’elles plaçaient une liste en position dominante, n’ont pas toujours suffi à la faire élire.
A l’issue du premier tour, il était en effet possible de calculer le « potentiel électoral » de chaque liste : une addition des scores des listes qualifiées et, le cas échéant, de celles qu’elles avaient absorbées. Mais les reports de voix n’ont pas toujours eu lieu dans les proportions anticipées. Dans 233 communes de plus de 3 500 habitants, la liste qui semblait la mieux armée pour l’emporter a finalement été battue – 110 de ces listes étaient issues d’une fusion. Dans les villes de plus de 30 000 habitants, 38 listes fusionnées et mieux placées pour l’emporter ont échoué, contre 33 qui ont réussi leur pari.
Par exemple, à Brest, la liste fusionnée du Parti socialiste (PS) et de La France insoumise (LFI) a obtenu des suffrages proches des résultats obtenus par les deux listes au premier tour. Le Rassemblement national (RN) a, lui, perdu près de 3 000 voix, tandis que le candidat Les Républicains (LR), Stéphane Roudaut, a fédéré 14 272 électeurs de plus qu’au premier tour – 27 points de plus que son « potentiel électoral » –, ce qui lui a assuré la victoire.
A Nantes, en revanche, la fusion PS-LFI a permis à la maire sortante, Johanna Rolland, de conserver sa place face à la liste LR de Foulques Chombart de Lauwe. Johanna Rolland a récolté environ 8 500 voix de plus qu’au premier tour – les quelque 18 400 voix supplémentaires données à son rival n’ont pas suffi à inverser la tendance.
Le module interactif ci-dessous vous permet de comparer, pour chaque ville ayant organisé un second tour, le potentiel électoral théorique et les résultats officiels du second tour. Vous pouvez filtrer les communes par taille, type d’alliance ou bloc politique.
${subtitle ? `
${subtitle}
` : « }
`;
grid.appendChild(card);
const chartEl = card.querySelector(« .mun_alliances_card_chart »);
const width = chartEl.offsetWidth || card.offsetWidth;
const isSmall = width < 600;
const margin = {
top: isSmall ? width * 0.02 : width * 0.02,
right: width * 0.01,
bottom: isSmall ? width * 0.01 : width * 0.01,
left: isSmall ? width * 0.001 : width * 0.001
};
const svg = d3.select(chartEl)
.append(« svg »)
.attr(« width », « 100% »)
.attr(« class », « chart »);
let x0 = 0;
const t1Segments = [];
const t1Rows = commune.listes;
for (const row of t1Rows) {
const pct = row.pct_exprimes;
if (!Number.isFinite(pct) || pct <= 0) continue;
const x1 = x0 + pct;
const nuanceMin = row.nuance;
const nuanceLM = row.nuance_lemonde;
const { light, dark, label } = getNuanceStyle(nuanceLM, nuanceMin);
const nuanceCode = String(nuanceLM || nuanceMin || label);
t1Segments.push({
ordre: row.ordre,
nuanceCode,
nuanceLabel: String(label),
pct,
x0,
x1,
light,
dark,
sourceRow: row,
t1Status: row.qualifie,
lib: row.lib ?? « »,
deptLabel
});
x0 = x1;
}
const missing = Math.max(0, 100 – x0);
if (missing > 0.05) {
t1Segments.push({
nuanceLabel: « Eliminés »,
nuanceCode: « __remainder__ »,
voix: null,
pct: missing,
x0: x0,
x1: 100,
light: « #E2E4E9 »,
dark: « #3C3C3C »,
isRemainder: true,
lib: commune.lib,
deptLabel,
});
}
let x0t2 = 0;
const t2Segments = [];
const fmtDecision = (s) => String(s || « »).trim();
for (const row of t1Rows) {
const decision = fmtDecision(row.decision_officielle);
if (decision == « Fusion (absorbée) ») continue;
if (decision.startsWith(« Désistement »)) continue;
if (decision.startsWith(« Fusionnable »)) continue;
if (decision === « Ne fusionne pas ») continue;
if (decision === « Maintien ») {
const pct = row.pct_exprimes;
if (!Number.isFinite(pct) || pct <= 0) continue;
const x1 = x0t2 + pct;
const { light, dark, label } = getNuanceStyle(row.nuance_lemonde, row.nuance);
t2Segments.push({
nuance: String(label),
pct,
x0: x0t2,
x1,
light,
dark,
isRemainder: false,
sourceRows: [row],
decision: « Maintien »,
lib: commune.lib,
deptLabel
});
x0t2 = x1;
continue;
}
if (decision === « Fusion (absorbante) ») {
let pct = row.pct_exprimes;
const sourceRows = [row];
const fusionTargets = String(row.n_panneau_absorbe || « »)
.split(« ; »)
.map((s) => {
const n = parseFR(s);
return Number.isFinite(n) ? String(Math.trunc(n)) : String(s || « »).trim();
})
.filter(Boolean);
const seen = new Set();
for (const targetPanneau of fusionTargets) {
if (seen.has(targetPanneau)) continue;
seen.add(targetPanneau);
const absorbed = commune.byPanneau.get(targetPanneau);
if (absorbed && Number.isFinite(absorbed.pct_exprimes)) {
pct += absorbed.pct_exprimes;
sourceRows.push(absorbed);
}
}
// if (fusionTargets.length && sourceRows.length === 1) {
// console.log(« Fusion cible introuvable (panneau) », commune.code_circo, row.n_panneau, fusionTargets);
// }
if (!Number.isFinite(pct) || pct <= 0) continue;
const x1 = x0t2 + pct;
const { light, dark } = getNuanceStyle(row.nuance_lemonde, row.nuance);
const fusionLabel = sourceRows
.map((r) => getNuanceStyle(r.nuance_lemonde, r.nuance).label)
.join(« + »);
t2Segments.push({
nuance: fusionLabel,
pct,
x0: x0t2,
x1,
light,
dark,
isRemainder: false,
sourceRows,
decision: « Fusion (absorbante) »,
lib: commune.lib,
deptLabel
});
x0t2 = x1;
continue;
}
}
const aRepartir = Math.max(0, 100 – x0t2);
if (aRepartir > 0.05) {
t2Segments.push({
nuance: « À répartir »,
pct: aRepartir,
x0: x0t2,
x1: Math.min(100, x0t2 + aRepartir),
light: « #E2E4E9 »,
dark: « #3C3C3C »,
isRemainder: true,
decision: « À répartir »,
lib: commune.lib,
deptLabel
});
x0t2 = x0t2 + aRepartir;
}
const remainder = t2Segments.filter(d => d.isRemainder);
const nonRemainder = t2Segments.filter(d => !d.isRemainder);
nonRemainder.sort((a, b) => b.pct – a.pct);
let cursor = 0;
for (const seg of […nonRemainder, …remainder]) {
seg.x0 = cursor;
seg.x1 = cursor + seg.pct;
cursor = seg.x1;
}
t2Segments.length = 0;
t2Segments.push(…nonRemainder, …remainder);
// Tour 2 officiel
let x0t3 = 0;
const t3Segments = [];
for (const row of t1Rows) {
const pct = row.resultat_t2;
if (!Number.isFinite(pct) || pct <= 0) continue;
const x1 = x0t3 + pct;
const { light, dark, label } = getNuanceStyle(row.nuance_lemonde, row.nuance);
t3Segments.push({
pct, x0: x0t3, x1, light, dark,
sourceRow: row,
lib: commune.lib, deptLabel,
isT3: true,
});
x0t3 = x1;
}
const t3Remainder = t3Segments.filter(d => d.isRemainder);
const t3NonRemainder = t3Segments.filter(d => !d.isRemainder);
t3NonRemainder.sort((a, b) => {
const aElu = a.sourceRow?.elu === « O »;
const bElu = b.sourceRow?.elu === « O »;
if (aElu && !bElu) return -1;
if (!aElu && bElu) return 1;
return b.pct – a.pct;
});
// Recalculer les x0/x1 après tri
let cursorT3 = 0;
for (const seg of […t3NonRemainder, …t3Remainder]) {
seg.x0 = cursorT3;
seg.x1 = cursorT3 + seg.pct;
cursorT3 = seg.x1;
}
t3Segments.length = 0;
t3Segments.push(…t3NonRemainder, …t3Remainder);
const hasT3Data = t3Segments.length > 0;
const missingT3 = Math.max(0, 100 – x0t3);
if (hasT3Data && missingT3 > 0.05) {
t3Segments.push({
pct: missingT3, x0: x0t3, x1: 100,
light: « #E2E4E9 », dark: « #3C3C3C »,
isRemainder: true,
lib: commune.lib, deptLabel,
isT3: true,
});
}
const labelW = isSmall ? 78 : 78;
const innerW = width – margin.left – margin.right – labelW;
const barH = 20;
const barY = 4;
const gapY = isSmall ? 18 : 22;
const barY2 = barY + barH + gapY;
const gapY3 = isSmall ? 18 : 22;
const barY3 = barY2 + barH + gapY3;
const neededH = hasT3Data
? margin.top + barY3 + barH + margin.bottom
: margin.top + barY2 + barH + margin.bottom;
svg.attr(« viewBox », [0, 0, width, neededH]);
const x = d3.scaleLinear().domain([0, 100]).range([0, innerW]);
const g = svg
.append(« g »)
.attr(« transform », `translate(${margin.left + labelW},${margin.top})`);
const t1SegByRow = new Map(
t1Segments
.filter(d => d.sourceRow)
.map(d => [d.sourceRow, d])
);
// Gérer les liens
const fusionTargets = t2Segments.filter(d => d.decision === « Fusion (absorbante) » && Array.isArray(d.sourceRows) && d.sourceRows.length);
if (fusionTargets.length) {
const gLinks = g.append(« g »).attr(« class », « t2-sankey-links »).attr(« pointer-events », « none »);
for (const ft of fusionTargets) {
const components = (ft.sourceRows || [])
.map(sr => {
const t1Seg = t1SegByRow.get(sr);
const pct = Number.isFinite(sr?.pct_exprimes) ? sr.pct_exprimes : (t1Seg?.pct ?? 0);
return { sr, t1Seg, pct };
})
.filter(d => d.t1Seg && Number.isFinite(d.pct) && d.pct > 0)
.sort((a, b) => b.pct – a.pct);
const denomUsed = components.reduce((s, c) => s + c.pct, 0) || ft.pct || 1;
const ySourceBottom = barY + barH;
const yTargetTop = barY2;
let accX = 0;
const xTargetBase = x(ft.x0);
const xTargetTotalW = Math.max(0, x(ft.x1) – x(ft.x0));
if (!components.length) continue;
for (const comp of components) {
const share = comp.pct / denomUsed;
const sLeft = x(comp.t1Seg.x0);
const sRight = x(comp.t1Seg.x1);
const tLeft = xTargetBase + accX * xTargetTotalW;
const tRight = tLeft + share * xTargetTotalW;
accX += share;
const strokeColor = isDark ? comp.t1Seg.dark : comp.t1Seg.light;
const pathD =
`M ${sLeft} ${ySourceBottom}
L ${sRight} ${ySourceBottom}
L ${tRight} ${yTargetTop}
L ${tLeft} ${yTargetTop}
Z`;
gLinks.append(« path »)
.attr(« d », pathD)
.attr(« fill », strokeColor)
.attr(« fill-opacity », 0.22)
.attr(« stroke », strokeColor)
.attr(« stroke-opacity », 0.35)
.attr(« stroke-width », 1);
}
}
}
g.append(« text »)
.attr(« class », « bar-label passelect »)
.attr(« x », -8)
.attr(« y », barY + barH / 2)
.attr(« dy », « 0.35em »)
.attr(« text-anchor », « end »)
.text(« Résultats T1 »);
g.selectAll(« rect.t1 »)
.data(t1Segments)
.enter()
.append(« rect »)
.attr(« class », « t1 »)
.attr(« x », (d) => x(d.x0))
.attr(« y », barY)
.attr(« height », barH)
.attr(« width », (d) => Math.max(0, x(d.x1) – x(d.x0)))
.attr(« fill », (d) => (isDark ? d.dark : d.light))
.attr(« stroke », isDark ? « rgba(255,255,255,0.35) » : « rgba(0,0,0,0.18) »)
.attr(« stroke-width », 1)
.attr(« fill-opacity », (d) => (d.t1Status === « Fusionnable » ? isDark ? 0.6 : 0.35 : 1))
.attr(« stroke-dasharray », (d) => (d.t1Status === « Fusionnable » ? « 3 2 » : null))
.attr(« shape-rendering », « crispEdges »)
.attr(« cursor », « pointer »)
.on(« mouseenter », showTooltip)
.on(« mousemove », showTooltip)
.on(« mouseleave », hideTooltip);
t1Segments
.filter(d => d.sourceRow?.decision_officielle?.startsWith(« Désistement »))
.forEach(d => {
const segW = Math.max(0, x(d.x1) – x(d.x0));
const cx = x(d.x0) + segW / 2;
const cy = barY + (barH / 2);
const pictoG = g.append(« g »).attr(« class », « t1-desistement-picto »);
pictoG.append(« text »)
.attr(« x », cx)
.attr(« y », cy)
.text(« × »);
});
g.append(« text »)
.attr(« class », « bar-label passelect »)
.attr(« x », -8)
.attr(« y », barY2 + barH / 2)
.attr(« dy », « 0.35em »)
.attr(« text-anchor », « end »)
.text(« Potentiel électoral »);
g.selectAll(« rect.t2 »)
.data(t2Segments)
.enter()
.append(« rect »)
.attr(« class », « t2 »)
.attr(« x », (d) => x(d.x0))
.attr(« y », barY2)
.attr(« height », barH)
.attr(« width », (d) => Math.max(0, x(d.x1) – x(d.x0)))
.attr(« fill », (d) => (isDark ? d.dark : d.light))
.attr(« fill-opacity », 1)
.attr(« stroke », isDark ? « rgba(255,255,255,0.8) » : « rgba(0,0,0,0.3) »)
.attr(« stroke-width », 1)
.attr(« stroke-linecap », « round »)
.attr(« stroke-linejoin », « round »)
.attr(« stroke-dasharray », « 4 3 »)
.attr(« cursor », « pointer »)
.on(« mouseenter », showTooltip)
.on(« mousemove », showTooltip)
.on(« mouseleave », hideTooltip);
if (hasT3Data) {
g.append(« text »)
.attr(« class », « bar-label passelect »)
.attr(« x », -8)
.attr(« y », barY3 + barH / 2)
.attr(« dy », « 0.35em »)
.attr(« text-anchor », « end »)
.text(« Résultats T2 »);
g.selectAll(« rect.t3 »)
.data(t3Segments)
.enter()
.append(« rect »)
.attr(« class », « t3 »)
.attr(« x », d => x(d.x0))
.attr(« y », barY3)
.attr(« height », barH)
.attr(« width », d => Math.max(0, x(d.x1) – x(d.x0)))
.attr(« fill », d => isDark ? d.dark : d.light)
.attr(« stroke », isDark ? « rgba(255,255,255,0.35) » : « rgba(0,0,0,0.18) »)
.attr(« stroke-width », 1)
.attr(« shape-rendering », « crispEdges »)
.attr(« cursor », « pointer »)
.on(« mouseenter », showTooltip)
.on(« mousemove », showTooltip)
.on(« mouseleave », hideTooltip);
t3Segments
.filter(d => !d.isRemainder && Number.isFinite(d.sourceRow?.diff_potentiel_t2))
.forEach((d, i) => {
const diff = d.sourceRow.diff_potentiel_t2;
const absD = Math.abs(diff);
if (absD < 0.05) return;
const segW = Math.max(0, x(d.x1) – x(d.x0));
if (segW < 14) return; // trop étroit
const cx = x(d.x0) + segW / 2;
const sign = diff > 0 ? « + » : « u2212 »;
const unit = i === 0 ? (absD >= 2 ? « u00A0pts » : « u00A0pt ») : « »;
const label = `${sign}${absD.toFixed(1).replace(« . », « , »)}${unit}`;
// positif = candidat a fait mieux que le potentiel
const color = diff > 0 ? « #2a9d2a » : « #d9522a »;
g.append(« text »)
.attr(« class », « t3-diff-label passelect »)
.attr(« x », cx)
.attr(« y », barY3 – 2)
.attr(« dy », « -0.15em »)
.attr(« text-anchor », « middle »)
.attr(« fill », color)
.text(label);
});
}
}
function showTooltip(event, d) {
const isT3 = !!d.isT3;
const isT2 = !isT3 && !!d.decision;
const isT1 = !isT2 && !isT3;
const row = d.sourceRow || (Array.isArray(d.sourceRows) ? d.sourceRows[0] : null);
let htmlTooltip = « »;
const titleCity = d.lib ? `
${d.lib}
` : « »;
// On a des résultats
if (row) {
const { light, dark, label: nuanceLabel } = getNuanceStyle(row.nuance_lemonde, row.nuance);
const nuanceColor = isDark ? dark : light;
const nuanceColorDark = dark || light;
const civ = row.tete_civ ? `${row.tete_civ} ` : « »;
const fullName = `${civ}${row.tete_prenom ?? « »} ${row.tete_nom ?? « »}`.trim();
let secondLine = « »;
if (isT1) {
secondLine = `${fmtPct(d.pct)} des suffrages exprimés au premier tour`;
} else if (isT2) {
secondLine = `Potentiel électoral : ${fmtPct(d.pct)}`;
} else if (isT3) {
secondLine = `${fmtPct(d.pct)} des suffrages exprimés au second tour`;
}
// Lignes additionnelles (fusion)
let extraContent = « »;
const badge = (isT3 && d.sourceRow?.elu === « O »)
? `
`
: « »;
if (isT3 && Number.isFinite(row.diff_potentiel_t2)) {
const diff = row.diff_potentiel_t2;
const absD = Math.abs(diff);
if (absD >= 0.05) {
const sign = diff > 0 ? « + » : « u2212 »;
const unit = absD >= 2 ? « pts » : « pt »;
const diffColor = diff > 0 ? « #2a9d2a » : « #d9522a »;
const voix = row.diff_potentiel_t2_voix;
const voixStr = Number.isFinite(voix)
? ` (${voix > 0 ? « + » : voix < 0 ? « u2212 » : « »}${thousands(Math.abs(Math.round(voix)))} voix)`
: « »;
extraContent = `${sign}${absD.toFixed(1).replace(« . », « , »)}u00A0${unit} ${voixStr} par rapport au potentiel électoral`;
}
}
if (isT1 && d.t1Status === « Fusionnable ») {
extraContent = `Liste éliminée mais fusionnable`;
}
if (isT1 && d.sourceRow?.decision_officielle?.startsWith(« Désistement »)) {
extraContent = `×Désistement au second tour`;
}
if (isT2 && d.decision === « Fusion (absorbante) ») {
const fused = (Array.isArray(d.sourceRows) ? d.sourceRows.slice(1) : []).filter(Boolean);
if (fused.length) {
extraContent = fused.map(r => {
const { light, dark } = getNuanceStyle(r.nuance_lemonde, r.nuance);
const nu2 = getNuanceStyle(r.nuance_lemonde, r.nuance).label;
const civ2 = r.tete_civ ? `${r.tete_civ} ` : « »;
const name2 = `${civ2}${r.tete_prenom ?? « »} ${r.tete_nom ?? « »}`.trim();
const dotColor = isDark ? dark : light;
const dot = ``;
return `${dot}Fusion avec ${name2} (${nu2})`;
}).join(«
« );
}
}
const extra = extraContent
? `
${extraContent}
`
: « »;
htmlTooltip = `
${titleCity}
${extra}
`;
} else {
if (isT1) {
htmlTooltip = `${titleCity}
Autres listes éliminéess : ${fmtPct(d.pct)}
`;
}
else if (isT2 && d.isRemainder) {
htmlTooltip = `${titleCity}
À répartir : ${fmtPct(d.pct)}
`;
} else if (isT3 && d.isRemainder) {
// CHELOU
htmlTooltip = `${titleCity}
Autre : ${fmtPct(d.pct)}
`;
} else {
return;
}
}
tooltip
.html(htmlTooltip)
.style(« opacity », 1);
if (window.innerWidth > 600) {
tooltip
.style(« left », (event.pageX – tooltip.node().getBoundingClientRect().width / 2) + « px »)
.style(« top », (event.pageY – tooltip.node().getBoundingClientRect().height – 8) + « px »);
}
}
function hideTooltip() {
tooltip.style(« opacity », 0);
}
const communes = Object.values(resultsByCommune).sort(
(a, b) => (b.population ?? 0) – (a.population ?? 0)
);
communes.forEach((commune, idx) => renderCommune(commune, idx));
loader.classList.add(« hidden »);
const loadMoreBtn = document.getElementById(« mun_alliances_load_more »);
function applyFilter() {
const cards = document.querySelectorAll(« .mun_alliances_card »);
let visibleTotal = 0;
let shownSoFar = 0;
cards.forEach(card => {
let matches = true;
if (activePopFilter === « pop_lt30 »)
matches = matches && card.dataset.popcat === « lt30 »;
else if (activePopFilter === « pop_gt30 »)
matches = matches && card.dataset.popcat === « gt30 »;
if (activeThematic.has(« fusion »))
matches = matches && card.dataset.fusion === « 1 »;
if (activeThematic.has(« fusion_gagnante »))
matches = matches && card.dataset.fusion_gagnante === « 1 »;
if (activeThematic.has(« fusion_infructueuse »))
matches = matches && card.dataset.fusion_infructueuse === « 1 »;
if (activeThematic.has(« gauche »))
matches = matches && card.dataset.gauche === « 1 »;
if (activeThematic.has(« droite »))
matches = matches && card.dataset.droite === « 1 »;
if (activeThematic.has(« extdroite »))
matches = matches && card.dataset.extdroite === « 1 »;
if (activeThematic.has(« lfi »))
matches = matches && card.dataset.lfi === « 1 »;
if (activeThematic.has(« inversion »))
matches = matches && card.dataset.inversion === « 1 »;
if (matches) {
visibleTotal++;
const withinPage = shownSoFar < shownCount;
card.style.display = withinPage ? « » : « none »;
if (withinPage) shownSoFar++;
} else {
card.style.display = « none »;
}
});
loadMoreBtn.style.display = visibleTotal > shownCount ? « » : « none »;
}
function sortCommunes() {
const cards = […document.querySelectorAll(« .mun_alliances_card »)];
cards.sort((a, b) => {
const cA = resultsByCommune[a.dataset.code];
const cB = resultsByCommune[b.dataset.code];
if (currentSort === « diff ») {
return (cB?.maxDiffAbs ?? 0) – (cA?.maxDiffAbs ?? 0);
}
return (cB?.population ?? 0) – (cA?.population ?? 0);
});
const grid = document.querySelector(« .mun_alliances_charts_grid »);
cards.forEach(card => grid.appendChild(card));
shownCount = PAGE_SIZE;
applyFilter();
}
const listeComm = communes.map((c) => {
const codeCirco5 = String(c.code_circo ?? « »).padStart(5, « 0 »);
const dept = codeCirco5.slice(0, 2);
return {
label: { name: `${c.lib} (${dept})` },
data: { code: c.code_circo },
};
});
const func_to_treat_result_donnees = (result) => {
const code = result?.data?.code;
activePopFilter = « pop_gt30 »;
activeThematic.clear();
// Filtres sur +30000
const pillsEls = document.querySelectorAll(« .mun_alliances_filters_pills .legend_bloc »);
pillsEls.forEach(p => {
p.classList.toggle(« legend_bloc__active », p.dataset.filter === « pop_gt30 »);
});
// On remet tout en affiché
applyFilter();
const cards = document.querySelectorAll(« .mun_alliances_card »);
cards.forEach(card => {
card.style.display = card.dataset.code === code ? « » : « none »;
});
searchActive = true;
loadMoreBtn.innerHTML = `Réinitialiser`;
loadMoreBtn.style.display = « »;
};
const reset_func = () => {
// const input = document.querySelector(`.lmui-search__bar input`);
// if (input) input.value = « »;
document.querySelector(« #search_mun_alliances .lmui-search__reset-button »)?.click();
searchActive = false;
activePopFilter = « pop_gt30 »;
activeThematic.clear();
shownCount = PAGE_SIZE;
pills.forEach(p => p.classList.toggle(« legend_bloc__active », p.dataset.filter === « pop_gt30 »));
loadMoreBtn.innerHTML = `Afficher plus de communes
`;
applyFilter();
};
autocomplete_decodeurs(
« search_mun_alliances »,
listeComm,
func_to_treat_result_donnees,
reset_func,
(x) => `${x.label.name}`,
6,
3,
slugify,
« Aucune commune avec ce nom »
);
document.addEventListener(« keydown », function (event) {
if (event.key === « Escape ») reset_func();
});
const pills = document.querySelectorAll(« .mun_alliances_filters_pills .legend_bloc »);
const popFilters = new Set([« pop_lt30 », « pop_gt30 »]);
const thematicFilters = new Set([« fusion », « gauche », « droite », « extdroite », « lfi », « inversion », « fusion_infructueuse », « fusion_gagnante »]);
pills.forEach(pill => {
pill.addEventListener(« click », () => {
const f = pill.dataset.filter;
if (searchActive) {
searchActive = false;
document.querySelector(« #search_mun_alliances .lmui-search__reset-button »)?.click();
loadMoreBtn.innerHTML = `Afficher plus de communes
`;
}
if (popFilters.has(f)) {
// Mutuellement exclusifs : toggle
activePopFilter = activePopFilter === f ? null : f;
shownCount = PAGE_SIZE;
} else if (thematicFilters.has(f)) {
// Toggle indépendant
if (activeThematic.has(f)) activeThematic.delete(f);
else activeThematic.add(f);
shownCount = PAGE_SIZE;
}
pills.forEach(p => {
const pf = p.dataset.filter;
const isActive =
pf === « all » ? (activePopFilter === null && activeThematic.size === 0) :
popFilters.has(pf) ? activePopFilter === pf :
thematicFilters.has(pf) ? activeThematic.has(pf) : false;
p.classList.toggle(« legend_bloc__active », isActive);
});
applyFilter();
});
});
const sortPills = document.querySelectorAll(« #mun_alliances_sort_pills .legend_bloc »);
sortPills.forEach(pill => {
pill.addEventListener(« click », () => {
sortPills.forEach(p => p.classList.remove(« legend_bloc__active »));
pill.classList.add(« legend_bloc__active »);
currentSort = pill.dataset.sort;
sortCommunes();
});
});
applyFilter();
// Watch back to top
const backBtn = document.getElementById(« mun_alliances_backtotop »);
const topEl = document.querySelector(« .mun_alliances_top »);
window.addEventListener(« scroll », () => {
const threshold = topEl
? topEl.getBoundingClientRect().bottom + window.scrollY + 300
: 300;
backBtn.classList.toggle(« visible », window.scrollY > threshold);
});
backBtn.addEventListener(« click », () => {
document.getElementById(« search_mun_alliances »).scrollIntoView({ behavior: « smooth », block: « center » });
document.querySelector(« #search_mun_alliances input »)?.focus();
});
loadMoreBtn.addEventListener(« click », () => {
if (searchActive) {
reset_func();
} else {
shownCount += PAGE_SIZE;
applyFilter();
}
});
}
// Tout lancer une première fois
drawAllAlliances();


