spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import Link from 'next/link';2import { Donut, HBars, SERIES } from '@/components/charts/charts';3import { fmtInt, num, titleCase } from '@/lib/format';4import { MISSION_LABELS, OBJECT_TYPE_LABELS, ORBIT_CLASS_COLORS, routes } from '@/lib/site';5import type { StatsSnapshot } from '@/lib/types';6import { Legend } from './local-charts';7import { ChartBlock, Note } from './shared';89const ORBIT_ORDER = ['LEO', 'MEO', 'GEO', 'HEO', 'OTHER', 'UNKNOWN'];1011/** Composition of the catalogue: orbit class (two donuts), object type, mission type, constellation shares. */12export function StatsComposition({ snapshot }: { snapshot: StatsSnapshot }) {13 const byOrbit = [...snapshot.by_orbit_class].sort((a, b) => ORBIT_ORDER.indexOf(a.orbit_class) - ORBIT_ORDER.indexOf(b.orbit_class));14 const activeDonut = byOrbit.map((r) => ({ label: r.orbit_class === 'UNKNOWN' ? 'Unclassified' : r.orbit_class, value: num(r.active) ?? 0, color: ORBIT_CLASS_COLORS[r.orbit_class] ?? 'var(--other)' })).filter((d) => d.value > 0);15 const onOrbitDonut = byOrbit.map((r) => ({ label: r.orbit_class === 'UNKNOWN' ? 'Unclassified' : r.orbit_class, value: num(r.on_orbit) ?? 0, color: ORBIT_CLASS_COLORS[r.orbit_class] ?? 'var(--other)' })).filter((d) => d.value > 0);1617 const byType = [...snapshot.by_object_type].sort((a, b) => (num(b.on_orbit) ?? 0) - (num(a.on_orbit) ?? 0));18 const typeBars = byType.map((r, i) => ({ label: OBJECT_TYPE_LABELS[r.object_type] ?? titleCase(r.object_type), value: num(r.on_orbit) ?? 0, color: SERIES[i % SERIES.length] }));1920 const byMission = [...snapshot.by_mission_type].sort((a, b) => (num(b.active) ?? 0) - (num(a.active) ?? 0));21 const missionBars = byMission.map((r) => ({ label: MISSION_LABELS[r.mission_type] ?? titleCase(r.mission_type), value: num(r.active) ?? 0, color: 'var(--series-3)' }));2223 const totalActive = num(snapshot.global.active_satellites) ?? 0;24 const constellations = [...snapshot.top_constellations].sort((a, b) => (num(b.active) ?? 0) - (num(a.active) ?? 0)).slice(0, 12);25 const constellationActive = constellations.reduce((s, c) => s + (num(c.active) ?? 0), 0);2627 return (28 <div className="space-y-12">29 <div className="grid gap-10 lg:grid-cols-2 lg:gap-12">30 <ChartBlock title="Active satellites by orbit class" hint={`${fmtInt(snapshot.global.active_satellites)} active`} derived>31 <Donut data={activeDonut} title="Active satellites by orbit class" size={150} />32 </ChartBlock>33 <ChartBlock title="Objects on orbit by orbit class" hint={`${fmtInt(snapshot.global.objects_on_orbit)} objects`} derived>34 <Donut data={onOrbitDonut} title="Objects on orbit by orbit class" size={150} />35 </ChartBlock>36 </div>37 <Legend items={ORBIT_ORDER.filter((c) => byOrbit.some((r) => r.orbit_class === c)).map((c) => ({ label: c === 'UNKNOWN' ? 'Unclassified (no element set)' : c, color: ORBIT_CLASS_COLORS[c] ?? 'var(--other)' }))} className="-mt-6" />3839 <div className="grid gap-10 lg:grid-cols-2 lg:gap-12">40 <ChartBlock title="Objects on orbit by type" hint="SATCAT object type">41 <HBars data={typeBars} />42 <table className="data-table mt-5 text-sm">43 <thead>44 <tr>45 <th>Type</th>46 <th className="num">On orbit</th>47 <th className="num">Catalogued</th>48 <th className="num">Decayed</th>49 </tr>50 </thead>51 <tbody>52 {byType.map((r) => {53 const total = num(r.total) ?? 0;54 const on = num(r.on_orbit) ?? 0;55 return (56 <tr key={r.object_type}>57 <td>{OBJECT_TYPE_LABELS[r.object_type] ?? titleCase(r.object_type)}</td>58 <td className="num tnum">{fmtInt(on)}</td>59 <td className="num tnum">{fmtInt(total)}</td>60 <td className="num tnum text-ink-3">{fmtInt(total - on)}</td>61 </tr>62 );63 })}64 </tbody>65 </table>66 </ChartBlock>67 <ChartBlock title="Active satellites by mission type" hint="Active payloads" derived note="Mission type is classified by SatelliteIndex from names, constellation registry and CelesTrak groups.">68 <HBars data={missionBars} />69 </ChartBlock>70 </div>7172 <ChartBlock title="Constellation shares" hint={`Top ${constellations.length} · ${totalActive ? ((constellationActive / totalActive) * 100).toFixed(1) : '—'}% of active satellites`} derived>73 <ul className="space-y-1.5">74 {constellations.map((c, i) => {75 const active = num(c.active) ?? 0;76 const pct = totalActive ? (active / totalActive) * 100 : 0;77 const maxActive = num(constellations[0]?.active) ?? 1;78 return (79 <li key={c.id} className="text-sm">80 <div className="flex items-baseline justify-between gap-3">81 <Link href={routes.constellation(c.slug)} className="link min-w-0 truncate">82 <span className="mono mr-2 text-[11px] text-ink-3">{String(i + 1).padStart(2, '0')}</span>83 {c.name}84 </Link>85 <span className="tnum shrink-0 text-xs text-ink-2">86 {fmtInt(active)} <span className="text-ink-3">· {pct < 0.1 ? '<0.1' : pct.toFixed(1)}%</span>87 </span>88 </div>89 <div className="mt-1 h-[6px] w-full overflow-hidden rounded-full bg-plane-2">90 <div className="h-full rounded-full" style={{ width: `${Math.max(1, (active / maxActive) * 100)}%`, background: ORBIT_CLASS_COLORS[c.orbit_class ?? ''] ?? 'var(--series-1)' }} />91 </div>92 </li>93 );94 })}95 </ul>96 <Note className="mt-3">97 Share = active satellites in the constellation ÷ all active satellites. Bar colour = orbit class. Constellation membership is derived from the SatelliteIndex registry —{' '}98 <Link href={routes.rankings('constellations')} className="text-accent hover:underline">full ranking</Link>.99 </Note>100 </ChartBlock>101 </div>102 );103}104