HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import Link from 'next/link';2import { predicateLabel } from '@/lib/site';34/*5 Compact layered SVG graph for lineage edges among a known set of nodes (family members): roots left, derived models to the right.6 Server-rendered, links on nodes, accessible edge list. For the interactive explorer use /graph.7*/89export type MiniNode = { id: string; label: string; href?: string; sub?: string };10export type MiniEdge = { source: string; target: string; predicate: string };1112const W = 720;13const BOX_W = 168;14const BOX_H = 26;15const ROW = 32;16const PAD = 8;17const trunc = (s: string, n: number) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);1819export function MiniGraph({ nodes, edges, className, title = 'Lineage' }: { nodes: MiniNode[]; edges: MiniEdge[]; className?: string; title?: string }) {20 const ids = new Set(nodes.map((n) => n.id));21 const E = edges.filter((e) => ids.has(e.source) && ids.has(e.target));22 if (!E.length) return <p className="text-sm text-ink-3">No lineage relation recorded among these members.</p>;23 const involved = new Set(E.flatMap((e) => [e.source, e.target]));24 const N = nodes.filter((n) => involved.has(n.id));25 // depth = longest chain from a root; edges point source (derived) → target (base): the base is upstream, so depth(source) = depth(target) + 126 const depth = new Map<string, number>();27 const upstream = new Map<string, string[]>();28 for (const e of E) upstream.set(e.source, [...(upstream.get(e.source) ?? []), e.target]);29 const visit = (id: string, seen: Set<string>): number => {30 if (depth.has(id)) return depth.get(id) as number;31 if (seen.has(id)) return 0;32 seen.add(id);33 const ups = upstream.get(id) ?? [];34 const d = ups.length ? 1 + Math.max(...ups.map((u) => visit(u, seen))) : 0;35 depth.set(id, d);36 return d;37 };38 for (const n of N) visit(n.id, new Set());39 const maxD = Math.max(...N.map((n) => depth.get(n.id) ?? 0));40 const cols = Array.from({ length: maxD + 1 }, () => [] as MiniNode[]);41 for (const n of N) cols[depth.get(n.id) ?? 0]!.push(n);42 const rows = Math.max(...cols.map((c) => c.length));43 const H = PAD * 2 + rows * ROW;44 const colX = (d: number) => (maxD === 0 ? W / 2 - BOX_W / 2 : PAD + (d * (W - PAD * 2 - BOX_W)) / maxD);45 const pos = new Map<string, { x: number; y: number }>();46 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 })));47 const tooMany = N.length > 40;48 return (49 <div className={className} data-mini-graph>50 <svg viewBox={`0 0 ${W} ${Math.min(H, 640)}`} className="block w-full" role="img" aria-label={`${title}: ${N.length} models, ${E.length} relations`}>51 <title>{title}</title>52 {E.map((e, i) => {53 const a = pos.get(e.target);54 const b = pos.get(e.source);55 if (!a || !b) return null;56 const x1 = a.x + BOX_W;57 const y1 = a.y + BOX_H / 2;58 const x2 = b.x;59 const y2 = b.y + BOX_H / 2;60 return (61 <g key={i}>62 <path d={`M${x1},${y1} C${(x1 + x2) / 2},${y1} ${(x1 + x2) / 2},${y2} ${x2},${y2}`} fill="none" stroke="var(--rule-strong)" strokeWidth={1.1} strokeDasharray={e.predicate === 'quantized_from' ? '3 2' : undefined} />63 <title>{`${nodes.find((n) => n.id === e.source)?.label} ${predicateLabel(e.predicate, 'out').toLowerCase()} ${nodes.find((n) => n.id === e.target)?.label}`}</title>64 </g>65 );66 })}67 {N.map((n) => {68 const p = pos.get(n.id)!;69 const body = (70 <g>71 <rect x={p.x} y={p.y} width={BOX_W} height={BOX_H} rx={3} fill="var(--surface)" stroke="var(--rule-strong)" />72 <text x={p.x + 7} y={p.y + 11} fontSize={10.5} fontWeight={500} fill="var(--ink)">73 {trunc(n.label, 26)}74 </text>75 {n.sub && (76 <text x={p.x + 7} y={p.y + 21} fontSize={8.5} fill="var(--ink-3)">77 {trunc(n.sub, 30)}78 </text>79 )}80 <title>{n.label}</title>81 </g>82 );83 return n.href ? (84 <a key={n.id} href={n.href} className="hover:opacity-80">85 {body}86 </a>87 ) : (88 <g key={n.id}>{body}</g>89 );90 })}91 </svg>92 {tooMany && <p className="text-[11px] text-ink-3">Large family — open the interactive graph for the full picture.</p>}93 <ul className="sr-only">94 {E.map((e, i) => (95 <li key={i}>96 <Link href={nodes.find((n) => n.id === e.source)?.href ?? '#'}>{nodes.find((n) => n.id === e.source)?.label}</Link> {predicateLabel(e.predicate, 'out').toLowerCase()} <Link href={nodes.find((n) => n.id === e.target)?.href ?? '#'}>{nodes.find((n) => n.id === e.target)?.label}</Link>97 </li>98 ))}99 </ul>100 </div>101 );102}103