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%
3.7 KB · 76 lines tsx
Raw Blame History
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