up details theme graph page
This commit is contained in:
parent
af1233c246
commit
b959c695ae
3 changed files with 90 additions and 181 deletions
|
@ -81,7 +81,7 @@ THEMES = {
|
|||
},
|
||||
"advertising_board": {
|
||||
"tag_filter": "advertising=board and message=political",
|
||||
"important_tags": ["operator", "content"],
|
||||
"important_tags": ["operator", ],
|
||||
},
|
||||
"building": {
|
||||
"tag_filter": "building",
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\Stats;
|
||||
use App\Entity\CityFollowUp;
|
||||
use App\Entity\Stats;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class FollowUpService
|
||||
|
@ -38,7 +38,7 @@ class FollowUpService
|
|||
} elseif ($type === 'police') {
|
||||
$objects = array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'police') ?? [];
|
||||
} elseif ($type === 'healthcare') {
|
||||
$objects = array_filter($elements, function($el) {
|
||||
$objects = array_filter($elements, function ($el) {
|
||||
return isset($el['tags']['healthcare'])
|
||||
|| ($el['tags']['amenity'] ?? null) === 'doctors'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'pharmacy'
|
||||
|
@ -74,7 +74,7 @@ class FollowUpService
|
|||
$objects = array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'public_bookcase') ?? [];
|
||||
} elseif ($type === 'playground') {
|
||||
$objects = array_filter($elements, fn($el) => ($el['tags']['leisure'] ?? null) === 'playground') ?? [];
|
||||
}elseif ($type === 'restaurant') {
|
||||
} elseif ($type === 'restaurant') {
|
||||
$objects = array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'restaurant') ?? [];
|
||||
} else {
|
||||
$objects = [];
|
||||
|
@ -133,8 +133,7 @@ class FollowUpService
|
|||
$completed[] = $el;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ... fallback pour les types sans tags attendus
|
||||
} // ... fallback pour les types sans tags attendus
|
||||
else {
|
||||
$completed = [];
|
||||
$partialCompletions = array_fill(0, count($data['objects']), 0);
|
||||
|
@ -377,11 +376,11 @@ class FollowUpService
|
|||
$now = new \DateTime();
|
||||
$refDate = clone $now;
|
||||
$refDate->modify('-7 days');
|
||||
|
||||
|
||||
// Récupérer toutes les mesures pour ce type
|
||||
$countData = [];
|
||||
$completionData = [];
|
||||
|
||||
|
||||
foreach ($followups as $fu) {
|
||||
if ($fu->getName() === $type . '_count') {
|
||||
$countData[] = [
|
||||
|
@ -396,20 +395,20 @@ class FollowUpService
|
|||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Trier par date
|
||||
usort($countData, fn($a, $b) => $a['date'] <=> $b['date']);
|
||||
usort($completionData, fn($a, $b) => $a['date'] <=> $b['date']);
|
||||
|
||||
|
||||
$countDelta = self::calculateDelta($countData, $refDate);
|
||||
$completionDelta = self::calculateDelta($completionData, $refDate);
|
||||
|
||||
|
||||
return [
|
||||
'count' => $countDelta,
|
||||
'completion' => $completionDelta
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calcule le delta pour une série de données
|
||||
*/
|
||||
|
@ -418,9 +417,9 @@ class FollowUpService
|
|||
if (empty($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
$last = end($data)['value'];
|
||||
|
||||
|
||||
// Chercher la mesure exacte à la date de référence
|
||||
$exactRef = null;
|
||||
foreach (array_reverse($data) as $point) {
|
||||
|
@ -429,18 +428,18 @@ class FollowUpService
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Si on a trouvé une mesure exacte, l'utiliser
|
||||
if ($exactRef !== null) {
|
||||
return $last - $exactRef;
|
||||
}
|
||||
|
||||
|
||||
// Sinon, chercher les deux mesures les plus proches pour faire une interpolation
|
||||
$beforeRef = null;
|
||||
$afterRef = null;
|
||||
$beforeDate = null;
|
||||
$afterDate = null;
|
||||
|
||||
|
||||
// Chercher la mesure juste avant la date de référence
|
||||
foreach (array_reverse($data) as $point) {
|
||||
if ($point['date'] < $refDate) {
|
||||
|
@ -449,7 +448,7 @@ class FollowUpService
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Chercher la mesure juste après la date de référence
|
||||
foreach ($data as $point) {
|
||||
if ($point['date'] > $refDate) {
|
||||
|
@ -458,7 +457,7 @@ class FollowUpService
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Si on a les deux mesures, faire une interpolation linéaire
|
||||
if ($beforeRef !== null && $afterRef !== null && $beforeDate !== null && $afterDate !== null) {
|
||||
$timeDiff = $afterDate->getTimestamp() - $beforeDate->getTimestamp();
|
||||
|
@ -467,17 +466,17 @@ class FollowUpService
|
|||
$interpolatedRef = $beforeRef + ($afterRef - $beforeRef) * $ratio;
|
||||
return $last - $interpolatedRef;
|
||||
}
|
||||
|
||||
|
||||
// Si on n'a qu'une mesure avant, l'utiliser
|
||||
if ($beforeRef !== null) {
|
||||
return $last - $beforeRef;
|
||||
}
|
||||
|
||||
|
||||
// Si on n'a qu'une mesure après, l'utiliser
|
||||
if ($afterRef !== null) {
|
||||
return $last - $afterRef;
|
||||
}
|
||||
|
||||
|
||||
// Si aucune mesure n'est disponible, retourner null
|
||||
return null;
|
||||
}
|
||||
|
@ -501,7 +500,7 @@ class FollowUpService
|
|||
'police' => ['phone', 'website'],
|
||||
'healthcare' => ['name', 'contact:phone', 'phone', 'email', 'contact:email'],
|
||||
'bicycle_parking' => ['capacity', 'covered'],
|
||||
'advertising_board' => ['operator', 'contact:phone'],
|
||||
'advertising_board' => ['operator'],
|
||||
'building' => ['building'],
|
||||
'rnb' => ['building', 'ref:FR:RNB'],
|
||||
'email' => ['name', 'phone'],
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
<link href='{{ asset('js/maplibre/maplibre-gl.css') }}' rel='stylesheet'/>
|
||||
<link href='{{ asset('css/city-sidebar.css') }}' rel='stylesheet'/>
|
||||
<style>
|
||||
#themeMap {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
|
@ -101,9 +105,6 @@
|
|||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.btn-josm {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Bouton flottant suggestion desktop */
|
||||
.suggestion-float-btn {
|
||||
|
@ -230,11 +231,19 @@
|
|||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="currentCount">{{ current_count }}</div>
|
||||
<div class="stat-value" id="currentCount">
|
||||
{{ current_count }}
|
||||
<i class="bi bi-load bi-spin"></i>
|
||||
...
|
||||
</div>
|
||||
<div class="stat-label">Nombre actuel</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="currentCompletion">{{ current_completion }}</div>
|
||||
<div class="stat-value" id="currentCompletion">
|
||||
{{ current_completion }}
|
||||
<i class="bi bi-load bi-spin"></i>
|
||||
...
|
||||
</div>
|
||||
<div class="stat-label">Complétion actuelle</div>
|
||||
</div>
|
||||
{# <div class="stat-card"> #}
|
||||
|
@ -251,11 +260,6 @@
|
|||
<div class="chart-container">
|
||||
<canvas id="themeChart"></canvas>
|
||||
</div>
|
||||
{% if completion_tags is defined %}
|
||||
completion_tags
|
||||
{{ dump(completion_tags[theme]) }}
|
||||
|
||||
{% endif %}
|
||||
{% if completion_tags is defined and completion_tags[theme] is defined %}
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
|
@ -264,7 +268,11 @@
|
|||
<div class="card-body p-2">
|
||||
<ul class="mb-0">
|
||||
{% for tag in completion_tags[theme] %}
|
||||
<li><code>{{ tag }}</code></li>
|
||||
<li>
|
||||
<a href="https://wiki.openstreetmap.org/FR:Key:{{ tag }}">
|
||||
<code>{{ tag }}</code>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><span class="text-muted">Aucun critère défini</span></li>
|
||||
{% endfor %}
|
||||
|
@ -273,11 +281,11 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card mt-5">
|
||||
<div class="card mt-5 ">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-tags"></i> Statistiques des tags utilisés dans les objets trouvés
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<div class="card-body p-4">
|
||||
<div id="tags-stats-block">
|
||||
<table class="table table-sm table-bordered mb-0" id="tags-stats-table"
|
||||
style="max-width:600px;">
|
||||
|
@ -298,12 +306,12 @@
|
|||
{# Bloc navigation autres thématiques #}
|
||||
{% if followup_labels is defined and icons is defined %}
|
||||
<hr>
|
||||
<div class="mt-4">
|
||||
<div class="mt-4 p-6 m-4">
|
||||
<h4>Autres thématiques de suivi :</h4>
|
||||
<ul class="list-inline">
|
||||
<ul class="list-inline p-6 m-4">
|
||||
{% for t, label in followup_labels %}
|
||||
{% if t != theme %}
|
||||
<li class="list-inline-item mb-2">
|
||||
<li class="list-inline-item mb-2 ml-4">
|
||||
<a href="{{ path('admin_followup_theme_graph', {'insee_code': stats.zone, 'theme': t}) }}"
|
||||
class="btn btn-outline-secondary">
|
||||
<i class="bi {{ icons[t]|default('bi-question-circle') }}"></i> {{ label }}
|
||||
|
@ -543,6 +551,7 @@
|
|||
const countData = {{ count_data|json_encode|raw }};
|
||||
console.log('Count data:', countData);
|
||||
|
||||
window.countData = countData
|
||||
const completionData = {{ completion_data|json_encode|raw }};
|
||||
console.log('Completion data:', completionData);
|
||||
|
||||
|
@ -622,7 +631,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
// Graphique fusionné
|
||||
console.log('completionData', completionData)
|
||||
const ctx = document.getElementById('themeChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
|
@ -637,152 +646,53 @@
|
|||
fill: true,
|
||||
tension: 0.1,
|
||||
yAxisID: 'y1',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Pourcentage de complétion',
|
||||
data
|
||||
:
|
||||
Array.isArray(completionData) ? completionData.map(d => ({x: new Date(d.date), y: d.value})) : [],
|
||||
borderColor
|
||||
:
|
||||
'#198754',
|
||||
backgroundColor
|
||||
:
|
||||
'rgba(25, 135, 84, 0.1)',
|
||||
borderWidth
|
||||
:
|
||||
2,
|
||||
fill
|
||||
:
|
||||
true,
|
||||
tension
|
||||
:
|
||||
0.1,
|
||||
yAxisID
|
||||
:
|
||||
'y2',
|
||||
}
|
||||
,
|
||||
// Add current data point if no historical data exists
|
||||
...
|
||||
((!Array.isArray(countData) || countData.length === 0) && typeof currentCount !== 'undefined' ? [{
|
||||
label: "Nombre actuel",
|
||||
data: [{x: new Date(), y: currentCount}],
|
||||
borderColor: '#dc3545',
|
||||
backgroundColor: 'rgba(220, 53, 69, 0.1)',
|
||||
borderWidth: 2,
|
||||
pointRadius: 5,
|
||||
fill: false,
|
||||
yAxisID: 'y1',
|
||||
}] : []),
|
||||
...
|
||||
((!Array.isArray(completionData) || completionData.length === 0) && typeof currentCompletion !== 'undefined' ? [{
|
||||
label: "Complétion actuelle",
|
||||
data: [{x: new Date(), y: currentCompletion}],
|
||||
borderColor: '#fd7e14',
|
||||
backgroundColor: 'rgba(253, 126, 20, 0.1)',
|
||||
borderWidth: 2,
|
||||
pointRadius: 5,
|
||||
fill: false,
|
||||
yAxisID: 'y2',
|
||||
}] : [])
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio
|
||||
:
|
||||
false,
|
||||
plugins
|
||||
:
|
||||
{
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
,
|
||||
tooltip: {
|
||||
mode: 'index', intersect
|
||||
:
|
||||
false
|
||||
}
|
||||
}
|
||||
,
|
||||
interaction: {
|
||||
mode: 'nearest', axis
|
||||
:
|
||||
'x', intersect
|
||||
:
|
||||
false
|
||||
}
|
||||
,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time
|
||||
:
|
||||
},
|
||||
{
|
||||
unit: 'day', displayFormats
|
||||
:
|
||||
{
|
||||
day: 'dd/MM/yyyy'
|
||||
}
|
||||
label: 'Pourcentage de complétion',
|
||||
data: Array.isArray(completionData) ? completionData.map(d => ({
|
||||
x: new Date(d.date),
|
||||
y: d.value
|
||||
})) : [],
|
||||
borderColor: '#198754',
|
||||
backgroundColor: 'rgba(25, 135, 84, 0.1)',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0.1,
|
||||
yAxisID: 'y2',
|
||||
}
|
||||
,
|
||||
title: {
|
||||
display: true, text
|
||||
:
|
||||
'Date'
|
||||
}
|
||||
}
|
||||
,
|
||||
y1: {
|
||||
type: 'linear',
|
||||
position
|
||||
:
|
||||
'left',
|
||||
title
|
||||
:
|
||||
{
|
||||
display: true, text
|
||||
:
|
||||
"Nombre d'objets"
|
||||
}
|
||||
,
|
||||
beginAtZero: true
|
||||
}
|
||||
,
|
||||
y2: {
|
||||
type: 'linear',
|
||||
position
|
||||
:
|
||||
'right',
|
||||
title
|
||||
:
|
||||
{
|
||||
display: true, text
|
||||
:
|
||||
'Complétion (%)'
|
||||
}
|
||||
,
|
||||
min: 0,
|
||||
max
|
||||
:
|
||||
100,
|
||||
grid
|
||||
:
|
||||
{
|
||||
drawOnChartArea: false
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {display: true},
|
||||
tooltip: {mode: 'index', intersect: false}
|
||||
},
|
||||
interaction: {mode: 'nearest', axis: 'x', intersect: false},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {unit: 'day', displayFormats: {day: 'dd/MM/yyyy'}},
|
||||
title: {display: true, text: 'Date'}
|
||||
},
|
||||
y1: {
|
||||
type: 'linear',
|
||||
position: 'left',
|
||||
title: {display: true, text: "Nombre d'objets"},
|
||||
beginAtZero: true
|
||||
},
|
||||
y2: {
|
||||
type: 'linear',
|
||||
position: 'right',
|
||||
title: {display: true, text: 'Complétion (%)'},
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: {drawOnChartArea: false}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
;
|
||||
});
|
||||
|
||||
// Initialiser les statistiques
|
||||
updateStats();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue