| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  | // Bubble chart du dashboard avec option de taille de bulle proportionnelle ou égale
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | let bubbleChart = null; // Déclaré en dehors pour garder la référence
 | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  | function waitForChartAndDrawBubble() { | 
					
						
							|  |  |  |  |     if (!window.Chart || !window.ChartDataLabels) { | 
					
						
							|  |  |  |  |         setTimeout(waitForChartAndDrawBubble, 50); | 
					
						
							|  |  |  |  |         return; | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |     const chartCanvas = document.getElementById('bubbleChart'); | 
					
						
							|  |  |  |  |     const toggle = document.getElementById('toggleBubbleSize'); | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |      | 
					
						
							|  |  |  |  |      | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |     function drawBubbleChart(proportional) { | 
					
						
							|  |  |  |  |         // Détruire toute instance Chart.js existante sur ce canvas (Chart.js v3+)
 | 
					
						
							|  |  |  |  |         const existing = window.Chart.getChart(chartCanvas); | 
					
						
							|  |  |  |  |         if (existing) { | 
					
						
							|  |  |  |  |             try { existing.destroy(); } catch (e) { console.warn('Erreur destroy Chart:', e); } | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |         if (bubbleChart) { | 
					
						
							|  |  |  |  |             try { bubbleChart.destroy(); } catch (e) { console.warn('Erreur destroy Chart:', e); } | 
					
						
							|  |  |  |  |             bubbleChart = null; | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |         // Forcer le canvas à occuper toute la largeur/hauteur du conteneur en pixels
 | 
					
						
							|  |  |  |  |     if (chartCanvas && chartCanvas.parentElement) { | 
					
						
							|  |  |  |  |         const parentRect = chartCanvas.parentElement.getBoundingClientRect(); | 
					
						
							|  |  |  |  |         console.log('parentRect', parentRect) | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |         chartCanvas.width = (parentRect.width); | 
					
						
							|  |  |  |  |         chartCanvas.height = (parentRect.height); | 
					
						
							|  |  |  |  |         chartCanvas.style.width = parentRect.width + 'px'; | 
					
						
							|  |  |  |  |         chartCanvas.style.height = parentRect.height + 'px'; | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |     if(!getBubbleData){ | 
					
						
							|  |  |  |  |         console.log('pas de getBubbleData') | 
					
						
							|  |  |  |  |         return ;  | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |         const bubbleChartData = getBubbleData(proportional); | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |     if(!bubbleChartData){ | 
					
						
							|  |  |  |  |         console.log('pas de bubbleChartData') | 
					
						
							|  |  |  |  |         return ;  | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |         // Calcul de la régression linéaire (moindres carrés)
 | 
					
						
							| 
									
										
										
										
											2025-06-24 13:16:48 +02:00
										 |  |  |  |         // On ne fait la régression que si on veut, mais l'axe X = fraicheur, Y = complétion
 | 
					
						
							|  |  |  |  |         const validPoints = bubbleChartData.filter(d => d.x !== null && d.y !== null); | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |         const n = validPoints.length; | 
					
						
							|  |  |  |  |         let regressionLine = null, slope = 0, intercept = 0; | 
					
						
							|  |  |  |  |         if (n >= 2) { | 
					
						
							|  |  |  |  |             let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0; | 
					
						
							|  |  |  |  |             validPoints.forEach(d => { | 
					
						
							| 
									
										
										
										
											2025-06-24 13:16:48 +02:00
										 |  |  |  |                 sumX += d.x; | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |                 sumY += d.y; | 
					
						
							| 
									
										
										
										
											2025-06-24 13:16:48 +02:00
										 |  |  |  |                 sumXY += d.x * d.y; | 
					
						
							|  |  |  |  |                 sumXX += d.x * d.x; | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |             }); | 
					
						
							|  |  |  |  |             const meanX = sumX / n; | 
					
						
							|  |  |  |  |             const meanY = sumY / n; | 
					
						
							|  |  |  |  |             slope = (sumXY - n * meanX * meanY) / (sumXX - n * meanX * meanX); | 
					
						
							|  |  |  |  |             intercept = meanY - slope * meanX; | 
					
						
							|  |  |  |  |             const xMin = Math.min(...validPoints.map(d => d.x)); | 
					
						
							|  |  |  |  |             const xMax = Math.max(...validPoints.map(d => d.x)); | 
					
						
							|  |  |  |  |             regressionLine = [ | 
					
						
							| 
									
										
										
										
											2025-06-24 13:16:48 +02:00
										 |  |  |  |                 { x: xMin, y: slope * xMin + intercept }, | 
					
						
							|  |  |  |  |                 { x: xMax, y: slope * xMax + intercept } | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |             ]; | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |         window.Chart.register(window.ChartDataLabels); | 
					
						
							|  |  |  |  |         bubbleChart = new window.Chart(chartCanvas.getContext('2d'), { | 
					
						
							|  |  |  |  |             type: 'bubble', | 
					
						
							|  |  |  |  |             data: { | 
					
						
							|  |  |  |  |                 datasets: [ | 
					
						
							|  |  |  |  |                     { | 
					
						
							|  |  |  |  |                         label: 'Villes', | 
					
						
							|  |  |  |  |                         data: bubbleChartData, | 
					
						
							|  |  |  |  |                         backgroundColor: bubbleChartData.map(d => `rgba(94, 255, 121, ${d.completion / 100})`), | 
					
						
							|  |  |  |  |                         borderColor: 'rgb(94, 255, 121)', | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  |                         datalabels: { | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |                             anchor: 'center', | 
					
						
							|  |  |  |  |                             align: 'center', | 
					
						
							|  |  |  |  |                             color: '#000', | 
					
						
							|  |  |  |  |                             display: true, | 
					
						
							|  |  |  |  |                             font: { weight: '400', size : "12px" }, | 
					
						
							|  |  |  |  |                             formatter: (value, context) => { | 
					
						
							|  |  |  |  |                                 return context.dataset.data[context.dataIndex].label; | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  |                             } | 
					
						
							|  |  |  |  |                         } | 
					
						
							|  |  |  |  |                     }, | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |                     regressionLine ? { | 
					
						
							|  |  |  |  |                         label: 'Régression linéaire', | 
					
						
							|  |  |  |  |                         type: 'line', | 
					
						
							|  |  |  |  |                         data: regressionLine, | 
					
						
							|  |  |  |  |                         borderColor: 'rgba(95, 168, 0, 0.7)', | 
					
						
							|  |  |  |  |                         borderWidth: 2, | 
					
						
							|  |  |  |  |                         pointRadius: 0, | 
					
						
							|  |  |  |  |                         fill: false, | 
					
						
							|  |  |  |  |                         order: 0, | 
					
						
							|  |  |  |  |                         tension: 0, | 
					
						
							|  |  |  |  |                         datalabels: { display: false } | 
					
						
							|  |  |  |  |                     } : null | 
					
						
							|  |  |  |  |                 ].filter(Boolean) | 
					
						
							|  |  |  |  |             }, | 
					
						
							|  |  |  |  |             options: { | 
					
						
							|  |  |  |  |                 plugins: { | 
					
						
							|  |  |  |  |                     datalabels: { | 
					
						
							|  |  |  |  |                         display: false | 
					
						
							|  |  |  |  |                     }, | 
					
						
							|  |  |  |  |                     legend: { display: true }, | 
					
						
							|  |  |  |  |                     tooltip: { | 
					
						
							|  |  |  |  |                         callbacks: { | 
					
						
							|  |  |  |  |                             label: (context) => { | 
					
						
							|  |  |  |  |                                 const d = context.raw; | 
					
						
							|  |  |  |  |                                 if (context.dataset.type === 'line') { | 
					
						
							| 
									
										
										
										
											2025-06-24 13:16:48 +02:00
										 |  |  |  |                                     return `Régression: y = ${slope.toFixed(2)} × x + ${intercept.toFixed(2)}`; | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |                                 } | 
					
						
							|  |  |  |  |                                 return [ | 
					
						
							|  |  |  |  |                                     `${d.label}`, | 
					
						
							| 
									
										
										
										
											2025-06-24 12:30:39 +02:00
										 |  |  |  |                                     `Fraîcheur moyenne: ${d.freshnessDays ? d.freshnessDays.toLocaleString() + ' jours' : 'N/A'}`, | 
					
						
							| 
									
										
										
										
											2025-06-24 13:16:48 +02:00
										 |  |  |  |                                     `Complétion: ${d.y.toFixed(2)}%`, | 
					
						
							|  |  |  |  |                                     `Population: ${d.population ? d.population.toLocaleString() : 'N/A'}`, | 
					
						
							|  |  |  |  |                                     `Nombre de lieux: ${d.r.toFixed(2)}`, | 
					
						
							| 
									
										
										
										
											2025-06-24 12:30:39 +02:00
										 |  |  |  |                                     `Budget: ${d.budget ? d.budget.toLocaleString() + ' €' : 'N/A'}`, | 
					
						
							|  |  |  |  |                                     `Budget/habitant: ${d.budgetParHabitant ? d.budgetParHabitant.toFixed(2) + ' €' : 'N/A'}`, | 
					
						
							|  |  |  |  |                                     `Budget/lieu: ${d.budgetParLieu ? d.budgetParLieu.toFixed(2) + ' €' : 'N/A'}` | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |                                 ]; | 
					
						
							|  |  |  |  |                             } | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  |                         } | 
					
						
							|  |  |  |  |                     } | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |                 }, | 
					
						
							|  |  |  |  |                 scales: { | 
					
						
							|  |  |  |  |                     x: { | 
					
						
							| 
									
										
										
										
											2025-06-24 13:16:48 +02:00
										 |  |  |  |                         type: 'linear', | 
					
						
							|  |  |  |  |                         title: { display: true, text: 'Fraîcheur moyenne (jours, plus petit = plus récent)' } | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |                     }, | 
					
						
							|  |  |  |  |                     y: { | 
					
						
							| 
									
										
										
										
											2025-06-24 13:16:48 +02:00
										 |  |  |  |                         title: { display: true, text: 'Taux de complétion (%)' }, | 
					
						
							|  |  |  |  |                         min: 0, | 
					
						
							|  |  |  |  |                         max: 100 | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  |                     } | 
					
						
							|  |  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |             } | 
					
						
							|  |  |  |  |         }); | 
					
						
							|  |  |  |  |         // Ajout du clic sur une bulle
 | 
					
						
							|  |  |  |  |         chartCanvas.onclick = function(evt) { | 
					
						
							|  |  |  |  |             const points = bubbleChart.getElementsAtEventForMode(evt, 'nearest', { intersect: true }, true); | 
					
						
							|  |  |  |  |             if (points.length > 0) { | 
					
						
							|  |  |  |  |                 const firstPoint = points[0]; | 
					
						
							|  |  |  |  |                 const dataIndex = firstPoint.index; | 
					
						
							|  |  |  |  |                 const stat = window.statsDataForBubble[dataIndex]; | 
					
						
							|  |  |  |  |                 if (stat && stat.zone) { | 
					
						
							|  |  |  |  |                     window.location.href = '/admin/stats/' + stat.zone; | 
					
						
							|  |  |  |  |                 } | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  |         }; | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |     // Initial draw
 | 
					
						
							|  |  |  |  |     console.log('[bubble chart] Initialisation avec taille proportionnelle ?', toggle?.checked); | 
					
						
							|  |  |  |  |     if(drawBubbleChart){ | 
					
						
							| 
									
										
										
										
											2025-06-23 00:47:49 +02:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |         drawBubbleChart(toggle && toggle.checked); | 
					
						
							|  |  |  |  |         // Listener
 | 
					
						
							|  |  |  |  |         toggle?.addEventListener('change', function() { | 
					
						
							|  |  |  |  |             console.log('[bubble chart] Toggle changé, taille proportionnelle ?', toggle?.checked); | 
					
						
							|  |  |  |  |             drawBubbleChart(toggle?.checked); | 
					
						
							|  |  |  |  |         }); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | function getBubbleData(proportional) { | 
					
						
							|  |  |  |  |     // Générer les données puis trier par rayon décroissant
 | 
					
						
							|  |  |  |  |     const data = window.statsDataForBubble?.map(stat => { | 
					
						
							|  |  |  |  |         const population = parseInt(stat.population, 10); | 
					
						
							|  |  |  |  |         const placesCount = parseInt(stat.placesCount, 10); | 
					
						
							|  |  |  |  |         const completion =  parseInt(stat.completionPercent, 10); | 
					
						
							| 
									
										
										
										
											2025-06-24 12:30:39 +02:00
										 |  |  |  |         // Fraîcheur moyenne : âge moyen en jours (plus récent à droite)
 | 
					
						
							|  |  |  |  |         let freshnessDays = null; | 
					
						
							|  |  |  |  |         if (stat.osmDataDateAvg) { | 
					
						
							|  |  |  |  |             const now = new Date(); | 
					
						
							|  |  |  |  |             const avgDate = new Date(stat.osmDataDateAvg); | 
					
						
							|  |  |  |  |             freshnessDays = Math.round((now - avgDate) / (1000 * 60 * 60 * 24)); | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |         // Pour l'axe X, on veut que les plus récents soient à droite (donc X = -freshnessDays)
 | 
					
						
							|  |  |  |  |         const x = freshnessDays !== null ? -freshnessDays : 0; | 
					
						
							|  |  |  |  |         // Budget
 | 
					
						
							|  |  |  |  |         const budget = stat.budgetAnnuel ? parseFloat(stat.budgetAnnuel) : null; | 
					
						
							|  |  |  |  |         const budgetParHabitant = (budget && population) ? budget / population : null; | 
					
						
							|  |  |  |  |         const budgetParLieu = (budget && placesCount) ? budget / placesCount : null; | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |         return { | 
					
						
							| 
									
										
										
										
											2025-06-24 12:30:39 +02:00
										 |  |  |  |             x: x, | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |             y: completion, | 
					
						
							|  |  |  |  |             r: proportional ? Math.sqrt(placesCount) * 2 : 12, | 
					
						
							|  |  |  |  |             label: stat.name, | 
					
						
							|  |  |  |  |             completion: stat.completionPercent || 0, | 
					
						
							| 
									
										
										
										
											2025-06-24 12:30:39 +02:00
										 |  |  |  |             zone: stat.zone, | 
					
						
							|  |  |  |  |             budget, | 
					
						
							|  |  |  |  |             budgetParHabitant, | 
					
						
							|  |  |  |  |             budgetParLieu, | 
					
						
							|  |  |  |  |             population, | 
					
						
							|  |  |  |  |             placesCount, | 
					
						
							|  |  |  |  |             freshnessDays | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |         }; | 
					
						
							|  |  |  |  |     }); | 
					
						
							|  |  |  |  |     // Trier du plus gros au plus petit rayon
 | 
					
						
							|  |  |  |  |     if(data){ | 
					
						
							|  |  |  |  |         data.sort((a, b) => b.r - a.r); | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-06-23 23:36:50 +02:00
										 |  |  |  |     return data; | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | document.addEventListener('DOMContentLoaded', function() { | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  |     waitForChartAndDrawBubble(); | 
					
						
							| 
									
										
										
										
											2025-06-24 12:30:39 +02:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |     // Forcer deleteMissing=1 sur le formulaire de labourage
 | 
					
						
							|  |  |  |  |     const labourerForm = document.getElementById('labourerForm'); | 
					
						
							|  |  |  |  |     if (labourerForm) { | 
					
						
							|  |  |  |  |         labourerForm.addEventListener('submit', function(e) { | 
					
						
							|  |  |  |  |             e.preventDefault(); | 
					
						
							|  |  |  |  |             const zipCode = document.getElementById('selectedZipCode').value; | 
					
						
							|  |  |  |  |             if (zipCode) { | 
					
						
							|  |  |  |  |                 window.location.href = '/admin/labourer/' + zipCode + '?deleteMissing=1'; | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  |         }); | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-06-21 18:37:31 +02:00
										 |  |  |  | });  |