Files
2026-04-23 00:53:19 +10:00

503 lines
18 KiB
TypeScript

// 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 ───────────────────────────────────────────────────────────
const dateCache = new Map<number, { year: number; month: number; day: number }>()
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<string, number>()
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<string, { count: number; code: string }>()
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<string, number>()
flights.forEach(f => {
const continents = new Set<string>()
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<string, { count: number; id: number, logo_url: string }>()
flights.forEach(f => {
const airline = f.airline
if (!airline?.name) return
const existing = counts.get(airline.name) ?? { count: 0, id: airline.id, logo_url: airline.logo_url }
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,
logo_url: (past.get(name) ?? upcoming.get(name))!.logo_url,
}))
.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<string, AirportItem>()
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
}
// ── Aircraft ──────────────────────────────────────────────────────────────────
interface AircraftItem {
label: string
id: number
past: number
upcoming: number
designator: string
}
export function getTopAircraft(flights: Flight[], upcomingFlights: Flight[]) {
function countAircraft(list: Flight[]) {
const counts = new Map<string, { count: number; id: number; displayName: string; designator: string }>()
list.forEach(f => {
const aircraft = f.aircraft
if (!aircraft?.designator) return
const existing = counts.get(aircraft.designator) ?? { count: 0, id: aircraft.id, displayName: aircraft.display_name_short, designator: aircraft.designator }
existing.count++
counts.set(aircraft.designator, existing)
})
return counts
}
const past = countAircraft(flights)
const upcoming = countAircraft(upcomingFlights)
const allNames = new Set([...past.keys(), ...upcoming.keys()])
const sorted: AircraftItem[] = [...allNames]
.map(name => ({
label: (past.get(name) ?? upcoming.get(name))!.displayName,
designator: (past.get(name) ?? upcoming.get(name))!.designator,
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 = {
aircraft: sorted,
series: [
{ name: 'Flights', data: sorted.map(s => s.past) },
{ name: 'Upcoming', data: sorted.map(s => s.upcoming) },
],
}
return result
}
// ── Routes ────────────────────────────────────────────────────────────────────
interface RouteItem {
label: string
depLabel: string
arrLabel: string
past: number
upcoming: number
}
export function getTopRoutes(flights: Flight[], upcomingFlights: Flight[]) {
function countRoutes(list: Flight[]) {
const counts = new Map<string, { count: number; depLabel: string; arrLabel: string }>()
list.forEach(f => {
const dep = airportLabel(f.departure_airport)
const arr = airportLabel(f.arrival_airport)
if (!dep || !arr) return
const key = `${dep}-${arr}`
const existing = counts.get(key) ?? { count: 0, depLabel: dep, arrLabel: arr }
existing.count++
counts.set(key, existing)
})
return counts
}
const past = countRoutes(flights)
const upcoming = countRoutes(upcomingFlights)
const allKeys = new Set([...past.keys(), ...upcoming.keys()])
const sorted: RouteItem[] = [...allKeys]
.map(key => {
const meta = (past.get(key) ?? upcoming.get(key))!
return {
label: key,
depLabel: meta.depLabel,
arrLabel: meta.arrLabel,
past: past.get(key)?.count ?? 0,
upcoming: upcoming.get(key)?.count ?? 0,
}
})
.sort((a, b) => (b.past + b.upcoming) - (a.past + a.upcoming))
const result = {
routes: sorted,
series: [
{ name: 'Flights', data: sorted.map(s => s.past) },
{ name: 'Upcoming', data: sorted.map(s => s.upcoming) },
],
}
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<typeof useFlightStats>
export function useFlightStats(flights: Ref<Flight[]>) {
const now = new Date()
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 topAircraft = computed(() => getTopAircraft(pastFlights.value, upcomingFlights.value))
const topRoutes = computed(() => getTopRoutes(pastFlights.value, upcomingFlights.value))
const flightTypes = computed(() => getFlightTypes(allFlights.value))
return {
pastFlights,
upcomingFlights,
perYear,
perMonth,
perDay,
reasons,
classes,
seatTypes,
countries,
continents,
flightTypes,
topAirlines,
topAirports,
topAircraft,
topRoutes,
}
}