spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import Link from 'next/link';2import { Section } from '@/components/ui/section';3import { Unavailable } from '@/components/ui/unavailable';4import { fmtInt, num } from '@/lib/format';5import { routes } from '@/lib/site';6import type { BucketStat } from '@/lib/types';78const SPECIAL_ORDER = ['MEO', 'GEO', 'HEO', 'OTHER', 'UNKNOWN'];910/** 0-200 … 1000-2000 by lower bound, then MEO, GEO, HEO, other/unknown. */11export function orderBuckets(buckets: BucketStat[]): BucketStat[] {12 const rank = (b: string) => {13 const m = /^(\d+)-/.exec(b);14 if (m) return Number(m[1]);15 const i = SPECIAL_ORDER.indexOf(b.toUpperCase());16 return 1_000_000 + (i < 0 ? SPECIAL_ORDER.length : i);17 };18 return [...buckets].sort((a, b) => rank(a.bucket) - rank(b.bucket));19}2021function label(b: string): string {22 return /^\d+-\d+$/.test(b) ? `${b} km` : b === 'UNKNOWN' ? 'Unknown' : b === 'OTHER' ? 'Other' : b;23}2425const SEGMENTS: { key: keyof BucketStat; label: string; color: string }[] = [26 { key: 'active_payloads', label: 'Active payloads', color: 'var(--series-1)' },27 { key: 'payloads', label: 'Inactive payloads', color: 'var(--series-7)' },28 { key: 'rocket_bodies', label: 'Rocket bodies', color: 'var(--series-4)' },29 { key: 'debris', label: 'Debris', color: 'var(--series-8)' },30];3132/** Orbital density as a horizontal bar profile (one row per altitude shell), stacked by object category. */33export function OrbitalDensity({ buckets }: { buckets: BucketStat[] }) {34 const rows = orderBuckets(buckets);35 const max = Math.max(1, ...rows.map((b) => num(b.objects) ?? 0));36 return (37 <Section eyebrow="Orbital density" title="Where the objects are" action={{ href: routes.stats(), label: 'Full statistics' }} className="py-0">38 <p className="mb-4 max-w-2xl text-xs text-ink-3">39 Objects on orbit by altitude shell (perigee) and orbit regime. Derived from the latest element sets — see the <Link href={routes.methodology()} className="link">methodology</Link>. Not a collision-risk metric.40 </p>41 {rows.length === 0 ? (42 <Unavailable what="Orbital density" />43 ) : (44 <>45 <ol className="space-y-1.5">46 {rows.map((b) => {47 const total = num(b.objects) ?? 0;48 const active = num(b.active_payloads) ?? 0;49 const payloads = Math.max(0, (num(b.payloads) ?? 0) - active);50 const parts = [active, payloads, num(b.rocket_bodies) ?? 0, num(b.debris) ?? 0];51 return (52 <li key={b.bucket} className="grid grid-cols-[5.5rem_minmax(0,1fr)_4rem] items-center gap-3 sm:grid-cols-[7rem_minmax(0,1fr)_5rem]">53 <span className="mono truncate text-xs text-ink-2">{label(b.bucket)}</span>54 <span className="flex h-[14px] w-full overflow-hidden rounded-sm bg-plane-2" role="img" aria-label={`${label(b.bucket)}: ${fmtInt(total)} objects`}>55 {parts.map((v, i) => (56 <span key={SEGMENTS[i]!.key} className="h-full" style={{ width: `${(v / max) * 100}%`, background: SEGMENTS[i]!.color, opacity: i === 1 ? 0.5 : 0.9 }} />57 ))}58 </span>59 <span className="tnum text-right text-xs text-ink">{fmtInt(total)}</span>60 </li>61 );62 })}63 </ol>64 <ul className="mt-4 flex flex-wrap gap-x-4 gap-y-1 text-2xs text-ink-2">65 {SEGMENTS.map((s, i) => (66 <li key={s.key} className="inline-flex items-center gap-1.5">67 <span className="inline-block size-2.5 rounded-sm" style={{ background: s.color, opacity: i === 1 ? 0.5 : 0.9 }} /> {s.label}68 </li>69 ))}70 </ul>71 </>72 )}73 </Section>74 );75}76