// composables/useFlightStats.ts import { computed, Ref, watch } from 'vue' import type { Airport, Flight } from '@/Types/types' const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] // ── Date part cache ─────────────────────────────────────────────────────────── // Intl.DateTimeFormat is expensive. We compute each flight's date parts once // and cache them for the lifetime of the page. const dateCache = new Map() function getDateParts(f: Flight): { year: number; month: number; day: number } { if (dateCache.has(f.id)) return dateCache.get(f.id)! const tz = f.departure_airport.timezone const date = new Date(f.departure_date) const fmt = (opts: Intl.DateTimeFormatOptions) => new Intl.DateTimeFormat('en', { timeZone: tz, ...opts }).format(date) const localDay = fmt({ weekday: 'long' }) const dayIdx = DAYS.indexOf(localDay) const parts = { year: Number(fmt({ year: 'numeric' })), month: Number(fmt({ month: 'numeric' })) - 1, day: dayIdx === -1 ? 0 : dayIdx, } dateCache.set(f.id, parts) return parts } // ── Per year / month / day ──────────────────────────────────────────────────── export function getFlightsPerYear(flights: Flight[], upcomingFlights: Flight[]) { console.time('getFlightsPerYear') const allFlights = [...flights, ...upcomingFlights] const allYears = new Set(allFlights.map(f => getDateParts(f).year)) const sorted = [...allYears].sort((a, b) => a - b) let min = sorted[0] let max = sorted[sorted.length - 1] while (max - min + 1 < 5) { min-- if (max - min + 1 < 5) max++ } const years = Array.from({ length: max - min + 1 }, (_, i) => min + i) const countByYear = (list: Flight[]) => years.map(year => list.filter(f => getDateParts(f).year === year).length) const result = { years, series: [ { name: 'Flown', data: countByYear(flights) }, { name: 'Upcoming', data: countByYear(upcomingFlights) }, ], } console.timeEnd('getFlightsPerYear') return result } export function getFlightsPerMonth(flights: Flight[], upcomingFlights: Flight[]) { console.time('getFlightsPerMonth') const countByMonth = (list: Flight[]) => MONTHS.map((_, i) => list.filter(f => getDateParts(f).month === i).length) const result = { months: MONTHS, series: [ { name: 'Flown', data: countByMonth(flights) }, { name: 'Upcoming', data: countByMonth(upcomingFlights) }, ], } console.timeEnd('getFlightsPerMonth') return result } export function getFlightsPerDay(flights: Flight[], upcomingFlights: Flight[]) { console.time('getFlightsPerDay') const countByDay = (list: Flight[]) => DAYS.map((_, i) => list.filter(f => getDateParts(f).day === i).length) const result = { days: DAYS, series: [ { name: 'Flown', data: countByDay(flights) }, { name: 'Upcoming', data: countByDay(upcomingFlights) }, ], } console.timeEnd('getFlightsPerDay') return result } // ── Grouping helpers ────────────────────────────────────────────────────────── function groupByName(flights: Flight[], accessor: (f: Flight) => string) { const counts = new Map() flights.forEach(f => { const key = accessor(f) ?? 'Unknown' counts.set(key, (counts.get(key) ?? 0) + 1) }) const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]) return { labels: sorted.map(([name]) => name), series: sorted.map(([, count]) => count), } } export function getFlightReasons(flights: Flight[]) { console.time('getFlightReasons') const result = groupByName(flights, f => f.flight_reason?.name ?? 'Unknown') console.timeEnd('getFlightReasons') return result } export function getFlightClasses(flights: Flight[]) { console.time('getFlightClasses') const result = groupByName(flights, f => f.flight_class?.name ?? 'Unknown') console.timeEnd('getFlightClasses') return result } export function getSeatTypes(flights: Flight[]) { console.time('getSeatTypes') const result = groupByName(flights, f => f.seat_type?.name ?? 'Unknown') console.timeEnd('getSeatTypes') return result } // ── Countries ───────────────────────────────────────────────────────────────── function countCountries(flights: Flight[]) { const counts = new Map() flights.forEach(f => { const dep = f.departure_airport?.region?.country const arr = f.arrival_airport?.region?.country if (dep?.name) { const existing = counts.get(dep.name) ?? { count: 0, code: dep.code } existing.count++ counts.set(dep.name, existing) } if (arr?.name && arr.name !== dep?.name) { const existing = counts.get(arr.name) ?? { count: 0, code: arr.code } existing.count++ counts.set(arr.name, existing) } }) return counts } export function getCountries(flights: Flight[], upcomingFlights: Flight[]) { console.time('getCountries') const past = countCountries(flights) const upcoming = countCountries(upcomingFlights) const allNames = new Set([...past.keys(), ...upcoming.keys()]) const sorted = [...allNames] .map(name => ({ name, code: (past.get(name) ?? upcoming.get(name))!.code, past: past.get(name)?.count ?? 0, upcoming: upcoming.get(name)?.count ?? 0, })) .sort((a, b) => (b.past + b.upcoming) - (a.past + a.upcoming)) const result = { countries: sorted, series: [ { name: 'Flights', data: sorted.map(s => s.past) }, { name: 'Upcoming', data: sorted.map(s => s.upcoming) }, ], } console.timeEnd('getCountries') return result } // ── Continents ──────────────────────────────────────────────────────────────── export function getContinents(flights: Flight[]) { console.time('getContinents') const counts = new Map() flights.forEach(f => { const continents = new Set() const dep = f.departure_airport.region?.continent?.name const arr = f.arrival_airport.region?.continent?.name if (dep) continents.add(dep) if (arr) continents.add(arr) continents.forEach(c => counts.set(c, (counts.get(c) ?? 0) + 1)) }) const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]) const result = { labels: sorted.map(([name]) => name), series: sorted.map(([, count]) => count), } console.timeEnd('getContinents') return result } // ── Airlines ────────────────────────────────────────────────────────────────── function countAirlines(flights: Flight[]) { const counts = new Map() flights.forEach(f => { const airline = f.airline if (!airline?.name) return const existing = counts.get(airline.name) ?? { count: 0, id: airline.id } existing.count++ counts.set(airline.name, existing) }) return counts } export function getTopAirlines(flights: Flight[], upcomingFlights: Flight[]) { console.time('getTopAirlines') const past = countAirlines(flights) const upcoming = countAirlines(upcomingFlights) const allNames = new Set([...past.keys(), ...upcoming.keys()]) const sorted = [...allNames] .map(name => ({ name, id: (past.get(name) ?? upcoming.get(name))!.id, past: past.get(name)?.count ?? 0, upcoming: upcoming.get(name)?.count ?? 0, })) .sort((a, b) => (b.past + b.upcoming) - (a.past + a.upcoming)) const result = { airlines: sorted, series: [ { name: 'Flights', data: sorted.map(s => s.past) }, { name: 'Upcoming', data: sorted.map(s => s.upcoming) }, ], } console.timeEnd('getTopAirlines') return result } // ── Airports ────────────────────────────────────────────────────────────────── interface AirportItem { label: string fullName: string departures: number arrivals: number upcoming: number } function airportLabel(airport: Airport | null | undefined): string | null { if (!airport) return null return airport.iata_code ?? airport.icao_code ?? airport.name ?? null } export function getTopAirports(flights: Flight[], upcomingFlights: Flight[]) { console.time('getTopAirports') const map = new Map() const empty = (): AirportItem => ({ departures: 0, arrivals: 0, upcoming: 0, label: '', fullName: '' }) flights.forEach(f => { const depLabel = airportLabel(f.departure_airport) const arrLabel = airportLabel(f.arrival_airport) if (depLabel) { const e = map.get(depLabel) ?? empty() e.departures++ e.label = depLabel e.fullName = f.departure_airport?.name ?? depLabel map.set(depLabel, e) } if (arrLabel) { const e = map.get(arrLabel) ?? empty() e.arrivals++ e.label = arrLabel e.fullName = f.arrival_airport?.name ?? arrLabel map.set(arrLabel, e) } }) upcomingFlights.forEach(f => { const depLabel = airportLabel(f.departure_airport) const arrLabel = airportLabel(f.arrival_airport) if (depLabel) { const e = map.get(depLabel) ?? empty() e.upcoming++ e.label = depLabel e.fullName = f.departure_airport?.name ?? depLabel map.set(depLabel, e) } if (arrLabel) { const e = map.get(arrLabel) ?? empty() e.upcoming++ e.label = arrLabel e.fullName = f.arrival_airport?.name ?? arrLabel map.set(arrLabel, e) } }) const sorted = [...map.values()] .sort((a, b) => (b.departures + b.arrivals + b.upcoming) - (a.departures + a.arrivals + a.upcoming) ) const result = { airports: sorted, series: [ { name: 'Departures', data: sorted.map(s => s.departures) }, { name: 'Arrivals', data: sorted.map(s => s.arrivals) }, { name: 'Upcoming', data: sorted.map(s => s.upcoming) }, ], } console.timeEnd('getTopAirports') return result } // ── Flight types ────────────────────────────────────────────────────────────── export function getFlightTypes(flights: Flight[]) { console.time('getFlightTypes') const counts = { International: 0, Domestic: 0 } flights.forEach(f => { const dep = f.departure_airport.region?.country?.id const arr = f.arrival_airport.region?.country?.id if (dep && arr) { dep === arr ? counts.Domestic++ : counts.International++ } }) const sorted = Object.entries(counts).filter(([, count]) => count > 0) const result = { labels: sorted.map(([name]) => name), series: sorted.map(([, count]) => count), } console.timeEnd('getFlightTypes') return result } // ── Composable ──────────────────────────────────────────────────────────────── export type FlightStats = ReturnType export function useFlightStats(flights: Ref) { const now = new Date() // Pre-warm the date cache as soon as flights are available so the first // filter interaction doesn't pay the Intl.DateTimeFormat cost. watch(flights, (list) => { console.time('dateCache warm') list.forEach(f => getDateParts(f)) console.timeEnd('dateCache warm') }, { immediate: true }) const pastFlights = computed(() => { console.time('pastFlights') const result = flights.value.filter(f => new Date(f.departure_date) <= now) console.timeEnd('pastFlights') return result }) const upcomingFlights = computed(() => { console.time('upcomingFlights') const result = flights.value.filter(f => new Date(f.departure_date) > now) console.timeEnd('upcomingFlights') return result }) const allFlights = computed(() => flights.value) const perYear = computed(() => getFlightsPerYear(pastFlights.value, upcomingFlights.value)) const perMonth = computed(() => getFlightsPerMonth(pastFlights.value, upcomingFlights.value)) const perDay = computed(() => getFlightsPerDay(pastFlights.value, upcomingFlights.value)) const reasons = computed(() => getFlightReasons(allFlights.value)) const classes = computed(() => getFlightClasses(allFlights.value)) const seatTypes = computed(() => getSeatTypes(allFlights.value)) const countries = computed(() => getCountries(pastFlights.value, upcomingFlights.value)) const continents = computed(() => getContinents(allFlights.value)) const topAirlines = computed(() => getTopAirlines(pastFlights.value, upcomingFlights.value)) const topAirports = computed(() => getTopAirports(pastFlights.value, upcomingFlights.value)) const flightTypes = computed(() => getFlightTypes(allFlights.value)) return { pastFlights, upcomingFlights, perYear, perMonth, perDay, reasons, classes, seatTypes, countries, continents, flightTypes, topAirlines, topAirports, } }