'use strict'; (function(root) { const W=184,H=104,GAP=34,ROW=166,PAD=36; // People appear once. Spouse groups share a row when this preserves the parent DAG. function layout(inputPeople,inputLinks,{selfId='',showArchived=false}={}) { const people=inputPeople.filter(p=>showArchived||!p.archived),byId=new Map(people.map(p=>[p.id,p])); const links=inputLinks.filter(e=>e.active&&byId.has(e.fromId)&&byId.has(e.toId)&&e.fromId!==e.toId); const adjacency=new Map(people.map(p=>[p.id,[]])); links.forEach(e=>{adjacency.get(e.fromId).push(e.toId);adjacency.get(e.toId).push(e.fromId);}); const seen=new Set(),components=[]; for(const p of people) { if(seen.has(p.id))continue; const ids=[p.id];seen.add(p.id); for(let i=0;i{ const x=byId.get(a),y=byId.get(b); return (x.birthDate||'9999').localeCompare(y.birthDate||'9999')||x.name.localeCompare(y.name,'zh-CN')||a.localeCompare(b); }; components.sort((a,b)=>Number(b.includes(selfId))-Number(a.includes(selfId))||b.length-a.length||order(a[0],b[0])); const nodes=[],sections=[],warnings=[];let offset=PAD,maxWidth=400; const connected=components.filter(c=>c.length>1),isolated=components.filter(c=>c.length===1).map(c=>c[0]); for(const [componentIndex,ids] of connected.entries()) { const idSet=new Set(ids),edges=links.filter(e=>idSet.has(e.fromId)); function ranked(alignSpouses) { const roots=new Map(ids.map(id=>[id,id])); function find(id){let r=id;while(roots.get(r)!==r)r=roots.get(r);while(roots.get(id)!==id){const next=roots.get(id);roots.set(id,r);id=next;}return r;} if(alignSpouses)for(const e of edges.filter(e=>e.kind==='spouse'))roots.set(find(e.toId),find(e.fromId)); const units=new Map();for(const id of ids){const key=find(id);if(!units.has(key))units.set(key,{id:key,ids:[],rank:0,out:new Set(),incoming:new Set()});units.get(key).ids.push(id);} for(const e of edges.filter(e=>e.kind==='parent')) { const a=units.get(find(e.fromId)),b=units.get(find(e.toId));if(a===b)return null; a.out.add(b.id);b.incoming.add(a.id); } const degree=new Map([...units.values()].map(u=>[u.id,u.incoming.size])); const queue=[...units.values()].filter(u=>!degree.get(u.id)); for(let i=0;i[id,{id,ids:[id],rank:0,out:new Set(),incoming:new Set()}])); warnings.push('检测到循环关系,请核对人物关系。'); } const rows=[]; for(const u of units.values()){u.ids.sort(order);u.width=u.ids.length*W+(u.ids.length-1)*GAP;(rows[u.rank]??=[]).push(u);} rows.forEach(row=>row.sort((a,b)=>order(a.ids[0],b.ids[0]))); // Barycentric passes keep sibling branches together without changing generations. const positions=new Map(); function measure(){for(const row of rows){let x=0;for(const u of row){positions.set(u.id,x+u.width/2);x+=u.width+GAP*2;}}} for(let pass=0;pass<4;pass++) { measure();const indices=rows.map((_,i)=>i);if(pass%2)indices.reverse(); for(const i of indices)rows[i].sort((a,b)=>{ const center=u=>{const ns=[...(pass%2?u.out:u.incoming)];return ns.length?ns.reduce((n,id)=>n+positions.get(id),0)/ns.length:positions.get(u.id);}; return center(a)-center(b)||order(a.ids[0],b.ids[0]); }); } const widths=rows.map(row=>row.reduce((n,u)=>n+u.width,0)+Math.max(0,row.length-1)*GAP*2); const width=Math.max(...widths)+PAD*2+70,height=rows.length*ROW+20; sections.push({x:PAD,y:offset,width:width-PAD*2,height,label:`家族分支 ${componentIndex+1} · ${ids.length} 位人物`}); rows.forEach((row,rank)=>{ let x=PAD+(width-PAD*2-70-widths[rank])/2; for(const u of row){for(const id of u.ids){nodes.push({id,x,y:offset+42+rank*ROW,width:W,height:H,rank,component:componentIndex});x+=W+GAP;}x+=GAP;} }); for(const node of nodes.filter(n=>n.component===componentIndex))node.corridor=width-PAD-46; offset+=height+48;maxWidth=Math.max(maxWidth,width); } if(isolated.length) { isolated.sort((a,b)=>Number(b===selfId)-Number(a===selfId)||order(a,b)); const beside=connected.length===1&&isolated.length<=4; const columns=beside?1:Math.min(5,isolated.length),width=columns*(W+GAP)-GAP+PAD*2; const x=beside?maxWidth:PAD,y=beside?PAD:offset,height=Math.ceil(isolated.length/columns)*150+42; sections.push({x,y,width:width-PAD*2,height,label:people.length===1?'从自己开始,连接每一代亲人':'待连接的人物'}); isolated.forEach((id,i)=>nodes.push({id,x:x+i%columns*(W+GAP),y:y+42+Math.floor(i/columns)*150,width:W,height:H,rank:0,component:-1})); offset=Math.max(offset,y+height);maxWidth=beside?maxWidth+width:Math.max(maxWidth,width); } const located=new Map(nodes.map(n=>[n.id,n])); const paths=links.map((e,i)=>{ const a=located.get(e.fromId),b=located.get(e.toId);let d; if(e.kind==='parent') { const sx=a.x+W/2,sy=a.y+H,tx=b.x+W/2,ty=b.y; if(b.rank===a.rank+1){const mid=(sy+ty)/2+(i%3-1)*9;d=`M${sx},${sy} V${mid} H${tx} V${ty}`;} else {const x=(a.corridor||maxWidth-PAD)+(i%5)*8;d=`M${sx},${sy} V${sy+18} H${x} V${ty-18} H${tx} V${ty}`;} }else{ const [left,right]=a.x<=b.x?[a,b]:[b,a]; if(a.y===b.y&&right.x-left.x<=W+GAP+1)d=`M${left.x+W},${left.y+H/2} H${right.x}`; else if(a.y===b.y){const top=a.y-16-(i%3)*8;d=`M${a.x+W/2},${a.y} V${top} H${b.x+W/2} V${b.y}`;} else {const x=(a.corridor||maxWidth-PAD)+(i%5)*8;d=`M${a.x+W},${a.y+H/2} H${x} V${b.y+H/2} H${b.x+W}`;} } return {...e,d}; }); return {nodes,paths,sections,width:maxWidth,height:Math.max(220,offset+PAD),warnings:[...new Set(warnings)],components:components.length}; } const api={layout};if(typeof module!=='undefined'&&module.exports)module.exports=api;else root.ZhaoFamilyGraph=api; })(typeof window!=='undefined'?window:globalThis);