diff --git a/package.json b/package.json index 6343eb1..589473b 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,14 @@ }, "description": "", "scripts": { - "prepare": "rollup -c" + "prepare": "rollup --config rollup.config.js", + "watch": "rollup --watch --config rollup.config.js" }, "devDependencies": { "@rollup/plugin-node-resolve": "^15.0", "d3-fetch": "^3.0", "d3-hierarchy": "^3.0", + "d3-path": "^3.0", "d3-scale": "^4.0", "d3-selection": "^3.0", "d3-shape": "^3.0", diff --git a/resources/css/svg.css b/resources/css/svg.css index 35d5149..9eff45d 100644 --- a/resources/css/svg.css +++ b/resources/css/svg.css @@ -36,6 +36,11 @@ fill: var(--sex-u-bg, rgb(255, 255, 255)); } +#webtrees-descendants-chart-container svg .spouse rect { + stroke-dasharray: 5; + stroke-width: 1px; +} + #webtrees-descendants-chart-container svg rect.image { stroke: rgb(175, 175, 175); } @@ -44,6 +49,8 @@ fill: none; stroke: rgb(175, 175, 175); stroke-width: 1.5px; + stroke-linecap: butt; + /*shape-rendering: crispEdges;*/ } #webtrees-descendants-chart-container svg g.name text { diff --git a/resources/js/descendants-chart.min.js b/resources/js/descendants-chart.min.js index cffc357..967caa0 100644 --- a/resources/js/descendants-chart.min.js +++ b/resources/js/descendants-chart.min.js @@ -1 +1 @@ -var t,e;t=this,e=function(t){var e="http://www.w3.org/1999/xhtml",n={svg:"http://www.w3.org/2000/svg",xhtml:e,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function r(t){var e=t+="",r=e.indexOf(":");return r>=0&&"xmlns"!==(e=t.slice(0,r))&&(t=t.slice(r+1)),n.hasOwnProperty(e)?{space:n[e],local:t}:t}function i(t){return function(){var n=this.ownerDocument,r=this.namespaceURI;return r===e&&n.documentElement.namespaceURI===e?n.createElement(t):n.createElementNS(r,t)}}function o(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function a(t){var e=r(t);return(e.local?o:i)(e)}function s(){}function h(t){return null==t?s:function(){return this.querySelector(t)}}function u(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}function l(){return[]}function c(t){return null==t?l:function(){return this.querySelectorAll(t)}}function f(t){return function(){return this.matches(t)}}function d(t){return function(e){return e.matches(t)}}var p=Array.prototype.find;function g(){return this.firstElementChild}var _=Array.prototype.filter;function m(){return Array.from(this.children)}function v(t){return new Array(t.length)}function y(t,e){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=e}function x(t){return function(){return t}}function w(t,e,n,r,i,o){for(var a,s=0,h=e.length,u=o.length;se?1:t>=e?0:NaN}function z(t){return function(){this.removeAttribute(t)}}function M(t){return function(){this.removeAttributeNS(t.space,t.local)}}function E(t,e){return function(){this.setAttribute(t,e)}}function S(t,e){return function(){this.setAttributeNS(t.space,t.local,e)}}function T(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttribute(t):this.setAttribute(t,n)}}function P(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,n)}}function C(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function L(t){return function(){this.style.removeProperty(t)}}function R(t,e,n){return function(){this.style.setProperty(t,e,n)}}function $(t,e,n){return function(){var r=e.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,n)}}function I(t,e){return t.style.getPropertyValue(e)||C(t).getComputedStyle(t,null).getPropertyValue(e)}function B(t){return function(){delete this[t]}}function D(t,e){return function(){this[t]=e}}function H(t,e){return function(){var n=e.apply(this,arguments);null==n?delete this[t]:this[t]=n}}function X(t){return t.trim().split(/^|\s+/)}function W(t){return t.classList||new Y(t)}function Y(t){this._node=t,this._names=X(t.getAttribute("class")||"")}function V(t,e){for(var n=W(t),r=-1,i=e.length;++r=0&&(e=t.slice(n+1),t=t.slice(0,n)),{type:t,name:e}}))}function st(t){return function(){var e=this.__on;if(e){for(var n,r=0,i=-1,o=e.length;r=0&&(this._names.splice(e,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var ft=[null];function dt(t,e){this._groups=t,this._parents=e}function pt(){return new dt([[document.documentElement]],ft)}function gt(t){return"string"==typeof t?new dt([[document.querySelector(t)]],[document.documentElement]):new dt([[t]],ft)}function _t(t,e){if(t=function(t){let e;for(;e=t.sourceEvent;)t=e;return t}(t),void 0===e&&(e=t.currentTarget),e){var n=e.ownerSVGElement||e;if(n.createSVGPoint){var r=n.createSVGPoint();return r.x=t.clientX,r.y=t.clientY,[(r=r.matrixTransform(e.getScreenCTM().inverse())).x,r.y]}if(e.getBoundingClientRect){var i=e.getBoundingClientRect();return[t.clientX-i.left-e.clientLeft,t.clientY-i.top-e.clientTop]}}return[t.pageX,t.pageY]}dt.prototype=pt.prototype={constructor:dt,select:function(t){"function"!=typeof t&&(t=h(t));for(var e=this._groups,n=e.length,r=new Array(n),i=0;i=z&&(z=A+1);!(y=_[z])&&++z=0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function e(e,n){return e&&n?t(e.__data__,n.__data__):!e-!n}t||(t=A);for(var n=this._groups,r=n.length,i=new Array(r),o=0;o1?this.each((null==e?L:"function"==typeof e?$:R)(t,e,null==n?"":n)):I(this.node(),t)},property:function(t,e){return arguments.length>1?this.each((null==e?B:"function"==typeof e?H:D)(t,e)):this.node()[t]},classed:function(t,e){var n=X(t+"");if(arguments.length<2){for(var r=W(this.node()),i=-1,o=n.length;++i{}};function vt(){for(var t,e=0,n=arguments.length,r={};e=0&&(n=t.slice(r+1),t=t.slice(0,r)),t&&!e.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))}function wt(t,e){for(var n,r=0,i=t.length;r0)for(var n,r,i=new Array(n),o=0;o=0&&e._call.call(void 0,t),e=e._next;--At}()}finally{At=0,function(){for(var t,e,n=kt,r=1/0;n;)n._call?(r>n._time&&(r=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:kt=e);Nt=t,Ht(r)}(),St=0}}function Dt(){var t=Pt.now(),e=t-Et;e>1e3&&(Tt-=e,Et=t)}function Ht(t){At||(zt&&(zt=clearTimeout(zt)),t-St>24?(t<1/0&&(zt=setTimeout(Bt,t-Pt.now()-Tt)),Mt&&(Mt=clearInterval(Mt))):(Mt||(Et=Pt.now(),Mt=setInterval(Dt,1e3)),At=1,Ct(Bt)))}function Xt(t,e,n){var r=new $t;return e=null==e?0:+e,r.restart((n=>{r.stop(),t(n+e)}),e,n),r}$t.prototype=It.prototype={constructor:$t,restart:function(t,e,n){if("function"!=typeof t)throw new TypeError("callback is not a function");n=(null==n?Lt():+n)+(null==e?0:+e),this._next||Nt===this||(Nt?Nt._next=this:kt=this,Nt=this),this._call=t,this._time=n,Ht()},stop:function(){this._call&&(this._call=null,this._time=1/0,Ht())}};var Wt=vt("start","end","cancel","interrupt"),Yt=[];function Vt(t,e,n,r,i,o){var a=t.__transition;if(a){if(n in a)return}else t.__transition={};!function(t,e,n){var r,i=t.__transition;function o(t){n.state=1,n.timer.restart(a,n.delay,n.time),n.delay<=t&&a(t-n.delay)}function a(o){var u,l,c,f;if(1!==n.state)return h();for(u in i)if((f=i[u]).name===n.name){if(3===f.state)return Xt(a);4===f.state?(f.state=6,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete i[u]):+u0)throw new Error("too late; already scheduled");return n}function Ot(t,e){var n=Ut(t,e);if(n.state>3)throw new Error("too late; already running");return n}function Ut(t,e){var n=t.__transition;if(!n||!(n=n[e]))throw new Error("transition not found");return n}function jt(t,e){var n,r,i,o=t.__transition,a=!0;if(o){for(i in e=null==e?null:e+"",o)(n=o[i]).name===e?(r=n.state>2&&n.state<5,n.state=6,n.timer.stop(),n.on.call(r?"interrupt":"cancel",t,t.__data__,n.index,n.group),delete o[i]):a=!1;a&&delete t.__transition}}function Gt(t,e,n){t.prototype=e.prototype=n,n.constructor=t}function Ft(t,e){var n=Object.create(t.prototype);for(var r in e)n[r]=e[r];return n}function Kt(){}var Qt=.7,Jt=1/Qt,Zt="\\s*([+-]?\\d+)\\s*",te="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*",ee="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*",ne=/^#([0-9a-f]{3,8})$/,re=new RegExp(`^rgb\\(${Zt},${Zt},${Zt}\\)$`),ie=new RegExp(`^rgb\\(${ee},${ee},${ee}\\)$`),oe=new RegExp(`^rgba\\(${Zt},${Zt},${Zt},${te}\\)$`),ae=new RegExp(`^rgba\\(${ee},${ee},${ee},${te}\\)$`),se=new RegExp(`^hsl\\(${te},${ee},${ee}\\)$`),he=new RegExp(`^hsla\\(${te},${ee},${ee},${te}\\)$`),ue={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};function le(){return this.rgb().formatHex()}function ce(){return this.rgb().formatRgb()}function fe(t){var e,n;return t=(t+"").trim().toLowerCase(),(e=ne.exec(t))?(n=e[1].length,e=parseInt(e[1],16),6===n?de(e):3===n?new me(e>>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?pe(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?pe(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=re.exec(t))?new me(e[1],e[2],e[3],1):(e=ie.exec(t))?new me(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=oe.exec(t))?pe(e[1],e[2],e[3],e[4]):(e=ae.exec(t))?pe(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=se.exec(t))?ke(e[1],e[2]/100,e[3]/100,1):(e=he.exec(t))?ke(e[1],e[2]/100,e[3]/100,e[4]):ue.hasOwnProperty(t)?de(ue[t]):"transparent"===t?new me(NaN,NaN,NaN,0):null}function de(t){return new me(t>>16&255,t>>8&255,255&t,1)}function pe(t,e,n,r){return r<=0&&(t=e=n=NaN),new me(t,e,n,r)}function ge(t){return t instanceof Kt||(t=fe(t)),t?new me((t=t.rgb()).r,t.g,t.b,t.opacity):new me}function _e(t,e,n,r){return 1===arguments.length?ge(t):new me(t,e,n,null==r?1:r)}function me(t,e,n,r){this.r=+t,this.g=+e,this.b=+n,this.opacity=+r}function ve(){return`#${be(this.r)}${be(this.g)}${be(this.b)}`}function ye(){const t=xe(this.opacity);return`${1===t?"rgb(":"rgba("}${we(this.r)}, ${we(this.g)}, ${we(this.b)}${1===t?")":`, ${t})`}`}function xe(t){return isNaN(t)?1:Math.max(0,Math.min(1,t))}function we(t){return Math.max(0,Math.min(255,Math.round(t)||0))}function be(t){return((t=we(t))<16?"0":"")+t.toString(16)}function ke(t,e,n,r){return r<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new Ae(t,e,n,r)}function Ne(t){if(t instanceof Ae)return new Ae(t.h,t.s,t.l,t.opacity);if(t instanceof Kt||(t=fe(t)),!t)return new Ae;if(t instanceof Ae)return t;var e=(t=t.rgb()).r/255,n=t.g/255,r=t.b/255,i=Math.min(e,n,r),o=Math.max(e,n,r),a=NaN,s=o-i,h=(o+i)/2;return s?(a=e===o?(n-r)/s+6*(n0&&h<1?0:a,new Ae(a,s,h,t.opacity)}function Ae(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}function ze(t){return(t=(t||0)%360)<0?t+360:t}function Me(t){return Math.max(0,Math.min(1,t||0))}function Ee(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}Gt(Kt,fe,{copy(t){return Object.assign(new this.constructor,this,t)},displayable(){return this.rgb().displayable()},hex:le,formatHex:le,formatHex8:function(){return this.rgb().formatHex8()},formatHsl:function(){return Ne(this).formatHsl()},formatRgb:ce,toString:ce}),Gt(me,_e,Ft(Kt,{brighter(t){return t=null==t?Jt:Math.pow(Jt,t),new me(this.r*t,this.g*t,this.b*t,this.opacity)},darker(t){return t=null==t?Qt:Math.pow(Qt,t),new me(this.r*t,this.g*t,this.b*t,this.opacity)},rgb(){return this},clamp(){return new me(we(this.r),we(this.g),we(this.b),xe(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:ve,formatHex:ve,formatHex8:function(){return`#${be(this.r)}${be(this.g)}${be(this.b)}${be(255*(isNaN(this.opacity)?1:this.opacity))}`},formatRgb:ye,toString:ye})),Gt(Ae,(function(t,e,n,r){return 1===arguments.length?Ne(t):new Ae(t,e,n,null==r?1:r)}),Ft(Kt,{brighter(t){return t=null==t?Jt:Math.pow(Jt,t),new Ae(this.h,this.s,this.l*t,this.opacity)},darker(t){return t=null==t?Qt:Math.pow(Qt,t),new Ae(this.h,this.s,this.l*t,this.opacity)},rgb(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*e,i=2*n-r;return new me(Ee(t>=240?t-240:t+120,i,r),Ee(t,i,r),Ee(t<120?t+240:t-120,i,r),this.opacity)},clamp(){return new Ae(ze(this.h),Me(this.s),Me(this.l),xe(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const t=xe(this.opacity);return`${1===t?"hsl(":"hsla("}${ze(this.h)}, ${100*Me(this.s)}%, ${100*Me(this.l)}%${1===t?")":`, ${t})`}`}}));var Se=t=>()=>t;function Te(t){return 1==(t=+t)?Pe:function(e,n){return n-e?function(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-t,n=1/n,function(r){return Math.pow(t+r*e,n)}}(e,n,t):Se(isNaN(e)?n:e)}}function Pe(t,e){var n=e-t;return n?function(t,e){return function(n){return t+n*e}}(t,n):Se(isNaN(t)?e:t)}var Ce=function t(e){var n=Te(e);function r(t,e){var r=n((t=_e(t)).r,(e=_e(e)).r),i=n(t.g,e.g),o=n(t.b,e.b),a=Pe(t.opacity,e.opacity);return function(e){return t.r=r(e),t.g=i(e),t.b=o(e),t.opacity=a(e),t+""}}return r.gamma=t,r}(1);function Le(t,e){return t=+t,e=+e,function(n){return t*(1-n)+e*n}}var Re=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,$e=new RegExp(Re.source,"g");function Ie(t,e){var n,r,i,o=Re.lastIndex=$e.lastIndex=0,a=-1,s=[],h=[];for(t+="",e+="";(n=Re.exec(t))&&(r=$e.exec(e));)(i=r.index)>o&&(i=e.slice(o,i),s[a]?s[a]+=i:s[++a]=i),(n=n[0])===(r=r[0])?s[a]?s[a]+=r:s[++a]=r:(s[++a]=null,h.push({i:a,x:Le(n,r)})),o=$e.lastIndex;return o180?e+=360:e-t>180&&(t+=360),o.push({i:n.push(i(n)+"rotate(",null,r)-2,x:Le(t,e)})):e&&n.push(i(n)+"rotate("+e+r)}(o.rotate,a.rotate,s,h),function(t,e,n,o){t!==e?o.push({i:n.push(i(n)+"skewX(",null,r)-2,x:Le(t,e)}):e&&n.push(i(n)+"skewX("+e+r)}(o.skewX,a.skewX,s,h),function(t,e,n,r,o,a){if(t!==n||e!==r){var s=o.push(i(o)+"scale(",null,",",null,")");a.push({i:s-4,x:Le(t,n)},{i:s-2,x:Le(e,r)})}else 1===n&&1===r||o.push(i(o)+"scale("+n+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,s,h),o=a=null,function(t){for(var e,n=-1,r=h.length;++n=0&&(t=t.slice(0,e)),!t||"start"===t}))}(e)?qt:Ot;return function(){var a=o(this,t),s=a.on;s!==r&&(i=(r=s).copy()).on(e,n),a.on=i}}var dn=pt.prototype.constructor;function pn(t){return function(){this.style.removeProperty(t)}}function gn(t,e,n){return function(r){this.style.setProperty(t,e.call(this,r),n)}}function _n(t,e,n){var r,i;function o(){var o=e.apply(this,arguments);return o!==i&&(r=(i=o)&&gn(t,o,n)),r}return o._value=e,o}function mn(t){return function(e){this.textContent=t.call(this,e)}}function vn(t){var e,n;function r(){var r=t.apply(this,arguments);return r!==n&&(e=(n=r)&&mn(r)),e}return r._value=t,r}var yn=0;function xn(t,e,n,r){this._groups=t,this._parents=e,this._name=n,this._id=r}function wn(){return++yn}var bn=pt.prototype;xn.prototype={constructor:xn,select:function(t){var e=this._name,n=this._id;"function"!=typeof t&&(t=h(t));for(var r=this._groups,i=r.length,o=new Array(i),a=0;a()=>t;function Tn(t,{sourceEvent:e,target:n,transform:r,dispatch:i}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:e,enumerable:!0,configurable:!0},target:{value:n,enumerable:!0,configurable:!0},transform:{value:r,enumerable:!0,configurable:!0},_:{value:i}})}function Pn(t,e,n){this.k=t,this.x=e,this.y=n}Pn.prototype={constructor:Pn,scale:function(t){return 1===t?this:new Pn(this.k*t,this.x,this.y)},translate:function(t,e){return 0===t&0===e?this:new Pn(this.k,this.x+this.k*t,this.y+this.k*e)},apply:function(t){return[t[0]*this.k+this.x,t[1]*this.k+this.y]},applyX:function(t){return t*this.k+this.x},applyY:function(t){return t*this.k+this.y},invert:function(t){return[(t[0]-this.x)/this.k,(t[1]-this.y)/this.k]},invertX:function(t){return(t-this.x)/this.k},invertY:function(t){return(t-this.y)/this.k},rescaleX:function(t){return t.copy().domain(t.range().map(this.invertX,this).map(t.invert,t))},rescaleY:function(t){return t.copy().domain(t.range().map(this.invertY,this).map(t.invert,t))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var Cn=new Pn(1,0,0);function Ln(t){for(;!t.__zoom;)if(!(t=t.parentNode))return Cn;return t.__zoom}function Rn(t){t.stopImmediatePropagation()}function $n(t){t.preventDefault(),t.stopImmediatePropagation()}function In(t){return!(t.ctrlKey&&"wheel"!==t.type||t.button)}function Bn(){var t=this;return t instanceof SVGElement?(t=t.ownerSVGElement||t).hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]:[[0,0],[t.clientWidth,t.clientHeight]]}function Dn(){return this.__zoom||Cn}function Hn(t){return-t.deltaY*(1===t.deltaMode?.05:t.deltaMode?1:.002)*(t.ctrlKey?10:1)}function Xn(){return navigator.maxTouchPoints||"ontouchstart"in this}function Wn(t,e,n){var r=t.invertX(e[0][0])-n[0][0],i=t.invertX(e[1][0])-n[1][0],o=t.invertY(e[0][1])-n[0][1],a=t.invertY(e[1][1])-n[1][1];return t.translate(i>r?(r+i)/2:Math.min(0,r)||Math.max(0,i),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}function Yn(t){var e=0,n=t.children,r=n&&n.length;if(r)for(;--r>=0;)e+=n[r].value;else e=1;t.value=e}function Vn(t,e){t instanceof Map?(t=[void 0,t],void 0===e&&(e=On)):void 0===e&&(e=qn);for(var n,r,i,o,a,s=new Gn(t),h=[s];n=h.pop();)if((i=e(n.data))&&(a=(i=Array.from(i)).length))for(n.children=i,o=a-1;o>=0;--o)h.push(r=i[o]=new Gn(i[o])),r.parent=n,r.depth=n.depth+1;return s.eachBefore(jn)}function qn(t){return t.children}function On(t){return Array.isArray(t)?t[1]:null}function Un(t){void 0!==t.data.value&&(t.value=t.data.value),t.data=t.data.data}function jn(t){var e=0;do{t.height=e}while((t=t.parent)&&t.height<++e)}function Gn(t){this.data=t,this.depth=this.height=0,this.parent=null}function Fn(t,e){return t.parent===e.parent?1:2}function Kn(t){var e=t.children;return e?e[0]:t.t}function Qn(t){var e=t.children;return e?e[e.length-1]:t.t}function Jn(t,e,n){var r=n/(e.i-t.i);e.c-=r,e.s+=n,t.c+=r,e.z+=n,e.m+=n}function Zn(t,e,n){return t.a.parent===e.parent?t.a:n}function tr(t,e){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=e}function er(){var t=Fn,e=1,n=1,r=null;function i(i){var h=function(t){for(var e,n,r,i,o,a=new tr(t,0),s=[a];e=s.pop();)if(r=e._.children)for(e.children=new Array(o=r.length),i=o-1;i>=0;--i)s.push(n=e.children[i]=new tr(r[i],i)),n.parent=e;return(a.parent=new tr(null,0)).children=[a],a}(i);if(h.eachAfter(o),h.parent.m=-h.z,h.eachBefore(a),r)i.eachBefore(s);else{var u=i,l=i,c=i;i.eachBefore((function(t){t.xl.x&&(l=t),t.depth>c.depth&&(c=t)}));var f=u===l?1:t(u,l)/2,d=f-u.x,p=e/(l.x+f+d),g=n/(c.depth||1);i.eachBefore((function(t){t.x=(t.x+d)*p,t.y=t.depth*g}))}return i}function o(e){var n=e.children,r=e.parent.children,i=e.i?r[e.i-1]:null;if(n){!function(t){for(var e,n=0,r=0,i=t.children,o=i.length;--o>=0;)(e=i[o]).z+=n,e.m+=n,n+=e.s+(r+=e.c)}(e);var o=(n[0].z+n[n.length-1].z)/2;i?(e.z=i.z+t(e._,i._),e.m=e.z-o):e.z=o}else i&&(e.z=i.z+t(e._,i._));e.parent.A=function(e,n,r){if(n){for(var i,o=e,a=e,s=n,h=o.parent.children[0],u=o.m,l=a.m,c=s.m,f=h.m;s=Qn(s),o=Kn(o),s&&o;)h=Kn(h),(a=Qn(a)).a=e,(i=s.z+c-o.z-u+t(s._,o._))>0&&(Jn(Zn(s,e,r),e,i),u+=i,l+=i),c+=s.m,u+=o.m,f+=h.m,l+=a.m;s&&!Qn(a)&&(a.t=s,a.m+=c-l),o&&!Kn(h)&&(h.t=o,h.m+=u-f,r=e)}return r}(e,i,e.parent.A||r[0])}function a(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function s(t){t.x*=e,t.y=t.depth*n}return i.separation=function(e){return arguments.length?(t=e,i):t},i.size=function(t){return arguments.length?(r=!1,e=+t[0],n=+t[1],i):r?null:[e,n]},i.nodeSize=function(t){return arguments.length?(r=!0,e=+t[0],n=+t[1],i):r?[e,n]:null},i}function nr(t){if(!t.ok)throw new Error(t.status+" "+t.statusText);return t.text()}Ln.prototype=Pn.prototype,Gn.prototype=Vn.prototype={constructor:Gn,count:function(){return this.eachAfter(Yn)},each:function(t,e){let n=-1;for(const r of this)t.call(e,r,++n,this);return this},eachAfter:function(t,e){for(var n,r,i,o=this,a=[o],s=[],h=-1;o=a.pop();)if(s.push(o),n=o.children)for(r=0,i=n.length;r=0;--r)o.push(n[r]);return this},find:function(t,e){let n=-1;for(const r of this)if(t.call(e,r,++n,this))return r},sum:function(t){return this.eachAfter((function(e){for(var n=+t(e.data)||0,r=e.children,i=r&&r.length;--i>=0;)n+=r[i].value;e.value=n}))},sort:function(t){return this.eachBefore((function(e){e.children&&e.children.sort(t)}))},path:function(t){for(var e=this,n=function(t,e){if(t===e)return t;var n=t.ancestors(),r=e.ancestors(),i=null;for(t=n.pop(),e=r.pop();t===e;)i=t,t=n.pop(),e=r.pop();return i}(e,t),r=[e];e!==n;)e=e.parent,r.push(e);for(var i=r.length;t!==n;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,e=[t];t=t.parent;)e.push(t);return e},descendants:function(){return Array.from(this)},leaves:function(){var t=[];return this.eachBefore((function(e){e.children||t.push(e)})),t},links:function(){var t=this,e=[];return t.each((function(n){n!==t&&e.push({source:n.parent,target:n})})),e},copy:function(){return Vn(this).eachBefore(Un)},[Symbol.iterator]:function*(){var t,e,n,r,i=this,o=[i];do{for(t=o.reverse(),o=[];i=t.pop();)if(yield i,e=i.children)for(n=0,r=e.length;nt.children));const n=er().nodeSize([this._configuration.orientation.nodeWidth,0]).separation((()=>.5));this._root=e,this._nodes=n(e)}get nodes(){return this._nodes}get root(){return this._root}createEmptyNode(t,e){return{id:0,xref:"",url:"",updateUrl:"",generation:t,name:"",firstNames:[],lastNames:[],preferredName:"",alternativeNames:[],isAltRtl:!1,sex:e,timespan:""}}}class pr{constructor(t,e){this._orientation=t,this._cornerRadius=e,this._imagePadding=5,this._imageRadius=Math.min(40,t.boxHeight/2-this._imagePadding),this._x=this.calculateX(),this._y=this.calculateY(),this._width=this.calculateImageWidth(),this._height=this.calculateImageHeight(),this._rx=this.calculateCornerRadius(),this._ry=this.calculateCornerRadius()}calculateX(){return-this._orientation.boxWidth/2+this._imagePadding}calculateY(){return this._orientation instanceof ur||this._orientation instanceof lr?-this._imageRadius:-this._orientation.boxHeight/2+this._imagePadding}calculateImageWidth(){return this._orientation instanceof ur||this._orientation instanceof lr?2*this._imageRadius:this._orientation.boxWidth-2*this._imagePadding}calculateImageHeight(){return 2*this._imageRadius}calculateCornerRadius(){return this._cornerRadius-this._imagePadding}get imagePadding(){return this._imagePadding}get imageRadius(){return this._imageRadius}set imageRadius(t){this._imageRadius=t}get x(){return this._x}get y(){return this._y}get rx(){return this._rx}get ry(){return this._ry}get width(){return this._width}get height(){return this._height}}class gr{constructor(t,e=null){this._orientation=t,this._image=e,this._textPadding=15,this._x=this.calculateX(),this._y=this.calculateY(),this._width=this.calculateWidth()}calculateX(){const t=-this._orientation.boxWidth/2+this._textPadding;return this._image?t+this._image.width:t}calculateY(){return this._orientation instanceof ur||this._orientation instanceof lr?-this._textPadding:this._image?this._image.y+this._image.height+2*this._textPadding:-this._orientation.boxHeight/2+2*this._textPadding}calculateWidth(){const t=this._orientation.boxWidth-2*this._textPadding;return this._image&&(this._orientation instanceof ur||this._orientation instanceof lr)?t-this._image.width:t}get x(){return this._x}get y(){return this._y}get width(){return this._width}}class _r{constructor(t){this._cornerRadius=20,this._showImage=!0,this._orientation=t,this._x=-t.boxWidth/2,this._y=-t.boxHeight/2,this._rx=this._cornerRadius,this._ry=this._cornerRadius,this._width=t.boxWidth,this._height=t.boxHeight,this._image=new pr(t,this._cornerRadius)}get showImage(){return this._showImage}set showImage(t){this._showImage=t}get x(){return this._x}get y(){return this._y}get rx(){return this._rx}get ry(){return this._ry}get width(){return this._width}get height(){return this._height}get image(){return this._image}get text(){return new gr(this._orientation,this._showImage?this._image:null)}}class mr{constructor(t,e,n){this._svg=t,this._configuration=e,this._hierarchy=n,this._hierarchy.root.x0=0,this._hierarchy.root.y0=0,this._orientation=this._configuration.orientation,this._box=new _r(this._orientation),this.draw(this._hierarchy.root)}draw(t){let e=this._hierarchy.nodes.descendants(),n=this._hierarchy.nodes.links();e.forEach((t=>{this._orientation.norm(t)})),this.drawLinks(n,t),this.drawNodes(e,t),e.forEach((t=>{t.x0=t.x,t.y0=t.y}))}drawNodes(t,e){let n=0,r=this;this._svg.defs.get().append("clipPath").attr("id","clip-image").append("rect").attr("rx",this._box.image.rx).attr("ry",this._box.image.ry).attr("x",this._box.image.x).attr("y",this._box.image.y).attr("width",this._box.image.width).attr("height",this._box.image.height);let i=this._svg.visual.selectAll("g.person").data(t,(t=>t.id||(t.id=++n))).enter().append("g").attr("class","person").attr("transform",(t=>"translate("+t.x+","+t.y+")"));i.append("rect").attr("class",(t=>"F"===t.data.sex?"female":"M"===t.data.sex?"male":"unknown")).attr("rx",this._box.rx).attr("ry",this._box.ry).attr("x",this._box.x).attr("y",this._box.y).attr("width",this._box.width).attr("height",this._box.height).attr("fill-opacity",.5),i.filter((t=>""!==t.data.xref)).each((function(t){let e=gt(this);e.append("title").text((t=>t.data.name));const n=r.getImageToLoad(t);if(r._box.showImage=!!n,r._box.showImage){let t=e.append("g").attr("class","image");t.append("rect").attr("rx",r._box.image.rx).attr("ry",r._box.image.ry).attr("x",r._box.image.x).attr("y",r._box.image.y).attr("width",r._box.image.width).attr("height",r._box.image.height).attr("fill","rgb(255, 255, 255)");let i=t.append("image").attr("x",r._box.image.x).attr("y",r._box.image.y).attr("width",r._box.image.width).attr("height",r._box.image.height).attr("clip-path","url(#clip-image)");(function(t,e=null){return fetch(t,e).then((t=>t.blob())).then((t=>new Promise(((e,n)=>{const r=new FileReader;r.onloadend=()=>e(r.result),r.onerror=n,r.readAsDataURL(t)}))))})(n).then((t=>i.attr("xlink:href",t))).catch((t=>{console.error(t)})),t.append("rect").attr("rx",r._box.image.rx).attr("ry",r._box.image.ry).attr("x",r._box.image.x).attr("y",r._box.image.y).attr("width",r._box.image.width).attr("height",r._box.image.height).attr("fill","none").attr("stroke","rgb(200, 200, 200)").attr("stroke-width",1.5)}r.addNames(e,t),r.addDates(e,t),r._box.showImage=!0}))}togglePerson(t,e){e.children?(e._children=e.children,e.children=null):(e.children=e._children,e._children=null),this.draw(e)}collapse(t){t.children&&(t._children=t.children,t._children.forEach((t=>this.collapse(t))),t.children=null)}addFirstNames(t,e){let n=0;for(let r of e.data.firstNames){let i=t.append("tspan").text(r);r===e.data.preferredName&&i.attr("class","preferred"),0!==n&&i.attr("dx","0.25em"),++n}}addLastNames(t,e,n=0){let r=0;for(let i of e.data.lastNames){let e=t.append("tspan").attr("class","lastName").text(i);0!==r&&e.attr("dx","0.25em"),0!==n&&e.attr("dx",n+"em"),++r}}truncateNames(t){let e=this._box.text.width;t.selectAll("tspan:not(.preferred):not(.lastName)").nodes().reverse().forEach((n=>gt(n).each(this.truncateText(t,e)))),t.selectAll("tspan.preferred").each(this.truncateText(t,e)),t.selectAll("tspan.lastName").each(this.truncateText(t,e))}truncateText(t,e){let n=this;return function(){let r=n.getTextLength(t),i=gt(this),o=i.text().split(/\s+/);for(let a=o.length-1;a>=0;--a)r>e&&(o[a]=o[a].slice(0,1)+".",i.text(o.join(" ")),r=n.getTextLength(t))}}truncateDate(t,e){let n=this;return function(){let r=n.getTextLength(t),i=gt(this),o=i.text();for(;r>e&&o.length>1;)o=o.slice(0,-1).trim(),i.text(o),r=n.getTextLength(t);"."===o[o.length-1]&&i.text(o.slice(0,-1).trim())}}getTextLength(t){let e=0;return t.selectAll("tspan").each((function(){e+=this.getComputedTextLength()})),e}addNames(t,e){let n=t.append("g").attr("class","name");if(this._orientation.splittNames){let t=n.append("text").attr("text-anchor","middle").attr("alignment-baseline","central").attr("dy",this._box.text.y),r=n.append("text").attr("text-anchor","middle").attr("alignment-baseline","central").attr("dy",this._box.text.y+20);this.addFirstNames(t,e),this.addLastNames(r,e),e.data.firstNames.length||e.data.lastNames.length||t.append("tspan").text(e.data.name),this.truncateNames(t),this.truncateNames(r)}else{let t=n.append("text").attr("text-anchor",this._configuration.rtl?"end":"start").attr("dx",this._box.text.x).attr("dy",this._box.text.y);this.addFirstNames(t,e),this.addLastNames(t,e,.25),e.data.firstNames.length||e.data.lastNames.length||t.append("tspan").text(e.data.name),this.truncateNames(t)}}addDates(t,e){let n=t.append("g").attr("class","table");if(this._orientation.splittNames){let t=n.append("text").attr("class","date").attr("text-anchor","middle").attr("alignment-baseline","central").attr("dy",this._box.text.y+50);t.append("title").text(e.data.timespan);let r=t.append("tspan").text(e.data.timespan);return void(this.getTextLength(t)>this._box.text.width&&(t.selectAll("tspan").each(this.truncateDate(t,this._box.text.width)),r.text(r.text()+"…")))}let r=20;if(e.data.birth){n.append("text").attr("class","date").attr("text-anchor","middle").attr("dominant-baseline","middle").attr("x",this._box.text.x).attr("dy",this._box.text.y+r).append("tspan").text("★").attr("x",this._box.text.x+5);let t=n.append("text").attr("class","date").attr("text-anchor",this._configuration.rtl?"end":"start").attr("dominant-baseline","middle").attr("x",this._box.text.x).attr("dy",this._box.text.y+r);t.append("title").text(e.data.birth);let i=t.append("tspan").text(e.data.birth).attr("x",this._box.text.x+15);this.getTextLength(t)>this._box.text.width-25&&(t.selectAll("tspan").each(this.truncateDate(t,this._box.text.width-25)),i.text(i.text()+"…"))}if(e.data.death){e.data.birth&&(r+=20),n.append("text").attr("class","date").attr("text-anchor","middle").attr("dominant-baseline","middle").attr("x",this._box.text.x).attr("dy",this._box.text.y+r).append("tspan").text("†").attr("x",this._box.text.x+5);let t=n.append("text").attr("class","date").attr("text-anchor",this._configuration.rtl?"end":"start").attr("dominant-baseline","middle").attr("x",this._box.text.x).attr("dy",this._box.text.y+r);t.append("title").text(e.data.death);let i=t.append("tspan").text(e.data.death).attr("x",this._box.text.x+15);this.getTextLength(t)>this._box.text.width-25&&(t.selectAll("tspan").each(this.truncateDate(t,this._box.text.width-25)),i.text(i.text().trim()+"…"))}}getImageToLoad(t){return t.data.thumbnail?t.data.thumbnail:""}drawLinks(t,e){this._svg.visual.selectAll("path.link").data(t,(t=>t.target.id)).enter().append("path").classed("link",!0).attr("d",(t=>this._orientation.elbow(t)))}}class vr{constructor(t){this._element=t.append("div").attr("class","overlay").style("opacity",1e-6)}show(t,e=0,n=null){this._element.select("p").remove(),this._element.append("p").attr("class","tooltip").text(t),this._element.transition().duration(e).style("opacity",1).on("end",(()=>{"function"==typeof n&&n()}))}hide(t=0,e=0){this._element.transition().delay(t).duration(e).style("opacity",1e-6)}get(){return this._element}}class yr{constructor(t){this._element=t.append("defs")}get(){return this._element}}class xr{constructor(t){this._zoom=null,this._parent=t,this.init()}init(){this._zoom=function(){var t,e,n,r=In,i=Bn,o=Wn,a=Hn,s=Xn,h=[0,1/0],u=[[-1/0,-1/0],[1/0,1/0]],l=250,c=Oe,f=vt("start","zoom","end"),d=500,p=0,g=10;function _(t){t.property("__zoom",Dn).on("wheel.zoom",k,{passive:!1}).on("mousedown.zoom",N).on("dblclick.zoom",A).filter(s).on("touchstart.zoom",z).on("touchmove.zoom",M).on("touchend.zoom touchcancel.zoom",E).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function m(t,e){return(e=Math.max(h[0],Math.min(h[1],e)))===t.k?t:new Pn(e,t.x,t.y)}function v(t,e,n){var r=e[0]-n[0]*t.k,i=e[1]-n[1]*t.k;return r===t.x&&i===t.y?t:new Pn(t.k,r,i)}function y(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function x(t,e,n,r){t.on("start.zoom",(function(){w(this,arguments).event(r).start()})).on("interrupt.zoom end.zoom",(function(){w(this,arguments).event(r).end()})).tween("zoom",(function(){var t=this,o=arguments,a=w(t,o).event(r),s=i.apply(t,o),h=null==n?y(s):"function"==typeof n?n.apply(t,o):n,u=Math.max(s[1][0]-s[0][0],s[1][1]-s[0][1]),l=t.__zoom,f="function"==typeof e?e.apply(t,o):e,d=c(l.invert(h).concat(u/l.k),f.invert(h).concat(u/f.k));return function(t){if(1===t)t=f;else{var e=d(t),n=u/e[2];t=new Pn(n,h[0]-e[0]*n,h[1]-e[1]*n)}a.zoom(null,t)}}))}function w(t,e,n){return!n&&t.__zooming||new b(t,e)}function b(t,e){this.that=t,this.args=e,this.active=0,this.sourceEvent=null,this.extent=i.apply(t,e),this.taps=0}function k(t,...e){if(r.apply(this,arguments)){var n=w(this,e).event(t),i=this.__zoom,s=Math.max(h[0],Math.min(h[1],i.k*Math.pow(2,a.apply(this,arguments)))),l=_t(t);if(n.wheel)n.mouse[0][0]===l[0]&&n.mouse[0][1]===l[1]||(n.mouse[1]=i.invert(n.mouse[0]=l)),clearTimeout(n.wheel);else{if(i.k===s)return;n.mouse=[l,i.invert(l)],jt(this),n.start()}$n(t),n.wheel=setTimeout(c,150),n.zoom("mouse",o(v(m(i,s),n.mouse[0],n.mouse[1]),n.extent,u))}function c(){n.wheel=null,n.end()}}function N(t,...e){if(!n&&r.apply(this,arguments)){var i=t.currentTarget,a=w(this,e,!0).event(t),s=gt(t.view).on("mousemove.zoom",f,!0).on("mouseup.zoom",d,!0),h=_t(t,i),l=t.clientX,c=t.clientY;Mn(t.view),Rn(t),a.mouse=[h,this.__zoom.invert(h)],jt(this),a.start()}function f(t){if($n(t),!a.moved){var e=t.clientX-l,n=t.clientY-c;a.moved=e*e+n*n>p}a.event(t).zoom("mouse",o(v(a.that.__zoom,a.mouse[0]=_t(t,i),a.mouse[1]),a.extent,u))}function d(t){s.on("mousemove.zoom mouseup.zoom",null),En(t.view,a.moved),$n(t),a.event(t).end()}}function A(t,...e){if(r.apply(this,arguments)){var n=this.__zoom,a=_t(t.changedTouches?t.changedTouches[0]:t,this),s=n.invert(a),h=n.k*(t.shiftKey?.5:2),c=o(v(m(n,h),a,s),i.apply(this,e),u);$n(t),l>0?gt(this).transition().duration(l).call(x,c,a,t):gt(this).call(_.transform,c,a,t)}}function z(n,...i){if(r.apply(this,arguments)){var o,a,s,h,u=n.touches,l=u.length,c=w(this,i,n.changedTouches.length===l).event(n);for(Rn(n),a=0;a{this._parent.attr("transform",t.transform)})),this._zoom.wheelDelta((t=>-t.deltaY*(1===t.deltaMode?.05:t.deltaMode?1:.002))),this._zoom.filter((t=>{if("wheel"===t.type){if(!t.ctrlKey)return!1;var e=Ln(this);if(e.k){if(e.k<=.1&&t.deltaY>0)return t.preventDefault(),!1;if(e.k>=20&&t.deltaY<0)return t.preventDefault(),!1}return!0}return t.button||"touchstart"!==t.type?!(t.ctrlKey&&"wheel"!==t.type||t.button):2===t.touches.length}))}get(){return this._zoom}}class wr{triggerDownload(t,e){let n=new MouseEvent("click",{view:window,bubbles:!1,cancelable:!0}),r=document.createElement("a");r.setAttribute("download",e),r.setAttribute("href",t),r.setAttribute("target","_blank"),r.dispatchEvent(n)}}class br extends wr{copyStylesInline(t,e){return new Promise((n=>{let r=["svg","g","text","textPath"];for(let n=0;n{let i=(new XMLSerializer).serializeToString(t),o=window.URL||window.webkitURL||window,a=new Blob([i],{type:"image/svg+xml;charset=utf-8"}),s=o.createObjectURL(a),h=new Image;h.onload=()=>{let t=this.createCanvas(e,n),i=t.getContext("2d");i.fillStyle="rgb(255,255,255)",i.fillRect(0,0,t.width,t.height),i.drawImage(h,0,0),o.revokeObjectURL(s);let a=t.toDataURL("image/png").replace("image/png","image/octet-stream");r(a)},h.src=s}))}cloneSvg(t){return new Promise((e=>{e(t.cloneNode(!0))}))}svgToImage(t,e){this.cloneSvg(t.get().node()).then((n=>{this.copyStylesInline(t.get().node(),n);const r=this.calculateViewBox(t.get().node()),i=r[2],o=r[3];n.setAttribute("width",i),n.setAttribute("height",o),n.setAttribute("viewBox",r),this.convertToDataUrl(n,i,o).then((t=>this.triggerDownload(t,e))).catch((()=>{console.log("Failed to save chart as PNG image")}))}))}}class kr extends wr{copyStylesInline(t,e){return new Promise((n=>{(function(t,e){return fetch(t,e).then(nr)})(t).then((t=>{t=t.replace(/#webtrees-descendants-chart-container /g,"");let r=document.createElementNS("http://www.w3.org/2000/svg","style");r.appendChild(document.createTextNode(t)),e.prepend(r),n(e)}))}))}convertToObjectUrl(t){return new Promise((e=>{let n=(new XMLSerializer).serializeToString(t),r=window.URL||window.webkitURL||window,i=new Blob([n],{type:"image/svg+xml;charset=utf-8"}),o=r.createObjectURL(i),a=new Image;a.onload=()=>{e(o)},a.src=o}))}cloneSvg(t){return new Promise((e=>{e(t.cloneNode(!0))}))}svgToImage(t,e,n){this.cloneSvg(t.get().node()).then((t=>this.copyStylesInline(e,t))).then((t=>this.convertToObjectUrl(t))).then((t=>this.triggerDownload(t,n))).catch((()=>{console.log("Failed to save chart as SVG image")}))}}class Nr{constructor(){this._exportClass=null}setExportClass(t){switch(t){case"png":this._exportClass=br;break;case"svg":this._exportClass=kr}}createExport(t){switch(this.setExportClass(t),t){case"png":case"svg":return new this._exportClass}}}class Ar{constructor(t,e){this._element=t.append("svg"),this._defs=new yr(this._element),this._visual=null,this._zoom=null,this._configuration=e,this.init()}init(){this._element.attr("width","100%").attr("height","100%").attr("text-rendering","optimizeLegibility").attr("text-anchor","middle").attr("xmlns:xlink","https://www.w3.org/1999/xlink")}initEvents(t){this._element.on("contextmenu",(t=>t.preventDefault())).on("wheel",(e=>{e.ctrlKey||t.show(this._configuration.labels.zoom,300,(()=>{t.hide(700,800)}))})).on("touchend",(e=>{e.touches.length<2&&t.hide(0,800)})).on("touchmove",(e=>{e.touches.length>=2?t.hide():t.show(this._configuration.labels.move)})).on("click",(t=>this.doStopPropagation(t)),!0),this._configuration.rtl&&this._element.classed("rtl",!0),this._visual=this._element.append("g"),this._zoom=new xr(this._visual),this._element.call(this._zoom.get())}doStopPropagation(t){t.defaultPrevented&&t.stopPropagation()}export(t){return(new Nr).createExport(t)}get defs(){return this._defs}get zoom(){return this._zoom}get visual(){return this._visual}get(){return this._element}}class zr{constructor(t,e){this._configuration=e,this._parent=t,this._hierarchy=new dr(this._configuration),this._data={}}get svg(){return this._svg}updateViewBox(){let t=this._svg.visual.node().getBBox(),e=this._parent.node().getBoundingClientRect(),n=Math.max(e.width,t.width),r=Math.max(e.height,t.height,300),i=(n-t.width)/2,o=(r-t.height)/2,a=Math.ceil(t.x-i-10),s=Math.ceil(t.y-o-10);n=Math.ceil(n+20),r=Math.ceil(r+20),this._svg.get().attr("viewBox",[a,s,n,r])}get data(){return this._data}set data(t){this._data=t,this._hierarchy.init(this._data)}draw(){this._parent.html(""),this._svg=new Ar(this._parent,this._configuration),this._overlay=new vr(this._parent),this._svg.initEvents(this._overlay),new mr(this._svg,this._configuration,this._hierarchy),this.bindClickEventListener(),this.updateViewBox()}bindClickEventListener(){let t=this;this._svg.visual.selectAll("g.person").filter((t=>""!==t.data.xref)).each((function(e){gt(this).on("click",(function(){t.personClick(e.data)}))}))}personClick(t){1===t.generation?this.redirectToIndividual(t.url):this.update(t.updateUrl)}redirectToIndividual(t){window.location=t}update(t){window.location=t}}t.DescendantsChart=class{constructor(t,e){this._selector=t,this._parent=gt(this._selector),this._configuration=new fr(e.labels,e.generations,e.treeLayout,e.rtl),this._chart=new zr(this._parent,this._configuration),this.init()}init(){gt("#centerButton").on("click",(()=>this.center())),gt("#exportPNG").on("click",(()=>this.exportPNG())),gt("#exportSVG").on("click",(()=>this.exportSVG()))}center(){this._chart.svg.get().transition().duration(750).call(this._chart.svg.zoom.get().transform,Cn)}get configuration(){return this._configuration}set cssFile(t){this._cssFile=t}update(t){this._chart.update(t)}draw(t){this._chart.data=t,this._chart.draw()}exportPNG(){this._chart.svg.export("png").svgToImage(this._chart.svg,"descendants-chart.png")}exportSVG(){this._chart.svg.export("svg").svgToImage(this._chart.svg,this._cssFile,"descendants-chart.svg")}},Object.defineProperty(t,"__esModule",{value:!0})},"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).WebtreesDescendantsChart={}); +var t,e;t=this,e=function(t){var e="http://www.w3.org/1999/xhtml",n={svg:"http://www.w3.org/2000/svg",xhtml:e,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function i(t){var e=t+="",i=e.indexOf(":");return i>=0&&"xmlns"!==(e=t.slice(0,i))&&(t=t.slice(i+1)),n.hasOwnProperty(e)?{space:n[e],local:t}:t}function r(t){return function(){var n=this.ownerDocument,i=this.namespaceURI;return i===e&&n.documentElement.namespaceURI===e?n.createElement(t):n.createElementNS(i,t)}}function o(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function a(t){var e=i(t);return(e.local?o:r)(e)}function s(){}function h(t){return null==t?s:function(){return this.querySelector(t)}}function u(){return[]}function l(t){return null==t?u:function(){return this.querySelectorAll(t)}}function c(t){return function(){return null==(e=t.apply(this,arguments))?[]:Array.isArray(e)?e:Array.from(e);var e}}function f(t){return function(){return this.matches(t)}}function d(t){return function(e){return e.matches(t)}}var p=Array.prototype.find;function g(){return this.firstElementChild}var _=Array.prototype.filter;function m(){return Array.from(this.children)}function y(t){return new Array(t.length)}function x(t,e){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=e}function v(t,e,n,i,r,o){for(var a,s=0,h=e.length,u=o.length;se?1:t>=e?0:NaN}function A(t){return function(){this.removeAttribute(t)}}function $(t){return function(){this.removeAttributeNS(t.space,t.local)}}function N(t,e){return function(){this.setAttribute(t,e)}}function T(t,e){return function(){this.setAttributeNS(t.space,t.local,e)}}function z(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttribute(t):this.setAttribute(t,n)}}function E(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,n)}}function S(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function P(t){return function(){this.style.removeProperty(t)}}function C(t,e,n){return function(){this.style.setProperty(t,e,n)}}function L(t,e,n){return function(){var i=e.apply(this,arguments);null==i?this.style.removeProperty(t):this.style.setProperty(t,i,n)}}function O(t,e){return t.style.getPropertyValue(e)||S(t).getComputedStyle(t,null).getPropertyValue(e)}function R(t){return function(){delete this[t]}}function I(t,e){return function(){this[t]=e}}function H(t,e){return function(){var n=e.apply(this,arguments);null==n?delete this[t]:this[t]=n}}function W(t){return t.trim().split(/^|\s+/)}function B(t){return t.classList||new D(t)}function D(t){this._node=t,this._names=W(t.getAttribute("class")||"")}function X(t,e){for(var n=B(t),i=-1,r=e.length;++i=0&&(this._names.splice(e,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var lt=[null];function ct(t,e){this._groups=t,this._parents=e}function ft(){return new ct([[document.documentElement]],lt)}function dt(t){return"string"==typeof t?new ct([[document.querySelector(t)]],[document.documentElement]):new ct([[t]],lt)}function pt(t,e){if(t=function(t){let e;for(;e=t.sourceEvent;)t=e;return t}(t),void 0===e&&(e=t.currentTarget),e){var n=e.ownerSVGElement||e;if(n.createSVGPoint){var i=n.createSVGPoint();return i.x=t.clientX,i.y=t.clientY,[(i=i.matrixTransform(e.getScreenCTM().inverse())).x,i.y]}if(e.getBoundingClientRect){var r=e.getBoundingClientRect();return[t.clientX-r.left-e.clientLeft,t.clientY-r.top-e.clientTop]}}return[t.pageX,t.pageY]}ct.prototype=ft.prototype={constructor:ct,select:function(t){"function"!=typeof t&&(t=h(t));for(var e=this._groups,n=e.length,i=new Array(n),r=0;r=A&&(A=M+1);!(x=m[A])&&++A=0;)(i=r[o])&&(a&&4^i.compareDocumentPosition(a)&&a.parentNode.insertBefore(i,a),a=i);return this},sort:function(t){function e(e,n){return e&&n?t(e.__data__,n.__data__):!e-!n}t||(t=M);for(var n=this._groups,i=n.length,r=new Array(i),o=0;o1?this.each((null==e?P:"function"==typeof e?L:C)(t,e,null==n?"":n)):O(this.node(),t)},property:function(t,e){return arguments.length>1?this.each((null==e?R:"function"==typeof e?H:I)(t,e)):this.node()[t]},classed:function(t,e){var n=W(t+"");if(arguments.length<2){for(var i=B(this.node()),r=-1,o=n.length;++r=0&&(e=t.slice(n+1),t=t.slice(0,n)),{type:t,name:e}}))}(t+""),a=o.length;if(!(arguments.length<2)){for(s=e?at:ot,i=0;i{}};function _t(){for(var t,e=0,n=arguments.length,i={};e=0&&(e=t.slice(n+1),t=t.slice(0,n)),t&&!i.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}}))),a=-1,s=o.length;if(!(arguments.length<2)){if(null!=e&&"function"!=typeof e)throw new Error("invalid callback: "+e);for(;++a0)for(var n,i,r=new Array(n),o=0;o=0&&e._call.call(void 0,t),e=e._next;--bt}()}finally{bt=0,function(){for(var t,e,n=vt,i=1/0;n;)n._call?(i>n._time&&(i=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:vt=e);wt=t,It(i)}(),Nt=0}}function Rt(){var t=zt.now(),e=t-$t;e>At&&(Tt-=e,$t=t)}function It(t){bt||(kt&&(kt=clearTimeout(kt)),t-Nt>24?(t<1/0&&(kt=setTimeout(Ot,t-zt.now()-Tt)),Mt&&(Mt=clearInterval(Mt))):(Mt||($t=zt.now(),Mt=setInterval(Rt,At)),bt=1,Et(Ot)))}function Ht(t,e,n){var i=new Ct;return e=null==e?0:+e,i.restart((n=>{i.stop(),t(n+e)}),e,n),i}Ct.prototype=Lt.prototype={constructor:Ct,restart:function(t,e,n){if("function"!=typeof t)throw new TypeError("callback is not a function");n=(null==n?St():+n)+(null==e?0:+e),this._next||wt===this||(wt?wt._next=this:vt=this,wt=this),this._call=t,this._time=n,It()},stop:function(){this._call&&(this._call=null,this._time=1/0,It())}};var Wt=_t("start","end","cancel","interrupt"),Bt=[],Dt=0,Xt=1,qt=2,Yt=3,Vt=4,jt=5,Ut=6;function Gt(t,e,n,i,r,o){var a=t.__transition;if(a){if(n in a)return}else t.__transition={};!function(t,e,n){var i,r=t.__transition;function o(t){n.state=Xt,n.timer.restart(a,n.delay,n.time),n.delay<=t&&a(t-n.delay)}function a(o){var u,l,c,f;if(n.state!==Xt)return h();for(u in r)if((f=r[u]).name===n.name){if(f.state===Yt)return Ht(a);f.state===Vt?(f.state=Ut,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete r[u]):+uDt)throw new Error("too late; already scheduled");return n}function Kt(t,e){var n=Qt(t,e);if(n.state>Yt)throw new Error("too late; already running");return n}function Qt(t,e){var n=t.__transition;if(!n||!(n=n[e]))throw new Error("transition not found");return n}function Zt(t,e){var n,i,r,o=t.__transition,a=!0;if(o){for(r in e=null==e?null:e+"",o)(n=o[r]).name===e?(i=n.state>qt&&n.state>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?xe(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?xe(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=he.exec(t))?new we(e[1],e[2],e[3],1):(e=ue.exec(t))?new we(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=le.exec(t))?xe(e[1],e[2],e[3],e[4]):(e=ce.exec(t))?xe(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=fe.exec(t))?Ne(e[1],e[2]/100,e[3]/100,1):(e=de.exec(t))?Ne(e[1],e[2]/100,e[3]/100,e[4]):pe.hasOwnProperty(t)?ye(pe[t]):"transparent"===t?new we(NaN,NaN,NaN,0):null}function ye(t){return new we(t>>16&255,t>>8&255,255&t,1)}function xe(t,e,n,i){return i<=0&&(t=e=n=NaN),new we(t,e,n,i)}function ve(t,e,n,i){return 1===arguments.length?((r=t)instanceof ee||(r=me(r)),r?new we((r=r.rgb()).r,r.g,r.b,r.opacity):new we):new we(t,e,n,null==i?1:i);var r}function we(t,e,n,i){this.r=+t,this.g=+e,this.b=+n,this.opacity=+i}function be(){return`#${$e(this.r)}${$e(this.g)}${$e(this.b)}`}function ke(){const t=Me(this.opacity);return`${1===t?"rgb(":"rgba("}${Ae(this.r)}, ${Ae(this.g)}, ${Ae(this.b)}${1===t?")":`, ${t})`}`}function Me(t){return isNaN(t)?1:Math.max(0,Math.min(1,t))}function Ae(t){return Math.max(0,Math.min(255,Math.round(t)||0))}function $e(t){return((t=Ae(t))<16?"0":"")+t.toString(16)}function Ne(t,e,n,i){return i<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new ze(t,e,n,i)}function Te(t){if(t instanceof ze)return new ze(t.h,t.s,t.l,t.opacity);if(t instanceof ee||(t=me(t)),!t)return new ze;if(t instanceof ze)return t;var e=(t=t.rgb()).r/255,n=t.g/255,i=t.b/255,r=Math.min(e,n,i),o=Math.max(e,n,i),a=NaN,s=o-r,h=(o+r)/2;return s?(a=e===o?(n-i)/s+6*(n0&&h<1?0:a,new ze(a,s,h,t.opacity)}function ze(t,e,n,i){this.h=+t,this.s=+e,this.l=+n,this.opacity=+i}function Ee(t){return(t=(t||0)%360)<0?t+360:t}function Se(t){return Math.max(0,Math.min(1,t||0))}function Pe(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}Jt(ee,me,{copy(t){return Object.assign(new this.constructor,this,t)},displayable(){return this.rgb().displayable()},hex:ge,formatHex:ge,formatHex8:function(){return this.rgb().formatHex8()},formatHsl:function(){return Te(this).formatHsl()},formatRgb:_e,toString:_e}),Jt(we,ve,te(ee,{brighter(t){return t=null==t?ie:Math.pow(ie,t),new we(this.r*t,this.g*t,this.b*t,this.opacity)},darker(t){return t=null==t?ne:Math.pow(ne,t),new we(this.r*t,this.g*t,this.b*t,this.opacity)},rgb(){return this},clamp(){return new we(Ae(this.r),Ae(this.g),Ae(this.b),Me(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:be,formatHex:be,formatHex8:function(){return`#${$e(this.r)}${$e(this.g)}${$e(this.b)}${$e(255*(isNaN(this.opacity)?1:this.opacity))}`},formatRgb:ke,toString:ke})),Jt(ze,(function(t,e,n,i){return 1===arguments.length?Te(t):new ze(t,e,n,null==i?1:i)}),te(ee,{brighter(t){return t=null==t?ie:Math.pow(ie,t),new ze(this.h,this.s,this.l*t,this.opacity)},darker(t){return t=null==t?ne:Math.pow(ne,t),new ze(this.h,this.s,this.l*t,this.opacity)},rgb(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,i=n+(n<.5?n:1-n)*e,r=2*n-i;return new we(Pe(t>=240?t-240:t+120,r,i),Pe(t,r,i),Pe(t<120?t+240:t-120,r,i),this.opacity)},clamp(){return new ze(Ee(this.h),Se(this.s),Se(this.l),Me(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const t=Me(this.opacity);return`${1===t?"hsl(":"hsla("}${Ee(this.h)}, ${100*Se(this.s)}%, ${100*Se(this.l)}%${1===t?")":`, ${t})`}`}}));var Ce=t=>()=>t;function Le(t){return 1==(t=+t)?Oe:function(e,n){return n-e?function(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-t,n=1/n,function(i){return Math.pow(t+i*e,n)}}(e,n,t):Ce(isNaN(e)?n:e)}}function Oe(t,e){var n=e-t;return n?function(t,e){return function(n){return t+n*e}}(t,n):Ce(isNaN(t)?e:t)}var Re=function t(e){var n=Le(e);function i(t,e){var i=n((t=ve(t)).r,(e=ve(e)).r),r=n(t.g,e.g),o=n(t.b,e.b),a=Oe(t.opacity,e.opacity);return function(e){return t.r=i(e),t.g=r(e),t.b=o(e),t.opacity=a(e),t+""}}return i.gamma=t,i}(1);function Ie(t,e){return t=+t,e=+e,function(n){return t*(1-n)+e*n}}var He=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,We=new RegExp(He.source,"g");function Be(t,e){var n,i,r,o=He.lastIndex=We.lastIndex=0,a=-1,s=[],h=[];for(t+="",e+="";(n=He.exec(t))&&(i=We.exec(e));)(r=i.index)>o&&(r=e.slice(o,r),s[a]?s[a]+=r:s[++a]=r),(n=n[0])===(i=i[0])?s[a]?s[a]+=i:s[++a]=i:(s[++a]=null,h.push({i:a,x:Ie(n,i)})),o=We.lastIndex;return o180?e+=360:e-t>180&&(t+=360),o.push({i:n.push(r(n)+"rotate(",null,i)-2,x:Ie(t,e)})):e&&n.push(r(n)+"rotate("+e+i)}(o.rotate,a.rotate,s,h),function(t,e,n,o){t!==e?o.push({i:n.push(r(n)+"skewX(",null,i)-2,x:Ie(t,e)}):e&&n.push(r(n)+"skewX("+e+i)}(o.skewX,a.skewX,s,h),function(t,e,n,i,o,a){if(t!==n||e!==i){var s=o.push(r(o)+"scale(",null,",",null,")");a.push({i:s-4,x:Ie(t,n)},{i:s-2,x:Ie(e,i)})}else 1===n&&1===i||o.push(r(o)+"scale("+n+","+i+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,s,h),o=a=null,function(t){for(var e,n=-1,i=h.length;++n=0&&(t=t.slice(0,e)),!t||"start"===t}))}(e)?Ft:Kt;return function(){var a=o(this,t),s=a.on;s!==i&&(r=(i=s).copy()).on(e,n),a.on=r}}(n,t,e))},attr:function(t,e){var n=i(t),r="transform"===n?Ue:tn;return this.attrTween(t,"function"==typeof e?(n.local?sn:an)(n,r,Je(this,"attr."+t,e)):null==e?(n.local?nn:en)(n):(n.local?on:rn)(n,r,e))},attrTween:function(t,e){var n="attr."+t;if(arguments.length<2)return(n=this.tween(n))&&n._value;if(null==e)return this.tween(n,null);if("function"!=typeof e)throw new Error;var r=i(t);return this.tween(n,(r.local?hn:un)(r,e))},style:function(t,e,n){var i="transform"==(t+="")?je:tn;return null==e?this.styleTween(t,function(t,e){var n,i,r;return function(){var o=O(this,t),a=(this.style.removeProperty(t),O(this,t));return o===a?null:o===n&&a===i?r:r=e(n=o,i=a)}}(t,i)).on("end.style."+t,gn(t)):"function"==typeof e?this.styleTween(t,function(t,e,n){var i,r,o;return function(){var a=O(this,t),s=n(this),h=s+"";return null==s&&(this.style.removeProperty(t),h=s=O(this,t)),a===h?null:a===i&&h===r?o:(r=h,o=e(i=a,s))}}(t,i,Je(this,"style."+t,e))).each(function(t,e){var n,i,r,o,a="style."+e,s="end."+a;return function(){var h=Kt(this,t),u=h.on,l=null==h.value[a]?o||(o=gn(e)):void 0;u===n&&r===l||(i=(n=u).copy()).on(s,r=l),h.on=i}}(this._id,t)):this.styleTween(t,function(t,e,n){var i,r,o=n+"";return function(){var a=O(this,t);return a===o?null:a===i?r:r=e(i=a,n)}}(t,i,e),n).on("end.style."+t,null)},styleTween:function(t,e,n){var i="style."+(t+="");if(arguments.length<2)return(i=this.tween(i))&&i._value;if(null==e)return this.tween(i,null);if("function"!=typeof e)throw new Error;return this.tween(i,function(t,e,n){var i,r;function o(){var o=e.apply(this,arguments);return o!==r&&(i=(r=o)&&function(t,e,n){return function(i){this.style.setProperty(t,e.call(this,i),n)}}(t,o,n)),i}return o._value=e,o}(t,e,null==n?"":n))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var e=t(this);this.textContent=null==e?"":e}}(Je(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},textTween:function(t){var e="text";if(arguments.length<1)return(e=this.tween(e))&&e._value;if(null==t)return this.tween(e,null);if("function"!=typeof t)throw new Error;return this.tween(e,function(t){var e,n;function i(){var i=t.apply(this,arguments);return i!==n&&(e=(n=i)&&function(t){return function(e){this.textContent=t.call(this,e)}}(i)),e}return i._value=t,i}(t))},remove:function(){return this.on("end.remove",function(t){return function(){var e=this.parentNode;for(var n in this.__transition)if(+n!==t)return;e&&e.removeChild(this)}}(this._id))},tween:function(t,e){var n=this._id;if(t+="",arguments.length<2){for(var i,r=Qt(this.node(),n).tween,o=0,a=r.length;o()=>t;function An(t,{sourceEvent:e,target:n,transform:i,dispatch:r}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:e,enumerable:!0,configurable:!0},target:{value:n,enumerable:!0,configurable:!0},transform:{value:i,enumerable:!0,configurable:!0},_:{value:r}})}function $n(t,e,n){this.k=t,this.x=e,this.y=n}$n.prototype={constructor:$n,scale:function(t){return 1===t?this:new $n(this.k*t,this.x,this.y)},translate:function(t,e){return 0===t&0===e?this:new $n(this.k,this.x+this.k*t,this.y+this.k*e)},apply:function(t){return[t[0]*this.k+this.x,t[1]*this.k+this.y]},applyX:function(t){return t*this.k+this.x},applyY:function(t){return t*this.k+this.y},invert:function(t){return[(t[0]-this.x)/this.k,(t[1]-this.y)/this.k]},invertX:function(t){return(t-this.x)/this.k},invertY:function(t){return(t-this.y)/this.k},rescaleX:function(t){return t.copy().domain(t.range().map(this.invertX,this).map(t.invert,t))},rescaleY:function(t){return t.copy().domain(t.range().map(this.invertY,this).map(t.invert,t))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var Nn=new $n(1,0,0);function Tn(t){for(;!t.__zoom;)if(!(t=t.parentNode))return Nn;return t.__zoom}function zn(t){t.stopImmediatePropagation()}function En(t){t.preventDefault(),t.stopImmediatePropagation()}function Sn(t){return!(t.ctrlKey&&"wheel"!==t.type||t.button)}function Pn(){var t=this;return t instanceof SVGElement?(t=t.ownerSVGElement||t).hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]:[[0,0],[t.clientWidth,t.clientHeight]]}function Cn(){return this.__zoom||Nn}function Ln(t){return-t.deltaY*(1===t.deltaMode?.05:t.deltaMode?1:.002)*(t.ctrlKey?10:1)}function On(){return navigator.maxTouchPoints||"ontouchstart"in this}function Rn(t,e,n){var i=t.invertX(e[0][0])-n[0][0],r=t.invertX(e[1][0])-n[1][0],o=t.invertY(e[0][1])-n[0][1],a=t.invertY(e[1][1])-n[1][1];return t.translate(r>i?(i+r)/2:Math.min(0,i)||Math.max(0,r),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}function In(){var t,e,n,i=Sn,r=Pn,o=Rn,a=Ln,s=On,h=[0,1/0],u=[[-1/0,-1/0],[1/0,1/0]],l=250,c=Ke,f=_t("start","zoom","end"),d=500,p=150,g=0,_=10;function m(t){t.property("__zoom",Cn).on("wheel.zoom",M,{passive:!1}).on("mousedown.zoom",A).on("dblclick.zoom",$).filter(s).on("touchstart.zoom",N).on("touchmove.zoom",T).on("touchend.zoom touchcancel.zoom",z).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function y(t,e){return(e=Math.max(h[0],Math.min(h[1],e)))===t.k?t:new $n(e,t.x,t.y)}function x(t,e,n){var i=e[0]-n[0]*t.k,r=e[1]-n[1]*t.k;return i===t.x&&r===t.y?t:new $n(t.k,i,r)}function v(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function w(t,e,n,i){t.on("start.zoom",(function(){b(this,arguments).event(i).start()})).on("interrupt.zoom end.zoom",(function(){b(this,arguments).event(i).end()})).tween("zoom",(function(){var t=this,o=arguments,a=b(t,o).event(i),s=r.apply(t,o),h=null==n?v(s):"function"==typeof n?n.apply(t,o):n,u=Math.max(s[1][0]-s[0][0],s[1][1]-s[0][1]),l=t.__zoom,f="function"==typeof e?e.apply(t,o):e,d=c(l.invert(h).concat(u/l.k),f.invert(h).concat(u/f.k));return function(t){if(1===t)t=f;else{var e=d(t),n=u/e[2];t=new $n(n,h[0]-e[0]*n,h[1]-e[1]*n)}a.zoom(null,t)}}))}function b(t,e,n){return!n&&t.__zooming||new k(t,e)}function k(t,e){this.that=t,this.args=e,this.active=0,this.sourceEvent=null,this.extent=r.apply(t,e),this.taps=0}function M(t,...e){if(i.apply(this,arguments)){var n=b(this,e).event(t),r=this.__zoom,s=Math.max(h[0],Math.min(h[1],r.k*Math.pow(2,a.apply(this,arguments)))),l=pt(t);if(n.wheel)n.mouse[0][0]===l[0]&&n.mouse[0][1]===l[1]||(n.mouse[1]=r.invert(n.mouse[0]=l)),clearTimeout(n.wheel);else{if(r.k===s)return;n.mouse=[l,r.invert(l)],Zt(this),n.start()}En(t),n.wheel=setTimeout((function(){n.wheel=null,n.end()}),p),n.zoom("mouse",o(x(y(r,s),n.mouse[0],n.mouse[1]),n.extent,u))}}function A(t,...e){if(!n&&i.apply(this,arguments)){var r=t.currentTarget,a=b(this,e,!0).event(t),s=dt(t.view).on("mousemove.zoom",(function(t){if(En(t),!a.moved){var e=t.clientX-l,n=t.clientY-c;a.moved=e*e+n*n>g}a.event(t).zoom("mouse",o(x(a.that.__zoom,a.mouse[0]=pt(t,r),a.mouse[1]),a.extent,u))}),!0).on("mouseup.zoom",(function(t){s.on("mousemove.zoom mouseup.zoom",null),function(t,e){var n=t.document.documentElement,i=dt(t).on("dragstart.drag",null);e&&(i.on("click.drag",kn,bn),setTimeout((function(){i.on("click.drag",null)}),0)),"onselectstart"in n?i.on("selectstart.drag",null):(n.style.MozUserSelect=n.__noselect,delete n.__noselect)}(t.view,a.moved),En(t),a.event(t).end()}),!0),h=pt(t,r),l=t.clientX,c=t.clientY;!function(t){var e=t.document.documentElement,n=dt(t).on("dragstart.drag",kn,bn);"onselectstart"in e?n.on("selectstart.drag",kn,bn):(e.__noselect=e.style.MozUserSelect,e.style.MozUserSelect="none")}(t.view),zn(t),a.mouse=[h,this.__zoom.invert(h)],Zt(this),a.start()}}function $(t,...e){if(i.apply(this,arguments)){var n=this.__zoom,a=pt(t.changedTouches?t.changedTouches[0]:t,this),s=n.invert(a),h=n.k*(t.shiftKey?.5:2),c=o(x(y(n,h),a,s),r.apply(this,e),u);En(t),l>0?dt(this).transition().duration(l).call(w,c,a,t):dt(this).call(m.transform,c,a,t)}}function N(n,...r){if(i.apply(this,arguments)){var o,a,s,h,u=n.touches,l=u.length,c=b(this,r,n.changedTouches.length===l).event(n);for(zn(n),a=0;a=0;)e+=n[i].value;else e=1;t.value=e}function Wn(t,e){t instanceof Map?(t=[void 0,t],void 0===e&&(e=Dn)):void 0===e&&(e=Bn);for(var n,i,r,o,a,s=new Yn(t),h=[s];n=h.pop();)if((r=e(n.data))&&(a=(r=Array.from(r)).length))for(n.children=r,o=a-1;o>=0;--o)h.push(i=r[o]=new Yn(r[o])),i.parent=n,i.depth=n.depth+1;return s.eachBefore(qn)}function Bn(t){return t.children}function Dn(t){return Array.isArray(t)?t[1]:null}function Xn(t){void 0!==t.data.value&&(t.value=t.data.value),t.data=t.data.data}function qn(t){var e=0;do{t.height=e}while((t=t.parent)&&t.height<++e)}function Yn(t){this.data=t,this.depth=this.height=0,this.parent=null}function Vn(t,e){return t.parent===e.parent?1:2}function jn(t){var e=t.children;return e?e[0]:t.t}function Un(t){var e=t.children;return e?e[e.length-1]:t.t}function Gn(t,e,n){var i=n/(e.i-t.i);e.c-=i,e.s+=n,t.c+=i,e.z+=n,e.m+=n}function Fn(t,e,n){return t.a.parent===e.parent?t.a:n}function Kn(t,e){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=e}function Qn(){var t=Vn,e=1,n=1,i=null;function r(r){var h=function(t){for(var e,n,i,r,o,a=new Kn(t,0),s=[a];e=s.pop();)if(i=e._.children)for(e.children=new Array(o=i.length),r=o-1;r>=0;--r)s.push(n=e.children[r]=new Kn(i[r],r)),n.parent=e;return(a.parent=new Kn(null,0)).children=[a],a}(r);if(h.eachAfter(o),h.parent.m=-h.z,h.eachBefore(a),i)r.eachBefore(s);else{var u=r,l=r,c=r;r.eachBefore((function(t){t.xl.x&&(l=t),t.depth>c.depth&&(c=t)}));var f=u===l?1:t(u,l)/2,d=f-u.x,p=e/(l.x+f+d),g=n/(c.depth||1);r.eachBefore((function(t){t.x=(t.x+d)*p,t.y=t.depth*g}))}return r}function o(e){var n=e.children,i=e.parent.children,r=e.i?i[e.i-1]:null;if(n){!function(t){for(var e,n=0,i=0,r=t.children,o=r.length;--o>=0;)(e=r[o]).z+=n,e.m+=n,n+=e.s+(i+=e.c)}(e);var o=(n[0].z+n[n.length-1].z)/2;r?(e.z=r.z+t(e._,r._),e.m=e.z-o):e.z=o}else r&&(e.z=r.z+t(e._,r._));e.parent.A=function(e,n,i){if(n){for(var r,o=e,a=e,s=n,h=o.parent.children[0],u=o.m,l=a.m,c=s.m,f=h.m;s=Un(s),o=jn(o),s&&o;)h=jn(h),(a=Un(a)).a=e,(r=s.z+c-o.z-u+t(s._,o._))>0&&(Gn(Fn(s,e,i),e,r),u+=r,l+=r),c+=s.m,u+=o.m,f+=h.m,l+=a.m;s&&!Un(a)&&(a.t=s,a.m+=c-l),o&&!jn(h)&&(h.t=o,h.m+=u-f,i=e)}return i}(e,r,e.parent.A||i[0])}function a(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function s(t){t.x*=e,t.y=t.depth*n}return r.separation=function(e){return arguments.length?(t=e,r):t},r.size=function(t){return arguments.length?(i=!1,e=+t[0],n=+t[1],r):i?null:[e,n]},r.nodeSize=function(t){return arguments.length?(i=!0,e=+t[0],n=+t[1],r):i?[e,n]:null},r}Tn.prototype=$n.prototype,Yn.prototype=Wn.prototype={constructor:Yn,count:function(){return this.eachAfter(Hn)},each:function(t,e){let n=-1;for(const i of this)t.call(e,i,++n,this);return this},eachAfter:function(t,e){for(var n,i,r,o=this,a=[o],s=[],h=-1;o=a.pop();)if(s.push(o),n=o.children)for(i=0,r=n.length;i=0;--i)o.push(n[i]);return this},find:function(t,e){let n=-1;for(const i of this)if(t.call(e,i,++n,this))return i},sum:function(t){return this.eachAfter((function(e){for(var n=+t(e.data)||0,i=e.children,r=i&&i.length;--r>=0;)n+=i[r].value;e.value=n}))},sort:function(t){return this.eachBefore((function(e){e.children&&e.children.sort(t)}))},path:function(t){for(var e=this,n=function(t,e){if(t===e)return t;var n=t.ancestors(),i=e.ancestors(),r=null;for(t=n.pop(),e=i.pop();t===e;)r=t,t=n.pop(),e=i.pop();return r}(e,t),i=[e];e!==n;)e=e.parent,i.push(e);for(var r=i.length;t!==n;)i.splice(r,0,t),t=t.parent;return i},ancestors:function(){for(var t=this,e=[t];t=t.parent;)e.push(t);return e},descendants:function(){return Array.from(this)},leaves:function(){var t=[];return this.eachBefore((function(e){e.children||t.push(e)})),t},links:function(){var t=this,e=[];return t.each((function(n){n!==t&&e.push({source:n.parent,target:n})})),e},copy:function(){return Wn(this).eachBefore(Xn)},[Symbol.iterator]:function*(){var t,e,n,i,r=this,o=[r];do{for(t=o.reverse(),o=[];r=t.pop();)if(yield r,e=r.children)for(n=0,i=e.length;n=0))throw new Error(`invalid digits: ${t}`);if(e>15)return ni;const n=10**e;return function(t){this._+=t[0];for(let e=1,i=t.length;eti)if(Math.abs(l*s-h*u)>ti&&r){let f=n-o,d=i-a,p=s*s+h*h,g=f*f+d*d,_=Math.sqrt(p),m=Math.sqrt(c),y=r*Math.tan((Zn-Math.acos((p+c-g)/(2*_*m)))/2),x=y/m,v=y/_;Math.abs(x-1)>ti&&this._append`L${t+x*u},${e+x*l}`,this._append`A${r},${r},0,0,${+(l*f>u*d)},${this._x1=t+v*s},${this._y1=e+v*h}`}else this._append`L${this._x1=t},${this._y1=e}`}arc(t,e,n,i,r,o){if(t=+t,e=+e,o=!!o,(n=+n)<0)throw new Error(`negative radius: ${n}`);let a=n*Math.cos(i),s=n*Math.sin(i),h=t+a,u=e+s,l=1^o,c=o?i-r:r-i;null===this._x1?this._append`M${h},${u}`:(Math.abs(this._x1-h)>ti||Math.abs(this._y1-u)>ti)&&this._append`L${h},${u}`,n&&(c<0&&(c=c%Jn+Jn),c>ei?this._append`A${n},${n},0,1,${l},${t-a},${e-s}A${n},${n},0,1,${l},${this._x1=h},${this._y1=u}`:c>ti&&this._append`A${n},${n},0,${+(c>=Zn)},${l},${this._x1=t+n*Math.cos(r)},${this._y1=e+n*Math.sin(r)}`)}rect(t,e,n,i){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+e}h${n=+n}v${+i}h${-n}Z`}toString(){return this._}}function ri(){return new ii}function oi(t){if(!t.ok)throw new Error(t.status+" "+t.statusText);return t.text()}ri.prototype=ii.prototype;const ai="right";class si{constructor(t,e){this._xOffset=60,this._yOffset=60,this._boxWidth=t,this._boxHeight=e,this._splittNames=!1}get xOffset(){return this._xOffset}get yOffset(){return this._yOffset}get splittNames(){return this._splittNames}get boxWidth(){return this._boxWidth}get boxHeight(){return this._boxHeight}direction(){throw"Abstract method direction() not implemented"}nodeWidth(){throw"Abstract method nodeWidth() not implemented"}norm(t){throw"Abstract method norm() not implemented"}elbow(t){throw"Abstract method elbow() not implemented"}}function hi(t,e){const n=e.xOffset/2,i=e.yOffset/2;let r=t.source.x,o=t.source.y;if(void 0!==t.spouse&&0===t.source.data.family?r-=(t.source.x-t.spouse.x)/2:o+=e.boxHeight/2*e.direction(),null===t.source.data.data&&(r-=e.boxWidth/2+n/2,o+=e.boxHeight/2*e.direction()),null!==t.target){let n=t.target.x,a=t.target.y-e.direction()*(e.boxHeight/2+i);const s=ri();return s.moveTo(r,o),s.lineTo(r,a),s.lineTo(n,a),s.lineTo(n,a+e.direction()*i),s.toString()}return function(t,e){const n=ri(),i=5,r=2;let o=t.source.y;if(t.source.data.family>0&&(o=t.spouse.y-t.source.data.family*e.direction()*i),null===t.coords&&(n.moveTo(t.spouse.x+e.boxWidth/2,o),n.lineTo(t.source.x-e.boxWidth/2,o)),t.coords&&t.coords.length>0){for(let i=0;i0&&(a=t.coords[i-1].x+e.boxWidth/2);let h=i>0?r:0,u=i+1<=t.coords.length?r:0;n.moveTo(a+h,o),n.lineTo(s-u,o)}n.moveTo(t.coords[t.coords.length-1].x+e.boxWidth/2+r,o),n.lineTo(t.source.x-e.boxWidth/2,o)}return n.toString()}(t,e)}class ui extends si{constructor(t,e){super(t,e),this._splittNames=!0}direction(){return 1}get nodeWidth(){return 2*this._boxWidth+this._yOffset}norm(t){t.y=this.direction()*t.depth*(this._boxHeight+this._yOffset)}elbow(t){return hi(t,this)}}class li extends si{constructor(t,e){super(t,e),this._splittNames=!0}direction(){return-1}get nodeWidth(){return 2*this._boxWidth+this._yOffset}norm(t){t.y=this.direction()*t.depth*(this._boxHeight+this._yOffset)}elbow(t){return hi(t,this)}}function ci(t,e){const n=e.xOffset/2,i=e.yOffset/2;let r=t.source.x,o=t.source.y;if(void 0!==t.spouse&&0===t.source.data.family?o-=(t.source.y-t.spouse.y)/2:r+=e.boxWidth/2*e.direction(),null===t.source.data.data&&(r+=e.boxWidth/2*e.direction(),o-=e.boxHeight/2+i/2),null!==t.target){let i=t.target.x-e.direction()*(e.boxWidth/2+n),a=t.target.y;const s=ri();return s.moveTo(r,o),s.lineTo(i,o),s.lineTo(i,a),s.lineTo(i+e.direction()*n,a),s.toString()}return function(t,e){const n=ri(),i=5,r=2;let o=t.source.x;if(t.source.data.family>0&&(o=t.spouse.x-t.source.data.family*e.direction()*i),null===t.coords&&(n.moveTo(o,t.spouse.y+e.boxHeight/2),n.lineTo(o,t.source.y-e.boxHeight/2)),t.coords&&t.coords.length>0){for(let i=0;i0&&(a=t.coords[i-1].y+e.boxHeight/2);let h=i>0?r:0,u=i+1<=t.coords.length?r:0;n.moveTo(o,a+h),n.lineTo(o,s-u)}n.moveTo(o,t.coords[t.coords.length-1].y+e.boxHeight/2+r),n.lineTo(o,t.source.y-e.boxHeight/2)}return n.toString()}(t,e)}class fi extends si{constructor(t,e){super(t,e),this._xOffset=40,this._yOffset=40}direction(){return 1}get nodeWidth(){return 2*this._boxHeight+this._xOffset}norm(t){t.y=t.x,t.x=this.direction()*t.depth*(this._boxWidth+this._xOffset)}elbow(t){return ci(t,this)}}class di extends si{constructor(t,e){super(t,e),this._xOffset=40,this._yOffset=40}direction(){return-1}get nodeWidth(){return 2*this._boxHeight+this._xOffset}norm(t){t.y=t.x,t.x=this.direction()*t.depth*(this._boxWidth+this._xOffset)}elbow(t){return ci(t,this)}}class pi{constructor(){this._orientations={down:new ui(150,175),up:new li(150,175),[ai]:new fi(300,80),left:new di(300,80)}}get(){return this._orientations}}class gi{constructor(t,e=4,n=ai,i=!1,r=1){this._treeLayout=n,this._orientations=new pi,this.duration=750,this.padding=15,this.imagePadding=5,this._generations=e,this.textPadding=8,this._fontSize=14,this.fontColor="rgb(0, 0, 0)",this.rtl=i,this.labels=t,this.direction=r}get generations(){return this._generations}set generations(t){this._generations=t}get treeLayout(){return this._treeLayout}set treeLayout(t){this._treeLayout=t}get orientation(){return this._orientations.get()[this.treeLayout]}}class _i{constructor(t){this._configuration=t,this._nodes=null,this._root=null}init(t){let e=Wn(t);const n=Qn().nodeSize([this._configuration.orientation.nodeWidth,0]).separation(((t,e)=>this.separation(t,e)));this._root=e,this._nodes=n(e)}separation(t,e){return void 0!==t.data.spouses||void 0!==e.data.spouse?.75:t.parent===e.parent?.5:.75}get nodes(){return this._nodes}get root(){return this._root}}class mi{constructor(t,e){this._orientation=t,this._cornerRadius=e,this._imagePadding=5,this._imageRadius=Math.min(40,t.boxHeight/2-this._imagePadding),this._x=this.calculateX(),this._y=this.calculateY(),this._width=this.calculateImageWidth(),this._height=this.calculateImageHeight(),this._rx=this.calculateCornerRadius(),this._ry=this.calculateCornerRadius()}calculateX(){return-this._orientation.boxWidth/2+this._imagePadding}calculateY(){return this._orientation instanceof fi||this._orientation instanceof di?-this._imageRadius:-this._orientation.boxHeight/2+this._imagePadding}calculateImageWidth(){return this._orientation instanceof fi||this._orientation instanceof di?2*this._imageRadius:this._orientation.boxWidth-2*this._imagePadding}calculateImageHeight(){return 2*this._imageRadius}calculateCornerRadius(){return this._cornerRadius-this._imagePadding}get imagePadding(){return this._imagePadding}get imageRadius(){return this._imageRadius}set imageRadius(t){this._imageRadius=t}get x(){return this._x}get y(){return this._y}get rx(){return this._rx}get ry(){return this._ry}get width(){return this._width}get height(){return this._height}}class yi{constructor(t,e=null){this._orientation=t,this._image=e,this._textPadding=15,this._x=this.calculateX(),this._y=this.calculateY(),this._width=this.calculateWidth()}calculateX(){const t=-this._orientation.boxWidth/2+this._textPadding;return this._image?t+this._image.width:t}calculateY(){return this._orientation instanceof fi||this._orientation instanceof di?-this._textPadding:this._image?this._image.y+this._image.height+2*this._textPadding:-this._orientation.boxHeight/2+2*this._textPadding}calculateWidth(){const t=this._orientation.boxWidth-2*this._textPadding;return this._image&&(this._orientation instanceof fi||this._orientation instanceof di)?t-this._image.width:t}get x(){return this._x}get y(){return this._y}get width(){return this._width}}class xi{constructor(t){this._cornerRadius=20,this._showImage=!0,this._orientation=t,this._x=-t.boxWidth/2,this._y=-t.boxHeight/2,this._rx=this._cornerRadius,this._ry=this._cornerRadius,this._width=t.boxWidth,this._height=t.boxHeight,this._image=new mi(t,this._cornerRadius)}get showImage(){return this._showImage}set showImage(t){this._showImage=t}get x(){return this._x}get y(){return this._y}get rx(){return this._rx}get ry(){return this._ry}get width(){return this._width}get height(){return this._height}get image(){return this._image}get text(){return new yi(this._orientation,this._showImage?this._image:null)}}class vi{constructor(t,e,n){this._svg=t,this._configuration=e,this._hierarchy=n,this._hierarchy.root.x0=0,this._hierarchy.root.y0=0,this._orientation=this._configuration.orientation,this._box=new xi(this._orientation),this.draw(this._hierarchy.root)}draw(t){let e=this._hierarchy.nodes.descendants(),n=[];e.shift(),e.forEach((t=>{this._orientation.norm(t)})),e.forEach((t=>{if(t.data&&t.data.spouse){const n=e.find((e=>e.data.data.xref===t.data.spouse));if(this._orientation instanceof fi||this._orientation instanceof di){const e=t.y-n.y-this._orientation.boxHeight-this._orientation.yOffset/2;0!==e&&0===t.data.family&&(t.y-=e/2,n.y+=e/2)}else{const e=t.x-n.x-this._orientation.boxWidth-this._orientation.xOffset/2;0!==e&&0===t.data.family&&(t.x-=e/2,n.x+=e/2)}}}));const i=e.find((t=>t.children&&t.children.length>0));void 0!==i&&i.each((t=>{if(void 0!==t.data.spouse&&null!==t.data.spouse&&t.children&&t.children.length>=1){let e=0;e=this._orientation instanceof fi||this._orientation instanceof di?this._orientation.boxHeight/2+this._orientation.yOffset/4:this._orientation.boxWidth/2+this._orientation.xOffset/4,this.moveChildren(t,e)}})),e.forEach((t=>{const i=e.find((e=>e.data.data.xref===t.data.spouse));if(t.children&&t.children.forEach((e=>{void 0!==e.data.spouse&&null!==e.data.spouse||n.push({spouse:i,source:t,target:e,coords:null})})),void 0!==i){let r=null;if(void 0!==i.data.spouses&&i.data.spouses.length>0){const n=i.data.spouses.indexOf(t.data.data.xref),o=i.data.spouses.slice(0,n);o.length>0&&(r=[],o.forEach((t=>{const n=e.find((e=>e.data.data.xref===t));r.push({x:n.x,y:n.y})})))}null!==t.data.data&&n.push({spouse:i,source:t,target:null,coords:r})}})),this.drawLinks(n,t),this.drawNodes(e,t),e.forEach((t=>{t.x0=t.x,t.y0=t.y}))}moveChildren(t,e){t.each((n=>{n.depth!==t.depth&&(0!==t.data.family&&1===t.children.length&&void 0===n.children||(this._orientation instanceof fi||this._orientation instanceof di?n.y-=e:n.x-=e))}))}drawNodes(t,e){let n=0,i=this;this._svg.defs.get().append("clipPath").attr("id","clip-image").append("rect").attr("rx",this._box.image.rx).attr("ry",this._box.image.ry).attr("x",this._box.image.x).attr("y",this._box.image.y).attr("width",this._box.image.width).attr("height",this._box.image.height);const r=this._svg.visual.selectAll("g.person").data(t,(t=>t.id||(t.id=++n))).enter().append("g").append("g").attr("class",(t=>"person"+(t.data.spouse?" spouse":""))).attr("transform",(t=>"translate("+t.x+","+t.y+")"));r.append("rect").attr("class",(t=>"F"===t.data.data.sex?"female":"M"===t.data.data.sex?"male":"unknown")).attr("rx",this._box.rx).attr("ry",this._box.ry).attr("x",this._box.x).attr("y",this._box.y).attr("width",this._box.width).attr("height",this._box.height).attr("fill-opacity",.5),r.filter((t=>""!==t.data.data.xref)).each((function(t){let e=dt(this);i.drawText(e,t.data)}))}drawText(t,e){t.append("title").text(e.data.name);const n=this.getImageToLoad(e);if(this._box.showImage=!!n,this._box.showImage){let e=t.append("g").attr("class","image");e.append("rect").attr("rx",this._box.image.rx).attr("ry",this._box.image.ry).attr("x",this._box.image.x).attr("y",this._box.image.y).attr("width",this._box.image.width).attr("height",this._box.image.height).attr("fill","rgb(255, 255, 255)");let i=e.append("image").attr("x",this._box.image.x).attr("y",this._box.image.y).attr("width",this._box.image.width).attr("height",this._box.image.height).attr("clip-path","url(#clip-image)");(function(t,e=null){return fetch(t,e).then((t=>t.blob())).then((t=>new Promise(((e,n)=>{const i=new FileReader;i.onloadend=()=>e(i.result),i.onerror=n,i.readAsDataURL(t)}))))})(n).then((t=>i.attr("xlink:href",t))).catch((t=>{console.error(t)})),e.append("rect").attr("rx",this._box.image.rx).attr("ry",this._box.image.ry).attr("x",this._box.image.x).attr("y",this._box.image.y).attr("width",this._box.image.width).attr("height",this._box.image.height).attr("fill","none").attr("stroke","rgb(200, 200, 200)").attr("stroke-width",1.5)}this.addNames(t,e),this.addDates(t,e),this._box.showImage=!0}togglePerson(t,e){e.children?(e._children=e.children,e.children=null):(e.children=e._children,e._children=null),this.draw(e)}collapse(t){t.children&&(t._children=t.children,t._children.forEach((t=>this.collapse(t))),t.children=null)}addFirstNames(t,e){let n=0;for(let i of e.data.firstNames){let r=t.append("tspan").text(i);i===e.data.preferredName&&r.attr("class","preferred"),0!==n&&r.attr("dx","0.25em"),++n}}addLastNames(t,e,n=0){let i=0;for(let r of e.data.lastNames){let e=t.append("tspan").attr("class","lastName").text(r);0!==i&&e.attr("dx","0.25em"),0!==n&&e.attr("dx",n+"em"),++i}}truncateNames(t){let e=this._box.text.width;t.selectAll("tspan:not(.preferred):not(.lastName)").nodes().reverse().forEach((n=>dt(n).each(this.truncateText(t,e)))),t.selectAll("tspan.preferred").each(this.truncateText(t,e)),t.selectAll("tspan.lastName").each(this.truncateText(t,e))}truncateText(t,e){let n=this;return function(){let i=n.getTextLength(t),r=dt(this),o=r.text().split(/\s+/);for(let a=o.length-1;a>=0;--a)i>e&&(o[a]=o[a].slice(0,1)+".",r.text(o.join(" ")),i=n.getTextLength(t))}}truncateDate(t,e){let n=this;return function(){let i=n.getTextLength(t),r=dt(this),o=r.text();for(;i>e&&o.length>1;)o=o.slice(0,-1).trim(),r.text(o),i=n.getTextLength(t);"."===o[o.length-1]&&r.text(o.slice(0,-1).trim())}}getTextLength(t){let e=0;return t.selectAll("tspan").each((function(){e+=this.getComputedTextLength()})),e}addNames(t,e){let n=t.append("g").attr("class","name");if(this._orientation.splittNames){let t=n.append("text").attr("text-anchor","middle").attr("alignment-baseline","central").attr("dy",this._box.text.y),i=n.append("text").attr("text-anchor","middle").attr("alignment-baseline","central").attr("dy",this._box.text.y+20);this.addFirstNames(t,e),this.addLastNames(i,e),e.data.firstNames.length||e.data.lastNames.length||t.append("tspan").text(e.data.name),this.truncateNames(t),this.truncateNames(i)}else{let t=n.append("text").attr("text-anchor",this._configuration.rtl?"end":"start").attr("dx",this._box.text.x).attr("dy",this._box.text.y);this.addFirstNames(t,e),this.addLastNames(t,e,.25),e.data.firstNames.length||e.data.lastNames.length||t.append("tspan").text(e.data.name),this.truncateNames(t)}}addDates(t,e){let n=t.append("g").attr("class","table");if(this._orientation.splittNames){let t=n.append("text").attr("class","date").attr("text-anchor","middle").attr("alignment-baseline","central").attr("dy",this._box.text.y+50);t.append("title").text(e.data.timespan);let i=t.append("tspan").text(e.data.timespan);return void(this.getTextLength(t)>this._box.text.width&&(t.selectAll("tspan").each(this.truncateDate(t,this._box.text.width)),i.text(i.text()+"…")))}let i=20;if(e.data.birth){n.append("text").attr("class","date").attr("text-anchor","middle").attr("dominant-baseline","middle").attr("x",this._box.text.x).attr("dy",this._box.text.y+i).append("tspan").text("★").attr("x",this._box.text.x+5);let t=n.append("text").attr("class","date").attr("text-anchor",this._configuration.rtl?"end":"start").attr("dominant-baseline","middle").attr("x",this._box.text.x).attr("dy",this._box.text.y+i);t.append("title").text(e.data.birth);let r=t.append("tspan").text(e.data.birth).attr("x",this._box.text.x+15);this.getTextLength(t)>this._box.text.width-25&&(t.selectAll("tspan").each(this.truncateDate(t,this._box.text.width-25)),r.text(r.text()+"…"))}if(e.data.death){e.data.birth&&(i+=20),n.append("text").attr("class","date").attr("text-anchor","middle").attr("dominant-baseline","middle").attr("x",this._box.text.x).attr("dy",this._box.text.y+i).append("tspan").text("†").attr("x",this._box.text.x+5);let t=n.append("text").attr("class","date").attr("text-anchor",this._configuration.rtl?"end":"start").attr("dominant-baseline","middle").attr("x",this._box.text.x).attr("dy",this._box.text.y+i);t.append("title").text(e.data.death);let r=t.append("tspan").text(e.data.death).attr("x",this._box.text.x+15);this.getTextLength(t)>this._box.text.width-25&&(t.selectAll("tspan").each(this.truncateDate(t,this._box.text.width-25)),r.text(r.text().trim()+"…"))}}getImageToLoad(t){return t.data.thumbnail?t.data.thumbnail:""}drawLinks(t,e){this._svg.visual.selectAll("path.link").data(t).enter().append("path").classed("link",!0).attr("d",(t=>this._orientation.elbow(t)))}}class wi{constructor(t){this._element=t.append("div").attr("class","overlay").style("opacity",1e-6)}show(t,e=0,n=null){this._element.select("p").remove(),this._element.append("p").attr("class","tooltip").text(t),this._element.transition().duration(e).style("opacity",1).on("end",(()=>{"function"==typeof n&&n()}))}hide(t=0,e=0){this._element.transition().delay(t).duration(e).style("opacity",1e-6)}get(){return this._element}}class bi{constructor(t){this._element=t.append("defs")}get(){return this._element}}class ki{constructor(t){this._zoom=null,this._parent=t,this.init()}init(){this._zoom=In(),this._zoom.scaleExtent([.1,20]).on("zoom",(t=>{this._parent.attr("transform",t.transform)})),this._zoom.wheelDelta((t=>-t.deltaY*(1===t.deltaMode?.05:t.deltaMode?1:.002))),this._zoom.filter((t=>{if("wheel"===t.type){if(!t.ctrlKey)return!1;const e=Tn(this);if(e.k){if(e.k<=.1&&t.deltaY>0)return t.preventDefault(),!1;if(e.k>=20&&t.deltaY<0)return t.preventDefault(),!1}return!0}return t.button||"touchstart"!==t.type?!(t.ctrlKey&&"wheel"!==t.type||t.button):2===t.touches.length}))}get(){return this._zoom}}class Mi{triggerDownload(t,e){let n=new MouseEvent("click",{view:window,bubbles:!1,cancelable:!0}),i=document.createElement("a");i.setAttribute("download",e),i.setAttribute("href",t),i.setAttribute("target","_blank"),i.dispatchEvent(n)}}class Ai extends Mi{copyStylesInline(t,e){let n=["svg","g","text","textPath"];for(let i=0;i{let r=(new XMLSerializer).serializeToString(t),o=window.URL||window.webkitURL||window,a=new Blob([r],{type:"image/svg+xml;charset=utf-8"}),s=o.createObjectURL(a),h=new Image;h.onload=()=>{let t=this.createCanvas(e,n),r=t.getContext("2d");r.fillStyle="rgb(255,255,255)",r.fillRect(0,0,t.width,t.height),r.drawImage(h,0,0),o.revokeObjectURL(s);let a=t.toDataURL("image/png").replace("image/png","image/octet-stream");i(a)},h.src=s}))}cloneSvg(t){return new Promise((e=>{e(t.cloneNode(!0))}))}svgToImage(t,e){const n=[4960,3508];this.cloneSvg(t.get().node()).then((e=>{this.copyStylesInline(t.get().node(),e);const i=this.calculateViewBox(t.get().node()),r=Math.max(n[0],i[2]),o=Math.max(n[1],i[3]);return e.setAttribute("width",""+r),e.setAttribute("height",""+o),e.setAttribute("viewBox",""+i),this.convertToDataUrl(e,r,o)})).then((t=>this.triggerDownload(t,e))).catch((()=>{console.log("Failed to save chart as PNG image")}))}}class $i extends Mi{copyStylesInline(t,e){return new Promise((n=>{(function(t,e){return fetch(t,e).then(oi)})(t).then((t=>{t=t.replace(/#webtrees-descendants-chart-container /g,"");let i=document.createElementNS("http://www.w3.org/2000/svg","style");i.appendChild(document.createTextNode(t)),e.prepend(i),n(e)}))}))}convertToObjectUrl(t){return new Promise((e=>{let n=(new XMLSerializer).serializeToString(t),i=window.URL||window.webkitURL||window,r=new Blob([n],{type:"image/svg+xml;charset=utf-8"}),o=i.createObjectURL(r),a=new Image;a.onload=()=>{e(o)},a.src=o}))}cloneSvg(t){return new Promise((e=>{e(t.cloneNode(!0))}))}svgToImage(t,e,n){this.cloneSvg(t.get().node()).then((t=>this.copyStylesInline(e,t))).then((t=>this.convertToObjectUrl(t))).then((t=>this.triggerDownload(t,n))).catch((()=>{console.log("Failed to save chart as SVG image")}))}}class Ni{constructor(){this._exportClass=null}setExportClass(t){switch(t){case"png":this._exportClass=Ai;break;case"svg":this._exportClass=$i}}createExport(t){switch(this.setExportClass(t),t){case"png":case"svg":return new this._exportClass}}}class Ti{constructor(t,e){this._element=t.append("svg"),this._defs=new bi(this._element),this._visual=null,this._zoom=null,this._configuration=e,this.init()}init(){this._element.attr("width","100%").attr("height","100%").attr("text-rendering","optimizeLegibility").attr("text-anchor","middle").attr("xmlns:xlink","https://www.w3.org/1999/xlink")}initEvents(t){this._element.on("contextmenu",(t=>t.preventDefault())).on("wheel",(e=>{e.ctrlKey||t.show(this._configuration.labels.zoom,300,(()=>{t.hide(200,600)}))})).on("touchend",(e=>{e.touches.length<2&&t.hide(0,600)})).on("touchmove",(e=>{e.touches.length>=2?t.hide():t.show(this._configuration.labels.move)})).on("click",(t=>this.doStopPropagation(t)),!0),this._configuration.rtl&&this._element.classed("rtl",!0),this._visual=this._element.append("g"),this._zoom=new ki(this._visual),this._element.call(this._zoom.get())}doStopPropagation(t){t.defaultPrevented&&t.stopPropagation()}export(t){return(new Ni).createExport(t)}get defs(){return this._defs}get zoom(){return this._zoom}get visual(){return this._visual}get(){return this._element}}class zi{constructor(t,e){this._configuration=e,this._parent=t,this._hierarchy=new _i(this._configuration),this._data={}}get svg(){return this._svg}updateViewBox(){let t=this._svg.visual.node().getBBox(),e=this._parent.node().getBoundingClientRect(),n=Math.max(e.width,t.width),i=Math.max(e.height,t.height,300),r=(n-t.width)/2,o=(i-t.height)/2,a=Math.ceil(t.x-r-10),s=Math.ceil(t.y-o-10);n=Math.ceil(n+20),i=Math.ceil(i+20),this._svg.get().attr("viewBox",[a,s,n,i])}get data(){return this._data}set data(t){this._data=t,this._hierarchy.init(this._data)}draw(){this._parent.html(""),this._svg=new Ti(this._parent,this._configuration),this._overlay=new wi(this._parent),this._svg.initEvents(this._overlay),new vi(this._svg,this._configuration,this._hierarchy),this.bindClickEventListener(),this.updateViewBox()}bindClickEventListener(){let t=this;this._svg.visual.selectAll("g.person").filter((t=>""!==t.data.data.xref)).each((function(e){dt(this).on("click",(function(){t.personClick(e.data)}))}))}personClick(t){1===t.data.generation?this.redirectToIndividual(t.data.url):this.update(t.data.updateUrl)}redirectToIndividual(t){window.location=t}update(t){window.location=t}}t.DescendantsChart=class{constructor(t,e){this._selector=t,this._parent=dt(this._selector),this._configuration=new gi(e.labels,e.generations,e.treeLayout,e.rtl),this._chart=new zi(this._parent,this._configuration),this.init()}init(){dt("#centerButton").on("click",(()=>this.center())),dt("#exportPNG").on("click",(()=>this.exportPNG())),dt("#exportSVG").on("click",(()=>this.exportSVG()))}center(){this._chart.svg.get().transition().duration(750).call(this._chart.svg.zoom.get().transform,Nn)}get configuration(){return this._configuration}set cssFile(t){this._cssFile=t}update(t){this._chart.update(t)}draw(t){this._chart.data=t,this._chart.draw()}exportPNG(){this._chart.svg.export("png").svgToImage(this._chart.svg,"descendants-chart.png")}exportSVG(){this._chart.svg.export("svg").svgToImage(this._chart.svg,this._cssFile,"descendants-chart.svg")}},Object.defineProperty(t,"__esModule",{value:!0})},"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).WebtreesDescendantsChart={}); diff --git a/resources/js/modules/chart.js b/resources/js/modules/chart.js index 49c6f42..2213bd1 100644 --- a/resources/js/modules/chart.js +++ b/resources/js/modules/chart.js @@ -93,7 +93,7 @@ export default class Chart /** * Returns the chart data. * - * @returns {Object} + * @returns {Data} */ get data() { @@ -103,7 +103,7 @@ export default class Chart /** * Sets the chart data. * - * @param {Object} value The chart data + * @param {Data} value The chart data */ set data(value) { @@ -130,32 +130,8 @@ export default class Chart // Init the events this._svg.initEvents(this._overlay); - let tree = new Tree(this._svg, this._configuration, this._hierarchy); - - // let personGroup = this._svg.get().select("g.personGroup"); - // let gradient = new Gradient(this._svg, this._configuration); - // let that = this; - // - // personGroup - // .selectAll("g.person") - // .data(this._hierarchy.nodes, (d) => d.data.id) - // .enter() - // .append("g") - // .attr("class", "person") - // .attr("id", (d) => "person-" + d.data.id); - // - // // Create a new selection in order to leave the previous enter() selection - // personGroup - // .selectAll("g.person") - // .each(function (d) { - // let person = d3.select(this); - // - // if (that._configuration.showColorGradients) { - // gradient.init(d); - // } - // - // new Person(that._svg, that._configuration, person, d); - // }); + // Create tree + new Tree(this._svg, this._configuration, this._hierarchy); this.bindClickEventListener(); this.updateViewBox(); @@ -170,14 +146,14 @@ export default class Chart this._svg.visual .selectAll("g.person") - .filter((d) => d.data.xref !== "") - .each(function (d) { - d3.select(this).on("click", function() { that.personClick(d.data); }); + .filter(person => person.data.data.xref !== "") + .each(function (person) { + d3.select(this).on("click", function() { that.personClick(person.data); }); }); } /** - * Method triggers either the "update" or "individual" method on the click on an person. + * Method triggers either the "update" or "individual" method on the click on a person. * * @param {Object} data The D3 data object * @@ -186,7 +162,7 @@ export default class Chart personClick(data) { // Trigger either "update" or "redirectToIndividual" method on click depending on person in chart - (data.generation === 1) ? this.redirectToIndividual(data.url) : this.update(data.updateUrl); + (data.data.generation === 1) ? this.redirectToIndividual(data.data.url) : this.update(data.data.updateUrl); } /** @@ -230,10 +206,10 @@ export default class Chart // $.ajax({ // type: 'POST', // url: indSelector.attr("data-ajax--url"), - // data: { q : data.xref } + // data: { q : data.data.xref } // }).then(function (data) { // // create the option and append to Select2 - // var option = new Option(data.results[0].text, data.results[0].id, true, true); + // var option = new Option(data.data.results[0].text, data.data.results[0].id, true, true); // indSelector.append(option).trigger('change'); // }); // }); diff --git a/resources/js/modules/chart/elbow/horizontal.js b/resources/js/modules/chart/elbow/horizontal.js index f23d104..b0df2e0 100644 --- a/resources/js/modules/chart/elbow/horizontal.js +++ b/resources/js/modules/chart/elbow/horizontal.js @@ -5,22 +5,120 @@ * LICENSE file that was distributed with this source code. */ +import * as d3 from "../../d3"; + /** - * Draw the horizontal connecting lines between the profile boxes for Left/Right and Right/Left layout. + * Returns the path to draw the horizontal connecting lines between the profile + * boxes for Left/Right and Right/Left layout. * - * @param {Object} datum D3 data object + * @param {Link} link The link object * @param {Orientation} orientation The current orientation + * + * @returns {String} + * + * Curved edges => https://observablehq.com/@bumbeishvili/curved-edges-horizontal-d3-v3-v4-v5-v6 */ -export default function(datum, orientation) +export default function(link, orientation) { - // Left => Right, Right => Left - const sourceX = datum.source.x + (orientation.direction() * (orientation.boxWidth / 2)), - sourceY = datum.source.y, - targetX = datum.target.x - (orientation.direction() * (orientation.boxWidth / 2)), - targetY = datum.target.y; - - return "M " + sourceX + " " + sourceY + - " H " + (sourceX + ((targetX - sourceX) / 2)) + - " V " + targetY + - " H " + targetX; + const halfXOffset = orientation.xOffset / 2; + const halfYOffset = orientation.yOffset / 2; + + let sourceX = link.source.x, + sourceY = link.source.y; + + if ((typeof link.spouse !== "undefined") && (link.source.data.family === 0)) { + // For the first family, the link to the child nodes begins between + // the individual and the first spouse. + sourceY -= (link.source.y - link.spouse.y) / 2; + } else { + // For each additional family, the link to the child nodes begins at the additional spouse. + sourceX += (orientation.boxWidth / 2) * orientation.direction(); + } + + // No spouse assigned to source node + if (link.source.data.data === null) { + sourceX += (orientation.boxWidth / 2) * orientation.direction(); + sourceY -= (orientation.boxHeight / 2) + (halfYOffset / 2); + } + + if (link.target !== null) { + let targetX = link.target.x - (orientation.direction() * ((orientation.boxWidth / 2) + halfXOffset)), + targetY = link.target.y; + + const path = d3.path(); + + // The line from source/spouse to target + path.moveTo(sourceX, sourceY); + path.lineTo(targetX, sourceY); + path.lineTo(targetX, targetY); + path.lineTo(targetX + (orientation.direction() * halfXOffset), targetY); + + return path.toString(); + } + + return createLinksBetweenSpouses(link, orientation); +} + +/** + * Returns the path needed to draw the lines between each spouse. + * + * @param {Link} link The link object + * @param {Orientation} orientation The current orientation + * + * @return {String} + */ +function createLinksBetweenSpouses(link, orientation) +{ + const path = d3.path(); + + // The distance between the connecting lines when there are multiple spouses + const spouseLineOffset = 5; + + // The distance from the line to the node. Causes the line to stop or begin just before the node, + // instead of going straight to the node, so that the connection to another spouse is clearer. + const lineStartOffset = 2; + + let sourceX = link.source.x; + + // Handle multiple spouses + if (link.source.data.family > 0) { + sourceX = link.spouse.x - (link.source.data.family * orientation.direction() * spouseLineOffset); + } + + // Add link between first spouse and source + if (link.coords === null) { + path.moveTo(sourceX, link.spouse.y + (orientation.boxHeight / 2)); + path.lineTo(sourceX, link.source.y - (orientation.boxHeight / 2)); + } + + // Append lines between source and all spouses + if (link.coords && (link.coords.length > 0)) { + for (let i = 0; i < link.coords.length; ++i) { + let startY = link.spouse.y + (orientation.boxHeight / 2); + let endY = link.coords[i].y - (orientation.boxHeight / 2); + + if (i > 0) { + startY = link.coords[i - 1].y + (orientation.boxHeight / 2); + } + + let startPosOffset = ((i > 0) ? lineStartOffset : 0); + let endPosOffset = (((i + 1) <= link.coords.length) ? lineStartOffset : 0); + + path.moveTo(sourceX, startY + startPosOffset); + path.lineTo(sourceX, endY - endPosOffset); + } + + // Add last part from previous spouse to actual spouse + path.moveTo( + sourceX, + link.coords[link.coords.length - 1].y + (orientation.boxHeight / 2) + lineStartOffset + ); + + path.lineTo( + sourceX, + link.source.y - (orientation.boxHeight / 2) + ); + } + + return path.toString(); } diff --git a/resources/js/modules/chart/elbow/vertical.js b/resources/js/modules/chart/elbow/vertical.js index 7cda187..c8c48d6 100644 --- a/resources/js/modules/chart/elbow/vertical.js +++ b/resources/js/modules/chart/elbow/vertical.js @@ -5,22 +5,118 @@ * LICENSE file that was distributed with this source code. */ +import * as d3 from "../../d3"; + +/** + * Returns the path to draw the vertical connecting lines between the profile + * boxes for Top/Bottom and Bottom/Top layout. + * + * @param {Link} link The link object + * @param {Orientation} orientation The current orientation + * + * @returns {String} + */ +export default function(link, orientation) +{ + const halfXOffset = orientation.xOffset / 2; + const halfYOffset = orientation.yOffset / 2; + + let sourceX = link.source.x, + sourceY = link.source.y; + + if ((typeof link.spouse !== "undefined") && (link.source.data.family === 0)) { + // For the first family, the link to the child nodes begins between + // the individual and the first spouse. + sourceX -= (link.source.x - link.spouse.x) / 2; + } else { + // For each additional family, the link to the child nodes begins at the additional spouse. + sourceY += (orientation.boxHeight / 2) * orientation.direction(); + } + + // No spouse assigned to source node + if (link.source.data.data === null) { + sourceX -= (orientation.boxWidth / 2) + (halfXOffset / 2); + sourceY += (orientation.boxHeight / 2) * orientation.direction(); + } + + if (link.target !== null) { + let targetX = link.target.x, + targetY = link.target.y - (orientation.direction() * ((orientation.boxHeight / 2) + halfYOffset)); + + const path = d3.path(); + + // The line from source/spouse to target + path.moveTo(sourceX, sourceY); + path.lineTo(sourceX, targetY); + path.lineTo(targetX, targetY); + path.lineTo(targetX, targetY + (orientation.direction() * halfYOffset)); + + return path.toString(); + } + + return createLinksBetweenSpouses(link, orientation); +} + /** - * Draw the vertical connecting lines between the profile boxes for Top/Bottom and Bottom/Top layout. + * Returns the path needed to draw the lines between each spouse. * - * @param {Object} datum D3 data object + * @param {Link} link The link object * @param {Orientation} orientation The current orientation + * + * @return {String} */ -export default function(datum, orientation) +function createLinksBetweenSpouses(link, orientation) { - // Top => Bottom, Bottom => Top - const sourceX = datum.source.x, - sourceY = datum.source.y + (orientation.direction() * (orientation.boxHeight / 2)), - targetX = datum.target.x, - targetY = datum.target.y - (orientation.direction() * (orientation.boxHeight / 2)); - - return "M " + sourceX + " " + sourceY + - " V " + (sourceY + ((targetY - sourceY) / 2)) + - " H " + targetX + - " V " + targetY; + const path = d3.path(); + + // The distance between the connecting lines when there are multiple spouses + const spouseLineOffset = 5; + + // The distance from the line to the node. Causes the line to stop or begin just before the node, + // instead of going straight to the node, so that the connection to another spouse is clearer. + const lineStartOffset = 2; + + let sourceY = link.source.y; + + // Handle multiple spouses + if (link.source.data.family > 0) { + sourceY = link.spouse.y - (link.source.data.family * orientation.direction() * spouseLineOffset); + } + + // Add link between first spouse and source + if (link.coords === null) { + path.moveTo(link.spouse.x + (orientation.boxWidth / 2), sourceY); + path.lineTo(link.source.x - (orientation.boxWidth / 2), sourceY); + } + + // Append lines between source and all spouses + if (link.coords && (link.coords.length > 0)) { + for (let i = 0; i < link.coords.length; ++i) { + let startX = link.spouse.x + (orientation.boxWidth / 2); + let endX = link.coords[i].x - (orientation.boxWidth / 2); + + if (i > 0) { + startX = link.coords[i - 1].x + (orientation.boxWidth / 2); + } + + let startPosOffset = ((i > 0) ? lineStartOffset : 0); + let endPosOffset = (((i + 1) <= link.coords.length) ? lineStartOffset : 0); + + path.moveTo(startX + startPosOffset, sourceY); + path.lineTo(endX - endPosOffset, sourceY); + } + + // Add last part from previous spouse to actual spouse + path.moveTo( + link.coords[link.coords.length - 1].x + (orientation.boxWidth / 2) + lineStartOffset, + sourceY + ); + + path.lineTo( + link.source.x - (orientation.boxWidth / 2), + sourceY + ); + } + + return path.toString(); } diff --git a/resources/js/modules/chart/hierarchy.js b/resources/js/modules/chart/hierarchy.js index 6931115..62ebe7f 100644 --- a/resources/js/modules/chart/hierarchy.js +++ b/resources/js/modules/chart/hierarchy.js @@ -35,77 +35,65 @@ export default class Hierarchy */ init(data) { - // Get the greatest depth - const getDepth = ({children}) => 1 + (children ? Math.max(...children.map(getDepth)) : 0); - // const maxGenerations = getDepth(data); - // Construct root node from the hierarchical data - let root = d3.hierarchy( - data, - data => { - return data.children; - }); + let root = d3.hierarchy(data); // Declares a tree layout and assigns the size const treeLayout = d3.tree() .nodeSize([this._configuration.orientation.nodeWidth, 0]) - .separation(() => 0.5); + .separation((left, right) => this.separation(left, right)); - // Map the node data to the tree layout + // Map the root node data to the tree layout this._root = root; this._nodes = treeLayout(root); } /** - * Returns the nodes. + * Returns the separation value. * - * @returns {Array} + * @param {Individual} left + * @param {Individual} right * - * @public + * @return {Number} */ - get nodes() + separation(left, right) { - return this._nodes; + // The left child has spouses (1 or more), add some space between the nodes + if (typeof left.data.spouses !== "undefined") { + return 0.75; + } + + // The right side is a spouse which is linked back to the actual child, so add some space + if (typeof right.data.spouse !== "undefined") { + return 0.75; + } + + // Single siblings and cousins should be close + // to each other if parents are the same + return left.parent === right.parent ? 0.5 : 0.75; } /** - * Returns the root note. + * Returns the nodes. * - * @returns {Object} + * @returns {Individual} * * @public */ - get root() + get nodes() { - return this._root; + return this._nodes; } /** - * Create an empty child node object. - * - * @param {Number} generation Generation of the node - * @param {String} sex The sex of the individual + * Returns the root note. * - * @returns {Object} + * @returns {Individual} * - * @private + * @public */ - createEmptyNode(generation, sex) + get root() { - return { - id : 0, - xref : "", - url : "", - updateUrl : "", - generation : generation, - name : "", - firstNames : [], - lastNames : [], - preferredName : "", - alternativeNames : [], - isAltRtl : false, - sex : sex, - timespan : "" - }; + return this._root; } } diff --git a/resources/js/modules/chart/orientation/orientation-bottomTop.js b/resources/js/modules/chart/orientation/orientation-bottomTop.js index 1991174..ef43f38 100644 --- a/resources/js/modules/chart/orientation/orientation-bottomTop.js +++ b/resources/js/modules/chart/orientation/orientation-bottomTop.js @@ -37,16 +37,16 @@ export default class OrientationBottomTop extends Orientation get nodeWidth() { - return (this._boxWidth * 2) + 30; + return (this._boxWidth * 2) + this._yOffset; } norm(d) { - d.y = this.direction() * d.depth * (this._boxHeight + 30); + d.y = this.direction() * d.depth * (this._boxHeight + this._yOffset); } - elbow(d) + elbow(link) { - return elbowVertical(d, this); + return elbowVertical(link, this); } } diff --git a/resources/js/modules/chart/orientation/orientation-leftRight.js b/resources/js/modules/chart/orientation/orientation-leftRight.js index 84b62fa..71942eb 100644 --- a/resources/js/modules/chart/orientation/orientation-leftRight.js +++ b/resources/js/modules/chart/orientation/orientation-leftRight.js @@ -26,6 +26,9 @@ export default class OrientationLeftRight extends Orientation constructor(boxWidth, boxHeight) { super(boxWidth, boxHeight); + + this._xOffset = 40; + this._yOffset = 40; } direction() @@ -35,20 +38,18 @@ export default class OrientationLeftRight extends Orientation get nodeWidth() { - return (this._boxHeight * 2) + 30; + return (this._boxHeight * 2) + this._xOffset; } norm(d) { - const oldX = d.x; - // Swap x and y values - d.x = this.direction() * d.depth * (this._boxWidth + 30); - d.y = oldX; + d.y = d.x; + d.x = this.direction() * d.depth * (this._boxWidth + this._xOffset); } - elbow(d) + elbow(link) { - return elbowHorizontal(d, this); + return elbowHorizontal(link, this); } } diff --git a/resources/js/modules/chart/orientation/orientation-rightLeft.js b/resources/js/modules/chart/orientation/orientation-rightLeft.js index 60e124a..9d7e535 100644 --- a/resources/js/modules/chart/orientation/orientation-rightLeft.js +++ b/resources/js/modules/chart/orientation/orientation-rightLeft.js @@ -26,6 +26,9 @@ export default class OrientationRightLeft extends Orientation constructor(boxWidth, boxHeight) { super(boxWidth, boxHeight); + + this._xOffset = 40; + this._yOffset = 40; } direction() @@ -35,20 +38,18 @@ export default class OrientationRightLeft extends Orientation get nodeWidth() { - return (this._boxHeight * 2) + 30; + return (this._boxHeight * 2) + this._xOffset; } norm(d) { - const oldX = d.x; - // Swap x and y values - d.x = this.direction() * d.depth * (this._boxWidth + 30); - d.y = oldX; + d.y = d.x; + d.x = this.direction() * d.depth * (this._boxWidth + this._xOffset); } - elbow(d) + elbow(link) { - return elbowHorizontal(d, this); + return elbowHorizontal(link, this); } } diff --git a/resources/js/modules/chart/orientation/orientation-topBottom.js b/resources/js/modules/chart/orientation/orientation-topBottom.js index 405b044..86dc772 100644 --- a/resources/js/modules/chart/orientation/orientation-topBottom.js +++ b/resources/js/modules/chart/orientation/orientation-topBottom.js @@ -37,16 +37,16 @@ export default class OrientationTopBottom extends Orientation get nodeWidth() { - return (this._boxWidth * 2) + 30; + return (this._boxWidth * 2) + this._yOffset; } norm(d) { - d.y = this.direction() * d.depth * (this._boxHeight + 30); + d.y = this.direction() * d.depth * (this._boxHeight + this._yOffset); } - elbow(d) + elbow(link) { - return elbowVertical(d, this); + return elbowVertical(link, this); } } diff --git a/resources/js/modules/chart/orientation/orientation.js b/resources/js/modules/chart/orientation/orientation.js index ea86de7..312de6b 100644 --- a/resources/js/modules/chart/orientation/orientation.js +++ b/resources/js/modules/chart/orientation/orientation.js @@ -22,11 +22,35 @@ export default class Orientation */ constructor(boxWidth, boxHeight) { + // The distance between single nodes + this._xOffset = 60; + this._yOffset = 60; + this._boxWidth = boxWidth; this._boxHeight = boxHeight; this._splittNames = false; } + /** + * Returns the x-offset between two boxes. + * + * @returns {Number} + */ + get xOffset() + { + return this._xOffset; + } + + /** + * Returns the y-offset between two boxes. + * + * @returns {Number} + */ + get yOffset() + { + return this._yOffset; + } + /** * Returns whether to splitt the names on multiple lines or not. * @@ -79,6 +103,8 @@ export default class Orientation /** * Normalizes the x and/or y values of an entry. + * + * @param {Individual} d */ norm(d) { @@ -88,9 +114,11 @@ export default class Orientation /** * Returns the elbow function depending on the orientation. * + * @param {Link} link + * * @returns {String} */ - elbow(d) + elbow(link) { throw "Abstract method elbow() not implemented"; } diff --git a/resources/js/modules/chart/svg.js b/resources/js/modules/chart/svg.js index 36da00f..d50d37e 100644 --- a/resources/js/modules/chart/svg.js +++ b/resources/js/modules/chart/svg.js @@ -70,14 +70,14 @@ export default class Svg this._configuration.labels.zoom, 300, () => { - overlay.hide(700, 800); + overlay.hide(200, 600); } ); } }) .on("touchend", (event) => { if (event.touches.length < 2) { - overlay.hide(0, 800); + overlay.hide(0, 600); } }) .on("touchmove", (event) => { diff --git a/resources/js/modules/chart/svg/export.js b/resources/js/modules/chart/svg/export.js index 88a0598..8833932 100644 --- a/resources/js/modules/chart/svg/export.js +++ b/resources/js/modules/chart/svg/export.js @@ -15,7 +15,7 @@ export default class Export { /** - * Triggers the download by creating a new anchor element an simulate a mouse click on it. + * Triggers the download by creating a new anchor element and simulate a mouse click on it. * * @param {String} imgURI The image URI data stream * @param {String} fileName The file name to use in the download dialog diff --git a/resources/js/modules/chart/svg/export/png.js b/resources/js/modules/chart/svg/export/png.js index c828f69..a471a04 100644 --- a/resources/js/modules/chart/svg/export/png.js +++ b/resources/js/modules/chart/svg/export/png.js @@ -20,35 +20,27 @@ export default class PngExport extends Export * Copies recursively all the styles from the list of container elements from the source * to the destination node. * - * @param {SVGGraphicsElement} sourceNode - * @param {SVGGraphicsElement} destinationNode + * @param {Element} sourceNode + * @param {Element} destinationNode */ copyStylesInline(sourceNode, destinationNode) { - return new Promise(resolve => { - let containerElements = ["svg", "g", "text", "textPath"]; - - for (let i = 0; i < destinationNode.childNodes.length; ++i) { - let child = destinationNode.childNodes[i]; + let containerElements = ["svg", "g", "text", "textPath"]; - if (containerElements.indexOf(child.tagName) !== -1) { - this.copyStylesInline(sourceNode.childNodes[i], child); - continue; - } + for (let i = 0; i < destinationNode.children.length; ++i) { + let element = destinationNode.children[i]; - let computedStyle = window.getComputedStyle(sourceNode.childNodes[i]); + if (containerElements.indexOf(element.tagName) !== -1) { + this.copyStylesInline(sourceNode.children[i], element); + continue; + } - if (computedStyle === null) { - continue; - } + let computedStyle = window.getComputedStyle(sourceNode.children[i]); - for (let j = 0; j < computedStyle.length; ++j) { - child.style.setProperty(computedStyle[j], computedStyle.getPropertyValue(computedStyle[j])); - } + for (let j = 0; j < computedStyle.length; ++j) { + element.style.setProperty(computedStyle[j], computedStyle.getPropertyValue(computedStyle[j])); } - - resolve(destinationNode); - }) + } } /** @@ -156,29 +148,29 @@ export default class PngExport extends Export //let scale = 300 / dpi(); // Paper sizes (width, height) in pixel at 300 DPI/PPI - // const paperSize = { - // 'A3': [4960, 3508], - // 'A4': [3508, 2480], - // 'A5': [2480, 1748] - // }; + const paperSize = { + 'A3': [4960, 3508], + 'A4': [3508, 2480], + 'A5': [2480, 1748] + }; this.cloneSvg(svg.get().node()) .then(newSvg => { this.copyStylesInline(svg.get().node(), newSvg); const viewBox = this.calculateViewBox(svg.get().node()); - const width = viewBox[2]; - const height = viewBox[3]; - - newSvg.setAttribute("width", width); - newSvg.setAttribute("height", height); - newSvg.setAttribute("viewBox", viewBox); - - this.convertToDataUrl(newSvg, width, height) - .then(imgURI => this.triggerDownload(imgURI, fileName)) - .catch(() => { - console.log("Failed to save chart as PNG image"); - }); + const width = Math.max(paperSize['A3'][0], viewBox[2]); + const height = Math.max(paperSize['A3'][1], viewBox[3]); + + newSvg.setAttribute("width", "" + width); + newSvg.setAttribute("height", "" + height); + newSvg.setAttribute("viewBox", "" + viewBox); + + return this.convertToDataUrl(newSvg, width, height); + }) + .then(imgURI => this.triggerDownload(imgURI, fileName)) + .catch(() => { + console.log("Failed to save chart as PNG image"); }); } } diff --git a/resources/js/modules/chart/svg/zoom.js b/resources/js/modules/chart/svg/zoom.js index db28eac..a41f528 100644 --- a/resources/js/modules/chart/svg/zoom.js +++ b/resources/js/modules/chart/svg/zoom.js @@ -67,7 +67,7 @@ export default class Zoom return false; } - var transform = d3.zoomTransform(this); + const transform = d3.zoomTransform(this); if (transform.k) { // Prevent zooming below lowest level diff --git a/resources/js/modules/chart/update.js b/resources/js/modules/chart/update.js index 7df2636..6fda75e 100644 --- a/resources/js/modules/chart/update.js +++ b/resources/js/modules/chart/update.js @@ -54,7 +54,7 @@ export default class Update ).then((data) => { // Initialize the new loaded data this._hierarchy.init(data); - that.draw(); + this.draw(); let indSelector = $(document.getElementById("xref")); diff --git a/resources/js/modules/common/dpi.js b/resources/js/modules/common/dpi.js index f6916e6..021c157 100644 --- a/resources/js/modules/common/dpi.js +++ b/resources/js/modules/common/dpi.js @@ -12,8 +12,8 @@ */ export default function() { - const element = document.createElement('div'); - element.style = 'width: 1in;' + const element = document.createElement("div"); + element.style.width = "1in"; document.body.appendChild(element); const offsetWidth = element.offsetWidth; diff --git a/resources/js/modules/constants.js b/resources/js/modules/constants.js index bb801c8..e4f4f5a 100644 --- a/resources/js/modules/constants.js +++ b/resources/js/modules/constants.js @@ -9,6 +9,7 @@ * The widths and heights of a single node in each tree layout. * * @type {Number} + * @const */ export const LAYOUT_HORIZONTAL_NODE_WIDTH = 300; export const LAYOUT_HORIZONTAL_NODE_HEIGHT = 80; @@ -19,6 +20,7 @@ export const LAYOUT_VERTICAL_NODE_HEIGHT = 175; * Tree layout variants. * * @type {String} + * @const * * @see \Fisharebest\Webtrees\Module\PedigreeChartModule */ @@ -31,6 +33,7 @@ export const LAYOUT_RIGHTLEFT = "left"; * Gender types. * * @type {String} + * @const */ export const SEX_MALE = "M"; export const SEX_FEMALE = "F"; diff --git a/resources/js/modules/d3.js b/resources/js/modules/d3.js index 15ce5f8..be0de43 100644 --- a/resources/js/modules/d3.js +++ b/resources/js/modules/d3.js @@ -8,12 +8,11 @@ /* https://github.com/d3/d3-selection https://github.com/d3/d3-transition -https://github.com/d3/d3-scale https://github.com/d3/d3-zoom https://github.com/d3/d3-hierarchy +https://github.com/d3/d3-path https://github.com/d3/d3-shape https://github.com/d3/d3-fetch -https://github.com/d3/d3-timer */ export { @@ -24,24 +23,20 @@ export { transition } from "d3-transition"; -export { - scaleLinear -} from "d3-scale"; - export * from "d3-zoom"; export { - hierarchy, partition, tree + Node, hierarchy, tree } from "d3-hierarchy"; export { - arc + path +} from "d3-path"; + +export { + line } from "d3-shape"; export { json, text } from "d3-fetch"; - -export { - timer -} from "d3-timer"; diff --git a/resources/js/modules/data.js b/resources/js/modules/data.js new file mode 100644 index 0000000..eaa5e03 --- /dev/null +++ b/resources/js/modules/data.js @@ -0,0 +1,71 @@ +/** + * This file is part of the package magicsunday/webtrees-descendants-chart. + * + * For the full copyright and license information, please read the + * LICENSE file that was distributed with this source code. + */ + +/** + * This files defines the internal used structures of objects. + * + * @author Rico Sonntag + * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 + * @link https://github.com/magicsunday/webtrees-descendants-chart/ + */ + +import {Node} from "./d3"; + +/** + * The plain person data. + * + * @typedef {Object} Data + * @property {String} xref The unique identifier of the person + * @property {String} sex The sex of the person + * @property {String} birth The birthdate of the person + * @property {String} death The death date of the person + * @property {String} timespan The lifetime description + * @property {String} thumbnail The URL of the thumbnail image + * @property {String} name The full name of the individual + * @property {String} preferredName The preferred first name + * @property {String[]} firstNames The list of first names + * @property {String[]} lastNames The list of last names + */ + +/** + * A person object. + * + * @typedef {Object} Person + * @property {null|Data} data The data object of the individual + * @property {undefined|String[]} spouses The list of assigned spouses (not available if "spouse" is set) + * @property {undefined|Object[]} children The list of children of this individual + * @property {undefined|Number} family The family index (0 = first family, 1 = second, ...) + * @property {undefined|String} spouse The XREF of the direct spouse of this individual + */ + +/** + * An individual. Extends the D3 Node object. + * + * @typedef {Node} Individual + * @property {Person} data The individual data + * @property {Individual[]} children The children of the node + * @property {Number} x The X-coordinate of the node + * @property {Number} y The Y-coordinate of the node + */ + +/** + * An X/Y coordinate. + * + * @typedef {Object} Coordinate + * @property {Number} x The X-coordinate + * @property {Number} y The Y-coordinate + */ + +/** + * A link between two nodes. + * + * @typedef {Object} Link + * @property {Individual} source The source individual + * @property {null|Individual} target The target individual + * @property {null|undefined|Individual} spouse The spouse of the source individual + * @property {null|Coordinate[]} coords The list of the spouse coordinates + */ diff --git a/resources/js/modules/index.js b/resources/js/modules/index.js index a40cbf0..bb5bcee 100644 --- a/resources/js/modules/index.js +++ b/resources/js/modules/index.js @@ -103,9 +103,12 @@ export class DescendantsChart /** * Updates the chart. * - * @param {Object} url The update url + * @param {String} url The update url */ - update(url) { this._chart.update(url); } + update(url) + { + this._chart.update(url); + } /** * Draws the chart. diff --git a/resources/js/modules/storage.js b/resources/js/modules/storage.js index 49a106b..0e8fd07 100644 --- a/resources/js/modules/storage.js +++ b/resources/js/modules/storage.js @@ -26,9 +26,9 @@ export class Storage } /** - * Register a HTML element. + * Register an HTML element. * - * @param {String} name The id or name of a HTML element + * @param {String} name The id or name of an HTML element */ register(name) { @@ -52,7 +52,7 @@ export class Storage } /** - * This methods stores the value of an input element depending on its type. + * This method stores the value of an input element depending on its type. * * @param {EventTarget|HTMLInputElement} element The HTML input element */ @@ -68,7 +68,7 @@ export class Storage /** * Returns the stored value belonging to the HTML element id. * - * @param {String} name The id or name of a HTML element + * @param {String} name The id or name of an HTML element * * @returns {String|Boolean|Number} */ @@ -80,7 +80,7 @@ export class Storage /** * Stores a value to the given HTML element id. * - * @param {String} name The id or name of a HTML element + * @param {String} name The id or name of an HTML element * @param {String|Boolean|Number} value The value to store */ write(name, value) diff --git a/resources/js/modules/tree.js b/resources/js/modules/tree.js index beab42c..8d614cf 100644 --- a/resources/js/modules/tree.js +++ b/resources/js/modules/tree.js @@ -9,6 +9,8 @@ import * as d3 from "./d3"; import dataUrl from "./common/dataUrl"; import {SEX_FEMALE, SEX_MALE} from "./constants"; import Box from "./chart/box"; +import OrientationLeftRight from "./chart/orientation/orientation-leftRight"; +import OrientationRightLeft from "./chart/orientation/orientation-rightLeft"; /** * The class handles the creation of the tree. @@ -46,34 +48,178 @@ export default class Tree /** * Draw the tree. * - * @param {Object} source The root object + * @param {Individual} source The root object * * @public */ draw(source) { + /** @type {Individual[]} */ let nodes = this._hierarchy.nodes.descendants(); - let links = this._hierarchy.nodes.links(); + let links = []; - // // Start with only the first few generations of ancestors showing - // nodes.forEach((person) => { - // if (person.children) { - // person.children.forEach((child) => this.collapse(child)); - // } - // }); + // Remove the pseudo root node + nodes.shift(); + + // Normalize for fixed-depth + nodes.forEach((individual) => { + this._orientation.norm(individual); + }); + + // Arrange the position of the first spouses so that they are close to each other + nodes.forEach((node) => { + if (node.data && node.data.spouse) { + const spouse = nodes.find(individual => individual.data.data.xref === node.data.spouse); + + if ((this._orientation instanceof OrientationLeftRight) + || (this._orientation instanceof OrientationRightLeft) + ) { + const diffY = (((node.y - spouse.y) - this._orientation.boxHeight) - (this._orientation.yOffset / 2)); + + // Only the first family + if ((diffY !== 0) && (node.data.family === 0)) { + node.y -= diffY / 2; + spouse.y += diffY / 2; + } + } else { + const diffX = (((node.x - spouse.x) - this._orientation.boxWidth) - (this._orientation.xOffset / 2)); + + // Only the first family + if ((diffX !== 0) && (node.data.family === 0)) { + node.x -= diffX / 2; + spouse.x += diffX / 2; + } + } + } + }); + + // Find the first node with children + const firstNodeWithChildren = nodes.find(node => node.children && (node.children.length > 0)); + + if (typeof firstNodeWithChildren !== "undefined") { + firstNodeWithChildren.each((node) => { + if ( + (typeof node.data.spouse !== "undefined") + && (node.data.spouse !== null) + && node.children + && (node.children.length >= 1) + ) { + let moveBy = 0; + + if ((this._orientation instanceof OrientationLeftRight) + || (this._orientation instanceof OrientationRightLeft) + ) { + moveBy = (this._orientation.boxHeight / 2) + (this._orientation.yOffset / 4); + } else { + moveBy = (this._orientation.boxWidth / 2) + (this._orientation.xOffset / 4); + } + + this.moveChildren(node, moveBy); + } + }); + } + + // Create list of links between source (node and spouses) and target nodes (children). + nodes.forEach((node) => { + const spouse = nodes.find(individual => individual.data.data.xref === node.data.spouse); + + if (node.children) { + node.children.forEach((child) => { + // Only add links between real children + if ((typeof child.data.spouse === "undefined") || (child.data.spouse === null)) { + links.push({ + spouse: spouse, + source: node, + target: child, + coords: null + }); + } + }); + } - // Normalize for fixed-depth. - nodes.forEach((person) => { - this._orientation.norm(person); + if (typeof spouse !== "undefined") { + let spousesCoords = null; + + // In order to draw only the respective intermediate lines, we need the information + // about the position of the previous spouses in the row. The coordinates are attached + // to the respective link as additional values so that they are available later when + // calculating the line points. + if ((typeof spouse.data.spouses !== "undefined") && (spouse.data.spouses.length > 0)) { + const indexOfSpouse = spouse.data.spouses.indexOf(node.data.data.xref); + const spousesBefore = spouse.data.spouses.slice(0, indexOfSpouse); + + if (spousesBefore.length > 0) { + spousesCoords = []; + + spousesBefore.forEach((xref) => { + // Find matching spouse in list of all nodes + const spouseBefore = nodes.find(individual => individual.data.data.xref === xref); + + // Keep track of the coordinates + spousesCoords.push({ + x: spouseBefore.x, + y: spouseBefore.y + }) + }); + } + } + + if (node.data.data !== null) { + // Add link between individual and spouse + links.push({ + spouse: spouse, + source: node, + target: null, + coords: spousesCoords + }); + } + } }); + // // Start with only the first few generations of descendants showing + // nodes.forEach((individual) => { + // if (individual.children) { + // individual.children.forEach((child) => this.collapse(child)); + // } + // }); + this.drawLinks(links, source); this.drawNodes(nodes, source); // Stash the old positions for transition. - nodes.forEach((person) => { - person.x0 = person.x; - person.y0 = person.y; + nodes.forEach((individual) => { + individual.x0 = individual.x; + individual.y0 = individual.y; + }); + } + + /** + * Moves all child nodes by the specified amount. + * + * @param {Individual} individual The individual whose children are to be moved + * @param {Number} moveBy The amount by which to move the child nodes + */ + moveChildren(individual, moveBy) + { + individual.each((child) => { + if (child.depth !== individual.depth) { + // - first family only + // - if more than one child + // - if child has children too + if ( + (individual.data.family === 0) + || (individual.children.length !== 1) + || (typeof child.children !== "undefined") + ) { + if ((this._orientation instanceof OrientationLeftRight) + || (this._orientation instanceof OrientationRightLeft) + ) { + child.y -= moveBy; + } else { + child.x -= moveBy; + } + } + } }); } @@ -107,8 +253,8 @@ export default class Tree /** * Draw the person boxes. * - * @param {Array} nodes Array of descendant nodes - * @param {Object} source The root object + * @param {Individual[]} nodes Array of descendant nodes + * @param {Object} source The root object * * @private */ @@ -141,99 +287,38 @@ export default class Tree let nodeEnter = node .enter() - .append("g") - .attr("class", "person") - // Add new nodes on the right side of their child's this._box. - // They will be transitioned into their proper position. - // .attr("transform", person => { - // return "translate(" + (this._configuration.direction * (source.y0 + (this._box.width / 2))) + ',' + source.x0 + ")"; - // }) - // .attr("transform", person => { - // return "translate(" + (this._configuration.direction * (source.y + (this._box.width / 2))) + ',' + source.x + ")"; - // }) - // .attr("transform", person => `translate(${source.y0}, ${source.x0})`) - .attr("transform", person => { - return "translate(" + person.x + "," + person.y + ")"; - }) - ; - - // Draw the rectangle person boxes. Start new boxes with 0 size so that we can - // transition them to their proper size. - nodeEnter + .append("g"); + + // Add person block + const personBlock = nodeEnter + .append('g') + .attr("class", person => "person" + (person.data.spouse ? " spouse" : "")) + .attr("transform", person => "translate(" + (person.x) + "," + (person.y) + ")"); + + // Draw the actual person rectangle with an opacity of 0.5 + personBlock .append("rect") - .attr("class", d => (d.data.sex === SEX_FEMALE) ? "female" : (d.data.sex === SEX_MALE) ? "male" : "unknown") + .attr( + "class", + person => (person.data.data.sex === SEX_FEMALE) + ? "female" + : (person.data.data.sex === SEX_MALE) ? "male" : "unknown" + ) .attr("rx", this._box.rx) .attr("ry", this._box.ry) .attr("x", this._box.x) .attr("y", this._box.y) .attr("width", this._box.width) .attr("height", this._box.height) - .attr("fill-opacity", 0.5) + .attr("fill-opacity", 0.5); // Names and Dates - nodeEnter - .filter(d => (d.data.xref !== "")) - .each(function (d) { + personBlock + .filter(person => person.data.data.xref !== "") + .each(function (person) { let element = d3.select(this); - element - .append("title") - .text(d => d.data.name); - - const imageUrlToLoad = that.getImageToLoad(d); - - // Check if image should be shown or hidden - that._box.showImage = !!imageUrlToLoad; - - if (that._box.showImage) { - let group = element - .append("g") - .attr("class", "image"); - - // Background of image (only required if thumbnail has transparency (like the silhouettes)) - group - .append("rect") - .attr("rx", that._box.image.rx) - .attr("ry", that._box.image.ry) - .attr("x", that._box.image.x) - .attr("y", that._box.image.y) - .attr("width", that._box.image.width) - .attr("height", that._box.image.height) - .attr("fill", "rgb(255, 255, 255)"); - - // The individual image - let image = group - .append("image") - .attr("x", that._box.image.x) - .attr("y", that._box.image.y) - .attr("width", that._box.image.width) - .attr("height", that._box.image.height) - .attr("clip-path", "url(#clip-image)"); - - dataUrl(imageUrlToLoad) - .then(dataUrl => image.attr("xlink:href", dataUrl)) - .catch((exception) => { - console.error(exception); - }); - - // Border around image - group - .append("rect") - .attr("rx", that._box.image.rx) - .attr("ry", that._box.image.ry) - .attr("x", that._box.image.x) - .attr("y", that._box.image.y) - .attr("width", that._box.image.width) - .attr("height", that._box.image.height) - .attr("fill", "none") - .attr("stroke", "rgb(200, 200, 200)") - .attr("stroke-width", 1.5); - } - - that.addNames(element, d); - that.addDates(element, d); - - that._box.showImage = true; + that.drawText(element, person.data); }); // // Merge the update and the enter selections @@ -410,11 +495,81 @@ export default class Tree // ; // // return; + } + /** + * Draws the image and text nodes. + * + * @param {selection} parent The parent element to which the elements are to be attached + * @param {Person} person The person object containing the individual data + */ + drawText(parent, person) + { + parent + .append("title") + .text(person.data.name); + + const imageUrlToLoad = this.getImageToLoad(person); + + // Check if image should be shown or hidden + this._box.showImage = !!imageUrlToLoad; + + if (this._box.showImage) { + let group = parent + .append("g") + .attr("class", "image"); + + // Background of image (only required if thumbnail has transparency (like the silhouettes)) + group + .append("rect") + .attr("rx", this._box.image.rx) + .attr("ry", this._box.image.ry) + .attr("x", this._box.image.x) + .attr("y", this._box.image.y) + .attr("width", this._box.image.width) + .attr("height", this._box.image.height) + .attr("fill", "rgb(255, 255, 255)"); + + // The individual image + let image = group + .append("image") + .attr("x", this._box.image.x) + .attr("y", this._box.image.y) + .attr("width", this._box.image.width) + .attr("height", this._box.image.height) + .attr("clip-path", "url(#clip-image)"); + + dataUrl(imageUrlToLoad) + .then(dataUrl => image.attr("xlink:href", dataUrl)) + .catch((exception) => { + console.error(exception); + }); + + // Border around image + group + .append("rect") + .attr("rx", this._box.image.rx) + .attr("ry", this._box.image.ry) + .attr("x", this._box.image.x) + .attr("y", this._box.image.y) + .attr("width", this._box.image.width) + .attr("height", this._box.image.height) + .attr("fill", "none") + .attr("stroke", "rgb(200, 200, 200)") + .attr("stroke-width", 1.5); + } + + this.addNames(parent, person); + this.addDates(parent, person); + + this._box.showImage = true; } /** * Update a person's state when they are clicked. + * + * @param {Event} event + * @param {Person} person The person object containing the individual data */ togglePerson(event, person) { @@ -442,6 +597,8 @@ export default class Tree * expanded it will only reveal one generation. If we don't recursively collapse the ancestors then when * the person is clicked on again to expand, all ancestors that were previously showing will be shown again. * If you want that behavior then just remove the recursion by removing the if block. + * + * @param person */ collapse(person) { @@ -466,19 +623,19 @@ export default class Tree * additional underline style in order to highlight this one. * * @param {selection} parent The parent ( or ) element to which the elements are to be attached - * @param {Object} datum The D3 data object containing the individual data + * @param {Person} person The D3 data object containing the individual data */ - addFirstNames(parent, datum) + addFirstNames(parent, person) { let i = 0; - for (let firstName of datum.data.firstNames) { + for (let firstName of person.data.firstNames) { // Create a element for each given name let tspan = parent.append("tspan") .text(firstName); // The preferred name - if (firstName === datum.data.preferredName) { + if (firstName === person.data.preferredName) { tspan.attr("class", "preferred"); } @@ -495,14 +652,14 @@ export default class Tree * Creates a single element for each last name and append it to the parent element. * * @param {selection} parent The parent ( or ) element to which the elements are to be attached - * @param {Object} datum The D3 data object containing the individual data + * @param {Person} person The person object containing the individual data * @param {Number} dx Additional space offset to add between names */ - addLastNames(parent, datum, dx = 0) + addLastNames(parent, person, dx = 0) { let i = 0; - for (let lastName of datum.data.lastNames) { + for (let lastName of person.data.lastNames) { // Create a element for each last name let tspan = parent.append("tspan") .attr("class", "lastName") @@ -636,9 +793,9 @@ export default class Tree * Add the individual names to the given parent element. * * @param {selection} parent The parent element to which the elements are to be attached - * @param {Object} datum The D3 data object + * @param {Person} person The person object containing the individual data */ - addNames(parent, datum) + addNames(parent, person) { let name = parent .append("g") @@ -656,15 +813,15 @@ export default class Tree .attr("alignment-baseline", "central") .attr("dy", this._box.text.y + 20); - this.addFirstNames(text1, datum); - this.addLastNames(text2, datum); + this.addFirstNames(text1, person); + this.addLastNames(text2, person); // If both first and last names are empty, add the full name as alternative - if (!datum.data.firstNames.length - && !datum.data.lastNames.length + if (!person.data.firstNames.length + && !person.data.lastNames.length ) { text1.append("tspan") - .text(datum.data.name); + .text(person.data.name); } this.truncateNames(text1); @@ -677,15 +834,15 @@ export default class Tree .attr("dx", this._box.text.x) .attr("dy", this._box.text.y); - this.addFirstNames(text1, datum); - this.addLastNames(text1, datum, 0.25); + this.addFirstNames(text1, person); + this.addLastNames(text1, person, 0.25); // If both first and last names are empty, add the full name as alternative - if (!datum.data.firstNames.length - && !datum.data.lastNames.length + if (!person.data.firstNames.length + && !person.data.lastNames.length ) { text1.append("tspan") - .text(datum.data.name); + .text(person.data.name); } this.truncateNames(text1); @@ -696,9 +853,9 @@ export default class Tree * Add the individual dates to the given parent element. * * @param {selection} parent The parent element to which the elements are to be attached - * @param {Object} datum The D3 data object + * @param {Person} person The person object containing the individual data */ - addDates(parent, datum) + addDates(parent, person) { let table = parent .append("g") @@ -713,10 +870,10 @@ export default class Tree .attr("dy", this._box.text.y + 50); text.append("title") - .text(datum.data.timespan); + .text(person.data.timespan); let tspan = text.append("tspan") - .text(datum.data.timespan); + .text(person.data.timespan); if (this.getTextLength(text) > this._box.text.width) { text.selectAll("tspan") @@ -730,7 +887,7 @@ export default class Tree let offset = 20; - if (datum.data.birth) { + if (person.data.birth) { let col1 = table .append("text") .attr("class", "date") @@ -752,11 +909,11 @@ export default class Tree .attr("dy", this._box.text.y + offset); col2.append("title") - .text(datum.data.birth); + .text(person.data.birth); let tspan = col2 .append("tspan") - .text(datum.data.birth) + .text(person.data.birth) .attr("x", this._box.text.x + 15); if (this.getTextLength(col2) > (this._box.text.width - 25)) { @@ -767,8 +924,8 @@ export default class Tree } } - if (datum.data.death) { - if (datum.data.birth) { + if (person.data.death) { + if (person.data.birth) { offset += 20; } @@ -793,11 +950,11 @@ export default class Tree .attr("dy", this._box.text.y + offset); col2.append("title") - .text(datum.data.death); + .text(person.data.death); let tspan = col2 .append("tspan") - .text(datum.data.death) + .text(person.data.death) .attr("x", this._box.text.x + 15); if (this.getTextLength(col2) > (this._box.text.width - 25)) { @@ -812,14 +969,14 @@ export default class Tree /** * Return the image file or the placeholder. * - * @param {Object} datum The D3 data object + * @param {Person} person The person object containing the individual data * * @returns {String} */ - getImageToLoad(datum) + getImageToLoad(person) { - if (datum.data.thumbnail) { - return datum.data.thumbnail; + if (person.data.thumbnail) { + return person.data.thumbnail; } return ""; @@ -828,26 +985,25 @@ export default class Tree /** * Draw the connecting lines. * - * @param {Array} links Array of links + * @param {Link[]} links Array of links * @param {Object} source The root object * * @private */ drawLinks(links, source) { - // let that = this; - - let link = this._svg.visual + let linkPath = this._svg.visual .selectAll("path.link") - .data(links, person => person.target.id); + .data(links);//, person => person.target.id); // Add new links. Transition new links from the source's old position to // the links final position. - let linkEnter = link + let linkEnter = linkPath .enter() .append("path") .classed("link", true) - .attr("d", person => this._orientation.elbow(person)); + .attr("d", link => this._orientation.elbow(link)); + // // Add new links. Transition new links from the source's old position to // // the links final position. @@ -898,9 +1054,9 @@ export default class Tree // */ // transitionElbow(datum) // { - // return "M" + datum.source.y + "," + datum.source.x - // + "H" + datum.source.y - // + "V" + datum.source.x - // + "H" + datum.source.y; + // return "M" + datum.data.source.y + "," + datum.data.source.x + // + "H" + datum.data.source.y + // + "V" + datum.data.source.x + // + "H" + datum.data.source.y; // } } diff --git a/resources/views/modules/descendants-chart/page.phtml b/resources/views/modules/descendants-chart/page.phtml index 092df48..bc2ec6d 100644 --- a/resources/views/modules/descendants-chart/page.phtml +++ b/resources/views/modules/descendants-chart/page.phtml @@ -89,9 +89,40 @@ use MagicSunday\Webtrees\DescendantsChart\Configuration; +
+
+ +
+ 'hideSpouses', + 'name' => 'hideSpouses', + 'label' => I18N::translate('Hide the spouses associated with each individual.'), + 'checked' => $configuration->getHideSpouses(), + ]) + ?> +
+
+
+
+
+ +
+
@@ -145,20 +176,56 @@ function getUrl(baseUrl, generations) return url.toString(); } +/** + * Set visibility of "Show more options" to previous state after page reload. + * Toggle text on "Show more options" on click. + * + * @param {Storage} storage + */ +function toggleMoreOptions(storage) +{ + // Options container + const showMoreOptions = document.getElementById('showMoreOptions'); + + // Options toggle button + const optionsToggle = document.getElementById('options'); + + showMoreOptions.addEventListener('shown.bs.collapse', () => { + storage.write("showMoreOptions", true); + }); + + showMoreOptions.addEventListener('hidden.bs.collapse', () => { + storage.write("showMoreOptions", false); + }); + + optionsToggle.addEventListener('click', () => { + Array.from(optionsToggle.children).forEach((element, index) => { + element.classList.toggle('d-none'); + }); + }); + + if (storage.read("showMoreOptions")) { + optionsToggle.click() + } +} + // Set up storage object -let storage = new WebtreesDescendantsChart.Storage("webtrees-descendants-chart"); +const storage = new WebtreesDescendantsChart.Storage("webtrees-descendants-chart"); // Register all form elements valid for storing data storage.register("generations"); storage.register("layout"); +// Handle option toggle button +toggleMoreOptions(storage); + // Set initial stored value for radio button group let formElements = document.getElementById("webtrees-descendants-chart-form").elements; formElements.namedItem("layout").value = storage.read("layout"); const generations = parseInt(storage.read("generations")); const treeLayout = storage.read("layout"); -const ajaxUrl = getUrl(, storage.read("generations")) +const ajaxUrl = getUrl(, storage.read("generations")); document.getElementById("descendants-chart-url") .setAttribute('data-wt-ajax-url', ajaxUrl); diff --git a/src/Configuration.php b/src/Configuration.php index 20d10f3..cc37463 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -118,4 +118,15 @@ public function getLayout(): string return Validator::queryParams($this->request) ->string('layout', self::DEFAULT_TREE_LAYOUT); } + + /** + * Returns whether to hide spouses or not. + * + * @return bool + */ + public function getHideSpouses(): bool + { + return Validator::queryParams($this->request) + ->boolean('hideSpouses', false); + } } diff --git a/src/Module.php b/src/Module.php index f155bb8..ff775fe 100644 --- a/src/Module.php +++ b/src/Module.php @@ -142,7 +142,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface $ajax = Validator::queryParams($request)->boolean('ajax', false); // Convert POST requests into GET requests for pretty URLs. - // This also updates the name above the form, which wont get updated if only a POST request is used + // This also updates the name above the form, which won't get updated if only a POST request is used if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { $validator = Validator::parsedBody($request); @@ -153,6 +153,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface 'tree' => $tree->name(), 'xref' => $validator->string('xref', ''), 'generations' => $validator->integer('generations', 4), + 'hideSpouses' => $validator->boolean('hideSpouses', false), 'layout' => $validator->string('layout', Configuration::LAYOUT_LEFTRIGHT), ] ) @@ -169,10 +170,15 @@ public function handle(ServerRequestInterface $request): ResponseInterface if ($ajax) { $this->layout = $this->name() . '::layouts/ajax'; + // The root node with all children + $root = [ + 'children' => $this->buildJsonTree($individual), + ]; + return $this->viewResponse( $this->name() . '::modules/descendants-chart/chart', [ - 'data' => $this->buildJsonTree($individual), + 'data' => $root, 'configuration' => $this->configuration, 'chartParams' => json_encode($this->getChartParameters(), JSON_THROW_ON_ERROR), 'stylesheet' => $this->assetUrl('css/descendants-chart.css'), @@ -247,32 +253,70 @@ private function buildJsonTree(?Individual $individual, int $generation = 1): ar return []; } - /** @var array> $data */ - $data = $this->getIndividualData($individual, $generation); $families = $individual->spouseFamilies(); + $parents = []; - if ($families->count() === 0) { - return $data; - } + $parents[$individual->xref()] = [ + 'data' => $this->getIndividualData($individual, $generation), + ]; - $childCount = 0; + if ($families->count() > 0) { + /** @var Family $family */ + foreach ($families as $familyIndex => $family) { + $children = []; + $spouse = null; - /** @var Family $family */ - foreach ($families as $family) { - foreach ($family->children() as $child) { - $childTree = $this->buildJsonTree($child, $generation + 1); + if (!$this->configuration->getHideSpouses()) { + $spouse = $family->spouse($individual); + } - if (count($childTree) > 0) { - $data['children'][] = $childTree; + foreach ($family->children() as $child) { + $childTree = $this->buildJsonTree($child, $generation + 1); + + if (count($childTree) > 0) { + foreach ($childTree as $childData) { + if ($childData['data'] !== null) { + $children[$childData['data']['xref']] = $childData; + } else { + $children[$childData['spouse']]['children'] = $childData['children']; + $children[$childData['spouse']]['family'] = $childData['family']; + } + } + } + } - ++$childCount; + $parentData = [ + 'data' => null, + 'spouse' => $individual->xref(), + 'family' => $familyIndex, + 'children' => array_values($children), + ]; + + if ($spouse !== null) { + $parentData['data'] = $this->getIndividualData($spouse, $generation); + + $parents[] = $parentData; + + // Add spouse to list + $parents[$individual->xref()]['spouses'][] = $spouse->xref(); + } else { + $parents[$individual->xref()]['family'] = $familyIndex; + + if (!isset($parents[$individual->xref()]['children'])) { + $parents[$individual->xref()]['children'] = []; + } + + // If there is no spouse merge all children from all families + // of the individual into one list + $parents[$individual->xref()]['children'] = array_merge( + $parents[$individual->xref()]['children'], + array_values($children) + ); } } } - $data['childCount'] = $childCount; - - return $data; + return array_values($parents); } /** @@ -292,7 +336,6 @@ private function getIndividualData(Individual $individual, int $generation): arr $alternativeNames = $nameProcessor->getAlternateNames(); return [ - 'id' => 0, 'xref' => $individual->xref(), 'url' => $individual->url(), 'updateUrl' => $this->getUpdateRoute($individual), @@ -325,6 +368,7 @@ private function getAjaxRoute(Individual $individual, string $xref): string [ 'ajax' => true, 'generations' => $this->configuration->getGenerations(), + 'hideSpouses' => $this->configuration->getHideSpouses(), 'layout' => $this->configuration->getLayout(), 'xref' => $xref, ]