From 27a2e997062e57255c3312dec7da4580566e7958 Mon Sep 17 00:00:00 2001 From: medina5 Date: Sat, 18 Oct 2025 11:12:09 +0200 Subject: [PATCH] Rapport individus --- individus.html | 6 +- src/individual.ts | 27 +++- src/individus.ts | 314 ++++++++++++++++++++++++++++++---------------- 3 files changed, 233 insertions(+), 114 deletions(-) diff --git a/individus.html b/individus.html index ba0c6c4..df36a3b 100644 --- a/individus.html +++ b/individus.html @@ -28,11 +28,11 @@
-
+
-
-
+
+
diff --git a/src/individual.ts b/src/individual.ts index d4acb0b..0be8953 100644 --- a/src/individual.ts +++ b/src/individual.ts @@ -76,9 +76,11 @@ function randomSex(age:number) { return Math.random() < c ? "F" : "M"; } -function randomSport(age:number) { +function randomSport(sexe: "M" | "F", age:number) { // Moyenne de sport qui diminue avec l’âge - const meanSport = Math.min(Math.max(0.65 - 0.006 * (age - 18), 0.05), 0.9); + let meanSport = Math.min(Math.max(0.65 - 0.0065 * (age - 18), 0.05), 0.9); + + if (sexe === "F") meanSport -= 0.15; // Paramètres de la distribution bêta const a = Math.max(meanSport * 6, 0.5); @@ -130,9 +132,18 @@ function randomWealth(education:number) { return wealth; } -function randomLecture(education:number, age:number) { +function randomLecture(sexe: "M" | "F", education:number, age:number) { + // Base : 15% + effet de l'éducation + effet de l'âge let meanRead = 0.15 + 0.18 * education + 0.002 * (age - 18); - meanRead = Math.min(Math.max(meanRead, 0.02), 0.98); // équivalent de np.clip + + // Femmes plus lectrices : ajout d’un bonus + if (sexe === "F") meanRead += 0.16; + + // Bonus sénior : lecture plus fréquente chez les séniors + if (age > 60) meanRead += 0.09; + + // Clipping entre 0.02 et 0.98 + meanRead = Math.min(Math.max(meanRead, 0.02), 0.98); // Paramètres de la distribution bêta const a = Math.max(meanRead * 7, 0.5); @@ -144,6 +155,10 @@ function randomLecture(education:number, age:number) { return reading; } +/** + * Musique pratique indépendante de l'âge + * @returns valeur normalisée (0-1) + */ function randomMusique() { return jStat.beta.sample(2, 2); } @@ -158,8 +173,8 @@ export function generate(n: number) { const etudes = randomEducation(age); const richesse = randomWealth(etudes); - const sport = randomSport(age); - const lecture = randomLecture(etudes, age); + const sport = randomSport(sexe, age); + const lecture = randomLecture(sexe, etudes, age); const musique = randomMusique(); individus.push({ id: i, prenom, sexe, age, etudes, richesse, sport, lecture, musique }); diff --git a/src/individus.ts b/src/individus.ts index 99e65a3..ff4e432 100644 --- a/src/individus.ts +++ b/src/individus.ts @@ -2,12 +2,15 @@ import './style.css'; import { generate } from "./individual"; import Chart from "chart.js/auto"; import ChartDataLabels from "chartjs-plugin-datalabels"; -import { jStat } from "jstat"; import { MatrixController, MatrixElement } from 'chartjs-chart-matrix'; +import { jStat } from "jstat"; + const individus = generate(1000); +const individualMale = individus.filter(i => i.sexe === "M"); +const individualFemale = individus.filter(i => i.sexe === "F"); // Fonction utilitaire pour afficher les pourcentages -const percentage = (value: number, total: number) => ((value / total) * 100).toFixed(1) + "%"; +const percentage = (value: number, total: number) => ((value / total) * 100).toFixed(0) + "%"; function formatStats(ages: number[]) { return { @@ -20,10 +23,16 @@ function formatStats(ages: number[]) { }; } +function histogramData(ages: number[]) { + return ageClasses.map((a, idx) => + ages.filter(age => age >= a && age < (ageClasses[idx + 1] ?? 80)).length + ); +} + const Ages:number[] = individus.map(i => i.age); // Séparer les âges par sexe -const AgesH = individus.filter(i => i.sexe === "M").map(i => i.age); -const AgesF = individus.filter(i => i.sexe === "F").map(i => i.age); +const AgesH = individualMale.map(i => i.age); +const AgesF = individualFemale.map(i => i.age); const stats = formatStats(Ages); for (const [key, value] of Object.entries(stats)) { @@ -33,11 +42,6 @@ for (const [key, value] of Object.entries(stats)) { // Construire des classes d'âge const ageClasses = Array.from({ length: 20 }, (_, i) => i * 3 + 18); -function histogramData(ages: number[]) { - return ageClasses.map((a, idx) => - ages.filter(age => age >= a && age < (ageClasses[idx + 1] ?? 80)).length - ); -} Chart.register(ChartDataLabels,MatrixController, MatrixElement); @@ -46,6 +50,59 @@ Chart.defaults.set('plugins.datalabels', { font: { weight: "bold" } }); +/** + * Doughnut Sexe + */ +const nbMales = individualMale.length; +const nbFemales = individus.length - nbMales; + +new Chart(document.getElementById("genreChart") as HTMLCanvasElement, { + type: "doughnut", + data: { + labels: ["Hommes", "Femmes"], + datasets: [{ + data: [nbMales, nbFemales], + backgroundColor: ["#4A90E2", "#FF69B4"], + }], + }, + options: { + plugins: { legend: { position: "bottom" }, + datalabels: { + formatter: (value, context) => { + const total = context.chart.data.datasets[0].data.reduce((a: number, b: number) => a + b, 0); + return percentage(value, total); + } + } + }, + }, +}); + +/** + * Doughnut Classes d'âges + */ +const classes = { + Jeune: individus.filter(i => i.age <= 30), + Adulte: individus.filter(i => i.age > 30 && i.age <= 60), + Senior: individus.filter(i => i.age > 60) +}; + +new Chart(document.getElementById("classeChart") as HTMLCanvasElement, { + type: "doughnut", + data: { + labels: ["Jeunes (≤30)", "Adultes (31–60)", "Seniors (>60)"], + datasets: [{ + data: [classes.Jeune.length, classes.Adulte.length, classes.Senior.length], + backgroundColor: ["#81C784", "#FFD54F", "#E57373"], + }], + }, + options: { + plugins: { legend: { position: "bottom" } }, + }, +}); + +/** + * Histogramme des ages + */ new Chart(document.getElementById("ageChart") as HTMLCanvasElement, { type: "bar", data: { @@ -64,7 +121,7 @@ new Chart(document.getElementById("ageChart") as HTMLCanvasElement, { ], }, options: { - responsive: true, + responsive: false, plugins: { title: { display: true, text: "Distribution des âges par sexe" }, datalabels: false @@ -76,41 +133,21 @@ new Chart(document.getElementById("ageChart") as HTMLCanvasElement, { }, }); -const hommes = individus.filter(i => i.sexe === "M").length; -const femmes = individus.length - hommes; -new Chart(document.getElementById("genreChart") as HTMLCanvasElement, { + +/** + * Éducation + */ +new Chart(document.getElementById("educationChart") as HTMLCanvasElement, { type: "doughnut", data: { - labels: ["Hommes", "Femmes"], + labels: ["Bac", "+2", "+3", "+5"], datasets: [{ - data: [hommes, femmes], - backgroundColor: ["#4A90E2", "#FF69B4"], - }], - }, - options: { - plugins: { legend: { position: "bottom" }, - datalabels: { - formatter: (value, context) => { - const total = context.chart.data.datasets[0].data.reduce((a: number, b: number) => a + b, 0); - return percentage(value, total); - } - } - }, - }, -}); - -const jeunes = individus.filter(i => i.age <= 30).length; -const adultes = individus.filter(i => i.age > 30 && i.age <= 60).length; -const seniors = individus.filter(i => i.age > 60).length; - -new Chart(document.getElementById("classeChart") as HTMLCanvasElement, { - type: "doughnut", - data: { - labels: ["Jeunes (≤30)", "Adultes (31–60)", "Seniors (>60)"], - datasets: [{ - data: [jeunes, adultes, seniors], - backgroundColor: ["#81C784", "#FFD54F", "#E57373"], + data: [ individus.filter(i => i.etudes == 0).length, + individus.filter(i => i.etudes == 1).length, + individus.filter(i => i.etudes == 2).length, + individus.filter(i => i.etudes == 3).length,], + backgroundColor: ["#55e0d9ff", "#81C784", "#FFD54F", "#E57373"], }], }, options: { @@ -118,6 +155,9 @@ new Chart(document.getElementById("classeChart") as HTMLCanvasElement, { }, }); +/** + * Richesse + */ new Chart(document.getElementById("wealthChart") as HTMLCanvasElement, { type: "doughnut", data: { @@ -140,92 +180,156 @@ new Chart(document.getElementById("wealthChart") as HTMLCanvasElement, { }, }); -new Chart(document.getElementById("educationChart") as HTMLCanvasElement, { - type: "doughnut", - data: { - labels: ["Bac", "+2", "+3", "+5"], - datasets: [{ - data: [ individus.filter(i => i.etudes == 0).length, - individus.filter(i => i.etudes == 1).length, - individus.filter(i => i.etudes == 2).length, - individus.filter(i => i.etudes == 3).length,], - backgroundColor: ["#81C784", "#FFD54F", "#E57373"], - }], - }, - options: { - plugins: { legend: { position: "bottom" } }, - }, +/** + * HeatMap + */ +const activites = ["Lecture", "Musique", "Sport"]; +const ages = Array.from({ length: 74 - 18 + 1 }, (_, i) => i + 18); + +// Labels Y : chaque activité doublée pour H/F +const yLabels = activites.flatMap(a => [`${a} H`, `${a} F`]); + +// Génération des données +const dataHeatmap = ages.flatMap((age, x) => { + const window = individus.filter(i => i.age >= age - 1 && i.age <= age + 1); // moyenne glissante + + // Pour chaque activité + return activites.flatMap((activite, y) => { + // map pour extraire les valeurs + // const valeurs = window.map(i => i[activite as keyof typeof i]); + + const hommes = window.filter(i => i.sexe === 'M'); + const femmes = window.filter(i => i.sexe === 'F'); + + // reduce pour la moyenne + const moyenneH = hommes.length > 0 + ? hommes.map(i => i[activite.toLowerCase() as keyof typeof i]) + .reduce((sum, val) => sum + val, 0) / hommes.length + : 0; + + const moyenneF = femmes.length > 0 + ? femmes.map(i => i[activite.toLowerCase() as keyof typeof i]) + .reduce((sum, val) => sum + val, 0) / femmes.length + : 0; + + return [ + { x, y: y * 2, v: moyenneH }, + { x, y: y * 2 + 1, v: moyenneF } + ]; + }); }); -new Chart(document.getElementById('heatmapChart') as HTMLCanvasElement, { - type: 'matrix', + +new Chart(document.getElementById("heatmapChart") as HTMLCanvasElement, { + type: "matrix", data: { - labels: individus.map((_, i) => `Individu ${i + 1}`), - datasets: [{ - label: 'Activités', - data: individus.map((individu, i) => [ - { x: 0, y: i, v: individu.sport }, - { x: 1, y: i, v: individu.musique }, - { x: 2, y: i, v: individu.lecture } - ]).flat(), - backgroundColor: ({ v }) => { - const r = Math.floor(255 - v * 100); - const g = Math.floor(255 - v * 50); - const b = Math.floor(255); - return `rgb(${r}, ${g}, ${b})`; - }, - borderWidth: 1, - borderColor: 'rgba(255, 255, 255, 0.6)', - width: ({ chart }) => (chart.chartArea?.width ?? 0) / 3 - 1, - height: ({ chart }) => (chart.chartArea?.height ?? 0) / individus.length - 1 - }] + datasets: [ + { + label: 'Activités par age et par sexe', + data: dataHeatmap, + borderWidth: 0, + backgroundColor: ({ raw }: { raw: any }) => { + const mean = 0.5; // ou calculé dynamiquement + const contrastFactor = 2; // augmente les écarts + let v = (raw.v - mean) * contrastFactor + mean; + v = Math.min(Math.max(v, 0), 1); // clip sur [0,1] + const light = 90; // OKLCH clair + const dark = 40; // OKLCH foncé + const c = Math.floor(light - (light - dark) * v); + return `oklch(${c}% 0.12 145)`; // vert + }, + width: ({ chart }: { chart: any }) => + (chart.chartArea?.width ?? 0) / ages.length, + height: ({ chart }: { chart: any }) => + (chart.chartArea?.height ?? 0) / yLabels.length - 1 + } + ] }, options: { responsive: true, - scales: { - x: { - type: 'category', - labels: ['Sport', 'Musique', 'Lecture'], - offset: true - }, - y: { - type: 'category', - labels: individus.map((_, i) => `Individu ${i + 1}`), - offset: true - } - }, plugins: { legend: { display: false }, + datalabels: false, tooltip: { callbacks: { - label: ({ raw }) => `Valeur : ${raw.v.toFixed(2)}` + title: (items) => { + const raw = (items[0] as any).raw; + return `${yLabels[raw.y]} - ${ages[raw.x]} ans`; + }, + label: ({ raw }: { raw: any }) => + ` ${(raw.v * 100).toFixed(0)}%` } } + }, + scales: { + x: { + type: 'linear', + min: -0.5, + max: ages.length - 0.5, + ticks: { + autoSkip: false, + callback: (_, i) => i % 5 === 0 ? ages[i] ?? '' : '', + stepSize: 1 + }, + offset: false, + grid: { display: false } + }, + y: { + type: 'linear', + min: -0.5, + max: yLabels.length - 0.5, + position: 'left', + ticks: { + callback: (_, i) => yLabels[i] ?? '', + crossAlign: 'near', + align: 'end', + stepSize: 1 + }, + offset: false, + grid: { display: false }, + reverse: false + } } } }); +const datasets = Object.entries(classes).map(([label, group], idx) => { + + const colors = [ + "rgba(75,192,192,0.2)", + "rgba(255,99,132,0.2)", + "rgba(255,206,86,0.2)" + ]; + + const borderColors = [ + "rgba(75,192,192,1)", + "rgba(255,99,132,1)", + "rgba(255,206,86,1)" + ]; + + return { + label, + data: [ + jStat.mean(group.map(i => i.etudes)) / 3, // normalisation si besoin + jStat.mean(group.map(i => i.richesse)) / 3, + jStat.mean(group.map(i => i.sport)), + jStat.mean(group.map(i => i.musique)), + jStat.mean(group.map(i => i.lecture)) + ], + backgroundColor: colors[idx], + borderColor: borderColors[idx], + pointBackgroundColor: borderColors[idx] + }; +}); new Chart(document.getElementById("radarChart") as HTMLCanvasElement, { type: "radar", data: { - labels: ["Etudes", "Richesse", "Sport", "Musique", "Lecture"], - datasets: [{ - label: "Valeurs moyennes", - data: [ - individus[10].etudes / 3, - individus[10].richesse / 3, - individus[10].sport, - individus[10].musique, - individus[10].lecture - ], - backgroundColor: "rgba(54,162,235,0.2)", - borderColor: "rgba(54,162,235,1)", - pointBackgroundColor: "rgba(54,162,235,1)" - }] + labels: ["Études", "Richesse", "Sport", "Musique", "Lecture"], + datasets: datasets, }, options: { - scales: { r: { min: 0, max: 1, ticks: { stepSize: 0.1 } } }, + scales: { r: { min: 0, max: 1, ticks: { stepSize: 0.25 } } }, plugins: { legend: { position: "top" }, datalabels: false } } });