135 lines
4.5 KiB
TypeScript
135 lines
4.5 KiB
TypeScript
import {
|
||
ComposedChart,
|
||
Bar,
|
||
Line,
|
||
XAxis,
|
||
YAxis,
|
||
CartesianGrid,
|
||
Tooltip,
|
||
ResponsiveContainer,
|
||
Cell,
|
||
} from 'recharts';
|
||
|
||
interface YearPoint {
|
||
date: string;
|
||
alt_at_midnight: number;
|
||
transit_alt: number;
|
||
usable_min: number;
|
||
moon_illumination: number;
|
||
}
|
||
|
||
interface Props {
|
||
points: YearPoint[];
|
||
}
|
||
|
||
const MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||
|
||
function altColor(alt: number): string {
|
||
if (alt >= 50) return 'var(--good)';
|
||
if (alt >= 30) return '#2ab8a0';
|
||
if (alt >= 15) return 'var(--warn)';
|
||
return 'var(--muted)';
|
||
}
|
||
|
||
export default function YearlyVisibility({ points }: Props) {
|
||
if (!points.length) return null;
|
||
|
||
// Sample to ~52 weekly points for readability
|
||
const stride = Math.max(1, Math.floor(points.length / 52));
|
||
const sampled = points.filter((_, i) => i % stride === 0);
|
||
|
||
const data = sampled.map(p => {
|
||
const d = new Date(p.date + 'T00:00:00Z');
|
||
return {
|
||
label: `${MONTH_ABBR[d.getUTCMonth()]} ${d.getUTCDate()}`,
|
||
month: d.getUTCMonth(),
|
||
alt: Math.round(p.alt_at_midnight * 10) / 10,
|
||
transit_alt: Math.round(p.transit_alt),
|
||
usable: Math.round(p.usable_min / 60 * 10) / 10,
|
||
moon: Math.round(p.moon_illumination * 100),
|
||
};
|
||
});
|
||
|
||
return (
|
||
<div>
|
||
<div style={{ fontSize: 11, color: 'var(--text-lo)', fontFamily: 'var(--font-mono)', marginBottom: 6 }}>
|
||
ALTITUDE AT MIDNIGHT — next 12 months (varies as transit shifts through seasons)
|
||
</div>
|
||
<div style={{ width: '100%', height: 160 }}>
|
||
<ResponsiveContainer>
|
||
<ComposedChart data={data} margin={{ top: 4, right: 8, bottom: 4, left: -18 }}>
|
||
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" vertical={false} />
|
||
<XAxis
|
||
dataKey="label"
|
||
tick={{ fill: 'var(--text-lo)', fontSize: 9, fontFamily: 'IBM Plex Mono' }}
|
||
interval={Math.floor(data.length / 12)}
|
||
tickLine={false}
|
||
/>
|
||
<YAxis
|
||
yAxisId="alt"
|
||
domain={[0, 90]}
|
||
tick={{ fill: 'var(--text-lo)', fontSize: 9, fontFamily: 'IBM Plex Mono' }}
|
||
tickLine={false}
|
||
tickFormatter={v => `${v}°`}
|
||
width={32}
|
||
/>
|
||
<YAxis
|
||
yAxisId="moon"
|
||
orientation="right"
|
||
domain={[0, 100]}
|
||
tick={{ fill: 'var(--text-lo)', fontSize: 9, fontFamily: 'IBM Plex Mono' }}
|
||
tickLine={false}
|
||
tickFormatter={v => `${v}%`}
|
||
width={28}
|
||
/>
|
||
<Tooltip
|
||
contentStyle={{
|
||
background: 'var(--bg-panel)',
|
||
border: '1px solid var(--border-hi)',
|
||
borderRadius: 4,
|
||
fontFamily: 'IBM Plex Mono',
|
||
fontSize: 11,
|
||
color: 'var(--text-hi)',
|
||
}}
|
||
formatter={(value: number, name: string) => {
|
||
if (name === 'alt') return [`${value}°`, 'Alt at midnight'];
|
||
if (name === 'moon') return [`${value}%`, 'Moon'];
|
||
return [value, name];
|
||
}}
|
||
/>
|
||
<Bar yAxisId="alt" dataKey="alt" radius={[1, 1, 0, 0]} maxBarSize={12}>
|
||
{data.map((entry, i) => (
|
||
<Cell key={i} fill={altColor(entry.alt)} fillOpacity={0.7} />
|
||
))}
|
||
</Bar>
|
||
<Line
|
||
yAxisId="moon"
|
||
type="monotone"
|
||
dataKey="moon"
|
||
stroke="#4d9de0"
|
||
strokeWidth={1}
|
||
dot={false}
|
||
strokeOpacity={0.5}
|
||
strokeDasharray="3 2"
|
||
/>
|
||
</ComposedChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 16, marginTop: 6, flexWrap: 'wrap' }}>
|
||
{[
|
||
{ color: 'var(--good)', label: '≥50° excellent' },
|
||
{ color: '#2ab8a0', label: '30–50° good' },
|
||
{ color: 'var(--warn)', label: '15–30° marginal' },
|
||
{ color: 'var(--muted)', label: '<15° poor' },
|
||
{ color: '#4d9de0', label: 'Moon %' },
|
||
].map(({ color, label }) => (
|
||
<div key={label} style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||
<div style={{ width: 10, height: 10, background: color, borderRadius: 2, opacity: 0.8 }} />
|
||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)' }}>{label}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|