bubble fraicheur des completions ajouté

This commit is contained in:
Tykayn 2025-06-24 13:16:48 +02:00 committed by tykayn
parent cd8369d08c
commit 93086eba60
18 changed files with 179 additions and 66 deletions

View file

@ -34,6 +34,12 @@ import {
updateMapHeightForLargeScreens
} from './utils.js';
import Tablesort from 'tablesort';
import TableSort from 'table-sort-js/table-sort.js';
import $ from 'jquery';
window.$ = $;
window.jQuery = $;
// Charger table-sortable (version non minifiée locale)
import '../assets/js/table-sortable.js';
window.Chart = Chart;
window.genererCouleurPastel = genererCouleurPastel;
@ -44,6 +50,7 @@ window.ChartDataLabels = ChartDataLabels;
window.maplibregl = maplibregl;
window.toggleCompletionInfo = toggleCompletionInfo;
window.updateMapHeightForLargeScreens = updateMapHeightForLargeScreens;
window.Tablesort = Tablesort;
Chart.register(ChartDataLabels);
@ -112,11 +119,6 @@ document.addEventListener('DOMContentLoaded', () => {
}
updateCompletionProgress();
// Activer le tri sur tous les tableaux désignés
document.querySelectorAll('.js-sort-table').forEach(table => {
new Tablesort(table);
});
// Focus sur le premier champ texte au chargement
// const firstTextInput = document.querySelector('input.form-control');
// if (firstTextInput) {
@ -131,13 +133,6 @@ document.addEventListener('DOMContentLoaded', () => {
parseCuisine();
// Tri automatique des tableaux
// const tables = document.querySelectorAll('table');
// tables.forEach(table => {
// table.classList.add('js-sort-table');
// });
// Modifier la fonction de recherche existante
const searchInput = document.getElementById('app_admin_labourer');
const suggestionList = document.getElementById('suggestionList');
@ -201,4 +196,27 @@ document.addEventListener('DOMContentLoaded', () => {
enableLabourageForm();
adjustListGroupFontSize('.list-group-item');
// Activer le tri naturel sur tous les tableaux avec la classe table-sort
document.querySelectorAll('table.table-sort').forEach(table => {
new TableSort(table);
});
// Initialisation du tri et filtrage sur les tableaux du dashboard et de la page stats
if (document.querySelector('#dashboard-table')) {
$('#dashboard-table').tableSortable({
pagination: false,
showPaginationLabel: true,
searchField: '#dashboard-table-search',
responsive: false
});
}
if (document.querySelector('#stats-table')) {
$('#stats-table').tableSortable({
pagination: false,
showPaginationLabel: true,
searchField: '#stats-table-search',
responsive: false
});
}
});

View file

@ -47,16 +47,17 @@ function waitForChartAndDrawBubble() {
}
// Calcul de la régression linéaire (moindres carrés)
const validPoints = bubbleChartData.filter(d => d.x > 0 && d.y > 0);
// On ne fait la régression que si on veut, mais l'axe X = fraicheur, Y = complétion
const validPoints = bubbleChartData.filter(d => d.x !== null && d.y !== null);
const n = validPoints.length;
let regressionLine = null, slope = 0, intercept = 0;
if (n >= 2) {
let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
validPoints.forEach(d => {
sumX += Math.log10(d.x);
sumX += d.x;
sumY += d.y;
sumXY += Math.log10(d.x) * d.y;
sumXX += Math.log10(d.x) * Math.log10(d.x);
sumXY += d.x * d.y;
sumXX += d.x * d.x;
});
const meanX = sumX / n;
const meanY = sumY / n;
@ -65,8 +66,8 @@ function waitForChartAndDrawBubble() {
const xMin = Math.min(...validPoints.map(d => d.x));
const xMax = Math.max(...validPoints.map(d => d.x));
regressionLine = [
{ x: xMin, y: slope * Math.log10(xMin) + intercept },
{ x: xMax, y: slope * Math.log10(xMax) + intercept }
{ x: xMin, y: slope * xMin + intercept },
{ x: xMax, y: slope * xMax + intercept }
];
}
window.Chart.register(window.ChartDataLabels);
@ -105,10 +106,8 @@ function waitForChartAndDrawBubble() {
].filter(Boolean)
},
options: {
// responsive: true,
plugins: {
datalabels: {
// Désactivé au niveau global, activé par dataset
display: false
},
legend: { display: true },
@ -117,14 +116,14 @@ function waitForChartAndDrawBubble() {
label: (context) => {
const d = context.raw;
if (context.dataset.type === 'line') {
return `Régression: y = ${slope.toFixed(2)} × log10(x) + ${intercept.toFixed(2)}`;
return `Régression: y = ${slope.toFixed(2)} × x + ${intercept.toFixed(2)}`;
}
return [
`${d.label}`,
`Population: ${d.x.toLocaleString()}`,
`Nombre de lieux: ${d.r.toFixed(2)}`,
`Complétion: ${d.y.toFixed(2)}%`,
`Fraîcheur moyenne: ${d.freshnessDays ? d.freshnessDays.toLocaleString() + ' jours' : 'N/A'}`,
`Complétion: ${d.y.toFixed(2)}%`,
`Population: ${d.population ? d.population.toLocaleString() : 'N/A'}`,
`Nombre de lieux: ${d.r.toFixed(2)}`,
`Budget: ${d.budget ? d.budget.toLocaleString() + ' €' : 'N/A'}`,
`Budget/habitant: ${d.budgetParHabitant ? d.budgetParHabitant.toFixed(2) + ' €' : 'N/A'}`,
`Budget/lieu: ${d.budgetParLieu ? d.budgetParLieu.toFixed(2) + ' €' : 'N/A'}`
@ -135,11 +134,13 @@ function waitForChartAndDrawBubble() {
},
scales: {
x: {
type: 'logarithmic',
title: { display: true, text: 'Population (échelle log)' }
type: 'linear',
title: { display: true, text: 'Fraîcheur moyenne (jours, plus petit = plus récent)' }
},
y: {
title: { display: true, text: 'Completion' }
title: { display: true, text: 'Taux de complétion (%)' },
min: 0,
max: 100
}
}
}

File diff suppressed because one or more lines are too long

0
assets/js/table-sortable.min.js vendored Normal file
View file

View file

@ -1,11 +1,7 @@
// Gestion du tri des tableaux
import Tablesort from 'tablesort';
// import Tablesort from 'tablesort';
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.js-sort-table').forEach(table => {
new Tablesort(table);
});
// Gestion du toggle gouttes/ronds sur la carte
const toggle = document.getElementById('toggleMarkers');
if (toggle && window.updateMarkers) {
@ -18,6 +14,6 @@ document.addEventListener('DOMContentLoaded', () => {
// Exposer une fonction pour (ré)appliquer le tri si besoin
export function applyTableSort() {
document.querySelectorAll('.js-sort-table').forEach(table => {
new Tablesort(table);
new window.Tablesort(table);
});
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250624103515 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE place ADD email_content LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, ADD place_count INT DEFAULT NULL
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE place DROP email_content, DROP place_count
SQL);
}
}

17
package-lock.json generated
View file

@ -5,6 +5,9 @@
"packages": {
"": {
"license": "UNLICENSED",
"dependencies": {
"jquery": "^3.7.1"
},
"devDependencies": {
"@babel/core": "^7.17.0",
"@babel/preset-env": "^7.16.0",
@ -14,6 +17,7 @@
"core-js": "^3.38.0",
"maplibre-gl": "^5.6.0",
"regenerator-runtime": "^0.13.9",
"table-sort-js": "^1.22.2",
"tablesort": "^5.6.0",
"webpack": "^5.74.0",
"webpack-cli": "^5.1.0"
@ -3904,6 +3908,12 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/jquery": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -5491,6 +5501,13 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/table-sort-js": {
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/table-sort-js/-/table-sort-js-1.22.2.tgz",
"integrity": "sha512-KUpmoYWH1TCnyiylE0HMCtMeAisl0KYBFjZfBL3CPHOlnhA8jy+RFfZbH6DwCpXAvmK73vsDAX54hg9J4DhuRQ==",
"dev": true,
"license": "MIT"
},
"node_modules/tablesort": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/tablesort/-/tablesort-5.6.0.tgz",

View file

@ -8,6 +8,7 @@
"core-js": "^3.38.0",
"maplibre-gl": "^5.6.0",
"regenerator-runtime": "^0.13.9",
"table-sort-js": "^1.22.2",
"tablesort": "^5.6.0",
"webpack": "^5.74.0",
"webpack-cli": "^5.1.0"
@ -19,5 +20,8 @@
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress"
},
"dependencies": {
"jquery": "^3.7.1"
}
}

View file

@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Request;
use function uuid_create;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\JsonResponse;
use Twig\Environment;
final class AdminController extends AbstractController
{
@ -23,7 +24,8 @@ final class AdminController extends AbstractController
public function __construct(
private EntityManagerInterface $entityManager,
private Motocultrice $motocultrice,
private BudgetService $budgetService
private BudgetService $budgetService,
private Environment $twig
) {
}
@ -99,6 +101,10 @@ final class AdminController extends AbstractController
$this->entityManager->persist($place);
$stats->addPlace($place);
$processedCount++;
// Générer le contenu de l'email avec le template
$emailContent = $this->twig->render('admin/email_content.html.twig', ['place' => $place]);
$place->setEmailContent($emailContent);
} elseif ($updateExisting) {
// Mettre à jour les données depuis Overpass uniquement si updateExisting est true
$existingPlace->update_place_from_overpass_data($placeData);
@ -443,6 +449,8 @@ final class AdminController extends AbstractController
$overpass_osm_ids = array_map(fn($place) => $place['id'], $places_overpass);
$batchSize = 200;
$i = 0;
foreach ($places_overpass as $placeData) {
// Vérifier si le lieu existe déjà (optimisé)
$existingPlace = $placesByOsmId[$placeData['id']] ?? null;
@ -471,6 +479,10 @@ final class AdminController extends AbstractController
$this->entityManager->persist($place);
$stats->addPlace($place);
$processedCount++;
// Générer le contenu de l'email avec le template
$emailContent = $this->twig->render('admin/email_content.html.twig', ['place' => $place]);
$place->setEmailContent($emailContent);
} elseif ($updateExisting) {
$existingPlace->setDead(false);
$existingPlace->update_place_from_overpass_data($placeData);
@ -478,19 +490,19 @@ final class AdminController extends AbstractController
$this->entityManager->persist($existingPlace);
$updatedCount++;
}
}
// Supprimer les lieux qui ne sont plus dans la réponse Overpass, si activé
if ($deleteMissing) {
$db_places = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);
foreach ($db_places as $db_place) {
if (!in_array($db_place->getOsmId(), $overpass_osm_ids)) {
$this->entityManager->remove($db_place);
$deletedCount++;
}
$i++;
// Flush/clear Doctrine tous les X lieux pour éviter l'explosion mémoire
if (($i % $batchSize) === 0) {
$this->entityManager->flush();
$this->entityManager->clear();
// Recharger les stats après clear
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
}
}
// Flush final
$this->entityManager->flush();
$this->entityManager->clear();
// Récupérer tous les commerces de la zone qui n'ont pas été supprimés
$commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);

View file

@ -120,6 +120,12 @@ class Place
#[ORM\Column(nullable: true)]
private ?int $osm_changeset = null;
#[ORM\Column(type: Types::TEXT, nullable: true, options: ['charset' => 'utf8mb4'])]
private ?string $emailContent = null;
#[ORM\Column(type: Types::INTEGER, nullable: true)]
private ?int $place_count = null;
public function getPlaceTypeName(): ?string
{
if ($this->main_tag == 'amenity=restaurant') {
@ -734,4 +740,15 @@ class Place
return $this;
}
public function getEmailContent(): ?string
{
return $this->emailContent;
}
public function setEmailContent(?string $emailContent): static
{
$this->emailContent = $emailContent;
return $this;
}
}

View file

@ -252,16 +252,21 @@ out meta;';
}
public function get_city_osm_from_zip_code($zip_code) {
// Détection spéciale pour Paris, Lyon, Marseille
if (preg_match('/^75(0[1-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|6[0-9]|7[0-9]|8[0-9]|9[0-9])$/', $zip_code)) {
$arr = intval(substr($zip_code, 2, 3));
return 'Paris ' . $arr . 'e arr.';
}
if (preg_match('/^69(0[1-9]|1[0-9]|2[0-9])$/', $zip_code)) {
$arr = intval(substr($zip_code, 2, 3));
return 'Lyon ' . $arr . 'e arr.';
}
if (preg_match('/^13(0[1-9]|1[0-6])$/', $zip_code)) {
$arr = intval(substr($zip_code, 2, 3));
return 'Marseille ' . $arr . 'e arr.';
}
// Requête Overpass pour obtenir la zone administrative de niveau 8 avec un nom
$query = "[out:json][timeout:25];
area[\"ref:INSEE\"=\"{$zip_code}\"]->.searchArea;
(
relation[\"admin_level\"=\"8\"][\"name\"][\"type\"=\"boundary\"][\"boundary\"=\"administrative\"](area.searchArea);
);
out body;
>;
out skel qt;";
$query = "[out:json][timeout:25];\n area[\"ref:INSEE\"=\"{$zip_code}\"]->.searchArea;\n (\n relation[\"admin_level\"=\"8\"][\"name\"][\"type\"=\"boundary\"][\"boundary\"=\"administrative\"](area.searchArea);\n );\n out body;\n >;\n out skel qt;";
$response = $this->client->request('POST', $this->overpassApiUrl, [
'body' => ['data' => $query]
]);
@ -389,10 +394,6 @@ out meta;';
public static function uuid_create_static() {
return $this->uuid_create();
}
public function uuid_create() {
return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
// 32 bits for "time_low"

View file

@ -28,5 +28,7 @@ En vous souhaitant une bonne journée.
<br>
<hr>
{% if place.id %}
<a href="{{ path('app_admin_commerce', {'id': place.id}) }}">Ne plus être sollicité pour mettre à jour mon commerce</a>
{% endif %}
</div>

View file

@ -25,7 +25,7 @@ commerces existants déjà en base: {{ commerces|length }}
</p>
{# {{ dump(commerces[0]) }} #}
<table class="table table-striped js-sort-table">
<table class="table table-striped table-sort">
{% include 'admin/stats/table-head.html.twig' %}
<tbody>

View file

@ -54,7 +54,7 @@
{{ stats.name }} - {{ stats.completionPercent }}% complété</h1>
</div>
<div class="col-md-6 col-12">
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 0}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 1}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
<button id="openInJOSM" class="btn btn-secondary ms-2">
<i class="bi bi-map"></i> Ouvrir dans JOSM
</button>
@ -262,7 +262,8 @@
</a>
</div>
</div>
<table class="table table-bordered table-striped table-hover table-responsive js-sort-table">
<input type="text" id="stats-table-search" class="form-control mb-2" placeholder="Filtrer les lieux...">
<table id="stats-table" class="table table-bordered table-striped table-hover table-responsive table-sort">
{% include 'admin/stats/table-head.html.twig' %}
<tbody>
{% for commerce in stats.places %}

View file

@ -18,7 +18,7 @@
<h1>Commerces fermés</h1>
<p>Voici la liste des commerces fermés :</p>
<div class="table-responsive">
<table class="table table-striped js-sort-table">
<table class="table table-striped table-sort">
<thead>
<tr>
<th>Nom du commerce</th>

View file

@ -137,8 +137,9 @@
<div class="row mt-4">
<div class="col-12">
<h2>Statistiques par ville</h2>
<input type="text" id="dashboard-table-search" class="form-control mb-2" placeholder="Filtrer les villes...">
<div class="table-responsive">
<table class="table table-striped js-sort-table">
<table id="dashboard-table" class="table table-striped table-sort">
<thead>
<tr>
<th>Ville</th>

View file

@ -9,7 +9,7 @@
<div class="row">
<div class="col-12 col-md-6 ">
<h2>Lieux modifiés</h2>
<table class="table table-striped table-hover table-responsive js-sort-table">
<table class="table table-striped table-hover table-responsive table-sort">
<thead>
<tr>
<th>Nom</th>
@ -33,7 +33,7 @@
</div>
<div class="col-12 col-md-6 ">
<h2>Lieux affichés</h2>
<table class="table table-striped table-hover table-responsive js-sort-table">
<table class="table table-striped table-hover table-responsive table-sort">
<thead>
<tr>
<th>Nom</th>

View file

@ -5,7 +5,7 @@
{% block body %}
<div class="container">
<h1>Commerces avec une note</h1>
<table class="table table-striped js-sort-table table-hover table-responsive">
<table class="table table-striped table-hover table-responsive table-sort">
<thead>
<tr>
<th>Nom</th>