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 = {}; 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 = {}; 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 = {}; 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 = { '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; 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(); const reportingSet = new Set(); 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' }); } }