osm-labo/templates/admin/followup_detailed_stats.html.twig

720 lines
33 KiB
Twig
Raw Normal View History

2025-11-23 15:48:29 +01:00
{% extends 'base.html.twig' %}
{% block title %}Stats détaillées - {{ stats.name }}{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link href='{{ asset('css/city-sidebar.css') }}' rel='stylesheet'/>
{% endblock %}
{% block body %}
<div class="container-fluid">
<div class="row">
<!-- Sidebar de navigation -->
<div class="col-12">
{% include 'admin/_city_sidebar.html.twig' with {'stats': stats, 'active_menu': 'followup_detailed_stats'} %}
</div>
<!-- Contenu principal -->
<div class="col-md-9 col-lg-10 main-content">
<div class="p-4">
<h1>Stats détaillées - {{ stats.name }} ({{ stats.zone }})</h1>
<p class="text-muted">Sélectionnez les thématiques à afficher pour comparer les courbes de décompte et complétion.</p>
<!-- Sélection des thématiques -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-check2-square"></i> Sélection des thématiques</h5>
</div>
<div class="card-body">
<div class="row">
{% for theme, label in followup_labels %}
<div class="col-md-4 col-lg-3 mb-2">
<div class="form-check">
<input class="form-check-input theme-checkbox" type="checkbox"
value="{{ theme }}" id="theme_{{ theme }}"
data-label="{{ label }}"
data-icon="{{ followup_icons[theme]|default('bi-question-circle') }}">
<label class="form-check-label" for="theme_{{ theme }}">
<i class="bi {{ followup_icons[theme]|default('bi-question-circle') }}"></i> {{ label }}
</label>
</div>
</div>
{% endfor %}
</div>
<div class="mt-3">
<button type="button" class="btn btn-sm btn-outline-primary" id="selectAll">Tout sélectionner</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="deselectAll">Tout désélectionner</button>
</div>
</div>
</div>
<!-- Options d'affichage -->
<div class="card mb-3">
<div class="card-body">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="showCount" checked>
<label class="form-check-label" for="showCount">
Afficher les décomptes
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="showCompletion" checked>
<label class="form-check-label" for="showCompletion">
Afficher les complétions
</label>
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="separateCharts" checked>
<label class="form-check-label" for="separateCharts">
Afficher deux graphiques séparés (décomptes et complétions)
</label>
</div>
<small class="text-muted">Décochez pour afficher un seul graphique combiné avec deux axes Y</small>
</div>
</div>
<!-- Graphique combiné (un seul) -->
<div class="card mb-4" id="combinedChartCard" style="display: none;">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-graph-up"></i> Graphique combiné</h5>
</div>
<div class="card-body">
<div id="combinedChartContainer">
<canvas id="combinedChart" style="max-height: 500px;"></canvas>
</div>
</div>
</div>
<!-- Graphique des décomptes -->
<div class="card mb-4" id="countChartCard">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-bar-chart"></i> Graphique des décomptes</h5>
</div>
<div class="card-body">
<div id="countChartContainer">
<canvas id="countChart" style="max-height: 500px;"></canvas>
</div>
</div>
</div>
<!-- Graphique des complétions -->
<div class="card" id="completionChartCard">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-percent"></i> Graphique des complétions (%)</h5>
</div>
<div class="card-body">
<div id="completionChartContainer">
<canvas id="completionChart" style="max-height: 500px;"></canvas>
</div>
</div>
</div>
<div class="mt-3">
<a href="{{ path('app_admin_stats', {'insee_code': stats.zone}) }}"
class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Retour à la fiche ville</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="/js/chartjs/chart.umd.js"></script>
<script src="/js/chartjs/chartjs-adapter-date-fns.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
<script>
const inseeCode = '{{ stats.zone }}';
const apiUrl = '{{ path('api_stats_followup_by_insee', {'insee': stats.zone}) }}';
let countChartInstance = null;
let completionChartInstance = null;
let combinedChartInstance = null;
let allData = null;
// Couleurs pour les différentes thématiques
const colors = [
{ count: 'rgba(54, 162, 235, 1)', completion: 'rgba(75, 192, 192, 1)' },
{ count: 'rgba(255, 99, 132, 1)', completion: 'rgba(255, 159, 64, 1)' },
{ count: 'rgba(153, 102, 255, 1)', completion: 'rgba(201, 203, 207, 1)' },
{ count: 'rgba(255, 205, 86, 1)', completion: 'rgba(54, 162, 235, 0.5)' },
{ count: 'rgba(75, 192, 192, 1)', completion: 'rgba(255, 99, 132, 0.5)' },
{ count: 'rgba(255, 159, 64, 1)', completion: 'rgba(153, 102, 255, 0.5)' },
{ count: 'rgba(201, 203, 207, 1)', completion: 'rgba(255, 205, 86, 0.5)' },
{ count: 'rgba(255, 99, 132, 0.5)', completion: 'rgba(75, 192, 192, 0.5)' },
];
// Charger les données depuis l'API
async function loadData() {
try {
console.log('Chargement des données depuis:', apiUrl);
const response = await fetch(apiUrl);
if (!response.ok) {
const errorText = await response.text();
console.error('Erreur HTTP:', response.status, errorText);
throw new Error(`Erreur ${response.status}: ${errorText}`);
}
allData = await response.json();
console.log('Données chargées:', allData);
updateChart();
} catch (error) {
console.error('Erreur:', error);
const countContainer = document.getElementById('countChartContainer');
const completionContainer = document.getElementById('completionChartContainer');
if (countContainer) {
countContainer.innerHTML = '<p class="text-danger">Erreur lors du chargement des données: ' + error.message + '</p>';
}
if (completionContainer) {
completionContainer.innerHTML = '<p class="text-danger">Erreur lors du chargement des données: ' + error.message + '</p>';
}
}
}
// Mettre à jour les graphiques
function updateChart() {
if (!allData) {
console.warn('Aucune donnée chargée');
return;
}
const showCount = document.getElementById('showCount').checked;
const showCompletion = document.getElementById('showCompletion').checked;
const separateCharts = document.getElementById('separateCharts').checked;
const selectedThemes = Array.from(document.querySelectorAll('.theme-checkbox:checked'))
.map(cb => cb.value);
// Afficher/masquer les conteneurs selon le mode
const combinedCard = document.getElementById('combinedChartCard');
const countCard = document.getElementById('countChartCard');
const completionCard = document.getElementById('completionChartCard');
if (separateCharts) {
combinedCard.style.display = 'none';
countCard.style.display = showCount ? 'block' : 'none';
completionCard.style.display = showCompletion ? 'block' : 'none';
} else {
combinedCard.style.display = (showCount || showCompletion) ? 'block' : 'none';
countCard.style.display = 'none';
completionCard.style.display = 'none';
}
if (selectedThemes.length === 0) {
// Détruire les graphiques existants
if (countChartInstance) {
countChartInstance.destroy();
countChartInstance = null;
}
if (completionChartInstance) {
completionChartInstance.destroy();
completionChartInstance = null;
}
if (combinedChartInstance) {
combinedChartInstance.destroy();
combinedChartInstance = null;
}
document.getElementById('countChartContainer').innerHTML = '<p class="text-muted">Sélectionnez au moins une thématique pour afficher le graphique.</p>';
document.getElementById('completionChartContainer').innerHTML = '<p class="text-muted">Sélectionnez au moins une thématique pour afficher le graphique.</p>';
document.getElementById('combinedChartContainer').innerHTML = '<p class="text-muted">Sélectionnez au moins une thématique pour afficher le graphique.</p>';
return;
}
// Préparer les données pour les décomptes
const countDatasets = [];
const completionDatasets = [];
let colorIndex = 0;
selectedThemes.forEach(theme => {
const themeData = allData.themes[theme];
if (!themeData) {
console.warn(`Pas de données pour le thème: ${theme}`);
return;
}
const checkbox = document.getElementById('theme_' + theme);
const label = checkbox ? checkbox.dataset.label : theme;
const color = colors[colorIndex % colors.length];
// Graphique des décomptes (seulement si showCount est coché)
if (showCount && themeData.count && themeData.count.length > 0) {
countDatasets.push({
label: label,
data: themeData.count.map(d => ({ x: d.date, y: d.value })),
borderColor: color.count,
backgroundColor: color.count.replace('1)', '0.1)'),
fill: false,
tension: 0.3,
});
}
// Graphique des complétions (seulement si showCompletion est coché)
if (showCompletion && themeData.completion && themeData.completion.length > 0) {
completionDatasets.push({
label: label,
data: themeData.completion.map(d => ({ x: d.date, y: d.value })),
borderColor: color.completion,
backgroundColor: color.completion.replace('1)', '0.1)'),
fill: false,
tension: 0.3,
});
}
colorIndex++;
});
if (separateCharts) {
// Créer/mettre à jour les graphiques séparés selon les cases cochées
if (showCount) {
updateCountChart(countDatasets);
} else {
if (countChartInstance) {
countChartInstance.destroy();
countChartInstance = null;
}
}
if (showCompletion) {
updateCompletionChart(completionDatasets);
} else {
if (completionChartInstance) {
completionChartInstance.destroy();
completionChartInstance = null;
}
}
// Détruire le graphique combiné s'il existe
if (combinedChartInstance) {
combinedChartInstance.destroy();
combinedChartInstance = null;
}
} else {
// Créer/mettre à jour le graphique combiné
if (showCount || showCompletion) {
updateCombinedChart(countDatasets, completionDatasets);
} else {
if (combinedChartInstance) {
combinedChartInstance.destroy();
combinedChartInstance = null;
}
}
// Détruire les graphiques séparés s'ils existent
if (countChartInstance) {
countChartInstance.destroy();
countChartInstance = null;
}
if (completionChartInstance) {
completionChartInstance.destroy();
completionChartInstance = null;
}
}
}
// Mettre à jour le graphique des décomptes
function updateCountChart(datasets) {
const container = document.getElementById('countChartContainer');
if (!container) return;
if (datasets.length === 0) {
if (countChartInstance) {
countChartInstance.destroy();
countChartInstance = null;
}
container.innerHTML = '<p class="text-muted">Aucune donnée de décompte disponible pour les thématiques sélectionnées.</p>';
return;
}
// Restaurer le canvas si nécessaire
if (container.innerHTML.includes('<p')) {
container.innerHTML = '<canvas id="countChart" style="max-height: 500px;"></canvas>';
}
const canvas = document.getElementById('countChart');
if (!canvas) {
console.error('Impossible de trouver le canvas des décomptes');
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Impossible d\'obtenir le contexte 2D pour les décomptes');
return;
}
if (countChartInstance) {
countChartInstance.destroy();
}
try {
countChartInstance = new Chart(ctx, {
type: 'line',
data: { datasets },
options: {
responsive: true,
maintainAspectRatio: true,
parsing: {
xAxisKey: 'x',
yAxisKey: 'y'
},
plugins: {
legend: {
display: true,
position: 'top',
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
title: function(context) {
if (context && context.length > 0 && context[0].parsed && context[0].parsed.x) {
return new Date(context[0].parsed.x).toLocaleDateString('fr-FR');
}
return '';
}
}
}
},
scales: {
x: {
type: 'time',
time: {
unit: 'day',
displayFormats: {
day: 'dd/MM/yyyy'
}
},
title: {
display: true,
text: 'Date'
}
},
y: {
type: 'linear',
position: 'left',
title: {
display: true,
text: "Nombre d'objets"
},
beginAtZero: true
}
}
}
});
console.log('Graphique des décomptes créé avec succès', datasets.length, 'datasets');
} catch (error) {
console.error('Erreur lors de la création du graphique des décomptes:', error);
}
}
// Mettre à jour le graphique des complétions
function updateCompletionChart(datasets) {
const container = document.getElementById('completionChartContainer');
if (!container) return;
if (datasets.length === 0) {
if (completionChartInstance) {
completionChartInstance.destroy();
completionChartInstance = null;
}
container.innerHTML = '<p class="text-muted">Aucune donnée de complétion disponible pour les thématiques sélectionnées.</p>';
return;
}
// Restaurer le canvas si nécessaire
if (container.innerHTML.includes('<p')) {
container.innerHTML = '<canvas id="completionChart" style="max-height: 500px;"></canvas>';
}
const canvas = document.getElementById('completionChart');
if (!canvas) {
console.error('Impossible de trouver le canvas des complétions');
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Impossible d\'obtenir le contexte 2D pour les complétions');
return;
}
if (completionChartInstance) {
completionChartInstance.destroy();
}
try {
completionChartInstance = new Chart(ctx, {
type: 'line',
data: { datasets },
options: {
responsive: true,
maintainAspectRatio: true,
parsing: {
xAxisKey: 'x',
yAxisKey: 'y'
},
plugins: {
legend: {
display: true,
position: 'top',
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
title: function(context) {
if (context && context.length > 0 && context[0].parsed && context[0].parsed.x) {
return new Date(context[0].parsed.x).toLocaleDateString('fr-FR');
}
return '';
},
label: function(context) {
return context.dataset.label + ': ' + context.parsed.y.toFixed(1) + '%';
}
}
}
},
scales: {
x: {
type: 'time',
time: {
unit: 'day',
displayFormats: {
day: 'dd/MM/yyyy'
}
},
title: {
display: true,
text: 'Date'
}
},
y: {
type: 'linear',
position: 'left',
title: {
display: true,
text: 'Complétion (%)'
},
min: 0,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
}
}
});
console.log('Graphique des complétions créé avec succès', datasets.length, 'datasets');
} catch (error) {
console.error('Erreur lors de la création du graphique des complétions:', error);
}
}
// Mettre à jour le graphique combiné
function updateCombinedChart(countDatasets, completionDatasets) {
const container = document.getElementById('combinedChartContainer');
if (!container) return;
const showCount = document.getElementById('showCount').checked;
const showCompletion = document.getElementById('showCompletion').checked;
// Combiner les datasets
const allDatasets = [];
let colorIndex = 0;
// Ajouter les datasets de décompte (seulement si showCount est coché)
if (showCount) {
countDatasets.forEach(dataset => {
allDatasets.push({
...dataset,
borderColor: colors[colorIndex % colors.length].count,
backgroundColor: colors[colorIndex % colors.length].count.replace('1)', '0.1)'),
yAxisID: 'y',
});
colorIndex++;
});
}
// Réinitialiser l'index pour les complétions si on a des décomptes
if (!showCount) {
colorIndex = 0;
}
// Ajouter les datasets de complétion (seulement si showCompletion est coché)
if (showCompletion) {
completionDatasets.forEach(dataset => {
allDatasets.push({
...dataset,
borderColor: colors[colorIndex % colors.length].completion,
backgroundColor: colors[colorIndex % colors.length].completion.replace('1)', '0.1)'),
yAxisID: showCount ? 'y1' : 'y', // Utiliser y1 seulement si on a aussi des décomptes
borderDash: showCount ? [5, 5] : [], // Pointillés seulement si on a aussi des décomptes
});
colorIndex++;
});
}
if (allDatasets.length === 0) {
if (combinedChartInstance) {
combinedChartInstance.destroy();
combinedChartInstance = null;
}
container.innerHTML = '<p class="text-muted">Aucune donnée disponible pour les thématiques sélectionnées.</p>';
return;
}
// Restaurer le canvas si nécessaire
if (container.innerHTML.includes('<p')) {
container.innerHTML = '<canvas id="combinedChart" style="max-height: 500px;"></canvas>';
}
const canvas = document.getElementById('combinedChart');
if (!canvas) {
console.error('Impossible de trouver le canvas combiné');
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Impossible d\'obtenir le contexte 2D pour le graphique combiné');
return;
}
if (combinedChartInstance) {
combinedChartInstance.destroy();
}
try {
combinedChartInstance = new Chart(ctx, {
type: 'line',
data: { datasets: allDatasets },
options: {
responsive: true,
maintainAspectRatio: true,
parsing: {
xAxisKey: 'x',
yAxisKey: 'y'
},
plugins: {
legend: {
display: true,
position: 'top',
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
title: function(context) {
if (context && context.length > 0 && context[0].parsed && context[0].parsed.x) {
return new Date(context[0].parsed.x).toLocaleDateString('fr-FR');
}
return '';
},
label: function(context) {
let label = context.dataset.label || '';
if (context.parsed.y !== null) {
// Si c'est une complétion (axe y1), ajouter le %
if (context.dataset.yAxisID === 'y1') {
label += ': ' + context.parsed.y.toFixed(1) + '%';
} else {
label += ': ' + context.parsed.y;
}
}
return label;
}
}
}
},
scales: {
x: {
type: 'time',
time: {
unit: 'day',
displayFormats: {
day: 'dd/MM/yyyy'
}
},
title: {
display: true,
text: 'Date'
}
},
y: {
type: 'linear',
position: 'left',
title: {
display: true,
text: showCount && showCompletion ? "Nombre d'objets" : (showCount ? "Nombre d'objets" : 'Complétion (%)')
},
beginAtZero: showCount || !showCompletion,
...(showCompletion && !showCount ? {
min: 0,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
} : {})
},
...(showCount && showCompletion ? {
y1: {
type: 'linear',
position: 'right',
title: {
display: true,
text: 'Complétion (%)'
},
min: 0,
max: 100,
grid: {
drawOnChartArea: false
},
ticks: {
callback: function(value) {
return value + '%';
}
}
}
} : {})
}
}
});
console.log('Graphique combiné créé avec succès', allDatasets.length, 'datasets');
} catch (error) {
console.error('Erreur lors de la création du graphique combiné:', error);
}
}
// Événements
document.addEventListener('DOMContentLoaded', function() {
loadData();
// Écouter les changements de sélection
document.querySelectorAll('.theme-checkbox').forEach(cb => {
cb.addEventListener('change', updateChart);
});
// Écouter les changements d'options d'affichage
document.getElementById('showCount').addEventListener('change', updateChart);
document.getElementById('showCompletion').addEventListener('change', updateChart);
document.getElementById('separateCharts').addEventListener('change', updateChart);
// Boutons sélection/désélection
document.getElementById('selectAll').addEventListener('click', function() {
document.querySelectorAll('.theme-checkbox').forEach(cb => cb.checked = true);
updateChart();
});
document.getElementById('deselectAll').addEventListener('click', function() {
document.querySelectorAll('.theme-checkbox').forEach(cb => cb.checked = false);
updateChart();
});
});
</script>
{% endblock %}