oedb-backend/frontend/src/app/pages/stats/stats.ts

298 lines
9.3 KiB
TypeScript

import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { OedbApi } from '../../services/oedb-api';
import oedb from '../../../oedb-types';
interface Event {
id?: string;
properties: {
what?: string;
createdate?: string;
start?: string;
stop?: string;
label?: string;
name?: string;
where?: string;
[key: string]: any;
};
geometry?: any;
}
interface WhatCount {
what: string;
count: number;
label: string;
emoji: string;
}
interface CreationMonth {
month: string;
count: number;
}
interface CreationWeek {
week: string;
count: number;
}
interface DurationBucket {
days: string;
count: number;
}
@Component({
selector: 'app-stats',
standalone: true,
imports: [CommonModule],
templateUrl: './stats.html',
styleUrl: './stats.scss'
})
export class Stats implements OnInit {
private oedbApi = inject(OedbApi);
private router = inject(Router);
isLoading = true;
events: Event[] = [];
whatCounts: WhatCount[] = [];
creationHistogram: CreationMonth[] = [];
creationHistogramByWeek: CreationWeek[] = [];
durationDistribution: DurationBucket[] = [];
recentEvents: Event[] = [];
meetingTypes: WhatCount[] = [];
reportingTypes: WhatCount[] = [];
ngOnInit() {
this.loadStats();
}
loadStats() {
this.isLoading = true;
this.oedbApi.getEvents({ limit: 10000 }).subscribe({
next: (response: any) => {
this.events = Array.isArray(response?.features) ? response.features : [];
this.processStats();
this.isLoading = false;
},
error: (err) => {
console.error('Error loading stats:', err);
this.isLoading = false;
}
});
}
processStats() {
// Compter par catégorie what
const whatMap: Record<string, number> = {};
for (const event of this.events) {
const what = event.properties?.what || 'non-défini';
whatMap[what] = (whatMap[what] || 0) + 1;
}
this.whatCounts = Object.entries(whatMap)
.map(([what, count]) => {
const preset = (oedb.presets.what as any)[what];
return {
what,
count,
label: preset?.label || what,
emoji: preset?.emoji || '📌'
};
})
.sort((a, b) => b.count - a.count)
.slice(0, 20); // Top 20
// Histogramme de création par mois
const creationMap: Record<string, number> = {};
for (const event of this.events) {
const createdate = event.properties?.createdate;
if (createdate) {
const date = new Date(createdate);
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
creationMap[monthKey] = (creationMap[monthKey] || 0) + 1;
}
}
this.creationHistogram = Object.entries(creationMap)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([month, count]) => ({ month, count }));
// Histogramme de création par semaine
const creationWeekMap: Record<string, number> = {};
for (const event of this.events) {
const createdate = event.properties?.createdate;
if (createdate) {
const date = new Date(createdate);
const weekKey = this.getWeekKey(date);
creationWeekMap[weekKey] = (creationWeekMap[weekKey] || 0) + 1;
}
}
this.creationHistogramByWeek = Object.entries(creationWeekMap)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([week, count]) => ({ week, count }));
// Distribution des durées
const durationMap: Record<string, number> = {
'0': 0, // 0 jour
'1': 0, // 1 jour
'2-7': 0, // 2-7 jours
'8-30': 0, // 8-30 jours
'31-90': 0, // 31-90 jours
'90+': 0 // Plus de 90 jours
};
for (const event of this.events) {
const start = event.properties?.start;
const stop = event.properties?.stop;
if (start && stop) {
const startDate = new Date(start);
const stopDate = new Date(stop);
const days = Math.floor((stopDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
if (days === 0) durationMap['0']++;
else if (days === 1) durationMap['1']++;
else if (days >= 2 && days <= 7) durationMap['2-7']++;
else if (days >= 8 && days <= 30) durationMap['8-30']++;
else if (days >= 31 && days <= 90) durationMap['31-90']++;
else durationMap['90+']++;
}
}
this.durationDistribution = Object.entries(durationMap)
.map(([days, count]) => ({ days, count }));
// 10 événements les plus récents
this.recentEvents = [...this.events]
.filter(e => e.properties?.createdate)
.sort((a, b) => {
const dateA = new Date(a.properties.createdate || 0).getTime();
const dateB = new Date(b.properties.createdate || 0).getTime();
return dateB - dateA;
})
.slice(0, 10);
// Identifier les types de réunions et signalements
this.identifySpecialTypes();
}
identifySpecialTypes() {
const whatPresets = oedb.presets.what as Record<string, any>;
const meetingKeywords = ['meeting', 'conférence', 'conférence', 'réunion', 'event', 'gathering', 'assembly'];
const reportingKeywords = ['signalement', 'incident', 'accident', 'obstacle', 'interruption', 'damage', 'obstruction'];
const meetingSet = new Set<string>();
const reportingSet = new Set<string>();
for (const [key, preset] of Object.entries(whatPresets)) {
const label = (preset.label || '').toLowerCase();
const description = (preset.description || '').toLowerCase();
const what = key.toLowerCase();
// Vérifier si c'est une réunion
const isMeeting = meetingKeywords.some(kw =>
label.includes(kw) || description.includes(kw) || what.includes(kw)
) || key.includes('community') || key.includes('culture');
// Vérifier si c'est un signalement
const isReporting = reportingKeywords.some(kw =>
label.includes(kw) || description.includes(kw) || what.includes(kw)
) || key.includes('traffic') || key.includes('hazard') || key.includes('weather');
// Vérifier si présence ou distance
if (isMeeting) {
const isOnline = label.includes('en ligne') || label.includes('online') ||
description.includes('en ligne') || description.includes('online') ||
what.includes('online') || preset.properties?.online;
if (this.events.some(e => e.properties?.what === key)) {
meetingSet.add(key);
}
}
if (isReporting && this.events.some(e => e.properties?.what === key)) {
reportingSet.add(key);
}
}
this.meetingTypes = Array.from(meetingSet)
.map(what => {
const preset = whatPresets[what];
const count = this.events.filter(e => e.properties?.what === what).length;
return {
what,
count,
label: preset?.label || what,
emoji: preset?.emoji || '📌'
};
})
.sort((a, b) => b.count - a.count);
this.reportingTypes = Array.from(reportingSet)
.map(what => {
const preset = whatPresets[what];
const count = this.events.filter(e => e.properties?.what === what).length;
return {
what,
count,
label: preset?.label || what,
emoji: preset?.emoji || '📌'
};
})
.sort((a, b) => b.count - a.count);
}
navigateToWhat(what: string) {
this.router.navigate(['/agenda'], { queryParams: { what } });
}
getMaxCount(): number {
return Math.max(...this.creationHistogram.map(h => h.count), 1);
}
getMaxWeekCount(): number {
return Math.max(...this.creationHistogramByWeek.map(h => h.count), 1);
}
getWeekKey(date: Date): string {
// Obtenir le premier jour de la semaine (lundi)
const d = new Date(date.getTime()); // Copier la date sans la modifier
const day = d.getDay();
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Ajuster pour que lundi soit 0
const monday = new Date(d.getFullYear(), d.getMonth(), diff);
// Obtenir le numéro de semaine ISO (semaine commençant le lundi)
const weekNumber = this.getISOWeek(monday);
return `${monday.getFullYear()}-S${String(weekNumber).padStart(2, '0')}`;
}
getISOWeek(date: Date): number {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
}
formatWeek(weekKey: string): string {
const [year, week] = weekKey.split('-S');
return `S${week} ${year}`;
}
getMaxDurationCount(): number {
return Math.max(...this.durationDistribution.map(d => d.count), 1);
}
formatMonth(monthKey: string): string {
const [year, month] = monthKey.split('-');
const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin', 'Juil', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'];
return `${months[parseInt(month) - 1]} ${year}`;
}
formatDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString('fr-FR', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
}