// 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 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 ratio = population > 0 ? (placesCount / population) * 1000 : 0; const completion = parseInt(stat.completionPercent, 10); return { x: population, y: completion, r: proportional ? Math.sqrt(placesCount) * 2 : 12, label: stat.name, completion: stat.completionPercent || 0, zone: stat.zone }; }); // Trier du plus gros au plus petit rayon data.sort((a, b) => b.r - a.r); return data; } document.addEventListener('DOMContentLoaded', function() { function waitForChartAndDrawBubble() { if (!window.Chart || !window.ChartDataLabels) { setTimeout(waitForChartAndDrawBubble, 50); return; } const chartCanvas = document.getElementById('bubbleChart'); const toggle = document.getElementById('toggleBubbleSize'); 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; } // 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) chartCanvas.width = (parentRect.width); chartCanvas.height = (parentRect.height); chartCanvas.style.width = parentRect.width + 'px'; chartCanvas.style.height = parentRect.height + 'px'; } const bubbleChartData = getBubbleData(proportional); // Calcul de la régression linéaire (moindres carrés) const validPoints = bubbleChartData.filter(d => d.x > 0 && d.y > 0); 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 => { sumX += Math.log10(d.x); sumY += d.y; sumXY += Math.log10(d.x) * d.y; sumXX += Math.log10(d.x) * Math.log10(d.x); }); 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 = [ { x: xMin, y: slope * Math.log10(xMin) + intercept }, { x: xMax, y: slope * Math.log10(xMax) + intercept } ]; } 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)', datalabels: { anchor: 'center', align: 'center', color: '#000', display: true, font: { weight: 'bold' }, formatter: (value, context) => { return context.dataset.data[context.dataIndex].label; } } }, regressionLine ? { label: 'Régression linéaire', type: 'line', data: regressionLine, borderColor: 'rgba(0, 0, 0, 0.7)', borderWidth: 2, pointRadius: 0, fill: false, order: 0, tension: 0, datalabels: { display: false } } : null ].filter(Boolean) }, options: { // responsive: true, plugins: { datalabels: { // Désactivé au niveau global, activé par dataset display: false }, legend: { display: true }, tooltip: { callbacks: { label: (context) => { const d = context.raw; if (context.dataset.type === 'line') { return `Régression: y = ${slope.toFixed(2)} × log10(x) + ${intercept.toFixed(2)}`; } return [ `${d.label}`, `Population: ${d.x.toLocaleString()}`, `Lieux / 1000 hab: ${d.y.toFixed(2)}`, `Total lieux: ${Math.round(Math.pow(d.r / 2, 2))}`, `Complétion: ${d.completion}%` ]; } } } }, scales: { x: { type: 'logarithmic', title: { display: true, text: 'Population (échelle log)' } }, y: { title: { display: true, text: 'Completion' } } } } }); // 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; } } }; } // Initial draw console.log('[bubble chart] Initialisation avec taille proportionnelle ?', toggle.checked); drawBubbleChart(toggle && toggle.checked); // Listener toggle.addEventListener('change', function() { console.log('[bubble chart] Toggle changé, taille proportionnelle ?', toggle.checked); drawBubbleChart(toggle.checked); }); } waitForChartAndDrawBubble(); });