refacto js, up graph

This commit is contained in:
Tykayn 2025-06-21 18:37:31 +02:00 committed by tykayn
parent d66bc5e40c
commit 68680e0569
11 changed files with 840 additions and 48 deletions

View file

@ -15,10 +15,14 @@ import './utils.js';
import './opening_hours.js';
import './josm.js';
import './edit.js';
import './table-map-toggles.js';
import './stats-charts.js';
import './dashboard-charts.js';
import Chart from 'chart.js/auto';
import ChartDataLabels from 'chartjs-plugin-datalabels';
import { genererCouleurPastel, setupCitySearch, handleAddCityFormSubmit, enableLabourageForm, getLabourerUrl, adjustListGroupFontSize } from './utils.js';
import maplibregl from 'maplibre-gl';
import { genererCouleurPastel, setupCitySearch, handleAddCityFormSubmit, enableLabourageForm, getLabourerUrl, adjustListGroupFontSize, toggleCompletionInfo } from './utils.js';
import Tablesort from 'tablesort';
window.Chart = Chart;
@ -26,6 +30,9 @@ window.genererCouleurPastel = genererCouleurPastel;
window.setupCitySearch = setupCitySearch;
window.handleAddCityFormSubmit = handleAddCityFormSubmit;
window.getLabourerUrl = getLabourerUrl;
window.ChartDataLabels = ChartDataLabels;
window.maplibregl = maplibregl;
window.toggleCompletionInfo = toggleCompletionInfo;
Chart.register(ChartDataLabels);
@ -95,13 +102,13 @@ document.addEventListener('DOMContentLoaded', () => {
});
// Focus sur le premier champ texte au chargement
const firstTextInput = document.querySelector('input.form-control');
if (firstTextInput) {
firstTextInput.focus();
console.log('focus sur le premier champ texte', firstTextInput);
} else {
console.log('pas de champ texte trouvé');
}
// const firstTextInput = document.querySelector('input.form-control');
// if (firstTextInput) {
// firstTextInput.focus();
// console.log('focus sur le premier champ texte', firstTextInput);
// } else {
// console.log('pas de champ texte trouvé');
// }
@ -179,4 +186,4 @@ document.addEventListener('DOMContentLoaded', () => {
enableLabourageForm();
adjustListGroupFontSize('.list-group-item');
});

180
assets/dashboard-charts.js Normal file
View file

@ -0,0 +1,180 @@
// 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;
return {
x: population,
y: ratio,
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: 'Commerces par habitant' }
}
}
}
});
// 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();
});

44
assets/stats-charts.js Normal file
View file

@ -0,0 +1,44 @@
// Affichage du graphique de fréquence des mises à jour par trimestre sur la page stats d'une ville
document.addEventListener('DOMContentLoaded', function() {
function drawModificationsByQuarterChart() {
if (!window.Chart) {
setTimeout(drawModificationsByQuarterChart, 50);
return;
}
if (typeof window.modificationsByQuarter === 'undefined') return;
const modifData = window.modificationsByQuarter;
const modifLabels = Object.keys(modifData);
const modifCounts = Object.values(modifData);
const modifCanvas = document.getElementById('modificationsByQuarterChart');
if (modifCanvas && modifLabels.length > 0) {
new window.Chart(modifCanvas.getContext('2d'), {
type: 'bar',
data: {
labels: modifLabels,
datasets: [{
label: 'Nombre de lieux modifiés',
data: modifCounts,
backgroundColor: 'rgba(54, 162, 235, 0.7)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
title: { display: true, text: 'Fréquence des mises à jour par trimestre' }
},
scales: {
y: { beginAtZero: true, title: { display: true, text: 'Nombre de lieux' } },
x: { title: { display: true, text: 'Trimestre' } }
}
}
});
} else if (modifCanvas) {
modifCanvas.parentNode.innerHTML = '<div class="alert alert-info">Aucune donnée de modification disponible pour cette ville.</div>';
}
}
drawModificationsByQuarterChart();
});

View file

@ -0,0 +1,23 @@
// Gestion du tri des tableaux
import Tablesort from 'tablesort';
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.js-sort-table').forEach(table => {
new Tablesort(table);
});
// Gestion du toggle gouttes/ronds sur la carte
const toggle = document.getElementById('toggleMarkers');
if (toggle && window.updateMarkers) {
toggle.addEventListener('change', (e) => {
window.updateMarkers(e.target.value);
});
}
});
// Exposer une fonction pour (ré)appliquer le tri si besoin
export function applyTableSort() {
document.querySelectorAll('.js-sort-table').forEach(table => {
new Tablesort(table);
});
}

View file

@ -338,9 +338,23 @@ export function calculateCompletion(properties) {
};
}
export function toggleCompletionInfo() {
const content = document.getElementById('completionInfoContent');
const icon = document.getElementById('completionInfoIcon');
if (content) {
const isVisible = content.style.display === 'block';
content.style.display = isVisible ? 'none' : 'block';
if (icon) {
icon.classList.toggle('bi-chevron-down', isVisible);
icon.classList.toggle('bi-chevron-up', !isVisible);
}
}
}
window.check_validity = check_validity;
window.colorHeadingTable = colorHeadingTable;
window.openInPanoramax = openInPanoramax;
window.listChangesets = listChangesets;
window.adjustListGroupFontSize = adjustListGroupFontSize;
window.calculateCompletion = calculateCompletion;
window.calculateCompletion = calculateCompletion;
window.toggleCompletionInfo = toggleCompletionInfo;