"use client"; // Radial fan chart of a focus person's ancestors (family-chart has no fan). // Each generation is a ring; slot p in generation g descends from slot floor(p/2) // in g-1. Click a wedge to refocus. type Props = { focusId: string; parentsOf: (id: string) => string[]; nameOf: (id: string) => string; yearOf: (id: string) => string; onSelect: (id: string) => void; generations?: number; }; const SIZE = 720; const CENTER = SIZE / 2; const FOCUS_R = 46; const SPAN = Math.PI * 1.6; // 288° fan function polar(r: number, a: number): [number, number] { // a = 0 points up, increasing clockwise. return [CENTER + r * Math.sin(a), CENTER - r * Math.cos(a)]; } function sector(r0: number, r1: number, a0: number, a1: number): string { const [x0, y0] = polar(r1, a0); const [x1, y1] = polar(r1, a1); const [x2, y2] = polar(r0, a1); const [x3, y3] = polar(r0, a0); const large = a1 - a0 > Math.PI ? 1 : 0; return `M${x0} ${y0} A${r1} ${r1} 0 ${large} 1 ${x1} ${y1} L${x2} ${y2} A${r0} ${r0} 0 ${large} 0 ${x3} ${y3} Z`; } function clip(s: string, n: number): string { return s.length > n ? s.slice(0, n - 1) + "…" : s; } export function FanChart({ focusId, parentsOf, nameOf, yearOf, onSelect, generations = 4, }: Props) { const gens: (string | null)[][] = [[focusId]]; for (let g = 1; g <= generations; g++) { const row: (string | null)[] = []; for (const slot of gens[g - 1]) { const ps = slot ? parentsOf(slot) : []; row.push(ps[0] ?? null, ps[1] ?? null); } gens.push(row); } const ringT = (CENTER - 60 - FOCUS_R) / generations; const start = -SPAN / 2; const wedges: React.ReactNode[] = []; for (let g = 1; g <= generations; g++) { const row = gens[g]; const w = SPAN / row.length; const r0 = FOCUS_R + (g - 1) * ringT; const r1 = FOCUS_R + g * ringT; row.forEach((id, i) => { const a0 = start + i * w; const a1 = start + (i + 1) * w; const mid = (a0 + a1) / 2; const [tx, ty] = polar((r0 + r1) / 2, mid); let deg = (mid * 180) / Math.PI; if (deg > 90 || deg < -90) deg += 180; // keep text upright wedges.push( id && onSelect(id)} style={{ cursor: id ? "pointer" : "default" }} > {id && ( = 3 ? 9 : 11, fill: "var(--foreground)" }} > {clip(nameOf(id), g >= 3 ? 12 : 18)} )} , ); }); } const [fx, fy] = [CENTER, CENTER]; return (
{wedges} {clip(nameOf(focusId), 12)} {yearOf(focusId)}
); }