1 2 3////// DATA 4const dataset1 = await d3.csv("data/WID Per Capita National Income.csv"); 5const dataset2 = await d3.csv("data/dataset2.csv"); 6const dataset3 = await d3.csv("data/WID National Income Time Series.csv"); 7 8 9////// CHARTS 10 11/// SECTION 1 12 13const labelCol = "Country "; 14const valueCol = "Value "; 15 16const data = dataset1 17 .map(d => ({ name: d[labelCol].trim(), value: +d[valueCol] })) 18 .sort((a, b) => d3.descending(a.value, b.value)) 19 20const color = d3.scaleOrdinal(d3.schemeSet2).domain(data.map(d => d.name)); 21 22async function barchart() { 23 d3.select("#chart_wrapper_1").html(""); // empty the chart area everytime (we click on a button) 24 25 // set the dimensions and margins of the graph 26 const margin = {top: 5, right: 30, bottom: 25, left: 180}, 27 width = 900 - margin.left - margin.right, 28 height = 300 - margin.top - margin.bottom; 29 30 // Append SVG Object to the Page 31 const svg = d3.select("#chart_wrapper_1") 32 .append("svg") 33 .attr("viewBox", "0 0 900 300") 34 35 36 .append("g") 37 .attr("transform","translate(" + margin.left + "," + margin.top + ")"); 38 39 // Add X axis 40 const x = d3.scaleLinear() 41 .domain([0, 70000]) 42 .range([ 0, width]); 43 svg.append("g") 44 .attr("transform", "translate(0," + height + ")") 45 .call(d3.axisBottom(x) 46 .tickValues(d3.range(0,70001,10000)) // ticks = every 10K 47 .tickFormat(d => (d/1000) + "K")) // changing from "10,000" to "10K" 48 .selectAll("text") 49 .style("text-anchor", "middle") 50 .style("font-size", "14px") 51 .style("font-family", "Quattrocento Sans"); 52 53 // Y axis 54 const y = d3.scaleBand() 55 .range([0, height]) 56 .domain(data.map(function(d) { return d.name; })) 57 .padding(.5); 58 59 // 1. On crée le groupe et on y attache l'axe 60 const yAxisGroup = svg.append("g") 61 .call(d3.axisLeft(y).tickSize(0)); // tickSize(0) enlève les ticks de l'axe y 62 63 // 2. On cache la ligne (domaine) 64 yAxisGroup.select(".domain") // on sélectionne le ligne de l'axe y 65 .style("display", "none"); // on cache la ligne de l'axe y 66 67 yAxisGroup.selectAll("text") 68 .style("font-size", "14px") 69 .style("font-family", "Quattrocento Sans") 70 71 72 //Bars 73 const bars = svg.selectAll("myRect") 74 .data(data) 75 .enter() 76 .append("rect") 77 .attr("x", x(0) + 5) // we add a little offset for the start of the bars, otherwise it's to close to the text on each line 78 .attr("y", function(d) { return y(d.name); }) 79 .attr("width", 0) // we start the bar at zero width so we can animate it, see below 80 .attr("height", y.bandwidth() ) 81 .attr("fill", d => color(d.name)) 82 83 // Animation 84 svg.selectAll("rect") 85 .transition() 86 .duration(800) 87 .attr("width", d => x(d.value)) 88 89 // Tooltip 90 const tooltip = d3.select("#chart_wrapper_1") 91 .append("div") 92 .attr("class", "tooltip") 93 .style("position", "absolute") 94 .style("visibility", "hidden") 95 .style("background-color", "rgba(0,0,0,0.8)") 96 .style("color","#fff") 97 .style("padding", "8px 12px") 98 .style("border-radius", "4px") 99 .style("font-family", "Quattrocento Sans") 100 .style("font-size", "14px") 101 .style("white-space", "nowrap") 102 .style("z-index", 1000); 103 104 bars.on("mouseover", function(event, d) { 105 const formatter = new Intl.NumberFormat('en-US', { 106 style: 'currency', 107 currency: 'USD', 108 maximumFractionDigits: 0 109 }); 110 111 tooltip.html(` 112 ${formatter.format(d.value)} 113 `) 114 .style("visibility", "visible"); 115 }) 116 .on("mousemove", function(event) { 117 tooltip 118 .style("top", (event.pageY - 10) + "px") 119 .style("left", (event.pageX + 15) + "px"); 120 }) 121 .on("mouseout", function() { 122 tooltip.style("visibility", "hidden"); 123 }); 124 125 126} 127 128barchart(); 129 130 131async function lollipop(){ 132 d3.select("#chart_wrapper_1").html(""); 133 // set the dimensions and margins of the graph
134 const margin = {top: 5, right: 30, bottom: 25, left: 180}, 135 width = 900 - margin.left - margin.right, 136 height = 300 - margin.top - margin.bottom; 137 138 // Append SVG Object to the Page 139 const svg = d3.select("#chart_wrapper_1") 140 .append("svg") 141 .attr("viewBox", "0 0 900 300") 142 143 144 .append("g") 145 .attr("transform","translate(" + margin.left + "," + margin.top + ")"); 146 147 // Add X axis 148 const x = d3.scaleLinear() 149 .domain([0, 70000]) 150 .range([ 0, width]); 151 svg.append("g") 152 .attr("transform", "translate(0," + height + ")") 153 .call(d3.axisBottom(x) 154 .tickValues(d3.range(0,70001,10000)) // ticks = every 10K 155 .tickFormat(d => (d/1000) + "K")) // changing from "10,000" to "10K" 156 .selectAll("text") 157 .style("text-anchor", "middle") 158 .style("font-size", "14px") 159 .style("font-family", "Quattrocento Sans"); 160 161 // Y axis 162 const y = d3.scaleBand() 163 .range([0, height]) 164 .domain(data.map(function(d) { return d.name; })) 165 .padding(.5); 166 167 // --- NOUVELLE APPROCHE POUR LES LABELS --- 168 // On crée les textes manuellement pour un contrôle total 169 const yLabels = svg.selectAll("yLabel") 170 .data(data) 171 .enter() 172 .append("text") 173 .attr("x", -5) // Position horizontale (à gauche de l'axe) 174 .attr("y", function(d) { 175 // CALCUL CRITIQUE : 176 // On prend le début de la bande + la moitié de sa hauteur. 177 // C'est EXACTEMENT le centre géométrique de la bande. 178 return y(d.name) + y.bandwidth() / 2; 179 }) 180 .text(function(d) { return d.name; }) 181 .style("font-size", "14px") 182 .style("font-family", "Quattrocento Sans") 183 .style("text-anchor", "end") // Aligner le texte à droite (vers l'axe) 184 .style("dominant-baseline", "middle"); // Maintenant, 'middle' fonctionne parfaitement car on est au centre géométrique 185 186 // Lines (Tiges) 187 // Note: On utilise la même logique pour le Y 188 const line = svg.selectAll("myLine") 189 .data(data) 190 .enter() 191 .append("line") 192 .attr("x1", x(0) + 5) 193 .attr("x2", x(0) + 5) 194 .attr("y1", function(d) { return y(d.name) + y.bandwidth() / 2; }) // Centre exact de la bande 195 .attr("y2", function(d) { return y(d.name) + y.bandwidth() / 2; }) // Centre exact de la bande 196 .attr("stroke", "black") 197 .attr("stroke-width", 1.5); // Optionnel : épaissir un peu la ligne pour la visibilité 198 199 // Circles 200 const circles = svg.selectAll("mycircle") 201 .data(data) 202 .enter() 203 .append("circle") 204 .attr("cx", x(0)) 205 .attr("cy", function(d) { return y(d.name) + y.bandwidth() / 2; }) // Centre exact de la bande 206 .attr("r", 10) 207 .style("fill", d => color(d.name)) 208 .attr("stroke", "black"); 209 210 // Animation (Mise à jour des cibles) 211 svg.selectAll("circle") 212 .transition() 213 .duration(2000) 214 .attr("cx", function(d) { return x(d.value); }); 215 216 svg.selectAll("line") 217 .transition() 218 .duration(2000) 219 .attr("x1", function(d) { return x(d.value); }); 220 221 // Tooltip 222 const tooltip = d3.select("#chart_wrapper_1") 223 .append("div") 224 .attr("class", "tooltip") 225 .style("position", "absolute") 226 .style("visibility", "hidden") 227 .style("background-color", "rgba(0,0,0,0.8)") 228 .style("color","#fff") 229 .style("padding", "8px 12px") 230 .style("border-radius", "4px") 231 .style("font-family", "Quattrocento Sans") 232 .style("font-size", "14px") 233 .style("white-space", "nowrap") 234 .style("z-index", 1000); 235 236 line.on("mouseover", function(event, d) { 237 const formatter = new Intl.NumberFormat('en-US', { 238 style: 'currency', 239 currency: 'USD', 240 maximumFractionDigits: 0 241 }); 242 243 tooltip.html(` 244 ${formatter.format(d.value)} 245 `) 246 .style("visibility", "visible"); 247 }) 248 .on("mousemove", function(event) { 249 tooltip 250 .style("top", (event.pageY - 10) + "px") 251 .style("left", (event.pageX + 15) + "px"); 252 }) 253 .on("mouseout", function() { 254 tooltip.style("visibility", "hidden"); 255 }); 256 circles.on("mouseover", function(event, d) { 257 const formatter = new Intl.NumberFormat('en-US', { 258 style: 'currency', 259 currency: 'USD', 260 maximumFractionDigits: 0 261 }); 262 263 tooltip.html(` 264 ${formatter.format(d.value)} 265 `) 266 .style("visibility", "visible"); 267 }) 268 .on("mousemove", function(event) { 269 tooltip 270 .style("top", (event.pageY - 10) + "px") 271 .style("left", (event.pageX + 15) + "px"); 272 }) 273 .on("mouseout", function() { 274 tooltip.style("visibility", "hidden"); 275 }); 276} 277async function proportional_bubbles(){ 278 d3.select("#chart_wrapper_1").html(""); 279 280 // Append SVG Object to the Page 281const tooltip = d3.select("body") 282 .append("div") 283 .attr("class", "donut-chart-tooltip") // Réutilise le même style
283CSS 284 .style("opacity", 0) 285 .style("position", "absolute") 286 .style("pointer-events", "none") 287 .style("background-color", "rgba(0, 0, 0, 0.8)") 288 .style("color", "#fff") 289 .style("padding", "8px 12px") 290 .style("border-radius", "8px") 291 .style("font-family", "sans-serif") 292 .style("font-size", "14px") 293 .style("white-space", "nowrap") 294 .style("z-index", 1000); 295 296// 1. Configuration du SVG 297const svg = d3.select("#chart_wrapper_1") 298 .append("svg") 299 .attr("viewBox", "0 0 900 300") 300 301 302 .append("g"); 303 304const width = 900; 305const height = 300; 306const margin = 1; 307 308const formatter = new Intl.NumberFormat('en-US', { 309 style: 'currency', 310 currency: 'USD', 311 maximumFractionDigits: 0 312}); 313 314// 2. Configuration du Pack Layout 315// On réduit un peu le padding pour que les bulles soient plus proches si désiré 316const pack = d3.pack() 317 .size([width - margin * 2, height - margin * 2]) 318 .padding(5); 319 320// Préparation des données 321// Assurez-vous que 'data' est un tableau d'objets {name: "...", value: ...} 322const root = pack(d3.hierarchy({children: data}) 323 .sum(d => d.value)); 324 325// 3. Création des groupes (nÅuds) 326const node = svg.append("g") 327 .selectAll("g") 328 .data(root.leaves()) 329 .join("g") 330 .attr("transform", d => `translate(${d.x},${d.y})`); 331 332// 4. Ajout des cercles 333// On stocke le rayon original dans un attribut data pour pouvoir y revenir 334node.append("circle") 335 .attr("fill", d => color(d.data.name)) // Adaptez si votre fonction color attend autre chose 336 .attr("r", d => d.r) 337 .attr("stroke", "white") 338 .attr("stroke-width", "1px") 339 .style("cursor", "pointer") 340 .attr("data-original-r", d => d.r); // Sauvegarde du rayon initial 341 342// 6. Gestion des événements (Survol / Sortie) 343node 344 .on("mouseover", function(event, d) { 345 const currentGroup = d3.select(this); 346 const circle = currentGroup.select("circle"); 347 const originalRadius = parseFloat(circle.attr("data-original-r")); 348 const newRadius = originalRadius * 1.2; // Agrandissement de 20% 349 350 // A. Mettre le groupe au premier plan (pour ne pas être caché par les voisins) 351 currentGroup.raise(); 352 353 // B. Animer le cercle (Rayon + Bordure) 354 circle 355 .transition() 356 .duration(200) 357 .attr("r", newRadius) 358 .attr("stroke", "#333") 359 .attr("stroke-width", "3px"); 360 361 // C. Ajuster le clip-path du texte pour qu'il suive l'agrandissement 362 // (Optionnel : si vous voulez que le texte reste visible sur la partie agrandie) 363 currentGroup.select("text") 364 .transition() 365 .duration(200) 366 .attr("clip-path", `circle(${newRadius})`); 367 368 // D. Afficher et positionner la Tooltip HTML 369 tooltip 370 .transition() 371 .duration(150) 372 .style("opacity", 1); 373 374 tooltip.html(` 375 <div style="display:flex; align-items:center;"> 376 377 <strong style="font-family: Quattrocento Sans; ">${d.data.name}</strong> 378 </div> 379 <div style="font-family: Quattrocento Sans; margin-top: 4px; "> 380 ${formatter.format(d.value)} 381 </div> 382 `); 383 384 tooltip 385 .style("left", (event.pageX + 15) + "px") 386 .style("top", (event.pageY - 10) + "px"); 387 }) 388 .on("mousemove", function(event) { 389 // Faire suivre la tooltip si la souris bouge 390 tooltip 391 .style("left", (event.pageX + 15) + "px") 392 .style("top", (event.pageY - 10) + "px"); 393 }) 394 .on("mouseout", function(event, d) { 395 const currentGroup = d3.select(this); 396 const circle = currentGroup.select("circle"); 397 const originalRadius = parseFloat(circle.attr("data-original-r")); 398 399 // A. Revenir au rayon et à la bordure d'origine 400 circle 401 .transition() 402 .duration(200) 403 .attr("r", originalRadius) 404 .attr("stroke-width", "0px"); 405 406 // B. Rétablir le clip-path original pour le texte 407 currentGroup.select("text") 408 .transition() 409 .duration(200) 410 .attr("clip-path", `circle(${originalRadius})`); 411 412 // C. Masquer la tooltip 413 tooltip 414 .transition() 415 .duration(150) 416 .style("opacity", 0); 417 }); 418} 419 420async function donut(){ 421 d3.select("#chart_wrapper_1").html(""); 422 // Append SVG Object to the Page 423 // 1. Configuration et Données 424 const svgContainer = d3.select("#chart_wrapper_1"); 425 const width = 900; 426 const height = 300; 427 const radius = 100; 428 429 // Création de la DIV pour la tooltip (une seule fois) 430 // On l'ajoute au 'body' pour qu'elle puisse flotter partout 431 const tooltip = d3.select("body") 432 .append("div") 433 .attr("class", "donut-chart-tooltip") 434 .style("opacity", 0) 435 .style("position", "absolute") 436 .style("pointer-events", "none") // La souris ignore la tooltip pour ne pas la faire clignoter 437 .style("background-color", "rgba(0, 0, 0, 0.8)") 438 .style("color", "#fff") 439 .style("padding", "8px 12px") 440 .style("border-radius", "8px") 441 .style("font-family", "Quattrocento Sans") 442 .style("font-size", "14px") 443 .style("white-space", "nowrap") 444 .style("z-index", 1000); 445 446 const svg = svgContainer 447 .append("svg") 448 .attr("viewBox", `0 0 ${width} ${height}`) 449 450 451 .append("g") 452 .attr("transform", `translate(${width / 2},${height / 2})`); 453 454 const arc = d3.arc() 455 .innerRadius(radius * 0.5) 456 .outerRadius(radius - 1); 457 458 // Arc pour l'effet de survol (agrandissement + bordure) 459 const arcHover = d3.arc() 460 .innerRadius(radius * 0.40) 461 .outerRadius(radius * 1.05); 462 463 const pie = d3.pie() 464 .padAngle(1 / radius) 465 .sort(null) 466 .value(d => d.value); 467 468 const formatter = new Intl.NumberFormat('en-US', { 469 style: 'currency', 470 currency: 'USD', 471 maximumFractionDigits: 0 472 }); 473 474 // 2. Création des segments 475 const paths = svg.append("g") 476 .selectAll("path") 477 .data(pie(data)) 478 .join("path") 479 .attr("d", arc) 480 .attr("fill", d => color(d.data.name)) 481 .style("cursor", "pointer"); 482 483 // 3. Gestion des événements (Survol / Sortie) 484 paths 485 .on("mouseover", function(event, d) { 486 // A. Effet visuel sur le segment 487 d3.select(this) 488 .transition() 489 .duration(200) 490 .attr("d", arcHover) // Agrandit le segment 491 .attr("stroke", "#333") // Change la bordure pour marquer le focus 492 .attr("stroke-width", "4px"); 493 494 // B. Affichage et positionnement de la Tooltip HTML 495 tooltip 496 .transition() 497 .duration(150) 498 .style("opacity", 1); 499 500 // Contenu de la tooltip (Nom + Valeur formatée) 501 tooltip.html(` 502 <div style="display:flex; align-items:center;"> 503 <strong>${d.data.name}</strong> 504 </div> 505 <div style="margin-top:4px; margin-left:18px; font-size:1.1em;"> 506 ${formatter.format(d.value)} 507 </div> 508 `); 509 510 // Positionnement : on suit la souris avec un léger décalage 511 tooltip 512 .style("left", (event.pageX + 15) + "px") 513 .style("top", (event.pageY - 10) + "px"); 514 }) 515 .on("mousemove", function(event) { 516 // Optionnel : Faire suivre la tooltip si la souris bouge beaucoup pendant le survol 517 tooltip 518 .style("left", (event.pageX + 15) + "px") 519 .style("top", (event.pageY - 10) + "px"); 520 }) 521 .on("mouseout", function(event, d) { 522 // A. Retour à la normale du segment 523 d3.select(this) 524 .transition() 525 .duration(200) 526 .attr("d", arc) 527 .attr("stroke-width", "0px"); 528 529 // B. Masquer la tooltip 530 tooltip 531 .transition() 532 .duration(150) 533 .style("opacity", 0); 534 }); 535} 536 537async function circular_bars(){ 538 d3.select("#chart_wrapper_1").html(""); 539 540 // ---- svg scaffold ------------------------------------------------------- 541 const width = 900, height = 300; 542 const svg = d3.select("#chart_wrapper_1") 543 .append("svg") 544 .attr("viewBox", `0 0 ${width} ${height}`) 545 546 ; 547 548 const cx = width / 2, cy = height / 2; 549 const outerMostRadius = 150; 550 const ringThickness = 9; 551 const ringGap = 15; 552 const startAngle = 0; 553 554 const plot = svg.append("g").attr("transform", `translate(${cx},${cy})`); 555 const maxValue = d3.max(data, d => d.value); 556 557 function angleFor(value) { 558 return startAngle + (value / maxValue) * 2 * Math.PI; 559 } 560 561 const barArc = d3.arc() 562 .startAngle(startAngle) 563 .innerRadius((d, i) => outerMostRadius - i * (ringThickness + ringGap) - ringThickness) 564 .outerRadius((d, i) => outerMostRadius - i * (ringThickness + ringGap)) 565 .cornerRadius(ringThickness / 2); 566 567 // ---- 1. CRÃATION DU TOOLTIP (HTML) -------------------------------------- 568 // On crée une div unique pour le tooltip, cachée par défaut 569 const tooltip = d3.select("#chart_wrapper_1") 570 .append("div") 571 .attr("class", "tooltip") 572 .style("position", "absolute") 573 .style("visibility", "hidden") 574 .style("background-color", "rgba(0,0,0,0.8)") 575 .style("color","#fff") 576 .style("padding", "8px 12px") 577 .style("border-radius", "4px") 578 .style("font-family", "Quattrocento Sans") 579 .style("font-size", "14px") 580 .style("white-space", "nowrap") 581 .style("z-index", 1000); 582 583 const bars = plot.selectAll(".bar") 584 .data(data) 585 .join("path") 586 .attr("class", "bar") 587 .attr("fill", d => color(d.name)) 588 .attr("stroke", "none") 589 // .style("cursor", "pointer") 590 // Initialisation de l'angle pour l'animation 591 .each(function (d) { this._current = { endAngle: startAngle }; }); 592 593 // Lancement de l'animation sur la sélection 'bars' 594 bars.transition() 595 .duration(750) 596 .ease(d3.easeCubicOut) 597 .attrTween("d", function (d, i) { 598 const interp = d3.interpolate(startAngle, angleFor(d.value)); 599 const node = this; 600 return function (t) { 601 const a = interp(t); 602 node._current.endAngle = a; 603 return barArc.endAngle(a)(d, i); 604 }; 605 }); 606 607 // AJOUT DES ÃVÃNEMENTS SUR LA SÃLECTION 'bars' (et non sur la transition) 608 bars.on("mouseover", function(event, d) { 609 const formatter = new Intl.NumberFormat('en-US', { 610 style: 'currency', 611 currency: 'USD', 612 maximumFractionDigits: 0 613 }); 614 615 tooltip.html(` 616 <strong>${d.name}</strong><br> 617 ${formatter.format(d.value)} 618 `) 619 .style("visibility", "visible"); 620 }) 621 .on("mousemove", function(event) { 622 tooltip 623 .style("top", (event.pageY - 10) + "px") 624 .style("left", (event.pageX + 15) + "px"); 625 }) 626 .on("mouseout", function() { 627 tooltip.style("visibility", "hidden"); 628 }); 629} 630 631// SECTION 2 632 633async function violin(){ 634// 0. Nettoyage et préparation du Tooltip 635d3.select("#chart_wrapper_2").html(""); 636 637// Création de la div pour le tooltip (cachée par défaut) 638const tooltip = d3.select("#chart_wrapper_2") 639 .append("div") 640 .style("position", "absolute") 641 .style("background", "rgba(0, 0, 0, 0.8)") 642 .style("color", "#fff") 643 .style("padding", "8px 12px") 644 .style("border-radius", "4px") 645 .style("font-family", "Quattrocento Sans") 646 .style("font-size", "14px") 647 .style("pointer-events", "none") // La souris ne bloque pas le tooltip 648 .style("opacity", 0) 649 .style("z-index", 10); 650 651// 1. Dimensions et marges 652var margin = {top: 10, right: 30, bottom: 40, left: 80}, // Marge gauche augmentée pour les labels 653 width = 900 - margin.left - margin.right, 654 height = 400 - margin.top - margin.bottom; // Hauteur augmentée 655 656// 2. Création du SVG 657const svg = d3.select("#chart_wrapper_2") 658 .append("svg") 659 .attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`) 660 661 662 .append("g") 663 .attr("transform", `translate(${margin.left},${margin.top})`); 664 665// Vérification des données 666if (!dataset2 || dataset2.length === 0) { 667 console.error("Erreur : dataset2 est vide."); 668} else { 669 670 // --- Extraction des régions et Ãchelle de Couleurs --- 671 var regions = [...new Set(dataset2.map(d => d.region))].sort(); 672 673 // Ãchelle de couleurs : une couleur distincte par région 674 var colorScale = d3.scaleOrdinal() 675 .domain(regions) 676 .range(d3.schemeTableau10); // Utilise la palette Tableau 10 (10 couleurs distinctes) 677 678 // --- Ãchelles --- 679 680 // Nettoyage des données : s'assurer que Value est un nombre 681 dataset2.forEach(d => { 682 d.Value = +d.Value; 683 }); 684 685 var yMin = d3.min(dataset2, d => d.Value); 686 var yMax = d3.max(dataset2, d => d.Value); 687 688 var y = d3.scaleLinear() 689 .domain([yMin, yMax]) 690 .range([height, 0]); 691 692 svg.append("g") 693 .call(d3.axisLeft(y).ticks(5)) 694 .selectAll("text") 695 .style("font-size", "14px") 696 .style("font-family", "Quattrocento Sans"); 697 698 var x = d3.scaleBand() 699 .range([0, width]) 700 .domain(regions) 701 .padding(0.1); 702 703 svg.append("g") 704 .attr("transform", `translate(0,${height})`) 705 .call(d3.axisBottom(x)) 706 .selectAll("text") 707 .style("text-anchor", "middle") 708 .style("font-size", "14px") 709 .style("font-family", "Quattrocento Sans"); 710 711 // --- Histogramme --- 712 var histogram = d3.histogram() 713 .domain(y.domain()) 714 .thresholds(y.ticks(30)) // Résolution un peu plus fine 715 .value(d => d); 716 717 // --- Calcul des stats (Rollup) + Médiane --- 718 var sumstat = Array.from( 719 d3.rollup( 720 dataset2, 721 (v) => { 722 const input = v.map(g => g.Value).filter(val => !isNaN(val)); 723 const bins = histogram(input); 724 725 // Calcul de la médiane pour le tooltip 726 const sorted = input.slice().sort((a, b) => a - b); 727 const mid = Math.floor(sorted.length / 2); 728 const median = sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + s
728orted[mid]) / 2; 729 730 // On attache la médiane au premier bin (ou on la stocke ailleurs, ici on l'ajoute comme propriété du tableau de bins) 731 bins.median = median; 732 return bins; 733 }, 734 (d) => d.region 735 ), 736 ([key, value]) => ({ key: key, value: value }) 737 ); 738 739 // --- Calcul de maxNum --- 740 var maxNum = 0; 741 for (let i = 0; i < sumstat.length; i++) { 742 let allBins = sumstat[i].value; 743 if (!allBins || allBins.length === 0) continue; 744 let lengths = allBins.map(bin => bin.length); 745 let longuest = d3.max(lengths); 746 if (longuest > maxNum) maxNum = longuest; 747 } 748 if (maxNum === 0) maxNum = 1; 749 750 var xNum = d3.scaleLinear() 751 .range([0, x.bandwidth()]) 752 .domain([-maxNum, maxNum]); 753 754 // --- Dessin des Violons avec Interactions --- 755 svg 756 .selectAll("myViolin") 757 .data(sumstat) 758 .enter() 759 .append("g") 760 .attr("transform", d => `translate(${x(d.key)},0)`) 761 .append("path") 762 .datum(d => d.value) 763 .style("fill", d => colorScale(sumstat.find(s => s.value === d).key)) // Couleur par région 764 .style("stroke", "#333") 765 .style("stroke-width", "1") 766 .style("opacity", 0.85) 767 .attr("d", d3.area() 768 .x0(d => xNum(-d.length)) 769 .x1(d => xNum(d.length)) 770 .y(d => y(d.x0)) 771 .curve(d3.curveCatmullRom) 772 ) 773 // --- Ãvénements Souris (Tooltip) --- 774 .on("mouseover", function(event, d) { 775 // Retrouver la région associée à ces données (d est le tableau de bins) 776 // On cherche dans sumstat quel objet a ce 'value' 777 const groupData = sumstat.find(s => s.value === d); 778 const regionName = groupData.key; 779 const medianValue = d.median; 780 781 // Formatage de la médiane (ex: 2 chiffres décimaux si nécessaire) 782 const medianFormatted = Number.isInteger(medianValue) ? medianValue : medianValue.toFixed(2); 783 784 tooltip.transition() 785 .duration(200) 786 .style("opacity", 1); 787 788 tooltip.html(` 789 <strong>${regionName}</strong><br/> 790 Median Value : ${d3.format("$,.0f")(medianFormatted)} 791 `) 792 .style("left", (event.pageX + 10) + "px") 793 .style("top", (event.pageY - 28) + "px"); 794 795 // Effet de surbrillance (optionnel) 796 d3.select(this) 797 .style("opacity", 1) 798 .style("stroke-width", "2"); 799 }) 800 .on("mousemove", function(event) { 801 tooltip 802 .style("left", (event.pageX + 10) + "px") 803 .style("top", (event.pageY - 28) + "px"); 804 }) 805 .on("mouseout", function() { 806 tooltip.transition() 807 .duration(500) 808 .style("opacity", 0); 809 810 // Retour à l'état normal 811 d3.select(this) 812 .style("opacity", 0.85) 813 .style("stroke-width", "1"); 814 }); 815} 816} 817 818violin(); 819async function jitter(){ 820 // 0. Nettoyage du conteneur 821d3.select("#chart_wrapper_2").html(""); 822 823// 1. Dimensions et marges 824var margin = {top: 10, right: 30, bottom: 40, left: 80}, 825 width = 900 - margin.left - margin.right, 826 height = 300 - margin.top - margin.bottom; 827 828// 2. Création du SVG 829const svg = d3.select("#chart_wrapper_2") 830 .append("svg") 831 .attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`) 832 833 834 .append("g") 835 .attr("transform", `translate(${margin.left},${margin.top})`); 836 837// Vérification des données 838if (!dataset2 || dataset2.length === 0) { 839 console.error("Erreur : dataset2 est vide."); 840} else { 841 842 // --- Préparation des données --- 843 844 // Conversion explicite en nombres et extraction des régions uniques 845 dataset2.forEach(d => { d.Value = +d.Value; }); 846 var regions = [...new Set(dataset2.map(d => d.region))].sort(); 847 848 // Ãchelle de couleurs (la même que pour le violon pour la cohérence) 849 var colorScale = d3.scaleOrdinal() 850 .domain(regions) 851 .range(d3.schemeTableau10); 852 853 // --- Calcul des statistiques (Boxplot) --- 854 // Remplacement de d3.nest() par d3.rollup() 855 var sumstat = Array.from( 856 d3.rollup( 857 dataset2, 858 (v) => { 859 // Extraction et tri des valeurs pour la région 860 const values = v.map(g => g.Value).filter(val => !isNaN(val)).sort(d3.ascending); 861 862 // Calcul des quartiles et de l'IQR 863 const q1 = d3.quantile(values, 0.25); 864 const median = d3.quantile(values, 0.5); 865 const q3 = d3.quantile(values, 0.75); 866 const iqr = q3 - q1; 867 868 // Calcul des moustaches (min/max) avec la règle de 1.5 * IQR 869 // On cherche la valeur réelle la plus proche dans les données, pas juste le calcul théorique 870 let min = q1 - 1.5 * iqr; 871 let max = q3 + 1.5 * iqr; 872 873 // Ajustement aux valeurs réelles les plus proches (whiskers) 874 const actualMin = values.find(val => val >= min) || values[0]; 875 const actualMax = values.slice().reverse().find(val => val <= max) || values[values.length - 1]; 876 877 return { 878 q1: q1, 879 median: median, 880 q3: q3, 881 iqr: iqr, 882 min: actualMin, 883 max: actualMax, 884 values: values // On garde les valeurs brutes si besoin 885 }; 886 }, 887 (d) => d.region 888 ), 889 ([key, value]) => ({ key: key, value: value }) 890 ); 891 892 // --- Ãchelles --- 893 894 // Ãchelle Y (Valeurs) 895 var yMin = d3.min(dataset2, d => d.Value); 896 var yMax = d3.max(dataset2, d => d.Value); 897 898 var y = d3.scaleLinear() 899 .domain([yMin * 0.95, yMax * 1.05]) 900 .range([height, 0]); 901 902 svg.append("g") 903 .call(d3.axisLeft(y).ticks(5)) 904 .selectAll("text") 905 .style("font-size", "14px") 906 .style("font-family", "Quattrocento Sans"); 907 908 // Ãchelle X (Régions) 909 var x = d3.scaleBand() 910 .range([0, width]) 911 .domain(regions)
912 .paddingInner(0.6) // Espace entre les boîtes (plus grand = boîtes plus fines) 913 .paddingOuter(0.4); 914 915 svg.append("g") 916 .attr("transform", `translate(0,${height})`) 917 .call(d3.axisBottom(x)) 918 .selectAll("text") 919 .style("text-anchor", "middle") 920 .style("font-size", "14px") 921 .style("font-family", "Quattrocento Sans"); 922 923 // Color scale for dots 924 var myColor = d3.scaleSequential() 925 .interpolator(d3.interpolateInferno) 926 .domain([yMin * 0.95, yMax * 1.05]) 927 928 // --- Dessin du Boxplot --- 929 930 // // 1. Ligne verticale (moustaches) 931 // svg.selectAll("vertLines") 932 // .data(sumstat) 933 // .enter() 934 // .append("line") 935 // .attr("x1", d => x(d.key) + x.bandwidth() / 2) // Centré dans la bande 936 // .attr("x2", d => x(d.key) + x.bandwidth() / 2) 937 // .attr("y1", d => y(d.value.min)) 938 // .attr("y2", d => y(d.value.max)) 939 // .attr("stroke", "#333") 940 // .attr("stroke-width", 2); 941 942 // // 2. Boîte principale (Rect) 943 // // La largeur est dynamique : c'est x.bandwidth() 944 // svg.selectAll("boxes") 945 // .data(sumstat) 946 // .enter() 947 // .append("rect") 948 // .attr("x", d => x(d.key)) 949 // .attr("y", d => y(d.value.q3)) 950 // .attr("height", d => y(d.value.q1) - y(d.value.q3)) 951 // .attr("width", x.bandwidth()) 952 // .attr("fill", d => colorScale(d.key)) // Couleur par région 953 // .attr("stroke", "#333") 954 // .attr("stroke-width", 1) 955 // .style("opacity", 0.7); 956 957 // // 3. Ligne médiane 958 // svg.selectAll("medianLines") 959 // .data(sumstat) 960 // .enter() 961 // .append("line") 962 // .attr("x1", d => x(d.key)) 963 // .attr("x2", d => x(d.key) + x.bandwidth()) 964 // .attr("y1", d => y(d.value.median)) 965 // .attr("y2", d => y(d.value.median)) 966 // .attr("stroke", "#fff") // Blanc pour contraster avec la boîte 967 // .attr("stroke-width", 2); 968 969 // --- Ajout des points individuels (Jitter) --- 970 971 // 1. On ajoute une classe spécifique ".jitter-point" pour les cibler proprement 972 svg.selectAll(".jitter-point") 973 .data(dataset2) 974 .enter() 975 .append("circle") 976 .attr("class", "jitter-point") // CLASSE AJOUTÃE 977 .attr("cx", d => { 978 const centerX = x(d.region) + x.bandwidth() / 2; 979 const jitterRange = x.bandwidth() * 0.8; 980 const randomOffset = (Math.random() - 0.5) * jitterRange; 981 return centerX + randomOffset; 982 }) 983 .attr("cy", d => y(d.Value)) 984 .attr("r", 3.5) 985 .attr("fill", "#ddd") 986 .attr("stroke", d => colorScale(d.region)) 987 .attr("stroke-width", 2); 988 989 // 2. Création du tooltip AVANT les événements, avec opacity: 0 par défaut 990 const tooltip = d3.select("#chart_wrapper_2") 991 .append("div") 992 .attr("class", "jitter-tooltip") // Classe pour le retrouver si besoin 993 .style("position", "absolute") 994 .style("opacity", 0) // INVISIBLE PAR DÃFAUT 995 .style("background", "rgba(0,0,0,0.8)") 996 .style("color", "#fff") 997 .style("padding", "8px 12px") 998 .style("border-radius", "4px") 999 .style("font-family", "Quattrocento Sans") 1000 .style("font-size", "14px") 1001 .style("white-space", "nowrap") 1002 .style("pointer-events", "none") // La souris passe au travers pour éviter les tremblements 1003 .style("z-index", 1000); 1004 1005 // 3. Application des événements uniquement sur la classe .jitter-point 1006 svg.selectAll(".jitter-point") 1007 .on("mouseover", function(event, d) { 1008 // Effet visuel sur le point 1009 d3.select(this) 1010 .attr("stroke", "#333") 1011 .attr("stroke-width", 1.5) 1012 .attr("r", 5); // Agrandissement léger 1013 1014 // Affichage du tooltip 1015 tooltip 1016 .style("opacity", 1) 1017 .html(`<strong>${d.Country}</strong><br>${d3.format("$,.0f")(d.Value)}`); 1018 }) 1019 .on("mousemove", function(event) { 1020 // UTILISATION DE event.pageX/Y pour un suivi parfait 1021 tooltip 1022 .style("left", (event.pageX + 15) + "px") 1023 .style("top", (event.pageY - 10) + "px"); 1024 }) 1025 .on("mouseout", function() { 1026 // Retour à la normale 1027 d3.select(this) 1028 .attr("stroke", d => colorScale(d.region)) 1029 .attr("stroke-width", 2) 1030 .attr("r", 3.5); 1031 1032 tooltip.style("opacity", 0); 1033 }); 1034} 1035} 1036 1037async function ridgeline(){ 1038 d3.select("#chart_wrapper_2").html(""); 1039 // Append SVG Object to the Page 1040 // const svg = d3.select("#chart_wrapper_2") 1041 // .append("svg") 1042 // .attr("viewBox", "0 0 900 300") 1043 // 1044 // 1045 // .append("g") 1046 // .attr("transform","translate(50,20)"); 1047 1048// // 1. Dimensions
1049// var margin = {top: 80, right: 100, bottom: 50, left: 110}, 1050// width = 900 - margin.left - margin.right, 1051// height = 300 - margin.top - margin.bottom; 1052 1053 1054// // 2. Création du SVG 1055// var svg = d3.select("#chart_wrapper_2") 1056// .append("svg") 1057// .attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`) 1058// 1059// .append("g") 1060// .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 1061 1062// // 3. Chargement des données 1063// d3.csv("https://raw.githubusercontent.com/zonination/perceptions/master/probly.csv").then(function(data) { 1064 1065// // Catégories fixes 1066// var categories = ["Almost Certainly", "Very Good Chance", "We Believe", "Likely", "About Even", "Little Chance", "Chances Are Slight", "Almost No Chance"]; 1067// var n = categories.length; 1068 1069// // CORRECTION ICI : Déclaration explicite de allMeans 1070// var allMeans = []; 1071 1072// for (let i = 0; i < categories.length; i++) { 1073// let currentGroup = categories[i]; 1074// // Calcul de la moyenne 1075// let mean = d3.mean(data, function(d) { return +d[currentGroup]; }); 1076// allMeans.push(mean); 1077// } 1078 1079// // Ãchelle de couleur 1080// var myColor = d3.scaleSequential() 1081// .domain([0, 100]) 1082// .interpolator(d3.interpolateViridis); 1083 1084// // Axe X 1085// var x = d3.scaleLinear() 1086// .domain([-10, 120]) 1087// .range([0, width]); 1088 1089// svg.append("g") 1090// .attr("class", "xAxis") 1091// .attr("transform", "translate(0," + height + ")") 1092// .call(d3.axisBottom(x).tickValues([0, 25, 50, 75, 100]).tickSize(-height)) 1093// .select(".domain").remove(); 1094 1095// // Label Axe X 1096// svg.append("text") 1097// .attr("text-anchor", "end") 1098// .attr("x", width) 1099// .attr("y", height + 40) 1100// .text("Probability (%)"); 1101 1102// // Ãchelle Y pour les densités 1103// var y = d3.scaleLinear() 1104// .domain([0, 0.25]) 1105// .range([height, 0]); 1106 1107// // Ãchelle Y pour les noms 1108// var yName = d3.scaleBand() 1109// .domain(categories) 1110// .range([0, height]) 1111// .paddingInner(1); 1112 1113// svg.append("g") 1114// .call(d3.axisLeft(yName).tickSize(0)) 1115// .select(".domain").remove(); 1116 1117// // Fonctions Mathématiques 1118// function kernelDensityEstimator(kernel, X) { 1119// return function(V) { 1120// return X.map(function(x) { 1121// return [x, d3.mean(V, function(v) { return kernel(x - v); })]; 1122// }); 1123// }; 1124// } 1125 1126// function kernelEpanechnikov(k) { 1127// return function(v) { 1128// return Math.abs(v /= k) <= 1 ? 0.75 * (1 - v * v) / k : 0; 1129// }; 1130// } 1131 1132// // Calcul de la densité 1133// var kde = kernelDensityEstimator(kernelEpanechnikov(7), x.ticks(40)); 1134// var allDensity = []; 1135 1136// for (let i = 0; i < n; i++) { 1137// let key = categories[i]; 1138// let values = data.map(function(d){ return +d[key]; }); 1139// let density = kde(values); 1140// allDensity.push({key: key, density: density}); 1141// } 1142 1143// // Dessin des aires 1144// svg.selectAll("areas") 1145// .data(allDensity) 1146// .enter() 1147// .append("path") 1148// .attr("transform", function(d){ return "translate(0," + (yName(d.key) - height) + ")"; }) 1149// .attr("fill", function(d){ 1150// var grp = d.key; 1151// var index = categories.indexOf(grp); 1152// var value = allMeans[index]; 1153// return myColor(value); 1154// }) 1155// .attr("opacity", 0.7) 1156// .attr("stroke", "#000") 1157// .attr("stroke-width", 0.5) 1158// .datum(function(d){ return d.density; }) 1159// .attr("d", d3.line() 1160// .curve(d3.curveBasis) 1161// .x(function(d) { return x(d[0]); }) 1162// .y(function(d) { return y(d[1]); }) 1163// ); 1164 1165// }).catch(function(error) { 1166// console.error("Erreur chargement données:", error); 1167// }); 1168 1169// 0. Nettoyage 1170d3.select("#chart_wrapper_2").html(""); 1171 1172// 1. Dimensions 1173var margin = {top: 60, right: 30, bottom: 40, left: 120}, 1174 width = 900 - margin.left - margin.right, 1175 height = 300 - margin.top - margin.bottom; 1176 1177var svg = d3.select("#chart_wrapper_2") 1178 .append("svg") 1179 .attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`) 1180 1181 1182 .append("g") 1183 .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 1184 1185if (!dataset2 || dataset2.length === 0) { 1186 console.error("Erreur : dataset2 est vide."); 1187} else { 1188 1189 // 2. Préparation des données 1190 dataset2.forEach(d => { d.Value = +d.Value; }); 1191 var categories = [...new Set(dataset2.map(d => d.region))].sort(); 1192 var n = categories.length; 1193 1194 // --- CHANGEMENT 1 : Couleurs fixes par région (plus de dégradé Viridis) --- 1195 // Utilisation de la palette Tableau 10 (couleurs distinctes et harmonieuses) 1196 var myColor = d3.scaleOrdinal() 1197 .domain(categories) 1198 .range(d3.schemeTableau10); 1199 1200 // 3. Ãchelle X 1201 var xMin = d3.min(dataset2, d => d.Value); 1202 var xMax = d3.max(dataset2, d => d.Value); 1203 1204 // --- CHANGEMENT 2 : Domaine X forcé à commencer à 0 --- 1205 // Cela évite que les courbes des régions pauvres soient coupées à gauche. 1206 // On garde une marge de 5% seulement sur le maximum. 1207 var x = d3.scaleLinear() 1208 .domain([0, xMax * 1.05]) 1209 .range([0, width]); 1210 1211 svg.append("g") 1212 .attr("transform", "translate(0," + height + ")") 1213 .call(d3.axisBottom(x).ticks(5)) 1214 .selectAll("text") 1215 .style("text-anchor", "middle") 1216 .style("font-size", "14px") 1217 .style("font-family", "Quattrocento Sans") 1218 .select(".domain").remove(); 1219 1220 // 4. Ãchelle Y (Noms) 1221 var yName = d3.scaleBand() 1222 .domain(categories) 1223 .range([0, height]) 1224 .paddingInner(0.9); // Chevauchement important 1225 1226 svg.append("g") 1227 .call(d3.axisLeft(yName).tickSize(0)) 1228 .selectAll("text") 1229 .style("font-size", "14px") 1230 .style("font-family", "Quattrocento Sans"); 1231 1232 svg.select(".domain").remove(); 1233 svg.selectAll(".tick line").remove(); 1234 1235 // 5. Fonctions Kernel 1236 function kernelDensityEstimator(kernel, X) { 1237 return function(V) { 1238 return X.map(function(x) { 1239 return [x, d3.mean(V, function(v) { return kernel(x - v); })]; 1240 }); 1241 }; 1242 } 1243 1244 function kernelEpanechnikov(k) { 1245 return function(v) { 1246 return Math.abs(v /= k) <= 1 ? 0.75 * (1 - v * v) / k : 0; 1247 }; 1248 } 1249 1250 // 6. Calcul des densités 1251 // Ajustement de la bande passante (bandwidth) basé sur l'étendue totale (de 0 à max) 1252 var bandwidth = (xMax - 0) / 15; 1253 var kde = kernelDensityEstimator(kernelEpanechnikov(bandwidth), x.ticks(80)); // Augmenté à 80 points pour plus de précision sur la grande plage 1254 1255 var allDensity = []; 1256 var maxDensity = 0; 1257 1258 for (let i = 0; i < n; i++) { 1259 let key = categories[i];
1260 let values = dataset2.filter(d => d.region === key).map(d => d.Value); 1261 let density = kde(values); 1262 allDensity.push({key: key, density: density}); 1263 1264 let localMax = d3.max(density, d => d[1]); 1265 if (localMax > maxDensity) maxDensity = localMax; 1266 } 1267 1268 // 7. Dessin 1269 var step = yName.step(); 1270 var maxPlotHeight = step * 0.8; 1271 1272 svg.selectAll("areas") 1273 .data(allDensity) 1274 .enter() 1275 .append("path") 1276 .attr("transform", function(d){ 1277 return "translate(0," + (yName(d.key) + yName.bandwidth()) + ")"; 1278 }) 1279 .attr("fill", function(d){ 1280 // Utilisation de la couleur fixe associée à la région (clé) 1281 return myColor(d.key); 1282 }) 1283 .attr("opacity", 0.8) 1284 .attr("stroke", "#fff") 1285 .attr("stroke-width", 1.5) 1286 .datum(function(d){ return d.density; }) 1287 .attr("d", d3.area() 1288 .curve(d3.curveBasis) 1289 .x(function(d) { return x(d[0]); }) 1290 .y0(0) 1291 .y1(function(d) { 1292 return -(d[1] / maxDensity) * maxPlotHeight; 1293 }) 1294 ); 1295 1296 // Lignes de séparation 1297 svg.selectAll("baselines") 1298 .data(categories) 1299 .enter() 1300 .append("line") 1301 .attr("x1", 0) 1302 .attr("x2", width) 1303 .attr("y1", d => yName(d) + yName.bandwidth()) 1304 .attr("y2", d => yName(d) + yName.bandwidth()) 1305 .attr("stroke", "#eee") 1306 .attr("stroke-width", 1); 1307} 1308 1309} 1310async function beeswarm(){ 1311 d3.select("#chart_wrapper_2").html(""); 1312 1313const width = 900; 1314const height = 300; 1315const margin = { top: 10, right: 20, bottom: 35, left: 80 }; 1316 1317// Make sure the wrapper can anchor the tooltip 1318d3.select("#chart_wrapper_2").style("position", "relative"); 1319 1320const svg = d3.select("#chart_wrapper_2") 1321 .append("svg") 1322 .attr("viewBox", "0 0 900 300") 1323 1324 ; 1325 1326// X scale: one band per region 1327const regions = Array.from(new Set(dataset2.map(d => d.region))); 1328const x = d3.scaleBand() 1329 .domain(regions) 1330 .range([margin.left, width - margin.right]) 1331 .padding(0.1); 1332 1333// Y scale: national income value 1334const y = d3.scaleLinear() 1335 .domain([0, d3.max(dataset2, d => d.Value)]).nice() 1336 .range([height - margin.bottom, margin.top]); 1337 1338// Color scale: one color per region 1339const color = d3.scaleOrdinal() 1340 .domain(regions) 1341 .range(d3.schemeTableau10); 1342 1343// Axes 1344// svg.append("g") 1345// .attr("transform", `translate(0,${height - margin.bottom})`) 1346// .call(d3.axisBottom(x)) 1347// .selectAll("text") 1348// .style("text-anchor", "middle") 1349// .style("font-size", "14px") 1350// .style("font-family", "Quattrocento Sans"); 1351 1352svg.append("g") 1353 .attr("transform", `translate(${margin.left},0)`) 1354 .call(d3.axisLeft(y)) 1355 .selectAll("text") 1356 .style("font-size", "14px") 1357 .style("font-family", "Quattrocento Sans"); 1358 1359// Tooltip (simple div-based) 1360const tooltip = d3.select("#chart_wrapper_2") 1361 .append("div") 1362 .style("position", "absolute") 1363 .style("opacity", 0) 1364 .style("background", "rgba(0,0,0,0.8)") 1365 .style("color","#fff") 1366 .style("padding", "8px 12px") 1367 .style("border-radius", "4px") 1368 .style("font-family", "Quattrocento Sans") 1369 .style("font-size", "14px") 1370 .style("white-space", "nowrap") 1371 .style("z-index", 1000); 1372 1373// Set up simulation nodes 1374const nodes = dataset2.map(d => ({ 1375 ...d, 1376 targetX: x(d.region) + x.bandwidth() / 2, 1377 targetY: y(d.Value) 1378})); 1379 1380const radius = 4; 1381 1382const simulation = d3.forceSimulation(nodes) 1383 .force("x", d3.forceX(d => d.targetX).strength(0.9)) 1384 .force("y", d3.forceY(d => d.targetY).strength(0.9)) 1385 .force("collide", d3.forceCollide(radius + 1)) 1386 .stop(); 1387 1388for (let i = 0; i < 200; i++) simulation.tick(); 1389 1390// Draw dots 1391svg.selectAll("circle") 1392 .data(nodes) 1393 .join("circle") 1394 .attr("cx", d => d.x) 1395 .attr("cy", d => d.y) 1396 .attr("r", radius) 1397 .attr("fill", d => color(d.region)) 1398 .attr("opacity", 0.85) 1399 .attr("stroke", "white") 1400 .attr("stroke-width", 0.5) 1401 .on("mouseover", function (event, d) { 1402 d3.select(this).attr("stroke", "#333").attr("stroke-width", 1.5); 1403 tooltip 1404 .style("opacity", 1) 1405 .html(`<strong>${d.Country}</strong><br>${d3.format("$,.0f")(d.Value)}`); // d3.format("$,.0f") sets the format as dollars & no decimals 1406 }) 1407 .on("mousemove", function (event) { 1408 const [mx, my] = d3.pointer(event, this.ownerSVGElement.parentNode); 1409 tooltip 1410 .style("left", (mx + 12) + "px") 1411 .style("top", (my - 12) + "px"); 1412 }) 1413 .on("mouseout", function () { 1414 d3.select(this).attr("stroke", "white").attr("stroke-width", 0.5); 1415 tooltip.style("opacity", 0); 1416 }); 1417} 1418 1419// SECTION 3 1420 1421async function line_chart(){ 1422 d3.select("#chart_wrapper_3").html(""); 1423 // Append SVG Object to the Page 1424 // const svg = d3.select("#chart_wrapper_3") 1425 // .append("svg") 1426 // .attr("viewBox", "0 0 900 300") 1427 // 1428 // .style("background-color", "orange") 1429 // .append("g") 1430 // .attr("transform","translate(50,20)"); 1431const margin = {top: 20, right: 30, bottom: 40, left: 80}, 1432 width = 900 - margin.left - margin.right, 1433 height = 500 - margin.top - margin.bottom; 1434 1435// Tooltip 1436const tooltip = d3.select("#chart_wrapper_3") 1437 .append("div") 1438 .style("position", "absolute") 1439 .style("visibility", "hidden") 1440 .style("background-color", "rgba(0,0,0,0.8)") 1441 .style("color","#fff") 1442 .style("padding", "8px 12px") 1443 .style("border-radius", "4px") 1444 .style("font-family", "Quattrocento Sans") 1445 .style("font-size", "14px") 1446 .style("white-space", "nowrap") 1447 .style("z-index", 1000); 1448 1449const svg = d3.select("#chart_wrapper_3") 1450 .append("svg") 1451 .attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`) 1452 1453 1454 .append("g") 1455 .attr("transform", `translate(${margin.left},${margin.top})`); 1456 1457// Préparation des données 1458const data = dataset3.map(d => ({ 1459 Year: +d.Year, 1460 Region: d.Region, 1461 Value: +d.Value 1462})); 1463 1464const sumstat = d3.group(data, d => d.Region); 1465 1466// Axes 1467const x = d3.scaleLinear() 1468 .domain(d3.extent(data, d => d.Year)) 1469 .range([0, width]); 1470 1471svg.append("g") 1472 .attr("transform", `translate(0, ${height})`) 1473 .call(d3.axisBottom(x).ticks(5).tickFormat(d3.format("d")) ) 1474 .selectAll("text") 1475 .style("font-family", "Quattrocento Sans") 1476 .style("font-size", "14px"); 1477 1478const y = d3.scaleLinear() 1479 .domain([0, d3.max(data, d => d.Value)]) 1480 .range([height, 0]); 1481 1482svg.append("g") 1483 .call(d3.axisLeft(y)) 1484 .selectAll("text") 1485 .style("font-family", "Quattrocento Sans") 1486 .style("font-size", "14px"); 1487 1488// Couleurs 1489const color = d3.scaleOrdinal() 1490 .domain(sumstat.keys()) 1491 .range(['#A6D854','#FC8D62', '#FFD92F','#E78AC3','#66C2A5','#E5C494', '#B3B3B3']); 1492 1493// Données pour le tooltip 1494const dataMap = new Map(); 1495data.forEach(d => { 1496 if (!dataMap.has(d.Year)) dataMap.set(d.Year, new Map()); 1497 dataMap.get(d.Year).set(d.Region, d.Value); 1498}); 1499const years = Array.from(dataMap.keys()).sort(d3.ascending); 1500 1501// Draw the lines 1502const lines = svg.selectAll(".line") 1503 .data(sumstat) 1504 .join("path") 1505 .attr("fill", "none") 1506 .attr("stroke", d => color(d[0])) 1507 .attr("stroke-width", 2) 1508 .attr("opacity", 1) // Opacité par défaut 1509 .attr("d", d => d3.line() 1510 .x(pt => x(pt.Year)) 1511 .y(pt => y(pt.Value)) 1512 (d[1]) 1513 ) 1514 .on("mouseover", function(event, d) { 1515 const regionName = d[0]; // Nom du groupe de la ligne survolée 1516 1517 // 1. Mettre en surbrillance la ligne actuelle 1518 d3.select(this) 1519 .style("stroke-width", 4) 1520 .style("opacity", 1) 1521 .style("stroke", color(regionName)); // S'assurer qu'elle garde sa couleur 1522 1523 // 2. Griser toutes les AUTRES lignes 1524 lines.filter(function() { 1525 return this !== d3.select(this).node(); // Ou comparer les données si plus robuste 1526 }) 1527 // Méthode plus sûre pour cibler les autres : 1528 lines.filter((d_other) => d_other[0] !== regionName) 1529 .style("stroke", "#b8b7b7") 1530 .style("stroke-width", 1) 1531 ; 1532 1533 tooltip.style("visibility", "visible"); 1534 }) 1535 .on("mousemove", function(event, d) { 1536 const mousePos = d3.pointer(event, svg.node())[0]; 1537 const yearAtMouse = x.invert(mousePos); 1538 const closestYearIndex = d3.bisector(y => y).center(years, yearAtMouse);
1539 const yearKey = years[closestYearIndex]; 1540 1541 const regionName = d[0]; 1542 const value = dataMap.get(yearKey)?.get(regionName); 1543 1544 if (value !== undefined) { 1545 tooltip 1546 .html(`Year: ${yearKey}<br>${regionName}<br>${d3.format("$,.0f")(value)}`) 1547 .style("left", (event.pageX + 10) + "px") 1548 .style("top", (event.pageY - 10) + "px"); 1549 } 1550 }) 1551 .on("mouseout", function() { 1552 // Réinitialiser TOUTES les lignes à leur état normal 1553 lines 1554 .style("stroke", d => color(d[0])) // Retour à la couleur d'origine 1555 .style("stroke-width", 2) 1556 .style("opacity", 1); 1557 1558 tooltip.style("visibility", "hidden"); 1559 }); 1560 1561} 1562 1563line_chart(); 1564async function waffle_series(){ 1565 d3.select("#chart_wrapper_3").html(""); 1566 1567function drawWaffleTimeSeries(data, containerId) { 1568 1569 const years = Array.from(new Set(data.map(d => d.Year))).sort((a, b) => a - b); 1570 const regions = Array.from(new Set(data.map(d => d.Region))); 1571 1572 const color = d3.scaleOrdinal() 1573 .domain(regions) 1574 .range(['#A6D854','#FC8D62', '#FFD92F','#E78AC3','#66C2A5','#E5C494', '#B3B3B3']); 1575 1576 // --- standard margin convention: fixed outer size, derive inner drawable area --- 1577 const margin = { top: 30, right: 30, bottom: 30, left: 30 }; 1578 const width = 900 - margin.left - margin.right; 1579 const height = 500 - margin.top - margin.bottom; 1580 1581 d3.select("#" + containerId).select("svg").remove(); // clear on redraw 1582 1583 const svg = d3.select("#" + containerId) 1584 .append("svg") 1585 .attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`) 1586 1587 1588 .append("g") 1589 .attr("transform", `translate(${margin.left},${margin.top})`); 1590 1591 1592 // --- reserve space for the year label under each waffle, then size the 1593 // waffles (square cells) to whatever's left, so everything fits 1594 // inside the fixed 900x300 box regardless of years/regions count --- 1595 const yearLabelH = 24; // year number + avg-income subtext 1596 const waffleAreaH = height - yearLabelH; 1597 1598 const gridSize = 10; // 10 x 10 = 100 squares per waffle 1599 const waffleGap = 20; 1600 const numYears = years.length; 1601 1602 const cellFromHeight = waffleAreaH / gridSize; 1603 const cellFromWidth = (width - (numYears - 1) * waffleGap) / numYears / gridSize; 1604 const cell = Math.max(4, Math.min(cellFromHeight, cellFromWidth)); // avoid degenerate sizes 1605 const squarePad = Math.max(1, cell * 0.15); 1606 const squareSize = cell - squarePad; 1607 const waffleDim = gridSize * cell - squarePad; 1608 1609 // center the row of waffles within `width` 1610 const rowWidth = numYears * waffleDim + (numYears - 1) * waffleGap; 1611 const rowStartX = Math.max(0, (width - rowWidth) / 2); 1612 1613 // On calcule la hauteur totale occupée par les waffles 1614 const wafflesTotalHeight = waffleDim; 1615 1616 // L'espace vertical disponible est la hauteur totale (height) moins l'espace réservé aux labels en bas (yearLabelH) 1617 const availableVerticalSpace = height - yearLabelH; 1618 1619 // On centre les waffles dans cet espace disponible 1620 const waffleTopY = (availableVerticalSpace - wafflesTotalHeight) / 2; 1621 1622 // --- tooltip --- 1623 // Create #tooltip with d3 if it isn't already on the page, so this 1624 // function is self-contained and doesn't depend on a static HTML div. 1625 let tooltip = d3.select("body") 1626 .append("div") 1627 .style("position", "absolute") 1628 .style("opacity", 0) 1629 .style("background", "rgba(0,0,0,0.8)") 1630 .style("color","#fff") 1631 .style("padding", "8px 12px") 1632 .style("border-radius", "4px") 1633 .style("font-family", "Quattrocento Sans") 1634 .style("font-size", "14px") 1635 .style("white-space", "nowrap") 1636 .style("z-index", 1000); 1637 1638 1639 // --- one waffle per year --- 1640 const yearG = svg.selectAll("g.year") 1641 .data(years) 1642 .join("g") 1643 .attr("class", "year") 1644 .attr("transform", (d, i) => `translate(${rowStartX + i * (waffleDim + waffleGap)}, ${waffleTopY})`); 1645 1646 yearG.each(function (year) { 1647 const g = d3.select(this); 1648 const yearRows = data.filter(d => d.Year === year); 1649 const total = d3.sum(yearRows, d => d.Value); 1650 1651 // Largest-remainder method: allocate 100 squares proportionally, exact sum = 100 1652 const raw = regions.map(r => {
1653 const row = yearRows.find(d => d.Region === r); 1654 const value = row ? row.Value : 0; 1655 const exact = total > 0 ? (value / total) * 100 : 0; 1656 return { Region: r, value, exact, count: Math.floor(exact), remainder: exact - Math.floor(exact) }; 1657 }); 1658 let allocated = d3.sum(raw, d => d.count); 1659 let remaining = 100 - allocated; 1660 raw.sort((a, b) => b.remainder - a.remainder); 1661 for (let i = 0; i < remaining; i++) raw[i % raw.length].count += 1; 1662 1663 // Guard: with a wide value range (~1.2K to ~65K here), a small nonzero 1664 // region can round down to 0 squares and disappear from the waffle. 1665 // Give every nonzero region at least 1 square, borrowing from whichever 1666 // region currently has the most (so the total stays at 100). 1667 raw.forEach(d => { 1668 if (d.value > 0 && d.count === 0) { 1669 const donor = raw.reduce((a, b) => (b.count > a.count ? b : a)); 1670 donor.count -= 1; 1671 d.count += 1; 1672 } 1673 }); 1674 1675 // Build a flat 100-length array of Region assignments, grouped (not interleaved) 1676 const order = regions; // keep legend order for square placement 1677 const countByRegion = new Map(raw.map(d => [d.Region, d.count])); 1678 const valueByRegion = new Map(raw.map(d => [d.Region, d.value])); 1679 const squares = []; 1680 order.forEach(r => { 1681 for (let i = 0; i < countByRegion.get(r); i++) squares.push(r); 1682 }); 1683 1684 g.selectAll("rect.square") 1685 .data(squares) 1686 .join("rect") 1687 .attr("class", "square") 1688 .attr("x", (d, i) => (i % gridSize) * cell) 1689 .attr("y", (d, i) => Math.floor(i / gridSize) * cell) 1690 .attr("width", squareSize) 1691 .attr("height", squareSize) 1692 .attr("rx", 1) 1693 .attr("fill", d => color(d)) 1694 .on("mousemove", function (event, d) { 1695 const pct = ((valueByRegion.get(d) / total) * 100).toFixed(1); 1696 tooltip 1697 .style("opacity", 1) 1698 .style("left", (event.pageX + 12) + "px") 1699 .style("top", (event.pageY - 24) + "px") 1700 .html(`<strong>${d}</strong><br>${d3.format("$,.0f")(valueByRegion.get(d).toLocaleString())} (${pct}%)`); 1701 }) 1702 .on("mouseleave", () => tooltip.style("opacity", 0)); 1703 1704 g.append("text") 1705 .attr("class", "year-label") 1706 .attr("x", waffleDim / 2) 1707 .attr("y", waffleDim + 20) 1708 .attr("text-anchor", "middle") 1709 .style("font-size", "14px") 1710 .style("font-family","Quattrocento Sans") 1711 .text(year); 1712 1713 // Avg income for the year, for absolute context alongside the % shares above 1714 g.append("text") 1715 .attr("class", "year-sub") 1716 .attr("x", waffleDim / 2) 1717 .attr("y", waffleDim + 40) 1718 .attr("text-anchor", "middle") 1719 .style("font-family", "Quattrocento Sans") 1720 .style("font-size", "12px") 1721 .style("fill", "#77746c") 1722 .text(`avg $${Math.round(total / regions.length).toLocaleString()}`); 1723 }); 1724} 1725 1726const yearsToShow = ["1800", "1900", "2000", "2025"]; 1727const dataset3Filtered = dataset3.filter(d => yearsToShow.includes(d.Year)); 1728drawWaffleTimeSeries(dataset3Filtered, "chart_wrapper_3"); 1729} 1730async function stacked_area(){ 1731d3.select("#chart_wrapper_3").html(""); 1732 1733// Make the wrapper positioned so we can absolutely-position a tooltip inside it 1734d3.select("#chart_wrapper_3").style("position", "relative"); 1735 1736// Append SVG Object to the Page 1737const svg = d3.select("#chart_wrapper_3") 1738 .append("svg") 1739 .attr("viewBox", "0 0 900 500") 1740 1741 ; 1742 1743// Specify the chartâs dimensions. 1744const width = 900; 1745const height = 500; 1746const marginTop = 30; 1747const marginRight = 40; 1748const marginBottom = 40; 1749const marginLeft = 80; 1750 1751// Index the raw data by Year -> Region for quick lookup on hover. 1752const dataByYear = d3.index(dataset3, d => d.Year, d => d.Region); 1753 1754// Determine the series that need to be stacked. 1755const series = d3.stack() 1756 .keys(d3.union(dataset3.map(d => d.Region))) 1757 .value(([, D], key) => D.get(key).Value) 1758 (dataByYear); 1759 1760// Prepare the scales for positional and color encodings. 1761const x = d3.scaleLinear() 1762 .domain(d3.extent(dataset3, d => d.Year)) 1763 .range([marginLeft, width - marginRight]); 1764 1765const y = d3.scaleLinear() 1766 .domain([0, d3.max(series, d => d3.max(d, d => d[1]))]) 1767 .rangeRound([height - marginBottom, marginTop]); 1768 1769const color = d3.scaleOrdinal() 1770 .domain(series.map(d => d.key)) 1771 .range(['#A6D854','#FC8D62', '#FFD92F','#E78AC3','#66C2A5','#E5C494', '#B3B3B3']); 1772 1773 1774 1775// Construct an area shape. 1776const area = d3.area() 1777 .x(d => x(d.data[0])) 1778 .y0(d => y(d[0])) 1779 .y1(d => y(d[1])); 1780 1781// Add the y-axis, remove the domain line, add grid lines. 1782svg.append("g") 1783 .attr("transform", `translate(${marginLeft},0)`) 1784 .call(d3.axisLeft(y).ticks(height / 80)) 1785 .call(g => g.select(".domain").remove()) 1786 .call(g => g.selectAll(".tick line").clone() 1787 .attr("x2", width - marginLeft - marginRight) 1788 .attr("stroke-opacity", 0.1)) 1789 .selectAll("text") 1790 .style("font-size", "14px") 1791 .style("font-family", "Quattrocento Sans"); 1792 1793// Append a path for each series. 1794const paths = svg.append("g") 1795 .selectAll("path") 1796 .data(series) 1797 .join("path") 1798 .attr("fill", d => color(d.key)) 1799 .attr("d", area); 1800 1801paths.append("title") 1802 .text(d => d.key); 1803 1804// Append the horizontal axis atop the area, with plain integer year labels. 1805svg.append("g") 1806 .attr("transform", `translate(0,${height - marginBottom})`)
1807 .call(d3.axisBottom(x).tickSizeOuter(0).tickFormat(d3.format("d"))) 1808 .selectAll("text") 1809 .style("text-anchor", "middle") 1810 .style("font-size", "14px") 1811 .style("font-family", "Quattrocento Sans"); 1812 1813// --- Hover tracking --- 1814 1815// Sorted list of unique years, used to snap the cursor to the nearest data point. 1816const years = Array.from(new Set(dataset3.map(d => d.Year))).sort((a, b) => a - b); 1817const bisectYear = d3.bisector(d => d).center; 1818 1819// Vertical guide line (hidden until hovered). 1820const hoverLine = svg.append("line") 1821 .attr("y1", marginTop) 1822 .attr("y2", height - marginBottom) 1823 .attr("stroke", "black") 1824 .attr("stroke-width", 1) 1825 .attr("stroke-dasharray", "3,3") 1826 .style("opacity", 0); 1827 1828// One dot per series, marking its value at the hovered year. 1829const hoverDots = svg.append("g") 1830 .selectAll("circle") 1831 .data(series) 1832 .join("circle") 1833 .attr("r", 4) 1834 .attr("fill", d => color(d.key)) 1835 .attr("stroke", "white") 1836 .attr("stroke-width", 1) 1837 .style("opacity", 0); 1838 1839// HTML tooltip, positioned absolutely over the chart wrapper. 1840const tooltip = d3.select("#chart_wrapper_3") 1841 .append("div") 1842 .style("position", "absolute") 1843 .style("pointer-events", "none") 1844 .style("background", "rgba(0,0,0,0.8)") 1845 .style("color","#fff") 1846 .style("padding", "8px 12px") 1847 .style("border-radius", "4px") 1848 .style("font-family", "Quattrocento Sans") 1849 .style("font-size", "14px") 1850 .style("white-space", "nowrap") 1851 .style("z-index", 1000) 1852 .style("opacity", 0); 1853 1854// Transparent overlay to capture mouse events across the whole plot area. 1855svg.append("rect") 1856 .attr("x", marginLeft) 1857 .attr("y", marginTop) 1858 .attr("width", width - marginLeft - marginRight) 1859 .attr("height", height - marginTop - marginBottom) 1860 .attr("fill", "none") 1861 .attr("pointer-events", "all") 1862 .on("mousemove", function (event) { 1863 const [mx] = d3.pointer(event, this); 1864 const yearAtCursor = x.invert(mx); 1865 const nearestYear = years[bisectYear(years, yearAtCursor)]; 1866 1867 const regionMap = dataByYear.get(nearestYear); 1868 if (!regionMap) return; 1869 1870 // Update guide line position. 1871 hoverLine 1872 .attr("x1", x(nearestYear)) 1873 .attr("x2", x(nearestYear)) 1874 .style("opacity", 1); 1875 1876 // Update each series' dot to sit at its stacked value for this year. 1877 hoverDots 1878 .attr("cx", x(nearestYear)) 1879 .attr("cy", d => { 1880 const point = d.find(p => p.data[0] === nearestYear); 1881 return point ? y(point[1]) : null; 1882 }) 1883 .style("opacity", d => d.find(p => p.data[0] === nearestYear) ? 1 : 0); 1884 1885 // Build tooltip content: one row per region, in series order. 1886 const rows = series.map(s => { 1887 const entry = regionMap.get(s.key); 1888 const value = entry ? entry.Value : 0; 1889 return `<div style="display:flex;align-items:center;gap:6px;"> 1890 <span style="width:10px;height:10px;background:${color(s.key)};display:inline-block;"></span> 1891 <span>${s.key}: ${d3.format("$,.0f")(value.toLocaleString())}</span> 1892 </div>`; 1893 }).join(""); 1894 1895 tooltip 1896 .html(`<strong>${nearestYear}</strong>${rows}`) 1897 .style("opacity", 1) 1898 .style("left", `${(event.offsetX ?? mx) + 15}px`) 1899 .style("top", `${(event.offsetY ?? y(0)) - 40}px`); 1900 }) 1901 .on("mouseleave", () => { 1902 hoverLine.style("opacity", 0); 1903 hoverDots.style("opacity", 0); 1904 tooltip.style("opacity", 0); 1905 }); 1906} 1907 1908async function streamgraph(){ 1909 d3.select("#chart_wrapper_3").html(""); 1910 1911 const margin = {top: 170, right: 160, bottom: 40, left: 70}; 1912 const width = 800; 1913 const height = 500; 1914 const innerWidth = width - margin.left - margin.right; 1915 const innerHeight = height - margin.top - margin.bottom; 1916 let innerChart; 1917 1918 // --- Build regionsInfo dynamically from the data instead of hardcoding, --- 1919 // --- since region names/counts will vary by dataset. --- 1920 const regionColors = ['#A6D854','#FC8D62', '#FFD92F','#E78AC3','#66C2A5','#E5C494', '#B3B3B3'] 1921 1922 1923 1924 1925 const buildRegionsInfo = (data) => { 1926 const regionNames = Array.from(new Set(data.map(d => d.Region.trim()))); 1927 return regionNames.map((name, i) => ({ 1928 id: name 1929 .toLowerCase() 1930 .replace(/[^a-z0-9]+/g, "-") // replace anything not a-z/0-9 with a hyphen 1931 .replace(/^-+|-+$/g, ""), // trim leading/trailing hyphens 1932 label: name, 1933 color: regionColors[i % regionColors.length] 1934 })); 1935 }; 1936 1937 1938 // --- Pivot long format (Region, Year, Value) into wide format --- 1939 // --- (one row per Year, one property per Region), which is what --- 1940 // --- d3.stack() and the rest of this code expect. --- 1941 const pivotToWide = (data, regionsInfo) => { 1942 const byYear = d3.group(data, d => +d.Year); 1943 return Array.from(byYear, ([year, rows]) => { 1944 const entry = { year: +year }; 1945 regionsInfo.forEach(region => {
1946 const row = rows.find(r => r.Region.trim() === region.label); // match on label, not id 1947 entry[region.id] = row ? +row.Value : 0; // but still store under the sanitized id 1948 }); 1949 return entry; 1950 }).sort((a, b) => a.year - b.year); 1951 }; 1952 1953 let formatsInfo; // keeping the same variable name used elsewhere in the code below 1954 1955 // Initialize the scales here 1956 const xScale = d3.scaleLinear(); 1957 const colorScale = d3.scaleOrdinal(); 1958 1959 const defineScales = (data) => { 1960 1961 xScale 1962 .domain([d3.min(data, d => d.year), d3.max(data, d => d.year)]) 1963 .range([0, innerWidth]); 1964 1965 colorScale 1966 .domain(formatsInfo.map(f => f.id)) 1967 .range(['#A6D854','#FC8D62', '#FFD92F','#E78AC3','#66C2A5','#E5C494', '#B3B3B3']); 1968 1969 }; 1970 1971 const createTooltip = (data) => { 1972 1973 const tooltipWidth = 100; 1974 const tooltipHeight = 190; 1975 const textColor = "#494e4f"; 1976 const textLineHeight = 22; 1977 1978 const tooltip = innerChart 1979 .append("g") 1980 .attr("class", "tooltip"); 1981 1982 tooltip 1983 .append("line") 1984 .attr("x1", 0) 1985 .attr("x2", 0) 1986 .attr("y1", -5) 1987 .attr("y2", innerHeight) 1988 .attr("stroke", textColor) 1989 .attr("stroke-width", 2) 1990 .attr("stroke-dasharray", "6 4"); 1991 1992 const firstYear = d3.min(data, d => d.year); 1993 const tooltipYear = tooltip 1994 .append("text") 1995 .attr("class", "tooltip-year") 1996 .attr("x", 0) 1997 .attr("y", innerHeight + 25) 1998 .style("font-size", "16px") 1999 .style("font-weight", 700) 2000 .style("font-family", "Quattrocento Sans") 2001 .style("fill", textColor) 2002 .attr("text-anchor", "middle") 2003 .text(firstYear); 2004 2005 const tooltipContent = tooltip 2006 .append("g") 2007 .attr("transform", `translate(${-1 * tooltipWidth/2}, ${-1 * margin.top + 30})`); 2008 const tooltipText = tooltipContent 2009 .append("text") 2010 .attr("class", "tooltip-content") 2011 .style("font-family", "Quattrocento Sans") 2012 .style("font-size", "13px") 2013 .style("font-weight", 500) 2014 .style("fill", textColor); 2015 2016 const dataFirstYear = data.find(item => item.year === firstYear); 2017 formatsInfo.forEach((format, i) => { 2018 tooltipText 2019 .append("tspan") 2020 .attr("class", `sales-${format.id}`) 2021 .attr("x", 0) 2022 .attr("y", i * textLineHeight) 2023 .text(`${format.label}: ${d3.format("$,.0f")(dataFirstYear[format.id])}`); 2024 2025 tooltipContent 2026 .append("circle") 2027 .attr("cx", -10) 2028 .attr("cy", i * textLineHeight - 5) 2029 .attr("r", 6) 2030 .attr("fill", format.color); 2031 }); 2032 2033 }; 2034 2035 const handleMouseEvents = (data) => { 2036 2037 innerChart.select(".hover-overlay") 2038 .on("mousemove", e => { 2039 2040 const xPosition = d3.pointer(e)[0]; 2041 innerChart.select(".tooltip") 2042 .attr("transform", `translate(${xPosition}, 0)`); 2043 2044 const year = Math.round(xScale.invert(xPosition)); 2045 innerChart.select(".tooltip-year").text(year); 2046 2047 const yearData = data.find(item => item.year === year); 2048 if (!yearData) return; 2049 2050 formatsInfo.forEach(format => { 2051 innerChart.select(`.sales-${format.id}`) 2052 .text(`${format.label}: ${d3.format("$,.0f")(yearData[format.id])}`); 2053 }); 2054 2055 }); 2056 2057 }; 2058 2059 const drawStreamGraph = (data) => { 2060 2061 const svg = d3.select("#chart_wrapper_3") 2062 .append("svg") 2063 .attr("viewBox", [0, 0, width, height]); 2064 2065 innerChart = svg 2066 .append("g") 2067 .attr("transform", `translate(${margin.left}, ${margin.top})`); 2068 2069 // --- Tick values: your data spans 1800+ rather than 1973â2020, --- 2070 // --- so generate ticks dynamically instead of hardcoding the range. --- 2071 const [minYear, maxYear] = xScale.domain(); 2072 const tickStep = 20; // adjust spacing to taste given the wider year range 2073 const bottomAxis = d3.axisBottom(xScale) 2074 .tickValues(d3.range(Math.ceil(minYear / tickStep) * tickStep, maxYear + 1, tickStep)) 2075 .tickSizeOuter(0) 2076 .tickSize(innerHeight * -1) 2077 .tickFormat(d3.format("d")); // plain integer years, no thousands separator 2078 2079 // innerChart 2080 // .append("g") 2081 // .attr("class", "x-axis-streamgraph") 2082 // .attr("transform", `translate(0, ${innerHeight})`) 2083 // .call(bottomAxis); 2084 2085 const stackGenerator = d3.stack() 2086 .keys(formatsInfo.map(f => f.id)) 2087 .order(d3.stackOrderAscending) 2088 .offset(d3.stackOffsetSilhouette); 2089 2090 const annotatedData = stackGenerator(data); 2091 2092 const minLowerBoundaries = []; 2093 const maxUpperBoundaries = []; 2094 annotatedData.forEach(series => { 2095 minLowerBoundaries.push(d3.min(series, d => d[0])); 2096 maxUpperBoundaries.push(d3.max(series, d => d[1])); 2097 }); 2098 const minDomain = d3.min(minLowerBoundaries); 2099 const maxDomain = d3.max(maxUpperBoundaries); 2100 2101 const yScale = d3.scaleLinear() 2102 .domain([minDomain, maxDomain]) 2103 .range([innerHeight, 0]) 2104 .nice(); 2105 2106 const areaGenerator = d3.area() 2107 .x(d => xScale(d.data.year)) 2108 .y0(d => yScale(d[0])) 2109 .y1(d => yScale(d[1])) 2110 .curve(d3.curveCatmullRom); 2111 2112 innerChart 2113 .append("g") 2114 .attr("class", "areas-container") 2115 .selectAll("path") 2116 .data(annotatedData) 2117 .join("path") 2118 .attr("class", d => `area area-${d.key}`) 2119 .attr("d", areaGenerator) 2120 .attr("fill", d => colorScale(d.key)); 2121 2122 innerChart 2123 .append("rect") 2124 .attr("class", "hover-overlay") 2125 .attr("width", innerWidth) 2126 .attr("height", innerHeight) 2127 .attr("fill", "none") 2128 .attr("pointer-events", "all"); 2129 2130 }; 2131 2132 // --- Build regionsInfo and pivot the raw long-format data before anything else runs --- 2133 formatsInfo = buildRegionsInfo(dataset3); // dataset3 = your raw Region/Year/Value rows
2134 const data_streamgraph = pivotToWide(dataset3, formatsInfo); 2135 2136 defineScales(data_streamgraph); 2137 drawStreamGraph(data_streamgraph); 2138 createTooltip(data_streamgraph); 2139 handleMouseEvents(data_streamgraph); 2140} 2141 2142 2143////// BUTTONS 2144 2145async function buttons_wrapper1(){ 2146 const barchart_button = d3 2147 .select("#buttons_wrapper_1") 2148 .append("button") 2149 .text("Bar chart") 2150 .on("click", () => { 2151 barchart(); 2152 }) 2153 const lollipop_button = d3 2154 .select("#buttons_wrapper_1") 2155 .append("button") 2156 .text("Lollipop chart") 2157 .on("click", () => { 2158 lollipop(); 2159 }) 2160 const proportional_shapes_button = d3 2161 .select("#buttons_wrapper_1") 2162 .append("button") 2163 .text("Proportional bubbles") 2164 .on("click", () => { 2165 proportional_bubbles(); 2166 }) 2167 // const treemap_button = d3 2168 // .select("#buttons_wrapper_1") 2169 // .append("button") 2170 // .text("Treemap") 2171 // .on("click", () => { 2172 // treemap(); 2173 // }) 2174 const donut_button = d3 2175 .select("#buttons_wrapper_1") 2176 .append("button") 2177 .text("Donut") 2178 .on("click", () => { 2179 donut(); 2180 }) 2181 // const waffles_button = d3 2182 // .select("#buttons_wrapper_1") 2183 // .append("button") 2184 // .text("Waffle chart") 2185 // .on("click", () => { 2186 // waffle(); 2187 // }) 2188 // const pictograms_button = d3 2189 // .select("#buttons_wrapper_1") 2190 // .append("button") 2191 // .text("Pictograms") 2192 // // .on("click", () => { 2193 // // lollipop(); 2194 // // }) 2195 const circular_bars_button = d3 2196 .select("#buttons_wrapper_1") 2197 .append("button") 2198 .text("Circular bars") 2199 .on("click", () => { 2200 circular_bars(); 2201 }) 2202 // const radial_bars_button = d3 2203 // .select("#buttons_wrapper_1") 2204 // .append("button") 2205 // .text("Radial bars") 2206 // .on("click", () => { 2207 // radial_bars(); 2208 // }) 2209} 2210async function buttons_wrapper2(){ 2211 const violin_button = d3 2212 .select("#buttons_wrapper_2") 2213 .append("button") 2214 .text("Violin") 2215 .on("click", () => { 2216 violin(); 2217 }) 2218 const jitter_button = d3 2219 .select("#buttons_wrapper_2") 2220 .append("button") 2221 .text("Jitter") 2222 .on("click", () => { 2223 jitter(); 2224 }) 2225 const ridgeline_button = d3 2226 .select("#buttons_wrapper_2") 2227 .append("button") 2228 .text("Ridgeline") 2229 .on("click", () => { 2230 ridgeline(); 2231 }) 2232 const beeswarm_button = d3 2233 .select("#buttons_wrapper_2") 2234 .append("button") 2235 .text("Beeswarm") 2236 .on("click", () => { 2237 beeswarm(); 2238 }) 2239} 2240async function buttons_wrapper3(){ 2241 const line_button = d3 2242 .select("#buttons_wrapper_3") 2243 .append("button") 2244 .text("Line chart") 2245 .on("click", () => { 2246 line_chart(); 2247 }) 2248 const waffle_series_button = d3 2249 .select("#buttons_wrapper_3") 2250 .append("button") 2251 .text("Waffle") 2252 .on("click", () => { 2253 waffle_series(); 2254 }) 2255 const stacked_area_button = d3 2256 .select("#buttons_wrapper_3") 2257 .append("button") 2258 .text("Stacked area chart") 2259 .on("click", () => { 2260 stacked_area(); 2261 }) 2262 const streamgraph_button = d3 2263 .select("#buttons_wrapper_3") 2264 .append("button") 2265 .text("Streamgraph") 2266 .on("click", () => { 2267 streamgraph(); 2268 }) 2269} 2270buttons_wrapper1(); 2271buttons_wrapper2(); 2272buttons_wrapper3();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.