566 lines
No EOL
29 KiB
Twig
566 lines
No EOL
29 KiB
Twig
{% extends 'base.html.twig' %}
|
|
|
|
{% block title %}Évolution des classements Wiki OSM{% endblock %}
|
|
|
|
{% block body %}
|
|
<div class="container mt-4">
|
|
{% include 'admin/_wiki_navigation.html.twig' %}
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h1>Évolution des classements Wiki OSM</h1>
|
|
{% if last_updated %}
|
|
<div class="text-muted">
|
|
Dernière mise à jour: {{ last_updated|date('d/m/Y H:i') }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if not json_exists %}
|
|
<div class="alert alert-warning">
|
|
<i class="bi bi-exclamation-triangle"></i> Aucune donnée de classement n'est disponible. Veuillez exécuter le script de scraping pour générer les données.
|
|
</div>
|
|
{% else %}
|
|
<!-- Global Metrics Section -->
|
|
<div class="card mb-4">
|
|
<div class="card-header bg-primary text-white">
|
|
<h2>Métriques globales</h2>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<canvas id="globalMetricsChart" width="400" height="200"></canvas>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<canvas id="stalenessDistributionChart" width="400" height="200"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-4">
|
|
<div class="col-md-12">
|
|
<h3>Évolution des métriques globales</h3>
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Pages totales</th>
|
|
<th>Score moyen</th>
|
|
<th>Sections (moy.)</th>
|
|
<th>Mots (moy.)</th>
|
|
<th>Liens (moy.)</th>
|
|
<th>Images (moy.)</th>
|
|
<th>Catégories (moy.)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for timestamp in timestamps %}
|
|
{% if global_metrics[timestamp] is defined %}
|
|
<tr>
|
|
<td>{{ timestamp|date('d/m/Y') }}</td>
|
|
<td>{{ global_metrics[timestamp].total_pages }}</td>
|
|
<td>
|
|
<div class="progress" style="height: 20px;">
|
|
{% set score = global_metrics[timestamp].avg_staleness %}
|
|
{% set score_class = score > 70 ? 'bg-danger' : (score > 40 ? 'bg-warning' : 'bg-success') %}
|
|
<div class="progress-bar {{ score_class }}" role="progressbar"
|
|
style="width: {{ score }}%;"
|
|
aria-valuenow="{{ score }}"
|
|
aria-valuemin="0"
|
|
aria-valuemax="100">
|
|
{{ score }}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>{{ global_metrics[timestamp].avg_sections }}</td>
|
|
<td>{{ global_metrics[timestamp].avg_words }}</td>
|
|
<td>{{ global_metrics[timestamp].avg_links }}</td>
|
|
<td>{{ global_metrics[timestamp].avg_images }}</td>
|
|
<td>{{ global_metrics[timestamp].avg_categories }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Page Rankings Section -->
|
|
<div class="card mb-4">
|
|
<div class="card-header bg-primary text-white">
|
|
<h2>Classement des pages</h2>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<div class="input-group">
|
|
<span class="input-group-text">Filtrer</span>
|
|
<input type="text" id="pageFilter" class="form-control" placeholder="Rechercher une page...">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select id="metricSelector" class="form-select">
|
|
<option value="staleness_score">Score de décrépitude</option>
|
|
<option value="word_diff">Différence de mots</option>
|
|
<option value="section_diff">Différence de sections</option>
|
|
<option value="link_diff">Différence de liens</option>
|
|
<option value="media_diff">Différence d'images</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select id="sortOrder" class="form-select">
|
|
<option value="desc">Décroissant</option>
|
|
<option value="asc">Croissant</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-hover" id="pagesTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Page</th>
|
|
<th>Score actuel</th>
|
|
<th>Évolution</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for key, page in pages %}
|
|
<tr data-page-key="{{ key }}">
|
|
<td>{{ page.title }}</td>
|
|
<td>
|
|
{% set latest_timestamp = timestamps|last %}
|
|
{% if page.metrics[latest_timestamp] is defined %}
|
|
{% set latest_score = page.metrics[latest_timestamp].staleness_score %}
|
|
<div class="progress" style="height: 20px;">
|
|
{% set score_class = latest_score > 70 ? 'bg-danger' : (latest_score > 40 ? 'bg-warning' : 'bg-success') %}
|
|
<div class="progress-bar {{ score_class }}" role="progressbar"
|
|
style="width: {{ latest_score }}%;"
|
|
aria-valuenow="{{ latest_score }}"
|
|
aria-valuemin="0"
|
|
aria-valuemax="100">
|
|
{{ latest_score }}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<span class="badge bg-secondary">N/A</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<canvas class="trend-chart" data-page-key="{{ key }}" width="200" height="50"></canvas>
|
|
</td>
|
|
<td>
|
|
<a href="{{ path('app_admin_wiki_compare', {'key': key}) }}" class="btn btn-sm btn-outline-primary">
|
|
<i class="bi bi-arrows-angle-expand"></i> Comparer
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Page Detail Modal -->
|
|
<div class="modal fade" id="pageDetailModal" tabindex="-1" aria-labelledby="pageDetailModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="pageDetailModalLabel">Détails de la page</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<h3 id="modalPageTitle"></h3>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<canvas id="pageDetailChart" width="700" height="300"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-3">
|
|
<div class="col-md-12">
|
|
<h4>Historique des métriques</h4>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm" id="pageMetricsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Score</th>
|
|
<th>Mots</th>
|
|
<th>Sections</th>
|
|
<th>Liens</th>
|
|
<th>Images</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<!-- Filled dynamically by JavaScript -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
|
<a href="#" id="comparePageBtn" class="btn btn-primary">Comparer</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block javascripts %}
|
|
{{ parent() }}
|
|
{% if json_exists %}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Data from controller
|
|
const timestamps = {{ timestamps|json_encode|raw }};
|
|
const pages = {{ pages|json_encode|raw }};
|
|
const globalMetrics = {{ global_metrics|json_encode|raw }};
|
|
|
|
// Format dates for display
|
|
const formatDates = timestamps.map(ts => {
|
|
const date = new Date(ts);
|
|
return date.toLocaleDateString('fr-FR');
|
|
});
|
|
|
|
// Global metrics chart
|
|
const globalMetricsCtx = document.getElementById('globalMetricsChart').getContext('2d');
|
|
const globalMetricsData = {
|
|
labels: formatDates,
|
|
datasets: [
|
|
{
|
|
label: 'Score moyen',
|
|
data: timestamps.map(ts => globalMetrics[ts]?.avg_staleness || 0),
|
|
borderColor: 'rgba(255, 99, 132, 1)',
|
|
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
|
fill: false,
|
|
tension: 0.1
|
|
},
|
|
{
|
|
label: 'Sections (moy.)',
|
|
data: timestamps.map(ts => globalMetrics[ts]?.avg_sections || 0),
|
|
borderColor: 'rgba(54, 162, 235, 1)',
|
|
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
|
fill: false,
|
|
tension: 0.1
|
|
},
|
|
{
|
|
label: 'Mots (moy. / 10)',
|
|
data: timestamps.map(ts => (globalMetrics[ts]?.avg_words || 0) / 10),
|
|
borderColor: 'rgba(255, 206, 86, 1)',
|
|
backgroundColor: 'rgba(255, 206, 86, 0.2)',
|
|
fill: false,
|
|
tension: 0.1
|
|
}
|
|
]
|
|
};
|
|
|
|
new Chart(globalMetricsCtx, {
|
|
type: 'line',
|
|
data: globalMetricsData,
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: 'Évolution des métriques globales'
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Staleness distribution chart
|
|
if (timestamps.length > 0) {
|
|
const latestTimestamp = timestamps[timestamps.length - 1];
|
|
const latestDistribution = globalMetrics[latestTimestamp]?.staleness_distribution || {};
|
|
|
|
const stalenessDistCtx = document.getElementById('stalenessDistributionChart').getContext('2d');
|
|
const stalenessDistData = {
|
|
labels: Object.keys(latestDistribution),
|
|
datasets: [{
|
|
label: 'Nombre de pages',
|
|
data: Object.values(latestDistribution),
|
|
backgroundColor: [
|
|
'rgba(25, 135, 84, 0.7)',
|
|
'rgba(140, 195, 38, 0.7)',
|
|
'rgba(255, 193, 7, 0.7)',
|
|
'rgba(255, 153, 0, 0.7)',
|
|
'rgba(232, 113, 55, 0.7)',
|
|
'rgba(220, 53, 69, 0.7)'
|
|
],
|
|
borderColor: [
|
|
'rgba(25, 135, 84, 1)',
|
|
'rgba(140, 195, 38, 1)',
|
|
'rgba(255, 193, 7, 1)',
|
|
'rgba(255, 153, 0, 1)',
|
|
'rgba(232, 113, 55, 1)',
|
|
'rgba(220, 53, 69, 1)'
|
|
],
|
|
borderWidth: 1
|
|
}]
|
|
};
|
|
|
|
new Chart(stalenessDistCtx, {
|
|
type: 'bar',
|
|
data: stalenessDistData,
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: 'Distribution des scores de décrépitude'
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Create small trend charts for each page
|
|
const trendCharts = document.querySelectorAll('.trend-chart');
|
|
trendCharts.forEach(canvas => {
|
|
const pageKey = canvas.dataset.pageKey;
|
|
const pageData = pages[pageKey];
|
|
|
|
if (pageData && pageData.metrics) {
|
|
const ctx = canvas.getContext('2d');
|
|
const metricValues = timestamps.map(ts => {
|
|
return pageData.metrics[ts]?.staleness_score || null;
|
|
});
|
|
|
|
// Filter out null values
|
|
const validData = metricValues.filter(val => val !== null);
|
|
|
|
// Calculate trend (increasing or decreasing)
|
|
let trendColor = 'rgba(75, 192, 192, 1)'; // Default: neutral
|
|
if (validData.length >= 2) {
|
|
const firstValid = validData[0];
|
|
const lastValid = validData[validData.length - 1];
|
|
if (lastValid > firstValid) {
|
|
trendColor = 'rgba(255, 99, 132, 1)'; // Red: getting worse
|
|
} else if (lastValid < firstValid) {
|
|
trendColor = 'rgba(75, 192, 192, 1)'; // Green: getting better
|
|
}
|
|
}
|
|
|
|
new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: formatDates,
|
|
datasets: [{
|
|
data: metricValues,
|
|
borderColor: trendColor,
|
|
backgroundColor: trendColor.replace('1)', '0.2)'),
|
|
tension: 0.1,
|
|
pointRadius: 0,
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: true
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false,
|
|
min: 0,
|
|
max: 100
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Page filtering
|
|
const pageFilter = document.getElementById('pageFilter');
|
|
const pagesTable = document.getElementById('pagesTable');
|
|
|
|
pageFilter.addEventListener('input', function() {
|
|
const filterText = this.value.toLowerCase();
|
|
const rows = pagesTable.querySelectorAll('tbody tr');
|
|
|
|
rows.forEach(row => {
|
|
const pageKey = row.dataset.pageKey;
|
|
const pageTitle = pages[pageKey]?.title.toLowerCase() || '';
|
|
|
|
if (pageTitle.includes(filterText) || pageKey.toLowerCase().includes(filterText)) {
|
|
row.style.display = '';
|
|
} else {
|
|
row.style.display = 'none';
|
|
}
|
|
});
|
|
});
|
|
|
|
// Metric selector and sorting
|
|
const metricSelector = document.getElementById('metricSelector');
|
|
const sortOrder = document.getElementById('sortOrder');
|
|
|
|
function sortPages() {
|
|
const metric = metricSelector.value;
|
|
const order = sortOrder.value;
|
|
const tbody = pagesTable.querySelector('tbody');
|
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
|
|
// Get the latest timestamp
|
|
const latestTs = timestamps[timestamps.length - 1];
|
|
|
|
rows.sort((a, b) => {
|
|
const keyA = a.dataset.pageKey;
|
|
const keyB = b.dataset.pageKey;
|
|
|
|
const valueA = pages[keyA]?.metrics[latestTs]?.[metric] || 0;
|
|
const valueB = pages[keyB]?.metrics[latestTs]?.[metric] || 0;
|
|
|
|
return order === 'asc' ? valueA - valueB : valueB - valueA;
|
|
});
|
|
|
|
// Clear and re-append rows
|
|
rows.forEach(row => tbody.appendChild(row));
|
|
}
|
|
|
|
metricSelector.addEventListener('change', sortPages);
|
|
sortOrder.addEventListener('change', sortPages);
|
|
|
|
// Initial sort
|
|
sortPages();
|
|
|
|
// Page detail modal
|
|
const pageDetailModal = new bootstrap.Modal(document.getElementById('pageDetailModal'));
|
|
const modalPageTitle = document.getElementById('modalPageTitle');
|
|
const comparePageBtn = document.getElementById('comparePageBtn');
|
|
const pageMetricsTable = document.getElementById('pageMetricsTable');
|
|
let pageDetailChart = null;
|
|
|
|
// Add click event to table rows
|
|
const tableRows = pagesTable.querySelectorAll('tbody tr');
|
|
tableRows.forEach(row => {
|
|
row.addEventListener('click', function() {
|
|
const pageKey = this.dataset.pageKey;
|
|
const pageData = pages[pageKey];
|
|
|
|
if (pageData) {
|
|
// Set modal title and compare button link
|
|
modalPageTitle.textContent = pageData.title;
|
|
comparePageBtn.href = `/wiki/compare/${pageKey}`;
|
|
|
|
// Fill metrics table
|
|
const tbody = pageMetricsTable.querySelector('tbody');
|
|
tbody.innerHTML = '';
|
|
|
|
timestamps.forEach(ts => {
|
|
if (pageData.metrics[ts]) {
|
|
const metrics = pageData.metrics[ts];
|
|
const row = document.createElement('tr');
|
|
|
|
// Format date
|
|
const date = new Date(ts);
|
|
const formattedDate = date.toLocaleDateString('fr-FR');
|
|
|
|
row.innerHTML = `
|
|
<td>${formattedDate}</td>
|
|
<td>${metrics.staleness_score || 0}</td>
|
|
<td>${metrics.word_diff || 0}</td>
|
|
<td>${metrics.section_diff || 0}</td>
|
|
<td>${metrics.link_diff || 0}</td>
|
|
<td>${metrics.media_diff || 0}</td>
|
|
`;
|
|
|
|
tbody.appendChild(row);
|
|
}
|
|
});
|
|
|
|
// Create detailed chart
|
|
const ctx = document.getElementById('pageDetailChart').getContext('2d');
|
|
|
|
// Destroy previous chart if exists
|
|
if (pageDetailChart) {
|
|
pageDetailChart.destroy();
|
|
}
|
|
|
|
pageDetailChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: formatDates,
|
|
datasets: [
|
|
{
|
|
label: 'Score de décrépitude',
|
|
data: timestamps.map(ts => pageData.metrics[ts]?.staleness_score || null),
|
|
borderColor: 'rgba(255, 99, 132, 1)',
|
|
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
|
fill: false
|
|
},
|
|
{
|
|
label: 'Différence de mots / 10',
|
|
data: timestamps.map(ts => (pageData.metrics[ts]?.word_diff || 0) / 10),
|
|
borderColor: 'rgba(54, 162, 235, 1)',
|
|
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
|
fill: false
|
|
},
|
|
{
|
|
label: 'Différence de sections',
|
|
data: timestamps.map(ts => pageData.metrics[ts]?.section_diff || null),
|
|
borderColor: 'rgba(255, 206, 86, 1)',
|
|
backgroundColor: 'rgba(255, 206, 86, 0.2)',
|
|
fill: false
|
|
},
|
|
{
|
|
label: 'Différence de liens',
|
|
data: timestamps.map(ts => pageData.metrics[ts]?.link_diff || null),
|
|
borderColor: 'rgba(75, 192, 192, 1)',
|
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
|
fill: false
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: `Évolution des métriques pour ${pageData.title}`
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Show modal
|
|
pageDetailModal.show();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
{% endif %}
|
|
{% endblock %} |