viewof aurélienetzoéfonttunsuperchart11 = {
const width = 600, height = 600;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const container = d3.create("div");
container.append("h2")
.style("text-align", "left")
.style("margin-bottom", "10px")
.text("Visualisation des données socio-économiques par quartier de Bukavu (RDCongo)");
const images2014 = await (async () => {
return {
B7: await FileAttachment("classe-age-2014.png").url(),
B9: await FileAttachment("duree-residence-2014.png").url(),
B11: await FileAttachment("scolarisation-2014.png").url(),
C2a: await FileAttachment("alea-naturel-2014.png").url()
};
})();
const colorSchemes = {
B7: ["#fdff00", "#ffb800", "#ff4e00", "#ff0000", "#ff00b8"],
B9: ["#ffec00", "#ffa500", "#ff5800", "#ff0000", "#ff00a8"],
B11: ["#5cc85a", "#d31c3a", "#008711"],
C2a: ["#51cf46", "#f3224f"]
};
container.append("label")
.attr("for", "variable-select") // Lier le label au select via l'ID
.text("Choisissez la donnée socio-économique :") // Le texte d'indication
.style("margin-right", "10px"); // Espacement entre le texte et le select
const select = container.append("select")
.attr("id", "variable-select") // Associer l'ID au select pour lier le label
.style("margin-bottom", "1px")
.on("change", function() {
update(this.value);
});
const variables = [
{ value: "B7", label: "Âge" },
{ value: "B9", label: "Durée de résidence" },
{ value: "B11", label: "Scolarisation" },
{ value: "C2a", label: "Expérience d'aléas naturels" }
];
select.selectAll("option")
.data(variables)
.enter()
.append("option")
.attr("value", d => d.value)
.text(d => d.label);
// === Créer un conteneur pour l'image et le SVG ===
const imageSvgContainer = container.append("div")
.style("display", "flex")
.style("align-items", "flex-start")
.style("justify-content", "space-between")
.style("width", "100%")
.style("max-width", "1200px")
.style("margin-top", "5px");
// === Créer un conteneur pour les images ===
const imagesContainer = imageSvgContainer.append("div")
.style("width", "40%");
const svgContainer = imageSvgContainer.append("div")
.style("width", "60%");
// === Créer une infobulle (tooltip) pour afficher les pourcentages au survol ===
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("visibility", "hidden")
.style("background-color", "white")
.style("border", "1px solid #ddd")
.style("border-radius", "4px")
.style("padding", "5px")
.style("pointer-events", "none")
.style("font-size", "12px")
.style("z-index", "1000");
// === Configuration de la carte GeoJSON ===
const projection = d3.geoTransverseMercator()
.rotate([-27, 0])
.fitSize([width, height], geojsonData);
const path = d3.geoPath().projection(projection);
// Créer un groupe pour les éléments géographiques (GeoJSON et camemberts)
const mapGroup = svg.append("g").attr("class", "map-group");
// Créer un groupe pour les légendes qui reste fixe (ne bouge pas avec le zoom)
const legendGroup = svg.append("g").attr("class", "legend-group");
// Fonction pour extraire les valeurs des données associées à chaque entité
function extractValues(feature, variable) {
const data = feature.properties.data;
const values = [];
for (const key in data) {
const cleanedValue = parseFloat(data[key].replace(",", "."));
if (variable === "B11" && key.startsWith("B11_")) {
values.push({ class: key, value: cleanedValue });
} else if (variable === "C2a" && key.startsWith("C2a_")) {
values.push({ class: key, value: cleanedValue });
} else if (key.startsWith(variable + "_classe_")) {
values.push({ class: key, value: cleanedValue });
}
}
return values;
}
// Fonction améliorée pour récupérer la population d'une entité
function getPopulation(feature) {
// Parcourir toutes les propriétés pour trouver une valeur de population
if (feature.properties && feature.properties.data) {
const data = feature.properties.data;
// D'abord chercher des champs spécifiques
const popFields = ["population", "Pop_totale", "Pop"];
for (const field of popFields) {
if (data[field]) {
const pop = parseFloat(data[field].replace(",", "."));
if (!isNaN(pop) && pop > 0) {
// Arrondir à l'unité près car ce sont des personnes
return Math.round(pop);
}
}
}
// Chercher n'importe quel champ contenant "pop" dans le nom (insensible à la casse)
for (const key in data) {
if (key.toLowerCase().includes('pop')) {
const pop = parseFloat(data[key].replace(",", "."));
if (!isNaN(pop) && pop > 0) {
// Arrondir à l'unité près car ce sont des personnes
return Math.round(pop);
}
}
}
// Si on n'a toujours pas trouvé, essayer de générer des valeurs réalistes basées sur l'ID
// Générer une valeur entre 15000 et 200000
return Math.round(15000 + Math.floor((feature.id || feature.properties.id || Math.random() * 200) * 925));
}
// Valeur par défaut si aucune donnée n'est trouvée
return 20000; // Une valeur moyenne
}
// === Créer une légende pour la taille des camemberts qui s'adapte au zoom ===
function addPopulationLegend(zoomScale = 1) {
// Supprimer l'ancienne légende de population si elle existe
legendGroup.selectAll(".population-legend").remove();
// On positionne la légende en haut à droite et on l'adapte au zoom
const popLegend = legendGroup.append("g")
.attr("class", "population-legend")
.attr("transform", `translate(${width - 150}, 20)`);
// Titre de la légende avec taille adaptée au zoom
popLegend.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("font-size", "12px") // Taille fixe car la légende n'est pas zoomée
.attr("font-weight", "bold")
.text("Population");
// Valeurs de population pour la légende (ajustées pour correspondre aux valeurs réelles)
const popValues = [15000, 50000, 200000];
// Fonction pour calculer le rayon basé sur la population
const radiusScale = createRadiusScale();
let yOffset = 20;
popValues.forEach((pop, i) => {
// Le rayon dans la légende doit représenter exactement la même échelle que
// celle utilisée pour les camemberts après ajustement par le zoom
const radius = radiusScale(pop) / zoomScale;
// Cercle d'exemple
popLegend.append("circle")
.attr("cx", radius)
.attr("cy", yOffset + radius)
.attr("r", radius)
.attr("fill", "none")
.attr("stroke", "#333")
.attr("stroke-width", 1);
// Texte - taille de police fixe car la légende n'est pas zoomée
popLegend.append("text")
.attr("x", radius * 2 + 10)
.attr("y", yOffset + radius + 5)
.attr("font-size", "10px")
.text(`${pop.toLocaleString()} hab.`);
yOffset += radius * 2 + 15;
});
}
// Créer une fonction d'échelle pour les rayons des camemberts
function createRadiusScale() {
// Créer une échelle racine carrée pour les rayons
// Min population = 15000, Max population = 200000
// Min rayon = 10, Max rayon = 40
return function(population) {
// Utiliser une échelle racine carrée pour une meilleure perception visuelle
const minPop = 15000;
const maxPop = 200000;
const minRadius = 10;
const maxRadius = 40;
// Normaliser la population entre 0 et 1
const normalizedPop = Math.min(1, Math.max(0, (population - minPop) / (maxPop - minPop)));
// Appliquer l'échelle racine carrée et calculer le rayon
return minRadius + Math.sqrt(normalizedPop) * (maxRadius - minRadius);
};
}
// Fonction pour mettre à jour la carte et les camemberts
function update(variable) {
// Supprimer les anciens camemberts et geojson
mapGroup.selectAll(".camembert").remove();
mapGroup.selectAll(".quartier").remove();
mapGroup.selectAll(".pie-group").remove(); // Supprimer les groupes de camemberts
// Dessiner les polygones GeoJSON en premier (fond)
geojsonData.features.forEach(feature => {
mapGroup.append("path")
.attr("d", path(feature))
.attr("class", "quartier")
.attr("fill", "#BBDEFB")
.attr("stroke", "#0D47A1")
.attr("stroke-width", 1);
});
// Créer une fonction d'échelle pour les rayons
const radiusScale = createRadiusScale();
// Ajouter les camemberts par dessus le fond GeoJSON avec taille proportionnelle à la population
geojsonData.features.forEach((feature, index) => {
const centroid = path.centroid(feature); // Calcul du centroïde de l'entité
const values = extractValues(feature, variable);
const total = d3.sum(values, d => d.value);
const colors = colorSchemes[variable] || d3.schemeCategory10;
// Obtenir la population et calculer le rayon
const population = getPopulation(feature);
const finalRadius = radiusScale(population);
// Afficher la population dans la console pour le débogage
console.log(`Quartier ${feature.properties.name || index} - Population: ${population}, Rayon: ${finalRadius}`);
// Créer un groupe pour chaque camembert
const pieGroup = mapGroup.append("g")
.attr("class", "pie-group")
.attr("transform", `translate(${centroid[0]},${centroid[1]})`)
.datum({
originalRadius: finalRadius,
population: population
});
// Modifier l'angle de départ pour commencer à 90° à droite (0 radians)
// Au lieu de commencer à -Math.PI/2 (position midi)
let startAngle = 0; // Commencer à 3h (à droite)
// Même principe d'inversion pour un sens horaire
const reversedValues = [...values].reverse();
reversedValues.forEach((d, i) => {
const angle = (d.value / total) * 2 * Math.PI;
const arc = d3.arc()
.innerRadius(0)
.outerRadius(finalRadius) // Taille proportionnelle à la population
.startAngle(startAngle)
.endAngle(startAngle + angle);
// Pour la couleur, on utilise l'index inversé pour conserver l'ordre des couleurs
const colorIndex = (values.length - 1) - i;
// Récupérer le nom de la classe formaté
const classLabels = {
"B7_classe_0.2": "Moins de 3 ans",
"B7_classe_3.11": "3–11 ans",
"B7_classe_12.17": "12–17 ans",
"B7_classe_18.65": "18–65 ans",
"B7_classe_66.100": "Plus de 65 ans",
"B9_classe_0.1": "Moins de 2 ans",
"B9_classe_2.10": "2-10 ans",
"B9_classe_11.20": "11–20 ans",
"B9_classe_21.50": "21–50 ans",
"B9_classe_51.100": "Plus de 50 ans",
"B11_oui_3": "Oui",
"B11_non_3": "Non",
"B11_en_cours_3": "En cours",
"C2a_oui": "Oui (a vécu un aléa)",
"C2a_non": "Non (jamais vécu)"
};
const percentage = (d.value / total * 100).toFixed(1);
const className = classLabels[d.class] || d.class;
// Ajout des segments de camembert avec événements au survol
pieGroup.append("path")
.attr("d", arc())
.attr("class", "camembert")
.attr("data-start-angle", startAngle)
.attr("data-end-angle", startAngle + angle)
.attr("fill", colors[colorIndex % colors.length])
.on("mouseover", function(event) {
// Afficher l'infobulle avec les détails
tooltip
.style("visibility", "visible")
.html(`<strong>${className}</strong> : ${percentage}%<br>Population du quartier : ${population.toLocaleString()}`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
// Mettre en évidence le segment au survol
d3.select(this)
.attr("opacity", 0.8)
.attr("stroke", "#333")
.attr("stroke-width", 2);
})
.on("mousemove", function(event) {
// Déplacer l'infobulle avec la souris
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function() {
// Cacher l'infobulle quand la souris sort
tooltip.style("visibility", "hidden");
// Remettre le segment à son état normal
d3.select(this)
.attr("opacity", 1)
.attr("stroke", "none");
});
startAngle += angle;
});
// Ajouter le nom du quartier si disponible
if (feature.properties.name) {
pieGroup.append("text")
.attr("text-anchor", "middle")
.attr("dy", finalRadius + 15)
.attr("font-size", "10px")
.attr("fill", "#333")
.text(feature.properties.name);
}
});
// Mettre à jour l'image dans le conteneur d'image
imagesContainer.selectAll("img").remove();
imagesContainer.append("img")
.attr("src", images2014[variable])
.style("width", "100%")
.style("height", "auto")
.style("object-fit", "contain");
// Supprimer la légende précédente s'il y en a une
legendGroup.selectAll(".variable-legend").remove();
// Créer la nouvelle légende avec une position fixe
const variableLegendGroup = legendGroup.append("g")
.attr("class", "variable-legend")
.attr("transform", "translate(20, 470)"); // Placer en bas à gauche
let legendTitle;
if (variable === "B7") {
legendTitle = "Proportion d'individus répartis par classe d'âge (2018)";
} else if (variable === "B9") {
legendTitle = "Proportion d'individus répartis suivant leur durée de résidence (2018)";
} else if (variable === "B11") {
legendTitle = "Proportion d'individus, de plus de 3 ans, scolarisés ou non (2018)";
} else if (variable === "C2a") {
legendTitle = "Proportion de ménages ayant déjà vécu un aléa naturel (2018)";
}
// Espacement dynamique pour éviter le chevauchement
let legendYOffset = 0;
variableLegendGroup.append("text")
.attr("x", 0)
.attr("y", legendYOffset)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text(legendTitle);
// Mettre à jour l'offset de la légende pour la suite
legendYOffset += 20; // Augmenter la valeur en fonction de la taille du titre
const classLabels = {
"B7_classe_0.2": "Moins de 3 ans",
"B7_classe_3.11": "3–11 ans",
"B7_classe_12.17": "12–17 ans",
"B7_classe_18.65": "18–65 ans",
"B7_classe_66.100": "Plus de 65 ans",
"B9_classe_0.1": "Moins de 2 ans",
"B9_classe_2.10": "2-10 ans",
"B9_classe_11.20": "11–20 ans",
"B9_classe_21.50": "21–50 ans",
"B9_classe_51.100": "Plus de 50 ans",
"B11_oui_3": "Oui",
"B11_non_3": "Non",
"B11_en_cours_3": "En cours",
"C2a_oui": "Oui (a vécu un aléa)",
"C2a_non": "Non (jamais vécu)"
};
const legendData = extractValues(geojsonData.features[0], variable);
legendData.forEach((d, i) => {
const g = variableLegendGroup.append("g")
.attr("transform", `translate(0, ${legendYOffset})`); // Espacement dynamique
g.append("rect")
.attr("width", 14)
.attr("height", 14)
.attr("fill", colorSchemes[variable][i % colorSchemes[variable].length]);
g.append("text")
.attr("x", 20)
.attr("y", 12)
.style("font-size", "10px")
.text(classLabels[d.class] || d.class);
// Mettre à jour l'offset de la légende après chaque ajout
legendYOffset += 20; // Ajuster selon l'espacement souhaité
});
// Ajouter la légende de population
addPopulationLegend();
}
// Création du zoom avec d3.zoom amélioré
const zoom = d3.zoom()
.scaleExtent([1, 8]) // Plage de zoom
.on("zoom", (event) => {
// Appliquer le zoom au groupe de la carte (GeoJSON)
mapGroup.attr("transform", event.transform);
// Ajuster les camemberts pour qu'ils gardent la même taille visuelle pendant le zoom
mapGroup.selectAll(".pie-group").each(function() {
const pieGroup = d3.select(this);
const parentData = pieGroup.datum();
const originalRadius = parentData.originalRadius;
// Calculer le facteur d'échelle inverse pour maintenir la taille visuelle
const scaleFactor = 1 / event.transform.k;
// Mettre à jour tous les segments de camembert
pieGroup.selectAll("path.camembert").each(function() {
const pathElement = d3.select(this);
const startAngle = parseFloat(pathElement.attr("data-start-angle"));
const endAngle = parseFloat(pathElement.attr("data-end-angle"));
// Recréer l'arc avec le rayon ajusté selon le zoom
const arc = d3.arc()
.innerRadius(0)
.outerRadius(originalRadius * scaleFactor) // Ajuster le rayon
.startAngle(startAngle)
.endAngle(endAngle);
// Appliquer le nouveau chemin d'arc
pathElement.attr("d", arc());
});
// Ajuster la position du texte du nom du quartier
pieGroup.selectAll("text").each(function() {
const textElement = d3.select(this);
if (!textElement.attr("original-dy")) {
textElement.attr("original-dy", textElement.attr("dy"));
}
// Ajuster la position du texte proportionnellement au zoom
const originalDy = parseFloat(textElement.attr("original-dy"));
textElement.attr("dy", originalDy * scaleFactor);
// Ajuster la taille de la police pour maintenir la lisibilité
if (!textElement.attr("original-font-size")) {
textElement.attr("original-font-size", textElement.style("font-size"));
}
const originalSize = parseFloat(textElement.attr("original-font-size"));
textElement.style("font-size", `${originalSize * scaleFactor}px`);
});
});
// Mettre à jour la légende de population pour l'adapter au zoom
// Utiliser le même facteur d'échelle que celui appliqué aux camemberts
addPopulationLegend(event.transform.k);
// Cacher l'infobulle lors du zoom
tooltip.style("visibility", "hidden");
});
svg.call(zoom); // Appliquer le zoom à l'élément svg
update(variables[0].value);
svgContainer.append(() => svg.node());
return container.node();
};