page de stats comparées
This commit is contained in:
parent
03fede15aa
commit
f7e9576c41
5 changed files with 800 additions and 0 deletions
|
|
@ -54,6 +54,9 @@
|
|||
<a class="nav-link {% if active_menu == 'followup_graph' %}active{% endif %}" href="{{ path('admin_followup_graph', {'insee_code': stats.zone}) }}">
|
||||
<i class="bi bi-graph-up"></i> Suivi OSM (graphes)
|
||||
</a>
|
||||
<a class="nav-link {% if active_menu == 'followup_detailed_stats' %}active{% endif %}" href="{{ path('admin_followup_detailed_stats', {'insee_code': stats.zone}) }}">
|
||||
<i class="bi bi-graph-up-arrow"></i> Stats détaillées (comparatif)
|
||||
</a>
|
||||
<a class="nav-link {% if active_menu == 'stats_evolutions' %}active{% endif %}" href="{{ path('app_public_stats_evolutions', {'insee_code': stats.zone}) }}">
|
||||
<i class="bi bi-activity"></i> Évolutions des objets
|
||||
</a>
|
||||
|
|
|
|||
719
templates/admin/followup_detailed_stats.html.twig
Normal file
719
templates/admin/followup_detailed_stats.html.twig
Normal file
|
|
@ -0,0 +1,719 @@
|
|||
{% 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 %}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue