spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
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