export 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]); }); }