SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
12.4 KB · 207 lines tsx
Raw Blame History
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