spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import Link from 'next/link';2import type { ReactNode } from 'react';3import { fmt1, fmtDate, fmtInt, fmtPct, num } from '@/lib/format';4import { routes } from '@/lib/site';5import { Derived } from './shared';67export type Row = Record<string, unknown>;8export const str = (r: Row, k: string): string | null => (typeof r[k] === 'string' ? (r[k] as string) : r[k] == null ? null : String(r[k]));9export const n = (r: Row, k: string): number => num(r[k] as number | string | null) ?? 0;1011export interface Column {12 key: string;13 label: string;14 num?: boolean;15 derived?: boolean;16 render: (r: Row) => ReactNode;17}1819export interface MetricDef {20 key: string;21 label: string;22 title: string;23 description: string;24 /** Label for the HBars chart / primary value. */25 primaryLabel: string;26 primary: (r: Row) => number;27 primaryFormat?: (v: number) => string;28 name: (r: Row) => string;29 href: (r: Row) => string | null;30 columns: Column[];31 note?: ReactNode;32 warn?: string;33}3435function NameCell({ href, name, sub, mono = false }: { href: string | null; name: string; sub?: ReactNode; mono?: boolean }) {36 return (37 <div className="min-w-0">38 {href ? (39 <Link href={href} className={`link font-medium ${mono ? 'mono' : ''}`}>{name}</Link>40 ) : (41 <span className={`font-medium ${mono ? 'mono' : ''}`}>{name}</span>42 )}43 {sub && <p className="mt-0.5 truncate text-xs text-ink-3">{sub}</p>}44 </div>45 );46}4748const shellHref = (shell: number) => routes.satellites(`orbit_class=LEO&min_perigee=${shell}&max_perigee=${shell + 50}&on_orbit=true`);4950export const METRICS: MetricDef[] = [51 {52 key: 'constellations',53 label: 'Constellations',54 title: 'Largest constellations by active satellites',55 description: 'Constellations ranked by active satellites, with fleet size, launches in the last 365 days and the derived activity score.',56 primaryLabel: 'Active satellites',57 primary: (r) => n(r, 'active'),58 name: (r) => str(r, 'name') ?? '—',59 href: (r) => (str(r, 'slug') ? routes.constellation(str(r, 'slug') as string) : null),60 columns: [61 { key: 'name', label: 'Constellation', render: (r) => <NameCell href={str(r, 'slug') ? routes.constellation(str(r, 'slug') as string) : null} name={str(r, 'name') ?? '—'} sub={str(r, 'operator_slug') ? <Link href={routes.operator(str(r, 'operator_slug') as string)} className="hover:text-accent">{str(r, 'operator_name')}</Link> : str(r, 'operator_name')} /> },62 { key: 'active', label: 'Active', num: true, render: (r) => fmtInt(n(r, 'active')) },63 { key: 'total', label: 'Total', num: true, render: (r) => fmtInt(n(r, 'total')) },64 { key: 'launched_last_365d', label: 'Launched 365 d', num: true, render: (r) => fmtInt(n(r, 'launched_last_365d')) },65 { key: 'activity_score', label: 'Activity score', num: true, derived: true, render: (r) => <span className="mono">{fmt1(r['activity_score'] as number | string | null)}</span> },66 ],67 note: (68 <>69 Constellation membership and the activity score are <Derived className="mx-1" /> metrics (0–10, weighted recent launch cadence and fleet health).70 </>71 ),72 },73 {74 key: 'operators',75 label: 'Operators',76 title: 'Largest operators by active payloads',77 description: 'Operators ranked by active payloads on orbit, with total fleet, payloads launched in the last 365 days, constellations and launches.',78 primaryLabel: 'Active payloads',79 primary: (r) => n(r, 'active_payloads'),80 name: (r) => str(r, 'name') ?? '—',81 href: (r) => (str(r, 'slug') ? routes.operator(str(r, 'slug') as string) : null),82 columns: [83 { key: 'name', label: 'Operator', render: (r) => <NameCell href={str(r, 'slug') ? routes.operator(str(r, 'slug') as string) : null} name={str(r, 'name') ?? '—'} sub={str(r, 'country_name') ?? str(r, 'country_code') ?? undefined} /> },84 { key: 'active_payloads', label: 'Active', num: true, render: (r) => fmtInt(n(r, 'active_payloads')) },85 { key: 'total_payloads', label: 'Total payloads', num: true, render: (r) => fmtInt(n(r, 'total_payloads')) },86 { key: 'payloads_last_365d', label: 'Launched 365 d', num: true, render: (r) => fmtInt(n(r, 'payloads_last_365d')) },87 { key: 'constellations', label: 'Constellations', num: true, render: (r) => fmtInt(n(r, 'constellations')) },88 { key: 'launches', label: 'Launches', num: true, render: (r) => fmtInt(n(r, 'launches')) },89 ],90 },91 {92 key: 'countries',93 label: 'Countries',94 title: 'Countries by active payloads',95 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).',96 primaryLabel: 'Active payloads',97 primary: (r) => n(r, 'active_payloads'),98 name: (r) => str(r, 'name') ?? '—',99 href: (r) => (str(r, 'slug') ? routes.country(str(r, 'slug') as string) : null),100 columns: [101 { key: 'name', label: 'Country', render: (r) => <NameCell href={str(r, 'slug') ? routes.country(str(r, 'slug') as string) : null} name={str(r, 'name') ?? '—'} sub={<span className="mono">{str(r, 'code')}</span>} /> },102 { key: 'active_payloads', label: 'Active payloads', num: true, render: (r) => fmtInt(n(r, 'active_payloads')) },103 { key: 'on_orbit_payloads', label: 'Payloads on orbit', num: true, render: (r) => fmtInt(n(r, 'on_orbit_payloads')) },104 { key: 'objects_on_orbit', label: 'Objects on orbit', num: true, render: (r) => fmtInt(n(r, 'objects_on_orbit')) },105 { key: 'operators', label: 'Operators', num: true, render: (r) => fmtInt(n(r, 'operators')) },106 { key: 'launches', label: 'Launches', num: true, render: (r) => fmtInt(n(r, 'launches')) },107 { key: 'payloads_last_365d', label: 'Launched 365 d', num: true, render: (r) => fmtInt(n(r, 'payloads_last_365d')) },108 ],109 },110 {111 key: 'countries-debris',112 label: 'Debris by country',113 title: 'Countries by debris on orbit',114 description: 'Countries ranked by catalogued debris fragments currently on orbit, with rocket bodies, objects on orbit and active payloads. Object counts only.',115 primaryLabel: 'Debris on orbit',116 primary: (r) => n(r, 'debris_on_orbit'),117 name: (r) => str(r, 'name') ?? '—',118 href: (r) => (str(r, 'slug') ? routes.country(str(r, 'slug') as string) : null),119 columns: [120 { key: 'name', label: 'Country', render: (r) => <NameCell href={str(r, 'slug') ? routes.country(str(r, 'slug') as string) : null} name={str(r, 'name') ?? '—'} sub={<span className="mono">{str(r, 'code')}</span>} /> },121 { key: 'debris_on_orbit', label: 'Debris on orbit', num: true, render: (r) => fmtInt(n(r, 'debris_on_orbit')) },122 { key: 'rocket_bodies_on_orbit', label: 'Rocket bodies', num: true, render: (r) => fmtInt(n(r, 'rocket_bodies_on_orbit')) },123 { key: 'objects_on_orbit', label: 'Objects on orbit', num: true, render: (r) => fmtInt(n(r, 'objects_on_orbit')) },124 { key: 'active_payloads', label: 'Active payloads', num: true, render: (r) => fmtInt(n(r, 'active_payloads')) },125 ],126 warn: 'Counts of catalogued objects attributed to the owner country. Not a collision-risk or responsibility metric.',127 },128 {129 key: 'launches',130 label: 'Launch sites',131 title: 'Busiest launch sites',132 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.',133 primaryLabel: 'Launches',134 primary: (r) => n(r, 'launches'),135 name: (r) => str(r, 'name') ?? '—',136 href: (r) => (str(r, 'slug') ? routes.launchSite(str(r, 'slug') as string) : null),137 columns: [138 { key: 'name', label: 'Launch site', render: (r) => <NameCell href={str(r, 'slug') ? routes.launchSite(str(r, 'slug') as string) : null} name={str(r, 'name') ?? '—'} sub={<><span className="mono">{str(r, 'code')}</span> · {str(r, 'country_name') ?? str(r, 'country_code') ?? '—'}</>} /> },139 { key: 'launches', label: 'Launches', num: true, render: (r) => fmtInt(n(r, 'launches')) },140 { key: 'launches_last_365d', label: 'Last 365 d', num: true, render: (r) => fmtInt(n(r, 'launches_last_365d')) },141 { key: 'payloads', label: 'Payloads', num: true, render: (r) => fmtInt(n(r, 'payloads')) },142 { key: 'last_launch', label: 'Last launch', render: (r) => <span className="mono text-xs">{fmtDate(str(r, 'last_launch'))}</span> },143 ],144 },145 {146 key: 'fastest-growing',147 label: 'Fastest growing',148 title: 'Fastest-growing constellations',149 description: 'Constellations ranked by growth: satellites launched in the last 365 days relative to the fleet size a year ago (minimum 5 launched).',150 primaryLabel: 'Growth (%)',151 primary: (r) => n(r, 'growth_pct'),152 primaryFormat: (v) => fmtPct(v, 0),153 name: (r) => str(r, 'name') ?? '—',154 href: (r) => (str(r, 'slug') ? routes.constellation(str(r, 'slug') as string) : null),155 columns: [156 { key: 'name', label: 'Constellation', render: (r) => <NameCell href={str(r, 'slug') ? routes.constellation(str(r, 'slug') as string) : null} name={str(r, 'name') ?? '—'} sub={str(r, 'operator_name') ?? undefined} /> },157 { key: 'growth_pct', label: 'Growth', num: true, derived: true, render: (r) => <span className="mono text-active">+{fmtPct(n(r, 'growth_pct'), 0)}</span> },158 { key: 'launched_last_365d', label: 'Launched 365 d', num: true, render: (r) => fmtInt(n(r, 'launched_last_365d')) },159 { key: 'active', label: 'Active', num: true, render: (r) => fmtInt(n(r, 'active')) },160 { key: 'total', label: 'Total', num: true, render: (r) => fmtInt(n(r, 'total')) },161 { key: 'first_launch', label: 'First launch', render: (r) => <span className="mono text-xs">{fmtDate(str(r, 'first_launch'))}</span> },162 ],163 note: (164 <>165 Growth = satellites launched in the last 365 d ÷ fleet a year ago (total − launched last 365 d), minimum 5 launched. <Derived className="mx-1" /> New constellations with no fleet a year ago show very large percentages.166 </>167 ),168 },169 {170 key: 'congested-shells',171 label: 'LEO shells',172 title: 'Most populated 50 km LEO shells',173 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.',174 primaryLabel: 'Objects on orbit',175 primary: (r) => n(r, 'objects'),176 name: (r) => `${fmtInt(n(r, 'shell_km'))}–${fmtInt(n(r, 'shell_km') + 50)} km`,177 href: (r) => shellHref(n(r, 'shell_km')),178 columns: [179 { key: 'shell_km', label: 'Shell (perigee)', render: (r) => <NameCell mono href={shellHref(n(r, 'shell_km'))} name={`${fmtInt(n(r, 'shell_km'))}–${fmtInt(n(r, 'shell_km') + 50)} km`} sub="Browse objects in this shell" /> },180 { key: 'objects', label: 'Objects', num: true, render: (r) => fmtInt(n(r, 'objects')) },181 { key: 'active_payloads', label: 'Active payloads', num: true, render: (r) => fmtInt(n(r, 'active_payloads')) },182 { key: 'debris', label: 'Debris', num: true, render: (r) => fmtInt(n(r, 'debris')) },183 { key: 'constellations', label: 'Constellations', num: true, render: (r) => fmtInt(n(r, 'constellations')) },184 ],185 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.',186 },187 {188 key: 'launch-years',189 label: 'Launch years',190 title: 'Launch years by orbital launches',191 description: 'Calendar years ranked by orbital launches with catalogued objects, with the number of payloads catalogued for that year.',192 primaryLabel: 'Launches',193 primary: (r) => n(r, 'launches'),194 name: (r) => String(n(r, 'year')),195 href: (r) => routes.launches(`year=${n(r, 'year')}`),196 columns: [197 { key: 'year', label: 'Year', render: (r) => <NameCell mono href={routes.launches(`year=${n(r, 'year')}`)} name={String(n(r, 'year'))} /> },198 { key: 'launches', label: 'Launches', num: true, render: (r) => fmtInt(n(r, 'launches')) },199 { key: 'payloads', label: 'Payloads', num: true, render: (r) => fmtInt(n(r, 'payloads')) },200 { key: 'ratio', label: 'Payloads / launch', num: true, derived: true, render: (r) => <span className="mono">{n(r, 'launches') ? fmt1(n(r, 'payloads') / n(r, 'launches')) : '—'}</span> },201 ],202 },203];204205export const DEFAULT_METRIC = 'constellations';206export const findMetric = (key: string | undefined): MetricDef => METRICS.find((m) => m.key === key) ?? (METRICS[0] as MetricDef);207