Add more airlines and fix edit bugs
This commit is contained in:
@@ -1,64 +1,45 @@
|
||||
// composables/useFlightStats.ts
|
||||
import {computed, ComputedRef, Ref} from 'vue'
|
||||
import type {Airport, Flight} from '@/Types/types'
|
||||
import { computed, Ref, watch } from 'vue'
|
||||
import type { Airport, Flight } from '@/Types/types'
|
||||
|
||||
const flightYear = (f: Flight): number =>
|
||||
Number(new Intl.DateTimeFormat('en', {
|
||||
timeZone: f.departure_airport.timezone,
|
||||
year: 'numeric',
|
||||
}).format(new Date(f.departure_date)))
|
||||
|
||||
const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
||||
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 dayIndex = (f: Flight): number => {
|
||||
const localDay = new Intl.DateTimeFormat('en', {
|
||||
timeZone: f.departure_airport.timezone,
|
||||
weekday: 'long',
|
||||
}).format(new Date(f.departure_date))
|
||||
const idx = DAYS.indexOf(localDay)
|
||||
return idx === -1 ? 0 : idx
|
||||
}
|
||||
const dateCache = new Map<number, { year: number; month: number; day: number }>()
|
||||
|
||||
export function getFlightsPerDay(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
const countByDay = (list: Flight[]) =>
|
||||
DAYS.map((_, i) => list.filter(f => dayIndex(f) === i).length)
|
||||
function getDateParts(f: Flight): { year: number; month: number; day: number } {
|
||||
if (dateCache.has(f.id)) return dateCache.get(f.id)!
|
||||
|
||||
return {
|
||||
days: DAYS,
|
||||
series: [
|
||||
{ name: 'Flown', data: countByDay(flights) },
|
||||
{ name: 'Upcoming', data: countByDay(upcomingFlights) },
|
||||
],
|
||||
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
|
||||
}
|
||||
|
||||
const monthIndex = (f: Flight): number =>
|
||||
Number(new Intl.DateTimeFormat('en', {
|
||||
timeZone: f.departure_airport.timezone,
|
||||
month: 'numeric',
|
||||
}).format(new Date(f.departure_date))) - 1
|
||||
|
||||
export function getFlightsPerMonth(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
const countByMonth = (list: Flight[]) =>
|
||||
MONTHS.map((_, i) => list.filter(f => monthIndex(f) === i).length)
|
||||
|
||||
return {
|
||||
months: MONTHS,
|
||||
series: [
|
||||
{ name: 'Flown', data: countByMonth(flights) },
|
||||
{ name: 'Upcoming', data: countByMonth(upcomingFlights) },
|
||||
],
|
||||
}
|
||||
}
|
||||
// ── Per year / month / day ────────────────────────────────────────────────────
|
||||
|
||||
export function getFlightsPerYear(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
console.time('getFlightsPerYear')
|
||||
const allFlights = [...flights, ...upcomingFlights]
|
||||
|
||||
const allYears = new Set<number>()
|
||||
allFlights.forEach(f => allYears.add(flightYear(f)))
|
||||
const sorted = [...allYears].sort((a, b) => a - b)
|
||||
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]
|
||||
@@ -69,17 +50,53 @@ export function getFlightsPerYear(flights: Flight[], upcomingFlights: Flight[])
|
||||
|
||||
const years = Array.from({ length: max - min + 1 }, (_, i) => min + i)
|
||||
const countByYear = (list: Flight[]) =>
|
||||
years.map(year => list.filter(f => flightYear(f) === year).length)
|
||||
years.map(year => list.filter(f => getDateParts(f).year === year).length)
|
||||
|
||||
return {
|
||||
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 => {
|
||||
@@ -94,17 +111,28 @@ function groupByName(flights: Flight[], accessor: (f: Flight) => string) {
|
||||
}
|
||||
|
||||
export function getFlightReasons(flights: Flight[]) {
|
||||
return groupByName(flights, f => f.flight_reason?.name ?? 'Unknown')
|
||||
console.time('getFlightReasons')
|
||||
const result = groupByName(flights, f => f.flight_reason?.name ?? 'Unknown')
|
||||
console.timeEnd('getFlightReasons')
|
||||
return result
|
||||
}
|
||||
|
||||
export function getFlightClasses(flights: Flight[]) {
|
||||
return groupByName(flights, f => f.flight_class?.name ?? 'Unknown')
|
||||
console.time('getFlightClasses')
|
||||
const result = groupByName(flights, f => f.flight_class?.name ?? 'Unknown')
|
||||
console.timeEnd('getFlightClasses')
|
||||
return result
|
||||
}
|
||||
|
||||
export function getSeatTypes(flights: Flight[]) {
|
||||
return groupByName(flights, f => f.seat_type?.name ?? 'Unknown')
|
||||
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 => {
|
||||
@@ -126,11 +154,12 @@ function countCountries(flights: Flight[]) {
|
||||
}
|
||||
|
||||
export function getCountries(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
console.time('getCountries')
|
||||
const past = countCountries(flights)
|
||||
const upcoming = countCountries(upcomingFlights)
|
||||
const allCountries = new Set([...past.keys(), ...upcoming.keys()])
|
||||
const allNames = new Set([...past.keys(), ...upcoming.keys()])
|
||||
|
||||
const sorted = [...allCountries]
|
||||
const sorted = [...allNames]
|
||||
.map(name => ({
|
||||
name,
|
||||
code: (past.get(name) ?? upcoming.get(name))!.code,
|
||||
@@ -139,16 +168,21 @@ export function getCountries(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
}))
|
||||
.sort((a, b) => (b.past + b.upcoming) - (a.past + a.upcoming))
|
||||
|
||||
return {
|
||||
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>()
|
||||
@@ -159,12 +193,16 @@ export function getContinents(flights: Flight[]) {
|
||||
continents.forEach(c => counts.set(c, (counts.get(c) ?? 0) + 1))
|
||||
})
|
||||
const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1])
|
||||
return {
|
||||
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 }>()
|
||||
flights.forEach(f => {
|
||||
@@ -178,11 +216,12 @@ function countAirlines(flights: Flight[]) {
|
||||
}
|
||||
|
||||
export function getTopAirlines(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
console.time('getTopAirlines')
|
||||
const past = countAirlines(flights)
|
||||
const upcoming = countAirlines(upcomingFlights)
|
||||
const allAirlines = new Set([...past.keys(), ...upcoming.keys()])
|
||||
const allNames = new Set([...past.keys(), ...upcoming.keys()])
|
||||
|
||||
const sorted = [...allAirlines]
|
||||
const sorted = [...allNames]
|
||||
.map(name => ({
|
||||
name,
|
||||
id: (past.get(name) ?? upcoming.get(name))!.id,
|
||||
@@ -191,15 +230,19 @@ export function getTopAirlines(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
}))
|
||||
.sort((a, b) => (b.past + b.upcoming) - (a.past + a.upcoming))
|
||||
|
||||
return {
|
||||
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
|
||||
@@ -214,7 +257,8 @@ function airportLabel(airport: Airport | null | undefined): string | null {
|
||||
}
|
||||
|
||||
export function getTopAirports(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
const map = new Map<string, AirportItem>()
|
||||
console.time('getTopAirports')
|
||||
const map = new Map<string, AirportItem>()
|
||||
const empty = (): AirportItem => ({ departures: 0, arrivals: 0, upcoming: 0, label: '', fullName: '' })
|
||||
|
||||
flights.forEach(f => {
|
||||
@@ -262,7 +306,7 @@ export function getTopAirports(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
(b.departures + b.arrivals + b.upcoming) - (a.departures + a.arrivals + a.upcoming)
|
||||
)
|
||||
|
||||
return {
|
||||
const result = {
|
||||
airports: sorted,
|
||||
series: [
|
||||
{ name: 'Departures', data: sorted.map(s => s.departures) },
|
||||
@@ -270,9 +314,14 @@ export function getTopAirports(flights: Flight[], upcomingFlights: Flight[]) {
|
||||
{ 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
|
||||
@@ -282,37 +331,56 @@ export function getFlightTypes(flights: Flight[]) {
|
||||
}
|
||||
})
|
||||
const sorted = Object.entries(counts).filter(([, count]) => count > 0)
|
||||
return {
|
||||
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()
|
||||
|
||||
const pastFlights = computed(() =>
|
||||
flights.value.filter(f => new Date(f.departure_date) <= now)
|
||||
)
|
||||
const upcomingFlights = computed(() =>
|
||||
flights.value.filter(f => new Date(f.departure_date) > now)
|
||||
)
|
||||
// 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))
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user