/** Visualisation panels for /constellation/[slug]. Server components, pure SVG charts. */
import Link from 'next/link';
import { AreaChart, Bars, Donut, Histogram } from '@/components/charts/charts';
import { Unavailable } from '@/components/ui/unavailable';
import { fmtDeg, fmtInt, fmtKm, num, titleCase } from '@/lib/format';
import { STATUS_COLORS, routes } from '@/lib/site';
import type { ConstellationDetail } from '@/lib/types';
import { Derived, ScrollTable } from './shared';
export function DeploymentTimeline({ growth }: { growth: ConstellationDetail['growth'] }) {
const monthly = growth.map((g) => ({ x: g.month.slice(0, 7), y: num(g.launched) ?? 0 }));
const cumulative = growth.map((g) => ({ x: g.month.slice(0, 7), y: num(g.cumulative) ?? 0 }));
if (!growth.length) return ;
return (
Cumulative satellites launched
String(x).slice(0, 4)} />
);
}
export function StatusDonut({ dist, total }: { dist: ConstellationDetail['status_distribution']; total: number | null }) {
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);
return ;
}
export function OrbitalShells({ d }: { d: ConstellationDetail }) {
const alt = d.altitude_histogram.map((h) => ({ bin: num(h.alt_km) ?? 0, value: num(h.satellites) ?? 0 })).filter((h) => h.bin >= 0);
const incl = d.inclination_histogram.map((h) => ({ bin: num(h.incl_deg) ?? 0, value: num(h.satellites) ?? 0 }));
const shells = [...d.shells].sort((a, b) => (num(b.satellites) ?? 0) - (num(a.satellites) ?? 0)).slice(0, 10);
const nothing = !alt.length && !incl.length && !shells.length;
if (nothing) return ;
return (
Perigee altitude · satellites per 25 km
Inclination · satellites per 5°
Largest shells (perigee × inclination)
{shells.length === 0 ? (
) : (
| Perigee |
Inclination |
Satellites |
{shells.map((s, i) => (
| {fmtKm(s.perigee_km)} |
{fmtDeg(s.inclination_deg)} |
{fmtInt(s.satellites)} |
))}
)}
Shells bucket current elements to 20 km × 1°. Orbit classes are derived.
);
}
export function DecaysByMonth({ rows }: { rows: ConstellationDetail['decays_by_month'] }) {
const data = rows.map((r) => ({ x: r.month.slice(0, 7), y: num(r.decayed) ?? 0 }));
const total = data.reduce((s, d) => s + d.y, 0);
if (!data.length) return null;
return (
{fmtInt(total)} decays recorded · per month
);
}
export function MembershipNote({ d }: { d: ConstellationDetail }) {
const methods = d.membership_methods ?? [];
return (
Constellation membership is from curated rules, never from a single upstream field. Rules for this constellation:
- Name patterns
- {d.match_patterns.length ? d.match_patterns.map((p) =>
{p}) : none}
- CelesTrak groups
- {d.celestrak_groups.length ? d.celestrak_groups.map((g) =>
{g}) : none}
Members by method
{methods.length ? (
{methods.map((m) => (
{m.method} · {fmtInt(m.satellites)}
))}
) : (
Per-method breakdown unavailable for this constellation.
)}
Full rule set and metric versions on the methodology page. Activity score is a derived index of recent launch cadence, not a safety metric.
);
}