import Link from 'next/link';
import { predicateLabel } from '@/lib/site';
/*
Compact layered SVG graph for lineage edges among a known set of nodes (family members): roots left, derived models to the right.
Server-rendered, links on nodes, accessible edge list. For the interactive explorer use /graph.
*/
export type MiniNode = { id: string; label: string; href?: string; sub?: string };
export type MiniEdge = { source: string; target: string; predicate: string };
const W = 720;
const BOX_W = 168;
const BOX_H = 26;
const ROW = 32;
const PAD = 8;
const trunc = (s: string, n: number) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
export function MiniGraph({ nodes, edges, className, title = 'Lineage' }: { nodes: MiniNode[]; edges: MiniEdge[]; className?: string; title?: string }) {
const ids = new Set(nodes.map((n) => n.id));
const E = edges.filter((e) => ids.has(e.source) && ids.has(e.target));
if (!E.length) return
No lineage relation recorded among these members.
;
const involved = new Set(E.flatMap((e) => [e.source, e.target]));
const N = nodes.filter((n) => involved.has(n.id));
// depth = longest chain from a root; edges point source (derived) → target (base): the base is upstream, so depth(source) = depth(target) + 1
const depth = new Map();
const upstream = new Map();
for (const e of E) upstream.set(e.source, [...(upstream.get(e.source) ?? []), e.target]);
const visit = (id: string, seen: Set): number => {
if (depth.has(id)) return depth.get(id) as number;
if (seen.has(id)) return 0;
seen.add(id);
const ups = upstream.get(id) ?? [];
const d = ups.length ? 1 + Math.max(...ups.map((u) => visit(u, seen))) : 0;
depth.set(id, d);
return d;
};
for (const n of N) visit(n.id, new Set());
const maxD = Math.max(...N.map((n) => depth.get(n.id) ?? 0));
const cols = Array.from({ length: maxD + 1 }, () => [] as MiniNode[]);
for (const n of N) cols[depth.get(n.id) ?? 0]!.push(n);
const rows = Math.max(...cols.map((c) => c.length));
const H = PAD * 2 + rows * ROW;
const colX = (d: number) => (maxD === 0 ? W / 2 - BOX_W / 2 : PAD + (d * (W - PAD * 2 - BOX_W)) / maxD);
const pos = new Map();
cols.forEach((c, d) => c.forEach((n, i) => pos.set(n.id, { x: colX(d), y: PAD + ((H - PAD * 2) / Math.max(1, c.length)) * (i + 0.5) - BOX_H / 2 })));
const tooMany = N.length > 40;
return (
{tooMany &&
Large family — open the interactive graph for the full picture.
}
{E.map((e, i) => (
-
n.id === e.source)?.href ?? '#'}>{nodes.find((n) => n.id === e.source)?.label} {predicateLabel(e.predicate, 'out').toLowerCase()} n.id === e.target)?.href ?? '#'}>{nodes.find((n) => n.id === e.target)?.label}
))}
);
}