add history in articles measures

This commit is contained in:
Tykayn 2025-09-08 10:20:51 +02:00 committed by tykayn
parent 1ed74c2e2f
commit 381f378db4
9 changed files with 1678 additions and 195 deletions

View file

@ -44,6 +44,49 @@ class WikiController extends AbstractController
'json_exists' => file_exists($outdatedPagesFile)
]);
}
/**
* Displays the evolution of page rankings over time
*/
#[Route('/wiki/rankings', name: 'app_admin_wiki_rankings')]
public function pageRankings(): Response
{
$rankingsFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/page_rankings.json';
$timestamps = [];
$pages = [];
$globalMetrics = [];
$lastUpdated = null;
if (file_exists($rankingsFile)) {
// Load the rankings data
try {
$rankingsData = json_decode(file_get_contents($rankingsFile), true);
if (json_last_error() === JSON_ERROR_NONE) {
$timestamps = $rankingsData['timestamps'] ?? [];
$pages = $rankingsData['pages'] ?? [];
$globalMetrics = $rankingsData['global_metrics'] ?? [];
// Get the last timestamp as last_updated
if (!empty($timestamps)) {
$lastUpdated = end($timestamps);
}
}
} catch (\Exception $e) {
// Log the error
error_log("Error loading rankings data: " . $e->getMessage());
}
}
return $this->render('admin/wiki_rankings.html.twig', [
'timestamps' => $timestamps,
'pages' => $pages,
'global_metrics' => $globalMetrics,
'last_updated' => $lastUpdated,
'json_exists' => file_exists($rankingsFile)
]);
}
/**
* Detects incorrect heading hierarchies in a list of sections
* For example, h4 directly under h2 without h3 in between
@ -1306,6 +1349,9 @@ EOT;
$detailedComparison = null;
$mediaDiff = 0;
$historyData = null;
$prevPage = null;
$nextPage = null;
$stalenessDistribution = null;
if (file_exists($jsonFile)) {
// Use memory-efficient approach to extract only the necessary data
@ -1377,6 +1423,91 @@ EOT;
// Combine them into a single array
$allPages = array_merge($regularPages, $specificPages);
// Sort pages by staleness score (descending)
usort($allPages, function($a, $b) {
$scoreA = $a['staleness_score'] ?? 0;
$scoreB = $b['staleness_score'] ?? 0;
return $scoreB <=> $scoreA; // Descending order
});
// Find the current page index in the sorted array
$currentIndex = -1;
foreach ($allPages as $index => $page) {
if (isset($page['key']) && $page['key'] === $key) {
$currentIndex = $index;
break;
}
}
// Determine previous and next pages
if ($currentIndex > 0) {
$prevPage = $allPages[$currentIndex - 1];
}
if ($currentIndex < count($allPages) - 1 && $currentIndex >= 0) {
$nextPage = $allPages[$currentIndex + 1];
}
// Create staleness score distribution data for histogram
$stalenessScores = [];
foreach ($allPages as $page) {
if (isset($page['staleness_score'])) {
$stalenessScores[] = $page['staleness_score'];
}
}
if (!empty($stalenessScores)) {
// Calculate statistics
$min = min($stalenessScores);
$max = max($stalenessScores);
$avg = array_sum($stalenessScores) / count($stalenessScores);
$median = $this->calculateMedian($stalenessScores);
// Create histogram bins (10 bins)
$binCount = 10;
$binSize = ($max - $min) / $binCount;
$bins = [];
$binLabels = [];
// Initialize bins
for ($i = 0; $i < $binCount; $i++) {
$bins[$i] = 0;
$binStart = $min + ($i * $binSize);
$binEnd = $binStart + $binSize;
$binLabels[$i] = round($binStart, 1) . ' - ' . round($binEnd, 1);
}
// Count scores in each bin
foreach ($stalenessScores as $score) {
$binIndex = min($binCount - 1, floor(($score - $min) / $binSize));
$bins[$binIndex]++;
}
// Find which bin the current page falls into
$currentPageScore = 0;
foreach ($allPages as $page) {
if (isset($page['key']) && $page['key'] === $key && isset($page['staleness_score'])) {
$currentPageScore = $page['staleness_score'];
break;
}
}
$currentPageBin = min($binCount - 1, floor(($currentPageScore - $min) / $binSize));
$stalenessDistribution = [
'scores' => $stalenessScores,
'min' => $min,
'max' => $max,
'avg' => $avg,
'median' => $median,
'bins' => $bins,
'binLabels' => $binLabels,
'currentPageScore' => $currentPageScore,
'currentPageBin' => $currentPageBin,
'totalPages' => count($stalenessScores)
];
}
// Find the page with the matching key
foreach ($allPages as $page) {
if (isset($page['key']) && $page['key'] === $key) {
@ -1792,7 +1923,10 @@ EOT;
'fr_sections' => $frSections,
'en_links' => $enLinks,
'fr_links' => $frLinks,
'history_data' => $historyData
'history_data' => $historyData,
'prev_page' => $prevPage,
'next_page' => $nextPage,
'staleness_distribution' => $stalenessDistribution
]);
}
@ -2207,4 +2341,30 @@ EOT;
{
return $this->extractJsonArrayByKey($filePath, 'specific_pages', $maxPages);
}
/**
* Calculate the median value of an array of numbers
*
* @param array $array Array of numbers
* @return float The median value
*/
private function calculateMedian(array $array): float
{
sort($array);
$count = count($array);
if ($count === 0) {
return 0;
}
$middle = floor($count / 2);
if ($count % 2 === 0) {
// Even number of elements, average the two middle values
return ($array[$middle - 1] + $array[$middle]) / 2;
} else {
// Odd number of elements, return the middle value
return $array[$middle];
}
}
}

View file

@ -56,6 +56,11 @@
<i class="bi bi-graph-up"></i> Scores de décrépitude
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if app.request.get('_route') == 'app_admin_wiki_rankings' %}active{% endif %}" href="{{ path('app_admin_wiki_rankings') }}">
<i class="bi bi-bar-chart-line"></i> Évolution des classements
</a>
</li>
</ul>
</div>
</div>

View file

@ -105,9 +105,32 @@
<div class="container mt-4">
{% include 'admin/_wiki_navigation.html.twig' %}
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
{% if prev_page is defined and prev_page is not null %}
<a href="{{ path('app_admin_wiki_compare', {'key': prev_page.key}) }}" class="btn btn-outline-primary">
<i class="bi bi-arrow-left"></i> Page précédente: {{ prev_page.key }}
</a>
{% else %}
<button class="btn btn-outline-secondary" disabled>
<i class="bi bi-arrow-left"></i> Pas de page précédente
</button>
{% endif %}
</div>
<div>
{% if next_page is defined and next_page is not null %}
<a href="{{ path('app_admin_wiki_compare', {'key': next_page.key}) }}" class="btn btn-outline-primary">
Page suivante: {{ next_page.key }} <i class="bi bi-arrow-right"></i>
</a>
{% else %}
<button class="btn btn-outline-secondary" disabled>
Pas de page suivante <i class="bi bi-arrow-right"></i>
</button>
{% endif %}
</div>
</div>
<h1>Comparaison Wiki OpenStreetMap - {{ key }}
{% if en_page.is_specific_page is defined and en_page.is_specific_page %}
<a href="{{ fr_page.url|default('https://wiki.openstreetmap.org/wiki/FR:' ~ key) }}">fr</a>
<a href="{{ en_page.url }}">en</a>
@ -117,7 +140,6 @@
{% endif %}
</h1>
<p class="lead">
{% if en_page.is_specific_page is defined and en_page.is_specific_page %}
Comparaison détaillée des pages wiki en français et en anglais pour "{{ key }}".
@ -159,6 +181,63 @@
</div>
</div>
{% if history_data is defined and history_data is not empty %}
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h2>Évolution du classement</h2>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-12">
<canvas id="rankingEvolutionChart" width="800" height="300"></canvas>
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Date</th>
<th>Score de décrépitude</th>
<th>Différence de mots</th>
<th>Différence de sections</th>
<th>Différence de liens</th>
<th>Différence d'images</th>
</tr>
</thead>
<tbody>
{% for entry in history_data %}
<tr>
<td>{{ entry.date }}</td>
<td>
<div class="progress" style="height: 20px;">
{% set score = entry.metrics.staleness_score %}
{% 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>{{ entry.metrics.word_diff }}</td>
<td>{{ entry.metrics.section_diff }}</td>
<td>{{ entry.metrics.link_diff }}</td>
<td>{{ entry.metrics.media_diff }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{# suggestions de grammalecte #}
{% if fr_page is defined and fr_page is not null %}
{% if detailed_comparison is defined and detailed_comparison is not null and detailed_comparison.section_comparison is defined and detailed_comparison.section_comparison is not null %}
@ -225,6 +304,59 @@
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-success text-white">
<h3>Sections communes</h3>
<span class="badge bg-light text-dark">
{% if detailed_comparison['section_comparison']['common'] is defined %}
{{ detailed_comparison['section_comparison']['common']|length }} sections communes
{% else %}
0 sections communes
{% endif %}
</span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th class="bg-primary text-white">Section anglaise</th>
<th class="bg-info text-white">Section française</th>
</tr>
</thead>
<tbody>
{% if detailed_comparison['section_comparison']['common'] is defined and detailed_comparison['section_comparison']['common'] is iterable %}
{% for section in detailed_comparison['section_comparison']['common'] %}
<tr>
<td class="title-level-{{ section.en.level|default(1) }}">
<span class="badge bg-secondary">h{{ section.en.level|default(1) }}</span>
{% if section.en.title is defined and section.en.title is not empty %}
<span class="section-title">{{ section.en.title }}</span>
{% endif %}
</td>
<td class="title-level-{{ section.fr.level|default(1) }}">
<span class="badge bg-secondary">h{{ section.fr.level|default(1) }}</span>
{% if section.fr.title is defined and section.fr.title is not empty %}
<span class="section-title">{{ section.fr.title }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="2" class="text-center">Aucune section commune trouvée</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="text-center mt-3">
{% if detailed_comparison is defined and detailed_comparison is not null and detailed_comparison.section_comparison is defined and detailed_comparison.section_comparison is not null %}
<button class="btn btn-outline-secondary copy-btn" data-content="different-sections">
@ -519,142 +651,311 @@
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-success text-white">
<h3>Images communes</h3>
<span class="badge bg-light text-dark">
{% if detailed_comparison.media_comparison.common is defined %}
{{ detailed_comparison.media_comparison.common|length }} images communes
{% else %}
0 images communes
{% endif %}
</span>
</div>
<div class="card-body">
<div class="row">
{% if detailed_comparison.media_comparison.common is defined and detailed_comparison.media_comparison.common is iterable %}
{% for media in detailed_comparison.media_comparison.common %}
<div class="col-md-6 mb-3">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h5>Version anglaise</h5>
</div>
<div class="card-body">
<img src="{{ media.en.src }}" class="img-fluid"
alt="{{ media.en.alt }}"
style="max-height: 150px; object-fit: contain;">
<p class="card-text small mt-2">{{ media.en.alt }}</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card h-100">
<div class="card-header bg-info text-white">
<h5>Version française</h5>
</div>
<div class="card-body">
<img src="{{ media.fr.src }}" class="img-fluid"
alt="{{ media.fr.alt }}"
style="max-height: 150px; object-fit: contain;">
<p class="card-text small mt-2">{{ media.fr.alt }}</p>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="col-12">
<div class="alert alert-info">
Aucune image commune trouvée entre les versions anglaise et française.
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{# {% if detailed_comparison and detailed_comparison.link_comparison %} #}
{# <div class="card mb-4"> #}
{# <div class="card-header"> #}
{# <h2>Comparaison des liens</h2> #}
{# </div> #}
{# <div class="card-body"> #}
{# <div class="row"> #}
{# <div class="col-md-6"> #}
{# <div class="card h-100"> #}
{# <div class="card-header bg-primary text-white"> #}
{# <h3>Liens en anglais</h3> #}
{# <span class="badge bg-light text-dark">{{ en_page.link_count }} liens</span> #}
{# </div> #}
{# <div class="card-body"> #}
{% if detailed_comparison and detailed_comparison.link_comparison %}
<div class="card mb-4">
<div class="card-header">
<h2>Comparaison des liens</h2>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h3>Liens uniquement en anglais</h3>
<span class="badge bg-light text-dark">
{% if detailed_comparison.link_comparison.en_only is defined %}
{{ detailed_comparison.link_comparison.en_only|length }} liens
{% else %}
0 liens
{% endif %}
</span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Texte du lien</th>
<th>URL</th>
</tr>
</thead>
<tbody>
{% if detailed_comparison.link_comparison.en_only is defined and detailed_comparison.link_comparison.en_only is iterable %}
{% for link in detailed_comparison.link_comparison.en_only %}
<tr>
<td>{{ link.text }}</td>
<td>
<a href="{{ link.href }}" target="_blank" class="small">
{{ link.href|slice(0, 30) }}{% if link.href|length > 30 %}...{% endif %}
</a>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="2" class="text-center">Aucun lien uniquement en anglais</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-info text-white">
<h3>Liens uniquement en français</h3>
<span class="badge bg-light text-dark">
{% if detailed_comparison.link_comparison.fr_only is defined %}
{{ detailed_comparison.link_comparison.fr_only|length }} liens
{% else %}
0 liens
{% endif %}
</span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Texte du lien</th>
<th>URL</th>
</tr>
</thead>
<tbody>
{% if detailed_comparison.link_comparison.fr_only is defined and detailed_comparison.link_comparison.fr_only is iterable %}
{% for link in detailed_comparison.link_comparison.fr_only %}
<tr>
<td>{{ link.text }}</td>
<td>
<a href="{{ link.href }}" target="_blank" class="small">
{{ link.href|slice(0, 30) }}{% if link.href|length > 30 %}...{% endif %}
</a>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="2" class="text-center">Aucun lien uniquement en français</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-success text-white">
<h3>Liens communs</h3>
<span class="badge bg-light text-dark">
{% if detailed_comparison.link_comparison.common is defined %}
{{ detailed_comparison.link_comparison.common|length }} liens communs
{% else %}
0 liens communs
{% endif %}
</span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th class="bg-primary text-white">Texte EN</th>
<th class="bg-primary text-white">URL EN</th>
<th class="bg-info text-white">Texte FR</th>
<th class="bg-info text-white">URL FR</th>
</tr>
</thead>
<tbody>
{% if detailed_comparison.link_comparison.common is defined and detailed_comparison.link_comparison.common is iterable %}
{% for link in detailed_comparison.link_comparison.common %}
<tr>
<td>{{ link.en.text }}</td>
<td>
<a href="{{ link.en.href }}" target="_blank" class="small">
{{ link.en.href|slice(0, 30) }}{% if link.en.href|length > 30 %}...{% endif %}
</a>
</td>
<td>{{ link.fr.text }}</td>
<td>
<a href="{{ link.fr.href }}" target="_blank" class="small">
{{ link.fr.href|slice(0, 30) }}{% if link.fr.href|length > 30 %}...{% endif %}
</a>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4" class="text-center">Aucun lien commun trouvé</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{# <h4>Comparaison des liens côte à côte</h4> #}
{# <div class="table-responsive"> #}
{# <table class="table table-sm"> #}
{# <thead> #}
{# <tr> #}
{# <th class="bg-primary text-white">Texte EN</th> #}
{# <th class="bg-primary text-white">URL EN</th> #}
{# <th class="bg-info text-white">Texte FR</th> #}
{# <th class="bg-info text-white">URL FR</th> #}
{# </tr> #}
{# </thead> #}
{# <tbody> #}
{# {% set en_links = detailed_comparison.link_comparison.en_only %} #}
{# {% set fr_links = detailed_comparison.link_comparison.fr_only %} #}
{# {% set max_links = max(en_links|length, fr_links|length) %} #}
{% if detailed_comparison and detailed_comparison.category_comparison %}
<div class="card mb-4">
<div class="card-header">
<h2>Comparaison des catégories</h2>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h3>Catégories uniquement en anglais</h3>
<span class="badge bg-light text-dark">
{% if detailed_comparison.category_comparison.en_only is defined %}
{{ detailed_comparison.category_comparison.en_only|length }} catégories
{% else %}
0 catégories
{% endif %}
</span>
</div>
<div class="card-body">
<ul class="list-group">
{% if detailed_comparison.category_comparison.en_only is defined and detailed_comparison.category_comparison.en_only is iterable and detailed_comparison.category_comparison.en_only|length > 0 %}
{% for category in detailed_comparison.category_comparison.en_only %}
<li class="list-group-item list-group-item-warning">{{ category }}</li>
{% endfor %}
{% else %}
<li class="list-group-item text-center">Aucune catégorie uniquement en anglais</li>
{% endif %}
</ul>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-info text-white">
<h3>Catégories uniquement en français</h3>
<span class="badge bg-light text-dark">
{% if detailed_comparison.category_comparison.fr_only is defined %}
{{ detailed_comparison.category_comparison.fr_only|length }} catégories
{% else %}
0 catégories
{% endif %}
</span>
</div>
<div class="card-body">
<ul class="list-group">
{% if detailed_comparison.category_comparison.fr_only is defined and detailed_comparison.category_comparison.fr_only is iterable and detailed_comparison.category_comparison.fr_only|length > 0 %}
{% for category in detailed_comparison.category_comparison.fr_only %}
<li class="list-group-item list-group-item-info">{{ category }}</li>
{% endfor %}
{% else %}
<li class="list-group-item text-center">Aucune catégorie uniquement en français</li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
{# {% for i in 0..(max_links - 1) %} #}
{# <tr> #}
{# {% if i < en_links|length %} #}
{# <td class="bg-light">{{ en_links[i].text }}</td> #}
{# <td class="bg-light"><a href="{{ en_links[i].href }}" #}
{# target="_blank" #}
{# class="small">{{ en_links[i].href|slice(0, 30) }} #}
{# ...</a></td> #}
{# {% else %} #}
{# <td class="bg-light"></td> #}
{# <td class="bg-light"></td> #}
{# {% endif %} #}
{# {% if i < fr_links|length %} #}
{# <td>{{ fr_links[i].text }}</td> #}
{# <td><a href="{{ fr_links[i].href }}" target="_blank" #}
{# class="small">{{ fr_links[i].href|slice(0, 30) }} #}
{# ...</a></td> #}
{# {% else %} #}
{# <td></td> #}
{# <td></td> #}
{# {% endif %} #}
{# </tr> #}
{# {% endfor %} #}
{# </tbody> #}
{# </table> #}
{# </div> #}
{# </div> #}
{# </div> #}
{# </div> #}
{# </div> #}
{# </div> #}
{# </div> #}
{# {% endif %} #}
{# {% if detailed_comparison and detailed_comparison.category_comparison %} #}
{# <div class="card mb-4"> #}
{# <div class="card-header"> #}
{# <h2>Comparaison des catégories</h2> #}
{# </div> #}
{# <div class="card-body"> #}
{# <div class="row"> #}
{# <div class="col-md-6"> #}
{# <div class="card h-100"> #}
{# <div class="card-header bg-primary text-white"> #}
{# <h3>Catégories en anglais</h3> #}
{# <span class="badge bg-light text-dark"> #}
{# {{ (detailed_comparison.category_comparison.en_only|length + detailed_comparison.category_comparison.common|length) }} catégories #}
{# </span> #}
{# </div> #}
{# <div class="card-body"> #}
{# <h4>Catégories communes #}
{# ({{ detailed_comparison.category_comparison.common|length }})</h4> #}
{# <ul class="list-group mb-3"> #}
{# {% for category in detailed_comparison.category_comparison.common %} #}
{# <li class="list-group-item">{{ category }}</li> #}
{# {% endfor %} #}
{# </ul> #}
{# <h4>Catégories uniquement en anglais #}
{# ({{ detailed_comparison.category_comparison.en_only|length }})</h4> #}
{# <ul class="list-group"> #}
{# {% for category in detailed_comparison.category_comparison.en_only %} #}
{# <li class="list-group-item list-group-item-warning">{{ category }}</li> #}
{# {% endfor %} #}
{# </ul> #}
{# </div> #}
{# </div> #}
{# </div> #}
{# <div class="col-md-6"> #}
{# <div class="card h-100"> #}
{# <div class="card-header bg-info text-white"> #}
{# <h3>Catégories en français</h3> #}
{# <span class="badge bg-light text-dark"> #}
{# {{ (detailed_comparison.category_comparison.fr_only|length + detailed_comparison.category_comparison.common|length) }} catégories #}
{# </span> #}
{# </div> #}
{# <div class="card-body"> #}
{# <h4>Catégories communes #}
{# ({{ detailed_comparison.category_comparison.common|length }})</h4> #}
{# <ul class="list-group mb-3"> #}
{# {% for category in detailed_comparison.category_comparison.common %} #}
{# <li class="list-group-item">{{ category }}</li> #}
{# {% endfor %} #}
{# </ul> #}
{# <h4>Catégories uniquement en français #}
{# ({{ detailed_comparison.category_comparison.fr_only|length }})</h4> #}
{# <ul class="list-group"> #}
{# {% for category in detailed_comparison.category_comparison.fr_only %} #}
{# <li class="list-group-item list-group-item-info">{{ category }}</li> #}
{# {% endfor %} #}
{# </ul> #}
{# </div> #}
{# </div> #}
{# </div> #}
{# </div> #}
{# </div> #}
{# </div> #}
{# {% endif %} #}
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-success text-white">
<h3>Catégories communes</h3>
<span class="badge bg-light text-dark">
{% if detailed_comparison.category_comparison.common is defined %}
{{ detailed_comparison.category_comparison.common|length }} catégories communes
{% else %}
0 catégories communes
{% endif %}
</span>
</div>
<div class="card-body">
<ul class="list-group">
{% if detailed_comparison.category_comparison.common is defined and detailed_comparison.category_comparison.common is iterable and detailed_comparison.category_comparison.common|length > 0 %}
{% for category in detailed_comparison.category_comparison.common %}
<li class="list-group-item">{{ category }}</li>
{% endfor %}
{% else %}
<li class="list-group-item text-center">Aucune catégorie commune trouvée</li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{# {% else %} #}
{# <div class="card mb-4"> #}
{# <div class="card-header bg-warning text-dark"> #}
@ -796,6 +1097,41 @@
</div>
</div>
{% if staleness_distribution is defined and staleness_distribution is not null %}
<div class="card mb-4">
<div class="card-header">
<h2>Répartition des scores de décrépitude</h2>
</div>
<div class="card-body">
<p>Ce graphique montre la répartition des scores de décrépitude pour toutes les pages wiki et où se situe la page courante :</p>
<div class="mb-4">
<canvas id="distributionChart" width="400" height="200"></canvas>
</div>
<div class="alert alert-info">
<p><strong>Statistiques de décrépitude :</strong></p>
<div class="row">
<div class="col-md-6">
<ul>
<li><strong>Nombre total de pages :</strong> {{ staleness_distribution.totalPages }}</li>
<li><strong>Score minimum :</strong> {{ staleness_distribution.min|round(2) }}</li>
<li><strong>Score maximum :</strong> {{ staleness_distribution.max|round(2) }}</li>
</ul>
</div>
<div class="col-md-6">
<ul>
<li><strong>Score moyen :</strong> {{ staleness_distribution.avg|round(2) }}</li>
<li><strong>Score médian :</strong> {{ staleness_distribution.median|round(2) }}</li>
<li><strong>Score de cette page :</strong> {{ staleness_distribution.currentPageScore|round(2) }}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if history_data is defined and history_data|length > 0 %}
<div class="card mb-4">
<div class="card-header">
@ -1078,6 +1414,162 @@
});
});
// Create ranking evolution chart if the element exists
const rankingEvolutionChartElement = document.getElementById('rankingEvolutionChart');
const historyData = {{ history_data|json_encode|raw }};
if (rankingEvolutionChartElement && historyData && historyData.length > 0) {
// Format the data for the chart
const dates = historyData.map(entry => entry.date);
const stalenessScores = historyData.map(entry => entry.metrics.staleness_score || 0);
const wordDiffs = historyData.map(entry => entry.metrics.word_diff || 0);
const sectionDiffs = historyData.map(entry => entry.metrics.section_diff || 0);
const linkDiffs = historyData.map(entry => entry.metrics.link_diff || 0);
// Create the chart
new Chart(rankingEvolutionChartElement, {
type: 'line',
data: {
labels: dates,
datasets: [
{
label: 'Score de décrépitude',
data: stalenessScores,
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
fill: false,
tension: 0.1
},
{
label: 'Différence de mots / 10',
data: wordDiffs.map(val => val / 10),
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.2)',
fill: false,
tension: 0.1
},
{
label: 'Différence de sections',
data: sectionDiffs,
borderColor: 'rgba(255, 206, 86, 1)',
backgroundColor: 'rgba(255, 206, 86, 0.2)',
fill: false,
tension: 0.1
},
{
label: 'Différence de liens',
data: linkDiffs,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
fill: false,
tension: 0.1
}
]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Évolution des métriques au fil du temps'
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// Create distribution histogram if the element exists
const distributionChartElement = document.getElementById('distributionChart');
if (distributionChartElement) {
// Get staleness distribution data from the template
const distributionData = {{ staleness_distribution|json_encode|raw }};
if (distributionData) {
// Prepare data for the histogram
const labels = distributionData.binLabels;
const counts = distributionData.bins;
const currentPageBin = distributionData.currentPageBin;
// Create colors array with the current page bin highlighted
const colors = counts.map((_, index) =>
index === currentPageBin ? 'rgba(255, 99, 132, 0.8)' : 'rgba(54, 162, 235, 0.6)'
);
// Create the histogram
const ctx = distributionChartElement.getContext('2d');
const distributionChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Nombre de pages',
data: counts,
backgroundColor: colors,
borderColor: colors.map(color => color.replace('0.6', '1').replace('0.8', '1')),
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Nombre de pages'
}
},
x: {
title: {
display: true,
text: 'Score de décrépitude'
}
}
},
plugins: {
title: {
display: true,
text: 'Répartition des scores de décrépitude (la page courante est en rouge)'
},
tooltip: {
callbacks: {
title: function(tooltipItems) {
const item = tooltipItems[0];
const binRange = item.label;
return `Score: ${binRange}`;
},
label: function(context) {
let label = 'Nombre de pages: ' + context.raw;
if (context.dataIndex === currentPageBin) {
label += ' (inclut la page courante)';
}
return label;
},
afterLabel: function(context) {
if (context.dataIndex === currentPageBin) {
return `Score de cette page: ${distributionData.currentPageScore.toFixed(2)}`;
}
return '';
}
}
},
legend: {
display: false
}
}
}
});
}
}
// Create history chart if the element exists
const historyChartElement = document.getElementById('historyChart');
if (historyChartElement) {

View file

@ -84,6 +84,7 @@ python3 wiki_compare.py</code></pre>
{# {{ page.reason }}#}
</td>
<td class="text-center">
{% if page.word_diff is defined %}
{% if page.word_diff > 0 %}
<span class="badge bg-danger">{{ page.word_diff }}</span>
{% elseif page.word_diff < 0 %}
@ -91,8 +92,12 @@ python3 wiki_compare.py</code></pre>
{% else %}
<span class="badge bg-secondary">0</span>
{% endif %}
{% else %}
<span class="badge bg-secondary">N/A</span>
{% endif %}
</td>
<td class="text-center">
{% if page.section_diff is defined %}
{% if page.section_diff > 0 %}
<span class="badge bg-danger">{{ page.section_diff }}</span>
{% elseif page.section_diff < 0 %}
@ -100,8 +105,12 @@ python3 wiki_compare.py</code></pre>
{% else %}
<span class="badge bg-secondary">0</span>
{% endif %}
{% else %}
<span class="badge bg-secondary">N/A</span>
{% endif %}
</td>
<td class="text-center">
{% if page.link_diff is defined %}
{% if page.link_diff > 0 %}
<span class="badge bg-danger">{{ page.link_diff }}</span>
{% elseif page.link_diff < 0 %}
@ -109,8 +118,12 @@ python3 wiki_compare.py</code></pre>
{% else %}
<span class="badge bg-secondary">0</span>
{% endif %}
{% else %}
<span class="badge bg-secondary">N/A</span>
{% endif %}
</td>
<td class="text-center">
{% if page.staleness_score is defined %}
<div class="progress" style="height: 20px;">
{% set score_class = page.staleness_score > 70 ? 'bg-danger' : (page.staleness_score > 40 ? 'bg-warning' : 'bg-success') %}
<div class="progress-bar {{ score_class }}" role="progressbar"
@ -121,13 +134,22 @@ python3 wiki_compare.py</code></pre>
{{ page.staleness_score }}
</div>
</div>
{% else %}
<span class="badge bg-secondary">N/A</span>
{% endif %}
</td>
<td class="text-center">
<div class="btn-group" role="group">
<a href="{{ page.en_page.url }}" target="_blank"
{% if page.url is defined and page.url %}
<a href="{{ page.url }}" target="_blank"
class="btn btn-sm btn-outline-primary" title="Version anglaise">
<i class="bi bi-translate"></i> EN
</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary" disabled>
<i class="bi bi-translate"></i> EN (URL manquante)
</button>
{% endif %}
{% if page.fr_page is defined and page.fr_page %}
{% if page.fr_page.url is defined %}
<a href="{{ page.fr_page.url }}" target="_blank"
@ -207,9 +229,9 @@ python3 wiki_compare.py</code></pre>
<tr>
<td>
<div class="d-flex align-items-center">
{% if page.en_page.description_img_url is defined and page.en_page.description_img_url %}
{% if page.description_img_url is defined and page.description_img_url %}
<div class="me-3">
<img src="{{ page.en_page.description_img_url }}"
<img src="{{ page.description_img_url }}"
alt="{% if page.key is defined %}{{ page.key }}{% elseif page.title is defined %}{{ page.title }}{% else %}Image{% endif %}"
style="max-width: 80px; max-height: 60px; object-fit: contain;">
</div>
@ -223,6 +245,7 @@ python3 wiki_compare.py</code></pre>
{# {{ page.reason }}#}
</td>
<td>
{% if page.staleness_score is defined %}
<div class="progress" style="height: 20px;">
{% set score_class = page.staleness_score > 70 ? 'bg-danger' : (page.staleness_score > 40 ? 'bg-warning' : 'bg-success') %}
<div class="progress-bar {{ score_class }}" role="progressbar"
@ -233,13 +256,22 @@ python3 wiki_compare.py</code></pre>
{{ page.staleness_score }}
</div>
</div>
{% else %}
<span class="badge bg-secondary">N/A</span>
{% endif %}
</td>
<td class="text-center">
<div class="btn-group" role="group">
<a href="{{ page.en_page.url }}" target="_blank"
{% if page.url is defined and page.url %}
<a href="{{ page.url }}" target="_blank"
class="btn btn-sm btn-outline-primary" title="Version anglaise">
<i class="bi bi-translate"></i> EN
</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary" disabled>
<i class="bi bi-translate"></i> EN (URL manquante)
</button>
{% endif %}
{% if page.fr_page is defined and page.fr_page %}
{% if page.fr_page.url is defined %}
<a href="{{ page.fr_page.url }}" target="_blank"
@ -320,6 +352,7 @@ python3 wiki_compare.py</code></pre>
{% else %}
labels.push("Page sans clé");
{% endif %}
{% if page.staleness_score is defined %}
scores.push({{ page.staleness_score }});
// Set color based on score
@ -336,6 +369,10 @@ python3 wiki_compare.py</code></pre>
{% else %}
colors.push('rgba(25, 135, 84, 0.7)'); // success (green)
{% endif %}
{% else %}
scores.push(0);
colors.push('rgba(108, 117, 125, 0.7)'); // secondary (gray)
{% endif %}
{% endfor %}
// Sort data by score (descending)

View file

@ -0,0 +1,566 @@
{% 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 %}

File diff suppressed because one or more lines are too long

View file

@ -116,7 +116,8 @@ SPECIFIC_PAGES = [
"Mapping_private_information",
"Any_tags_you_like",
"Organised_Editing/Best_Practices",
"Map_features"
"Map_features",
"Wiki"
]
def fetch_desynchronized_pages():
@ -280,12 +281,104 @@ def save_to_json(data, filename):
except IOError as e:
logger.error(f"Error saving data to {filename}: {e}")
def calculate_global_metrics(data):
"""
Calculate global metrics for all pages in the data
Args:
data: Data containing regular_pages and specific_pages
Returns:
dict: Dictionary with global metrics
"""
# Combine regular and specific pages for global metrics
all_pages = data.get('regular_pages', []) + data.get('specific_pages', [])
# Initialize metrics
metrics = {
'total_pages': len(all_pages),
'avg_sections': 0,
'avg_words': 0,
'avg_links': 0,
'avg_images': 0,
'avg_categories': 0,
'avg_staleness': 0,
'pages_with_en_fr': 0,
'pages_missing_fr': 0,
'staleness_distribution': {
'0-20': 0,
'21-40': 0,
'41-60': 0,
'61-80': 0,
'81-100': 0,
'100+': 0
}
}
# Skip if no pages
if not all_pages:
return metrics
# Calculate totals
total_sections = 0
total_words = 0
total_links = 0
total_images = 0
total_categories = 0
total_staleness = 0
for page in all_pages:
# Count pages with/without French version
if page.get('fr_page'):
metrics['pages_with_en_fr'] += 1
else:
metrics['pages_missing_fr'] += 1
# Add to staleness distribution
staleness = page.get('staleness_score', 0)
total_staleness += staleness
if staleness <= 20:
metrics['staleness_distribution']['0-20'] += 1
elif staleness <= 40:
metrics['staleness_distribution']['21-40'] += 1
elif staleness <= 60:
metrics['staleness_distribution']['41-60'] += 1
elif staleness <= 80:
metrics['staleness_distribution']['61-80'] += 1
elif staleness <= 100:
metrics['staleness_distribution']['81-100'] += 1
else:
metrics['staleness_distribution']['100+'] += 1
# Add to totals
total_sections += page.get('section_diff', 0) if 'section_diff' in page else 0
total_words += page.get('word_diff', 0) if 'word_diff' in page else 0
total_links += page.get('link_diff', 0) if 'link_diff' in page else 0
total_images += page.get('media_diff', 0) if 'media_diff' in page else 0
# Count categories if available
if page.get('category_comparison'):
cat_count = len(page['category_comparison'].get('en_only', []))
total_categories += cat_count
# Calculate averages
metrics['avg_sections'] = round(total_sections / len(all_pages), 2)
metrics['avg_words'] = round(total_words / len(all_pages), 2)
metrics['avg_links'] = round(total_links / len(all_pages), 2)
metrics['avg_images'] = round(total_images / len(all_pages), 2)
metrics['avg_categories'] = round(total_categories / len(all_pages), 2)
metrics['avg_staleness'] = round(total_staleness / len(all_pages), 2)
return metrics
def save_with_history(data, filename):
"""
Save data to a JSON file while preserving history
This function loads existing data from the file (if it exists),
adds the new data to the history, and saves the updated data back to the file.
It also calculates global metrics for the current data.
Args:
data: New data to save
@ -302,10 +395,14 @@ def save_with_history(data, filename):
if 'history' not in existing_data:
existing_data['history'] = {}
# Add current regular_pages and specific_pages to history
# Calculate global metrics for the current data
global_metrics = calculate_global_metrics(data)
# Add current regular_pages, specific_pages, and global metrics to history
history_entry = {
'regular_pages': data.get('regular_pages', []),
'specific_pages': data.get('specific_pages', [])
'specific_pages': data.get('specific_pages', []),
'global_metrics': global_metrics
}
# Add the entry to history with timestamp as key
@ -314,6 +411,7 @@ def save_with_history(data, filename):
# Update the current data
existing_data['regular_pages'] = data.get('regular_pages', [])
existing_data['specific_pages'] = data.get('specific_pages', [])
existing_data['global_metrics'] = global_metrics
existing_data['last_updated'] = current_timestamp
# Save the updated data
@ -321,11 +419,120 @@ def save_with_history(data, filename):
json.dump(existing_data, f, indent=2, ensure_ascii=False)
logger.info(f"Data with history saved to {filename}")
# Also save a separate ranking history file
save_ranking_history(existing_data, "page_rankings.json")
except (IOError, json.JSONDecodeError) as e:
logger.error(f"Error saving data with history to {filename}: {e}")
# Fallback to regular save if there's an error
save_to_json(data, filename)
def save_ranking_history(data, filename):
"""
Save ranking history to a separate JSON file
This function extracts ranking data from the history and saves it in a format
optimized for displaying ranking evolution over time.
Args:
data: Data containing history entries
filename (str): Name of the file to save rankings
"""
try:
# Initialize ranking data structure
ranking_data = {
'timestamps': [],
'pages': {},
'global_metrics': {}
}
# Extract history entries
history = data.get('history', {})
# Sort timestamps chronologically
sorted_timestamps = sorted(history.keys())
ranking_data['timestamps'] = sorted_timestamps
# Process each page to track its metrics over time
all_page_keys = set()
# First, collect all unique page keys across all history entries
for timestamp in sorted_timestamps:
entry = history[timestamp]
# Add global metrics for this timestamp
if 'global_metrics' in entry:
ranking_data['global_metrics'][timestamp] = entry['global_metrics']
# Collect page keys from regular pages
for page in entry.get('regular_pages', []):
all_page_keys.add(page['key'])
# Collect page keys from specific pages
for page in entry.get('specific_pages', []):
all_page_keys.add(page['key'])
# Initialize data structure for each page
for page_key in all_page_keys:
ranking_data['pages'][page_key] = {
'title': page_key,
'metrics': {}
}
# Fill in metrics for each page at each timestamp
for timestamp in sorted_timestamps:
entry = history[timestamp]
# Process regular pages
for page in entry.get('regular_pages', []):
page_key = page['key']
# Extract metrics we want to track
metrics = {
'staleness_score': page.get('staleness_score', 0),
'word_diff': page.get('word_diff', 0),
'section_diff': page.get('section_diff', 0),
'link_diff': page.get('link_diff', 0),
'media_diff': page.get('media_diff', 0)
}
# Store metrics for this timestamp
ranking_data['pages'][page_key]['metrics'][timestamp] = metrics
# Store page title if available
if 'en_page' in page and page['en_page']:
ranking_data['pages'][page_key]['title'] = page['en_page'].get('page_title', page_key)
# Process specific pages
for page in entry.get('specific_pages', []):
page_key = page['key']
# Extract metrics we want to track
metrics = {
'staleness_score': page.get('staleness_score', 0),
'word_diff': page.get('word_diff', 0),
'section_diff': page.get('section_diff', 0),
'link_diff': page.get('link_diff', 0),
'media_diff': page.get('media_diff', 0)
}
# Store metrics for this timestamp
ranking_data['pages'][page_key]['metrics'][timestamp] = metrics
# Store page title if available
if 'en_page' in page and page['en_page']:
ranking_data['pages'][page_key]['title'] = page['en_page'].get('page_title', page_key)
# Save the ranking data
with open(filename, 'w', encoding='utf-8') as f:
json.dump(ranking_data, f, indent=2, ensure_ascii=False)
logger.info(f"Ranking history saved to {filename}")
except (IOError, json.JSONDecodeError) as e:
logger.error(f"Error saving ranking history to {filename}: {e}")
def check_grammar_with_grammalecte(text):
"""
Check grammar in French text using grammalecte-cli

View file

@ -12,6 +12,8 @@ Key:harassment_prevention,en,https://wiki.openstreetmap.org/wiki/Key:harassment_
Key:harassment_prevention,fr,https://wiki.openstreetmap.org/wiki/FR:Key:harassment_prevention,2025-07-03,15,328,83,14,66.72,https://wiki.openstreetmap.org/w/images/thumb/7/76/Osm_element_node.svg/30px-Osm_element_node.svg.png
Proposal process,en,https://wiki.openstreetmap.org/wiki/Proposal process,2025-08-13,46,5292,202,4,172.34,https://wiki.openstreetmap.org/w/images/thumb/c/c2/Save_proposal_first.png/761px-Save_proposal_first.png
Proposal process,fr,https://wiki.openstreetmap.org/wiki/FR:Proposal process,2023-09-22,15,0,0,0,172.34,
Outil de Manipulation et d'Organisation,en,https://wiki.openstreetmap.org/wiki/Outil de Manipulation et d'Organisation,2025-09-02,9,0,0,0,0.6,
Outil de Manipulation et d'Organisation,fr,https://wiki.openstreetmap.org/wiki/FR:Outil de Manipulation et d'Organisation,2025-09-02,13,0,0,0,0.6,
Automated_Edits_code_of_conduct,en,https://wiki.openstreetmap.org/wiki/Automated_Edits_code_of_conduct,2025-07-26,19,0,0,0,23.1,
Automated_Edits_code_of_conduct,fr,https://wiki.openstreetmap.org/wiki/FR:Automated_Edits_code_of_conduct,2025-04-03,17,0,0,0,23.1,
Key:cuisine,en,https://wiki.openstreetmap.org/wiki/Key:cuisine,2025-07-23,17,3422,693,303,107.73,https://upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Food_montage.jpg/200px-Food_montage.jpg
@ -44,6 +46,8 @@ Any_tags_you_like,fr,https://wiki.openstreetmap.org/wiki/FR:Any_tags_you_like,20
Organised_Editing/Best_Practices,en,https://wiki.openstreetmap.org/wiki/Organised_Editing/Best_Practices,2025-07-18,16,501,10,1,100,https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/Ambox_warning_pn.svg/40px-Ambox_warning_pn.svg.png
Map_features,en,https://wiki.openstreetmap.org/wiki/Map_features,2025-07-21,125,21926,4255,2222,507.98,https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/Bar_MXCT.JPG/100px-Bar_MXCT.JPG
Map_features,fr,https://wiki.openstreetmap.org/wiki/FR:Map_features,2018-12-27,103,23159,5516,3062,507.98,https://wiki.openstreetmap.org/w/images/c/c4/Aerialway_gondola_render.png
Wiki,en,https://wiki.openstreetmap.org/wiki/Wiki,2025-02-24,16,669,40,1,302.87,https://wiki.openstreetmap.org/w/images/thumb/b/b7/OpenStreetMap_Wiki_MainPage.png/300px-OpenStreetMap_Wiki_MainPage.png
Wiki,fr,https://wiki.openstreetmap.org/wiki/FR:Wiki,2021-01-04,14,645,37,1,302.87,https://wiki.openstreetmap.org/w/images/thumb/b/b7/OpenStreetMap_Wiki_MainPage.png/300px-OpenStreetMap_Wiki_MainPage.png
https://wiki.openstreetmap.org/wiki/FR:Quality_Assurance,fr,https://wiki.openstreetmap.org/wiki/FR:Quality_Assurance,2015-05-16,16,0,0,0,0,
https://wiki.openstreetmap.org/wiki/Quality_Assurance,en,https://wiki.openstreetmap.org/wiki/Quality_Assurance,2025-06-01,19,0,0,0,100,
https://wiki.openstreetmap.org/wiki/FR:Nominatim/Installation,fr,https://wiki.openstreetmap.org/wiki/FR:Nominatim/Installation,2016-08-22,32,0,0,0,0,

1 key language url last_modified sections word_count link_count media_count staleness_score description_img_url
12 Key:harassment_prevention fr https://wiki.openstreetmap.org/wiki/FR:Key:harassment_prevention 2025-07-03 15 328 83 14 66.72 https://wiki.openstreetmap.org/w/images/thumb/7/76/Osm_element_node.svg/30px-Osm_element_node.svg.png
13 Proposal process en https://wiki.openstreetmap.org/wiki/Proposal process 2025-08-13 46 5292 202 4 172.34 https://wiki.openstreetmap.org/w/images/thumb/c/c2/Save_proposal_first.png/761px-Save_proposal_first.png
14 Proposal process fr https://wiki.openstreetmap.org/wiki/FR:Proposal process 2023-09-22 15 0 0 0 172.34
15 Outil de Manipulation et d'Organisation en https://wiki.openstreetmap.org/wiki/Outil de Manipulation et d'Organisation 2025-09-02 9 0 0 0 0.6
16 Outil de Manipulation et d'Organisation fr https://wiki.openstreetmap.org/wiki/FR:Outil de Manipulation et d'Organisation 2025-09-02 13 0 0 0 0.6
17 Automated_Edits_code_of_conduct en https://wiki.openstreetmap.org/wiki/Automated_Edits_code_of_conduct 2025-07-26 19 0 0 0 23.1
18 Automated_Edits_code_of_conduct fr https://wiki.openstreetmap.org/wiki/FR:Automated_Edits_code_of_conduct 2025-04-03 17 0 0 0 23.1
19 Key:cuisine en https://wiki.openstreetmap.org/wiki/Key:cuisine 2025-07-23 17 3422 693 303 107.73 https://upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Food_montage.jpg/200px-Food_montage.jpg
46 Organised_Editing/Best_Practices en https://wiki.openstreetmap.org/wiki/Organised_Editing/Best_Practices 2025-07-18 16 501 10 1 100 https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/Ambox_warning_pn.svg/40px-Ambox_warning_pn.svg.png
47 Map_features en https://wiki.openstreetmap.org/wiki/Map_features 2025-07-21 125 21926 4255 2222 507.98 https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/Bar_MXCT.JPG/100px-Bar_MXCT.JPG
48 Map_features fr https://wiki.openstreetmap.org/wiki/FR:Map_features 2018-12-27 103 23159 5516 3062 507.98 https://wiki.openstreetmap.org/w/images/c/c4/Aerialway_gondola_render.png
49 Wiki en https://wiki.openstreetmap.org/wiki/Wiki 2025-02-24 16 669 40 1 302.87 https://wiki.openstreetmap.org/w/images/thumb/b/b7/OpenStreetMap_Wiki_MainPage.png/300px-OpenStreetMap_Wiki_MainPage.png
50 Wiki fr https://wiki.openstreetmap.org/wiki/FR:Wiki 2021-01-04 14 645 37 1 302.87 https://wiki.openstreetmap.org/w/images/thumb/b/b7/OpenStreetMap_Wiki_MainPage.png/300px-OpenStreetMap_Wiki_MainPage.png
51 https://wiki.openstreetmap.org/wiki/FR:Quality_Assurance fr https://wiki.openstreetmap.org/wiki/FR:Quality_Assurance 2015-05-16 16 0 0 0 0
52 https://wiki.openstreetmap.org/wiki/Quality_Assurance en https://wiki.openstreetmap.org/wiki/Quality_Assurance 2025-06-01 19 0 0 0 100
53 https://wiki.openstreetmap.org/wiki/FR:Nominatim/Installation fr https://wiki.openstreetmap.org/wiki/FR:Nominatim/Installation 2016-08-22 32 0 0 0 0