import Link from 'next/link'; import type { ReactNode } from 'react'; import { fmt1, fmtDate, fmtInt, fmtPct, num } from '@/lib/format'; import { routes } from '@/lib/site'; import { Derived } from './shared'; export type Row = Record; export const str = (r: Row, k: string): string | null => (typeof r[k] === 'string' ? (r[k] as string) : r[k] == null ? null : String(r[k])); export const n = (r: Row, k: string): number => num(r[k] as number | string | null) ?? 0; export interface Column { key: string; label: string; num?: boolean; derived?: boolean; render: (r: Row) => ReactNode; } export interface MetricDef { key: string; label: string; title: string; description: string; /** Label for the HBars chart / primary value. */ primaryLabel: string; primary: (r: Row) => number; primaryFormat?: (v: number) => string; name: (r: Row) => string; href: (r: Row) => string | null; columns: Column[]; note?: ReactNode; warn?: string; } function NameCell({ href, name, sub, mono = false }: { href: string | null; name: string; sub?: ReactNode; mono?: boolean }) { return (
{href ? ( {name} ) : ( {name} )} {sub &&

{sub}

}
); } const shellHref = (shell: number) => routes.satellites(`orbit_class=LEO&min_perigee=${shell}&max_perigee=${shell + 50}&on_orbit=true`); export const METRICS: MetricDef[] = [ { key: 'constellations', label: 'Constellations', title: 'Largest constellations by active satellites', description: 'Constellations ranked by active satellites, with fleet size, launches in the last 365 days and the derived activity score.', primaryLabel: 'Active satellites', primary: (r) => n(r, 'active'), name: (r) => str(r, 'name') ?? '—', href: (r) => (str(r, 'slug') ? routes.constellation(str(r, 'slug') as string) : null), columns: [ { key: 'name', label: 'Constellation', render: (r) => {str(r, 'operator_name')} : str(r, 'operator_name')} /> }, { key: 'active', label: 'Active', num: true, render: (r) => fmtInt(n(r, 'active')) }, { key: 'total', label: 'Total', num: true, render: (r) => fmtInt(n(r, 'total')) }, { key: 'launched_last_365d', label: 'Launched 365 d', num: true, render: (r) => fmtInt(n(r, 'launched_last_365d')) }, { key: 'activity_score', label: 'Activity score', num: true, derived: true, render: (r) => {fmt1(r['activity_score'] as number | string | null)} }, ], note: ( <> Constellation membership and the activity score are metrics (0–10, weighted recent launch cadence and fleet health). ), }, { key: 'operators', label: 'Operators', title: 'Largest operators by active payloads', description: 'Operators ranked by active payloads on orbit, with total fleet, payloads launched in the last 365 days, constellations and launches.', primaryLabel: 'Active payloads', primary: (r) => n(r, 'active_payloads'), name: (r) => str(r, 'name') ?? '—', href: (r) => (str(r, 'slug') ? routes.operator(str(r, 'slug') as string) : null), columns: [ { key: 'name', label: 'Operator', render: (r) => }, { key: 'active_payloads', label: 'Active', num: true, render: (r) => fmtInt(n(r, 'active_payloads')) }, { key: 'total_payloads', label: 'Total payloads', num: true, render: (r) => fmtInt(n(r, 'total_payloads')) }, { key: 'payloads_last_365d', label: 'Launched 365 d', num: true, render: (r) => fmtInt(n(r, 'payloads_last_365d')) }, { key: 'constellations', label: 'Constellations', num: true, render: (r) => fmtInt(n(r, 'constellations')) }, { key: 'launches', label: 'Launches', num: true, render: (r) => fmtInt(n(r, 'launches')) }, ], }, { key: 'countries', label: 'Countries', title: 'Countries by active payloads', description: 'Countries ranked by active payloads, with objects on orbit, operators, launches and payloads launched in the last 365 days (SATCAT owner codes mapped to ISO 3166).', primaryLabel: 'Active payloads', primary: (r) => n(r, 'active_payloads'), name: (r) => str(r, 'name') ?? '—', href: (r) => (str(r, 'slug') ? routes.country(str(r, 'slug') as string) : null), columns: [ { key: 'name', label: 'Country', render: (r) => {str(r, 'code')}} /> }, { key: 'active_payloads', label: 'Active payloads', num: true, render: (r) => fmtInt(n(r, 'active_payloads')) }, { key: 'on_orbit_payloads', label: 'Payloads on orbit', num: true, render: (r) => fmtInt(n(r, 'on_orbit_payloads')) }, { key: 'objects_on_orbit', label: 'Objects on orbit', num: true, render: (r) => fmtInt(n(r, 'objects_on_orbit')) }, { key: 'operators', label: 'Operators', num: true, render: (r) => fmtInt(n(r, 'operators')) }, { key: 'launches', label: 'Launches', num: true, render: (r) => fmtInt(n(r, 'launches')) }, { key: 'payloads_last_365d', label: 'Launched 365 d', num: true, render: (r) => fmtInt(n(r, 'payloads_last_365d')) }, ], }, { key: 'countries-debris', label: 'Debris by country', title: 'Countries by debris on orbit', description: 'Countries ranked by catalogued debris fragments currently on orbit, with rocket bodies, objects on orbit and active payloads. Object counts only.', primaryLabel: 'Debris on orbit', primary: (r) => n(r, 'debris_on_orbit'), name: (r) => str(r, 'name') ?? '—', href: (r) => (str(r, 'slug') ? routes.country(str(r, 'slug') as string) : null), columns: [ { key: 'name', label: 'Country', render: (r) => {str(r, 'code')}} /> }, { key: 'debris_on_orbit', label: 'Debris on orbit', num: true, render: (r) => fmtInt(n(r, 'debris_on_orbit')) }, { key: 'rocket_bodies_on_orbit', label: 'Rocket bodies', num: true, render: (r) => fmtInt(n(r, 'rocket_bodies_on_orbit')) }, { key: 'objects_on_orbit', label: 'Objects on orbit', num: true, render: (r) => fmtInt(n(r, 'objects_on_orbit')) }, { key: 'active_payloads', label: 'Active payloads', num: true, render: (r) => fmtInt(n(r, 'active_payloads')) }, ], warn: 'Counts of catalogued objects attributed to the owner country. Not a collision-risk or responsibility metric.', }, { key: 'launches', label: 'Launch sites', title: 'Busiest launch sites', description: 'Launch sites ranked by orbital launches with at least one catalogued object, with launches in the last 365 days, payloads and last launch date.', primaryLabel: 'Launches', primary: (r) => n(r, 'launches'), name: (r) => str(r, 'name') ?? '—', href: (r) => (str(r, 'slug') ? routes.launchSite(str(r, 'slug') as string) : null), columns: [ { key: 'name', label: 'Launch site', render: (r) => {str(r, 'code')} · {str(r, 'country_name') ?? str(r, 'country_code') ?? '—'}} /> }, { key: 'launches', label: 'Launches', num: true, render: (r) => fmtInt(n(r, 'launches')) }, { key: 'launches_last_365d', label: 'Last 365 d', num: true, render: (r) => fmtInt(n(r, 'launches_last_365d')) }, { key: 'payloads', label: 'Payloads', num: true, render: (r) => fmtInt(n(r, 'payloads')) }, { key: 'last_launch', label: 'Last launch', render: (r) => {fmtDate(str(r, 'last_launch'))} }, ], }, { key: 'fastest-growing', label: 'Fastest growing', title: 'Fastest-growing constellations', description: 'Constellations ranked by growth: satellites launched in the last 365 days relative to the fleet size a year ago (minimum 5 launched).', primaryLabel: 'Growth (%)', primary: (r) => n(r, 'growth_pct'), primaryFormat: (v) => fmtPct(v, 0), name: (r) => str(r, 'name') ?? '—', href: (r) => (str(r, 'slug') ? routes.constellation(str(r, 'slug') as string) : null), columns: [ { key: 'name', label: 'Constellation', render: (r) => }, { key: 'growth_pct', label: 'Growth', num: true, derived: true, render: (r) => +{fmtPct(n(r, 'growth_pct'), 0)} }, { key: 'launched_last_365d', label: 'Launched 365 d', num: true, render: (r) => fmtInt(n(r, 'launched_last_365d')) }, { key: 'active', label: 'Active', num: true, render: (r) => fmtInt(n(r, 'active')) }, { key: 'total', label: 'Total', num: true, render: (r) => fmtInt(n(r, 'total')) }, { key: 'first_launch', label: 'First launch', render: (r) => {fmtDate(str(r, 'first_launch'))} }, ], note: ( <> Growth = satellites launched in the last 365 d ÷ fleet a year ago (total − launched last 365 d), minimum 5 launched. New constellations with no fleet a year ago show very large percentages. ), }, { key: 'congested-shells', label: 'LEO shells', title: 'Most populated 50 km LEO shells', description: 'LEO altitude shells (50 km, by perigee) ranked by number of tracked objects on orbit, with active payloads, debris and constellations present. Object counts only — not a collision-risk metric.', primaryLabel: 'Objects on orbit', primary: (r) => n(r, 'objects'), name: (r) => `${fmtInt(n(r, 'shell_km'))}–${fmtInt(n(r, 'shell_km') + 50)} km`, href: (r) => shellHref(n(r, 'shell_km')), columns: [ { key: 'shell_km', label: 'Shell (perigee)', render: (r) => }, { key: 'objects', label: 'Objects', num: true, render: (r) => fmtInt(n(r, 'objects')) }, { key: 'active_payloads', label: 'Active payloads', num: true, render: (r) => fmtInt(n(r, 'active_payloads')) }, { key: 'debris', label: 'Debris', num: true, render: (r) => fmtInt(n(r, 'debris')) }, { key: 'constellations', label: 'Constellations', num: true, render: (r) => fmtInt(n(r, 'constellations')) }, ], warn: 'SatelliteIndex orbital density (object counts per 50 km perigee shell), NOT a collision-risk metric. No conjunction or collision probability is computed or implied.', }, { key: 'launch-years', label: 'Launch years', title: 'Launch years by orbital launches', description: 'Calendar years ranked by orbital launches with catalogued objects, with the number of payloads catalogued for that year.', primaryLabel: 'Launches', primary: (r) => n(r, 'launches'), name: (r) => String(n(r, 'year')), href: (r) => routes.launches(`year=${n(r, 'year')}`), columns: [ { key: 'year', label: 'Year', render: (r) => }, { key: 'launches', label: 'Launches', num: true, render: (r) => fmtInt(n(r, 'launches')) }, { key: 'payloads', label: 'Payloads', num: true, render: (r) => fmtInt(n(r, 'payloads')) }, { key: 'ratio', label: 'Payloads / launch', num: true, derived: true, render: (r) => {n(r, 'launches') ? fmt1(n(r, 'payloads') / n(r, 'launches')) : '—'} }, ], }, ]; export const DEFAULT_METRIC = 'constellations'; export const findMetric = (key: string | undefined): MetricDef => METRICS.find((m) => m.key === key) ?? (METRICS[0] as MetricDef);