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.5 KB · 130 lines tsx
Raw Blame History
1/** Visualisation panels for /constellation/[slug]. Server components, pure SVG charts. */2import Link from 'next/link';3import { AreaChart, Bars, Donut, Histogram } from '@/components/charts/charts';4import { Unavailable } from '@/components/ui/unavailable';5import { fmtDeg, fmtInt, fmtKm, num, titleCase } from '@/lib/format';6import { STATUS_COLORS, routes } from '@/lib/site';7import type { ConstellationDetail } from '@/lib/types';8import { Derived, ScrollTable } from './shared';910export function DeploymentTimeline({ growth }: { growth: ConstellationDetail['growth'] }) {11  const monthly = growth.map((g) => ({ x: g.month.slice(0, 7), y: num(g.launched) ?? 0 }));12  const cumulative = growth.map((g) => ({ x: g.month.slice(0, 7), y: num(g.cumulative) ?? 0 }));13  if (!growth.length) return <Unavailable what="Deployment timeline" />;14  return (15    <div className="grid gap-6 lg:grid-cols-2">16      <div>17        <p className="eyebrow mb-2">Cumulative satellites launched</p>18        <AreaChart data={cumulative} title="Cumulative satellites launched by month" height={180} xLabel={(x) => String(x).slice(0, 4)} />19      </div>20      <div>21        <p className="eyebrow mb-2">Launched per month</p>22        <Bars data={monthly} title="Satellites launched per month" height={180} color="var(--series-2)" highlightLast />23      </div>24    </div>25  );26}2728export function StatusDonut({ dist, total }: { dist: ConstellationDetail['status_distribution']; total: number | null }) {29  const data = dist.map((d) => ({ label: titleCase(d.status.toLowerCase()), value: num(d.count) ?? 0, color: STATUS_COLORS[d.status] ?? 'var(--other)' })).filter((d) => d.value > 0).sort((a, b) => b.value - a.value);30  return <Donut data={data} title="Satellites by status" total={total ?? undefined} size={150} />;31}3233export function OrbitalShells({ d }: { d: ConstellationDetail }) {34  const alt = d.altitude_histogram.map((h) => ({ bin: num(h.alt_km) ?? 0, value: num(h.satellites) ?? 0 })).filter((h) => h.bin >= 0);35  const incl = d.inclination_histogram.map((h) => ({ bin: num(h.incl_deg) ?? 0, value: num(h.satellites) ?? 0 }));36  const shells = [...d.shells].sort((a, b) => (num(b.satellites) ?? 0) - (num(a.satellites) ?? 0)).slice(0, 10);37  const nothing = !alt.length && !incl.length && !shells.length;38  if (nothing) return <Unavailable what="Orbital shell data (no current orbital elements for this constellation)" />;39  return (40    <div className="grid gap-6 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">41      <div className="space-y-6">42        <div>43          <p className="eyebrow mb-2">Perigee altitude · satellites per 25 km</p>44          <Histogram data={alt} title="Perigee altitude histogram" unit=" km" height={150} xTicks={6} />45        </div>46        <div>47          <p className="eyebrow mb-2">Inclination · satellites per 5°</p>48          <Histogram data={incl} title="Inclination histogram" unit="°" height={130} color="var(--series-2)" xTicks={6} />49        </div>50      </div>51      <div>52        <p className="eyebrow mb-2">Largest shells (perigee × inclination)</p>53        {shells.length === 0 ? (54          <Unavailable what="Shell table" compact />55        ) : (56          <ScrollTable>57            <table className="data-table">58              <thead>59                <tr>60                  <th className="num">Perigee</th>61                  <th className="num">Inclination</th>62                  <th className="num">Satellites</th>63                </tr>64              </thead>65              <tbody>66                {shells.map((s, i) => (67                  <tr key={i}>68                    <td className="num tnum">{fmtKm(s.perigee_km)}</td>69                    <td className="num tnum">{fmtDeg(s.inclination_deg)}</td>70                    <td className="num tnum font-medium">{fmtInt(s.satellites)}</td>71                  </tr>72                ))}73              </tbody>74            </table>75          </ScrollTable>76        )}77        <p className="mt-2 text-xs text-ink-3">Shells bucket current elements to 20 km × 1°. Orbit classes are <Link href={routes.methodology()} className="text-accent hover:underline">derived</Link>.</p>78      </div>79    </div>80  );81}8283export function DecaysByMonth({ rows }: { rows: ConstellationDetail['decays_by_month'] }) {84  const data = rows.map((r) => ({ x: r.month.slice(0, 7), y: num(r.decayed) ?? 0 }));85  const total = data.reduce((s, d) => s + d.y, 0);86  if (!data.length) return null;87  return (88    <div>89      <p className="eyebrow mb-2">{fmtInt(total)} decays recorded · per month</p>90      <Bars data={data} title="Decayed satellites per month" height={150} color="var(--series-8)" />91    </div>92  );93}9495export function MembershipNote({ d }: { d: ConstellationDetail }) {96  const methods = d.membership_methods ?? [];97  return (98    <div className="space-y-3 text-sm text-ink-2">99      <p className="flex flex-wrap items-center gap-2">100        <span>Constellation membership is</span> <Derived /> <span>from curated rules, never from a single upstream field. Rules for this constellation:</span>101      </p>102      <dl className="grid gap-3 sm:grid-cols-2">103        <div>104          <dt className="eyebrow">Name patterns</dt>105          <dd className="mt-1 flex flex-wrap gap-1.5">{d.match_patterns.length ? d.match_patterns.map((p) => <code key={p} className="mono rounded bg-plane-2 px-1.5 py-0.5 text-xs text-ink">{p}</code>) : <span className="text-ink-3">none</span>}</dd>106        </div>107        <div>108          <dt className="eyebrow">CelesTrak groups</dt>109          <dd className="mt-1 flex flex-wrap gap-1.5">{d.celestrak_groups.length ? d.celestrak_groups.map((g) => <code key={g} className="mono rounded bg-plane-2 px-1.5 py-0.5 text-xs text-ink">{g}</code>) : <span className="text-ink-3">none</span>}</dd>110        </div>111      </dl>112      <div>113        <p className="eyebrow">Members by method</p>114        {methods.length ? (115          <ul className="mt-1 flex flex-wrap gap-x-4 gap-y-1">116            {methods.map((m) => (117              <li key={m.method} className="tnum"><code className="mono text-xs text-ink">{m.method}</code> · {fmtInt(m.satellites)}</li>118            ))}119          </ul>120        ) : (121          <p className="mt-1 text-xs text-ink-3">Per-method breakdown unavailable for this constellation.</p>122        )}123      </div>124      <p className="text-xs text-ink-3">125        Full rule set and metric versions on the <Link href={routes.methodology()} className="text-accent hover:underline">methodology page</Link>. Activity score is a derived index of recent launch cadence, not a safety metric.126      </p>127    </div>128  );129}130