up historique

This commit is contained in:
Tykayn 2025-06-21 11:28:31 +02:00 committed by tykayn
parent ad4170db14
commit c274fd6a63
12 changed files with 448 additions and 616 deletions

View file

@ -54,7 +54,7 @@
{{ stats.name }} - {{ stats.completionPercent }}% complété</h1>
</div>
<div class="col-md-6 col-12">
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 0}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
<button id="openInJOSM" class="btn btn-secondary ms-2">
<i class="bi bi-map"></i> Ouvrir dans JOSM
</button>
@ -83,7 +83,7 @@
{% endif %}
{# Affichage de la fraîcheur des données OSM #}
{% if stats.osmDataDateMin and stats.osmDataDateMax and stats.osmDataDateAvg %}
{# {% if stats.osmDataDateMin and stats.osmDataDateMax and stats.osmDataDateAvg %}
{% set now = "now"|date("U") %}
{% set minDate = stats.osmDataDateMin|date("U") %}
{% set maxDate = stats.osmDataDateMax|date("U") %}
@ -147,7 +147,7 @@
</div>
</div>
</div>
{% endif %}
{% endif %} #}
<div class="row">
<div class="col-md-3 col-12">
@ -249,14 +249,14 @@
<h2>Requête Overpass</h2>
</div>
<div class="card-body">
<pre class="p-4 bg-light">
{{query_places|raw}}
</pre>
<a href="https://overpass-turbo.eu/?Q={{ query_places|url_encode }}" class="btn btn-primary" target="_blank">
<pre class="p-4 bg-light">
{{ overpass_query|raw }}
</pre>
<a href="{{ overpass_query_url }}" class="btn btn-primary" target="_blank">
<i class="bi bi-box-arrow-up-right"></i> Exécuter dans Overpass Turbo
</a>
</div>
</div>
</div>
</div>
<div id="history">
<h2>Historique des {{ statsHistory|length }} stats</h2>
<table class="table table-bordered table-striped table-hover table-responsive js-sort-table">
@ -305,18 +305,36 @@
<div id="completionInfoContent" style="display: none;" class="mt-3">
<p>Le score de complétion est calculé en fonction de plusieurs critères :</p>
<ul>
<li>Nom du commerce (obligatoire)</li>
<li>Nom du commerce</li>
<li>Adresse complète (numéro, rue, code postal)</li>
<li>Horaires d'ouverture</li>
<li>Site web</li>
<li>Numéro de téléphone</li>
<li>Accessibilité PMR</li>
<li>Note descriptive</li>
</ul>
<p>Chaque critère rempli augmente le score de complétion. Un commerce parfaitement renseigné aura un score de 100%.</p>
<p>Chaque critère rempli augmente le score de complétion d'une part égale.
Un commerce parfaitement renseigné aura un score de 100%.</p>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="bi bi-calendar-event"></i> Fréquence des mises à jour (par trimestre)
</div>
<div class="card-body">
<canvas id="modificationsByQuarterChart" style="min-height: 250px; width: 100%;"></canvas>
</div>
</div>
</div>
</div>
<div class="accordion mb-3" id="accordionStats">
<div class="accordion-item">
<h2 class="accordion-header" id="headingOne">
</div>
</div>
<!-- Bouton caché pour JOSM -->
<a id="josmButton" style="display: none;"></a>
@ -326,9 +344,11 @@
{% block javascripts %}
{{ parent() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
<script>
<script type="module">
// Attendre que le DOM et tous les scripts soient chargés
document.addEventListener('DOMContentLoaded', function() {
// Vérifier que Chart.js est disponible
@ -351,22 +371,27 @@
let selectedFeature = null;
// Fonction pour calculer la distribution des taux de complétion
function calculateCompletionDistribution(features) {
function calculateCompletionDistribution(elements) {
const buckets = Array(11).fill(0); // 0-10%, 11-20%, ..., 91-100%
features.forEach(feature => {
const completion = calculateCompletion(feature.properties);
const bucketIndex = Math.min(Math.floor(completion.percentage / 10), 10);
buckets[bucketIndex]++;
elements.forEach(element => {
if (element.tags) {
const completion = calculateCompletion(element.tags);
const bucketIndex = Math.min(Math.floor(completion.percentage / 10), 10);
buckets[bucketIndex]++;
}
});
return buckets;
}
// Fonction pour créer le graphique de complétion
function createCompletionChart(features) {
const ctx = document.getElementById('completionChart').getContext('2d');
const distribution = calculateCompletionDistribution(features);
function createCompletionChart(elements) {
const chartElement = document.getElementById('completionChart');
if (!chartElement) return;
const ctx = chartElement.getContext('2d');
const distribution = calculateCompletionDistribution(elements);
if (completionChart) {
completionChart.destroy();
@ -401,19 +426,19 @@
// Fonction pour charger les lieux depuis l'API Overpass
async function loadPlaces() {
try {
const response = await fetch(`https://overpass-api.de/api/interpreter?data={{query_places|raw}}`);
const data = await response.json();
const response = await fetch(`https://overpass-api.de/api/interpreter?data={{ overpass_query|url_encode|raw }}`);
const geojsonData = await response.json();
if (data.features && data.features.length > 0) {
// Mettre à jour les statistiques
const totallieux = data.features.length;
document.getElementById('totallieux').textContent = totallieux;
if (geojsonData.elements && geojsonData.elements.length > 0) {
const totallieux = geojsonData.elements.length;
const totallieuxElement = document.getElementById('totallieux');
if (totallieuxElement) {
totallieuxElement.textContent = totallieux;
}
// Calculer et afficher la distribution des taux de complétion
createCompletionChart(data.features);
createCompletionChart(geojsonData.elements);
// Mettre à jour les marqueurs sur la carte
dropMarkers = updateMarkers(data.features, map, currentMarkerType, dropMarkers, data);
dropMarkers = updateMarkers(geojsonData.elements, map, currentMarkerType, dropMarkers, geojsonData);
}
} catch (error) {
console.error('Erreur lors du chargement des lieux:', error);
@ -477,6 +502,57 @@
// Charger les lieux au démarrage
loadPlaces();
// Graphique des modifications par trimestre
const modificationsData = JSON.parse('{{ modificationsByQuarter|raw }}');
const quarterLabels = Object.keys(modificationsData);
const quarterValues = Object.values(modificationsData);
console.log('modificationsData', modificationsData);
console.log('quarterLabels', quarterLabels);
console.log('quarterValues', quarterValues);
if (quarterLabels.length > 0) {
const chartElement = document.getElementById('modificationsByQuarterChart');
if (chartElement) {
const ctxQuarter = chartElement.getContext('2d');
new Chart(ctxQuarter, {
type: 'bar',
data: {
labels: quarterLabels,
datasets: [{
label: 'Nombre de modifications',
data: quarterValues,
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
} else {
console.warn('Canvas #modificationsByQuarterChart introuvable dans le DOM');
}
} else {
console.warn('Aucune donnée de trimestre à afficher pour le graphique.');
const chartElement = document.getElementById('modificationsByQuarterChart');
if (chartElement) {
chartElement.parentNode.innerHTML += '<div class="text-muted p-3">Aucune donnée de modification disponible pour cette zone.</div>';
}
}
});
</script>
@ -494,12 +570,12 @@
let contextMenu = null; // Menu contextuel
function calculateCompletion(element) {
let completionCount = 0;
let totalFields = 0;
function calculateCompletion(element) {
let completionCount = 0;
let totalFields = 0;
let missingFields = [];
const fieldsToCheck = [
const fieldsToCheck = [
{ name: 'name', label: 'Nom du commerce' },
{ name: 'contact:street', label: 'Rue' },
{ name: 'contact:housenumber', label: 'Numéro' },
@ -507,12 +583,12 @@ function calculateCompletion(element) {
{ name: 'contact:website', label: 'Site web' },
{ name: 'contact:phone', label: 'Téléphone' },
{ name: 'wheelchair', label: 'Accessibilité PMR' }
];
];
fieldsToCheck.forEach(field => {
totalFields++;
fieldsToCheck.forEach(field => {
totalFields++;
if (element.tags && element.tags[field.name]) {
completionCount++;
completionCount++;
} else {
missingFields.push(field.label);
}
@ -537,10 +613,10 @@ function getCompletionColor(completion) {
}
function createPopupContent(element) {
function createPopupContent(element) {
const completion = calculateCompletion(element);
let content = `
<div class="mb-2">
let content = `
<div class="mb-2">
<h5>${element.tags?.name || 'Sans nom'}</h5>
<div class="d-flex gap-2">
<a class="btn btn-primary btn-sm" href="/admin/placeType/${element.type}/${element.id}">
@ -550,9 +626,9 @@ function createPopupContent(element) {
<i class="bi bi-map"></i> OSM
</a>
</div>
</div>
`;
</div>
`;
if (completion.percentage < 100) {
content += `
<div class="alert alert-warning mt-2">
@ -565,17 +641,17 @@ function createPopupContent(element) {
}
content += '<table class="table table-sm mt-2">';
// Ajouter tous les tags
if (element.tags) {
for (const tag in element.tags) {
content += `<tr><td><strong>${tag}</strong></td><td>${element.tags[tag]}</td></tr>`;
}
}
// Ajouter tous les tags
if (element.tags) {
for (const tag in element.tags) {
content += `<tr><td><strong>${tag}</strong></td><td>${element.tags[tag]}</td></tr>`;
content += '</table>';
return content;
}
}
content += '</table>';
return content;
}
function updateMarkers(features, map, currentMarkerType, dropMarkers, overpassData) {
// Supprimer tous les marqueurs existants
@ -822,7 +898,7 @@ window.updateMarkers = updateMarkers;
const tempLink = document.createElement('a');
tempLink.style.display = 'none';
document.body.appendChild(tempLink);
tempLink.href = josmUrl;
tempLink.click();
document.body.removeChild(tempLink);
@ -908,7 +984,7 @@ window.updateMarkers = updateMarkers;
document.getElementById('openInJOSM').addEventListener('click', openInJOSM);
// Attendre que la carte soit chargée avant d'ajouter les écouteurs d'événements
map.on('load', function() {
map.on('load', function() {
map_is_loaded = true;
// Changer le curseur au survol des marqueurs
map.on('mouseenter', function(e) {
@ -984,6 +1060,7 @@ window.updateMarkers = updateMarkers;
icon.classList.add('bi-chevron-down');
}
}
window.toggleCompletionInfo = toggleCompletionInfo;
// infos depuis complète tes commerces : CTC
@ -1102,7 +1179,6 @@ function makeDonutGraphOfTags() {
labels: {
boxWidth: 15,
padding: 15,
font: {
size: 12
}
}
@ -1115,10 +1191,11 @@ function makeDonutGraphOfTags() {
}
}
}
}
});
}
})
};
makeDonutGraphOfTags();
markClosedSiretsOnTable();
</script>
{% endblock %}
{% endblock %}

View file

@ -117,8 +117,8 @@
<div class="small osm-modification-info">
<div>
{{ commerce.osmDataDate|date('Y-m-d H:i') }}
<i class="bi bi-calendar"></i>
{{ commerce.osmDataDate|date('d/m/Y H:i') }}
</div>
{% if commerce.osmUser %}
<div>

View file

@ -66,7 +66,7 @@
Statistiques des villes (nombre de commerces)
</div>
<div class="card-body">
<canvas id="statsBubbleChart" style="min-height: 400px; width: 100%;"></canvas>
<canvas id="statsBubbleChart" style="min-height: 400px; width: 100%;" data-stats="{{ stats|raw }}"></canvas>
</div>
</div>
</div>
@ -110,7 +110,7 @@
</tr>
</thead>
<tbody>
{% for stat in stats %}
{% for stat in stats_list %}
<tr>
<td><a href="{{ path('app_admin_stats', {'insee_code': stat.zone}) }}" title="Voir les statistiques de cette ville">
{{ stat.name }}
@ -121,8 +121,8 @@
</a></td>
<td>{{ stat.zone }}</td>
<td>{{ stat.completionPercent }}%</td>
<td>{{ stat.placesCount }}</td>
<td>{{ (stat.placesCount / (stat.population or 1 ))|round(2) }}</td>
<td>{{ stat.places|length }}</td>
<td>{{ (stat.places|length / (stat.population or 1 ))|round(2) }}</td>
<td>
<div class="btn-group" role="group">
<a href="{{ path('app_admin_stats', {'insee_code': stat.zone}) }}" class="btn btn-sm btn-primary" title="Voir les statistiques de cette ville">
@ -158,113 +158,5 @@
{% block javascripts %}
{{ parent() }}
{# Les scripts sont maintenant gérés par Webpack Encore via app.js #}
<script>
document.addEventListener('DOMContentLoaded', function() {
const statsDataRaw = [
{% for stat in stats %}
{% if stat.placesCount > 0 and stat.name is not null and stat.population > 0 %}
{
label: '{{ (stat.name ~ " (" ~ stat.zone ~ ")")|e('js') }}',
placesCount: {{ stat.placesCount }},
completion: {{ stat.completionPercent|default(0) }},
x: {{ stat.population }},
y: {{ (stat.placesCount / stat.population * 1000)|round(2) }}
},
{% endif %}
{% endfor %}
];
const ctx = document.getElementById('statsBubbleChart');
if (ctx && statsDataRaw.length > 0) {
const statsData = statsDataRaw.map(d => ({
...d,
r: Math.sqrt(d.placesCount) * 2.5 // Utilise la racine carrée pour la taille, avec un facteur d'échelle
}));
new Chart(ctx.getContext('2d'), {
type: 'bubble',
data: {
datasets: [{
label: 'Commerces par ville',
data: statsData,
backgroundColor: statsData.map(d => `hsla(120, 60%, 70%, ${d.completion / 120 + 0.2})`),
borderColor: 'hsl(120, 60%, 40%)',
borderWidth: 1,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
const data = context.dataset.data[context.dataIndex];
let label = data.label || '';
if (label) {
label += ': ';
}
label += `${data.placesCount} commerces, ${data.y} pour 1000 hab., ${data.completion}% complétion`;
return label;
}
}
}
},
scales: {
x: {
type: 'logarithmic',
title: {
display: true,
text: 'Population (échelle log)'
}
},
y: {
title: {
display: true,
text: 'Commerces pour 1000 habitants'
}
}
}
}
});
}
// La fonction est maintenant globale grâce à l'import dans app.js
if (typeof colorizePercentageCells === 'function') {
colorizePercentageCells('td:nth-child(3)');
}
// Gérer le formulaire de labourage
const labourageForm = document.getElementById('labourerForm');
const citySearchInput = document.getElementById('citySearch');
const selectedZipCodeInput = document.getElementById('selectedZipCode');
const labourageBtn = labourageForm.querySelector('button[type="submit"]');
const originalBtnHtml = labourageBtn.innerHTML;
if (labourageForm && citySearchInput && typeof setupCitySearch === 'function') {
setupCitySearch('citySearch', 'citySuggestions', function (suggestion) {
// Afficher le spinner et désactiver le bouton
labourageBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Chargement...';
labourageBtn.disabled = true;
citySearchInput.disabled = true;
if (suggestion.insee) {
window.location.href = `/admin/labourer/${suggestion.insee}`;
} else if (suggestion.postcode) {
// Moins probable, mais en solution de repli
window.location.href = `/admin/labourer/${suggestion.postcode}`;
}
});
labourageForm.addEventListener('submit', function(e) {
e.preventDefault();
alert("Veuillez rechercher et sélectionner une ville directement dans la liste de suggestions.");
});
}
});
</script>
{# Le script du graphique est maintenant dans assets/app.js #}
{% endblock %}