PageSourceSearch

https://alraven3.github.io/d3-chart-gallery/src/main.js

js alraven3.github.io collected 2026-10-03 08:36:38 UTC 75,906 bytes, 2,272 lines download raw bytes

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.