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