/** * Template Name: Bootslander * Template URL: https://bootstrapmade.com/bootslander-free-bootstrap-landing-page-template/ * Updated: Mar 17 2024 with Bootstrap v5.3.3 * Author: BootstrapMade.com * License: https://bootstrapmade.com/license/ */ const analyses = { steps: { 2022: ` <h3>2022 : Une année de progression générale</h3> <p style="font-size: 0.9em;">En 2022, les habitudes de marche des utilisateurs ont montré une progression constante, avec une augmentation continue de l'activité de marche chez tous les membres. <strong>Corentin</strong> a dominé l'année avec un pic impressionnant de 10 500 pas, restant le leader tout au long de l'année grâce à sa régularité. <strong>Anis</strong> a suivi de près, affichant une hausse régulière de son activité, bien qu'il n'ait pas atteint les sommets de Corentin. <strong>Maya</strong> a également observé une montée progressive de son activité, culminant en fin d'année à un niveau légèrement inférieur à celui d'Anis. La tendance générale a montré que tous les utilisateurs ont augmenté leur nombre de pas de manière continue, avec des variations d'intensité, mais une dynamique de progrès global. Corentin a terminé l'année en tête, suivi d'Anis et de Maya, mettant en lumière une année marquée par une progression constante des habitudes de marche.</p> </p> `, 2023: ` <h3> 2023:Analyse des habitudes de marche des utilisateurs en 2023</h3> <p style="font-size: 0.9em;">En 2023, les habitudes de marche des utilisateurs ont varié en fonction des saisons et de leurs rythmes de vie. <strong>Anis</strong>, le grand marcheur de l'année, a atteint un pic impressionnant de 11 500 pas en mars lors de son voyage en Suisse, mais a vu son activité chuter en été à cause de la chaleur. Cependant, il a progressivement retrouvé ses habitudes de marche en fin d'année. <strong>Corentin</strong>, quant à lui, a commencé l'année avec une activité faible, avant de connaître une forte montée en février. Son activité a toutefois diminué à l'automne, probablement à cause du froid et des journées plus courtes. <strong>Maya</strong> a maintenu un rythme globalement stable, mais a observé une baisse d'activité durant l'hiver et le printemps, avant de connaître une hausse significative en été grâce à ses emplois étudiants. En revanche, <strong>Amira</strong>, qui a commencé à utiliser son iPhone seulement en septembre, a montré une activité plus faible que les autres, avec une baisse marquée en fin d'année. En observant ces comportements, on constate que la majorité des utilisateurs ont réduit leur activité à la rentrée, influencée par le stress des examens et la fraîcheur de l'automne. En été, les utilisateurs comme Anis et Maya ont enregistré des pics d'activité, souvent liés à des voyages ou des emplois nécessitant plus de déplacements. Ces variations soulignent l'impact des saisons et des rythmes personnels sur les habitudes de marche.</p> `, 2024: ` <h3>2024 : Une année de comportements variés</h3> <p style="font-size: 0.9em;">En 2024, les habitudes de marche des utilisateurs ont montré des comportements variés influencés par les saisons, les examens et les fêtes. <strong>Corentin</strong> a enregistré un pic notable en décembre, atteignant plus de 10 000 pas, probablement dû aux préparatifs des fêtes de fin d'année, offrant ainsi un boost temporaire à son activité physique. <strong>Anis</strong>, de son côté, a affiché une marche régulière tout au long de l'année, oscillant entre 6 500 et 9 000 pas, avec des variations accrues en fin d'année, semblables à celles de Corentin, probablement liées aux fêtes et aux changements dans son emploi du temps. <strong>Maya</strong>, vivant près de l'université, a maintenu un rythme compris entre 3 000 et 7 500 pas, mais a connu une baisse significative en fin d'année, probablement en raison du stress des examens et des périodes intensives de révisions. <strong>Amira</strong>, elle aussi proche de l'université, a affiché une activité réduite tout au long de l'année, particulièrement en novembre, période où les révisions l'ont retenue chez elle. Les tendances communes de 2024 montrent une diminution de l'activité en hiver, particulièrement chez Amira et Maya, affectées par le stress des examens. En revanche, Corentin et Anis ont montré une activité accrue en décembre, influencée par les fêtes de fin d'année. Ces dynamiques soulignent l'impact des facteurs sociaux, académiques et saisonniers sur les habitudes de marche des utilisateurs.</p> `, }, distanceCalories: { 2023: ` <h3>Comparaison des calories brûlées vs distance parcourue en 2023</h3> <p style="font-size: 0.95em;">Anis conserve sa place de leader en 2023, marchant et brûlant le plus de calories tout au long de l'année, tandis que <strong>Corentin</strong> le suit de près, affichant également une activité constante. <strong>Amira</strong> dépasse légèrement <strong>Maya</strong> en termes de distance parcourue et de calories brûlées. Un événement marquant se produit en <strong>juillet</strong>, où les comportements des membres diffèrent : Maya, Anis, et Amira marchent peu et brûlent peu de calories, en raison de leur séjour en Algérie, où ils privilégient la voiture plutôt que la marche. En revanche, Corentin se distingue en atteignant son pic d’activité, marchant et brûlant le plus de calories ce mois-là. Les tendances générales de 2023 confirment les habitudes établies : Anis reste le plus actif, suivi de Corentin, tandis qu’Amira dépasse Maya pour la première fois. Les variations saisonnières sont également influencées par des événements spécifiques, tels que les vacances en Algérie ou les préparatifs des fêtes de fin d’année.</p> `, 2024: ` <h3>2024 : Comparaison distance-calories</h3> <p style="font-size: 1.0em;">Cette visualisation met en évidence la relation entre la distance parcourue et les calories brûlées pour chaque utilisateur, offrant un aperçu des tendances dans leurs habitudes physiques. <strong>Anis</strong> se distingue comme celui qui marche le plus et brûle le plus de calories, atteignant un pic de 484 calories brûlées en mai après avoir parcouru 6,73 km. En décembre, il reste régulier, avec 6,58 km parcourus et 474 calories brûlées. <strong>Corentin</strong> suit de près, avec une activité soutenue, notamment en décembre, mais ses performances restent en deçà de celles d'Anis. <strong>Maya</strong> et <strong>Amira</strong> affichent une activité plus modérée, bien qu'elles connaissent une hausse notable en décembre, probablement en raison des préparatifs des fêtes de fin d'année. En revanche, les mois de faible activité ne montrent pas de tendance claire, chaque utilisateur ayant des variations uniques, reflétant des contraintes et habitudes personnelles.</p>s `, }, calories: { 2022: ` <h3>2022 : Distance et calories</h3> <p style="font-size: 0.95em;">En 2022, avec l’introduction du suivi des calories brûlées en juillet, <strong>Corentin</strong> prend la tête du groupe, atteignant un pic de 220 calories, légèrement au-dessus d'<strong>Anis</strong>, qui reste un peu en retrait. <strong>Anis</strong> continue de maintenir une activité soutenue tout au long de l’année, mais n’atteint pas les 220 calories de Corentin. De son côté, <strong>Maya</strong> affiche une dépense calorique modeste, avec un maximum de 60 calories pour l'année. Enfin, <strong>Amira</strong> ne dispose pas de données sur les calories brûlées, n'ayant pas encore d'iPhone.</p> `, 2023: ` <h3>2023 : Analyse des calories brûlées</h3> <p style="font-size: 0.95em;">Anis reste le membre le plus actif, atteignant un pic de plus de 500 calories brûlées en mars. Malgré une baisse durant les mois d'été, il maintient une dépense calorique supérieure à celle des autres, témoignage de sa régularité dans les activités physiques. <strong>Corentin</strong> affiche une évolution stable, bien qu'il subisse une baisse à partir de juillet, avec environ 110 calories brûlées par mois. Les <strong>filles</strong>, <strong>Maya</strong> et <strong>Amira</strong>, montrent des tendances intéressantes : Maya se distingue par sa constance, avec une légère hausse en septembre, tandis qu'Amira progresse notablement en fin d'année, atteignant son pic en décembre avec 100 calories brûlées. En fin d'année, tous les membres montrent une dynamique plus active, renforçant l'idée d'une période de plus grande activité physique.</p> `, 2024: ` <h3>2024 : Analyse des calories</h3> <p style="font-size: 0.95em;">En 2024, les habitudes de sommeil des membres du groupe révèlent des tendances intéressantes. <strong>Amira</strong> se distingue comme la plus grande dormeuse, dépassant régulièrement les autres membres avec une moyenne de 6 heures de sommeil par nuit en décembre. Même lors des périodes stressantes, comme les examens, elle parvient à maintenir un bon niveau de sommeil. <strong>Anis</strong>, quant à lui, présente un sommeil stable et régulier tout au long de l'année, offrant une constance comparable à celle d'Amira malgré ses activités physiques soutenues. À l'inverse, <strong>Maya</strong> est celle qui dort le moins, enregistrant en décembre une durée de sommeil minimale de seulement 0,70 heure, probablement en raison des nuits blanches liées aux examens. Enfin, <strong>Corentin</strong> présente un sommeil plus variable, sans tendance marquée, ses habitudes de sommeil oscillant au fil des mois.</p> `, }, }; function updateAnalysis(visualization, year) { const container = document.getElementById("analysis-content-" + visualization); if (analyses[visualization] && analyses[visualization][year]) { container.innerHTML = analyses[visualization][year]; } else { container.innerHTML = "<p>Analyse indisponible pour cette année.</p>"; } } (function() { "use strict"; /** * Easy selector helper function */ const select = (el, all = false) => { el = el.trim() if (all) { return [...document.querySelectorAll(el)] } else { return document.querySelector(el) } } /** * Easy event listener function */ const on = (type, el, listener, all = false) => { let selectEl = select(el, all) if (selectEl) { if (all) { selectEl.forEach(e => e.addEventListener(type, listener)) } else { selectEl.addEventListener(type, listener) } } } /** * Easy on scroll event listener */ const onscroll = (el, listener) => { el.addEventListener('scroll', listener) } /** * Navbar links active state on scroll */ let navbarlinks = select('#navbar .scrollto', true) const navbarlinksActive = () => { let position = window.scrollY + 200 navbarlinks.forEach(navbarlink => { if (!navbarlink.hash) return let section = select(navbarlink.hash) if (!section) return if (position >= section.offsetTop && position <= (section.offsetTop + section.offsetHeight)) { navbarlink.classList.add('active') } else { navbarlink.classList.remove('active') } }) } window.addEventListener('load', navbarlinksActive) onscroll(document, navbarlinksActive) /** * Scrolls to an element with header offset */ const scrollto = (el) => { let header = select('#header') let offset = header.offsetHeight if (!header.classList.contains('header-scrolled')) { offset -= 20 } let elementPos = select(el).offsetTop window.scrollTo({ top: elementPos - offset, behavior: 'smooth' }) } /** * Toggle .header-scrolled class to #header when page is scrolled */ let selectHeader = select('#header') if (selectHeader) { const headerScrolled = () => { if (window.scrollY > 100) { selectHeader.classList.add('header-scrolled') } else { selectHeader.classList.remove('header-scrolled') } } window.addEventListener('load', headerScrolled) onscroll(document, headerScrolled) } /** * Back to top button */ let backtotop = select('.back-to-top') if (backtotop) { const toggleBacktotop = () => { if (window.scrollY > 100) { backtotop.classList.add('active') } else { backtotop.classList.remove('active') } } window.addEventListener('load', toggleBacktotop) onscroll(document, toggleBacktotop) } /** * Mobile nav toggle */ on('click', '.mobile-nav-toggle', function(e) { select('#navbar').classList.toggle('navbar-mobile') this.classList.toggle('bi-list') this.classList.toggle('bi-x') }) /** * Mobile nav dropdowns activate */ on('click', '.navbar .dropdown > a', function(e) { if (select('#navbar').classList.contains('navbar-mobile')) { e.preventDefault() this.nextElementSibling.classList.toggle('dropdown-active') } }, true) /** * Scrool with ofset on links with a class name .scrollto */ on('click', '.scrollto', function(e) { if (select(this.hash)) { e.preventDefault() let navbar = select('#navbar') if (navbar.classList.contains('navbar-mobile')) { navbar.classList.remove('navbar-mobile') let navbarToggle = select('.mobile-nav-toggle') navbarToggle.classList.toggle('bi-list') navbarToggle.classList.toggle('bi-x') } scrollto(this.hash) } }, true) /** * Scroll with ofset on page load with hash links in the url */ window.addEventListener('load', () => { if (window.location.hash) { if (select(window.location.hash)) { scrollto(window.location.hash) } } }); /** * Preloader */ let preloader = select('#preloader'); if (preloader) { window.addEventListener('load', () => { preloader.remove() }); } /** * Initiate glightbox */ const glightbox = GLightbox({ selector: '.glightbox' }); /** * Initiate gallery lightbox */ const galleryLightbox = GLightbox({ selector: '.gallery-lightbox' }); /** * Testimonials slider */ new Swiper('.testimonials-slider', { speed: 600, loop: true, autoplay: { delay: 5000, disableOnInteraction: false }, slidesPerView: 'auto', pagination: { el: '.swiper-pagination', type: 'bullets', clickable: true } }); /** * Animation on scroll */ window.addEventListener('load', () => { AOS.init({ duration: 1000, easing: 'ease-in-out', once: true, mirror: false }) }); /** * Initiate Pure Counter */ new PureCounter(); })(); function renderStepsVisualization() { fetch('../static/js/final_combined_with_all_data.json') // Chemin à adapter si nécessaire .then((response) => response.json()) .then((data) => { const parseDate = d3.timeParse("%Y-%m-%d"); const formatYear = d3.timeFormat("%Y"); const formatMonth = d3.timeFormat("%Y-%m"); const members = ["Corentin", "Maya", "Anis", "Amira"]; data.forEach(d => d.date = parseDate(d.date)); const margin = { top: 50, right: 230, bottom: 150, left: 70 }; const width = 800 - margin.left - margin.right; const height = 500 - margin.top - margin.bottom; const svgContainer = d3.select("#steps-visualization") .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom + 50); // +50 pour la légende // Ajout du titre svgContainer.append("text") .attr("x", (width + margin.left + margin.right) / 2.5 ) // Centré horizontalement .attr("y", 20) // Positionné en haut .attr("text-anchor", "middle") .style("font-size", "18px") .style("font-weight", "bold") .text("Comparaison des pas des utilisateurs"); const svg = svgContainer.append("g") .attr("transform", `translate(${margin.left},${margin.top})`); const years = [2022, 2023, 2024]; const stepsPrevYearBtn = document.getElementById("stepsPrevYear"); const stepsNextYearBtn = document.getElementById("stepsNextYear"); const stepsCurrentYearDisplay = document.getElementById("stepsCurrentYear"); stepsPrevYearBtn.addEventListener("click", () => { const currentYearIndex = years.indexOf(parseInt(stepsCurrentYearDisplay.textContent)); if (currentYearIndex > 0) { const newYear = years[currentYearIndex - 1]; stepsCurrentYearDisplay.textContent = newYear; updateVisualization(newYear); updateAnalysis("steps", newYear); stepsNextYearBtn.disabled = false; if (currentYearIndex - 1 === 0) { stepsPrevYearBtn.disabled = true; } } }); stepsNextYearBtn.addEventListener("click", () => { const currentYearIndex = years.indexOf(parseInt(stepsCurrentYearDisplay.textContent)); if (currentYearIndex < years.length - 1) { const newYear = years[currentYearIndex + 1]; stepsCurrentYearDisplay.textContent = newYear; updateVisualization(newYear); updateAnalysis("steps", newYear); stepsPrevYearBtn.disabled = false; if (currentYearIndex + 1 === years.length - 1) { stepsNextYearBtn.disabled = true; } } }); stepsPrevYearBtn.disabled = years.indexOf(parseInt(stepsCurrentYearDisplay.textContent)) === 0; stepsNextYearBtn.disabled = years.indexOf(parseInt(stepsCurrentYearDisplay.textContent)) === years.length - 1; updateVisualization(2024); function updateVisualization(selectedYear) { const filteredData = data.filter(d => formatYear(d.date) === selectedYear.toString()); if (filteredData.length === 0) { console.log(`Aucune donnée pour l'année ${selectedYear}`); return; } const groupedData = d3.groups(filteredData, d => formatMonth(d.date)); const aggregatedData = groupedData.map(([month, records]) => { const aggregated = { month }; members.forEach(member => { aggregated[`Steps_${member}`] = d3.mean(records, d => d[`Steps_${member}`] || 0); }); return aggregated; }); svg.selectAll("*").remove(); const xScale = d3.scaleBand() .domain(aggregatedData.map(d => d.month)) .range([0, width]) .padding(0.2) const yScale = d3.scaleLinear() .domain([0, d3.max(aggregatedData, d => Math.max(...members.map(member => d[`Steps_${member}`])))]).nice() .range([height, 0]); const colorMap = { "Maya": "#0f7e06", "Corentin": "#1d38e3", "Anis": "#d6bff4", "Amira": "#7e09bd" }; svg.append("g") .attr("transform", `translate(0, ${height})`) .call(d3.axisBottom(xScale)) .selectAll("text") .attr("transform", "rotate(-45)") .style("text-anchor", "end"); svg.append("g").call(d3.axisLeft(yScale)); svg.append("text") .attr("x", width / 2) .attr("y", height + 70) .attr("text-anchor", "middle") .style("font-size", "14px") .text("Mois"); svg.append("text") .attr("transform", "rotate(-90)") .attr("x", -height / 2) .attr("y", -50) .attr("text-anchor", "middle") .style("font-size", "14px") .text("Nombre de pas"); members.forEach((member) => { const lineData = aggregatedData.map(d => { let steps = d[`Steps_${member}`] === -1.0 ? 0 : d[`Steps_${member}`]; return { month: d.month, steps }; }); const line = d3.line() .x(d => xScale(d.month) + xScale.bandwidth() / 2) .y(d => yScale(d.steps)) .defined(d => d.steps !== 0); svg.append("path") .data([lineData]) .attr("class", `line-${member}`) .attr("d", line) .attr("fill", "none") .attr("stroke", colorMap[member]) .attr("stroke-width", 2); }); const legend = svg.append("g") .attr("transform", `translate(${width / 20}, ${height + 80})`); members.forEach((member, i) => { const legendGroup = legend.append("g") .attr("transform", `translate(${i * 150}, 0)`); legendGroup.append("rect") .attr("width", 15) .attr("height", 15) .attr("fill", colorMap[member]); legendGroup.append("text") .attr("x", 20) .attr("y", 12) .style("font-size", "12px") .text(member); }); } }); } // VISU 2 function renderDistanceVisualization() { fetch('../static/js/final_combined_with_all_data.json') // Chemin à adapter si nécessaire .then((response) => response.json()) .then((data) => { const parseDate = d3.timeParse("%Y-%m-%d"); const formatYear = d3.timeFormat("%Y"); const formatMonth = d3.timeFormat("%Y-%m"); const members = ["Corentin", "Maya", "Anis", "Amira"]; data.forEach(d => d.date = parseDate(d.date)); const margin = { top: 50, right: 230, bottom: 150, left: 70 }; const width = 800 - margin.left - margin.right; const height = 500 - margin.top - margin.bottom; const svg = d3.select("#distance-visualization") .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", `translate(${margin.left},${margin.top})`); const years = [2023, 2024]; const prevYearBtn = document.getElementById("distancePrevYear"); const nextYearBtn = document.getElementById("distanceNextYear"); const currentYearDisplay = document.getElementById("distanceCurrentYear"); // Gestion des événements des boutons prevYearBtn.addEventListener("click", () => { const currentYearIndex = years.indexOf(parseInt(currentYearDisplay.textContent)); if (currentYearIndex > 0) { const newYear = years[currentYearIndex - 1]; currentYearDisplay.textContent = newYear; updateVisualization(newYear); updateAnalysis("distanceCalories", newYear); // Activer/désactiver les boutons nextYearBtn.disabled = false; if (currentYearIndex - 1 === 0) { prevYearBtn.disabled = true; } } }); nextYearBtn.addEventListener("click", () => { const currentYearIndex = years.indexOf(parseInt(currentYearDisplay.textContent)); if (currentYearIndex < years.length - 1) { const newYear = years[currentYearIndex + 1]; currentYearDisplay.textContent = newYear; updateVisualization(newYear); updateAnalysis("distanceCalories", newYear); // Activer/désactiver les boutons prevYearBtn.disabled = false; if (currentYearIndex + 1 === years.length - 1) { nextYearBtn.disabled = true; } } }); // Initialisation des boutons prevYearBtn.disabled = years.indexOf(parseInt(currentYearDisplay.textContent)) === 0; nextYearBtn.disabled = years.indexOf(parseInt(currentYearDisplay.textContent)) === years.length - 1; const tooltip = d3.select("body").append("div") .attr("class", "tooltip-distance") .style("opacity", 0); function updateVisualization(selectedYear) { const filteredData = data.filter(d => formatYear(d.date) === selectedYear.toString()); const groupedData = d3.groups(filteredData, d => formatMonth(d.date)); const colorMap = { "Maya": "#0f7e06", "Corentin": "#1d38e3", "Anis": "#d6bff4", "Amira": "#7e09bd" }; const aggregatedData = groupedData.map(([month, records]) => { const aggregated = { month }; members.forEach(member => { aggregated[`Distance_${member}`] = d3.mean(records, d => { const distance = d[`Distance_${member}`]; return (distance !== -1) ? distance : undefined; }) || 0; aggregated[`Calories_${member}`] = d3.mean(records, d => { const calories = d[`Calories_${member}`]; return (calories !== -1) ? calories : undefined; }) || 0; }); return aggregated; }); svg.selectAll("*").remove(); const bubbleSizeScale = d3.scaleLinear() .domain([0, d3.max(aggregatedData, d => Math.max(...members.map(member => d[`Distance_${member}`])) )]) .range([2, 20]); const xScale = d3.scaleLinear() .domain([0, d3.max(aggregatedData, d => Math.max(...members.map(member => d[`Distance_${member}`])) )]) .range([0, width]); const yScale = d3.scaleLinear() .domain([0, d3.max(aggregatedData, d => Math.max(...members.map(member => d[`Calories_${member}`])) )]) .range([height, 0]); svg.append("text") .attr("x", width / 2) .attr("y", margin.top - 75) .attr("text-anchor", "middle") .style("font-size", "16px") .style("font-weight", "bold") .text("Comparaison des calories brûlées vs distance parcourue"); svg.append("g") .attr("transform", `translate(0, ${height})`) .call(d3.axisBottom(xScale)); svg.append("text") .attr("x", width / 2) .attr("y", height + 70) .attr("text-anchor", "middle") .style("font-size", "14px") .text("Distance (KM)"); svg.append("g").call(d3.axisLeft(yScale)); svg.append("text") .attr("transform", "rotate(-90)") .attr("x", -height / 2) .attr("y", -50) .attr("text-anchor", "middle") .style("font-size", "14px") .text("Calories brûlées"); members.forEach((member) => { svg.selectAll(`.bubble-${member}`) .data(aggregatedData) .enter() .append("circle") .attr("cx", d => xScale(d[`Distance_${member}`])) .attr("cy", d => yScale(d[`Calories_${member}`])) .attr("r", d => 0.8* bubbleSizeScale(d[`Distance_${member}`])) .attr("fill", d => { return (d[`Distance_${member}`] === 0 || d[`Calories_${member}`] === 0) ? "gray" : colorMap[member]; }) .style("opacity", 0.7) .on("mouseover", function(event, d) { tooltip.transition().duration(200).style("opacity", 0.9); const monthFormatted = d3.timeFormat("%B")(parseDate(d.month)); // Formatage du mois const distance = d[`Distance_${member}`] ? d[`Distance_${member}`].toFixed(2) : "Pas de données"; const calories = d[`Calories_${member}`] ? d[`Calories_${member}`].toFixed(2) : "Pas de données"; tooltip.html(` <strong>${member}</strong><br> Mois : ${d.month}<br> Distance : ${distance} km<br> Calories : ${calories} cal `) .style("left", `${event.pageX + 10}px`) .style("top", `${event.pageY - 28}px`); }) .on("mousemove", function(event) { tooltip.style("left", `${event.pageX + 10}px`).style("top", `${event.pageY - 28}px`); }) .on("mouseout", function() { tooltip.transition().duration(500).style("opacity", 0); }); // Ajouter une légende horizontale sous le graphique const legend = svg.append("g") .attr("transform", `translate(${width / 2 - (members.length * 110) / 2}, ${height + 90})`); // Positionnement horizontal centré members.forEach((member, i) => { const legendGroup = legend.append("g") .attr("transform", `translate(${i * 100}, 0)`); // Espacement horizontal entre les éléments // Rectangle coloré pour la légende legendGroup.append("rect") .attr("width", 15) .attr("height", 15) .attr("fill", colorMap[member]) .style("opacity", 0.8); // Texte descriptif à côté du rectangle legendGroup.append("text") .attr("x", 20) // Décalage horizontal par rapport au rectangle .attr("y", 12) // Alignement vertical au centre .style("font-size", "12px") .text(member); }); }); } updateVisualization(years[1]); }); } //VISU 3 function renderCaloriesVisualization() { fetch('../static/js/final_combined_with_all_data.json') .then((response) => response.json()) .then((data) => { const parseDate = d3.timeParse("%Y-%m-%d"); const formatYear = d3.timeFormat("%Y"); const formatMonth = d3.timeFormat("%Y-%m"); const members = ["Corentin", "Maya", "Anis", "Amira"]; data.forEach(d => d.date = parseDate(d.date)); const margin = { top: 50, right: 230, bottom: 150, left: 70 }; const width = 800 - margin.left - margin.right; const height = 500 - margin.top - margin.bottom; const svg = d3.select("#calories-visualization") .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", `translate(${margin.left},${margin.top})`); const years = [2022, 2023, 2024]; const prevYearBtn = document.getElementById("caloriesPrevYear"); const nextYearBtn = document.getElementById("caloriesNextYear"); const currentYearDisplay = document.getElementById("caloriesCurrentYear"); // Gestion des événements des boutons prevYearBtn.addEventListener("click", () => { const currentYearIndex = years.indexOf(parseInt(currentYearDisplay.textContent)); if (currentYearIndex > 0) { const newYear = years[currentYearIndex - 1]; currentYearDisplay.textContent = newYear; updateVisualization(newYear); updateAnalysis("calories", newYear); // Activer/désactiver les boutons nextYearBtn.disabled = false; if (currentYearIndex - 1 === 0) { prevYearBtn.disabled = true; } } }); nextYearBtn.addEventListener("click", () => { const currentYearIndex = years.indexOf(parseInt(currentYearDisplay.textContent)); if (currentYearIndex < years.length - 1) { const newYear = years[currentYearIndex + 1]; currentYearDisplay.textContent = newYear; updateVisualization(newYear); updateAnalysis("calories", newYear); // Activer/désactiver les boutons prevYearBtn.disabled = false; if (currentYearIndex + 1 === years.length - 1) { nextYearBtn.disabled = true; } } }); // Initialisation des boutons prevYearBtn.disabled = years.indexOf(parseInt(currentYearDisplay.textContent)) === 0; nextYearBtn.disabled = years.indexOf(parseInt(currentYearDisplay.textContent)) === years.length - 1; updateVisualization(2024); function updateVisualization(selectedYear) { const filteredData = data.filter(d => formatYear(d.date) === selectedYear.toString()); if (filteredData.length === 0) { console.log(`Aucune donnée pour l'année ${selectedYear}`); return; } const groupedData = d3.groups(filteredData, d => formatMonth(d.date)); const aggregatedData = groupedData.map(([month, records]) => { const aggregated = { month }; members.forEach(member => { aggregated[`Calories_${member}`] = d3.mean(records, d => d[`Calories_${member}`] || 0); }); return aggregated; }); svg.selectAll("*").remove(); svg.append("text") .attr("x", width / 2) .attr("y", margin.top - 60) .attr("text-anchor", "middle") .style("font-size", "16px") .style("font-weight", "bold") .text("Analyse des calories brûlées par mois"); const xScale = d3.scaleBand() .domain(aggregatedData.map(d => d.month)) .range([0, width]) .padding(0.2); const yScale = d3.scaleLinear() .domain([0, d3.max(aggregatedData, d => Math.max(...members.map(member => d[`Calories_${member}`])))]).nice() .range([height, 0]); const colorScale = d3.scaleOrdinal() .domain(members) .range(["#1d38e3", "#0f7e06", "#d6bff4", "#7e09bd"]); svg.append("g") .attr("transform", `translate(0, ${height})`) .call(d3.axisBottom(xScale)) .selectAll("text") .attr("transform", "rotate(-45)") .style("text-anchor", "end"); svg.append("text") .attr("x", width / 2) .attr("y", height + 70) .attr("text-anchor", "middle") .style("font-size", "14px") .text("Mois"); svg.append("g").call(d3.axisLeft(yScale)); svg.append("text") .attr("transform", "rotate(-90)") .attr("x", -height / 2) .attr("y", -50) .attr("text-anchor", "middle") .style("font-size", "14px") .text("Calories brûlées"); // Ajout de la légende const legend = svg.append("g") .attr("transform", `translate(${width / 2 - (members.length * 120) / 2}, ${height + 100})`); // Positionnement members.forEach((member, i) => { const legendGroup = legend.append("g") .attr("transform", `translate(${i * 120}, 0)`); // Espacement horizontal // Rectangle coloré legendGroup.append("rect") .attr("width", 15) .attr("height", 15) .attr("fill", colorScale(member)) .style("opacity", 0.8); // Texte descriptif legendGroup.append("text") .attr("x", 20) // Position par rapport au rectangle .attr("y", 12) // Alignement vertical .style("font-size", "12px") .text(member); }); members.forEach(member => { const areaData = aggregatedData.map(d => ({ month: d.month, calories: d[`Calories_${member}`] || 0 })); const area = d3.area() .x(d => xScale(d.month) + xScale.bandwidth() / 2) .y0(yScale(0.8)) .y1(d => yScale(d.calories )); svg.append("path") .data([areaData]) .attr("fill", colorScale(member)) .attr("opacity", 0.8) .attr("d", area); }); } }); } // Visu4 function renderSleepVisualization() { fetch('../static/js/final_combined_with_all_data.json') // Adapter le chemin si nécessaire .then((response) => response.json()) .then((data) => { const parseDate = d3.timeParse("%Y-%m-%d"); const formatYear = d3.timeFormat("%Y"); const formatMonth = d3.timeFormat("%Y-%m"); const formatDay = d3.timeFormat("%d"); const members = ["Corentin", "Maya", "Anis", "Amira"]; data.forEach(d => d.date = parseDate(d.date)); // Dimensions et marges const margin = { top: 50, right: 230, bottom: 150, left: 70 }; const width = 800 - margin.left - margin.right; const height = 500 - margin.top - margin.bottom; // Créer le conteneur SVG principal const svg = d3.select("#sleep-visualization") .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", `translate(${margin.left},${margin.top})`); // Créer un conteneur pour le graphique détaillé const detailContainer = d3.select("#sleep-visualization") .append("div") .attr("id", "detail-container") .style("margin-top", "20px"); // Définir les années et la plage d'affichage const years = [2021, 2022, 2023, 2024]; // Créer le slider // Ajoutez les boutons Bootstrap pour naviguer entre les années // Gestion des événements des boutons let currentYearIndex = years.indexOf(2024); // Année initiale // Fonction pour mettre à jour l'état des boutons function updateButtonStates() { if (currentYearIndex <= 0) { document.getElementById("prevYear").style.display = "hidden"; // Cacher le bouton précédent } else { document.getElementById("prevYear").style.display = "inline-block"; // Réafficher le bouton précédent } if (currentYearIndex >= years.length - 1) { document.getElementById("nextYear").style.display = "hidden"; // Cacher le bouton suivant } else { document.getElementById("nextYear").style.display = "inline-block"; // Réafficher le bouton suivant } } // Initialiser l'état des boutons updateButtonStates(); // Événement pour le bouton précédent document.getElementById("prevYear").addEventListener("click", () => { if (currentYearIndex > 0) { currentYearIndex--; document.getElementById("currentYear").textContent = years[currentYearIndex]; updateVisualization(years[currentYearIndex]); updateButtonStates(); // Mettre à jour l'état des boutons } }); // Événement pour le bouton suivant document.getElementById("nextYear").addEventListener("click", () => { if (currentYearIndex < years.length - 1) { currentYearIndex++; document.getElementById("currentYear").textContent = years[currentYearIndex]; updateVisualization(years[currentYearIndex]); updateButtonStates(); // Mettre à jour l'état des boutons } }); // Tooltip const tooltip = d3.select("body").append("div") .attr("class", "tooltip-sleep") .style("opacity", 0); // Fonction de mise à jour de la visualisation function updateVisualization(selectedYear) { const filteredData = data.filter(d => formatYear(d.date) === selectedYear.toString()); const groupedData = d3.groups(filteredData, d => formatMonth(d.date)); const colorMap = { "Maya": "#0f7e06", "Corentin": "#1d38e3", "Anis": "#d6bff4", "Amira": "#7e09bd" }; const aggregatedData = groupedData.map(([month, records]) => { const aggregated = { month }; members.forEach(member => { aggregated[`Sleep_${member}`] = d3.mean(records, d => d[`Sleep_${member}`] || 0); }); return aggregated; }); svg.selectAll("*").remove(); svg.append("text") .attr("x", width / 2) // Centrer horizontalement .attr("y", margin.top - 60) // Positionner légèrement au-dessus du graphique .attr("text-anchor", "middle") // Centrer le texte .style("font-size", "16px") // Taille de police .style("font-weight", "bold") // Gras .text("Analyse des heures de sommeil par mois"); const xScale = d3.scaleBand() .domain(aggregatedData.map(d => d.month)) .range([0, width]) .padding(0.2); const yScale = d3.scaleLinear() .domain([0, d3.max(aggregatedData, d => Math.max(...members.map(member => d[`Sleep_${member}`])))]).nice() .range([height, 0]); const colorScale = d3.scaleOrdinal(d3.schemeCategory10).domain(members); // Axe X svg.append("g") .attr("transform", `translate(0, ${height})`) .call(d3.axisBottom(xScale)) .selectAll("text") .attr("transform", "rotate(-45)") .style("text-anchor", "end"); // Légende de l'axe X svg.append("text") .attr("x", width / 2) .attr("y", height + 70) .attr("text-anchor", "middle") .style("font-size", "14px") .text("Mois"); // Axe Y svg.append("g").call(d3.axisLeft(yScale)); // Légende de l'axe Y svg.append("text") .attr("transform", "rotate(-90)") .attr("x", -height / 2) .attr("y", -50) .attr("text-anchor", "middle") .style("font-size", "14px") .text("Sommeil moyen (heures)"); // Légende des membres (rectangles colorés) // Créer un conteneur pour la légende const legend = svg.append("g") .attr("transform", `translate(${width / 3}, ${height + margin.bottom - 50})`) // Positionner la légende en bas, centrée .attr("text-anchor", "middle"); // Ajouter les éléments de la légende members.forEach((member, i) => { legend.append("rect") .attr("x", i * 100 - (members.length * 50)) // Espacement horizontal entre les rectangles .attr("y", 0) .attr("width", 15) .attr("height", 15) .attr("fill", colorMap[member]); legend.append("text") .attr("x", i * 100 - (members.length * 50) + 20) // Texte à côté du rectangle .attr("y", 12) .text(member) .style("font-size", "12px") .attr("text-anchor", "start"); }); // Ajouter un élément pour "Pas de données" legend.append("rect") .attr("x", members.length * 100 - (members.length * 50)) // Position pour le rectangle gris .attr("y", 0) .attr("width", 15) .attr("height", 15) .attr("fill", "lightgrey"); legend.append("text") .attr("x", members.length * 100 - (members.length * 50) + 20) // Texte à côté du rectangle gris .attr("y", 12) .text("Pas de données") .style("font-size", "12px") .attr("text-anchor", "start"); // Barres pour chaque membre members.forEach((member, i) => { svg.selectAll(`.bar-sleep-${member}`) .data(aggregatedData) .enter() .append("rect") .attr("x", d => xScale(d.month) + i * (xScale.bandwidth() / members.length)) .attr("y", d => { const value = d[`Sleep_${member}`]; return value === -1.0 ? yScale(2) : yScale(value); // Placer les -1.0 à une hauteur fixe, ici 2 heures }) .attr("width", xScale.bandwidth() / members.length) .attr("height", d => { const value = d[`Sleep_${member}`]; return value === -1.0 ? height - yScale(2) : height - yScale(value); // Barres grisées si -1.0 }) .attr("fill", d => { const value = d[`Sleep_${member}`]; return value === -1.0 ? "#D3D3D3" : colorMap[member]; // Gris pour -1.0 }) .on("mouseover", function(event, d) { tooltip.transition().duration(200).style("opacity", .9); // Transition d'apparition du tooltip tooltip.html(`Mois : ${d.month}<br>Sommeil moyen : ${d[`Sleep_${member}`] === -1.0 ? "Pas de données" : d[`Sleep_${member}`].toFixed(2)} heures`) // Affichage du tooltip .style("left", (event.pageX + 5) + "px") .style("top", (event.pageY - 28) + "px"); }) .on("mouseout", function() { tooltip.transition().duration(500).style("opacity", 0); // Transition de disparition du tooltip }) .on("click", (event, d) => { const memberColor = colorMap[member]; // Récupérer la couleur du membre showDetailChart(d.month, member, selectedYear, memberColor); // Passer la couleur à la fonction showDetailChart }); }); } // Fonction pour afficher les détails function showDetailChart(month, member, year, memberColor) { // Affiche le modal const modal = d3.select("#modal"); modal.style("display", "block"); // Fermer le modal modal.select(".close").on("click", () => { modal.style("display", "none"); d3.select("#detail-visualization").selectAll("*").remove(); }); const detailContainer = d3.select("#detail-visualization"); detailContainer.selectAll("*").remove(); const filteredData = data.filter(d => formatYear(d.date) === year.toString() && formatMonth(d.date) === month); const dailyData = d3.groups(filteredData, d => formatDay(d.date)).map(([day, records]) => ({ day: day, value: d3.mean(records, d => d[`Sleep_${member}`] || 0) })); const detailSvg = detailContainer.append("svg") .attr("width", 600) .attr("height", 400) .append("g") .attr("transform", "translate(50, 50)"); const xScale = d3.scaleBand() .domain(dailyData.map(d => d.day)) .range([0, 500]) .padding(0.1); const yScale = d3.scaleLinear() .domain([0, d3.max(dailyData, d => d.value)]).nice() .range([300, 0]); // Ajout de l'axe X detailSvg.append("g") .attr("transform", "translate(0, 300)") .call(d3.axisBottom(xScale)); // Ajout de l'axe Y detailSvg.append("g") .call(d3.axisLeft(yScale)); // Titre du graphique detailSvg.append("text") .attr("x", 250) .attr("y", -20) .attr("text-anchor", "middle") .style("font-size", "16px") .text(`${member} - Sommeil du mois de ${month} (${year})`); // Ajouter la légende de l'axe X (Jours) detailSvg.append("text") .attr("x", 250) .attr("y", 340) .attr("text-anchor", "middle") .style("font-size", "12px") .text("Jours du mois"); // Ajouter la légende de l'axe Y (Heures de sommeil) detailSvg.append("text") .attr("transform", "rotate(-90)") .attr("x", -200) .attr("y", -40) .attr("text-anchor", "middle") .style("font-size", "12px") .text("Heures de sommeil moyen"); // Dessin des barres detailSvg.selectAll(".bar") .data(dailyData) .enter() .append("rect") .attr("x", d => xScale(d.day)) .attr("y", d => { const value = d.value; return value < 0 || value === null ? yScale(2) : yScale(value); }) .attr("width", xScale.bandwidth()) .attr("height", d => { const value = d.value; return value < 0 || value === null ? 300 - yScale(2) : 300 - yScale(value); }) .attr("fill", d => { const value = d.value; return value < 0 || value === null ? "lightgrey" : memberColor; }) .on("mouseover", function(event, d) { tooltip.transition().duration(200).style("opacity", 1); tooltip.html(` <div style="text-align: center;"> <strong>Jour :</strong> ${d.day}<br> <strong>Sommeil :</strong> ${d.value === -1.0 || d.value === null ? "Pas de données" : d.value.toFixed(2)} heures </div> `); }) .on("mousemove", function(event) { tooltip .style("left", (event.pageX + 15) + "px") // Décalage pour positionner le tooltip .style("top", (event.pageY + 15) + "px"); }) .on("mouseout", function() { tooltip.transition().duration(500).style("opacity", 0); }); } // Initialisation de la visualisation updateVisualization(years[3]); // Mettre à jour la visualisation lorsque le slider est déplacé rangeSlider.on("input", function() { const selectedYear = years[this.value]; yearDisplay.text(selectedYear); updateVisualization(selectedYear); }); }); } // Visu 5 function renderSleepActivityVisualization() { fetch('../static/js/final_combined_with_all_data.json') // Adapter le chemin si nécessaire .then(response => response.json()) .then(data => { const svg = d3.select("#sleep-activity-visualization") .append("svg") .attr("width", 700) .attr("height", 300); const margin = { top: 20, right: 150, bottom: 50, left: 50 }; const width = +svg.attr("width") - margin.left - margin.right; const height = +svg.attr("height") - margin.top - margin.bottom; const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`); const tooltip = d3.select("body").append("div") .attr("class", "tooltip") .style("position", "absolute") .style("visibility", "hidden") .style("background", "#fff") .style("border", "1px solid #ccc") .style("padding", "5px") .style("border-radius", "4px") .style("font-size", "12px"); const colorMap = { "Maya": "#0f7e06", "Corentin": "#1d38e3", "Anis": "#d6bff4", "Amira": "#7e09bd" }; const getISOWeekNumber = (date) => { const tempDate = new Date(date); tempDate.setHours(0, 0, 0, 0); tempDate.setDate(tempDate.getDate() + 4 - (tempDate.getDay() || 7)); const yearStart = new Date(tempDate.getFullYear(), 0, 1); return Math.ceil(((tempDate - yearStart) / 86400000 + 1) / 7); }; const filteredData = data.filter(d => { const date = new Date(d.date); return date >= new Date("2023-10-01") && date <= new Date("2024-12-31"); }); const groupedData = d3.group(filteredData, d => { const date = new Date(d.date); const weekNumber = getISOWeekNumber(date); return `${date.getFullYear()}-W${weekNumber}`; }); const processedData = Array.from(groupedData, ([week, records]) => { return records.map(d => ([ { name: "Anis", steps: d.Steps_Anis, sleep: d.Sleep_Anis, calories: d.Calories_Anis }, { name: "Maya", steps: d.Steps_Maya, sleep: d.Sleep_Maya, calories: d.Calories_Maya }, { name: "Corentin", steps: d.Steps_Corentin, sleep: d.Sleep_Corentin, calories: d.Calories_Corentin }, { name: "Amira", steps: d.Steps_Amira, sleep: d.Sleep_Amira, calories: d.Calories_Amira } ].filter(d => d.steps > 0 && d.sleep > 0))).flat(); }); const x = d3.scaleLinear() .domain([0, Math.ceil(d3.max(processedData.flat(), d => d.steps))]) .range([0, width]); const y = d3.scaleLinear() .domain([0, 18]) .range([height, 0]); const radius = d3.scaleSqrt() .domain([0, Math.ceil(d3.max(processedData.flat(), d => d.calories))]) .range([3, 15]); g.append("g") .attr("transform", `translate(0,${height})`) .call(d3.axisBottom(x).ticks(10)) .append("text") .attr("fill", "black") .attr("x", width / 2) .attr("y", 40) .attr("text-anchor", "middle") .text("Steps"); g.append("g") .call(d3.axisLeft(y)) .append("text") .attr("fill", "black") .attr("transform", "rotate(-90)") .attr("x", -height / 2) .attr("y", -40) .attr("text-anchor", "middle") .text("Sleep (hours)"); const legend = svg.append("g") .attr("transform", `translate(${width + 20}, 50)`); legend.selectAll("rect") .data(Object.keys(colorMap)) .enter() .append("rect") .attr("x", 0) .attr("y", (d, i) => i * 20) .attr("width", 15) .attr("height", 15) .attr("fill", d => colorMap[d]); legend.selectAll("text") .data(Object.keys(colorMap)) .enter() .append("text") .attr("x", 20) .attr("y", (d, i) => i * 20 + 12) .text(d => d); const slider = document.getElementById("date-slider"); const playButton = document.getElementById("play-button"); let playing = false; let interval; slider.max = processedData.length - 1; const update = (index) => { const currentData = processedData[index]; const weekLabel = Array.from(groupedData.keys())[index]; document.getElementById("date-label").textContent = weekLabel; g.selectAll("circle").remove(); g.selectAll("circle") .data(currentData) .enter() .append("circle") .attr("cx", d => x(d.steps)) .attr("cy", d => y(d.sleep)) .attr("r", d => radius(d.calories)) .attr("fill", d => colorMap[d.name]) .attr("opacity", 0.7) .on("mouseover", (event, d) => { tooltip.style("visibility", "visible") .text(`${d.name}: Steps: ${d.steps}, Sleep: ${d.sleep}, Calories: ${d.calories}`); }) .on("mousemove", event => { tooltip.style("top", `${event.pageY - 10}px`) .style("left", `${event.pageX + 10}px`); }) .on("mouseout", () => { tooltip.style("visibility", "hidden"); }); }; playButton.addEventListener("click", () => { if (!playing) { playing = true; playButton.textContent = "Pause"; let index = 0; interval = setInterval(() => { if (index >= processedData.length) { clearInterval(interval); playButton.textContent = "Play"; playing = false; } else { slider.value = index; update(index); index++; } }, 800); } else { clearInterval(interval); playButton.textContent = "Play"; playing = false; } }); slider.addEventListener("input", (event) => update(+event.target.value)); update(0); }) .catch(error => console.error("Error loading data:", error)); } // Visu 6 function renderRadialDistanceChart() { fetch("../static/js/final_combined_with_all_data.json") .then((response) => response.json()) .then((data) => { const width = 300; const height = 300; const innerRadius = 30; const outerRadius = Math.min(width, height) / 2 - 20; // Filter data const filteredData = data.filter((d) => { const date = new Date(d.date); return date >= new Date("2023-10-01") && date <= new Date("2024-12-31"); }); // Group data by ISO week const groupedData = d3.group(filteredData, (d) => { const date = new Date(d.date); const weekNumber = getISOWeekNumber(date); return `${date.getFullYear()}-W${weekNumber}`; }); const processedData = Array.from(groupedData, ([week, records]) => { const aggregated = { week: week, year: week.split("-")[0], Distance_Anis: d3.sum(records, (d) => (d.Distance_Anis > 0 ? d.Distance_Anis : 0)), Distance_Maya: d3.sum(records, (d) => (d.Distance_Maya > 0 ? d.Distance_Maya : 0)), Distance_Corentin: d3.sum(records, (d) => (d.Distance_Corentin > 0 ? d.Distance_Corentin : 0)), Distance_Amira: d3.sum(records, (d) => (d.Distance_Amira > 0 ? d.Distance_Amira : 0)), Sleep_Anis: d3.mean(records, (d) => (d.Sleep_Anis > 0 ? d.Sleep_Anis : 0)), Sleep_Maya: d3.mean(records, (d) => (d.Sleep_Maya > 0 ? d.Sleep_Maya : 0)), Sleep_Corentin: d3.mean(records, (d) => (d.Sleep_Corentin > 0 ? d.Sleep_Corentin : 0)), Sleep_Amira: d3.mean(records, (d) => (d.Sleep_Amira > 0 ? d.Sleep_Amira : 0)), }; return aggregated; }); const users = ["Anis", "Maya", "Corentin", "Amira"]; users.forEach((user) => { const personKey = `Distance_${user}`; const sleepKey = `Sleep_${user}`; d3.select(`#chart-${user}`).html(""); // Clear the previous chart const svg = d3.select(`#chart-${user}`) .append("svg") .attr("width", width) .attr("height", height) .attr("viewBox", [-width / 2, -height / 2, width, height]) .attr("style", "width: 100%; height: auto; font: 10px sans-serif;"); // Tooltip const tooltip = d3.select("body").append("div") .attr("class", "tooltip-radial") .style("opacity", 0) .style("position", "absolute") .style("background", "rgba(0, 0, 0, 0.7)") .style("color", "white") .style("padding", "8px") .style("border-radius", "4px") .style("pointer-events", "none"); // Scales const x = d3.scaleBand() .domain(processedData.map((d) => d.week)) .range([0, 2 * Math.PI]) .align(0); const y = d3.scaleRadial() .domain([0, d3.max(processedData, (d) => d[personKey])]) .range([innerRadius, outerRadius]); const color = d3.scaleLinear() .domain([0, d3.max(processedData, (d) => d[sleepKey])]) .range(["lightblue", "darkblue"]); // Bars svg.append("g") .selectAll("path") .data(processedData) .join("path") .attr("d", d3.arc() .innerRadius(innerRadius) .outerRadius((d) => d[personKey] > 0 ? y(d[personKey]) : y(10)) .startAngle((d) => x(d.week)) .endAngle((d) => x(d.week) + x.bandwidth()) .padAngle(0.02) .padRadius(innerRadius)) .attr("fill", (d) => d[personKey] > 0 ? color(d[sleepKey]) : "#ccc") .on("mouseover", function (event, d) { tooltip.transition().duration(200).style("opacity", 0.9); tooltip.html(` <strong>${user}</strong><br> Semaine : ${d.week}<br> Distance : ${d[personKey] > 0 ? d[personKey].toFixed(2) : "N/A"} km<br> Sommeil : ${d[sleepKey] > 0 ? d[sleepKey].toFixed(2) + "h" : "N/A"} `) .style("left", `${event.pageX + 10}px`) .style("top", `${event.pageY - 28}px`); }) .on("mousemove", function (event) { tooltip.style("left", `${event.pageX + 10}px`).style("top", `${event.pageY - 28}px`); }) .on("mouseout", function () { tooltip.transition().duration(500).style("opacity", 0); }); // Week Labels with grouped years svg.append("g") .selectAll("g") .data(processedData) .join("g") .attr("transform", (d) => { const midAngle = (x(d.week) + x.bandwidth() / 2) * 180 / Math.PI - 90; // Angle médian const radius = outerRadius + 10; // Position juste en dehors des barres return ` rotate(${midAngle}) translate(${radius},0) `; }) .call((g) => { g.append("text") .text((d, i) => { // Affiche l'année une seule fois pour la première semaine de chaque année if (i === 0 || d.year !== processedData[i - 1].year) { return `${d.year} ${d.week.split("-")[1]}`; // Année et numéro de semaine } return `${d.week.split("-")[1]}`; // Numéro de semaine }) .attr("text-anchor", "middle") .style("font-size", "6px") // Réduction de la taille pour ne pas encombrer .style("fill", "#666"); // Couleur discrète pour les étiquettes }); // Add user label svg.append("text") .attr("text-anchor", "middle") .attr("dy", "0.5em") .style("font-size", "10px") .style("font-weight", "bold") .text(user); // Radial circles const distanceTicks = y.ticks(5); const circleGroup = svg.append("g"); circleGroup.selectAll("circle") .data(distanceTicks) .join("circle") .attr("r", (d) => y(d)) .attr("fill", "none") .attr("stroke", "#ccc") .attr("stroke-dasharray", "4 2"); circleGroup.selectAll("text") .data(distanceTicks) .join("text") .attr("x", 0) .attr("y", (d) => -y(d)) .attr("dy", "-0.3em") .attr("text-anchor", "middle") .style("font-size", "6px") .text((d) => `${d.toFixed(0)} km`); const defs = svg.append("defs"); const gradient = defs.append("linearGradient") .attr("id", "gradient") .attr("x1", "0%") .attr("y1", "0%") .attr("x2", "100%") .attr("y2", "0%"); gradient.append("stop").attr("offset", "0%").attr("stop-color", "lightblue"); gradient.append("stop").attr("offset", "100%").attr("stop-color", "darkblue"); const legendTooltip = d3.select("body").append("div") .attr("class", "tooltip-legend") .style("position", "absolute") .style("padding", "10px") .style("background", "#fff") .style("border", "1px solid #ccc") .style("border-radius", "5px") .style("box-shadow", "0 4px 8px rgba(0, 0, 0, 0.1)") // Ajout d'un effet d'ombre .style("font-size", "12px") // Police ajustée .style("display", "none"); // Extraction des valeurs dynamiques pour Min et Max const sleepMin = d3.min(processedData, (d) => d[sleepKey]); const sleepMax = d3.max(processedData, (d) => d[sleepKey]); // Afficher la légende au survol svg.on("mouseover", (event) => { legendTooltip.style("display", "block") .style("left", `${event.pageX + 10}px`) .style("top", `${event.pageY}px`) .html(` <strong>Durée de sommeil</strong><br> <div style="width: 100px; height: 10px; background: linear-gradient(lightblue, darkblue); margin-top: 5px;"></div> <div style="display: flex; justify-content: space-between; margin-top: 5px;"> <small>${sleepMin.toFixed(1)}h</small> <small>${sleepMax.toFixed(1)}h</small> </div> <div style="width: 100px; height: 10px; background: #ccc; margin-top: 10px;"></div> <small style="display: block; text-align: center; margin-top: 5px;">Valeurs manquantes</small> `); }); // Masquer la légende svg.on("mouseout", () => { legendTooltip.style("display", "none"); }); }); }) .catch((error) => console.error("Error loading data:", error)); function getISOWeekNumber(date) { const tempDate = new Date(date); tempDate.setDate(tempDate.getDate() + 4 - (tempDate.getDay() || 7)); const yearStart = new Date(tempDate.getFullYear(), 0, 1); return Math.ceil(((tempDate - yearStart) / 86400000 + 1) / 7); } } document.addEventListener("DOMContentLoaded", function () { renderStepsVisualization(); renderDistanceVisualization(); renderCaloriesVisualization(); renderSleepVisualization(); renderSleepActivityVisualization(); renderRadialDistanceChart() });