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%
5.3 KB · 90 lines tsx
Raw Blame History
1import Link from 'next/link';2import { HBars, Histogram } from '@/components/charts/charts';3import { Unavailable } from '@/components/ui/unavailable';4import { fmtInt, num } from '@/lib/format';5import { ORBIT_CLASS_COLORS, routes } from '@/lib/site';6import type { BucketStat, DensityPayload } from '@/lib/types';7import { GroupedHistogram } from './local-charts';8import { ChartBlock, Note, TwoCol } from './shared';910const BUCKET_ORDER = ['0-200', '200-300', '300-400', '400-500', '500-600', '600-700', '700-800', '800-900', '900-1000', '1000-2000', 'MEO', 'GEO', 'HEO', 'OTHER', 'UNKNOWN'];1112export function sortBuckets<T extends { bucket: string }>(buckets: T[]): T[] {13  const idx = (b: string) => {14    const i = BUCKET_ORDER.indexOf(b);15    return i === -1 ? BUCKET_ORDER.length : i;16  };17  return [...buckets].sort((a, b) => idx(a.bucket) - idx(b.bucket));18}1920function bucketLabel(b: string): string {21  if (/^\d+-\d+$/.test(b)) return `LEO ${b} km`;22  return b === 'UNKNOWN' ? 'Unclassified' : b;23}24function bucketColor(b: string): string {25  if (/^\d+-\d+$/.test(b)) return ORBIT_CLASS_COLORS.LEO ?? 'var(--leo)';26  return ORBIT_CLASS_COLORS[b] ?? 'var(--other)';27}2829/** Orbital density section (informational object counts). `density` null → Unavailable. */30export function StatsDensity({ density, fallbackBuckets }: { density: DensityPayload | null; fallbackBuckets?: BucketStat[] }) {31  const buckets = sortBuckets(density?.buckets ?? fallbackBuckets ?? []);32  const bucketBars = buckets.map((b) => ({ label: bucketLabel(b.bucket), value: num(b.objects) ?? 0, color: bucketColor(b.bucket) }));3334  const leo = density ? [...density.leo_profile_25km].map((r) => ({ bin: num(r.alt_km) ?? 0, values: [num(r.active_payloads) ?? 0, num(r.debris) ?? 0] })).sort((a, b) => a.bin - b.bin) : [];35  const incl = density ? [...density.inclination_profile_5deg].map((r) => ({ bin: num(r.incl_deg) ?? 0, value: num(r.objects) ?? 0 })).sort((a, b) => a.bin - b.bin) : [];36  const inclActive = density ? [...density.inclination_profile_5deg].map((r) => ({ bin: num(r.incl_deg) ?? 0, values: [num(r.active_payloads) ?? 0, (num(r.objects) ?? 0) - (num(r.active_payloads) ?? 0)] })).sort((a, b) => a.bin - b.bin) : [];3738  return (39    <div className="space-y-12">40      <Note tone="warn">41        {density?.methodology ?? 'Objects on orbit by perigee altitude. Informational density, not a collision-risk metric.'} SatelliteIndex publishes object counts only — never collision probabilities.{' '}42        <Link href={routes.methodology()} className="text-accent hover:underline">Methodology</Link>43      </Note>4445      <TwoCol>46        <ChartBlock title="Objects on orbit by altitude band" hint="Perigee altitude" derived>47          {bucketBars.length ? <HBars data={bucketBars} /> : <Unavailable what="Altitude bands" />}48          <table className="data-table stack mt-5 text-sm">49            <thead>50              <tr>51                <th>Band</th>52                <th className="num">Objects</th>53                <th className="num">Active payloads</th>54                <th className="num">Debris</th>55                <th className="num">Rocket bodies</th>56              </tr>57            </thead>58            <tbody>59              {buckets.map((b) => (60                <tr key={b.bucket}>61                  <td className="primary" data-label="Band">{bucketLabel(b.bucket)}</td>62                  <td className="num tnum" data-label="Objects">{fmtInt(b.objects)}</td>63                  <td className="num tnum" data-label="Active payloads">{fmtInt(b.active_payloads)}</td>64                  <td className="num tnum" data-label="Debris">{fmtInt(b.debris)}</td>65                  <td className="num tnum" data-label="Rocket bodies">{fmtInt(b.rocket_bodies)}</td>66                </tr>67              ))}68            </tbody>69          </table>70        </ChartBlock>7172        <div className="space-y-10">73          <ChartBlock title="LEO altitude profile" hint="25 km bins · perigee" derived>74            {leo.length ? <GroupedHistogram data={leo} series={[{ label: 'Active payloads', color: 'var(--series-1)' }, { label: 'Debris', color: 'var(--series-4)' }]} title="LEO altitude profile: active payloads vs debris per 25 km" unit=" km" height={180} /> : <Unavailable what="LEO profile" />}75          </ChartBlock>76          <ChartBlock title="Inclination profile" hint="5° bins · all objects on orbit" derived>77            {incl.length ? <Histogram data={incl} title="Objects on orbit by inclination (5° bins)" unit="°" color="var(--series-2)" height={150} xTicks={8} /> : <Unavailable what="Inclination profile" />}78            {inclActive.length > 0 && <GroupedHistogram data={inclActive} series={[{ label: 'Active payloads', color: 'var(--series-3)' }, { label: 'Other objects', color: 'var(--inactive)' }]} title="Inclination profile: active payloads vs other objects" unit="°" height={150} className="mt-4" />}79          </ChartBlock>80        </div>81      </TwoCol>8283      <Note>84        Debris growth by launch year, fragmentation sources and the largest tracked objects are on <Link href={routes.debris()} className="text-accent hover:underline">/debris</Link>. The most populated 50 km shells are listed under{' '}85        <Link href={routes.rankings('congested-shells')} className="text-accent hover:underline">Rankings → LEO shells</Link>.86      </Note>87    </div>88  );89}90