import { useEffect, useMemo, useState } from "react"; import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; type MergeFrequencyPoint = { date?: string; week?: string; mergeCount: number; }; type MergeFrequencyResponse = { success: boolean; data?: { bucket: "day" | "week"; days: number; points: MergeFrequencyPoint[]; forecastPoints?: number; forecast?: MergeFrequencyPoint[]; forecastMethod?: string; }; }; type Props = { owner: string; repo: string; }; export function MergeFrequencyPanel({ owner, repo }: Props) { const [bucket, setBucket] = useState<"day" | "week">("week"); const [days, setDays] = useState(84); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [points, setPoints] = useState([]); const [forecast, setForecast] = useState([]); useEffect(() => { const controller = new AbortController(); async function load() { setLoading(true); setError(""); try { const response = await fetch( `/api/repos/${owner}/${repo}/analytics/merge-frequency?bucket=${bucket}&days=${days}&forecastPoints=6`, { signal: controller.signal } ); if (!response.ok) { throw new Error("Failed to load merge frequency analytics"); } const payload = (await response.json()) as MergeFrequencyResponse; setPoints(payload.data?.points || []); setForecast(payload.data?.forecast || []); } catch (err) { if ((err as Error).name === "AbortError") return; setError("Unable to load merge frequency data"); } finally { setLoading(false); } } load(); return () => controller.abort(); }, [owner, repo, bucket, days]); const chartData = useMemo(() => { const historical = points.map((point) => ({ label: point.date || point.week || "", merged: point.mergeCount, forecast: null as number | null, segment: "historical", })); if (!forecast.length) { return historical; } const bridgeLabel = forecast[0]?.date || forecast[0]?.week || ""; const bridgeValue = historical.length ? historical[historical.length - 1]?.merged : null; const forecastRows = forecast.map((point, index) => ({ label: point.date || point.week || `future-${index}`, merged: null as number | null, forecast: point.mergeCount, segment: "forecast", })); // Add an overlap point so dashed line starts from last historical value. if (bridgeValue !== null && bridgeLabel) { forecastRows.unshift({ label: bridgeLabel, merged: null, forecast: bridgeValue, segment: "forecast", }); } return [...historical, ...forecastRows]; }, [points, forecast]); const avgMergeCount = useMemo(() => { if (!points.length) return 0; const total = points.reduce((sum, point) => sum + point.mergeCount, 0); return Math.round((total / points.length) * 100) / 100; }, [points]); return (

Merge Frequency Trend

Historical merge cadence with short-range forecast.

Average merged
{avgMergeCount}
Last period
{points[points.length - 1]?.mergeCount ?? 0}
Forecast horizon
{forecast.length ? `${forecast.length} ${bucket}s` : "0"}
{loading ? (
Loading merge frequency...
) : error ? (
{error}
) : ( { if (typeof value !== "string") return ""; if (value.includes("-W")) return value; const parsed = new Date(`${value}T00:00:00.000Z`); if (Number.isNaN(parsed.getTime())) return value; return parsed.toLocaleDateString(undefined, { month: "short", day: "numeric" }); }} /> )}
); }