Public
Edited
Jun 3
Insert cell
Insert cell
Insert cell
//Import des shp des quartiers de Bukavu (14 quartiers)
geojson = FileAttachment("quartiers_ok.geojson").json()
Insert cell
//Import du csv de données par quartier
// Charge le fichier CSV et découpe correctement chaque ligne
csv = FileAttachment("donnees-quartier@1.csv").text().then(text => {
// Divise le texte en lignes
const rows = text.split("\n");

// Récupère les en-têtes (première ligne)
const headers = rows[0].split(";");

// Transforme chaque ligne de données en un objet
return rows.slice(1).map(row => {
const values = row.split(";");
let obj = {};
headers.forEach((header, i) => {
obj[header.trim()] = values[i]?.trim(); // Map les valeurs aux en-têtes
});
return obj;
});
});
Insert cell
//Test 1 : csv
csv
Insert cell
//Test 2 : geojson
geojson.features[0]
Insert cell
geojsonData = {
const data = await csv;

// Crée une map avec des ID forcés en string
const dataMap = new Map(data.map(d => [String(d.id), d]));

geojson.features.forEach(f => {
const zoneId = String(f.properties.id);
f.properties.data = dataMap.get(zoneId);
});

return geojson;
}
Insert cell
//Test 3 : attributs de la jointure
geojsonData.features[0].properties
Insert cell
geojsonData.features.slice(0, 5) // Affiche les 5 premiers objets pour vérifier que tes données sont bien chargées
Insert cell
//Test 4 : géométrie de la jointure
geojsonData.features[0].geometry
Insert cell
geojsonData.features.length
Insert cell
viewof map = {
const width = 800, height = 600;

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

// Projection UTM 35S (EPSG:32735), proche approximation via geoTransverseMercator
const projection = d3.geoTransverseMercator()
.rotate([-27, 0]) // approximation pour EPSG:32735 (centré sur Bukavu)
.fitSize([width, height], geojsonData);

const path = d3.geoPath().projection(projection);

svg.selectAll("path")
.data(geojsonData.features)
.join("path")
.attr("d", path)
.attr("fill", "#BBDEFB")
.attr("stroke", "#0D47A1")
.attr("stroke-width", 1);

return svg.node();
}
Insert cell
Object.keys(geojsonData.features[0].properties)
Insert cell
// Ceci doit te montrer toutes les clés valides du premier quartier
Object.keys(geojsonData.features[0].properties).filter(k => k.startsWith("B7_classe_"))
Insert cell
import {
style,
world,
disclaimer,
france,
viz
} from "@neocartocnrs/geoviz-appendix"
Insert cell
rdc = viz.tool.featurecollection(
world.features.filter((d) => d.properties.ISO3 == "COD")
)
Insert cell
{
let svg = viz.create({ domain: rdc, margin: 20 });
svg.path({ datum: world, fill: "#CCC" });
svg.path({ datum: rdc, fill: "#0D47A1" });

let inset = viz.container.create({
parent: svg,
pos: [200, 700],
width: 250,
projection: d3.geoOrthographic().rotate([-20, 0])
});
inset.path({ datum: world, fill: "#BBDEFB" });
inset.path({ datum: rdc, fill: "#0D47A1" });
inset.outline({ fill: "none", stroke: "#AAA", strokeWidth: 3 });
return svg.render();
}
Insert cell
geojsonData.features.forEach(f => {
f.properties.Population = f.properties.data.Population;
});
Insert cell
//Import des shp des communes de Bukavu
geojsoncommunes = FileAttachment("communes_ok.geojson").json()
Insert cell
geojsonkivu = FileAttachment("lake-kivu_ok.geojson").json()
Insert cell
geojsonruzizi = FileAttachment("ruzizi_ok.geojson").json()
Insert cell
geojsonbukavu = FileAttachment("Bukavu.geojson").json()
Insert cell
viewof testCommune = viz.path({
data: geojsoncommunes,
fill: "rgba(0,0,0,0.1)",
stroke: "black",
tip: 'Commune de $Commune'
})
Insert cell
{
let svg = viz.create({
projection: d3.geoMercator(),
domain: geojsonData,
margin: 30,
zoomable: true
});

svg.tile({ url: "worldimagery" });
svg.path({ data: geojsoncommunes, stroke: "black", strokeWidth: 1.0, fill: "rgba(0,0,0,0)", });
svg.path({ data: geojsonData, stroke: "white", strokeWidth: 1.5, fill: "rgba(0,0,0,0)", });

svg.header({ text: "Aperçu de Bukavu", fill: "black" });
return svg.render();
}
Insert cell
{
let svg = viz.create({
projection: d3.geoMercator(),
domain: geojsonData,
margin: 20,
zoomable: true
});
viz.tile (svg, {url: "worldimagery" });

viz.path (svg, { data: geojsoncommunes, stroke: "black", strokeWidth: 2, fill: "rgba(0,0,0,0)", tip: 'Commune de $Commune' });

viz.path (svg, { data: geojsonkivu, stroke: "rgba(0,0,0,0)", strokeWidth: 0.2, fill: "rgba(0,0,0,0)", tip: 'Lac Kivu', tipstyle : {overFill : "cyan"} });

viz.path (svg, { data: geojsonruzizi, stroke: "rgba(0,0,0,0)", strokeWidth: 0.2, fill: "rgba(0,0,0,0)", tip: 'Rivière Ruzizi', tipstyle : {overFill : "cyan"} });

/*viz.path (svg, {
data: geojsonData,
stroke: "white",
strokeWidth: 1.0,
fill: "#BBDEFB",
fillOpacity: 0.2,
//tip: 'Quartier $Nom de la commune de $Commune ($Population habitants en 2018)',
tipstyle: {
fontSize: 14,
fill: "white",
background: "#38896F",
fontFamily: "Arial",
fontWeight: "bold",
textDecoration: "none",
overOpacity: null,
overFillOpacity: 0.4,
overFill: "#38896F",
overStroke: "#38896F",
overStrokeOpacity: 1,
overStrokeWidth: 1.5,
raise: true
}
});*/

svg.header({ text: "Aperçu de Bukavu", fill: "black" });
viz.footer(svg, {
text: "Zoé Léonard, 2025 - Source : ?, 2018",
textAnchor: "end",
fill : "black"
});
return svg.render();
}
Insert cell
viewof map1 = {
const container = html`<div style="position: relative; width: 400px; height: 400px;"></div>`;


// Couche 2 : communes
const layer2 = viz.path({
data: geojsoncommunes,
fill: "rgba(0,0,0,0)",
stroke: "black",
strokeWidth: 1.5,
});

// Couche 1 : quartiers
const layer1 = viz.path({
data: geojsonData,
fill: "#BBDEFB",
stroke: "#0D47A1",
tip: 'Quartier $Nom de la commune de $Commune ($Population habitants en 2018)',
tipstyle: {
fontSize: 14,
fill: "white",
background: "#38896F",
fontFamily: "Arial",
fontWeight: "bold",
textDecoration: "none",
overFillOpacity: 0.4,
overFill: "#38896F",
overStroke: "#38896F",
overStrokeOpacity: 1,
overStrokeWidth: 1.5,
raise: true
}
});

container.appendChild(layer1);
container.appendChild(layer2);

return container;
}

Insert cell
//Sélection des variables
viewof variable = Inputs.select(
[
["B7", "Âge"],
["B9", "Durée de résidence"],
["B11", "Scolarisation"],
["C2a", "Expérience d'aléas naturels"]
],
{
label: "Sélectionner la variable :",
format: ([, label]) => label
}
)
Insert cell
images2014 = {
return {
B7: FileAttachment("classe-age-2014.png").url(),
B9: FileAttachment("duree-residence-2014.png").url(),
B11: FileAttachment("scolarisation-2014.png").url(),
C2a: FileAttachment("alea-naturel-2014.png").url()
};
}
Insert cell
md`![](${images2014[variable]})`
Insert cell
viewof map2 = {
const width = 1200, height = 600;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const container = d3.create("div");

// === Ajouter menu de sélection ===
const select = container.append("select")
.style("margin-bottom", "1px")
.on("change", function() {
update(this.value); // Passer la variable sélectionnée
});

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);

// === Projection et carte ===
const projection = d3.geoTransverseMercator()
.rotate([-27, 0])
.fitSize([width, height], geojsonData);

const path = d3.geoPath().projection(projection);

const zoom = d3.zoom()
.scaleExtent([1, 8])
.on("zoom", zoomed);

svg.call(zoom);

const mapGroup = svg.append("g");

const quartiers = mapGroup.selectAll("path.quartier")
.data(geojsonData.features)
.join("path")
.attr("class", "quartier")
.attr("d", path)
.attr("fill", "#BBDEFB")
.attr("stroke", "#0D47A1")
.attr("stroke-width", 1);

const legendGroup = svg.append("g")
.attr("transform", "translate(20, 400)");

function getCentroid(feature) {
if (feature.geometry.type === "Polygon") {
return path.centroid(feature);
} else if (feature.geometry.type === "MultiPolygon") {
const centroids = feature.geometry.coordinates.map(polygonCoordinates => {
const polygon = { type: "Polygon", coordinates: polygonCoordinates };
return path.centroid(polygon);
});
return [
d3.mean(centroids.map(c => c[0])),
d3.mean(centroids.map(c => c[1]))
];
}
return [0, 0];
}

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;
}

const classLabels = {
// B7 : Âge
"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 : Durée de résidence
"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 : Scolarisation
"B11_oui_3": "Scolarisé",
"B11_non_3": "Non scolarisé",
"B11_en_cours_3": "En cours",

// C2a : Aléas
"C2a_oui": "Oui (a vécu un aléa)",
"C2a_non": "Non (jamais vécu)"
};

function getLegendTitle(variable) {
const titles = {
B7: "Proportion d'individus répartis par classe d'âge (2018)",
B9: "Proportion d'individus répartis suivant leur durée de résidence (2018)",
B11: "Proportion d'individus, de plus de 3 ans, scolarisés ou non (2018)",
C2a: "Proportion de ménages ayant déjà vécu un aléa naturel"
};
return titles[variable] || "Répartition";
}

function update(variable) {
if (!variable) return;

// Supprimer anciens camemberts
mapGroup.selectAll(".camembert").remove();

geojsonData.features.forEach(feature => {
const centroid = getCentroid(feature);
if (centroid && centroid[0] !== null) {
const values = extractValues(feature, variable);
const total = d3.sum(values, d => d.value);
if (total > 0) {
let startAngle = 0;
values.forEach((d, i) => {
const angle = (d.value / total) * 2 * Math.PI;
const arc = d3.arc()
.innerRadius(0)
.outerRadius(20)
.startAngle(startAngle)
.endAngle(startAngle + angle);

mapGroup.append("path")
.attr("d", arc())
.attr("class", "camembert")
.attr("fill", d3.schemeCategory10[i % 10])
.attr("transform", `translate(${centroid[0]},${centroid[1]})`);

startAngle += angle;
});
}
}
});

// === Mettre à jour légende ===
legendGroup.selectAll("*").remove();

legendGroup.append("text")
.attr("x", 0)
.attr("y", -20)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text(getLegendTitle(variable));

const legendData = extractValues(geojsonData.features[0], variable);
legendData.forEach((d, i) => {
const g = legendGroup.append("g")
.attr("transform", `translate(0, ${i * 20})`);
g.append("rect")
.attr("width", 14)
.attr("height", 14)
.attr("fill", d3.schemeCategory10[i % 10]);
g.append("text")
.attr("x", 20)
.attr("y", 12)
.style("font-size", "10px")
.text(classLabels[d.class] || d.class);
});
}

function zoomed(event) {
const transform = event.transform;
mapGroup.attr("transform", transform);
mapGroup.selectAll(".camembert")
.attr("transform", function() {
const t = d3.select(this).attr("transform");
const [x, y] = t.match(/[\d.]+/g).map(Number);
const scaleFactor = 1 / transform.k;
return `translate(${x},${y}) scale(${scaleFactor})`;
});
}

// Initialiser avec la première variable (B7)
update(variables[0].value);

container.append(() => svg.node());
return container.node();
}
Insert cell
viewof chart = {
const width = 1200, height = 600;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const container = d3.create("div");

// === Ajouter menu de sélection ===
const select = container.append("select")
.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);


// Projection
const projection = d3.geoTransverseMercator()
.rotate([-27, 0])
.fitSize([width, height], geojsonData);

const path = d3.geoPath().projection(projection);

const zoom = d3.zoom()
.scaleExtent([1, 8])
.on("zoom", zoomed);

svg.call(zoom);

const mapGroup = svg.append("g");

const quartiers = mapGroup.selectAll("path.quartier")
.data(geojsonData.features)
.join("path")
.attr("class", "quartier")
.attr("d", path)
.attr("fill", "#BBDEFB")
.attr("stroke", "#0D47A1")
.attr("stroke-width", 1);

const legendGroup = svg.append("g")
.attr("transform", "translate(20, 400)");

function getCentroid(feature) {
if (feature.geometry.type === "Polygon") {
return path.centroid(feature);
} else if (feature.geometry.type === "MultiPolygon") {
const centroids = feature.geometry.coordinates.map(polygonCoordinates => {
const polygon = { type: "Polygon", coordinates: polygonCoordinates };
return path.centroid(polygon);
});
return [
d3.mean(centroids.map(c => c[0])),
d3.mean(centroids.map(c => c[1]))
];
}
return [0, 0];
}

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;
}


const classLabels = {
// B7 : Âge
"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 : Durée de résidence
"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 : Scolarisation
"B11_oui_3": "Scolarisé",
"B11_non_3": "Non scolarisé",
"B11_en_cours_3": "En cours",

// C2a : Aléas
"C2a_oui": "Oui (a vécu un aléa)",
"C2a_non": "Non (jamais vécu)"
};
function getLegendTitle(variable) {
const titles = {
B7: "Proportion d'individus répartis par classe d'âge (2018)",
B9: "Proportion d'individus répartis suivant leur durée de résidence (2018)",
B11: "Proportion d'individus, de plus de 3 ans, scolarisés ou non (2018)",
C2a: "Proportion de ménages ayant déjà vécu un aléa naturel"
};
return titles[variable] || "Répartition";
}

function update(variable) {
// Supprimer anciens camemberts
mapGroup.selectAll(".camembert").remove();

geojsonData.features.forEach(feature => {
const centroid = getCentroid(feature);
if (centroid && centroid[0] !== null) {
const values = extractValues(feature, variable);
const total = d3.sum(values, d => d.value);
if (total > 0) {
let startAngle = 0;
values.forEach((d, i) => {
const angle = (d.value / total) * 2 * Math.PI;
const arc = d3.arc()
.innerRadius(0)
.outerRadius(20)
.startAngle(startAngle)
.endAngle(startAngle + angle);

mapGroup.append("path")
.attr("d", arc())
.attr("class", "camembert")
.attr("fill", d3.schemeCategory10[i % 10])
.attr("transform", `translate(${centroid[0]},${centroid[1]})`);

startAngle += angle;
});
}
}
});

// === Mettre à jour légende ===
legendGroup.selectAll("*").remove();

legendGroup.append("text")
.attr("x", 0)
.attr("y", -20)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text(getLegendTitle(variable));

const legendData = extractValues(geojsonData.features[0], variable);
legendData.forEach((d, i) => {
const g = legendGroup.append("g")
.attr("transform", `translate(0, ${i * 20})`);
g.append("rect")
.attr("width", 14)
.attr("height", 14)
.attr("fill", d3.schemeCategory10[i % 10]);
g.append("text")
.attr("x", 20)
.attr("y", 12)
.style("font-size", "11px")
.text(classLabels[d.class] || d.class);
});
}

function zoomed(event) {
const transform = event.transform;
mapGroup.attr("transform", transform);
mapGroup.selectAll(".camembert")
.attr("transform", function() {
const t = d3.select(this).attr("transform");
const [x, y] = t.match(/[\d.]+/g).map(Number);
const scaleFactor = 1 / transform.k;
return `translate(${x},${y}) scale(${scaleFactor})`;
});
}

update(variables[0].value);

container.append(() => svg.node());
return container.node();
};
Insert cell
//*miniPlots = html`
//<div>
//<div class="box">
//<div width = 10%>${viewof chart}</div>
//<div width = 10%>${viewof map}</div>
//</div>
//</div>`

Insert cell
viewof selectVariable = {
const variables = [
{ value: "B7", label: "Âge" },
{ value: "B9", label: "Durée de résidence" },
{ value: "B11", label: "Scolarisation" },
{ value: "C2a", label: "Aléas naturels" }
];

const select = html`<select style="margin-bottom: 1px;">
${variables.map(d => html`<option value=${d.value}>${d.label}</option>`)}
</select>`;

// Charger les images à partir de FileAttachment
const images2014 = {
B7: FileAttachment("classe-age-2014.png").url(),
B9: FileAttachment("duree-residence-2014.png").url(),
B11: FileAttachment("scolarisation-2014.png").url(),
C2a: FileAttachment("alea-naturel-2014.png").url()
};

// Fonction pour mettre à jour l'image en fonction de la variable
function updateImage(variable) {
const container = d3.select("#image-container");
// Supprimer l'image précédente
container.selectAll("img").remove();
// Ajouter la nouvelle image
container.append("img")
.attr("src", images2014[variable])
.attr("width", 1200)
.attr("height", 600)
.style("display", "block")
.style("max-width", "100%")
.style("height", "auto");
}

select.addEventListener("change", function() {
const selectedValue = select.value;
updateImage(selectedValue); // Mettre à jour l'image lorsque l'utilisateur sélectionne une variable
});

// Initialiser avec une image par défaut
updateImage("B7");

return select;
};
Insert cell
// Vérifiez si vous obtenez une image correctement
html`<img src="${FileAttachment("classe-age-2014.png").url()}" width="400" height="400"/>`
Insert cell
viewof selectVariable2 = {
const container = d3.create("div");

// Menu de sélection des variables
const select = container.append("select")
.style("margin-bottom", "1px")
.on("change", function() {
updateImage(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 élément d'image
const imageContainer = container.append("div")
.attr("style", "margin-top: 20px;");

const image = imageContainer.append("img")
.attr("width", 400)
.attr("height", 400)
.attr("style", "display: block; margin-top: 20px;");

// Chargement des images PNG pour 2014
const images2014 = {
B7: FileAttachment("classe-age-2014.png").url(),
B9: FileAttachment("duree-residence-2014.png").url(),
B11: FileAttachment("scolarisation-2014.png").url(),
C2a: FileAttachment("alea-naturel-2014.png").url()
};

// Fonction pour mettre à jour l'image en fonction de la sélection
function updateImage(variable) {
const imageUrl = images2014[variable];

// Vérifiez si l'image existe pour la variable sélectionnée
if (imageUrl) {
image.attr("src", imageUrl); // Mettre à jour l'URL de l'image
} else {
console.log(`Image non trouvée pour la variable: ${variable}`);
}
}

// Retourner le container de la cellule
return container.node();
};

Insert cell
viewof map4 = {
const width = 1200, height = 600;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const container = d3.create("div");

// Définition des labels pour les classes des variables
const classLabels = {
// B7 : Âge
"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 : Durée de résidence
"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 : Scolarisation
"B11_oui_3": "Scolarisé",
"B11_non_3": "Non scolarisé",
"B11_en_cours_3": "En cours",

// C2a : Aléas
"C2a_oui": "Oui (a vécu un aléa)",
"C2a_non": "Non (jamais vécu)"
};

const projection = d3.geoTransverseMercator()
.rotate([-27, 0])
.fitSize([width, height], geojsonData);

const path = d3.geoPath().projection(projection);

const mapGroup = svg.append("g");

const quartiers = mapGroup.selectAll("path.quartier")
.data(geojsonData.features)
.join("path")
.attr("class", "quartier")
.attr("d", path)
.attr("fill", "#BBDEFB")
.attr("stroke", "#0D47A1")
.attr("stroke-width", 1);

const legendGroup = svg.append("g")
.attr("transform", "translate(20, 400)");

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;
}

function getLegendTitle(variable) {
const titles = {
B7: "Proportion d'individus répartis par classe d'âge (2018)",
B9: "Proportion d'individus répartis suivant leur durée de résidence (2018)",
B11: "Proportion d'individus, de plus de 3 ans, scolarisés ou non (2018)",
C2a: "Proportion de ménages ayant déjà vécu un aléa naturel"
};
return titles[variable] || "Répartition";
}

function update(variable) {
mapGroup.selectAll(".camembert").remove();

geojsonData.features.forEach(feature => {
const centroid = path.centroid(feature);
if (centroid && centroid[0] !== null) {
const values = extractValues(feature, variable);
const total = d3.sum(values, d => d.value);
if (total > 0) {
let startAngle = 0;
values.forEach((d, i) => {
const angle = (d.value / total) * 2 * Math.PI;
const arc = d3.arc()
.innerRadius(0)
.outerRadius(20)
.startAngle(startAngle)
.endAngle(startAngle + angle);

mapGroup.append("path")
.attr("d", arc())
.attr("class", "camembert")
.attr("fill", d3.schemeCategory10[i % 10])
.attr("transform", `translate(${centroid[0]},${centroid[1]})`);

startAngle += angle;
});
}
}
});

// === Mettre à jour légende ===
legendGroup.selectAll("*").remove();

legendGroup.append("text")
.attr("x", 0)
.attr("y", -20)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text(getLegendTitle(variable));

const legendData = extractValues(geojsonData.features[0], variable);
legendData.forEach((d, i) => {
const g = legendGroup.append("g")
.attr("transform", `translate(0, ${i * 20})`);
g.append("rect")
.attr("width", 14)
.attr("height", 14)
.attr("fill", d3.schemeCategory10[i % 10]);
g.append("text")
.attr("x", 20)
.attr("y", 12)
.style("font-size", "10px")
.text(classLabels[d.class] || d.class);
});
}

update("B7"); // Initialisation avec la première variable

container.append(() => svg.node());
return container.node();
};
Insert cell
viewof map5 = {
const width = 1200, height = 600;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const container = d3.create("div");

// Définir les labels des classes (comme dans la cellule 2)
const classLabels = {
// B7 : Âge
"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 : Durée de résidence
"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 : Scolarisation
"B11_oui_3": "Scolarisé",
"B11_non_3": "Non scolarisé",
"B11_en_cours_3": "En cours",

// C2a : Aléas
"C2a_oui": "Oui (a vécu un aléa)",
"C2a_non": "Non (jamais vécu)"
};

// Projection de la carte
const projection = d3.geoTransverseMercator()
.rotate([-27, 0])
.fitSize([width, height], geojsonData);

const path = d3.geoPath().projection(projection);

const mapGroup = svg.append("g");

const quartiers = mapGroup.selectAll("path.quartier")
.data(geojsonData.features)
.join("path")
.attr("class", "quartier")
.attr("d", path)
.attr("fill", "#BBDEFB")
.attr("stroke", "#0D47A1")
.attr("stroke-width", 1);

const legendGroup = svg.append("g")
.attr("transform", "translate(20, 400)");

// Fonction pour extraire les valeurs des données pour chaque feature
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 pour obtenir le titre de la légende
function getLegendTitle(variable) {
const titles = {
B7: "Proportion d'individus répartis par classe d'âge (2018)",
B9: "Proportion d'individus répartis suivant leur durée de résidence (2018)",
B11: "Proportion d'individus, de plus de 3 ans, scolarisés ou non (2018)",
C2a: "Proportion de ménages ayant déjà vécu un aléa naturel"
};
return titles[variable] || "Répartition";
}

// Fonction pour mettre à jour la carte et la légende
function update(variable) {
// Supprimer les anciens camemberts
mapGroup.selectAll(".camembert").remove();

geojsonData.features.forEach(feature => {
const centroid = path.centroid(feature);
if (centroid && centroid[0] !== null) {
const values = extractValues(feature, variable);
const total = d3.sum(values, d => d.value);
if (total > 0) {
let startAngle = 0;
values.forEach((d, i) => {
const angle = (d.value / total) * 2 * Math.PI;
const arc = d3.arc()
.innerRadius(0)
.outerRadius(20)
.startAngle(startAngle)
.endAngle(startAngle + angle);

mapGroup.append("path")
.attr("d", arc())
.attr("class", "camembert")
.attr("fill", d3.schemeCategory10[i % 10])
.attr("transform", `translate(${centroid[0]},${centroid[1]})`);

startAngle += angle;
});
}
}
});

// Mettre à jour la légende
legendGroup.selectAll("*").remove();

legendGroup.append("text")
.attr("x", 0)
.attr("y", -20)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text(getLegendTitle(variable));

const legendData = extractValues(geojsonData.features[0], variable);
legendData.forEach((d, i) => {
const g = legendGroup.append("g")
.attr("transform", `translate(0, ${i * 20})`);
g.append("rect")
.attr("width", 14)
.attr("height", 14)
.attr("fill", d3.schemeCategory10[i % 10]);
g.append("text")
.attr("x", 20)
.attr("y", 12)
.style("font-size", "10px")
.text(classLabels[d.class] || d.class);
});

// Mise à jour de l'image
const imageUrl = images2014[variable]; // Récupérer l'URL de l'image en fonction de la variable
const image = container.append("img")
.attr("src", imageUrl)
.attr("width", width)
.attr("height", height)
.attr("style", "display: block; margin-top: 20px;");
}

// Mettre à jour avec la première variable (B7)
update("B7");

container.append(() => svg.node());
return container.node();
};

Insert cell
Plot.plot({
width: 928,
height: 500,
marginTop: 30,
marginRight: 0,
marginBottom: 30,
marginLeft: 70,
y: {
label: "test"
},
x: {
label: null
},
marks: [
Plot.barY(geojsonData, { x: "Nom", y: "B7_classe_0.2", fill: "#55a37c" }),
Plot.ruleY([20000])
]
})
Insert cell
viewof aurélienfaitunsuperchart = {
const width = 1200, height = 600;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const container = d3.create("div");

// === Définir les images via FileAttachment ===
const images2014 = {
B7: FileAttachment("classe-age-2014.png").url(),
B9: FileAttachment("duree-residence-2014.png").url(),
B11: FileAttachment("scolarisation-2014.png").url(),
C2a: FileAttachment("alea-naturel-2014.png").url()
};

// === Ajouter menu de sélection ===
const select = container.append("select")
.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);

// === Ajouter une image à côté de la carte ===
const image = container.append("img")
.attr("width", 200)
.attr("height", 200)
.style("margin-left", "20px");

// Projection
const projection = d3.geoTransverseMercator()
.rotate([-27, 0])
.fitSize([width, height], geojsonData);

const path = d3.geoPath().projection(projection);
const zoom = d3.zoom()
.scaleExtent([1, 8])
.on("zoom", zoomed);

svg.call(zoom);
const mapGroup = svg.append("g");

const quartiers = mapGroup.selectAll("path.quartier")
.data(geojsonData.features)
.join("path")
.attr("class", "quartier")
.attr("d", path)
.attr("fill", "#BBDEFB")
.attr("stroke", "#0D47A1")
.attr("stroke-width", 1);

const legendGroup = svg.append("g")
.attr("transform", "translate(20, 400)");

function getLegendTitle(variable) {
const titles = {
B7: "Proportion d'individus répartis par classe d'âge (2018)",
B9: "Proportion d'individus répartis suivant leur durée de résidence (2018)",
B11: "Proportion d'individus, de plus de 3 ans, scolarisés ou non (2018)",
C2a: "Proportion de ménages ayant déjà vécu un aléa naturel"
};
return titles[variable] || "Répartition";
}

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;
}

function getCentroid(feature) {
if (feature.geometry.type === "Polygon") {
return path.centroid(feature);
} else if (feature.geometry.type === "MultiPolygon") {
const centroids = feature.geometry.coordinates.map(polygonCoordinates => {
const polygon = { type: "Polygon", coordinates: polygonCoordinates };
return path.centroid(polygon);
});
return [
d3.mean(centroids.map(c => c[0])),
d3.mean(centroids.map(c => c[1]))
];
}
return [0, 0];
}

function update(variable) {
// Supprimer anciens camemberts
mapGroup.selectAll(".camembert").remove();

geojsonData.features.forEach(feature => {
const centroid = getCentroid(feature);
const values = extractValues(feature, variable);
const total = d3.sum(values, d => d.value);

if (total > 0) {
let startAngle = 0;
values.forEach((d, i) => {
const angle = (d.value / total) * 2 * Math.PI;
const arc = d3.arc()
.innerRadius(0)
.outerRadius(20)
.startAngle(startAngle)
.endAngle(startAngle + angle);

mapGroup.append("path")
.attr("d", arc())
.attr("class", "camembert")
.attr("fill", d3.schemeCategory10[i % 10])
.attr("transform", `translate(${centroid[0]},${centroid[1]})`);

startAngle += angle;
});
}
});

// Mettre à jour l'image
image.attr("src", images2014[variable]);

// Mettre à jour la légende
legendGroup.selectAll("*").remove();
legendGroup.append("text")
.attr("x", 0)
.attr("y", -20)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text(getLegendTitle(variable));
}

function zoomed(event) {
const transform = event.transform;
mapGroup.attr("transform", transform);
}

// Initialisation avec la première variable
update(variables[0].value);
container.append(() => svg.node());
return container.node();
};

Insert cell
viewof imagesessaie {
const images2014 = {};

images2014.B7 = await FileAttachment("classe-age-2014.png").url();
images2014.B9 = await FileAttachment("duree-residence-2014.png").url();
images2014.B11 = await FileAttachment("scolarisation-2014.png").url();
images2014.C2a = await FileAttachment("alea-naturel-2014.png").url();
console.log(images2014);
return images2014;
};

Insert cell
viz.path({
data: geojsonData,
fill: "#BBDEFB",
stroke: "#0D47A1",
tip: 'Quartier $Nom de la commune de $Commune ($Population habitants en 2018)',
tipstyle: {
fontSize: 14,
fill: "white",
background: "#38896F",
fontFamily: "Arial",
fontWeight: "bold",
textDecoration: "none",
overOpacity: null,
overFillOpacity: 0.4,
overFill: "#38896F",
overStroke: "#38896F",
overStrokeOpacity: 1,
overStrokeWidth: 1.5,
raise: true
}
})
Insert cell
{
let svg = viz.create({
projection: d3.geoMercator(),
domain: geojsonData,
margin: 50,
zoomable: true
});
viz.tile (svg, {url: "worldimagery" });

viz.path (svg, { data: geojsoncommunes, stroke: "white", strokeWidth: 2, fill: "rgba(0,0,0,0)", tip: 'Commune de $Commune' });

viz.path (svg, { data: geojsonkivu, stroke: "rgba(0,0,0,0)", strokeWidth: 0.2, fill: "rgba(0,0,0,0)", tip: 'Lac Kivu', tipstyle : {overFill : "cyan"} });

viz.path (svg, { data: geojsonruzizi, stroke: "rgba(0,0,0,0)", strokeWidth: 0.2, fill: "rgba(0,0,0,0)", tip: 'Rivière Ruzizi', tipstyle : {overFill : "cyan"} });

svg.header({ text: "Ville de Bukavu (RDC) : ses communes et son hydrographie", fill: "black", });
viz.footer(svg, {
text: "Zoé Léonard, 2025 - Source : ?, 2018",
textAnchor: "end",
fill : "black"
});
return svg.render();
}
Insert cell
{
let svg = viz.create({
projection: d3.geoMercator(),
domain: geojsonData,
margin: 50,
zoomable: true
});

// Fond cartographique
viz.tile(svg, {
url: "worldimagery"
});

// Quartiers avec info détaillée et survol stylisé
viz.path(svg, {
data: geojsonData,
fill: "#BBDEFB",
stroke: "#0D47A1",
strokeWidth: 1,
tip: d => `Quartier : <b>${d.properties.Nom}</b><br>Commune : ${d.properties.Commune}<br>Population : ${d.properties.Population.toLocaleString()} habitants (2018)`,
tipstyle: {
fontSize: 14,
fill: "white",
background: "#38896F",
fontFamily: "Arial",
fontWeight: "bold",
overFillOpacity: 0.4,
overFill: "#38896F",
overStroke: "#38896F",
overStrokeOpacity: 1,
overStrokeWidth: 1.5,
raise: true
}
});


// Communes (contours)
viz.path(svg, {
data: geojsoncommunes,
stroke: "white",
strokeWidth: 2,
fill: "rgba(0,0,0,0)",
tip: 'Commune de $Commune'
});

// Lac Kivu
viz.path(svg, {
data: geojsonkivu,
stroke: "rgba(0,0,0,0)",
strokeWidth: 0.2,
fill: "rgba(0,0,0,0)",
tip: 'Lac Kivu',
tipstyle: {
overFill: "cyan"
}
});

// Rivière Ruzizi
viz.path(svg, {
data: geojsonruzizi,
stroke: "rgba(0,0,0,0)",
strokeWidth: 0.2,
fill: "rgba(0,0,0,0)",
tip: 'Rivière Ruzizi',
tipstyle: {
overFill: "cyan"
}
});

// Titre
svg.header({
text: "Ville de Bukavu (RDC) : ses communes, quartiers et son hydrographie",
fill: "black"
});

// Légende / source
viz.footer(svg, {
text: "Zoé Léonard, 2025 - Source : ?, 2018",
textAnchor: "end",
fill: "black"
});

return svg.render();
}
Insert cell
{
let svg = viz.create({
projection: d3.geoMercator(),
domain: geojsonData,
margin: 50,
zoomable: true
});
viz.tile (svg, {url: "worldimagery" });

viz.path (svg, { data: geojsoncommunes, stroke: "white", strokeWidth: 2, fill: "rgba(0,0,0,0)", tip: 'Commune de $Commune' });

viz.path (svg, { data: geojsonkivu, stroke: "rgba(0,0,0,0)", strokeWidth: 0.2, fill: "rgba(0,0,0,0)", tip: 'Lac Kivu', tipstyle : {overFill : "cyan"} });

viz.path (svg, { data: geojsonruzizi, stroke: "rgba(0,0,0,0)", strokeWidth: 0.2, fill: "rgba(0,0,0,0)", tip: 'Rivière Ruzizi', tipstyle : {overFill : "cyan"} });

svg.header({ text: "Ville de Bukavu (RDC) : ses communes et son hydrographie", fill: "black", });
viz.footer(svg, {
text: "Zoé Léonard, 2025 - Source : ?, 2018",
textAnchor: "end",
fill : "black"
});
return svg.render();
}
Insert cell
viewof aurélienetzoéfonttunsuperchart9 = {
const width = 600, height = 600;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const container = d3.create("div");

// Ajouter un titre général pour la visualisation
container.append("h2")
.style("text-align", "left")
.style("margin-bottom", "10px")
.text("Visualisation des données socio-économiques par quartier de Bukavu (RDCongo)");
// === Charger les URLs des images ===
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()
};
})();

// === Définir les couleurs personnalisées ===
const colorSchemes = {
B7: ["#fdff00", "#ffb800", "#ff4e00", "#ff0000", "#ff00b8"],
B9: ["#ffec00", "#ffa500", "#ff5800", "#ff0000", "#ff00a8"],
B11: ["#5cc85a", "#d31c3a", "#008711"],
C2a: ["#51cf46", "#f3224f"]
};

// === Ajouter menu de sélection ===

// Ajouter un label pour l'indication à gauche du menu de sélection
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 fixe pour les légendes (non affecté par le zoom)
const fixedLegendGroup = svg.append("g").attr("class", "fixed-legends");
// 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 ===
function addPopulationLegend() {
// Supprimer l'ancienne légende de population si elle existe
fixedLegendGroup.selectAll(".population-legend").remove();
// Créer la légende des tailles de population
const popLegend = fixedLegendGroup.append("g")
.attr("class", "population-legend")
.attr("transform", `translate(${width - 150}, 20)`);
// Titre de la légende
popLegend.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("font-size", "12px")
.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 - doit correspondre à celle utilisée pour les camemberts
const radiusScale = createRadiusScale();
let yOffset = 20;
popValues.forEach((pop, i) => {
const radius = radiusScale(pop);
// 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
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
});
// Pour créer des camemberts dans le sens horaire avec D3.js
// On commence à -Math.PI/2 (position midi) et on va dessiner dans le sens horaire
// Pour ce faire, on inverse l'ordre des valeurs avant de les dessiner
// On inverse l'ordre des valeurs pour obtenir un sens horaire
const reversedValues = [...values].reverse();
let startAngle = -Math.PI/2; // Commencer à midi (en haut)
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
fixedLegendGroup.selectAll(".legend").remove();

// Créer la nouvelle légende avec une position fixe
const legendGroup = fixedLegendGroup.append("g")
.attr("class", "legend")
.attr("transform", "translate(20, 470)"); // Placer plus bas que les camemberts

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;
legendGroup.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 = legendGroup.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`);
});
});
// 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();
};

//toujours échelle et sens début camembert
Insert cell
viewof aurélienetzoéfonttunsuperchart8 = {
const width = 600, height = 600;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const container = d3.create("div");

// Ajouter un titre général pour la visualisation
container.append("h2")
.style("text-align", "left")
.style("margin-bottom", "10px")
.text("Visualisation des données socio-économiques par quartier de Bukavu (RDCongo)");
// === Charger les URLs des images ===
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()
};
})();

// === Définir les couleurs personnalisées ===
const colorSchemes = {
B7: ["#fdff00", "#ffb800", "#ff4e00", "#ff0000", "#ff00b8"],
B9: ["#ffec00", "#ffa500", "#ff5800", "#ff0000", "#ff00a8"],
B11: ["#5cc85a", "#d31c3a", "#008711"],
C2a: ["#51cf46", "#f3224f"]
};

// === Ajouter menu de sélection ===

// Ajouter un label pour l'indication à gauche du menu de sélection
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 fixe pour les légendes (non affecté par le zoom)
const fixedLegendGroup = svg.append("g").attr("class", "fixed-legends");
// 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) {
return 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) {
return 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 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 ===
function addPopulationLegend() {
// Supprimer l'ancienne légende de population si elle existe
fixedLegendGroup.selectAll(".population-legend").remove();
// Créer la légende des tailles de population
const popLegend = fixedLegendGroup.append("g")
.attr("class", "population-legend")
.attr("transform", `translate(${width - 150}, 20)`);
// Titre de la légende
popLegend.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("font-size", "12px")
.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 - doit correspondre à celle utilisée pour les camemberts
const radiusScale = createRadiusScale();
let yOffset = 20;
popValues.forEach((pop, i) => {
const radius = radiusScale(pop);
// 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
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
});
// Pour chaque segment du camembert
let startAngle = 0;
values.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);
// 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("fill", colors[i % 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
fixedLegendGroup.selectAll(".legend").remove();

// Créer la nouvelle légende avec une position fixe
const legendGroup = fixedLegendGroup.append("g")
.attr("class", "legend")
.attr("transform", "translate(20, 470)"); // Placer plus bas que les camemberts

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;
legendGroup.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 = legendGroup.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);
// 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();
};


//Amélioration pour la suite : commencer cercle anti horloger (ou inverse?) pour correspondre avec png. population enlver les décimals (et donc arrondir)
//Améliorer : échelle des propotions des cercles doit zoomer avec le zoom pour que leur taille corresponde à la légende (cercles qui rétrecissent en zoomant ? NON)
Insert cell
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");

// Ajouter un titre général pour la visualisation
container.append("h2")
.style("text-align", "left")
.style("margin-bottom", "10px")
.text("Visualisation des données socio-économiques par quartier de Bukavu (RDCongo)");
// === Charger les URLs des images ===
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()
};
})();

// === Définir les couleurs personnalisées ===
const colorSchemes = {
B7: ["#fdff00", "#ffb800", "#ff4e00", "#ff0000", "#ff00b8"],
B9: ["#ffec00", "#ffa500", "#ff5800", "#ff0000", "#ff00a8"],
B11: ["#5cc85a", "#d31c3a", "#008711"],
C2a: ["#51cf46", "#f3224f"]
};

// === Ajouter menu de sélection ===

// Ajouter un label pour l'indication à gauche du menu de sélection
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();
};
Insert cell
viewof aurélienetzoéfonttunsuperchart12 = {
const width = 600, height = 600;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const container = d3.create("div");

// Ajouter un titre général pour la visualisation
container.append("h2")
.style("text-align", "left")
.style("margin-bottom", "10px")
.text("Visualisation des données socio-économiques par quartier de Bukavu (RDCongo)");
// === Charger les URLs des images ===
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()
};
})();

// === Définir les couleurs personnalisées ===
const colorSchemes = {
B7: ["#fdff00", "#ffb800", "#ff4e00", "#ff0000", "#ff00b8"],
B9: ["#ffec00", "#ffa500", "#ff5800", "#ff0000", "#ff00a8"],
B11: ["#5cc85a", "#d31c3a", "#008711"],
C2a: ["#51cf46", "#f3224f"]
};

// === Ajouter menu de sélection ===

// Ajouter un label pour l'indication à gauche du menu de sélection
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 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);
};
}

// === 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
const popLegend = legendGroup.append("g")
.attr("class", "population-legend")
.attr("transform", `translate(${width - 150}, 20)`);
// Titre de la légende
popLegend.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("Population");
// Valeurs de population pour la légende
const popValues = [15000, 50000, 200000];
// Fonction pour calculer le rayon basé sur la population
const radiusScale = createRadiusScale();
let yOffset = 20;
popValues.forEach((pop, i) => {
// CORRECTION ICI: Appliquer exactement la même logique de mise à l'échelle
// que pour les camemberts, en multipliant par scaleFactor (1/zoomScale)
const radius = radiusScale(pop) / zoomScale;
// Cercle d'exemple avec rayon ajusté selon le zoom
popLegend.append("circle")
.attr("cx", radius)
.attr("cy", yOffset + radius)
.attr("r", radius)
.attr("fill", "none")
.attr("stroke", "#333")
.attr("stroke-width", 1);
// Texte
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;
});
}

// 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 avec facteur de zoom = 1 (état initial)
addPopulationLegend(1);
}

// 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);
// Facteur d'échelle inverse pour maintenir la taille visuelle constante
const scaleFactor = 1 / event.transform.k;
// 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;
// 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`);
});
});
// CORRECTION ICI: Mise à jour de la légende de population avec le même facteur de zoom
// Passer event.transform.k directement à addPopulationLegend
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();
};
Insert cell
viewof quartierBarChart4 = {
// Créer un conteneur principal
const container = d3.create("div");
// Dimensions du graphique
const margin = {top: 30, right: 150, bottom: 80, left: 60};
const width = 900 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
// Créer le SVG
const svg = container.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Référence à la variable sélectionnée dans la première visualisation
// On utilise viewof pour accéder à la sélection de l'autre visualisation
const variableSelect = d3.select("#variable-select").node();
let selectedVariable = variableSelect ? variableSelect.value : "B7";
// Variable pour stocker le mode d'affichage (effectifs ou pourcentages)
let displayMode = "effectifs";
// Observer les changements sur le menu déroulant de la première visualisation
if (variableSelect) {
variableSelect.addEventListener("change", function() {
selectedVariable = this.value;
updateBarChart(selectedVariable, displayMode);
});
}
// Fonction pour formater les étiquettes des catégories
function formatCategoryLabel(category) {
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)"
};
return classLabels[category] || category;
}
// Extraire les valeurs d'une caractéristique pour une variable donnée
function extractValues(feature, variable) {
const data = feature.properties.data;
const values = [];
for (const key in data) {
if (!data[key]) continue; // Ignorer les valeurs vides
// Convertir la valeur en nombre (remplacer la virgule par un point)
let cleanedValue;
if (typeof data[key] === 'string') {
cleanedValue = parseFloat(data[key].replace(",", "."));
} else {
cleanedValue = data[key]; // Déjà un nombre
}
// Vérifier si la clé correspond à la variable demandée
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 pour obtenir le nom du quartier à partir des propriétés
function getQuartierName(feature) {
if (feature.properties && feature.properties.data) {
const data = feature.properties.data;
// Chercher spécifiquement la colonne Nom (nom du quartier)
if (data["Nom"]) {
return data["Nom"];
}
// Si aucun nom n'est trouvé, utiliser le nom dans les properties
if (feature.properties.name) {
return feature.properties.name;
}
}
// Valeur par défaut si aucun nom n'est trouvé
return `Q${feature.id || Math.floor(Math.random() * 100)}`;
}
// Obtenir la population d'une caractéristique
function getPopulation(feature) {
if (feature.properties && feature.properties.data) {
const data = feature.properties.data;
// 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) {
return pop;
}
}
}
// Chercher des champs contenant "pop"
for (const key in data) {
if (key.toLowerCase().includes('pop')) {
const pop = parseFloat(data[key].replace(",", "."));
if (!isNaN(pop) && pop > 0) {
return pop;
}
}
}
// Générer une valeur basée sur l'ID
return 15000 + Math.floor((feature.id || feature.properties.id || Math.random() * 200) * 925);
}
return 20000; // Valeur par défaut
}
// Fonction pour préparer les données pour le graphique en bâtons
function prepareBarChartData(variable) {
// Initialiser un tableau pour stocker les données formatées
const barData = [];
// Parcourir toutes les caractéristiques (quartiers)
geojsonData.features.forEach(feature => {
// Obtenir les valeurs pour la variable sélectionnée
const values = extractValues(feature, variable);
// Obtenir la population totale du quartier
const population = getPopulation(feature);
// Obtenir le nom du quartier depuis la propriété A4_Quartie
const quartierName = getQuartierName(feature);

// Pour chaque valeur, déterminer si c'est un effectif ou un pourcentage
values.forEach(item => {
// Si la valeur est supérieure à 100, c'est probablement un effectif
// Sinon, c'est probablement un pourcentage
const isEffectif = item.value > 100;
let effectif, pourcentage;
if (isEffectif) {
// C'est déjà un effectif
effectif = Math.round(item.value); // Arrondir sans décimales
pourcentage = (effectif / population) * 100; // Calculer le pourcentage
} else {
// C'est un pourcentage
pourcentage = item.value;
effectif = Math.round((pourcentage / 100) * population); // Calculer l'effectif
}
barData.push({
quartier: quartierName,
categorie: item.class,
categorieLabel: formatCategoryLabel(item.class),
effectif: effectif,
pourcentage: pourcentage
});
});
});
return barData;
}
// Fonction pour mettre à jour le graphique en bâtons
function updateBarChart(variable, mode = "effectifs") {
// Effacer le contenu du SVG
svg.selectAll("*").remove();
// Préparer les données
const barData = prepareBarChartData(variable);
// Obtenir la liste unique des quartiers et des catégories
const quartiers = [...new Set(barData.map(d => d.quartier))];
const categories = [...new Set(barData.map(d => d.categorie))];
const categoriesLabels = [...new Set(barData.map(d => d.categorieLabel))];
// Échelle pour l'axe X (quartiers)
const x0 = d3.scaleBand()
.domain(quartiers)
.range([0, width])
.paddingInner(0.2);
// Échelle pour les groupes dans chaque quartier (catégories)
const x1 = d3.scaleBand()
.domain(categories)
.range([0, x0.bandwidth()])
.padding(0.05);
// Échelle pour l'axe Y (effectifs ou pourcentages selon le mode)
const y = d3.scaleLinear()
.domain([0, d3.max(barData, d => mode === "effectifs" ? d.effectif : d.pourcentage)])
.nice()
.range([height, 0]);
// Définir les couleurs selon les schémas existants
const colorSchemes = {
B7: ["#fdff00", "#ffb800", "#ff4e00", "#ff0000", "#ff00b8"],
B9: ["#ffec00", "#ffa500", "#ff5800", "#ff0000", "#ff00a8"],
B11: ["#5cc85a", "#d31c3a", "#008711"],
C2a: ["#51cf46", "#f3224f"]
};
// Utiliser les couleurs du schéma existant
const colorScale = d3.scaleOrdinal()
.domain(categories)
.range(colorSchemes[variable] || d3.schemeCategory10);
// Ajouter les barres avec une organisation groupée par quartier
const quartierGroups = svg.append("g")
.selectAll("g")
.data(quartiers)
.enter()
.append("g")
.attr("transform", d => `translate(${x0(d)},0)`);
// Pour chaque quartier, ajouter les barres des différentes catégories
quartierGroups.selectAll("rect")
.data(quartier => {
return barData.filter(d => d.quartier === quartier);
})
.enter()
.append("rect")
.attr("x", d => x1(d.categorie))
.attr("y", d => y(mode === "effectifs" ? d.effectif : d.pourcentage))
.attr("width", x1.bandwidth())
.attr("height", d => height - y(mode === "effectifs" ? d.effectif : d.pourcentage))
.attr("fill", d => colorScale(d.categorie))
.on("mouseover", function(event, d) {
// Afficher les informations au survol
tooltip
.style("visibility", "visible")
.html(`<strong>${d.quartier} - ${d.categorieLabel}</strong><br>
Effectif: ${Math.round(d.effectif).toLocaleString()}<br>
Pourcentage: ${d.pourcentage.toFixed(1)}%`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
// Mettre en évidence la barre
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
tooltip.style("visibility", "hidden");
// Remettre la barre à son état normal
d3.select(this)
.attr("opacity", 1)
.attr("stroke", "none");
});
// Ajouter l'axe X (quartiers)
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x0))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)")
.style("font-weight", "bold") // Mettre en gras les noms des quartiers
.style("font-size", "11px"); // Augmenter la taille de police
// Ajouter l'axe Y (effectifs ou pourcentages selon le mode)
const yAxis = svg.append("g")
.attr("class", "y-axis");
// Format de l'axe Y selon le mode
if (mode === "effectifs") {
yAxis.call(d3.axisLeft(y).tickFormat(d => d3.format(",")(Math.round(d)))); // Format avec séparateur de milliers, arrondi
} else {
yAxis.call(d3.axisLeft(y).tickFormat(d => d3.format(".1f")(d) + " %")); // Format pourcentage avec 1 décimale
}
// Étiquette de l'axe Y
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -margin.left + 20)
.attr("x", -height/2)
.attr("text-anchor", "middle")
.attr("fill", "#000")
.style("font-weight", "bold")
.text(mode === "effectifs" ? "Effectifs" : "Pourcentages (%)");
// Ajouter un titre au graphique
let chartTitle;
if (variable === "B7") {
chartTitle = mode === "effectifs" ?
"Effectifs par quartier et par classe d'âge (2018)" :
"Pourcentages par quartier et par classe d'âge (2018)";
} else if (variable === "B9") {
chartTitle = mode === "effectifs" ?
"Effectifs par quartier et par durée de résidence (2018)" :
"Pourcentages par quartier et par durée de résidence (2018)";
} else if (variable === "B11") {
chartTitle = mode === "effectifs" ?
"Effectifs par quartier et statut de scolarisation (2018)" :
"Pourcentages par quartier et statut de scolarisation (2018)";
} else if (variable === "C2a") {
chartTitle = mode === "effectifs" ?
"Effectifs par quartier selon l'expérience d'aléas naturels (2018)" :
"Pourcentages par quartier selon l'expérience d'aléas naturels (2018)";
}
svg.append("text")
.attr("x", width / 2)
.attr("y", -10)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("font-weight", "bold")
.text(chartTitle);
// Créer une légende
const legend = svg.append("g")
.attr("class", "legend")
.attr("transform", `translate(${width + 10}, 0)`);
// Titre de la légende
legend.append("text")
.attr("x", 0)
.attr("y", -10)
.style("font-size", "12px")
.style("font-weight", "bold")
.text("Catégories");

legend.selectAll("rect")
.data(categories.map((cat, i) => ({category: cat, label: categoriesLabels[i]})))
.enter()
.append("rect")
.attr("x", 0)
.attr("y", (d, i) => i * 20 + 5)
.attr("width", 15)
.attr("height", 15)
.attr("fill", d => colorScale(d.category));
legend.selectAll("text")
.data(categories.map((cat, i) => ({category: cat, label: categoriesLabels[i]})))
.enter()
.append("text")
.attr("x", 25)
.attr("y", (d, i) => i * 20 + 17)
.style("font-size", "12px")
.text(d => d.label);
}
// Créer l'infobulle (tooltip)
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");
// Ajouter un sélecteur de mode d'affichage (effectifs ou pourcentages)
const displayModeContainer = container.append("div")
.style("margin-top", "10px")
.style("margin-bottom", "10px");
displayModeContainer.append("label")
.attr("for", "display-mode-select")
.text("Mode d'affichage : ")
.style("margin-right", "10px");
const displayModeSelect = displayModeContainer.append("select")
.attr("id", "display-mode-select")
.on("change", function() {
// Mettre à jour le mode d'affichage et actualiser le graphique
displayMode = this.value;
updateBarChart(selectedVariable, displayMode);
});
displayModeSelect.selectAll("option")
.data([
{ value: "effectifs", label: "Effectifs" },
{ value: "pourcentages", label: "Pourcentages" }
])
.enter()
.append("option")
.attr("value", d => d.value)
.text(d => d.label);
// Initialiser le graphique avec la variable par défaut
updateBarChart(selectedVariable, displayMode);
return container.node();
}
Insert cell
{
let svg = viz.create({
projection: d3.geoMercator(),
domain: geojsonQuartierDensite
});
//Affichage des quartiers et de la densité de population sous forme d'une carte choroplèthe avec comme méthode quantile et 5 classes
svg.plot({
data: geojsonQuartierDensite,
var: "densite",
type: "choro",
method: "quantile",
nb: 5,
precision: 0,
colors: "Reds",
leg_title: "Densité de population par quartier en 2018",
leg_subtitle: "(Habitants/km2)",
leg_type: "horizontal",
leg_pos: [150, 900],
tip: d => `${d.properties.Nom} (Commune de ${d.properties.Commune})
Densité : ${d.properties.densiteok} hab/km²
Population : ${d.properties['donnees-quartier_Population_Arr']} habitants`,
});

// Limites des communes
viz.path(svg, {
data: geojsoncommunes,
fill: "none",
stroke: "black",
strokeWidth: 2,
});
// Flèche du Nord
viz.north(svg, {scale: 1.5,pos: [30, 930], fill: "black"});

// Échelle
viz.scalebar(svg, {pos: [10, 990], distance: [0, 1, 2], units: "m", tickSize: 4, fontSize: 10});

// Sources
viz.footer(svg, {
text: "Zoé Léonard, 2025 - Source : ISP Bukavu (2018) & Estimations de l'enquête de terrain (2018)",
fontSize: 10,
textAnchor: "end",
fill: "black"
});
return svg.render();
}
Insert cell
geojsonQuartierDensite = FileAttachment("quartier_densite_18_okok.geojson").json()
Insert cell
viewof aurélienetzoéfonttunsuperchart10 = {
const width = 600, height = 600;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const container = d3.create("div");
//CHARGER LES IMAGES
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()
};
})();

//DEFINIR LES CODES COULEURS DE CHAQUE VARIABLE (pour correspondre à ceux de 2014)
const colorSchemes = {
B7: ["#fdff00", "#ffb800", "#ff4e00", "#ff0000", "#ff00b8"],
B9: ["#ffec00", "#ffa500", "#ff5800", "#ff0000", "#ff00a8"],
B11: ["#5cc85a", "#d31c3a", "#008711"],
C2a: ["#51cf46", "#f3224f"]
};

//MENU SELECTION

// Ajouter un label pour l'indication à gauche du menu de sélection
container.append("label")
.attr("for", "variable-select") // Lier le label au select via l'ID
.text("Indicateur :") // 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);

//CREATION D'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 s'adapte au 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 pour récupérer la population des quartiers
function getPopulation(feature) {
return feature.properties.data.Population_Arr || 999; //si population inconnue ou nulle retourne 999
}


// === 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();
// Créer la légende des tailles de population
const popLegend = legendGroup.append("g")
.attr("class", "population-legend")
.attr("transform", `translate(${width - 150}, 20)`);
// Titre de la légende
popLegend.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("font-size", `${12 / zoomScale}px`)
.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 - doit correspondre à celle utilisée pour les camemberts
const radiusScale = createRadiusScale();
let yOffset = 20;
popValues.forEach((pop, i) => {
const radius = radiusScale(pop) / zoomScale; // Ajuster le rayon selon le zoom
// 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 / zoomScale); // Ajuster l'épaisseur du trait
// Texte
popLegend.append("text")
.attr("x", radius * 2 + 10)
.attr("y", yOffset + radius + 5)
.attr("font-size", `${10 / zoomScale}px`) // Ajuster la taille du texte
.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 plus bas que les camemberts

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
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();
};
Insert cell
viewof quartierBarChart5 = {
// Créer un conteneur principal
const container = d3.create("div");
// Dimensions du graphique
const margin = {top: 30, right: 150, bottom: 80, left: 60};
const width = 900 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
// Créer le SVG
const svg = container.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Référence à la variable sélectionnée dans la première visualisation
// On utilise viewof pour accéder à la sélection de l'autre visualisation
const variableSelect = d3.select("#variable-select").node();
let selectedVariable = variableSelect ? variableSelect.value : "B7";
// Variable pour stocker le mode d'affichage (effectifs ou pourcentages)
let displayMode = "effectifs";
// Observer les changements sur le menu déroulant de la première visualisation
if (variableSelect) {
variableSelect.addEventListener("change", function() {
selectedVariable = this.value;
updateBarChart(selectedVariable, displayMode);
});
}
// Fonction pour formater les étiquettes des catégories
function formatCategoryLabel(category) {
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)"
};
return classLabels[category] || category;
}
// Extraire les valeurs d'une caractéristique pour une variable donnée
function extractValues(feature, variable) {
const data = feature.properties.data;
const values = [];
for (const key in data) {
if (!data[key]) continue; // Ignorer les valeurs vides
// Convertir la valeur en nombre (remplacer la virgule par un point)
let cleanedValue;
if (typeof data[key] === 'string') {
cleanedValue = parseFloat(data[key].replace(",", "."));
} else {
cleanedValue = data[key]; // Déjà un nombre
}
// Vérifier si la clé correspond à la variable demandée
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 pour obtenir le nom du quartier à partir des propriétés
function getQuartierName(feature) {
if (feature.properties && feature.properties.data) {
const data = feature.properties.data;
// Chercher spécifiquement la colonne Nom (nom du quartier)
if (data["Nom"]) {
return data["Nom"];
}
// Si aucun nom n'est trouvé, utiliser le nom dans les properties
if (feature.properties.name) {
return feature.properties.name;
}
}
// Valeur par défaut si aucun nom n'est trouvé
return `Q${feature.id || Math.floor(Math.random() * 100)}`;
}
// Obtenir la population d'une caractéristique
function getPopulation(feature) {
if (feature.properties && feature.properties.data) {
const data = feature.properties.data;
// 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) {
return pop;
}
}
}
// Chercher des champs contenant "pop"
for (const key in data) {
if (key.toLowerCase().includes('pop')) {
const pop = parseFloat(data[key].replace(",", "."));
if (!isNaN(pop) && pop > 0) {
return pop;
}
}
}
// Générer une valeur basée sur l'ID
return 15000 + Math.floor((feature.id || feature.properties.id || Math.random() * 200) * 925);
}
return 20000; // Valeur par défaut
}
// Fonction pour préparer les données pour le graphique en bâtons
function prepareBarChartData(variable) {
// Initialiser un tableau pour stocker les données formatées
const barData = [];
// Parcourir toutes les caractéristiques (quartiers)
geojsonData.features.forEach(feature => {
// Obtenir les valeurs pour la variable sélectionnée
const values = extractValues(feature, variable);
// Obtenir la population totale du quartier
const population = getPopulation(feature);
// Obtenir le nom du quartier depuis la propriété A4_Quartie
const quartierName = getQuartierName(feature);

// Pour chaque valeur, déterminer si c'est un effectif ou un pourcentage
values.forEach(item => {
// Si la valeur est supérieure à 100, c'est probablement un effectif
// Sinon, c'est probablement un pourcentage
const isEffectif = item.value > 100;
let effectif, pourcentage;
if (isEffectif) {
// C'est déjà un effectif
effectif = Math.round(item.value); // Arrondir sans décimales
pourcentage = (effectif / population) * 100; // Calculer le pourcentage
} else {
// C'est un pourcentage
pourcentage = item.value;
effectif = Math.round((pourcentage / 100) * population); // Calculer l'effectif
}
barData.push({
quartier: quartierName,
categorie: item.class,
categorieLabel: formatCategoryLabel(item.class),
effectif: effectif,
pourcentage: pourcentage
});
});
});
return barData;
}
// Fonction pour mettre à jour le graphique en bâtons
function updateBarChart(variable, mode = "effectifs") {
// Effacer le contenu du SVG
svg.selectAll("*").remove();
// Préparer les données
const barData = prepareBarChartData(variable);
// Obtenir la liste unique des quartiers et des catégories
const quartiers = [...new Set(barData.map(d => d.quartier))];
const categories = [...new Set(barData.map(d => d.categorie))];
const categoriesLabels = [...new Set(barData.map(d => d.categorieLabel))];
// Définir les couleurs selon les schémas existants
const colorSchemes = {
B7: ["#fdff00", "#ffb800", "#ff4e00", "#ff0000", "#ff00b8"],
B9: ["#ffec00", "#ffa500", "#ff5800", "#ff0000", "#ff00a8"],
B11: ["#5cc85a", "#d31c3a", "#008711"],
C2a: ["#51cf46", "#f3224f"]
};
// Utiliser les couleurs du schéma existant
const colorScale = d3.scaleOrdinal()
.domain(categories)
.range(colorSchemes[variable] || d3.schemeCategory10);
if (mode === "effectifs") {
// Mode effectifs : barres groupées
// Échelle pour l'axe X (quartiers)
const x0 = d3.scaleBand()
.domain(quartiers)
.range([0, width])
.paddingInner(0.2);
// Échelle pour les groupes dans chaque quartier (catégories)
const x1 = d3.scaleBand()
.domain(categories)
.range([0, x0.bandwidth()])
.padding(0.05);
// Échelle pour l'axe Y (effectifs)
const y = d3.scaleLinear()
.domain([0, d3.max(barData, d => d.effectif)])
.nice()
.range([height, 0]);
// Ajouter les barres avec une organisation groupée par quartier
const quartierGroups = svg.append("g")
.selectAll("g")
.data(quartiers)
.enter()
.append("g")
.attr("transform", d => `translate(${x0(d)},0)`);
// Pour chaque quartier, ajouter les barres des différentes catégories
quartierGroups.selectAll("rect")
.data(quartier => {
return barData.filter(d => d.quartier === quartier);
})
.enter()
.append("rect")
.attr("x", d => x1(d.categorie))
.attr("y", d => y(d.effectif))
.attr("width", x1.bandwidth())
.attr("height", d => height - y(d.effectif))
.attr("fill", d => colorScale(d.categorie))
.on("mouseover", function(event, d) {
// Afficher les informations au survol
tooltip
.style("visibility", "visible")
.html(`<strong>${d.quartier} - ${d.categorieLabel}</strong><br>
Effectif: ${Math.round(d.effectif).toLocaleString()}<br>
Pourcentage: ${d.pourcentage.toFixed(1)}%`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
// Mettre en évidence la barre
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
tooltip.style("visibility", "hidden");
// Remettre la barre à son état normal
d3.select(this)
.attr("opacity", 1)
.attr("stroke", "none");
});
// Ajouter l'axe X (quartiers)
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x0))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)")
.style("font-weight", "bold")
.style("font-size", "11px");
// Ajouter l'axe Y (effectifs)
svg.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(y).tickFormat(d => d3.format(",")(Math.round(d))));
} else {
// Mode pourcentages : barres empilées à 100%
// Préparer les données pour l'empilement
const stackData = [];
quartiers.forEach(quartier => {
const quartierData = { quartier: quartier };
const quartierValues = barData.filter(d => d.quartier === quartier);
// Calculer le total des pourcentages pour normaliser à 100%
const totalPourcentage = d3.sum(quartierValues, d => d.pourcentage);
quartierValues.forEach(d => {
// Normaliser les pourcentages pour qu'ils totalisent 100%
quartierData[d.categorie] = totalPourcentage > 0 ? (d.pourcentage / totalPourcentage) * 100 : 0;
quartierData[`${d.categorie}_label`] = d.categorieLabel;
quartierData[`${d.categorie}_effectif`] = d.effectif;
quartierData[`${d.categorie}_pourcentage_original`] = d.pourcentage;
});
stackData.push(quartierData);
});
// Créer le générateur de stack
const stack = d3.stack()
.keys(categories)
.value((d, key) => d[key] || 0);
const stackedData = stack(stackData);
// Échelle pour l'axe X (quartiers)
const x = d3.scaleBand()
.domain(quartiers)
.range([0, width])
.padding(0.2);
// Échelle pour l'axe Y (pourcentages de 0 à 100)
const y = d3.scaleLinear()
.domain([0, 100])
.range([height, 0]);
// Créer les barres empilées
svg.append("g")
.selectAll("g")
.data(stackedData)
.enter()
.append("g")
.attr("fill", d => colorScale(d.key))
.selectAll("rect")
.data(d => d)
.enter()
.append("rect")
.attr("x", d => x(d.data.quartier))
.attr("y", d => y(d[1]))
.attr("height", d => y(d[0]) - y(d[1]))
.attr("width", x.bandwidth())
.on("mouseover", function(event, d) {
const category = d3.select(this.parentNode).datum().key;
const categoryLabel = d.data[`${category}_label`];
const effectif = d.data[`${category}_effectif`];
const pourcentageOriginal = d.data[`${category}_pourcentage_original`];
tooltip
.style("visibility", "visible")
.html(`<strong>${d.data.quartier} - ${categoryLabel}</strong><br>
Effectif : ${Math.round(effectif).toLocaleString()}<br>
Pourcentage : ${pourcentageOriginal.toFixed(1)}%<br>`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
d3.select(this)
.attr("opacity", 0.8)
.attr("stroke", "#333")
.attr("stroke-width", 2);
})
.on("mousemove", function(event) {
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function() {
tooltip.style("visibility", "hidden");
d3.select(this)
.attr("opacity", 1)
.attr("stroke", "none");
});
// Ajouter l'axe X (quartiers)
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)")
.style("font-weight", "bold")
.style("font-size", "11px");
// Ajouter l'axe Y (pourcentages)
svg.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(y).tickFormat(d => d + "%"));
}
// Étiquette de l'axe Y
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -margin.left + 20)
.attr("x", -height/2)
.attr("text-anchor", "middle")
.attr("fill", "#000")
.style("font-weight", "bold")
.text(mode === "effectifs" ? "Effectifs" : "Pourcentages (%)");
// Ajouter un titre au graphique
let chartTitle;
if (variable === "B7") {
chartTitle = mode === "effectifs" ?
"Effectifs par quartier et par classe d'âge (2018)" :
"Répartition par quartier et par classe d'âge (2018)";
} else if (variable === "B9") {
chartTitle = mode === "effectifs" ?
"Effectifs par quartier et par durée de résidence (2018)" :
"Répartition par quartier et par durée de résidence (2018)";
} else if (variable === "B11") {
chartTitle = mode === "effectifs" ?
"Effectifs par quartier et statut de scolarisation (2018)" :
"Répartition par quartier et statut de scolarisation (2018)";
} else if (variable === "C2a") {
chartTitle = mode === "effectifs" ?
"Effectifs par quartier selon l'expérience d'aléas naturels (2018)" :
"Répartition par quartier selon l'expérience d'aléas naturels (2018)";
}
svg.append("text")
.attr("x", width / 2)
.attr("y", -10)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("font-weight", "bold")
.text(chartTitle);
// Créer une légende
const legend = svg.append("g")
.attr("class", "legend")
.attr("transform", `translate(${width + 10}, 0)`);
// Titre de la légende
legend.append("text")
.attr("x", 0)
.attr("y", -10)
.style("font-size", "12px")
.style("font-weight", "bold")
.text("Catégories");

// Corriger la légende : créer d'abord les rectangles
legend.selectAll("rect")
.data(categories)
.enter()
.append("rect")
.attr("x", 0)
.attr("y", (d, i) => i * 20 + 5)
.attr("width", 15)
.attr("height", 15)
.attr("fill", d => colorScale(d));
// Puis créer les textes séparément pour éviter les conflits
legend.selectAll(".legend-text")
.data(categories)
.enter()
.append("text")
.attr("class", "legend-text")
.attr("x", 25)
.attr("y", (d, i) => i * 20 + 17)
.style("font-size", "12px")
.text(d => formatCategoryLabel(d));
}
// Créer l'infobulle (tooltip)
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");
// Ajouter un sélecteur de mode d'affichage (effectifs ou pourcentages)
const displayModeContainer = container.append("div")
.style("margin-top", "10px")
.style("margin-bottom", "10px");
displayModeContainer.append("label")
.attr("for", "display-mode-select")
.text("Mode d'affichage : ")
.style("margin-right", "10px");
const displayModeSelect = displayModeContainer.append("select")
.attr("id", "display-mode-select")
.on("change", function() {
// Mettre à jour le mode d'affichage et actualiser le graphique
displayMode = this.value;
updateBarChart(selectedVariable, displayMode);
});
displayModeSelect.selectAll("option")
.data([
{ value: "effectifs", label: "Effectifs" },
{ value: "pourcentages", label: "Pourcentages" }
])
.enter()
.append("option")
.attr("value", d => d.value)
.text(d => d.label);
// Initialiser le graphique avec la variable par défaut
updateBarChart(selectedVariable, displayMode);
return container.node();
}
Insert cell
Insert cell
viewof QuartierBatonChart = {
//CREATION D'UN CONTAINER PRINCIPAL
const container = d3.create("div");
//DIMENSIONS DU GRAPHE
const margin = {top: 30, right: 150, bottom: 80, left: 60};
const width = 900 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
//CREATION D'UN SVG
const svg = container.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Référence à la variable sélectionnée dans la première visualisation
// On utilise viewof pour accéder à la sélection de l'autre visualisation
const variableSelect = d3.select("#variable-select").node();
let selectedVariable = variableSelect ? variableSelect.value : "B7";
// Variable pour stocker le mode d'affichage (effectifs ou pourcentages)
let displayMode = "effectifs";
// Observer les changements sur le menu déroulant de la première visualisation
if (variableSelect) {
variableSelect.addEventListener("change", function() {
selectedVariable = this.value;
updateBarChart(selectedVariable, displayMode);
});
}
// Fonction pour formater les étiquettes des catégories
function formatCategoryLabel(category) {
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)"
};
return classLabels[category] || category;
}
// Extraire les valeurs d'une caractéristique pour une variable donnée
function extractValues(feature, variable) {
const data = feature.properties.data;
const values = [];
for (const key in data) {
if (!data[key]) continue; // Ignorer les valeurs vides
// Convertir la valeur en nombre (remplacer la virgule par un point)
let cleanedValue;
if (typeof data[key] === 'string') {
cleanedValue = parseFloat(data[key].replace(",", "."));
} else {
cleanedValue = data[key]; // Déjà un nombre
}
// Vérifier si la clé correspond à la variable demandée
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 POUR OBTENIR LE NOM DES QUARTIERS
function getQuartierName(feature) {
return feature.properties.data["Nom"];
}
//FONCTION POUR OBTENIR LES POPULATIONS
function getPopulation(feature) {
const pop = parseFloat(feature.properties.data["Population_Arr"].replace(",", "."));
return !isNaN(pop) && pop > 0 ? pop : null;
}
// Fonction pour préparer les données pour le graphique en bâtons
function prepareBarChartData(variable) {
// Initialiser un tableau pour stocker les données formatées
const barData = [];
// Parcourir toutes les caractéristiques (quartiers)
geojsonData.features.forEach(feature => {
// Obtenir les valeurs pour la variable sélectionnée
const values = extractValues(feature, variable);
// Obtenir la population totale du quartier
const population = getPopulation(feature);
// Obtenir le nom du quartier depuis la propriété A4_Quartie
const quartierName = getQuartierName(feature);

// Pour chaque valeur, déterminer si c'est un effectif ou un pourcentage
values.forEach(item => {
// Si la valeur est supérieure à 100, c'est probablement un effectif
// Sinon, c'est probablement un pourcentage
const isEffectif = item.value > 100;
let effectif, pourcentage;
if (isEffectif) {
// C'est déjà un effectif
effectif = Math.round(item.value); // Arrondir sans décimales
pourcentage = (effectif / population) * 100; // Calculer le pourcentage
} else {
// C'est un pourcentage
pourcentage = item.value;
effectif = Math.round((pourcentage / 100) * population); // Calculer l'effectif
}
barData.push({
quartier: quartierName,
categorie: item.class,
categorieLabel: formatCategoryLabel(item.class),
effectif: effectif,
pourcentage: pourcentage
});
});
});
return barData;
}
// Fonction pour mettre à jour le graphique en bâtons
function updateBarChart(variable, mode = "effectifs") {
// Effacer le contenu du SVG
svg.selectAll("*").remove();
// Préparer les données
const barData = prepareBarChartData(variable);
// Obtenir la liste unique des quartiers et des catégories
const quartiers = [...new Set(barData.map(d => d.quartier))];
const categories = [...new Set(barData.map(d => d.categorie))];
const categoriesLabels = [...new Set(barData.map(d => d.categorieLabel))];
// Définir les couleurs selon les schémas existants
const colorSchemes = {
B7: ["#fdff00", "#ffb800", "#ff4e00", "#ff0000", "#ff00b8"],
B9: ["#ffec00", "#ffa500", "#ff5800", "#ff0000", "#ff00a8"],
B11: ["#5cc85a", "#d31c3a", "#008711"],
C2a: ["#51cf46", "#f3224f"]
};
// Utiliser les couleurs du schéma existant
const colorScale = d3.scaleOrdinal()
.domain(categories)
.range(colorSchemes[variable] || d3.schemeCategory10);
if (mode === "effectifs") {
// Mode effectifs : barres groupées
// Échelle pour l'axe X (quartiers)
const x0 = d3.scaleBand()
.domain(quartiers)
.range([0, width])
.paddingInner(0.2);
// Échelle pour les groupes dans chaque quartier (catégories)
const x1 = d3.scaleBand()
.domain(categories)
.range([0, x0.bandwidth()])
.padding(0.05);
// Échelle pour l'axe Y (effectifs)
const y = d3.scaleLinear()
.domain([0, d3.max(barData, d => d.effectif)])
.nice()
.range([height, 0]);
// Ajouter les barres avec une organisation groupée par quartier
const quartierGroups = svg.append("g")
.selectAll("g")
.data(quartiers)
.enter()
.append("g")
.attr("transform", d => `translate(${x0(d)},0)`);
// Pour chaque quartier, ajouter les barres des différentes catégories
quartierGroups.selectAll("rect")
.data(quartier => {
return barData.filter(d => d.quartier === quartier);
})
.enter()
.append("rect")
.attr("x", d => x1(d.categorie))
.attr("y", d => y(d.effectif))
.attr("width", x1.bandwidth())
.attr("height", d => height - y(d.effectif))
.attr("fill", d => colorScale(d.categorie))
.on("mouseover", function(event, d) {
// Afficher les informations au survol
tooltip
.style("visibility", "visible")
.html(`<strong>${d.quartier} - ${d.categorieLabel}</strong><br>
Effectif: ${Math.round(d.effectif).toLocaleString()}<br>
Pourcentage: ${d.pourcentage.toFixed(1)}%`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
// Mettre en évidence la barre
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
tooltip.style("visibility", "hidden");
// Remettre la barre à son état normal
d3.select(this)
.attr("opacity", 1)
.attr("stroke", "none");
});
// Ajouter l'axe X (quartiers)
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x0))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)")
.style("font-weight", "bold")
.style("font-size", "11px");
// Ajouter l'axe Y (effectifs)
svg.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(y).tickFormat(d => d3.format(",")(Math.round(d))));
} else {
// Mode pourcentages : barres empilées à 100%
// Préparer les données pour l'empilement
const stackData = [];
quartiers.forEach(quartier => {
const quartierData = { quartier: quartier };
const quartierValues = barData.filter(d => d.quartier === quartier);
// Calculer le total des pourcentages pour normaliser à 100%
const totalPourcentage = d3.sum(quartierValues, d => d.pourcentage);
quartierValues.forEach(d => {
// Normaliser les pourcentages pour qu'ils totalisent 100%
quartierData[d.categorie] = totalPourcentage > 0 ? (d.pourcentage / totalPourcentage) * 100 : 0;
quartierData[`${d.categorie}_label`] = d.categorieLabel;
quartierData[`${d.categorie}_effectif`] = d.effectif;
quartierData[`${d.categorie}_pourcentage_original`] = d.pourcentage;
});
stackData.push(quartierData);
});
// Créer le générateur de stack
const stack = d3.stack()
.keys(categories)
.value((d, key) => d[key] || 0);
const stackedData = stack(stackData);
// Échelle pour l'axe X (quartiers)
const x = d3.scaleBand()
.domain(quartiers)
.range([0, width])
.padding(0.2);
// Échelle pour l'axe Y (pourcentages de 0 à 100)
const y = d3.scaleLinear()
.domain([0, 100])
.range([height, 0]);
// Créer les barres empilées
svg.append("g")
.selectAll("g")
.data(stackedData)
.enter()
.append("g")
.attr("fill", d => colorScale(d.key))
.selectAll("rect")
.data(d => d)
.enter()
.append("rect")
.attr("x", d => x(d.data.quartier))
.attr("y", d => y(d[1]))
.attr("height", d => y(d[0]) - y(d[1]))
.attr("width", x.bandwidth())
.on("mouseover", function(event, d) {
const category = d3.select(this.parentNode).datum().key;
const categoryLabel = d.data[`${category}_label`];
const effectif = d.data[`${category}_effectif`];
const pourcentageOriginal = d.data[`${category}_pourcentage_original`];
tooltip
.style("visibility", "visible")
.html(`<strong>${d.data.quartier} - ${categoryLabel}</strong><br>
Effectif : ${Math.round(effectif).toLocaleString()}<br>
Pourcentage : ${pourcentageOriginal.toFixed(1)}%<br>`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
d3.select(this)
.attr("opacity", 0.8)
.attr("stroke", "#333")
.attr("stroke-width", 2);
})
.on("mousemove", function(event) {
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function() {
tooltip.style("visibility", "hidden");
d3.select(this)
.attr("opacity", 1)
.attr("stroke", "none");
});
// Ajouter l'axe X (quartiers)
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)")
.style("font-weight", "bold")
.style("font-size", "11px");
// Ajouter l'axe Y (pourcentages)
svg.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(y).tickFormat(d => d + "%"));
}
// Étiquette de l'axe Y
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -margin.left + 20)
.attr("x", -height/2)
.attr("text-anchor", "middle")
.attr("fill", "#000")
.style("font-weight", "bold")
.text(mode === "effectifs" ? "Effectifs" : "Pourcentages (%)");
// Ajouter un titre au graphique
let chartTitle;
if (variable === "B7") {
chartTitle = mode === "effectifs" ?
"Effectifs par quartier et par classe d'âge (2018)" :
"Répartition par quartier et par classe d'âge (2018)";
} else if (variable === "B9") {
chartTitle = mode === "effectifs" ?
"Effectifs par quartier et par durée de résidence (2018)" :
"Répartition par quartier et par durée de résidence (2018)";
} else if (variable === "B11") {
chartTitle = mode === "effectifs" ?
"Effectifs par quartier et statut de scolarisation (2018)" :
"Répartition par quartier et statut de scolarisation (2018)";
} else if (variable === "C2a") {
chartTitle = mode === "effectifs" ?
"Effectifs par quartier selon l'expérience d'aléas naturels (2018)" :
"Répartition par quartier selon l'expérience d'aléas naturels (2018)";
}
svg.append("text")
.attr("x", width / 2)
.attr("y", -10)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("font-weight", "bold")
.text(chartTitle);
// Créer une légende
const legend = svg.append("g")
.attr("class", "legend")
.attr("transform", `translate(${width + 10}, 0)`);
// Titre de la légende
legend.append("text")
.attr("x", 0)
.attr("y", -10)
.style("font-size", "12px")
.style("font-weight", "bold")
.text("Catégories");

// Corriger la légende : créer d'abord les rectangles
legend.selectAll("rect")
.data(categories)
.enter()
.append("rect")
.attr("x", 0)
.attr("y", (d, i) => i * 20 + 5)
.attr("width", 15)
.attr("height", 15)
.attr("fill", d => colorScale(d));
// Puis créer les textes séparément pour éviter les conflits
legend.selectAll(".legend-text")
.data(categories)
.enter()
.append("text")
.attr("class", "legend-text")
.attr("x", 25)
.attr("y", (d, i) => i * 20 + 17)
.style("font-size", "12px")
.text(d => formatCategoryLabel(d));
}
// Créer l'infobulle (tooltip)
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");
// Ajouter un sélecteur de mode d'affichage (effectifs ou pourcentages)
const displayModeContainer = container.append("div")
.style("margin-top", "10px")
.style("margin-bottom", "10px");
displayModeContainer.append("label")
.attr("for", "display-mode-select")
.text("Mode d'affichage : ")
.style("margin-right", "10px");
const displayModeSelect = displayModeContainer.append("select")
.attr("id", "display-mode-select")
.on("change", function() {
// Mettre à jour le mode d'affichage et actualiser le graphique
displayMode = this.value;
updateBarChart(selectedVariable, displayMode);
});
displayModeSelect.selectAll("option")
.data([
{ value: "effectifs", label: "Effectifs" },
{ value: "pourcentages", label: "Pourcentages" }
])
.enter()
.append("option")
.attr("value", d => d.value)
.text(d => d.label);
// Initialiser le graphique avec la variable par défaut
updateBarChart(selectedVariable, displayMode);
return container.node();
}
Insert cell
viewof QuartierBatonChart12 = {
// === CONFIGURATION DU GRAPHIQUE ===
const container = d3.create("div");
const margin = {top: 30, right: 150, bottom: 80, left: 60};
const width = 900 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
// Création du SVG principal
const svg = container.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// === VARIABLES GLOBALES ===
let selectedVariable = "B7"; // Variable par défaut
let displayMode = "effectifs"; // Mode d'affichage par défaut
// === CONFIGURATION DES COULEURS ===
const colorSchemes = {
B7: ["#fdff00", "#ffb800", "#ff4e00", "#ff0000", "#ff00b8"],
B9: ["#ffec00", "#ffa500", "#ff5800", "#ff0000", "#ff00a8"],
B11: ["#5cc85a", "#d31c3a", "#008711"],
C2a: ["#51cf46", "#f3224f"]
};
// === DICTIONNAIRE DES LABELS ===
const categoryLabels = {
"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)"
};
// === FONCTIONS UTILITAIRES ===
// Formater les labels des catégories
function formatLabel(category) {
return categoryLabels[category] || category;
}
// Nettoyer et convertir une valeur en nombre
function cleanValue(value) {
if (typeof value === 'string') {
return parseFloat(value.replace(",", "."));
}
return value;
}
// Extraire les données d'un quartier pour une variable donnée
function getQuartierData(feature, variable) {
const data = feature.properties.data;
const values = [];
for (const key in data) {
if (!data[key]) continue;
const cleanedValue = cleanValue(data[key]);
const shouldInclude =
(variable === "B11" && key.startsWith("B11_")) ||
(variable === "C2a" && key.startsWith("C2a_")) ||
(key.startsWith(variable + "_classe_"));
if (shouldInclude) {
values.push({ class: key, value: cleanedValue });
}
}
return values;
}
// Obtenir le nom du quartier
function getQuartierName(feature) {
return feature.properties.data["Nom"];
}
// Obtenir la population du quartier
function getPopulation(feature) {
const pop = cleanValue(feature.properties.data["Population_Arr"]);
return !isNaN(pop) && pop > 0 ? pop : null;
}
// === PRÉPARATION DES DONNÉES ===
function prepareData(variable) {
const result = [];
geojsonData.features.forEach(feature => {
const quartierName = getQuartierName(feature);
const population = getPopulation(feature);
const values = getQuartierData(feature, variable);
values.forEach(item => {
// Détecter si c'est un effectif ou un pourcentage
const isEffectif = item.value > 100;
let effectif, pourcentage;
if (isEffectif) {
effectif = Math.round(item.value);
pourcentage = (effectif / population) * 100;
} else {
pourcentage = item.value;
effectif = Math.round((pourcentage / 100) * population);
}
result.push({
quartier: quartierName,
categorie: item.class,
categorieLabel: formatLabel(item.class),
effectif: effectif,
pourcentage: pourcentage
});
});
});
return result;
}
// === CRÉATION DU TOOLTIP ===
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("font-size", "12px")
.style("z-index", "1000");
// === FONCTIONS D'INTERACTION ===
function showTooltip(event, d) {
tooltip
.style("visibility", "visible")
.html(`<strong>${d.quartier} - ${d.categorieLabel}</strong><br>
Nombre d'effectifs : ${d.effectif.toLocaleString()}<br>
Pourcentage : ${d.pourcentage.toFixed(1)}%`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
d3.select(event.target)
.attr("opacity", 0.8)
.attr("stroke", "#333")
.attr("stroke-width", 2);
}
function moveTooltip(event) {
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
}
function hideTooltip(event) {
tooltip.style("visibility", "hidden");
d3.select(event.target)
.attr("opacity", 1)
.attr("stroke", "none");
}
// === GRAPHIQUE EN BARRES GROUPÉES (EFFECTIFS) ===
function drawGroupedBars(data) {
const quartiers = [...new Set(data.map(d => d.quartier))];
const categories = [...new Set(data.map(d => d.categorie))];
// Échelles
const x0 = d3.scaleBand().domain(quartiers).range([0, width]).paddingInner(0.2);
const x1 = d3.scaleBand().domain(categories).range([0, x0.bandwidth()]).padding(0.05);
const y = d3.scaleLinear().domain([0, d3.max(data, d => d.effectif)]).nice().range([height, 0]);
const colorScale = d3.scaleOrdinal().domain(categories).range(colorSchemes[selectedVariable]);
// Groupes par quartier
const quartierGroups = svg.selectAll(".quartier-group")
.data(quartiers)
.enter()
.append("g")
.attr("class", "quartier-group")
.attr("transform", d => `translate(${x0(d)},0)`);
// Barres
quartierGroups.selectAll("rect")
.data(quartier => data.filter(d => d.quartier === quartier))
.enter()
.append("rect")
.attr("x", d => x1(d.categorie))
.attr("y", d => y(d.effectif))
.attr("width", x1.bandwidth())
.attr("height", d => height - y(d.effectif))
.attr("fill", d => colorScale(d.categorie))
.on("mouseover", showTooltip)
.on("mousemove", moveTooltip)
.on("mouseout", hideTooltip);
// Axes
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x0))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)")
.style("font-weight", "bold")
.style("font-size", "11px");
svg.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(y).tickFormat(d => d3.format(",")(Math.round(d))));
}
// === GRAPHIQUE EN BARRES EMPILÉES (POURCENTAGES) ===
function drawStackedBars(data) {
const quartiers = [...new Set(data.map(d => d.quartier))];
const categories = [...new Set(data.map(d => d.categorie))];
// Préparation des données pour l'empilement
const stackData = quartiers.map(quartier => {
const quartierData = { quartier: quartier };
const quartierValues = data.filter(d => d.quartier === quartier);
const totalPourcentage = d3.sum(quartierValues, d => d.pourcentage);
quartierValues.forEach(d => {
quartierData[d.categorie] = totalPourcentage > 0 ? (d.pourcentage / totalPourcentage) * 100 : 0;
quartierData[`${d.categorie}_info`] = d; // Garder les infos originales
});
return quartierData;
});
// Générateur de stack
const stack = d3.stack().keys(categories).value((d, key) => d[key] || 0);
const stackedData = stack(stackData);
// Échelles
const x = d3.scaleBand().domain(quartiers).range([0, width]).padding(0.2);
const y = d3.scaleLinear().domain([0, 100]).range([height, 0]);
const colorScale = d3.scaleOrdinal().domain(categories).range(colorSchemes[selectedVariable]);
// Barres empilées
svg.selectAll(".stack-group")
.data(stackedData)
.enter()
.append("g")
.attr("class", "stack-group")
.attr("fill", d => colorScale(d.key))
.selectAll("rect")
.data(d => d)
.enter()
.append("rect")
.attr("x", d => x(d.data.quartier))
.attr("y", d => y(d[1]))
.attr("height", d => y(d[0]) - y(d[1]))
.attr("width", x.bandwidth())
.on("mouseover", function(event, d) {
const category = d3.select(this.parentNode).datum().key;
const info = d.data[`${category}_info`];
showTooltip(event, info);
})
.on("mousemove", moveTooltip)
.on("mouseout", hideTooltip);
// Axes
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)")
.style("font-weight", "bold")
.style("font-size", "11px");
svg.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(y).tickFormat(d => d + "%"));
}
// === CRÉATION DE LA LÉGENDE ===
function createLegend(categories) {
const colorScale = d3.scaleOrdinal().domain(categories).range(colorSchemes[selectedVariable]);
const legend = svg.append("g")
.attr("class", "legend")
.attr("transform", `translate(${width + 10}, 0)`);
legend.append("text")
.attr("x", 0)
.attr("y", -10)
.style("font-size", "12px")
.style("font-weight", "bold")
.text("Catégories");
// Rectangles colorés
legend.selectAll("rect")
.data(categories)
.enter()
.append("rect")
.attr("x", 0)
.attr("y", (d, i) => i * 20 + 5)
.attr("width", 15)
.attr("height", 15)
.attr("fill", d => colorScale(d));
// Textes
legend.selectAll(".legend-text")
.data(categories)
.enter()
.append("text")
.attr("class", "legend-text")
.attr("x", 25)
.attr("y", (d, i) => i * 20 + 17)
.style("font-size", "12px")
.text(d => formatLabel(d));
}
// === FONCTION PRINCIPALE DE MISE À JOUR ===
function updateChart(variable, mode) {
selectedVariable = variable;
displayMode = mode;
// Nettoyer le SVG
svg.selectAll("*").remove();
// Préparer les données
const data = prepareData(variable);
const categories = [...new Set(data.map(d => d.categorie))];
// Dessiner le graphique selon le mode
if (mode === "effectifs") {
drawGroupedBars(data);
} else {
drawStackedBars(data);
}
// Ajouter les titres et légendes
addTitle(variable, mode);
addYAxisLabel(mode);
createLegend(categories);
}
// === AJOUT DU TITRE ===
function addTitle(variable, mode) {
const titles = {
B7: mode === "effectifs" ?
"Effectifs par quartier et par classe d'âge (2018)" :
"Répartition par quartier et par classe d'âge (2018)",
B9: mode === "effectifs" ?
"Effectifs par quartier et par durée de résidence (2018)" :
"Répartition par quartier et par durée de résidence (2018)",
B11: mode === "effectifs" ?
"Effectifs par quartier et statut de scolarisation (2018)" :
"Répartition par quartier et statut de scolarisation (2018)",
C2a: mode === "effectifs" ?
"Effectifs par quartier selon l'expérience d'aléas naturels (2018)" :
"Répartition par quartier selon l'expérience d'aléas naturels (2018)"
};
svg.append("text")
.attr("x", width / 2)
.attr("y", -10)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("font-weight", "bold")
.text(titles[variable]);
}
// === AJOUT DU LABEL DE L'AXE Y ===
function addYAxisLabel(mode) {
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -margin.left + 20)
.attr("x", -height/2)
.attr("text-anchor", "middle")
.attr("fill", "#000")
.style("font-weight", "bold")
.text(mode === "effectifs" ? "Effectifs" : "Pourcentages (%)");
}
// === INTERFACE UTILISATEUR ===
// Sélecteur de mode d'affichage
const displayModeContainer = container.append("div")
.style("margin-top", "10px")
.style("margin-bottom", "10px");
displayModeContainer.append("label")
.text("Mode d'affichage : ")
.style("margin-right", "10px");
const displayModeSelect = displayModeContainer.append("select")
.on("change", function() {
updateChart(selectedVariable, this.value);
});
displayModeSelect.selectAll("option")
.data([
{ value: "effectifs", label: "Effectifs" },
{ value: "pourcentages", label: "Pourcentages" }
])
.enter()
.append("option")
.attr("value", d => d.value)
.text(d => d.label);
// === CONNEXION AVEC LA PREMIÈRE VISUALISATION ===
const variableSelect = d3.select("#variable-select").node();
if (variableSelect) {
selectedVariable = variableSelect.value;
variableSelect.addEventListener("change", function() {
updateChart(this.value, displayMode);
});
}
// === INITIALISATION ===
updateChart(selectedVariable, displayMode);
return container.node();
}
Insert cell
// === ANALYSEUR DE DONNÉES POUR DIAGNOSTIC ===
// Ajoutez ce code temporairement dans votre Observable pour diagnostiquer

viewof DataAnalyzer = {
const container = d3.create("div");
// Style pour l'affichage
container.style("font-family", "monospace")
.style("background", "#f5f5f5")
.style("padding", "20px")
.style("border-radius", "8px");
container.append("h3").text("🔍 ANALYSE DES DONNÉES");
// === ANALYSE DES DONNÉES BRUTES ===
container.append("h4").text("1. Échantillon des données brutes :");
const sampleData = geojsonData.features.slice(0, 2); // Prendre 2 quartiers
sampleData.forEach((feature, index) => {
const quartierName = feature.properties.data["Nom"];
const population = feature.properties.data["Population_Arr"];
container.append("div")
.style("margin", "10px 0")
.style("padding", "10px")
.style("background", "white")
.style("border-radius", "4px")
.html(`<strong>Quartier ${index + 1}: ${quartierName}</strong><br>
Population: ${population}`);
// Analyser toutes les variables B7, B9, B11, C2a
const variables = ["B7", "B9", "B11", "C2a"];
variables.forEach(variable => {
const varDiv = container.append("div")
.style("margin-left", "20px")
.style("margin", "5px 0");
varDiv.append("strong").text(`Variable ${variable}:`);
const relevantKeys = Object.keys(feature.properties.data).filter(key => {
return (variable === "B11" && key.startsWith("B11_")) ||
(variable === "C2a" && key.startsWith("C2a_")) ||
(key.startsWith(variable + "_classe_"));
});
const ul = varDiv.append("ul").style("margin", "5px 0");
let totalValue = 0;
relevantKeys.forEach(key => {
const value = feature.properties.data[key];
const cleanValue = parseFloat(String(value).replace(",", "."));
totalValue += cleanValue;
ul.append("li")
.style("color", cleanValue > 100 ? "red" : "blue")
.text(`${key}: ${value} (nettoyé: ${cleanValue})`);
});
varDiv.append("div")
.style("font-weight", "bold")
.style("color", totalValue > 120 ? "red" : totalValue < 80 ? "orange" : "green")
.text(`TOTAL: ${totalValue.toFixed(2)} ${totalValue > 120 ? "→ Probablement EFFECTIFS" : totalValue < 80 ? "→ Données incomplètes?" : "→ Probablement POURCENTAGES"}`);
});
});
// === ANALYSE DES CALCULS ===
container.append("h4").text("2. Test des calculs pour B7 :");
const testFeature = geojsonData.features[0];
const testQuartier = testFeature.properties.data["Nom"];
const testPopulation = parseFloat(String(testFeature.properties.data["Population_Arr"]).replace(",", "."));
container.append("div").text(`Quartier test: ${testQuartier}, Population: ${testPopulation}`);
const b7Keys = Object.keys(testFeature.properties.data).filter(key => key.startsWith("B7_classe_"));
const testTable = container.append("table")
.style("border-collapse", "collapse")
.style("margin", "10px 0");
// En-têtes
const header = testTable.append("tr");
["Catégorie", "Valeur originale", "Valeur nettoyée", "Si c'est un effectif → %", "Si c'est un % → effectif"].forEach(h => {
header.append("th")
.style("border", "1px solid #ccc")
.style("padding", "8px")
.style("background", "#e0e0e0")
.text(h);
});
b7Keys.forEach(key => {
const originalValue = testFeature.properties.data[key];
const cleanValue = parseFloat(String(originalValue).replace(",", "."));
const asPercentage = (cleanValue / testPopulation) * 100;
const asEffectif = (cleanValue / 100) * testPopulation;
const row = testTable.append("tr");
[
key,
originalValue,
cleanValue.toFixed(2),
asPercentage.toFixed(2) + "%",
Math.round(asEffectif)
].forEach((cell, i) => {
row.append("td")
.style("border", "1px solid #ccc")
.style("padding", "8px")
.style("background", i === 3 && asPercentage > 100 ? "#ffeeee" : i === 4 && asEffectif > testPopulation ? "#ffeeee" : "white")
.text(cell);
});
});
// === RECOMMANDATIONS ===
container.append("h4").text("3. Questions à vérifier :");
const questions = [
"Les valeurs > 100 sont-elles vraiment des effectifs ?",
"Les valeurs < 100 sont-elles vraiment des pourcentages ?",
"Y a-t-il une colonne qui indique le type de données ?",
"Les totaux par quartier font-ils sens ?",
"La population totale est-elle correcte ?"
];
const questionsList = container.append("ul");
questions.forEach(q => {
questionsList.append("li").text(q);
});
// === SUGGESTIONS DE VARIABLES À VÉRIFIER ===
container.append("h4").text("4. Variables à examiner dans vos données :");
const allKeys = new Set();
geojsonData.features.slice(0, 3).forEach(feature => {
Object.keys(feature.properties.data).forEach(key => allKeys.add(key));
});
const sortedKeys = Array.from(allKeys).sort();
const keysList = container.append("div")
.style("max-height", "200px")
.style("overflow-y", "scroll")
.style("background", "white")
.style("padding", "10px")
.style("border-radius", "4px");
sortedKeys.forEach(key => {
keysList.append("div")
.style("margin", "2px 0")
.style("color", key.includes("B7") || key.includes("B9") || key.includes("B11") || key.includes("C2a") ? "blue" : "#666")
.text(key);
});
return container.node();
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more