« ;
document.querySelector(« #dec_deuxieme_para »).innerHTML = asterisque + textes_dec.texte_asterique;

function extraireTitre(civilite) {
const titre = civilite.split( » « )[0];
if (titre === « M. ») {
return textes_dec.maire_elu;
} else if (titre === « Mme ») {
return textes_dec.maire_elue;
} else {
return textes_dec.maire_elu;
}
}

function initTooltipBrt(selector) {
let currentTooltip = null;

function removeCurrentTooltip() {
if (currentTooltip) {
currentTooltip.remove();
currentTooltip = null;
}
}

function showTooltip(elmt) {
removeHoverTooltip();
removeCurrentTooltip();

let tt_fixe = document.createElement(« div »);
tt_fixe.classList.add(« tooltipbrt »);
tt_fixe.innerHTML = `

×

${elmt.getAttribute(« data-tt »)}

`;
document.body.appendChild(tt_fixe);

// Positionnement du tooltip
if (window.innerWidth > 500) {
const rect = elmt.getBoundingClientRect();
tt_fixe.style.top = rect.top – 14 + window.scrollY – tt_fixe.offsetHeight + « px »;
tt_fixe.style.left = rect.left + rect.width / 2 – tt_fixe.offsetWidth / 2 + 2 + « px »;
}

// Gestionnaire pour fermer le tooltip
tt_fixe.querySelector(« .fermer-tooltip »).addEventListener(« click », function () {
removeCurrentTooltip();
reset_func();
d3.selectAll(« #dec_mun circle.bubulle »).classed(« opacified », false);
d3.selectAll(« .dec_nuance »).classed(« choisi », false);
d3.selectAll(« .dec_nuance.latotale »).classed(« choisi », true);
});

currentTooltip = tt_fixe;
return tt_fixe;
}

// Sélectionne les éléments
let tt_fixe_elmts = getA(selector);

// Ajoute les événements de clic
forEach(tt_fixe_elmts, function (elmt) {
elmt.addEventListener(« click », function (e) {
e.stopPropagation();
showTooltip(this);
});
});

// Fonction pour afficher le tooltip manuellement
function showTooltipManually(elmt) {
forEach(tt_fixe_elmts, function (e) {
e.classList.remove(« selected-commune »);
});
elmt.classList.add(« selected-commune »);
// on poireaute un peu
setTimeout(() => showTooltip(elmt), 25);
}

// Retourne les fonctions utiles
return {
showTooltipManually,
showTooltip,
};
}

function showHoverTooltip(elmt) {
removeHoverTooltip();
const tt = document.createElement(« div »);
tt.classList.add(« minitt », « tooltipbrt »);
tt.innerHTML = elmt.getAttribute(« data-tt ») +  »;
document.body.appendChild(tt);

const rect = elmt.getBoundingClientRect();
tt.style.top = rect.top – 14 + window.scrollY – tt.offsetHeight + « px »;
tt.style.left = rect.left + 2 + rect.width / 2 – tt.offsetWidth / 2 + « px »;
}

function removeHoverTooltip() {
const existing = document.querySelector(« .minitt »);
if (existing) existing.remove();
}

function dessinerBubulles(langue) {
// on modifie quelques textes en dur
if (langue != « fr ») {
d3.select(« #search_mun input »).attr(« placeholder », « Search for a town or a city »);
}

// Variables globales
const largeur = document.getElementById(« dec_mun »).offsetWidth;
const mobileDec = document.getElementById(« contenant_carte »).offsetWidth < 600;
const ratioMun = mobileDec ? 1.4 : 1.3;
const hauteur = largeur * ratioMun;
const is_dark = document.querySelector(« html »).getAttribute(« data-color-mode ») === « dark »;

// Initialisation du SVG
const svg = d3
.select(« #dec_mun »)
.append(« svg »)
.attr(« width », « 100% »)
.attr(« viewBox », `${largeur * -0.025} ${largeur * ratioMun * -0.05} ${largeur} ${largeur * ratioMun}`),
dec_dept = svg.append(« g »),
dec_ligne = svg.append(« g »),
dec_communes = svg.append(« g »),
dec_textes = svg.append(« g »);

// Chargement des données
// URL finale : https://assets-decodeurs.lemonde.fr/decodeurs/municipales_2026_snippets/municipales/exports/2026/T2/listes_en_tete.json
// test : https://assets-decodeurs.lemonde.fr/decodeurs/municipales_2026_snippets/municipales/exports/2026test2/T2/listes_en_tete.json
Promise.all([d3.json(« //assets-decodeurs.lemonde.fr/decodeurs/assets/municipales2026/brt_3500.json »), d3.csv(« //assets-decodeurs.lemonde.fr/decodeurs/assets/municipales2026/commplus3500.csv »), d3.json(« //assets-decodeurs.lemonde.fr/decodeurs/municipales_2026_snippets/municipales/exports/2026/T2/listes_en_tete.json »), d3.csv(« //assets-decodeurs.lemonde.fr/sheets/wAn2GJcj3n7zw3Ivl7sP8tBTw6KHCg_3917.csv »), d3.csv(« //assets-decodeurs.lemonde.fr/sheets/wAn2GJcj3n7zw3Ivl7sP8tBTw6KHCg_3918.csv »), d3.csv(« //assets-decodeurs.lemonde.fr/sheets/wAn2GJcj3n7zw3Ivl7sP8tBTw6KHCg_3985.csv »)]).then(
function ([geoData, popData, resultData, nuancesMin, nuancesLM, blocsData]) {
const dicoPop = {},
listeComm = [],
listeBlocs = [],
listeNuancesLM = [];

let decompteBlocs = {},
decompteNuances = {},
decompteTours = {},
dataGraphe = {},
dataGraphePop = {},
totalBubulles = 0,
resultBubulles = 0,
totalPop = 0,
resultPop = 0;

// si on a des résultats
if (resultData.length > 0) {
document.querySelector(« #titre_tetesliste »).innerHTML = textes_dec.leg_tetesliste + asterisque;
document.querySelector(« #titre_nuanceliste »).innerHTML = textes_dec.leg_blocs;
} else {
document.querySelector(« #titre_tetesliste »).innerHTML = textes_dec.pasresultat;
d3.selectAll(« #titre_nuanceliste, #dec_deuxieme_para »).style(« display », « none »);
}

// mes dicos

const resultDict = resultData.reduce((acc, current) => {
acc[current.code_circo] = current;
return acc;
}, {}),
dicoMin = nuancesMin.reduce((acc, e) => {
e.n = +e.n;
acc[e.code] = e;
return acc;
}, {}),
dicoLM = nuancesLM.reduce((acc, e) => {
e.n = +e.n;
acc[e.code] = e;
return acc;
}, {}),
dicoBloc = blocsData.reduce((acc, e) => {
e.n = +e.n;
acc[e.bloc] = e;
return acc;
}, {});

function listerLesListes(circonscription, maxListes) {
if (!maxListes) {
maxListes = 10;
}

var html= »

    « ;

    // Vérifier chaque liste jusqu’à la liste 10
    for (let i = 1; i <= maxListes; i++) {
    const voix = circonscription[`liste_${i}_voix`];

    // Si les voix existent, ajouter les informations de la liste
    if (voix != null) {
    // On vérifie si voix n’est pas null
    const d = {
    nuance: circonscription[`liste_${i}_nuance`] ? circonscription[`liste_${i}_nuance`] : « LDIV »,
    nuance_lemonde: circonscription[`liste_${i}_nuance_lemonde`] ? circonscription[`liste_${i}_nuance_lemonde`] : null,
    voix: voix,
    elus: circonscription[`liste_${i}_elus`],
    qualifie: circonscription[`liste_${i}_qualifie`],
    tete: circonscription[`liste_${i}_tete`],
    };

    const prct = (+d.voix / +circonscription.mentions_exprimes) * 100;
    // Utilisation de la nuance pour le style, même si elle peut être nulle
    var couleur = dicoMin[d.nuance] ? dicoMin[d.nuance].couleur : «  »,
    couleur_dark = dicoMin[d.nuance]?.couleur_dark ?? «  »;

    // la couleur de la nuance LM d’abord
    if (dicoLM[d.nuance_lemonde]) {
    var couleur = dicoLM[d.nuance_lemonde] ? dicoLM[d.nuance_lemonde].couleur : «  »,
    couleur_dark = dicoLM[d.nuance_lemonde]?.couleur_dark ?? «  »;
    }
    // Couleur par défaut si nuance est nulle
    var nomLong = dicoMin[d.nuance] ? dicoMin[d.nuance].nom_long : « Liste inconnue »;
    var nomCourt = dicoMin[d.nuance] ? dicoMin[d.nuance].nom_court : « DIV »;
    if (langue == « en ») {
    var nomLong = dicoMin[d.nuance] ? dicoMin[d.nuance].nom_long_en : « Unkown list »;
    var nomCourt = dicoMin[d.nuance] ? dicoMin[d.nuance].nom_court_en : « MISC. »;
    }
    var classQualif = «  »,
    texteElu = «  »,
    rabe_nuance_lm = «  »,
    badge = «  »;

    if (d.qualifie) {
    classQualif =  » municipalesResults__resultItem__nom–enabled »;
    }
    // si on a une liste élue au premier tour
    if (circonscription.status == « Elue en T1″) {
    classQualif =  » municipalesResults__resultItem__nom–enabled »;
    texteElu = extraireTitre(d.tete) +  » au T1 « ;
    badge= » ‘;
    }
    if (circonscription.status == « Elue » && i == 1) {
    classQualif =  » municipalesResults__resultItem__nom–enabled »;
    texteElu = extraireTitre(d.tete) +  » « ;
    badge= » ‘;
    }
    tetedeliste = d.tete;
    if (langue == « en ») {
    tetedeliste = d.tete.replace(« M. « , «  »).replace(« Mme « , «  »);
    }

    // on vérifie si on n’a pas une tête de liste nuancée par LM,
    // et on l’ajoute au besoin
    if (d.nuance_lemonde) {
    if (d.nuance_lemonde != d.nuance) {
    if (langue == « fr ») {
    rabe_nuance_lm = « ,  » + textes_dec.tetedeliste +  »  » + dicoLM[d.nuance_lemonde].nom_court;
    } else {
    rabe_nuance_lm = dicoLM[d.nuance_lemonde] ? dicoLM[d.nuance_lemonde].nom_court_en : «  »;
    }
    }
    }

    // Composition du HTML
    html += ‘

  1. ‘;
    html +=  »;
    // la liste
    html += ‘
    ‘;
    html += ‘
    ‘;
    html += ‘

    ‘ + tetedeliste + badge + texteElu;
    if (langue == « fr ») {
    html +=  » ( » + nomCourt + rabe_nuance_lm + « ) « ;
    } else {
    html +=  » ( » + (rabe_nuance_lm ? rabe_nuance_lm : nomCourt) + « ) « ;
    }
    html += « 

    « ;
    html += ‘

    ‘ + (langue == « fr » ? milliers(prct) : thousands(prct)) + textes_dec.prct +  » –  » + (langue == « fr » ? milliers(d.voix) : thousands(d.voix)) +  »  » + textes_dec.voix + « 

    « ;
    html += « 

    « ;
    html += « 

    « ;
    html += « 

  2. « ;
    }
    }
    html += « 

« ;
return html;
}

// Remplir le dictionnaire de population
popData.forEach((row) => (dicoPop[row.code_insee] = +row[« PMUN »]));

// Paramètres pour les cercles
const minPop = 3000,
maxPop = d3.max(popData, (d) => +d[« PMUN »]),
valeursCercles = mobileDec ? [1, largeur / 38] : [1.5, largeur / 36],
unite = valeursCercles[1],
echelleRayon = d3.scaleSqrt().domain([minPop, maxPop]).range([valeursCercles[0], valeursCercles[1]]).clamp(true);

// Extraction des points et départements
const points = topojson.feature(geoData, geoData.objects.p_com),
departements = topojson.feature(geoData, geoData.objects.a_dept);

// Projection et path
const projection = d3
.geoIdentity()
.reflectY(true)
.fitSize([largeur * 0.95, largeur * ratioMun * 0.9], points),
path = d3.geoPath().projection(projection);

// Dessiner les départements/la Fronce
// dec_dept.selectAll(« .departement »).data(departements.features)tnter().append(« path »).attr(« class », « departement »).attr(« d », path);

dec_dept
.append(« path »)
.attr(« class », « departement »)
.attr(« d », path(topojson.merge(geoData, geoData.objects.a_dept.geometries)));

// la légende
const legende = svg
.append(« g »)
.attr(« class », « legende passelect »)
.attr(« transform », « translate( » + largeur * 0.075 + « ,  » + hauteur * 0.075 + « ) »);

const valeursLeg = mobileDec ? [200000, 2000000] : [100000, 700000, 2000000],
xLabel = largeur * 0.08;

legende
.selectAll(« circle.leg_stroke »)
.data(valeursLeg)
.join(« circle »)
.attr(« class », « leg_stroke »)
.attr(« cx », 0)
.attr(« cy », (d) => -echelleRayon(d))
.attr(« r », (d) => echelleRayon(d))
.style(« fill », « none »);

legende
.selectAll(« line.leg_stroke »)
.data(valeursLeg)
.join(« line »)
.attr(« class », « leg_stroke »)
.attr(« x1 », 0)
.attr(« x2 », xLabel)
.attr(« y1 », (d) => -echelleRayon(d) * 2)
.attr(« y2 », (d, i) => -echelleRayon(d) * 2)
.style(« stroke-dasharray », « 2,2 »);

legende
.selectAll(« text.texte_leg »)
.data(valeursLeg)
.join(« text »)
.attr(« class », « texte_leg »)
.attr(« x », xLabel * 1.1)
.attr(« y », (d) => 3 + -echelleRayon(d) * 2)
.text(function (d) {
if (langue == « fr ») {
return milliers(d) + (d >= 1000000 ?  » d’hab. » : «  »);
} else {
return thousands(d) + (d >= 1000000 ?  » inhab. » : «  »);
}
});

// Préparation des données pour les cercles
// + la liste pour le menu déroulant
const circlesData = points.features
.filter((d, i) => dicoPop[d.properties.c] !== undefined /*&& i < 600*/)
.map((d) => {
listeComm.push({
label: { name: d.properties.l +  » ( » + d.properties.d + « ) » },
data: { name: d.properties.l, dept: d.properties.d, c: d.properties.c },
});

const pop = dicoPop[d.properties.c] || 0;
var radiusTemp = 1;
// variables
if (pop) {
radiusTemp = echelleRayon(pop);
}
cXtemp = projection(d.geometry.coordinates)[0];
cYtemp = projection(d.geometry.coordinates)[1];
if (dicoPosition[d.properties.c]) {
tempProj = projection([dicoPosition[d.properties.c][0], dicoPosition[d.properties.c][1]]);
cXtemp = tempProj[0];
cYtemp = tempProj[1];
}

return {
cx: cXtemp,
cy: cYtemp,
r: radiusTemp,
l: d.properties.l,
d: d.properties.d,
id: d.properties.c,
p: dicoPop[d.properties.c],
};
});

const simulation = d3
.forceSimulation(circlesData)
.force(« x », d3.forceX((d) => d.cx).strength(0.5))
.force(« y », d3.forceY((d) => d.cy).strength(0.5))
.force(« collide », d3.forceCollide((d) => d.r + 0.2).strength(0.5))
.on(« tick », ticked)
.on(« end », () => {
simulation.stop();
});

// Dessin des cercles
const circles = dec_communes
.selectAll(« circle »)
.data(circlesData)
.enter()
.append(« circle »)
.attr(« class », function (d, i) {
if (resultDict[d.id]) {
resultBubulles++;
resultPop += d.p;
}
totalBubulles++;
totalPop += d.p;
return resultDict[d.id] ? « bubulle resultat » : « bubulle sansresultat »;
})
.attr(« data-insee », (d) => d.id)
.attr(« r », (d) => d.r)
.attr(« data-tt », function (d) {
if (resultDict[d.id]) {
e = resultDict[d.id];
let html= »

 » + d.l +  » ( » + d.d + « )

« ;
if (e) {
html += listerLesListes(e, 5);
} else {
html += ‘

‘ + textes_dec.pasresultat + « 

« ;
}
html +=  »;
html += ‘ »;
return html;
} else {
let html= »

 » + d.l +  » ( » + d.d + « )

« ;
html += « 

 » + textes_dec.noresult + « 

« ;
html +=  »;
html += ‘ »;
return html;
}
})
.attr(« data-status », function (d) {
if (resultDict[d.id]) {
// je fais le compte
if (!decompteTours[resultDict[d.id].status]) {
decompteTours[resultDict[d.id].status] = 0;
}
decompteTours[resultDict[d.id].status]++;
// je remplis
return resultDict[d.id] ? resultDict[d.id].status : «  »;
}
})
.attr(« data-bloc », function (d) {
if (resultDict[d.id]) {
if (dicoLM[resultDict[d.id].liste_1_nuance]) {
if (dicoLM[resultDict[d.id].liste_1_nuance].bloc) {
// on crée la dataGraphe
if (!dataGraphe[dicoLM[resultDict[d.id].liste_1_nuance].bloc]) {
dataGraphe[dicoLM[resultDict[d.id].liste_1_nuance].bloc] = { T1: 0, T2: 0 };
dataGraphePop[dicoLM[resultDict[d.id].liste_1_nuance].bloc] = { T1: 0, T2: 0 };
}
// on ajoute la population
// T2
if (resultDict[d.id].status == « Elue ») {
dataGraphe[dicoLM[resultDict[d.id].liste_1_nuance].bloc][« T2 »]++;
dataGraphePop[dicoLM[resultDict[d.id].liste_1_nuance].bloc][« T2 »] += d.p;
}
// T1
if (resultDict[d.id].status == « Elue en T1 ») {
dataGraphe[dicoLM[resultDict[d.id].liste_1_nuance].bloc][« T1 »]++;
dataGraphePop[dicoLM[resultDict[d.id].liste_1_nuance].bloc][« T1 »] += d.p;
}
// on remplit le décompte
if (!decompteBlocs[dicoLM[resultDict[d.id].liste_1_nuance].bloc]) {
decompteBlocs[dicoLM[resultDict[d.id].liste_1_nuance].bloc] = 0;
}
decompteBlocs[dicoLM[resultDict[d.id].liste_1_nuance].bloc]++;
return dicoLM[resultDict[d.id].liste_1_nuance].bloc;
}
} else if (dicoMin[resultDict[d.id].liste_1_nuance]) {
if (dicoMin[resultDict[d.id].liste_1_nuance].bloc) {
// on crée la dataGraphe
if (!dataGraphe[dicoMin[resultDict[d.id].liste_1_nuance].bloc]) {
dataGraphe[dicoMin[resultDict[d.id].liste_1_nuance].bloc] = { T1: 0, T2: 0 };
dataGraphePop[dicoMin[resultDict[d.id].liste_1_nuance].bloc] = { T1: 0, T2: 0 };
}
// on ajoute la population
// T2
if (resultDict[d.id].status == « Elue ») {
dataGraphe[dicoMin[resultDict[d.id].liste_1_nuance].bloc][« T2 »]++;
dataGraphePop[dicoMin[resultDict[d.id].liste_1_nuance].bloc][« T2 »] += d.p;
}
// T1
if (resultDict[d.id].status == « Elue en T1 ») {
dataGraphe[dicoMin[resultDict[d.id].liste_1_nuance].bloc][« T1 »]++;
dataGraphePop[dicoMin[resultDict[d.id].liste_1_nuance].bloc][« T1 »] += d.p;
}
// on remplit le décompte ministère
if (!decompteBlocs[dicoMin[resultDict[d.id].liste_1_nuance].bloc]) {
decompteBlocs[dicoMin[resultDict[d.id].liste_1_nuance].bloc] = 0;
}
decompteBlocs[dicoMin[resultDict[d.id].liste_1_nuance].bloc]++;
return dicoMin[resultDict[d.id].liste_1_nuance].bloc;
}
}
}
})
.attr(« data-nuance-lm », function (d) {
if (resultDict[d.id] && resultDict[d.id].liste_1_nuance_lemonde) {
// on remplit le décompte LM
if (!decompteNuances[resultDict[d.id].liste_1_nuance_lemonde]) {
decompteNuances[resultDict[d.id].liste_1_nuance_lemonde] = 0;
}
decompteNuances[resultDict[d.id].liste_1_nuance_lemonde]++;
return resultDict[d.id].liste_1_nuance_lemonde;
} else if (resultDict[d.id] && resultDict[d.id].liste_1_nuance) {
if (!decompteNuances[resultDict[d.id].liste_1_nuance]) {
decompteNuances[resultDict[d.id].liste_1_nuance] = 0;
}
decompteNuances[resultDict[d.id].liste_1_nuance]++;
return resultDict[d.id].liste_1_nuance;
}
})
.attr(« style », function (d) {
if (resultDict[d.id]) {
// je stocke les blocs depuis les nuances LM
if (dicoLM[resultDict[d.id].liste_1_nuance]) {
if (listeBlocs.indexOf(dicoLM[resultDict[d.id].liste_1_nuance].bloc) == -1) {
listeBlocs.push(dicoLM[resultDict[d.id].liste_1_nuance].bloc);
}
} else if (dicoMin[resultDict[d.id].liste_1_nuance]) {
// les blocs depuis les nuances ministère
if (listeBlocs.indexOf(dicoMin[resultDict[d.id].liste_1_nuance].bloc) == -1) {
listeBlocs.push(dicoMin[resultDict[d.id].liste_1_nuance].bloc);
}
} else {
console.warn(« ouille, pas de nuance « intérieur » », d.id, resultDict[d.id].liste_1_nuance);
}
if (resultDict[d.id].liste_1_nuance_lemonde) {
if (listeNuancesLM.indexOf(resultDict[d.id].liste_1_nuance_lemonde) == -1) {
listeNuancesLM.push(resultDict[d.id].liste_1_nuance_lemonde);
}
}
if (dicoLM[resultDict[d.id].liste_1_nuance_lemonde]) {
styleCercle = « –color:  » + dicoLM[resultDict[d.id].liste_1_nuance_lemonde].couleur + « ; –dark-color:  » + dicoLM[resultDict[d.id].liste_1_nuance_lemonde].couleur_dark + « ;–stroke-color: » + d3.rgb(dicoLM[resultDict[d.id].liste_1_nuance_lemonde].couleur).darker(1) + « ;–stroke-dark-color: » + d3.rgb(dicoLM[resultDict[d.id].liste_1_nuance_lemonde].couleur_dark).darker(1) + « ; »;
} else if (dicoMin[resultDict[d.id].liste_1_nuance]) {
styleCercle = « –color:  » + dicoMin[resultDict[d.id].liste_1_nuance].couleur + « ; –dark-color:  » + dicoMin[resultDict[d.id].liste_1_nuance].couleur_dark + « ;–stroke-color: » + d3.rgb(dicoMin[resultDict[d.id].liste_1_nuance].couleur).darker(1) + « ;–stroke-dark-color: » + d3.rgb(dicoMin[resultDict[d.id].liste_1_nuance].couleur_dark).darker(1) + « ; »;
}
return styleCercle;
} else {
return « –color: #323232; –dark-color: #A4A4A4;–stroke-color:#636363;–stroke-dark-color:#cdcdcd; »;
}
})
.style(« stroke-width », (d) => (resultDict[d.id] ? 0 : 0.25))
.on(« mouseover », function () {
d3.select(this).style(« stroke-width », 1.5);
if (!mobileDec) {
showHoverTooltip(this);
}
})
.on(« mouseout », function () {
d3.select(this).style(« stroke-width », (d) => (resultDict[d.id] ? 0 : 0.25));
removeHoverTooltip();
});

// on lance la création du graphe
dessinerGraphe(« #graphe_t1t2 », dataGraphe, dataGraphePop, dicoBloc);

// j’ajoute le texte pour les DROM
if (langue == « fr ») {
var listeDrom = [
// ligne du bas
{ nom: « S.-Pierre-et-M. », nom_court: « SPM », hauteur: 0.93, largeur: 0.2 },
{ nom: « Nouvelle-Calédonie », nom_court: « N.-Calédonie », hauteur: 0.93, largeur: 0.39 },
{ nom: « Polynésie fr. », nom_court: « Poly. fr. », hauteur: 0.93, largeur: 0.57 },
{ nom: « Mayotte », nom_court: « Mayotte », hauteur: 0.93, largeur: 0.74 },
// ligne du haut ligne
{ nom: « Guadeloupe », nom_court: « Guadeloupe », hauteur: 0.73, largeur: 0.2 },
{ nom: « Martinique », nom_court: « Martinique », hauteur: 0.73, largeur: 0.37 },
{ nom: « Guyane », nom_court: « Guyane », hauteur: 0.73, largeur: 0.55 },
{ nom: « La Réunion », nom_court: « Réunion », hauteur: 0.73, largeur: 0.74 },
];
} else {
var listeDrom = [
// ligne du bas
{ nom: « St. Pierre and M. », nom_court: « SPM », hauteur: 0.93, largeur: 0.2 },
{ nom: « New Caledonia », nom_court: « N-Caledonia », hauteur: 0.93, largeur: 0.39 },
{ nom: « French Polynesia », nom_court: « Fr Polynesia », hauteur: 0.93, largeur: 0.57 },
{ nom: « Mayotte », nom_court: « Mayotte », hauteur: 0.93, largeur: 0.74 },
// ligne du haut ligne
{ nom: « Guadeloupe », nom_court: « Guadeloupe », hauteur: 0.73, largeur: 0.2 },
{ nom: « Martinique », nom_court: « Martinique », hauteur: 0.73, largeur: 0.37 },
{ nom: « French Guiana », nom_court: « Fr Guiana », hauteur: 0.73, largeur: 0.55 },
{ nom: « La Réunion », nom_court: « Réunion », hauteur: 0.73, largeur: 0.74 },
];
}

dec_textes
.selectAll(« text.ile »)
.data(listeDrom)
.enter()
.append(« text »)
.attr(« class », « ile passelect »)
.attr(« x », (d) => largeur * d.largeur)
.attr(« y », (d) => hauteur * (mobileDec ? d.hauteur – 0.02 : d.hauteur))
.text((d) => (mobileDec ? d.nom_court : d.nom));

// après la création des bubulles, on peut remplir
// le premier para avec quelques infos au long
if (langue == « fr » && resultData.length > 0) {
texte_html = « Cette carte de France affiche les résultats des élections municipales dans les  » + milliers(resultBubulles) +  » communes françaises dont les listes se voient attribuer une couleur politique par les préfectures. Elles représentent  » + milliers(resultPop) + (resultPop < 1000000 ?  »  » :  » d’ ») + « habitants. »;
if (resultBubulles < decompteTours[« Elue en T1″]) {
texte_html +=  » Parmi elles,  » + milliers(decompteTours[« Elue en T1″]) +  » ont déjà une élu une liste avec plus de 50 % des voix au premier tour. »;
}
document.querySelector(« #dec_premier_para »).innerHTML = texte_html;
} else {
document.getElementById(« dec_premier_para »).remove();
}

/*██ /██ /██
| ██ /██__/ | ██
| ██ /██████ /██████ /██████ /███████ /███████ /██████
| ██ /██__ ██ /██__ ██ /██__ ██| ██__ ██ /██__ ██ /██__ ██
| ██| ████████| ██ ██| ████████| ██ ██| ██ | ██| ████████
| ██| ██_____/| ██ | ██| ██_____/| ██ | ██| ██ | ██| ██_____/
| ██| ███████| ███████| ███████| ██ | ██| ███████| ███████
|__/ _______/ ____ ██ _______/|__/ |__/ _______/ _______/
/██ ██
| ██████/ *INTERACTIVE
_____*/

if (resultData.length > 0) {
var legendeBlocs= »

 » + textes_dec.toutafficher + « 

« ;

listeBlocs.sort((a, b) => {
const valA = dicoBloc[a] ? dicoBloc[a].n : Infinity;
const valB = dicoBloc[b] ? dicoBloc[b].n : Infinity;
return valA – valB;
});

for (const [i, d] of listeBlocs.entries()) {
if (dicoBloc[d]) {
e = dicoBloc[d];
if (decompteBlocs[e.bloc] >= minLegende) {
legendeBlocs += ‘

‘ + (langue == « fr » ? e.bloc : e.bloc_en) + ‘ ‘ + decompteBlocs[e.bloc] + « 

« ;
}
}
}

legendeCouleurs = d3.select(« #dec_leg_min »).html(legendeBlocs);

// on va faire aussi une légende avec les nuances LM
var legendeNuancesLM = ‘

‘ + textes_dec.toutafficher + « 

« ;

listeNuancesLM.sort((a, b) => {
const valA = dicoLM[a] ? dicoLM[a].n : Infinity;
const valB = dicoLM[b] ? dicoLM[b].n : Infinity;
return valA – valB;
});

for (const [i, d] of listeNuancesLM.entries()) {
if (dicoLM[d]) {
e = dicoLM[d];
if (decompteNuances[e.code] >= minLegende) {
legendeNuancesLM += ‘

‘ + (langue == « fr » ? e.nom_court : e.nom_court_en) + ‘ ‘ + decompteNuances[e.code] + « 

« ;
}
}
}

legendeCouleursLM = d3.select(« #dec_leg_lm »).html(legendeNuancesLM);

var leg_tours= »

 » + textes_dec.toutafficher + « 

« ;
leg_tours += ‘

‘ + textes_dec.deuxiemetour +  »  » + (langue == « fr » ? milliers(decompteTours[« Elue »]) : thousands(decompteTours[« Elue »])) + « 

« ;
leg_tours += ‘

‘ + textes_dec.liste_elue +  »  » + (langue == « fr » ? milliers(decompteTours[« Elue en T1 »]) : thousands(decompteTours[« Elue en T1 »])) + « 

« ;

var legendeStatus = d3.select(« #dec_leg_elu »).html(leg_tours);

// Variables pour stocker les sélections
var selectedStatus = « tout »;
var selectedBloc = « tout »;
var selectedNuanceLM = « tout »;

// Fonction de filtrage combiné
function filtrerLesCercles() {
svg.selectAll(« circle.bubulle »).classed(« opacified », function () {
var currentStatus = d3.select(this).attr(« data-status »);
var currentNuance = d3.select(this).attr(« data-bloc »);
var currentNuanceLM = d3.select(this).attr(« data-nuance-lm »);

// Condition pour vérifier si le cercle doit être affiché
var isVisible = (currentStatus === selectedStatus || selectedStatus === « tout ») && (currentNuance === selectedBloc || selectedBloc === « tout ») && (currentNuanceLM === selectedNuanceLM || selectedNuanceLM === « tout »);

return !isVisible;
});
}

// Interaction pour le status
legendeStatus.selectAll(« .dec_status »).on(« click », function () {
d3.selectAll(« .tooltipbrt »).remove();
d3.selectAll(« #dec_leg_elu .dec_status »).classed(« choisi », false);
d3.select(this).classed(« choisi », true);

selectedStatus = d3.select(this).attr(« data-status »);
filtrerLesCercles();
});

// Interaction pour les nuances
legendeCouleurs.selectAll(« .dec_nuance_min »).on(« click », function () {
d3.selectAll(« .tooltipbrt »).remove();
d3.selectAll(« .dec_nuance_min »).classed(« choisi », false);
d3.select(this).classed(« choisi », true);

selectedBloc = d3.select(this).attr(« data-bloc »);
selectedNuanceLM = « tout »;
d3.selectAll(« .dec_nuance_lm »).classed(« choisi », false);
d3.selectAll(« .dec_nuance_lm.latotale »).classed(« choisi », true);
d3.select(this).classed(« choisi », true);
filtrerLesCercles();
});

// Interaction pour nuance LM
legendeCouleursLM.selectAll(« .dec_nuance_lm »).on(« click », function () {
d3.selectAll(« .tooltipbrt »).remove();
d3.selectAll(« #dec_leg_lm .dec_nuance_lm »).classed(« choisi », false);
d3.select(this).classed(« choisi », true);

selectedNuanceLM = d3.select(this).attr(« data-nuance-lm »);
selectedBloc = « tout »;
d3.selectAll(« .dec_nuance_min »).classed(« choisi », false);
d3.selectAll(« .dec_nuance_min.latotale »).classed(« choisi », true);
filtrerLesCercles();
});
}

// fin de la gestion de la légende interactive

// Mise à jour des positions des cercles
function ticked() {
circles.attr(« cx », (d) => d.x).attr(« cy », (d) => d.y);
}

// Initialisation du tooltip
const { showTooltipManually, showTooltip } = initTooltipBrt(« #dec_mun .bubulle »);

// la fonction qui traite les résultats
const func_to_treat_result_donnees = (result) => {
setTimeout(() => d3.selectAll(« #dec_mun circle.bubulle »).classed(« opacified », (d) => d.id !== result.data.c), 25);

const selectedCircle = document.querySelector(`#dec_mun circle.bubulle[data-insee= »${result.data.c} »]`);
if (selectedCircle) {
showTooltipManually(selectedCircle);
selectedCircle.scrollIntoView({ behavior: « smooth », block: « center » });
}
};

// Initialisation de l’autocomplete
if (langue == « fr ») {
autocomplete_decodeurs(« search_mun », listeComm, func_to_treat_result_donnees, reset_func, (x) => `${x.label.name}`, 6, 3, slugify, « Aucune commune avec ce nom »);
} else {
autocomplete_decodeurs(« search_mun », listeComm, func_to_treat_result_donnees, reset_func, (x) => `${x.label.name}`, 6, 3, slugify, « No town or city with that name »);
}
// quand on tape échap
document.addEventListener(« keydown », function (event) {
if (event.key === « Escape ») {
reset_func();
const circles = getA(« #dec_mun circle.bubulle »);
var selectedStatus = « tout »;
var selectedBloc = « tout »;
var selectedNuanceLM = « tout »;

forEach(circles, function (circle) {
circle.classList.remove(« selected-commune »);
});
d3.selectAll(« .tooltipbrt »).remove();
}
});

// Gestionnaire pour fermer le tooltip en cliquant ailleurs
document.addEventListener(« click », function (e) {
if (!e.target.closest(« #dec_mun circle ») && !e.target.closest(« #search_mun »)) {
const circles = getA(« #dec_mun circle.bubulle »);
forEach(circles, function (circle) {
circle.classList.remove(« selected-commune »);
});
}
});
}
);
}

// Fonction pour réinitialiser la carte
const reset_func = () => {
document.querySelector(« #search_mun input »).value = «  »;
d3.selectAll(« #dec_mun circle.bubulle »).classed(« opacified », false);
d3.select(« #search_mun .lmui-search__button »).style(« visibility », « visible »);
d3.select(« #search_mun .lmui-search__reset-button »).style(« visibility », « hidden »);
// on remet à zéro les filtres
d3.selectAll(« .dec_nuance »).classed(« choisi », false);
d3.selectAll(« .dec_nuance.latotale »).classed(« choisi », true);
var selectedBloc = « tout »;
var selectedNuanceLM = « tout »;
};

dessinerBubulles(langue);

Les résultats des principaux partis et blocs politiques

`;
});
toFill += `

${textsMap[`color_note_${currentBloc}`]}


${textsMap.precision_methodo}

`;
htmlContainer.innerHTML = toFill;
}

function inittooltipdecodeurs(selector) {
let currentTooltip = null;
function removeCurrentTooltip() {
if (currentTooltip) {
currentTooltip.remove();
currentTooltip = null;
}
}
function showTooltip(elmt) {
removeCurrentTooltip();

let ttFixe = document.createElement(« div »);
ttFixe.classList.add(« tooltipdecodeurs »);
ttFixe.innerHTML = elmt.getAttribute(« data-tt ») + « ;

document.body.appendChild(ttFixe);

// Ne pas appliquer de positionnement en JavaScript en mobile
if (!isMobile) {
const rect = elmt.getBoundingClientRect();
ttFixe.style.top = rect.top – 14 + window.scrollY – ttFixe.offsetHeight + « px »;
ttFixe.style.left = rect.left + rect.width / 2 – ttFixe.offsetWidth / 2 + 2 + « px »;
}

// Ajouter un gestionnaire pour fermer le tooltip en cliquant sur la croix en mobile
ttFixe.addEventListener(« click », function (e) {
if (e.target.tagName === « DIV » && e.target.classList.contains(« tooltipdecodeurs ») && window.innerWidth <= 600) {
removeCurrentTooltip();
reset_func();
}
});

currentTooltip = ttFixe;
return ttFixe;
}

// Sélectionne les éléments
let ttFixe_elmts = getA(selector);

// Ajoute les événements
forEach(ttFixe_elmts, function (elmt) {
elmt.addEventListener(« mouseover », function () {
showTooltip(this);
});

elmt.addEventListener(« mouseout », function () {
// Ne supprime pas le tooltip si on a cliqué sur une commune
if (!this.classList.contains(« selected-commune »)) {
removeCurrentTooltip();
}
});
});

// Retourne la fonction pour afficher le tooltip manuellement
return function (elmt) {
// Marque le cercle comme sélectionné
forEach(ttFixe_elmts, function (e) {
e.classList.remove(« selected-commune »);
});
elmt.classList.add(« selected-commune »);

return showTooltip(elmt);
};
}

const formatNb = (number, locale= »fr-FR », round = 2, style= »decimal ») => {
return `${number.toLocaleString(locale, { maximumFractionDigits: round, style })}`;
};

async function drawMap(langue) {

// Variables globales

const largeur = document.getElementById(`dec_mun_prct_${snippetId}`).offsetWidth;
const isMobile = largeur < 600;
const ratioMun = isMobile ? 1.4 : 1.2;
const hauteur = largeur * ratioMun;
const isDark = document.querySelector(« html »).getAttribute(« data-color-mode ») === « dark »;

// Initialisation du SVG

const svg = d3
.select(`#dec_mun_prct_${snippetId}`)
.append(« svg »)
.attr(« width », « 100% »)
.attr(« viewBox », `${largeur * -0.025} ${largeur * ratioMun * -0.05} ${largeur} ${largeur * ratioMun}`);

const decDept = svg.append(« g »);
const decCommunes = svg.append(« g »);
const decTextes = svg.append(« g »);

// Données

const geoData = await d3.json(« //assets-decodeurs.lemonde.fr/decodeurs/assets/municipales2026/brt_3500.json »);
const popData = await d3.csv(« //assets-decodeurs.lemonde.fr/decodeurs/assets/municipales2026/commplus3500.csv »);

const dicoPop = {};
const listeComm = [];
function agregerResultats(datasetsArray) {
const dictGlobal = {};
(datasetsArray || []).forEach(dataset => {
if (!dataset?.listes_communes) return;
const nuanceCode = dataset.code;

dataset.listes_communes.forEach(current => {
if (!current || current.code_circo == null) return;
const id = current.code_circo;
const statusCommune = current.status;
if (!dictGlobal[id]) {
dictGlobal[id] = {
…current,
voix: 0,
status: null,
listes: [],
detailListes: [],
};
}
const isEnAttente = statusCommune === « En attente »;
if (isEnAttente) {
if (dictGlobal[id].status !== « disponible ») {
dictGlobal[id].status = « en_attente »;
}
return;
}
// Résultats disponibles : agrégation bloc
const voixListe = +current.voix || 0;
dictGlobal[id].voix += voixListe;
dictGlobal[id].status = « disponible »;
if (nuanceCode && !dictGlobal[id].listes.includes(nuanceCode)) {
dictGlobal[id].listes.push(nuanceCode);
}
// Détail par liste pour cette commune
const exprimés = current.mentions?.exprimes;
const hasExprimes = exprimés != null && +exprimés > 0;
const prctListe = hasExprimes && voixListe > 0
? (voixListe / +exprimés) * 100
: 0;
const tete = current.tete || {};
const eluPremierTour = current.ordre === 1 && current.pourvu === « T1 »;
const eluDeuxiemeTour = current.ordre === 1 && current.pourvu === « T2 »;

dictGlobal[id].detailListes.push({
nuance: nuanceCode,
nuance_lemonde: current.nuance_lemonde ?? null,
voix: voixListe,
prct: prctListe,
tete: {
nom: tete.nom || null,
prenom: tete.prenom || null,
civ: tete.civ || null,
},
eluPremierTour,
eluDeuxiemeTour,
});
});
});
// Calcul du % global bloc par commune
Object.values(dictGlobal).forEach(commune => {
const exprimés = commune.mentions?.exprimes;
const hasExprimes = exprimés != null && +exprimés > 0;
if (commune.status === « disponible » && commune.voix != null && hasExprimes) {
commune.prct = (+commune.voix / +exprimés) * 100;
} else if (commune.status === « en_attente ») {
commune.prct = null;
} else {
commune.prct = 0;
}
});
return dictGlobal;
}

const datasetsT1init = (dataByBlocT1[currentBloc] || []).map(d => d.nuanceData).filter(Boolean);
const result2026DictT1 = agregerResultats(datasetsT1init);
const datasetsT2init = (dataByBlocT2[currentBloc] || []).map(d => d.nuanceData).filter(Boolean);
const result2026DictT2 = agregerResultats(datasetsT2init);

const result2026Dict = currentTour === « T2 » ? result2026DictT2 : result2026DictT1;

// console.log(« ——result2026Dict », result2026Dict);

const allPrctValues = [
…Object.values(result2026Dict).map(d => d.prct),
];

const maxPrct = typeof d3.max(allPrctValues) == ‘undefined’ || d3.max(allPrctValues) < 10 ? 10 : d3.max(allPrctValues);

// Adapter la couleur en fonction du bloc
function getBlocColorMax(bloc) {
const colors = blocColors[bloc];
const couleur = colors?.couleur;
const couleurDark = colors?.couleur_dark;
if (!couleur && !couleurDark) return colorMax;
return isDark ? (d3.rgb(couleurDark).darker(1) || d3.rgb(couleur).darker(1)) : (d3.rgb(couleur).darker(1) || d3.rgb(couleurDark).darker(1));
}

let colorScale = d3.scaleSequential(
d3.interpolate(colorMin, getBlocColorMax(currentBloc))
).domain([0, maxPrct]);

// Création de la légende colorée
const legendWidth = isMobile ? largeur * 0.7 : largeur * 0.5;
const legendHeight = isMobile ? 10 : 12;
const legendMargin = { top: isMobile ? 18 : 15, right: isMobile ? 40 : 20, bottom: 18, left: isMobile ? 12 : 20 };

const legendSvg = d3.select(« .legend_color »)
.append(« svg »)
.attr(« width », « 100% »)
.attr(« height », legendHeight + legendMargin.top + legendMargin.bottom);

const legendContainerEl = document.querySelector(`.dec_mun_prct_container[data-attr=${snippetId}] .legend_color`);
const legendContainerWidth = legendContainerEl ? legendContainerEl.offsetWidth : largeur;

const legendRoot = legendSvg.append(« g »)
.attr(« transform », `translate(${(legendContainerWidth / 2) – (legendWidth / 2)}, ${legendMargin.top})`);

const legendScore = legendRoot.append(« g »)
.attr(« id », « legend_score »);

// dégradé score
const gradientId = `score-gradient-${snippetId}`;
const gradient = legendSvg.append(« defs »)
.append(« linearGradient »)
.attr(« id », gradientId)
.attr(« x1 », « 0% »)
.attr(« x2 », « 100% »);

// Ajout des stops de couleur
const numStops = 10;
for (let i = 0; i <= numStops; i++) {
const value = (i / numStops) * maxPrct;
gradient.append(« stop »)
.attr(« offset », `${(i / numStops) * 100}%`)
.attr(« stop-color », colorScale(value));
}

// Barre de dégradé
legendScore.append(« rect »)
.attr(« class », « legend_color_bar »)
.attr(« width », legendWidth)
.attr(« height », legendHeight)
.style(« fill », `url(#${gradientId})`);

// Labels min et max
const legendScale = d3.scaleLinear()
.domain([0, maxPrct])
.range([0, legendWidth]);

const legendAxis = d3.axisBottom(legendScale)
.ticks(6)
.tickFormat((d) => Math.round(d) +  » % »);

legendScore.append(« g »)
.attr(« class », « legend_color_axis »)
.attr(« transform », `translate(0, ${legendHeight})`)
.call(legendAxis)
.call(g => g.select(« .domain »).remove());

// Titre de la légende
legendScore.append(« text »)
.attr(« class », « legend_color_title »)
.attr(« x », legendWidth / 2)
.attr(« y », -5)
.attr(« text-anchor », « middle »)
.text(`${textsMap.titre_legende_score} ${textsMap.prct}`);

function buildListeRows(detailBloc) {
let html= »

    « ;
    detailBloc.forEach((l) => {
    const colors = nuanceMinistere[l.nuance] ?? blocColors[currentBloc] ?? {};
    const couleur = colors.couleur || « #888″;
    const couleur_dark = colors.couleur_dark || couleur;
    const nomComplet = [l.tete?.civ, l.tete?.prenom, l.tete?.nom].filter(Boolean).join( » « );
    const prctStr = l.prct != null ? `${formatNb(l.prct, langue, 1)}${langue === « fr-FR » ?  » % » : « % »}` : «  »;
    const voixStr = `${formatNb(l.voix, langue, 0)} ${textsMap.voix}`;
    const isElu = l.eluPremierTour || l.eluDeuxiemeTour;
    const badge = isElu
    ? ``
    : «  »;
    html += ‘
  1. ‘;
    html += « ;
    html += ‘
    ‘;
    let libelleNuance = nuanceNomCourt[l.nuance] ?? l.nuance ?? « – »;
    if (l.nuance_lemonde && l.nuance_lemonde !== l.nuance) {
    const nomCourtLM = nuanceNomCourt[l.nuance_lemonde] ?? l.nuance_lemonde;
    const teteDeListeLabel = textsMap.tete_de_liste ?? « tête de liste »;
    libelleNuance += « ,  » + teteDeListeLabel +  »  » + nomCourtLM;
    }
    html += `

    ${nomComplet} ${badge} (${libelleNuance})

    `;
    html += `

    ${prctStr} – ${voixStr}

    `;
    html += « 

  2. « ;
    });
    html += « 

« ;
return html;
}
function makeTooltipContent(d) {
const blocName = langue === « fr-FR » ? currentBloc : blocColors[currentBloc]?.bloc_en ?? currentBloc;
let html = `

${d.l} (${d.d})

`;
// Tour 2
html += `

${textsMap.tour2 ?? « Tour 2 »}

`;

const s2 = d.statusBlocT2 || « aucune_liste »;
if (s2 === « aucune_liste ») {
if (d.pasDeTourDeux) {
html += `

${textsMap.pas_de_second_tour ?? « Pas de second tour : une liste a été élue dès le premier tour »}

`;
} else {
html += `

${textsMap.aucune_liste_bloc}

`;
}
} else if (s2 === « en_attente ») {
html += `

${textsMap.en_attente}

`;
} else {
html += `

${d.value2026T2 != null ? `${formatNb(d.value2026T2, langue, 1)}${textsMap.suffrages} ${blocName}` : textsMap.pasresultat}

`;
if (d.detailBlocT2.length > 0) {
html += « ;
html += buildListeRows(d.detailBlocT2);
}
}
html += « ;
// Tour 1
html += `

${textsMap.tour1 ?? « Tour 1 »}

`;
const s1 = d.statusBlocT1 || « aucune_liste »;
if (s1 === « en_attente ») {
html += `

${textsMap.en_attente}

`;
} else if (s1 === « aucune_liste ») {
html += `

${textsMap.aucune_liste_bloc}

`;
} else {
html += `

${d.value2026T1 != null ? `${formatNb(d.value2026T1, langue, 1)}${textsMap.suffrages} ${blocName}` : textsMap.pasresultat}

`;
if (d.detailBlocT1.length > 0) {
html += « ;
html += buildListeRows(d.detailBlocT1);
}
}
html += `

${textsMap.pop_mun} ${formatNb(d.p, langue, 0)} ${textsMap.hab}

`;
return html;
}

// Remplir le dictionnaire de population
popData.forEach((row) => (dicoPop[row.code_insee] = +row[« PMUN »]));

// Paramètres pour les cercles
const minPop = 3000;
const maxPop = d3.max(popData, (d) => +d[« PMUN »]);

const valeursCercles = isMobile ? [1, largeur / 38] : [1.5, largeur / 36];
// Échelle pour le rayon des cercles
const echelleRayon = d3
.scaleSqrt()
.domain([minPop, maxPop])
.range([valeursCercles[0], valeursCercles[1]])
.clamp(true);
// Extraction des points et départements
const points = topojson.feature(geoData, geoData.objects.p_com);
// const departements = topojson.feature(geoData, geoData.objects.a_dept);

// Projection et path
const projection = d3
.geoIdentity()
.reflectY(true)
.fitSize([largeur * 0.95, largeur * ratioMun * 0.9], points);
const path = d3.geoPath().projection(projection);

// Dessin des départements
decDept.append(« path »).attr(« class », « departement »).attr(« d », path(topojson.merge(geoData, geoData.objects.a_dept.geometries)));

// la légende taille des cercles
const legende = svg
.append(« g »)
.attr(« class », « legende »)
.attr(« transform », « translate( » + largeur * 0.075 + « ,  » + hauteur * 0.075 + « ) »);

const valeursLeg = isMobile ? [200000, 2000000] : [100000, 700000, 2000000];
const xLabel = largeur * 0.08;

legende
.selectAll(« circle.leg_stroke »)
.data(valeursLeg)
.join(« circle »)
.attr(« class », « leg_stroke »)
.attr(« cx », 0)
.attr(« cy », (d) => -echelleRayon(d))
.attr(« r », (d) => echelleRayon(d))
.style(« fill », « none »);

legende
.selectAll(« line.leg_stroke »)
.data(valeursLeg)
.join(« line »)
.attr(« class », « leg_stroke »)
.attr(« x1 », 0)
.attr(« x2 », xLabel)
.attr(« y1 », (d) => -echelleRayon(d) * 2)
.attr(« y2 », (d, i) => -echelleRayon(d) * 2)
.style(« stroke-dasharray », « 2,2 »);

legende
.selectAll(« text.texte_leg »)
.data(valeursLeg)
.join(« text »)
.attr(« class », « texte_leg »)
.attr(« x », xLabel * 1.1)
.attr(« y », (d) => 3 + -echelleRayon(d) * 2)
.text(function (d) {
if (d >= 1000000) {
const millions = Math.round(d / 1000000);
if (langue === « fr-FR ») {
return `${millions} million${millions > 1 ? « s » : «  »} d’hab.`;
}
return `${millions} million inhab.`;
}
return formatNb(d, langue, 0);
});

// Préparation des données pour les cercles
const circlesData = points.features
.filter((d) => dicoPop[d.properties.c] !== undefined)
.map((d) => {
listeComm.push({
label: { name: d.properties.l +  » ( » + d.properties.d + « ) » },
data: { name: d.properties.l, dept: d.properties.d, c: d.properties.c },
});

const id = d.properties.c;
const pop = dicoPop[id] || 0;
let radiusTemp = pop ? echelleRayon(pop) : 1;

let cXtemp = projection(d.geometry.coordinates)[0];
let cYtemp = projection(d.geometry.coordinates)[1];
if (dicoPosition[id]) {
const tempProj = projection([dicoPosition[id][0], dicoPosition[id][1]]);
cXtemp = tempProj[0];
cYtemp = tempProj[1];
}

const resultEntryT1 = result2026DictT1[id];
const resultEntryT2 = result2026DictT2[id];
const pasDeTourDeux = (resultEntryT1?.detailListes ?? []).some(l => l.eluPremierTour);

return {
cx: cXtemp,
cy: cYtemp,
r: radiusTemp,
l: d.properties.l,
d: d.properties.d,
id,
p: dicoPop[id],
// Données T1
value2026T1: resultEntryT1 ? resultEntryT1.prct : null,
statusBlocT1: resultEntryT1?.status ?? « aucune_liste »,
detailBlocT1: resultEntryT1?.detailListes ?? [],
// Données T2
value2026T2: resultEntryT2 ? resultEntryT2.prct : null,
statusBlocT2: resultEntryT2?.status ?? « aucune_liste »,
detailBlocT2: resultEntryT2?.detailListes ?? [],
pasDeTourDeux,
};
});

// Dessin des cercles
const circles = decCommunes
.selectAll(« circle »)
.data(circlesData)
.enter()
.append(« circle »)
.attr(« data-insee », d => d.id)
.attr(« class », « bubulle »)
.attr(« r », d => d.r)
.attr(« data-tt », d => makeTooltipContent(d))
.attr(« fill », d => {
const value = currentTour === « T1 » ? d.value2026T1 : d.value2026T2;
if (value === null) return `var(–prct-no-result)`;
return colorScale(value);
})
.attr(« stroke », d => {
const value = currentTour === « T1 » ? d.value2026T1 : d.value2026T2;
return value === null
? `var(–prct-no-result-stroke)`
: d3.rgb(
colorScale(value)
).darker(1);
})
.style(« stroke-width », d =>
(currentTour === « T1 » ? d.value2026T1 : d.value2026T2) === null ? 0 : 0.25
)
.on(« mouseover », function () {
d3.select(this).style(« stroke-width », 1.5);
// terre brûlée, on vire la classe .opacified partout
d3.selectAll(`#dec_mun_prct_${snippetId} circle.bubulle`).classed(« opacified », false);
})
.on(« mouseout », function () {
d3.select(this).style(« stroke-width », (d) => ((currentTour === « T1 » ? d.value2026T1 : d.value2026T2) === null ? 0 : 0.25));
});

decTextes
.selectAll(« text.ile »)
.data(textDrom)
.enter()
.append(« text »)
.attr(« class », « ile »)
.attr(« x », (d) => largeur * d.largeur)
.attr(« y », (d) => hauteur * (isMobile ? d.hauteur – 0.02 : d.hauteur))
.text((d) => (isMobile ? d.nom_court : d.nom));

// Mise à jour des positions des cercles
function ticked() {
circles.attr(« cx », (d) => d.x).attr(« cy », (d) => d.y);
}

const simulation = d3
.forceSimulation(circlesData)
.force(« x », d3.forceX((d) => d.cx).strength(0.5))
.force(« y », d3.forceY((d) => d.cy).strength(0.5))
.force(« collide », d3.forceCollide((d) => d.r + 0.1).strength(0.5))
.on(« tick », ticked)
.on(« end », () => {
simulation.stop();
});

// Watch bloc update
const blocDivs = document.querySelectorAll(`.dec_mun_prct_container[data-attr=${snippetId}] .legend_bloc`);
blocDivs.forEach(bloc => {
bloc.addEventListener(‘click’, () => {
const blocClicked = bloc.dataset.bloc;
blocDivs.forEach(bloc => {
bloc.classList.remove(‘legend_bloc__active’);
});
bloc.classList.add(‘legend_bloc__active’);
switchBloc(blocClicked);
});
});

function switchBloc(bloc) {
currentBloc = bloc;
const dsT1 = (dataByBlocT1[bloc] || []).map(d => d.nuanceData).filter(Boolean);
const dsT2 = (dataByBlocT2[bloc] || []).map(d => d.nuanceData).filter(Boolean);
const newDictT1 = agregerResultats(dsT1);
const newDictT2 = agregerResultats(dsT2);
const newDictCurrent = currentTour === « T2 » ? newDictT2 : newDictT1;
circlesData.forEach(d => {
const e1 = newDictT1[d.id];
const e2 = newDictT2[d.id];
d.value2026T1 = e1 ? e1.prct : null;
d.statusBlocT1 = e1?.status ?? « aucune_liste »;
d.detailBlocT1 = e1?.detailListes ?? [];
d.value2026T2 = e2 ? e2.prct : null;
d.statusBlocT2 = e2?.status ?? « aucune_liste »;
d.detailBlocT2 = e2?.detailListes ?? [];
d.pasDeTourDeux = (e1?.detailListes ?? []).some(l => l.eluPremierTour);
const eCurrent = currentTour === « T2 » ? e2 : e1;
d.value2026 = eCurrent ? eCurrent.prct : null;
});
circles.attr(« data-tt », d => makeTooltipContent(d));
updateLegend(newDictCurrent, bloc);
updateMap();
}

function updateLegend(result2026Dict, bloc) {
const allPrctValues = Object.values(result2026Dict).map(d => d.prct);
const newMaxPrct = typeof d3.max(allPrctValues) == ‘undefined’ || d3.max(allPrctValues) < 10 ? 10 : d3.max(allPrctValues);

const blocColorMax = getBlocColorMax(bloc);

// Nouvelle échelle avec la couleur du bloc
colorScale = d3.scaleSequential(
d3.interpolate(colorMin, blocColorMax)
).domain([0, newMaxPrct]);

// Mettre à jour les stops du dégradé
legendSvg.select(`#${gradientId}`)
.selectAll(« stop »)
.attr(« stop-color », (d, i) => colorScale((i / numStops) * newMaxPrct));
legendScale.domain([0, newMaxPrct]);
legendRoot.select(« .legend_color_axis »)
.call(legendAxis)
.call(g => g.select(« .domain »).remove());
legendRoot.select(« .legend_color_title »)
.text(`${textsMap.titre_legende_score} ${textsMap.prct}`);

// color note
const noteEl = document.querySelector(
`.dec_mun_prct_container[data-attr=${snippetId}] .legend_color_note`
);
if (!noteEl) return;
const key = `color_note_${bloc}`;
noteEl.innerHTML = textsMap[key] || «  »;
}

// update cercles
function updateMap() {
circles
.on(« mouseover », function () {
d3.select(this).style(« stroke-width », 1.5);
d3.selectAll(`#dec_mun_prct_${snippetId} circle.bubulle`).classed(« opacified », false);
})
.on(« mouseout », function () {
d3.select(this).style(« stroke-width », (d) => (d.value2026 === null ? 0 : 0.25));
})
.transition()
.duration(600)
.attr(« fill », d => {
const value = currentTour === « T1 » ? d.value2026T1 : d.value2026T2;
if (value === null) return `var(–prct-no-result)`;
return colorScale(value);
})
.attr(« stroke », d => {
const value = currentTour === « T1 » ? d.value2026T1 : d.value2026T2;
return value === null
? `var(–prct-no-result-stroke)`
: d3.rgb(
colorScale(value)
).darker(1);
})
.style(« stroke-width », (d) => ((currentTour === « T1 » ? d.value2026T1 : d.value2026T2) === null ? 0 : 0.25));
}

// Fonction pour traiter la sélection d’une commune
const func_to_treat_result_donnees = (result) => {
// Désopacifie tous les cercles
d3.selectAll(`#dec_mun_prct_${snippetId} circle.bubulle`).classed(« opacified », false);
// Opacifie tous les cercles sauf celui sélectionné
d3.selectAll(`#dec_mun_prct_${snippetId} circle.bubulle`)
.filter((d) => d.id !== result.data.c)
.classed(« opacified », true);

// Trouve le cercle correspondant
const selectedCircle = document.querySelector(`#dec_mun_prct_${snippetId} circle[data-insee= »${result.data.c} »]`);
if (selectedCircle) {
showTooltipManually(selectedCircle);
}
};

// Initialisation de l’autocomplete
if (langue == « fr-FR ») {
autocomplete_decodeurs(`search_mun_prct_${snippetId}`, listeComm, func_to_treat_result_donnees, reset_func, (x) => `${x.label.name}`, 6, 3, slugify, « Aucune commune avec ce nom »);
} else {
autocomplete_decodeurs(`search_mun_prct_${snippetId}`, listeComm, func_to_treat_result_donnees, reset_func, (x) => `${x.label.name}`, 6, 3, slugify, « No town or city with that name »);
}
// quand on tape échap
document.addEventListener(« keydown », function (event) {
if (event.key === « Escape ») {
reset_func();
const circles = getA(`#dec_mun_prct_${snippetId} circle.bubulle`);

forEach(circles, function (circle) {
circle.classList.remove(« selected-commune »);
});
d3.selectAll(« .tooltipdecodeurs »).remove();
}
});

// Initialisation du tooltip
const showTooltipManually = inittooltipdecodeurs(`#dec_mun_prct_${snippetId} .bubulle`);

// Gestionnaire pour fermer le tooltip en cliquant ailleurs
document.addEventListener(« click », function (e) {
if (!e.target.closest(`#dec_mun_prct_${snippetId} circle`) && !e.target.closest(`#search_mun_prct_${snippetId}`)) {
const circles = getA(`#dec_mun_prct_${snippetId} circle.bubulle`);
forEach(circles, function (circle) {
circle.classList.remove(« selected-commune »);
});
}
});

// Gérer les onglets par tour
const tourTabs = document.querySelectorAll(
`.dec_mun_prct_container[data-attr=${snippetId}] [data-tour]`
);
tourTabs.forEach(btn => {
btn.addEventListener(« click », () => {
const newTour = btn.dataset.tour;
currentTour = newTour;
tourTabs.forEach(b => b.classList.remove(« lmui-tab_enabled »));
btn.classList.add(« lmui-tab_enabled »);
switchBloc(currentBloc);
});
});
}
// Fonction pour réinitialiser la carte
const reset_func = () => {
document.querySelector(`#search_mun_prct_${snippetId} input`).value = «  »;
d3.selectAll(`#dec_mun_prct_${snippetId} circle.bubulle`).classed(« opacified », false);
};

// Start
fillHtml();
drawMap(langue);
}

initPercentMap();

Il vous reste 0% de cet article à lire. La suite est réservée aux abonnés.

Share.
Exit mobile version