From 9321011bfedd755132bfc2108efeb83230bd94c9 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Mon, 16 Mar 2026 21:04:11 +0100 Subject: [PATCH] Scope font-size rules to fng-container to avoid overriding full-diagram styles --- resources/css/family-nav-graph.css | 8 +-- resources/js/family-nav-graph.min.js | 2 +- resources/js/modules/lib/chart/box.js | 4 +- resources/js/modules/lib/layout/layout.js | 59 +++++++++++++++++++++-- 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/resources/css/family-nav-graph.css b/resources/css/family-nav-graph.css index eb0c80a..fc8be7e 100644 --- a/resources/css/family-nav-graph.css +++ b/resources/css/family-nav-graph.css @@ -64,15 +64,15 @@ fill: rgba(255, 255, 255, 0.6); } -/* Card text */ -.person-card .person-name { - font-size: 9px; +/* Card text — scoped to fng-container to avoid overriding full-diagram */ +.fng-container .person-card .person-name { + font-size: 8px; font-weight: 600; fill: #212529; dominant-baseline: auto; } -.person-card .person-dates { +.fng-container .person-card .person-dates { font-size: 8px; fill: #6c757d; dominant-baseline: auto; diff --git a/resources/js/family-nav-graph.min.js b/resources/js/family-nav-graph.min.js index a147905..410ce86 100644 --- a/resources/js/family-nav-graph.min.js +++ b/resources/js/family-nav-graph.min.js @@ -1 +1 @@ -!function(){"use strict";function t(t,s,o){const i=new Map;for(const n of t)i.set(n.id,n);const a=o.cardWidth,c=o.cardHeight,u=o.horizontalSpacing,l=o.verticalSpacing;o.targetWidth;const h=function(t,n){const e=new Map;e.set(n,0);const r=[n],s=new Set([n]);for(;r.length>0;){const n=r.shift(),o=e.get(n),i=t.get(n);if(i){for(const n of i.rels.spouses||[])!s.has(n)&&t.has(n)&&(e.set(n,o),s.add(n),r.push(n));for(const n of i.rels.parents||[])!s.has(n)&&t.has(n)&&(e.set(n,o-1),s.add(n),r.push(n));for(const n of i.rels.children||[])!s.has(n)&&t.has(n)&&(e.set(n,o+1),s.add(n),r.push(n))}}return e}(i,s),f=function(t){const n=new Map;for(const[e,r]of t){const s=(r.rels.parents||[]).filter(n=>t.has(n));if(0===s.length)continue;const o=[...s].sort().join("|");n.has(o)||n.set(o,{parents:[...s].sort(),children:[]}),n.get(o).children.push(e)}return[...n.values()]}(i),p=f.filter(t=>t.parents.includes(s)&&t.children.length>0).slice(0,2),d=i.get(s),g=new Set(d&&d.rels.spouses||[]);if(g.size>0)for(const[t,n]of h)0!==n||t===s||g.has(t)||h.set(t,-.5);const y=new Set;for(const t of p)for(const n of t.parents)n!==s&&y.add(n);if(p.length>=2)for(const t of y)h.has(t)&&h.set(t,.5);const m=new Map;for(const[t,n]of h)m.has(n)||m.set(n,[]),m.get(n).push(t);const _=new Map,x=[...m.keys()].sort((t,n)=>t-n);let v=0;for(const t of x){const r=n(m.get(t),t,s,f,i,p);if(t>=1&&p.length>=2){const t=r.filter(t=>p[0].children.includes(t)),n=r.filter(t=>p[1].children.includes(t)),s=e(t,1),o=e(n,1),i=Math.max(s.length,o.length),h=-(2*a+u)/2+a/2,f=h+a+u;for(let t=0;t0?r.length:2);for(let n=0;n=2,h=n.get(s),f=12;for(const e of t){if(o.indexOf(e)>=0&&l)continue;const t=e.parents.map(t=>n.get(t)).filter(Boolean),s=e.children.map(t=>({id:t,...n.get(t)})).filter(t=>void 0!==t.x);if(0===t.length||0===s.length)continue;const i=Math.max(...t.map(t=>t.y))+c,u=i+.3*(Math.min(...s.map(t=>t.y))-c-i),h=t.reduce((t,n)=>t+n.x,0)/t.length;if(t.length>=2){const n=t.map(t=>t.x).sort((t,n)=>t-n);a.push({path:`M ${n[0]} ${u} L ${n[n.length-1]} ${u}`,cssClass:"link couple-link"})}for(const n of t)a.push({path:`M ${n.x} ${n.y+c} L ${n.x} ${u}`,cssClass:"link ancestor-link"});r(a,s,h,u,c)}if(l&&h)for(let t=0;tt!==s),l=i?n.get(i):null,p=e.children.map(t=>({id:t,...n.get(t)})).filter(t=>void 0!==t.x);if(!l||0===p.length)continue;const d=l.y+c,g=Math.min(...p.map(t=>t.y))-c-d,y=1===p.length;let m;m=0===t?Math.min(...p.map(t=>t.x))-u/2-f:Math.max(...p.map(t=>t.x))+u/2+f;const _=d+.3*g,x=(h.y+c+l.y-c)/2;if(a.push({path:`M ${h.x} ${h.y+c} L ${h.x} ${x}`,cssClass:"link couple-link"}),a.push({path:`M ${h.x} ${x} L ${m} ${x}`,cssClass:"link couple-link"}),a.push({path:`M ${m} ${x} L ${m} ${_}`,cssClass:"link couple-link"}),a.push({path:`M ${l.x} ${d} L ${l.x} ${_}`,cssClass:"link couple-link"}),a.push({path:`M ${m} ${_} L ${l.x} ${_}`,cssClass:"link couple-link"}),y)a.push({path:`M ${l.x} ${_} L ${l.x} ${p[0].y-c}`,cssClass:"link descendant-link"});else{const t=Math.min(...p.map(t=>t.y))-c-6,n=(m+l.x)/2;a.push({path:`M ${n} ${_} L ${n} ${t}`,cssClass:"link descendant-link"}),r(a,p,m,t,c)}}const p=i.get(s),d=new Set;for(const t of o)for(const n of t.parents)n!==s&&d.add(n);if(p&&h&&l)for(const t of p.rels.spouses||[]){if(d.has(t))continue;const e=n.get(t);if(!e)continue;const r=h.y+c+8;a.push({path:`M ${h.x} ${h.y+c} L ${h.x} ${r}`,cssClass:"link couple-link"}),a.push({path:`M ${h.x} ${r} L ${e.x} ${r}`,cssClass:"link couple-link"}),a.push({path:`M ${e.x} ${r} L ${e.x} ${e.y+c}`,cssClass:"link couple-link"})}return a}(f,_,o,s,p,i);return{persons:w,connections:M}}function n(t,n,e,r,s,o){return-.5===n?function(t,n,e){const r=[],s=new Set;for(const o of n){const n=o.children.filter(n=>t.includes(n)&&!s.has(n));n.sort((t,n)=>(e.get(t)?.data?.birthYear||9999)-(e.get(n)?.data?.birthYear||9999));for(const t of n)r.push(t),s.add(t)}for(const n of t)s.has(n)||r.push(n);return r}(t,r,s):n<0?function(t,n){const e=n.filter(n=>n.parents.some(n=>t.includes(n)));if(0===e.length)return[...t];const r=[],s=new Set,o=[...e],i=o.shift();for(const n of i.parents)t.includes(n)&&!s.has(n)&&(r.push(n),s.add(n));for(;o.length>0;){const n=o.findIndex(t=>t.parents.some(t=>s.has(t))),e=n>=0?o.splice(n,1)[0]:o.shift();for(const n of e.parents)t.includes(n)&&!s.has(n)&&(r.push(n),s.add(n))}for(const n of t)s.has(n)||r.push(n);return r}(t,r):0===n?function(t,n,e,r){const s=r.get(n),o=[],i=new Set;o.push(n),i.add(n);for(const n of s.rels.spouses||[])t.includes(n)&&!i.has(n)&&(o.push(n),i.add(n));for(const n of t)i.has(n)||o.push(n);return o}(t,e,0,s):.5===n?function(t,n){const e=[],r=new Set;for(const s of n)for(const n of s.parents)t.includes(n)&&!r.has(n)&&(e.push(n),r.add(n));for(const n of t)r.has(n)||e.push(n);return e}(t,o):function(t,n,e,r){const s=[],o=new Set;for(const n of r)for(const e of n.children)t.includes(e)&&!o.has(e)&&(s.push(e),o.add(e));const i=e.filter(t=>t.parents.includes(n));for(const n of i)if(!r.includes(n))for(const e of n.children)t.includes(e)&&!o.has(e)&&(s.push(e),o.add(e));for(const n of t)o.has(n)||s.push(n);return s}(t,e,r,o)}function e(t,n){const e=[];for(let r=0;rt-n);if(a.length>1){const n=a.map(t=>t-s-6);r>n[0]&&(n[0]=r);const o=n[n.length-1];t.push({path:`M ${e} ${r} L ${e} ${o}`,cssClass:"link descendant-link"});for(let r=0;rt.x).sort((t,n)=>t-n),l=Math.min(u[0],e),h=Math.max(u[u.length-1],e);t.push({path:`M ${l} ${c} L ${h} ${c}`,cssClass:"link descendant-link"});for(const n of o)t.push({path:`M ${n.x} ${c} L ${n.x} ${n.y-s}`,cssClass:"link descendant-link"})}}else{const o=a[0]-s-6,i=Math.max(o,r);if(r1&&t.push({path:`M ${e} ${i} L ${n[0].x} ${i}`,cssClass:"link descendant-link"}),t.push({path:`M ${n[0].x} ${i} L ${n[0].x} ${n[0].y-s}`,cssClass:"link descendant-link"});else{const r=n.map(t=>t.x).sort((t,n)=>t-n),o=Math.min(r[0],e),a=Math.max(r[r.length-1],e);t.push({path:`M ${o} ${i} L ${a} ${i}`,cssClass:"link descendant-link"});for(const e of n)t.push({path:`M ${e.x} ${i} L ${e.x} ${e.y-s}`,cssClass:"link descendant-link"})}}}function s(t,n,e,r){const s=n.data,o=e.cardWidth,i=e.cardHeight,a=`sex-${(s.gender||"u").toLowerCase()}`,c=n.isMain?"is-root":"",u=t.append("g").attr("class",`person-card ${a} ${c}`.trim()).attr("transform",`translate(${n.x-o/2}, ${n.y-i/2})`).style("cursor","pointer").on("click",t=>{t.stopPropagation(),r({id:n.id,data:s})});if(s.hasMoreAncestors){const t=u.append("g").attr("class","more-ancestors-indicator").style("cursor","pointer").on("click",t=>{t.stopPropagation(),r({id:n.id,data:s})}),e=10,i=7,a=4,c=o-25,l=-14,h=c-a/2-e,f=c+a/2,p=l+i;t.append("line").attr("x1",h+e/2).attr("y1",p).attr("x2",f+e/2).attr("y2",p),t.append("line").attr("x1",c).attr("y1",p).attr("x2",c).attr("y2",0),t.append("rect").attr("x",h).attr("y",l).attr("width",e).attr("height",i).attr("rx",1).attr("ry",1),t.append("rect").attr("x",f).attr("y",l).attr("width",e).attr("height",i).attr("rx",1).attr("ry",1)}if(s.hasMoreDescendants){const t=u.append("g").attr("class","more-descendants-indicator").style("cursor","pointer").on("click",t=>{t.stopPropagation(),r({id:n.id,data:s})}),e=10,a=7,c=4,l=o-25,h=i+7,f=l-c/2-e,p=l+c/2,d=h;t.append("line").attr("x1",l).attr("y1",i).attr("x2",l).attr("y2",d),t.append("line").attr("x1",f+e/2).attr("y1",d).attr("x2",p+e/2).attr("y2",d),t.append("rect").attr("x",f).attr("y",h).attr("width",e).attr("height",a).attr("rx",1).attr("ry",1),t.append("rect").attr("x",p).attr("y",h).attr("width",e).attr("height",a).attr("rx",1).attr("ry",1)}u.append("rect").attr("width",o).attr("height",i).attr("rx",6).attr("ry",6);const l=28,h=(i-l)/2,f=`fng-clip-${n.id}-${Math.random().toString(36).slice(2,6)}`;if(u.append("clipPath").attr("id",f).append("circle").attr("cx",20).attr("cy",h+14).attr("r",13),s.avatar)u.append("image").attr("href",s.avatar).attr("x",6).attr("y",h).attr("width",l).attr("height",l).attr("preserveAspectRatio","xMidYMid slice").attr("clip-path",`url(#${f})`);else{u.append("circle").attr("cx",20).attr("cy",h+14).attr("r",13).attr("class","photo-placeholder");const t=20,n=h+14;u.append("circle").attr("cx",t).attr("cy",n-3).attr("r",5).attr("class","silhouette"),u.append("ellipse").attr("cx",t).attr("cy",n+8).attr("rx",7).attr("ry",5).attr("class","silhouette")}const p=function(t,n,e){const r=t&&!t.match(/^@[A-Z]\.N\.$/)?t:"",s=n&&!n.match(/^@[A-Z]\.N\.$/)?n:"";if(!r&&!s){return(e?e.replace(/@[A-Z]\.N\./g,"…").trim():"")||"???"}const o=r?r.split(/\s+/)[0]:"";if(o&&s)return`${o} ${s}`;return o||s||"???"}(s["first name"]||"",s["last name"]||"",s.fullName),d=o-41-5;u.append("text").attr("class","person-name").attr("x",41).attr("y",i/2-4).text(function(t,n,e){const r=Math.floor(n/e);return!t||t.length<=r?t||"":t.substring(0,r-1)+"…"}(p,d,5.5));const g=(y=s.birthYear,m=s.deathYear,_=s.isDead,y||m?y&&m?`${y}–${m}`:y&&_?`${y}–?`:y?`* ${y}`:`† ${m}`:"");var y,m,_;return g&&u.append("text").attr("class","person-dates").attr("x",41).attr("y",i/2+9).text(g),u}var o="http://www.w3.org/1999/xhtml",i={svg:"http://www.w3.org/2000/svg",xhtml:o,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function a(t){var n=t+="",e=n.indexOf(":");return e>=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),i.hasOwnProperty(n)?{space:i[n],local:t}:t}function c(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===o&&n.documentElement.namespaceURI===o?n.createElement(t):n.createElementNS(e,t)}}function u(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function l(t){var n=a(t);return(n.local?u:c)(n)}function h(){}function f(t){return null==t?h:function(){return this.querySelector(t)}}function p(){return[]}function d(t){return function(){return null==(n=t.apply(this,arguments))?[]:Array.isArray(n)?n:Array.from(n);var n}}function g(t){return function(n){return n.matches(t)}}var y=Array.prototype.find;function m(){return this.firstElementChild}var _=Array.prototype.filter;function x(){return Array.from(this.children)}function v(t){return new Array(t.length)}function $(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function w(t,n,e,r,s,o){for(var i,a=0,c=n.length,u=o.length;an?1:t>=n?0:NaN}function S(t){return function(){this.removeAttribute(t)}}function L(t){return function(){this.removeAttributeNS(t.space,t.local)}}function b(t,n){return function(){this.setAttribute(t,n)}}function N(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function E(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function B(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function P(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function D(t){return function(){this.style.removeProperty(t)}}function H(t,n,e){return function(){this.style.setProperty(t,n,e)}}function Y(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function q(t){return function(){delete this[t]}}function O(t,n){return function(){this[t]=n}}function R(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function W(t){return t.trim().split(/^|\s+/)}function I(t){return t.classList||new j(t)}function j(t){this._node=t,this._names=W(t.getAttribute("class")||"")}function z(t,n){for(var e=I(t),r=-1,s=n.length;++r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var ht=[null];function ft(t,n){this._groups=t,this._parents=n}ft.prototype={constructor:ft,select:function(t){"function"!=typeof t&&(t=f(t));for(var n=this._groups,e=n.length,r=new Array(e),s=0;s=$&&($=v+1);!(x=m[$])&&++$=0;)(r=s[o])&&(i&&4^r.compareDocumentPosition(i)&&i.parentNode.insertBefore(r,i),i=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=C);for(var e=this._groups,r=e.length,s=new Array(r),o=0;o1?this.each((null==n?D:"function"==typeof n?Y:H)(t,n,null==e?"":e)):function(t,n){return t.style.getPropertyValue(n)||P(t).getComputedStyle(t,null).getPropertyValue(n)}(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?q:"function"==typeof n?R:O)(t,n)):this.node()[t]},classed:function(t,n){var e=W(t+"");if(arguments.length<2){for(var r=I(this.node()),s=-1,o=e.length;++s=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}(t+""),i=o.length;if(!(arguments.length<2)){for(a=n?at:it,r=0;r{t.data.url&&(window.location.href=t.data.url)},l=c.append("g").attr("class","edges");for(const t of r.connections)l.append("path").attr("class",t.cssClass).attr("d",t.path);for(const t of r.persons)s(c,t,this.config,u)}computeBounds(t,n){const e=this.config.cardWidth/2,r=this.config.cardHeight/2;let s=1/0,o=-1/0,i=1/0,a=-1/0;for(const n of t.persons){s=Math.min(s,n.x-e),o=Math.max(o,n.x+e);const t=n.data.hasMoreAncestors?14:0,c=n.data.hasMoreDescendants?14:0;i=Math.min(i,n.y-r-t),a=Math.max(a,n.y+r+c)}for(const n of t.connections){const t=n.path.match(/-?[\d.]+/g);if(t)for(let n=0;n0;){const n=r.shift(),o=e.get(n),i=t.get(n);if(i){for(const n of i.rels.spouses||[])!s.has(n)&&t.has(n)&&(e.set(n,o),s.add(n),r.push(n));for(const n of i.rels.parents||[])!s.has(n)&&t.has(n)&&(e.set(n,o-1),s.add(n),r.push(n));for(const n of i.rels.children||[])!s.has(n)&&t.has(n)&&(e.set(n,o+1),s.add(n),r.push(n))}}return e}(i,s),f=function(t){const n=new Map;for(const[e,r]of t){const s=(r.rels.parents||[]).filter(n=>t.has(n));if(0===s.length)continue;const o=[...s].sort().join("|");n.has(o)||n.set(o,{parents:[...s].sort(),children:[]}),n.get(o).children.push(e)}return[...n.values()]}(i),p=f.filter(t=>t.parents.includes(s)&&t.children.length>0).slice(0,2),d=i.get(s),g=new Set(d&&d.rels.spouses||[]);if(g.size>0)for(const[t,n]of h)0!==n||t===s||g.has(t)||h.set(t,-.5);const y=new Set;for(const t of p)for(const n of t.parents)n!==s&&y.add(n);if(p.length>=1&&g.size>=2)for(const t of y)h.has(t)&&h.set(t,.5);const m=new Map;for(const[t,n]of h)m.has(n)||m.set(n,[]),m.get(n).push(t);const _=new Map,x=[...m.keys()].sort((t,n)=>t-n);let $=0;for(const t of x){const r=n(m.get(t),t,s,f,i,p);if(.5===t&&1===p.length){const t=-(2*a+l)/2+a/2+a+l;for(const n of r)_.set(n,{x:t,y:$});$+=c+.6*u;continue}if(t>=1&&p.length>=2){const t=r.filter(t=>p[0].children.includes(t)),n=r.filter(t=>p[1].children.includes(t)),s=e(t,1),o=e(n,1),i=Math.max(s.length,o.length),h=-(2*a+l)/2+a/2,f=h+a+l;for(let t=0;t0?r.length:2);for(let n=0;n=2,p=o.length>=1&&h.length>=2,d=n.get(s),g=12;for(const e of t){if(o.indexOf(e)>=0&&p)continue;const t=e.parents.map(t=>n.get(t)).filter(Boolean),s=e.children.map(t=>({id:t,...n.get(t)})).filter(t=>void 0!==t.x);if(0===t.length||0===s.length)continue;const i=Math.max(...t.map(t=>t.y))+c,l=i+.3*(Math.min(...s.map(t=>t.y))-c-i),u=t.reduce((t,n)=>t+n.x,0)/t.length;if(t.length>=2){const n=t.map(t=>t.x).sort((t,n)=>t-n);a.push({path:`M ${n[0]} ${l} L ${n[n.length-1]} ${l}`,cssClass:"link couple-link"})}for(const n of t)a.push({path:`M ${n.x} ${n.y+c} L ${n.x} ${l}`,cssClass:"link ancestor-link"});r(a,s,u,l,c)}if(f&&d)for(let t=0;tt!==s),u=i?n.get(i):null,h=e.children.map(t=>({id:t,...n.get(t)})).filter(t=>void 0!==t.x);if(!u||0===h.length)continue;const f=u.y+c,p=Math.min(...h.map(t=>t.y))-c-f,y=1===h.length;let m;m=0===t?Math.min(...h.map(t=>t.x))-l/2-g:Math.max(...h.map(t=>t.x))+l/2+g;const _=f+.3*p,x=(d.y+c+u.y-c)/2;if(a.push({path:`M ${d.x} ${d.y+c} L ${d.x} ${x}`,cssClass:"link couple-link"}),a.push({path:`M ${d.x} ${x} L ${m} ${x}`,cssClass:"link couple-link"}),a.push({path:`M ${m} ${x} L ${m} ${_}`,cssClass:"link couple-link"}),a.push({path:`M ${u.x} ${f} L ${u.x} ${_}`,cssClass:"link couple-link"}),a.push({path:`M ${m} ${_} L ${u.x} ${_}`,cssClass:"link couple-link"}),y)a.push({path:`M ${u.x} ${_} L ${u.x} ${h[0].y-c}`,cssClass:"link descendant-link"});else{const t=Math.min(...h.map(t=>t.y))-c-6,n=(m+u.x)/2;a.push({path:`M ${n} ${_} L ${n} ${t}`,cssClass:"link descendant-link"}),r(a,h,m,t,c)}}if(!f&&p&&d){const t=o[0],e=t.parents.find(t=>t!==s),i=e?n.get(e):null,l=t.children.map(t=>({id:t,...n.get(t)})).filter(t=>void 0!==t.x);if(i&&l.length>0){const t=i.y+c,n=t+.3*(Math.min(...l.map(t=>t.y))-c-t);a.push({path:`M ${d.x} ${d.y+c} L ${d.x} ${n}`,cssClass:"link couple-link"}),a.push({path:`M ${d.x} ${n} L ${i.x} ${n}`,cssClass:"link couple-link"}),a.push({path:`M ${i.x} ${t} L ${i.x} ${n}`,cssClass:"link couple-link"});r(a,l,(d.x+i.x)/2,n,c)}}const y=new Set;for(const t of o)for(const n of t.parents)n!==s&&y.add(n);if(u&&d&&p)for(const t of u.rels.spouses||[]){if(y.has(t))continue;const e=n.get(t);if(!e)continue;const r=d.y+c+8;a.push({path:`M ${d.x} ${d.y+c} L ${d.x} ${r}`,cssClass:"link couple-link"}),a.push({path:`M ${d.x} ${r} L ${e.x} ${r}`,cssClass:"link couple-link"}),a.push({path:`M ${e.x} ${r} L ${e.x} ${e.y+c}`,cssClass:"link couple-link"})}return a}(f,_,o,s,p,i);return{persons:w,connections:M}}function n(t,n,e,r,s,o){return-.5===n?function(t,n,e){const r=[],s=new Set;for(const o of n){const n=o.children.filter(n=>t.includes(n)&&!s.has(n));n.sort((t,n)=>(e.get(t)?.data?.birthYear||9999)-(e.get(n)?.data?.birthYear||9999));for(const t of n)r.push(t),s.add(t)}for(const n of t)s.has(n)||r.push(n);return r}(t,r,s):n<0?function(t,n){const e=n.filter(n=>n.parents.some(n=>t.includes(n)));if(0===e.length)return[...t];const r=[],s=new Set,o=[...e],i=o.shift();for(const n of i.parents)t.includes(n)&&!s.has(n)&&(r.push(n),s.add(n));for(;o.length>0;){const n=o.findIndex(t=>t.parents.some(t=>s.has(t))),e=n>=0?o.splice(n,1)[0]:o.shift();for(const n of e.parents)t.includes(n)&&!s.has(n)&&(r.push(n),s.add(n))}for(const n of t)s.has(n)||r.push(n);return r}(t,r):0===n?function(t,n,e,r){const s=r.get(n),o=[],i=new Set;o.push(n),i.add(n);for(const n of s.rels.spouses||[])t.includes(n)&&!i.has(n)&&(o.push(n),i.add(n));for(const n of t)i.has(n)||o.push(n);return o}(t,e,0,s):.5===n?function(t,n){const e=[],r=new Set;for(const s of n)for(const n of s.parents)t.includes(n)&&!r.has(n)&&(e.push(n),r.add(n));for(const n of t)r.has(n)||e.push(n);return e}(t,o):function(t,n,e,r){const s=[],o=new Set;for(const n of r)for(const e of n.children)t.includes(e)&&!o.has(e)&&(s.push(e),o.add(e));const i=e.filter(t=>t.parents.includes(n));for(const n of i)if(!r.includes(n))for(const e of n.children)t.includes(e)&&!o.has(e)&&(s.push(e),o.add(e));for(const n of t)o.has(n)||s.push(n);return s}(t,e,r,o)}function e(t,n){const e=[];for(let r=0;rt-n);if(a.length>1){const n=a.map(t=>t-s-6);r>n[0]&&(n[0]=r);const o=n[n.length-1];t.push({path:`M ${e} ${r} L ${e} ${o}`,cssClass:"link descendant-link"});for(let r=0;rt.x).sort((t,n)=>t-n),u=Math.min(l[0],e),h=Math.max(l[l.length-1],e);t.push({path:`M ${u} ${c} L ${h} ${c}`,cssClass:"link descendant-link"});for(const n of o)t.push({path:`M ${n.x} ${c} L ${n.x} ${n.y-s}`,cssClass:"link descendant-link"})}}else{const o=a[0]-s-6,i=Math.max(o,r);if(r1&&t.push({path:`M ${e} ${i} L ${n[0].x} ${i}`,cssClass:"link descendant-link"}),t.push({path:`M ${n[0].x} ${i} L ${n[0].x} ${n[0].y-s}`,cssClass:"link descendant-link"});else{const r=n.map(t=>t.x).sort((t,n)=>t-n),o=Math.min(r[0],e),a=Math.max(r[r.length-1],e);t.push({path:`M ${o} ${i} L ${a} ${i}`,cssClass:"link descendant-link"});for(const e of n)t.push({path:`M ${e.x} ${i} L ${e.x} ${e.y-s}`,cssClass:"link descendant-link"})}}}function s(t,n,e,r){const s=n.data,o=e.cardWidth,i=e.cardHeight,a=`sex-${(s.gender||"u").toLowerCase()}`,c=n.isMain?"is-root":"",l=t.append("g").attr("class",`person-card ${a} ${c}`.trim()).attr("transform",`translate(${n.x-o/2}, ${n.y-i/2})`).style("cursor","pointer").on("click",t=>{t.stopPropagation(),r({id:n.id,data:s})});if(s.hasMoreAncestors){const t=l.append("g").attr("class","more-ancestors-indicator").style("cursor","pointer").on("click",t=>{t.stopPropagation(),r({id:n.id,data:s})}),e=10,i=7,a=4,c=o-25,u=-14,h=c-a/2-e,f=c+a/2,p=u+i;t.append("line").attr("x1",h+e/2).attr("y1",p).attr("x2",f+e/2).attr("y2",p),t.append("line").attr("x1",c).attr("y1",p).attr("x2",c).attr("y2",0),t.append("rect").attr("x",h).attr("y",u).attr("width",e).attr("height",i).attr("rx",1).attr("ry",1),t.append("rect").attr("x",f).attr("y",u).attr("width",e).attr("height",i).attr("rx",1).attr("ry",1)}if(s.hasMoreDescendants){const t=l.append("g").attr("class","more-descendants-indicator").style("cursor","pointer").on("click",t=>{t.stopPropagation(),r({id:n.id,data:s})}),e=10,a=7,c=4,u=o-25,h=i+7,f=u-c/2-e,p=u+c/2,d=h;t.append("line").attr("x1",u).attr("y1",i).attr("x2",u).attr("y2",d),t.append("line").attr("x1",f+e/2).attr("y1",d).attr("x2",p+e/2).attr("y2",d),t.append("rect").attr("x",f).attr("y",h).attr("width",e).attr("height",a).attr("rx",1).attr("ry",1),t.append("rect").attr("x",p).attr("y",h).attr("width",e).attr("height",a).attr("rx",1).attr("ry",1)}l.append("rect").attr("width",o).attr("height",i).attr("rx",6).attr("ry",6);const u=28,h=(i-u)/2,f=`fng-clip-${n.id}-${Math.random().toString(36).slice(2,6)}`;if(l.append("clipPath").attr("id",f).append("circle").attr("cx",20).attr("cy",h+14).attr("r",13),s.avatar)l.append("image").attr("href",s.avatar).attr("x",6).attr("y",h).attr("width",u).attr("height",u).attr("preserveAspectRatio","xMidYMid slice").attr("clip-path",`url(#${f})`);else{l.append("circle").attr("cx",20).attr("cy",h+14).attr("r",13).attr("class","photo-placeholder");const t=20,n=h+14;l.append("circle").attr("cx",t).attr("cy",n-3).attr("r",5).attr("class","silhouette"),l.append("ellipse").attr("cx",t).attr("cy",n+8).attr("rx",7).attr("ry",5).attr("class","silhouette")}const p=function(t,n,e){const r=t&&!t.match(/^@[A-Z]\.N\.$/)?t:"",s=n&&!n.match(/^@[A-Z]\.N\.$/)?n:"";if(!r&&!s){return(e?e.replace(/@[A-Z]\.N\./g,"…").trim():"")||"???"}const o=r?r.split(/\s+/)[0]:"";if(o&&s)return`${o} ${s}`;return o||s||"???"}(s["first name"]||"",s["last name"]||"",s.fullName),d=o-41;l.append("text").attr("class","person-name").attr("x",41).attr("y",i/2-4).text(function(t,n,e){const r=Math.floor(n/e);return!t||t.length<=r?t||"":t.substring(0,r-1)+"…"}(p,d,4.5));const g=(y=s.birthYear,m=s.deathYear,_=s.isDead,y||m?y&&m?`${y}–${m}`:y&&_?`${y}–?`:y?`* ${y}`:`† ${m}`:"");var y,m,_;return g&&l.append("text").attr("class","person-dates").attr("x",41).attr("y",i/2+9).text(g),l}var o="http://www.w3.org/1999/xhtml",i={svg:"http://www.w3.org/2000/svg",xhtml:o,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function a(t){var n=t+="",e=n.indexOf(":");return e>=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),i.hasOwnProperty(n)?{space:i[n],local:t}:t}function c(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===o&&n.documentElement.namespaceURI===o?n.createElement(t):n.createElementNS(e,t)}}function l(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function u(t){var n=a(t);return(n.local?l:c)(n)}function h(){}function f(t){return null==t?h:function(){return this.querySelector(t)}}function p(){return[]}function d(t){return function(){return null==(n=t.apply(this,arguments))?[]:Array.isArray(n)?n:Array.from(n);var n}}function g(t){return function(n){return n.matches(t)}}var y=Array.prototype.find;function m(){return this.firstElementChild}var _=Array.prototype.filter;function x(){return Array.from(this.children)}function $(t){return new Array(t.length)}function v(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function w(t,n,e,r,s,o){for(var i,a=0,c=n.length,l=o.length;an?1:t>=n?0:NaN}function S(t){return function(){this.removeAttribute(t)}}function L(t){return function(){this.removeAttributeNS(t.space,t.local)}}function b(t,n){return function(){this.setAttribute(t,n)}}function N(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function E(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function B(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function P(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function D(t){return function(){this.style.removeProperty(t)}}function H(t,n,e){return function(){this.style.setProperty(t,n,e)}}function Y(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function q(t){return function(){delete this[t]}}function O(t,n){return function(){this[t]=n}}function R(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function W(t){return t.trim().split(/^|\s+/)}function z(t){return t.classList||new I(t)}function I(t){this._node=t,this._names=W(t.getAttribute("class")||"")}function j(t,n){for(var e=z(t),r=-1,s=n.length;++r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var ht=[null];function ft(t,n){this._groups=t,this._parents=n}ft.prototype={constructor:ft,select:function(t){"function"!=typeof t&&(t=f(t));for(var n=this._groups,e=n.length,r=new Array(e),s=0;s=v&&(v=$+1);!(x=m[v])&&++v=0;)(r=s[o])&&(i&&4^r.compareDocumentPosition(i)&&i.parentNode.insertBefore(r,i),i=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=C);for(var e=this._groups,r=e.length,s=new Array(r),o=0;o1?this.each((null==n?D:"function"==typeof n?Y:H)(t,n,null==e?"":e)):function(t,n){return t.style.getPropertyValue(n)||P(t).getComputedStyle(t,null).getPropertyValue(n)}(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?q:"function"==typeof n?R:O)(t,n)):this.node()[t]},classed:function(t,n){var e=W(t+"");if(arguments.length<2){for(var r=z(this.node()),s=-1,o=e.length;++s=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}(t+""),i=o.length;if(!(arguments.length<2)){for(a=n?at:it,r=0;r{t.data.url&&(window.location.href=t.data.url)},u=c.append("g").attr("class","edges");for(const t of r.connections)u.append("path").attr("class",t.cssClass).attr("d",t.path);for(const t of r.persons)s(c,t,this.config,l)}computeBounds(t,n){const e=this.config.cardWidth/2,r=this.config.cardHeight/2;let s=1/0,o=-1/0,i=1/0,a=-1/0;for(const n of t.persons){s=Math.min(s,n.x-e),o=Math.max(o,n.x+e);const t=n.data.hasMoreAncestors?14:0,c=n.data.hasMoreDescendants?14:0;i=Math.min(i,n.y-r-t),a=Math.max(a,n.y+r+c)}for(const n of t.connections){const t=n.path.match(/-?[\d.]+/g);if(t)for(let n=0;n= 2) { + if (activeRootFamilies.length >= 1 && rootSpouses.size >= 2) { for (const sid of activeSpouseIds) { if (gen.has(sid)) gen.set(sid, 0.5); } @@ -80,6 +80,17 @@ export function computeLayout(persons, mainId, config) { const ids = genGroups.get(g); const ordered = orderGeneration(ids, g, mainId, families, personById, activeRootFamilies); + // Single active spouse staggered: position at right column (same x as S1) + if (g === 0.5 && activeRootFamilies.length === 1) { + const rowWidth = 2 * cw + hGap; + const rightX = -rowWidth / 2 + cw / 2 + cw + hGap; + for (const id of ordered) { + positions.set(id, { x: rightX, y: currentY }); + } + currentY += ch + vGap * 0.6; + continue; + } + // Multi-spouse children: position each family's children as // two side-by-side columns (1 card wide each) to stay within 2-card width if (g >= 1 && activeRootFamilies.length >= 2) { @@ -433,7 +444,10 @@ function buildConnectors(families, positions, config, mainId, activeRootFamilies const halfH = config.cardHeight / 2; const cw = config.cardWidth; const vGap = config.verticalSpacing; + const rootPerson = personById.get(mainId); + const rootSpouseIds = rootPerson?.rels?.spouses || []; const multiSpouse = activeRootFamilies.length >= 2; + const staggeredLayout = activeRootFamilies.length >= 1 && rootSpouseIds.length >= 2; const rootPos = positions.get(mainId); const margin = 12; @@ -441,8 +455,8 @@ function buildConnectors(families, positions, config, mainId, activeRootFamilies const rootFamIdx = activeRootFamilies.indexOf(family); const isActiveRootFamily = rootFamIdx >= 0; - // Multi-spouse active families get custom routing below - if (isActiveRootFamily && multiSpouse) continue; + // Staggered/multi-spouse active families get custom routing below + if (isActiveRootFamily && staggeredLayout) continue; const parentPos = family.parents .map((pid) => positions.get(pid)) @@ -559,15 +573,50 @@ function buildConnectors(families, positions, config, mainId, activeRootFamilies } } + // ── Single active spouse staggered: root → S2 → children ── + if (!multiSpouse && staggeredLayout && rootPos) { + const family = activeRootFamilies[0]; + const spouseId = family.parents.find((p) => p !== mainId); + const spousePos = spouseId ? positions.get(spouseId) : null; + const childPos = family.children + .map((cid) => ({ id: cid, ...positions.get(cid) })) + .filter((c) => c.x !== undefined); + + if (spousePos && childPos.length > 0) { + const spouseBottomY = spousePos.y + halfH; + const childTopY = Math.min(...childPos.map((c) => c.y)) - halfH; + const gap = childTopY - spouseBottomY; + const coupleBarY = spouseBottomY + gap * 0.3; + + // Root vertical down to couple bar + connections.push({ + path: `M ${rootPos.x} ${rootPos.y + halfH} L ${rootPos.x} ${coupleBarY}`, + cssClass: "link couple-link", + }); + // Horizontal couple bar from root to spouse + connections.push({ + path: `M ${rootPos.x} ${coupleBarY} L ${spousePos.x} ${coupleBarY}`, + cssClass: "link couple-link", + }); + // Spouse vertical down to couple bar + connections.push({ + path: `M ${spousePos.x} ${spouseBottomY} L ${spousePos.x} ${coupleBarY}`, + cssClass: "link couple-link", + }); + // Children from couple bar, spine centered between root and spouse + const spineX = (rootPos.x + spousePos.x) / 2; + drawChildConnectors(connections, childPos, spineX, coupleBarY, halfH, vGap); + } + } + // ── Couple links for childless spouses (same row as root) ── - const rootPerson = personById.get(mainId); const childlessActiveIds = new Set(); for (const f of activeRootFamilies) { for (const pid of f.parents) { if (pid !== mainId) childlessActiveIds.add(pid); } } - if (rootPerson && rootPos && multiSpouse) { + if (rootPerson && rootPos && staggeredLayout) { for (const sid of rootPerson.rels.spouses || []) { if (childlessActiveIds.has(sid)) continue; const spPos = positions.get(sid);