├── phpstan-baseline.neon ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── ci.yml ├── .phplint.yml ├── resources ├── lang │ ├── cs │ │ └── messages.mo │ ├── da │ │ ├── messages.mo │ │ └── messages.po │ ├── de │ │ └── messages.mo │ ├── es │ │ ├── messages.mo │ │ └── messages.po │ ├── fr │ │ ├── messages.mo │ │ └── messages.po │ ├── is │ │ └── messages.mo │ ├── nb │ │ └── messages.mo │ ├── nl │ │ └── messages.mo │ ├── nn │ │ └── messages.mo │ ├── ru │ │ ├── messages.mo │ │ └── messages.po │ ├── sv │ │ ├── messages.mo │ │ └── messages.po │ ├── uk │ │ └── messages.mo │ ├── vi │ │ └── messages.mo │ ├── en-US │ │ ├── messages.mo │ │ └── messages.po │ └── sr-Latn │ │ └── messages.mo ├── js │ ├── modules │ │ ├── lib │ │ │ ├── common │ │ │ │ ├── dpi.js │ │ │ │ └── dataUrl.js │ │ │ ├── constants.js │ │ │ ├── d3.js │ │ │ ├── chart │ │ │ │ ├── text │ │ │ │ │ └── measure.js │ │ │ │ ├── svg │ │ │ │ │ ├── export.js │ │ │ │ │ ├── export-factory.js │ │ │ │ │ ├── defs.js │ │ │ │ │ ├── zoom.js │ │ │ │ │ └── export │ │ │ │ │ │ └── png.js │ │ │ │ ├── orientation │ │ │ │ │ ├── orientation-bottomTop.js │ │ │ │ │ ├── orientation-topBottom.js │ │ │ │ │ ├── orientation-leftRight.js │ │ │ │ │ ├── orientation-rightLeft.js │ │ │ │ │ └── orientation.js │ │ │ │ ├── orientation-collection.js │ │ │ │ ├── update.js │ │ │ │ ├── overlay.js │ │ │ │ ├── box.js │ │ │ │ ├── box │ │ │ │ │ ├── text.js │ │ │ │ │ └── image.js │ │ │ │ └── svg.js │ │ │ ├── storage.js │ │ │ └── tree │ │ │ │ ├── link-drawer.js │ │ │ │ └── elbow │ │ │ │ ├── vertical.js │ │ │ │ └── horizontal.js │ │ ├── custom │ │ │ ├── hierarchy.js │ │ │ ├── data.js │ │ │ └── configuration.js │ │ └── index.js │ └── descendants-chart-storage.min.js ├── views │ ├── modules │ │ ├── charts │ │ │ └── chart.phtml │ │ ├── descendants-chart │ │ │ ├── form │ │ │ │ ├── orientation.phtml │ │ │ │ ├── generations.phtml │ │ │ │ └── layout.phtml │ │ │ ├── chart.phtml │ │ │ ├── config.phtml │ │ │ └── page.phtml │ │ └── components │ │ │ ├── checkbox.phtml │ │ │ ├── buttonbar │ │ │ ├── help-button.phtml │ │ │ ├── center-button.phtml │ │ │ ├── fullscreen-button.phtml │ │ │ ├── png-export-button.phtml │ │ │ └── svg-export-button.phtml │ │ │ └── buttonbar.phtml │ └── layouts │ │ └── ajax.phtml ├── images │ ├── silhouette-U.svg │ ├── silhouette-X.svg │ ├── silhouette-M.svg │ └── silhouette-F.svg └── css │ ├── svg.css │ └── descendants-chart.css ├── assets ├── control-panel-modules.png └── descendants-chart-4-generations.png ├── phpstan.neon ├── module.php ├── package.json ├── src ├── Traits │ ├── ModuleChartTrait.php │ ├── ModuleCustomTrait.php │ └── ModuleConfigTrait.php └── Model │ └── Node.php ├── rector.php ├── rollup.config.js ├── .php-cs-fixer.dist.php ├── composer.json └── README.md /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: [] 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | custom: https://paypal.me/magicsunday 3 | -------------------------------------------------------------------------------- /.phplint.yml: -------------------------------------------------------------------------------- 1 | path: ./ 2 | jobs: 10 3 | cache: .build/.phplint.cache 4 | extensions: 5 | - php 6 | exclude: 7 | - .build 8 | -------------------------------------------------------------------------------- /resources/lang/cs/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/cs/messages.mo -------------------------------------------------------------------------------- /resources/lang/da/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/da/messages.mo -------------------------------------------------------------------------------- /resources/lang/de/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/de/messages.mo -------------------------------------------------------------------------------- /resources/lang/es/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/es/messages.mo -------------------------------------------------------------------------------- /resources/lang/fr/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/fr/messages.mo -------------------------------------------------------------------------------- /resources/lang/is/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/is/messages.mo -------------------------------------------------------------------------------- /resources/lang/nb/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/nb/messages.mo -------------------------------------------------------------------------------- /resources/lang/nl/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/nl/messages.mo -------------------------------------------------------------------------------- /resources/lang/nn/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/nn/messages.mo -------------------------------------------------------------------------------- /resources/lang/ru/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/ru/messages.mo -------------------------------------------------------------------------------- /resources/lang/sv/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/sv/messages.mo -------------------------------------------------------------------------------- /resources/lang/uk/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/uk/messages.mo -------------------------------------------------------------------------------- /resources/lang/vi/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/vi/messages.mo -------------------------------------------------------------------------------- /assets/control-panel-modules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/assets/control-panel-modules.png -------------------------------------------------------------------------------- /resources/lang/en-US/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/en-US/messages.mo -------------------------------------------------------------------------------- /resources/lang/sr-Latn/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/resources/lang/sr-Latn/messages.mo -------------------------------------------------------------------------------- /assets/descendants-chart-4-generations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicsunday/webtrees-descendants-chart/HEAD/assets/descendants-chart-4-generations.png -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - %currentWorkingDirectory%/.build/vendor/phpstan/phpstan-strict-rules/rules.neon 3 | - %currentWorkingDirectory%/.build/vendor/phpstan/phpstan-deprecation-rules/rules.neon 4 | - %currentWorkingDirectory%/phpstan-baseline.neon 5 | 6 | parameters: 7 | # You can currently choose from 10 levels (0 is the loosest and 9 is the strictest). 8 | level: 9 9 | 10 | paths: 11 | - %currentWorkingDirectory%/src/ 12 | 13 | excludePaths: 14 | - %currentWorkingDirectory%/.build/ 15 | 16 | fileExtensions: 17 | - php 18 | 19 | # Ignored validations 20 | treatPhpDocTypesAsCertain: false 21 | -------------------------------------------------------------------------------- /resources/js/modules/lib/common/dpi.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | /** 9 | * Returns the DPI of the current device. 10 | * 11 | * @returns {number} 12 | */ 13 | export default function() 14 | { 15 | const element = document.createElement("div"); 16 | element.style.width = "1in"; 17 | 18 | document.body.appendChild(element); 19 | const offsetWidth = element.offsetWidth; 20 | document.body.removeChild(element); 21 | 22 | return (window.devicePixelRatio || 1) * offsetWidth; 23 | } 24 | -------------------------------------------------------------------------------- /resources/views/modules/charts/chart.phtml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | 24 |
25 | 26 | 29 | -------------------------------------------------------------------------------- /resources/images/silhouette-U.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/images/silhouette-X.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/views/layouts/ajax.phtml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /module.php: -------------------------------------------------------------------------------- 1 | addPsr4('MagicSunday\\Webtrees\\ModuleBase\\', __DIR__ . '/vendor/magicsunday/webtrees-module-base/src'); 21 | $loader->addPsr4('MagicSunday\\Webtrees\\DescendantsChart\\', __DIR__ . '/src'); 22 | $loader->register(); 23 | 24 | // Create and return instance of the module 25 | return Registry::container()->get(Module::class); 26 | -------------------------------------------------------------------------------- /resources/js/modules/lib/common/dataUrl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | /** 9 | * Returns the input as data URL, prefixed with data: scheme. 10 | * 11 | * @param {RequestInfo} input 12 | * @param {null|RequestInit} init 13 | * 14 | * @returns {Promise} 15 | */ 16 | export default function(input, init = null) 17 | { 18 | return fetch(input, init) 19 | .then(response => response.blob()) 20 | .then(blob => new Promise( 21 | (resolve, reject) => { 22 | const reader = new FileReader(); 23 | reader.onloadend = () => resolve(reader.result); 24 | reader.onerror = reject; 25 | reader.readAsDataURL(blob); 26 | } 27 | ) 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /resources/views/modules/descendants-chart/form/orientation.phtml: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 |
22 | 23 |
24 |
25 | 'layout', 28 | 'options' => $configuration->getLayouts(), 29 | 'selected' => $configuration->getLayout(), 30 | ]) 31 | ?> 32 |
33 |
34 | -------------------------------------------------------------------------------- /resources/views/modules/descendants-chart/form/generations.phtml: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | 24 |
25 | 'generations', 28 | 'selected' => $configuration->getGenerations(), 29 | 'options' => $configuration->getGenerationsList(), 30 | 'class' => 'form-control-sm', 31 | ]) 32 | ?> 33 |
34 |
35 | -------------------------------------------------------------------------------- /resources/js/descendants-chart-storage.min.js: -------------------------------------------------------------------------------- 1 | var e,t;e=this,t=function(e){e.Storage=class{constructor(e){this._name=e,this._storage=JSON.parse(localStorage.getItem(this._name))||{}}register(e){let t=document.querySelector('input[id^="'+e+'"]:checked, select[id^="'+e+'"]')||document.querySelector('input[id^="'+e+'"]');if(null===t)return;let o=this.read(e);null!==o?t.type&&"radio"===t.type||t.type&&"checkbox"===t.type?t.checked=o:t.value=o:this.onInput(t),document.querySelectorAll('input[id^="'+e+'"], select[id^="'+e+'"]').forEach(e=>e.addEventListener("input",e=>{this.onInput(e.target)}))}onInput(e){e.type&&"checkbox"===e.type?this.write(e.name,e.checked):this.write(e.name,e.value)}read(e){return this._storage.hasOwnProperty(e)?this._storage[e]:null}write(e,t){this._storage[e]=t;try{localStorage.setItem(this._name,JSON.stringify(this._storage))}catch(o){console.log("There wasn't enough space to store '"+e+"' with value '"+t+"' in the local storage.")}}}},"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).WebtreesDescendantsChart={}); 2 | -------------------------------------------------------------------------------- /resources/js/modules/lib/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | /** 9 | * The widths and heights of a single node in each tree layout. 10 | * 11 | * @type {number} 12 | * @const 13 | */ 14 | export const LAYOUT_HORIZONTAL_NODE_WIDTH = 325; 15 | export const LAYOUT_HORIZONTAL_NODE_HEIGHT = 95; 16 | export const LAYOUT_VERTICAL_NODE_WIDTH = 160; 17 | export const LAYOUT_VERTICAL_NODE_HEIGHT = 175; 18 | 19 | export const LAYOUT_VERTICAL_NODE_HEIGHT_OFFSET = 30; 20 | 21 | /** 22 | * Tree layout variants. 23 | * 24 | * @type {string} 25 | * @const 26 | * 27 | * @see \Fisharebest\Webtrees\Module\PedigreeChartModule 28 | */ 29 | export const LAYOUT_TOPBOTTOM = "down"; 30 | export const LAYOUT_BOTTOMTOP = "up"; 31 | export const LAYOUT_LEFTRIGHT = "right"; 32 | export const LAYOUT_RIGHTLEFT = "left"; 33 | 34 | /** 35 | * Gender types. 36 | * 37 | * @type {string} 38 | * @const 39 | */ 40 | export const SEX_MALE = "M"; 41 | export const SEX_FEMALE = "F"; 42 | -------------------------------------------------------------------------------- /resources/js/modules/lib/d3.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file that was distributed with this source code. 6 | */ 7 | 8 | /* 9 | https://github.com/d3/d3-fetch 10 | https://github.com/d3/d3-hierarchy 11 | https://github.com/d3/d3-path 12 | https://github.com/d3/d3-scale 13 | https://github.com/d3/d3-selection 14 | https://github.com/d3/d3-shape 15 | https://github.com/d3/d3-timer 16 | https://github.com/d3/d3-transition 17 | https://github.com/d3/d3-zoom 18 | */ 19 | 20 | export { 21 | json, text 22 | } from "d3-fetch"; 23 | 24 | export { 25 | Node, hierarchy, partition, tree 26 | } from "d3-hierarchy"; 27 | 28 | export { 29 | path 30 | } from "d3-path"; 31 | 32 | export { 33 | scaleLinear 34 | } from "d3-scale"; 35 | 36 | export { 37 | select, selectAll 38 | } from "d3-selection"; 39 | 40 | export { 41 | timer 42 | } from "d3-timer"; 43 | 44 | export { 45 | arc 46 | } from "d3-shape"; 47 | 48 | export { 49 | transition 50 | } from "d3-transition"; 51 | 52 | export * from "d3-zoom"; 53 | -------------------------------------------------------------------------------- /resources/images/silhouette-M.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/views/modules/components/checkbox.phtml: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 29 | $checked ?? null, 32 | 'disabled' => $disabled ?? null, 33 | 'id' => $id ?? null, 34 | 'label' => $label, 35 | 'name' => $name, 36 | 'value' => $value ?? '1', 37 | 'unchecked' => $unchecked, 38 | ]) 39 | ?> 40 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/text/measure.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | let measureCanvas = null; 9 | 10 | /** 11 | * Measures the given text and return its width depending on the used font (including size and weight). 12 | * 13 | * @param {string} text The text whose length is to be determined 14 | * @param {string} fontFamily The font family used to calculate the length 15 | * @param {string} fontSize The font size used to calculate the length 16 | * @param {number} fontWeight The font weight used to calculate the length 17 | * 18 | * @returns {number} 19 | */ 20 | export default function(text, fontFamily, fontSize, fontWeight = 400) 21 | { 22 | if (measureCanvas === null) { 23 | measureCanvas = document.createElement("canvas"); 24 | } 25 | 26 | const context = measureCanvas.getContext("2d"); 27 | const font = `${fontWeight || ''} ${fontSize} ${fontFamily}`; 28 | 29 | if (context.font !== font) { 30 | context.font = font; 31 | } 32 | 33 | return context.measureText(text).width; 34 | } 35 | -------------------------------------------------------------------------------- /resources/views/modules/components/buttonbar/help-button.phtml: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/svg/export.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | /** 9 | * Base export class. 10 | * 11 | * @author Rico Sonntag 12 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 13 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 14 | */ 15 | export default class Export 16 | { 17 | /** 18 | * Triggers the download by creating a new anchor element and simulate a mouse click on it. 19 | * 20 | * @param {string} imgURI The image URI data stream 21 | * @param {string} fileName The file name to use in the download dialog 22 | */ 23 | triggerDownload(imgURI, fileName) 24 | { 25 | let event = new MouseEvent("click", { 26 | view: window, 27 | bubbles: false, 28 | cancelable: true 29 | }); 30 | 31 | let a = document.createElement("a"); 32 | a.setAttribute("download", fileName); 33 | a.setAttribute("href", imgURI); 34 | a.setAttribute("target", "_blank"); 35 | a.dispatchEvent(event); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webtrees-descendants-chart", 3 | "version": "2.0.1-dev", 4 | "description": "This modules provides an SVG descendants chart for the [webtrees](https://www.webtrees.net) genealogy application.", 5 | "keywords": [ 6 | "webtrees", 7 | "module", 8 | "descendant", 9 | "chart" 10 | ], 11 | "type": "module", 12 | "homepage": "https://github.com/magicsunday/webtrees-descendants-chart", 13 | "license": "GPL-3.0-or-later", 14 | "author": "Rico Sonntag ", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/magicsunday/webtrees-descendants-chart.git" 18 | }, 19 | "scripts": { 20 | "prepare": "rollup --config rollup.config.js", 21 | "watch": "rollup --watch --config rollup.config.js" 22 | }, 23 | "devDependencies": { 24 | "@rollup/plugin-node-resolve": "^15.2.3", 25 | "@rollup/plugin-terser": "^0.4.4", 26 | "rollup-plugin-license": "^3.5.2", 27 | "d3-fetch": "^3.0.1", 28 | "d3-hierarchy": "^3.1.2", 29 | "d3-path": "^3.1.0", 30 | "d3-scale": "^4.0.2", 31 | "d3-selection": "^3.0.0", 32 | "d3-shape": "^3.2.0", 33 | "d3-timer": "^3.0.1", 34 | "d3-transition": "^3.0.1", 35 | "d3-zoom": "^3.0.0", 36 | "rollup": "^4.9.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/svg/export-factory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import PngExport from "./export/png"; 9 | import SvgExport from "./export/svg"; 10 | 11 | /** 12 | * The file export factory. 13 | * 14 | * @author Rico Sonntag 15 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 16 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 17 | */ 18 | export default class ExportFactory 19 | { 20 | constructor() 21 | { 22 | this._exportClass = null; 23 | } 24 | 25 | setExportClass(type) 26 | { 27 | switch (type) { 28 | case 'png': 29 | this._exportClass = PngExport; 30 | break; 31 | case 'svg': 32 | this._exportClass = SvgExport; 33 | break; 34 | default: 35 | break; 36 | } 37 | }; 38 | 39 | createExport(type) 40 | { 41 | this.setExportClass(type); 42 | 43 | switch (type) { 44 | case 'png': 45 | return new this._exportClass(); 46 | case 'svg': 47 | return new this._exportClass(); 48 | } 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/orientation/orientation-bottomTop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import Orientation from "./orientation"; 9 | import elbowVertical from "../../tree/elbow/vertical"; 10 | 11 | /** 12 | * This class handles the orientation of the tree. 13 | * 14 | * @author Rico Sonntag 15 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 16 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 17 | */ 18 | export default class OrientationBottomTop extends Orientation 19 | { 20 | /** 21 | * Constructor. 22 | * 23 | * @param {number} boxWidth The width of a single individual box 24 | * @param {number} boxHeight The height of a single individual box 25 | */ 26 | constructor(boxWidth, boxHeight) 27 | { 28 | super(boxWidth, boxHeight); 29 | 30 | this._splittNames = true; 31 | } 32 | 33 | get direction() 34 | { 35 | return -1; 36 | } 37 | 38 | get nodeWidth() 39 | { 40 | return this._boxWidth + this._xOffset; 41 | } 42 | 43 | get nodeHeight() 44 | { 45 | return this._boxHeight + this._yOffset; 46 | } 47 | 48 | norm(d) 49 | { 50 | d.y *= this.direction; 51 | } 52 | 53 | elbow(link) 54 | { 55 | return elbowVertical(link, this); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/orientation/orientation-topBottom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import Orientation from "./orientation"; 9 | import elbowVertical from "../../tree/elbow/vertical"; 10 | 11 | /** 12 | * This class handles the orientation of the tree. 13 | * 14 | * @author Rico Sonntag 15 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 16 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 17 | */ 18 | export default class OrientationTopBottom extends Orientation 19 | { 20 | /** 21 | * Constructor. 22 | * 23 | * @param {number} boxWidth The width of a single individual box 24 | * @param {number} boxHeight The height of a single individual box 25 | */ 26 | constructor(boxWidth, boxHeight) 27 | { 28 | super(boxWidth, boxHeight); 29 | 30 | this._splittNames = true; 31 | } 32 | 33 | get direction() 34 | { 35 | return 1; 36 | } 37 | 38 | get nodeWidth() 39 | { 40 | return this._boxWidth + this._xOffset; 41 | } 42 | 43 | get nodeHeight() 44 | { 45 | return this._boxHeight + this._yOffset; 46 | } 47 | 48 | norm(d) 49 | { 50 | d.y *= this.direction; 51 | } 52 | 53 | elbow(link) 54 | { 55 | return elbowVertical(link, this); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Traits/ModuleChartTrait.php: -------------------------------------------------------------------------------- 1 | 22 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 23 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 24 | */ 25 | trait ModuleChartTrait 26 | { 27 | use \Fisharebest\Webtrees\Module\ModuleChartTrait; 28 | 29 | public function chartMenuClass(): string 30 | { 31 | return 'menu-chart-descendants'; 32 | } 33 | 34 | public function chartBoxMenu(Individual $individual): ?Menu 35 | { 36 | return $this->chartMenu($individual); 37 | } 38 | 39 | public function chartTitle(Individual $individual): string 40 | { 41 | return I18N::translate('Descendants chart of %s', $individual->fullName()); 42 | } 43 | 44 | public function chartUrl(Individual $individual, array $parameters = []): string 45 | { 46 | return route( 47 | self::ROUTE_DEFAULT, 48 | [ 49 | 'xref' => $individual->xref(), 50 | 'tree' => $individual->tree()->name(), 51 | ] + $parameters 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/svg/defs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | /** 9 | * SVG definition class 10 | * 11 | * @author Rico Sonntag 12 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 13 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 14 | */ 15 | export default class Defs 16 | { 17 | /** 18 | * Constructor. 19 | * 20 | * @param {Selection} svg The selected D3 parent element container 21 | */ 22 | constructor(svg) 23 | { 24 | // Create the element 25 | this._element = svg.append("defs"); 26 | } 27 | 28 | /** 29 | * Returns the internal element. 30 | * 31 | * @returns {selection} 32 | */ 33 | get() 34 | { 35 | return this._element; 36 | } 37 | 38 | /** 39 | * @param {function|string} select 40 | * 41 | * @returns {Selection} 42 | */ 43 | select(select) 44 | { 45 | return this._element.select(select); 46 | } 47 | 48 | /** 49 | * @param {function|string|null} select 50 | * 51 | * @returns {Selection} 52 | */ 53 | selectAll(select) 54 | { 55 | return this._element.selectAll(select); 56 | } 57 | 58 | /** 59 | * @param {string|function} name 60 | * 61 | * @returns {Selection} 62 | */ 63 | append(name) 64 | { 65 | return this._element.append(name); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/orientation/orientation-leftRight.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import Orientation from "./orientation"; 9 | import elbowHorizontal from "../../tree/elbow/horizontal"; 10 | 11 | /** 12 | * This class handles the orientation of the tree. 13 | * 14 | * @author Rico Sonntag 15 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 16 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 17 | */ 18 | export default class OrientationLeftRight extends Orientation 19 | { 20 | /** 21 | * Constructor. 22 | * 23 | * @param {number} boxWidth The width of a single individual box 24 | * @param {number} boxHeight The height of a single individual box 25 | */ 26 | constructor(boxWidth, boxHeight) 27 | { 28 | super(boxWidth, boxHeight); 29 | 30 | this._xOffset = 40; 31 | this._yOffset = 20; 32 | } 33 | 34 | get direction() 35 | { 36 | return this.isDocumentRtl ? -1 : 1; 37 | } 38 | 39 | get nodeWidth() 40 | { 41 | return this._boxHeight + this._yOffset; 42 | } 43 | 44 | get nodeHeight() 45 | { 46 | return this._boxWidth + this._xOffset; 47 | } 48 | 49 | norm(d) 50 | { 51 | // Swap x and y values 52 | [d.x, d.y] = [d.y * this.direction, d.x]; 53 | } 54 | 55 | elbow(link) 56 | { 57 | return elbowHorizontal(link, this); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/orientation/orientation-rightLeft.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import Orientation from "./orientation"; 9 | import elbowHorizontal from "../../tree/elbow/horizontal"; 10 | 11 | /** 12 | * This class handles the orientation of the tree. 13 | * 14 | * @author Rico Sonntag 15 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 16 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 17 | */ 18 | export default class OrientationRightLeft extends Orientation 19 | { 20 | /** 21 | * Constructor. 22 | * 23 | * @param {number} boxWidth The width of a single individual box 24 | * @param {number} boxHeight The height of a single individual box 25 | */ 26 | constructor(boxWidth, boxHeight) 27 | { 28 | super(boxWidth, boxHeight); 29 | 30 | this._xOffset = 40; 31 | this._yOffset = 20; 32 | } 33 | 34 | get direction() 35 | { 36 | return this.isDocumentRtl ? 1 : -1; 37 | } 38 | 39 | get nodeWidth() 40 | { 41 | return this._boxHeight + this._yOffset; 42 | } 43 | 44 | get nodeHeight() 45 | { 46 | return this._boxWidth + this._xOffset; 47 | } 48 | 49 | norm(d) 50 | { 51 | // Swap x and y values 52 | [d.x, d.y] = [d.y * this.direction, d.x]; 53 | } 54 | 55 | elbow(link) 56 | { 57 | return elbowHorizontal(link, this); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /resources/images/silhouette-F.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/views/modules/components/buttonbar.phtml: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 |
22 | 23 | 24 |
25 | getHidePngExport() === false) || ($configuration->getHideSvgExport() === false)): ?> 26 |
27 | getHidePngExport() === false): ?> 28 | 29 | 30 | getHideSvgExport() === false): ?> 31 | 32 | 33 |
34 | 35 | 37 | */ ?> 38 |
39 | 40 | 51 | -------------------------------------------------------------------------------- /src/Traits/ModuleCustomTrait.php: -------------------------------------------------------------------------------- 1 | 21 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 22 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 23 | */ 24 | trait ModuleCustomTrait 25 | { 26 | use \Fisharebest\Webtrees\Module\ModuleCustomTrait; 27 | 28 | public function customModuleAuthorName(): string 29 | { 30 | return self::CUSTOM_AUTHOR; 31 | } 32 | 33 | public function customModuleVersion(): string 34 | { 35 | return self::CUSTOM_VERSION; 36 | } 37 | 38 | public function customModuleLatestVersionUrl(): string 39 | { 40 | return self::CUSTOM_LATEST_VERSION; 41 | } 42 | 43 | // protected function extractVersion(string $content): string 44 | // { 45 | // return json_decode($content, true)['tag_name'] ?? ''; 46 | // } 47 | 48 | public function customModuleLatestVersion(): string 49 | { 50 | return (new VersionInformation($this))->fetchLatestVersion(); 51 | } 52 | 53 | public function customModuleSupportUrl(): string 54 | { 55 | return self::CUSTOM_SUPPORT_URL; 56 | } 57 | 58 | public function customTranslations(string $language): array 59 | { 60 | $languageFile = $this->resourcesFolder() . 'lang/' . $language . '/messages.mo'; 61 | 62 | return file_exists($languageFile) ? (new Translation($languageFile))->asArray() : []; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: magicsunday 7 | 8 | --- 9 | 10 | Thank you for helping to further develop the module! In order to understand and implement your request for a new function or adjustment as clearly as possible, a detailed description is important. Please fill in the following points: 11 | 12 | # Description of the desired function 13 | _Describe the desired function or customization as specifically as possible._ 14 | **Example:** "I wish the module offered an additional filter option to sort people by year of birth." 15 | 16 | **Enter:** 17 | 18 | 19 | # Purpose of the function 20 | _Explain why this feature is useful or what problem it would solve._ 21 | **Example:** "The additional filter option would make it easier to find people from a specific time period, which is especially important for historical analyses." 22 | 23 | **Enter:** 24 | 25 | 26 | # Concrete use cases 27 | _Describe how the feature would be used in practice._ 28 | **Example:** "On the Advanced Search page, a dropdown menu could be added where the year of birth can be entered. Once entered, only people born in the specified year will be displayed." 29 | 30 | **Enter:** 31 | 32 | 33 | # Priority of the wish 34 | _How important is this feature to you? Is it essential or more of a 'nice-to-have'?_ 35 | **Example:** "It is a desirable improvement that is not urgent but would make the workflow much easier." 36 | 37 | **Enter:** 38 | 39 | 40 | # Technical details (if relevant) 41 | _Are there any technical requirements, dependencies or suggestions for implementation?_ 42 | **Example:** "Perhaps the function could build on the existing database query for advanced search." 43 | 44 | **Enter:** 45 | 46 | 47 | # **Note** 48 | A **clear and well thought-out description** of your request helps to plan and implement the function precisely. Thank you for your support in the further development of the module! 49 | -------------------------------------------------------------------------------- /resources/views/modules/components/buttonbar/center-button.phtml: -------------------------------------------------------------------------------- 1 | 15 | 16 | 27 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 24 | __DIR__ . '/src', 25 | ]); 26 | 27 | $rectorConfig->skip([ 28 | __DIR__ . '/.build', 29 | __DIR__ . '/test', 30 | ]); 31 | 32 | $rectorConfig->phpstanConfig('phpstan.neon'); 33 | $rectorConfig->importNames(); 34 | $rectorConfig->removeUnusedImports(); 35 | $rectorConfig->disableParallel(); 36 | 37 | // Define what rule sets will be applied 38 | $rectorConfig->sets([ 39 | SetList::EARLY_RETURN, 40 | SetList::TYPE_DECLARATION, 41 | SetList::CODING_STYLE, 42 | SetList::CODE_QUALITY, 43 | SetList::DEAD_CODE, 44 | LevelSetList::UP_TO_PHP_74, 45 | ]); 46 | 47 | // Skip some rules 48 | $rectorConfig->skip([ 49 | CatchExceptionNameMatchingTypeRector::class, 50 | LocallyCalledStaticMethodToNonStaticRector::class, 51 | ParamTypeByMethodCallTypeRector::class, 52 | RemoveUselessParamTagRector::class, 53 | RemoveUselessReturnTagRector::class, 54 | RemoveUselessVarTagRector::class, 55 | ]); 56 | }; 57 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/orientation-collection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import { 9 | LAYOUT_BOTTOMTOP, 10 | LAYOUT_LEFTRIGHT, 11 | LAYOUT_RIGHTLEFT, 12 | LAYOUT_TOPBOTTOM, 13 | LAYOUT_VERTICAL_NODE_WIDTH, 14 | LAYOUT_VERTICAL_NODE_HEIGHT, 15 | LAYOUT_HORIZONTAL_NODE_WIDTH, 16 | LAYOUT_HORIZONTAL_NODE_HEIGHT 17 | } from "../constants"; 18 | 19 | import OrientationTopBottom from "./orientation/orientation-topBottom"; 20 | import OrientationBottomTop from "./orientation/orientation-bottomTop"; 21 | import OrientationLeftRight from "./orientation/orientation-leftRight"; 22 | import OrientationRightLeft from "./orientation/orientation-rightLeft"; 23 | 24 | /** 25 | * This class handles the orientation of the tree. 26 | * 27 | * @author Rico Sonntag 28 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 29 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 30 | */ 31 | export default class OrientationCollection 32 | { 33 | /** 34 | * Constructor. 35 | */ 36 | constructor() 37 | { 38 | this._orientations = { 39 | [LAYOUT_TOPBOTTOM]: new OrientationTopBottom(LAYOUT_VERTICAL_NODE_WIDTH, LAYOUT_VERTICAL_NODE_HEIGHT), 40 | [LAYOUT_BOTTOMTOP]: new OrientationBottomTop(LAYOUT_VERTICAL_NODE_WIDTH, LAYOUT_VERTICAL_NODE_HEIGHT), 41 | [LAYOUT_LEFTRIGHT]: new OrientationLeftRight(LAYOUT_HORIZONTAL_NODE_WIDTH, LAYOUT_HORIZONTAL_NODE_HEIGHT), 42 | [LAYOUT_RIGHTLEFT]: new OrientationRightLeft(LAYOUT_HORIZONTAL_NODE_WIDTH, LAYOUT_HORIZONTAL_NODE_HEIGHT) 43 | }; 44 | } 45 | 46 | /** 47 | * Returns the internal element. 48 | * 49 | * @returns {Array} 50 | */ 51 | get() 52 | { 53 | return this._orientations; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 'Create a report to help us improve ' 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: magicsunday 7 | 8 | --- 9 | 10 | Thank you for helping to improve the module! In order to analyze and resolve the reported error as quickly as possible, a detailed error description is crucial. The more information you provide, the easier it is for us to find the cause. Please fill in the following: 11 | 12 | # Description of the problem 13 | _Describe what exactly happened and how the error manifests itself._ 14 | **Example:** "After saving a setting, an error message appears: 'Undefined index'." 15 | 16 | **Enter:** 17 | 18 | 19 | # Expected behavior 20 | _Explain what you expected._ 21 | **Example:** "The settings should be saved and a success message should be displayed." 22 | 23 | **Enter:** 24 | 25 | 26 | # Steps to reproduction 27 | _List all the steps necessary to reproduce the error._ 28 | **Example:** 29 | 1. Open the module. 30 | 2. Click "Settings". 31 | 3. Change XYZ and click "Save". 32 | 33 | **Enter:** 34 | 35 | 36 | # Error messages or screenshots 37 | _If an error message appears, copy the full text. Screenshots also help a lot._ 38 | 39 | **Enter:** 40 | 41 | 42 | # Technical Details 43 | _Please specify the technical conditions:_ 44 | * Webtrees version: (e.g. 2.x.x) 45 | * Module version: (if known) 46 | * Browser and operating system: (e.g. Chrome 117 on Windows 11) 47 | 48 | **Enter:** 49 | 50 | 51 | # Additional Information 52 | _Were there any special circumstances involved? Did you notice anything unusual? Was the module working correctly before a change or update?_ 53 | 54 | **Enter:** 55 | 56 | 57 | # Log files 58 | _Check the webtrees logs or server logs for entries related to the error and attach them._ 59 | **Example:** `[2024-12-21 15:30:45] ERROR: Undefined function xyz() in /modules/modulname/module.php on line 123.` 60 | 61 | **Enter:** 62 | 63 | 64 | # Note 65 | Only with a detailed error description can we quickly find the cause of the problem and resolve it. Thank you for your help! 66 | -------------------------------------------------------------------------------- /resources/css/svg.css: -------------------------------------------------------------------------------- 1 | /* SVG export styles */ 2 | .webtrees-descendants-chart-container svg { 3 | min-height: 50dvh; 4 | } 5 | 6 | /* Required for the exported SVG to display properly in Inkscape */ 7 | .webtrees-descendants-chart-container svg.rtl { 8 | direction: rtl !important; 9 | unicode-bidi: bidi-override !important; 10 | } 11 | 12 | .webtrees-descendants-chart-container svg rect.female { 13 | stroke: var(--sex-f-fg, rgb(255, 32, 128)); 14 | stroke-dasharray: none; 15 | stroke-width: 3px; 16 | fill: var(--sex-f-bg, rgb(233, 218, 241)); 17 | } 18 | 19 | .webtrees-descendants-chart-container svg rect.male { 20 | stroke: var(--sex-m-fg, rgb(129, 169, 203)); 21 | stroke-dasharray: none; 22 | stroke-width: 3px; 23 | fill: var(--sex-m-bg, rgb(237, 247, 253)); 24 | } 25 | 26 | .webtrees-descendants-chart-container svg rect.unknown { 27 | stroke: var(--sex-u-fg, rgb(129, 169, 203)); 28 | stroke-dasharray: 5; 29 | stroke-width: 3px; 30 | fill: var(--sex-u-bg, rgb(255, 255, 255)); 31 | } 32 | 33 | .webtrees-descendants-chart-container svg rect.male.spouse, 34 | .webtrees-descendants-chart-container svg rect.female.spouse, 35 | .webtrees-descendants-chart-container svg rect.unknown.spouse { 36 | stroke-dasharray: 5; 37 | stroke-width: 3px; 38 | stroke-opacity: 50%; 39 | } 40 | 41 | .webtrees-descendants-chart-container svg path.link { 42 | fill: none; 43 | stroke: rgb(175, 175, 175); 44 | stroke-width: 1.5px; 45 | stroke-linecap: butt; 46 | shape-rendering: geometricPrecision; 47 | } 48 | 49 | .webtrees-descendants-chart-container svg text.date { 50 | fill: currentColor; 51 | font-weight: normal; 52 | font-size: 13px; 53 | } 54 | 55 | .webtrees-descendants-chart-container svg text.wt-chart-box-name { 56 | fill: var(--link-color, currentColor); 57 | } 58 | 59 | .webtrees-descendants-chart-container svg text.wt-chart-box-name-alt { 60 | fill: currentColor; 61 | font-weight: 500; 62 | font-size: 0.85em; 63 | } 64 | 65 | .webtrees-descendants-chart-container svg text.wt-chart-box-name:hover:not(.wt-chart-box-name-alt) { 66 | fill: var(--link-color-hover); 67 | } 68 | -------------------------------------------------------------------------------- /resources/views/modules/components/buttonbar/fullscreen-button.phtml: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | -------------------------------------------------------------------------------- /resources/views/modules/descendants-chart/chart.phtml: -------------------------------------------------------------------------------- 1 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 63 | 64 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/update.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import * as d3 from "./../d3"; 9 | 10 | /** 11 | * This class handles the visual update of all text and path elements. 12 | * 13 | * @author Rico Sonntag 14 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 15 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 16 | */ 17 | export default class Update 18 | { 19 | /** 20 | * Constructor. 21 | * 22 | * @param {Svg} svg 23 | * @param {Configuration} configuration The application configuration 24 | * @param {Hierarchy} hierarchy 25 | */ 26 | constructor(svg, configuration, hierarchy) 27 | { 28 | this._svg = svg; 29 | this._configuration = configuration; 30 | this._hierarchy = hierarchy; 31 | } 32 | 33 | /** 34 | * Update the chart with data loaded from AJAX. 35 | * 36 | * @param {string} url The update URL 37 | * @param {Function} callback The callback method to execute after the update 38 | * 39 | * @public 40 | */ 41 | update(url, callback) 42 | { 43 | let that = this; 44 | 45 | this._svg 46 | .selectAll("g.person") 47 | .classed("hover", false) 48 | .on("click", null) 49 | .on("mouseover", null) 50 | .on("mouseout", null); 51 | 52 | d3.json( 53 | url 54 | ).then((data) => { 55 | // Initialize the new loaded data 56 | this._hierarchy.init(data); 57 | this.draw(); 58 | 59 | let indSelector = $(document.getElementById("xref")); 60 | 61 | $.ajax({ 62 | type: "POST", 63 | url: indSelector.attr("data-ajax--url"), 64 | data: { 65 | q : data.data.xref 66 | } 67 | }).then(function (data) { 68 | // Create the option and append to Select2 69 | let option = new Option(data.results[0].text, data.results[0].id, true, true); 70 | 71 | indSelector.append(option); 72 | indSelector.trigger("change"); 73 | }); 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /resources/views/modules/components/buttonbar/png-export-button.phtml: -------------------------------------------------------------------------------- 1 | 15 | 16 | 35 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/overlay.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | /** 9 | * This class handles the tooltip overlay. 10 | * 11 | * @author Rico Sonntag 12 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 13 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 14 | */ 15 | export default class Overlay 16 | { 17 | /** 18 | * Constructor. 19 | * 20 | * @param {Selection} parent The selected D3 parent element container 21 | */ 22 | constructor(parent) 23 | { 24 | // Create the tooltip overlay container 25 | this._element = parent 26 | .append("div") 27 | .attr("class", "overlay") 28 | .style("opacity", 1e-6); 29 | } 30 | 31 | /** 32 | * Stop any pending transition and hide overlay immediately. 33 | * 34 | * @param {string} text Text to display in overlay 35 | * @param {number} duration Duration of transition in msec 36 | * @param {Function} callback Callback method to execute on end of transition 37 | */ 38 | show(text, duration = 0, callback = null) 39 | { 40 | // Remove any previously added

element 41 | this._element 42 | .select("p") 43 | .remove(); 44 | 45 | this._element 46 | .append("p") 47 | .attr("class", "tooltip") 48 | .text(text); 49 | 50 | this._element 51 | .transition() 52 | .duration(duration) 53 | .style("opacity", 1) 54 | .on("end", () => { 55 | if (typeof callback === "function") { 56 | callback(); 57 | } 58 | }); 59 | } 60 | 61 | /** 62 | * Stop any pending transition and hide overlay immediately. 63 | * 64 | * @param {number} delay Delay in milliseconds to wait before transition should start 65 | * @param {number} duration Duration of transition in milliseconds 66 | */ 67 | hide(delay = 0, duration = 0) 68 | { 69 | this._element 70 | .transition() 71 | .delay(delay) 72 | .duration(duration) 73 | .style("opacity", 1e-6); 74 | } 75 | 76 | /** 77 | * Returns the internal element. 78 | * 79 | * @returns {Selection} 80 | */ 81 | get() 82 | { 83 | return this._element; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | php: [ '8.3', '8.4' ] 15 | 16 | steps: 17 | - id: checkout 18 | name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - id: setup_php 22 | name: Set up PHP version ${{ matrix.php }} 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php }} 26 | tools: composer:v2 27 | 28 | - name: Validate composer.json and composer.lock 29 | run: composer validate 30 | 31 | - id: composer-cache-vars 32 | name: Composer Cache Vars 33 | run: | 34 | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 35 | echo "timestamp=$(date +"%s")" >> $GITHUB_OUTPUT 36 | 37 | - id: composer-cache-dependencies 38 | name: Cache Composer dependencies 39 | uses: actions/cache@v4 40 | with: 41 | path: ${{ steps.composer-cache-vars.outputs.dir }} 42 | key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ steps.composer-cache-vars.outputs.timestamp }} 43 | restore-keys: | 44 | ${{ runner.os }}-composer-${{ matrix.php }}- 45 | ${{ runner.os }}-composer- 46 | 47 | - id: install 48 | name: Install dependencies 49 | run: | 50 | composer validate 51 | composer install --no-progress 52 | 53 | - id: lint 54 | name: Lint 55 | if: ${{ always() && steps.install.conclusion == 'success' }} 56 | run: | 57 | composer ci:test:php:lint 58 | 59 | - id: cgl 60 | name: CGL 61 | if: ${{ always() && steps.install.conclusion == 'success' }} 62 | run: | 63 | composer ci:cgl -- --dry-run 64 | 65 | - id: phpstan 66 | name: PHPStan 67 | if: ${{ always() && steps.install.conclusion == 'success' }} 68 | run: | 69 | composer ci:test:php:phpstan -- --error-format=github 70 | 71 | - id: rector 72 | name: Rector 73 | if: ${{ always() && steps.install.conclusion == 'success' }} 74 | run: | 75 | composer ci:test:php:rector 76 | -------------------------------------------------------------------------------- /resources/views/modules/components/buttonbar/svg-export-button.phtml: -------------------------------------------------------------------------------- 1 | 15 | 16 | 35 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from "./package.json" with {type: "json"}; 2 | import resolve from "@rollup/plugin-node-resolve"; 3 | import terser from "@rollup/plugin-terser"; 4 | import license from "rollup-plugin-license"; 5 | 6 | export default [ 7 | // descendants-chart.js 8 | { 9 | input: "resources/js/modules/index.js", 10 | output: [ 11 | { 12 | name: "WebtreesDescendantsChart", 13 | file: "resources/js/descendants-chart-" + pkg.version + ".js", 14 | format: "umd" 15 | } 16 | ], 17 | plugins: [ 18 | resolve(), 19 | license({ 20 | banner: ` 21 | This file is part of the package magicsunday/<%= pkg.name %>. 22 | 23 | For the full copyright and license information, please read the 24 | LICENSE file that was distributed with this source code. 25 | 26 | Generated: <%= moment().format('YYYY-MM-DD HH:mm:ss') %> 27 | Version: <%= pkg.version %>` 28 | }) 29 | ] 30 | }, 31 | { 32 | input: "resources/js/modules/index.js", 33 | output: [ 34 | { 35 | name: "WebtreesDescendantsChart", 36 | file: "resources/js/descendants-chart-" + pkg.version + ".min.js", 37 | format: "umd" 38 | } 39 | ], 40 | plugins: [ 41 | resolve(), 42 | terser({ 43 | mangle: true, 44 | compress: true, 45 | module: true, 46 | output: { 47 | comments: false 48 | } 49 | }), 50 | license({ 51 | banner: ` 52 | This file is part of the package magicsunday/<%= pkg.name %>. 53 | 54 | For the full copyright and license information, please read the 55 | LICENSE file that was distributed with this source code. 56 | 57 | Generated: <%= moment().format('YYYY-MM-DD HH:mm:ss') %> 58 | Version: <%= pkg.version %>` 59 | }) 60 | ] 61 | }, 62 | 63 | // descendants-chart-storage.js 64 | { 65 | input: "resources/js/modules/lib/storage.js", 66 | output: [ 67 | { 68 | name: "WebtreesDescendantsChart", 69 | file: "resources/js/descendants-chart-storage.js", 70 | format: "umd" 71 | } 72 | ], 73 | plugins: [ 74 | resolve() 75 | ] 76 | }, 77 | { 78 | input: "resources/js/modules/lib/storage.js", 79 | output: [ 80 | { 81 | name: "WebtreesDescendantsChart", 82 | file: "resources/js/descendants-chart-storage.min.js", 83 | format: "umd" 84 | } 85 | ], 86 | plugins: [ 87 | resolve(), 88 | terser({ 89 | mangle: true, 90 | compress: true, 91 | module: true, 92 | output: { 93 | comments: false 94 | } 95 | }) 96 | ] 97 | } 98 | ]; 99 | -------------------------------------------------------------------------------- /resources/js/modules/custom/hierarchy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import * as d3 from "../lib/d3"; 9 | import OrientationTopBottom from "../lib/chart/orientation/orientation-topBottom.js"; 10 | import OrientationBottomTop from "../lib/chart/orientation/orientation-bottomTop.js"; 11 | import {LAYOUT_VERTICAL_NODE_HEIGHT_OFFSET} from "../lib/constants.js"; 12 | 13 | /** 14 | * This class handles the hierarchical data. 15 | * 16 | * @author Rico Sonntag 17 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 18 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 19 | */ 20 | export default class Hierarchy 21 | { 22 | /** 23 | * Constructor. 24 | * 25 | * @param {Configuration} configuration The application configuration 26 | */ 27 | constructor(configuration) 28 | { 29 | this._configuration = configuration; 30 | this._nodes = null; 31 | this._root = null; 32 | } 33 | 34 | /** 35 | * Initialize the hierarchical chart data. 36 | * 37 | * @param {object} data The JSON encoded chart data 38 | */ 39 | init(data) 40 | { 41 | // Adjust box height if we are going to display the alternative names 42 | if (this._configuration.showAlternativeName) { 43 | if ((this._configuration.orientation instanceof OrientationTopBottom) 44 | || (this._configuration.orientation instanceof OrientationBottomTop) 45 | ) { 46 | this._configuration.orientation.boxHeight += LAYOUT_VERTICAL_NODE_HEIGHT_OFFSET; 47 | } 48 | } 49 | 50 | // // Declares a tree layout and assigns the size 51 | // const treeLayout = d3.tree() 52 | // .nodeSize([this._configuration.orientation.nodeWidth, 0]) 53 | // .separation(this.separation); 54 | 55 | // Construct root node from the hierarchical data 56 | this._root = d3.hierarchy(data); 57 | 58 | // Map the root node data to the tree layout 59 | // treeLayout(this._root); 60 | 61 | // Assign a unique ID to each node 62 | this._root.descendants().forEach((d, i) => { 63 | d.id = i; 64 | }); 65 | 66 | // this._nodes = treeLayout(root); 67 | // this._nodes = this._root.descendants(); 68 | // 69 | // // Remove the pseudo root node 70 | // this._nodes.shift(); 71 | } 72 | 73 | /** 74 | * Returns the nodes. 75 | * 76 | * @returns {Individual[]} 77 | * 78 | * @public 79 | */ 80 | get nodes() 81 | { 82 | return this._nodes; 83 | } 84 | 85 | /** 86 | * Returns the root note. 87 | * 88 | * @returns {Individual} 89 | * 90 | * @public 91 | */ 92 | get root() 93 | { 94 | return this._root; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/box.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import Image from "./box/image"; 9 | import Text from "./box/text"; 10 | 11 | /** 12 | * A person box. 13 | * 14 | * @author Rico Sonntag 15 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 16 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 17 | */ 18 | export default class Box 19 | { 20 | /** 21 | * Constructor. 22 | * 23 | * @param {Orientation} orientation The current orientation 24 | */ 25 | constructor(orientation) 26 | { 27 | // The default corner radius 28 | this._cornerRadius = 20; 29 | this._orientation = orientation; 30 | 31 | // Calculate values 32 | this._x = -(orientation.boxWidth / 2); 33 | this._y = -(orientation.boxHeight / 2); 34 | this._rx = this._cornerRadius; 35 | this._ry = this._cornerRadius; 36 | this._width = orientation.boxWidth; 37 | this._height = orientation.boxHeight; 38 | 39 | this._image = new Image(orientation, this._cornerRadius); 40 | } 41 | 42 | /** 43 | * Returns the X-coordinate of the center of the box. 44 | * 45 | * @returns {number} 46 | */ 47 | get x() 48 | { 49 | return this._x; 50 | } 51 | 52 | /** 53 | * Returns the Y-coordinate of the center of the box. 54 | * 55 | * @returns {number} 56 | */ 57 | get y() 58 | { 59 | return this._y; 60 | } 61 | 62 | /** 63 | * Returns the horizontal corner radius of the box. 64 | * 65 | * @returns {number} 66 | */ 67 | get rx() 68 | { 69 | return this._rx; 70 | } 71 | 72 | /** 73 | * Returns the vertical corner radius of the box. 74 | * 75 | * @returns {number} 76 | */ 77 | get ry() 78 | { 79 | return this._ry; 80 | } 81 | 82 | /** 83 | * Returns the width of the box. 84 | * 85 | * @returns {number} 86 | */ 87 | get width() 88 | { 89 | return this._width; 90 | } 91 | 92 | /** 93 | * Returns the height of the box. 94 | * 95 | * @returns {number} 96 | */ 97 | get height() 98 | { 99 | return this._height; 100 | } 101 | 102 | /** 103 | * Returns the image container. 104 | * 105 | * @returns {Image} 106 | */ 107 | get image() 108 | { 109 | return this._image; 110 | } 111 | 112 | /** 113 | * Returns the text container instance. 114 | * 115 | * @returns {Text} 116 | */ 117 | get text() 118 | { 119 | return new Text( 120 | this._orientation, 121 | this._image 122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /resources/views/modules/descendants-chart/config.phtml: -------------------------------------------------------------------------------- 1 | 24 | 25 | [route(ControlPanel::class) => I18N::translate('Control panel'), route(ModulesAllPage::class) => I18N::translate('Modules'), $title]]) ?> 26 | 27 |

28 |

29 | 30 |
31 |
32 | $configuration]) ?> 33 |
34 |
35 | $configuration]) ?> 36 |
37 |
38 | $configuration, 'moduleName' => $moduleName]) ?> 39 |
40 | 41 |
42 |
43 |
44 | 45 |
46 |
47 | 'hideSvgExport', 50 | 'label' => I18N::translate('Hide the SVG export button'), 51 | 'checked' => $configuration->getHideSvgExport(), 52 | 'unchecked' => '0', 53 | ]) 54 | ?> 55 | 56 |
57 | 'hidePngExport', 60 | 'label' => I18N::translate('Hide the PNG export button'), 61 | 'checked' => $configuration->getHidePngExport(), 62 | 'unchecked' => '0', 63 | ]) 64 | ?> 65 |
66 |
67 |
68 |
69 | 70 |
71 |
72 | 75 | 76 | 77 | 78 | 79 | 80 |
81 |
82 | 83 | 84 |
85 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/box/text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import OrientationLeftRight from "../orientation/orientation-leftRight"; 9 | import OrientationRightLeft from "../orientation/orientation-rightLeft"; 10 | import OrientationTopBottom from "../orientation/orientation-topBottom"; 11 | import OrientationBottomTop from "../orientation/orientation-bottomTop"; 12 | 13 | /** 14 | * The person text box container. 15 | * 16 | * @author Rico Sonntag 17 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 18 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 19 | */ 20 | export default class Text 21 | { 22 | /** 23 | * Constructor. 24 | * 25 | * @param {Orientation} orientation The current orientation 26 | * @param {null|Image} image The image 27 | */ 28 | constructor(orientation, image = null) 29 | { 30 | this._orientation = orientation; 31 | this._image = image; 32 | this._textPaddingX = 15; 33 | this._textPaddingY = 15; 34 | 35 | if ((this._orientation instanceof OrientationTopBottom) 36 | || (this._orientation instanceof OrientationBottomTop) 37 | ) { 38 | this._textPaddingX = 5; 39 | this._textPaddingY = 15; 40 | } 41 | 42 | // Calculate values 43 | this._x = this.calculateX(); 44 | this._y = this.calculateY(); 45 | this._width = this.calculateWidth(); 46 | } 47 | 48 | /** 49 | * Returns the calculated X-coordinate. 50 | * 51 | * @returns {number} 52 | */ 53 | calculateX() 54 | { 55 | return -(this._orientation.boxWidth / 2) + this._textPaddingX; 56 | } 57 | 58 | /** 59 | * Returns the calculated Y-coordinate. 60 | * 61 | * @returns {number} 62 | */ 63 | calculateY() 64 | { 65 | if ((this._orientation instanceof OrientationLeftRight) 66 | || (this._orientation instanceof OrientationRightLeft) 67 | ) { 68 | return -this._textPaddingY; 69 | } 70 | 71 | return this._image.y + this._image.height + (this._textPaddingY * 2); 72 | } 73 | 74 | /** 75 | * Calculate the available text width. 76 | * 77 | * @returns {number} 78 | */ 79 | calculateWidth() 80 | { 81 | // Width of the text minus the right/left padding 82 | return this._orientation.boxWidth - (this._textPaddingX * 2); 83 | } 84 | 85 | /** 86 | * Returns the X-coordinate of the text start. 87 | * 88 | * @returns {number} 89 | */ 90 | get x() 91 | { 92 | return this._x; 93 | } 94 | 95 | /** 96 | * Returns the Y-coordinate of the text start. 97 | * 98 | * @returns {number} 99 | */ 100 | get y() 101 | { 102 | return this._y; 103 | } 104 | 105 | /** 106 | * Returns the width of the text. 107 | * 108 | * @returns {number} 109 | */ 110 | get width() 111 | { 112 | return this._width; 113 | 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /resources/views/modules/descendants-chart/form/layout.phtml: -------------------------------------------------------------------------------- 1 | 20 | 21 |
22 |
23 | 24 |
25 |
26 | 'hideSpouses', 29 | 'label' => I18N::translate('Hide the spouses associated with an individual'), 30 | 'checked' => $configuration->getHideSpouses(), 31 | 'unchecked' => '0', 32 | ]) 33 | ?> 34 | 35 |
36 | 'showMarriedNames', 39 | 'label' => I18N::translate('Show married names'), 40 | 'checked' => $configuration->getShowMarriedNames(), 41 | 'unchecked' => '0', 42 | ]) 43 | ?> 44 | 45 | 46 | 47 |
48 | 49 |
50 | 'openNewTabOnClick', 53 | 'label' => I18N::translate('Open individual in new browser window/tab'), 54 | 'checked' => $configuration->getOpenNewTabOnClick(), 55 | 'unchecked' => '0', 56 | ]) 57 | ?> 58 | 59 | 60 | 61 |
62 | 63 |
64 | 'showAlternativeName', 67 | 'label' => I18N::translate('Show alternative name of individual'), 68 | 'checked' => $configuration->getShowAlternativeName(), 69 | 'unchecked' => '0', 70 | ]) 71 | ?> 72 | 73 | 74 | 75 |
76 |
77 |
78 | -------------------------------------------------------------------------------- /resources/js/modules/custom/data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import {Node} from "../lib/d3"; 9 | 10 | /** 11 | * This files defines the internal used structures of objects. 12 | * 13 | * @author Rico Sonntag 14 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 15 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 16 | */ 17 | 18 | /** 19 | * The plain person data. 20 | * 21 | * @typedef {object} Data 22 | * @property {number} id The unique ID of the person 23 | * @property {string} xref The unique identifier of the person 24 | * @property {string} sex The sex of the person 25 | * @property {string} birth The birthdate of the person 26 | * @property {string} death The death date of the person 27 | * @property {string} timespan The lifetime description 28 | * @property {string} thumbnail The URL of the thumbnail image 29 | * @property {string} name The full name of the individual 30 | * @property {string} preferredName The preferred first name 31 | * @property {string[]} firstNames The list of first names 32 | * @property {string[]} lastNames The list of last names 33 | * @property {string} alternativeName The alternative name of the individual 34 | */ 35 | 36 | /** 37 | * A person object. 38 | * 39 | * @typedef {object} Person 40 | * @property {null|Data} data The data object of the individual 41 | * @property {undefined|Number[]} spouses The list of assigned spouse IDs (not available if "spouse" is set) 42 | * @property {undefined|Object[]} children The list of children of this individual 43 | * @property {undefined|Number} family The family index (0 = first family, 1 = second, ...) 44 | * @property {undefined|Number} spouse The unique ID of the direct spouse of this individual 45 | */ 46 | 47 | /** 48 | * An individual. Extends the D3 Node object. 49 | * 50 | * @typedef {Node} Individual 51 | * @property {Person} data The individual data 52 | * @property {Individual[]} children The children of the node 53 | * @property {number} x The X-coordinate of the node 54 | * @property {number} y The Y-coordinate of the node 55 | */ 56 | 57 | /** 58 | * An X/Y coordinate. 59 | * 60 | * @typedef {object} Coordinate 61 | * @property {number} x The X-coordinate 62 | * @property {number} y The Y-coordinate 63 | */ 64 | 65 | /** 66 | * A link between two nodes. 67 | * 68 | * @typedef {object} Link 69 | * @property {Individual} source The source individual 70 | * @property {null|Individual} target The target individual 71 | * @property {null|undefined|Individual} spouse The spouse of the source individual 72 | * @property {null|Coordinate[]} coords The list of the spouse coordinates 73 | */ 74 | 75 | /** 76 | * @typedef {object} NameElementData 77 | * @property {Data} data 78 | * @property {boolean} isRtl 79 | * @property {boolean} isAltRtl 80 | * @property {boolean} withImage 81 | */ 82 | 83 | /** 84 | * @typedef {object} LabelElementData 85 | * @property {string} label 86 | * @property {boolean} isPreferred 87 | * @property {boolean} isLastName 88 | * @property {boolean} isNameRtl 89 | */ 90 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/svg/zoom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import * as d3 from "./../../d3"; 9 | 10 | /** 11 | * Constants 12 | * 13 | * @type {number} 14 | */ 15 | const MIN_ZOOM = 0.1; 16 | const MAX_ZOOM = 20.0; 17 | 18 | /** 19 | * This class handles the zoom. 20 | * 21 | * @author Rico Sonntag 22 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 23 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 24 | */ 25 | export default class Zoom 26 | { 27 | /** 28 | * Constructor. 29 | * 30 | * @param {Selection} parent The selected D3 parent element container 31 | */ 32 | constructor(parent) 33 | { 34 | this._zoom = null; 35 | this._parent = parent; 36 | 37 | this.init(); 38 | } 39 | 40 | /** 41 | * Initializes a new D3 zoom behavior. 42 | * 43 | * @private 44 | */ 45 | init() 46 | { 47 | // Setup zoom and pan 48 | this._zoom = d3.zoom(); 49 | 50 | this._zoom 51 | .scaleExtent([MIN_ZOOM, MAX_ZOOM]) 52 | .on("zoom", (event) => { 53 | this._parent.attr("transform", event.transform); 54 | }); 55 | 56 | // Adjust the wheel delta (see defaultWheelDelta() in zoom.js, which adds 57 | // a 10-times offset if ctrlKey is pressed) 58 | this._zoom.wheelDelta((event) => { 59 | return -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002); 60 | }); 61 | 62 | // Add zoom filter 63 | this._zoom.filter((event) => { 64 | // Allow "wheel" event only while the control key is pressed 65 | if (event.type === "wheel") { 66 | if (!event.ctrlKey) { 67 | return false; 68 | } 69 | 70 | const transform = d3.zoomTransform(this); 71 | 72 | if (transform.k) { 73 | // Prevent zooming below the lowest level 74 | if ((transform.k <= MIN_ZOOM) && (event.deltaY > 0)) { 75 | // Prevent browser page zoom while holding down the control key 76 | event.preventDefault(); 77 | return false; 78 | } 79 | 80 | // Prevent zooming above highest level 81 | if ((transform.k >= MAX_ZOOM) && (event.deltaY < 0)) { 82 | // Prevent browser page zoom while holding down the control key 83 | event.preventDefault(); 84 | return false; 85 | } 86 | } 87 | 88 | return true; 89 | } 90 | 91 | // Allow touch events only with two fingers 92 | if (!event.button && (event.type === "touchstart")) { 93 | return event.touches.length === 2; 94 | } 95 | 96 | return (!event.ctrlKey || event.type === 'wheel') && !event.button; 97 | }); 98 | } 99 | 100 | /** 101 | * Returns the internal d3 zoom behavior. 102 | * 103 | * @returns {d3.zoom} 104 | */ 105 | get() 106 | { 107 | return this._zoom; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Traits/ModuleConfigTrait.php: -------------------------------------------------------------------------------- 1 | 24 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 25 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 26 | */ 27 | trait ModuleConfigTrait 28 | { 29 | use \Fisharebest\Webtrees\Module\ModuleConfigTrait; 30 | 31 | /** 32 | * @param ServerRequestInterface $request 33 | * 34 | * @return ResponseInterface 35 | */ 36 | public function getAdminAction(ServerRequestInterface $request): ResponseInterface 37 | { 38 | $this->layout = 'layouts/administration'; 39 | 40 | return $this->viewResponse( 41 | $this->name() . '::modules/descendants-chart/config', 42 | [ 43 | 'configuration' => new Configuration($request, $this), 44 | 'moduleName' => $this->name(), 45 | 'title' => $this->title(), 46 | 'description' => $this->description(), 47 | ] 48 | ); 49 | } 50 | 51 | /** 52 | * @param ServerRequestInterface $request 53 | * 54 | * @return ResponseInterface 55 | */ 56 | public function postAdminAction(ServerRequestInterface $request): ResponseInterface 57 | { 58 | $configuration = new Configuration($request, $this); 59 | 60 | $this->setPreference( 61 | 'default_generations', 62 | (string) $configuration->getGenerations() 63 | ); 64 | $this->setPreference( 65 | 'default_layout', 66 | $configuration->getLayout() 67 | ); 68 | $this->setPreference( 69 | 'default_hideSpouses', 70 | (string) $configuration->getHideSpouses() 71 | ); 72 | $this->setPreference( 73 | 'default_showMarriedNames', 74 | (string) $configuration->getShowMarriedNames() 75 | ); 76 | $this->setPreference( 77 | 'default_openNewTabOnClick', 78 | (string) $configuration->getOpenNewTabOnClick() 79 | ); 80 | $this->setPreference( 81 | 'default_showAlternativeName', 82 | (string) $configuration->getShowAlternativeName() 83 | ); 84 | $this->setPreference( 85 | 'default_hideSvgExport', 86 | (string) $configuration->getHideSvgExport() 87 | ); 88 | $this->setPreference( 89 | 'default_hidePngExport', 90 | (string) $configuration->getHidePngExport() 91 | ); 92 | 93 | FlashMessages::addMessage( 94 | I18N::translate( 95 | 'The preferences for the module “%s” have been updated.', 96 | $this->title() 97 | ), 98 | 'success' 99 | ); 100 | 101 | return redirect($this->getConfigLink()); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Model/Node.php: -------------------------------------------------------------------------------- 1 | 20 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 21 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 22 | */ 23 | class Node implements JsonSerializable 24 | { 25 | /** 26 | * @var NodeData 27 | */ 28 | protected NodeData $data; 29 | 30 | /** 31 | * The ID of the spouse. 32 | * 33 | * @var int 34 | */ 35 | protected int $spouse = 0; 36 | 37 | /** 38 | * The ID of the family. 39 | * 40 | * @var int 41 | */ 42 | protected int $family = 0; 43 | 44 | /** 45 | * The list of children. 46 | * 47 | * @var Node[] 48 | */ 49 | protected array $children = []; 50 | 51 | /** 52 | * The list of all spouses. 53 | * 54 | * @var int[] 55 | */ 56 | protected array $spouses = []; 57 | 58 | /** 59 | * Constructor. 60 | * 61 | * @param NodeData $data 62 | */ 63 | public function __construct(NodeData $data) 64 | { 65 | $this->data = $data; 66 | } 67 | 68 | /** 69 | * @return NodeData 70 | */ 71 | public function getData(): NodeData 72 | { 73 | return $this->data; 74 | } 75 | 76 | /** 77 | * @param int $spouse 78 | * 79 | * @return Node 80 | */ 81 | public function setSpouse(int $spouse): Node 82 | { 83 | $this->spouse = $spouse; 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * @param int $family 90 | * 91 | * @return Node 92 | */ 93 | public function setFamily(int $family): Node 94 | { 95 | $this->family = $family; 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * @return Node[] 102 | */ 103 | public function getChildren(): array 104 | { 105 | return $this->children; 106 | } 107 | 108 | /** 109 | * @param Node[] $children 110 | * 111 | * @return Node 112 | */ 113 | public function setChildren(array $children): Node 114 | { 115 | $this->children = $children; 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * @param int $spouse 122 | * 123 | * @return Node 124 | */ 125 | public function addSpouse(int $spouse): Node 126 | { 127 | $this->spouses[] = $spouse; 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Returns the relevant data as an array. 134 | * 135 | * @return array 136 | */ 137 | public function jsonSerialize(): array 138 | { 139 | $jsonData = [ 140 | 'data' => $this->data, 141 | 'family' => $this->family, 142 | ]; 143 | 144 | if ($this->spouse !== 0) { 145 | $jsonData['spouse'] = $this->spouse; 146 | } 147 | 148 | if ($this->children !== []) { 149 | $jsonData['children'] = $this->children; 150 | } 151 | 152 | if ($this->spouses !== []) { 153 | $jsonData['spouses'] = $this->spouses; 154 | } 155 | 156 | return $jsonData; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 41 | ->setRules([ 42 | '@PSR12' => true, 43 | '@PER-CS2.0' => true, 44 | '@Symfony' => true, 45 | 46 | // Additional custom rules 47 | 'declare_strict_types' => true, 48 | 'concat_space' => [ 49 | 'spacing' => 'one', 50 | ], 51 | 'header_comment' => [ 52 | 'header' => $header, 53 | 'comment_type' => 'PHPDoc', 54 | 'location' => 'after_open', 55 | 'separate' => 'both', 56 | ], 57 | 'phpdoc_to_comment' => false, 58 | 'phpdoc_no_alias_tag' => false, 59 | 'no_superfluous_phpdoc_tags' => false, 60 | 'phpdoc_separation' => [ 61 | 'groups' => [ 62 | [ 63 | 'author', 64 | 'license', 65 | 'link', 66 | ], 67 | ], 68 | ], 69 | 'no_alias_functions' => true, 70 | 'whitespace_after_comma_in_array' => [ 71 | 'ensure_single_space' => true, 72 | ], 73 | 'single_line_throw' => false, 74 | 'self_accessor' => false, 75 | 'global_namespace_import' => [ 76 | 'import_classes' => true, 77 | 'import_constants' => true, 78 | 'import_functions' => true, 79 | ], 80 | 'function_declaration' => [ 81 | 'closure_function_spacing' => 'one', 82 | 'closure_fn_spacing' => 'one', 83 | ], 84 | 'binary_operator_spaces' => [ 85 | 'operators' => [ 86 | '=' => 'align_single_space_minimal', 87 | '=>' => 'align_single_space_minimal', 88 | ], 89 | ], 90 | 'yoda_style' => [ 91 | 'equal' => false, 92 | 'identical' => false, 93 | 'less_and_greater' => false, 94 | 'always_move_variable' => false, 95 | ], 96 | 'blank_line_before_statement' => [ 97 | 'statements' => [ 98 | 'return', 99 | 'if', 100 | 'throw', 101 | ], 102 | ], 103 | ]) 104 | ->setFinder( 105 | PhpCsFixer\Finder::create() 106 | ->exclude('.build') 107 | ->exclude('.github') 108 | ->in(__DIR__) 109 | ); 110 | 111 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/orientation/orientation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | /** 9 | * The orientation base class. 10 | * 11 | * @author Rico Sonntag 12 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 13 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 14 | */ 15 | export default class Orientation 16 | { 17 | /** 18 | * Constructor. 19 | * 20 | * @param {number} boxWidth The width of a single individual box 21 | * @param {number} boxHeight The height of a single individual box 22 | */ 23 | constructor(boxWidth, boxHeight) 24 | { 25 | // The distance between single nodes 26 | this._xOffset = 30; 27 | this._yOffset = 40; 28 | 29 | this._boxWidth = boxWidth; 30 | this._boxHeight = boxHeight; 31 | this._splittNames = false; 32 | } 33 | 34 | /** 35 | * Returns TRUE if the document is in RTL direction. 36 | * 37 | * @returns {boolean} 38 | */ 39 | get isDocumentRtl() 40 | { 41 | return document.dir === "rtl"; 42 | } 43 | 44 | /** 45 | * Returns the x-offset between two boxes. 46 | * 47 | * @returns {number} 48 | */ 49 | get xOffset() 50 | { 51 | return this._xOffset; 52 | } 53 | 54 | /** 55 | * Returns the y-offset between two boxes. 56 | * 57 | * @returns {number} 58 | */ 59 | get yOffset() 60 | { 61 | return this._yOffset; 62 | } 63 | 64 | /** 65 | * Returns whether to splitt the names on multiple lines or not. 66 | * 67 | * @returns {boolean} 68 | */ 69 | get splittNames() 70 | { 71 | return this._splittNames; 72 | } 73 | 74 | /** 75 | * Returns the width of the box. 76 | * 77 | * @returns {number} 78 | */ 79 | get boxWidth() 80 | { 81 | return this._boxWidth; 82 | } 83 | 84 | /** 85 | * Returns the height of the box. 86 | * 87 | * @returns {number} 88 | */ 89 | get boxHeight() 90 | { 91 | return this._boxHeight; 92 | } 93 | 94 | /** 95 | * Returns the height of the box. 96 | * 97 | * @params {number} boxHeight 98 | */ 99 | set boxHeight(boxHeight) 100 | { 101 | this._boxHeight = boxHeight; 102 | } 103 | 104 | /** 105 | * Returns the direction. 106 | * 107 | * @returns {number} 108 | */ 109 | get direction() 110 | { 111 | throw "Abstract method direction() not implemented"; 112 | } 113 | 114 | /** 115 | * Returns the width of the node. 116 | * 117 | * @returns {number} 118 | */ 119 | get nodeWidth() 120 | { 121 | throw "Abstract method nodeWidth() not implemented"; 122 | } 123 | 124 | /** 125 | * Returns the height of the node. 126 | * 127 | * @returns {number} 128 | */ 129 | get nodeHeight() 130 | { 131 | throw "Abstract method nodeHeight() not implemented"; 132 | } 133 | 134 | /** 135 | * Normalizes the x and/or y values of an entry. 136 | * 137 | * @param {Individual} d 138 | */ 139 | norm(d) 140 | { 141 | throw "Abstract method norm() not implemented"; 142 | } 143 | 144 | /** 145 | * Returns the elbow function depending on the orientation. 146 | * 147 | * @param {Link} link 148 | * 149 | * @returns {string} 150 | */ 151 | elbow(link) 152 | { 153 | throw "Abstract method elbow() not implemented"; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /resources/js/modules/lib/storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | /** 9 | * This class handles the storage of form values. 10 | * 11 | * @author Rico Sonntag 12 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 13 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 14 | */ 15 | export class Storage 16 | { 17 | /** 18 | * Constructor. 19 | * 20 | * @param {string} name The name of the storage 21 | */ 22 | constructor(name) 23 | { 24 | this._name = name; 25 | this._storage = JSON.parse(localStorage.getItem(this._name)) || {}; 26 | } 27 | 28 | /** 29 | * Register an HTML element. 30 | * 31 | * @param {string} name The ID of an HTML element 32 | */ 33 | register(name) 34 | { 35 | // Use "querySelector" here as the ID of checkbox elements may additionally contain a hyphen and the value 36 | // Query checked elements (radio and checkbox) separately 37 | let input = document.querySelector('input[id^="' + name + '"]:checked, select[id^="' + name + '"]') 38 | || document.querySelector('input[id^="' + name + '"]'); 39 | 40 | if (input === null) { 41 | return; 42 | } 43 | 44 | let storedValue = this.read(name); 45 | 46 | if (storedValue !== null) { 47 | if (input.type && (input.type === "radio")) { 48 | input.checked = storedValue; 49 | } else { 50 | if (input.type && (input.type === "checkbox")) { 51 | input.checked = storedValue; 52 | } else { 53 | input.value = storedValue; 54 | } 55 | } 56 | } else { 57 | this.onInput(input); 58 | } 59 | 60 | // Add event listener to all inputs by their IDs 61 | document 62 | .querySelectorAll('input[id^="' + name + '"], select[id^="' + name + '"]') 63 | .forEach( 64 | (input) => input.addEventListener("input", (event) => { 65 | this.onInput(event.target); 66 | }) 67 | ); 68 | } 69 | 70 | /** 71 | * This method stores the value of an input element depending on its type. 72 | * 73 | * @param {EventTarget|HTMLInputElement} element The HTML input element 74 | */ 75 | onInput(element) 76 | { 77 | if (element.type && (element.type === "checkbox")) { 78 | this.write(element.name, element.checked); 79 | } else { 80 | this.write(element.name, element.value); 81 | } 82 | } 83 | 84 | /** 85 | * Returns the stored value belonging to the HTML element id. 86 | * 87 | * @param {string} name The id or name of an HTML element 88 | * 89 | * @returns {null|String|Boolean|Number} 90 | */ 91 | read(name) 92 | { 93 | if (this._storage.hasOwnProperty(name)) { 94 | return this._storage[name]; 95 | } 96 | 97 | return null; 98 | } 99 | 100 | /** 101 | * Stores a value to the given HTML element id. 102 | * 103 | * @param {string} name The id or name of an HTML element 104 | * @param {string|Boolean|Number} value The value to store 105 | */ 106 | write(name, value) 107 | { 108 | this._storage[name] = value; 109 | 110 | try { 111 | localStorage.setItem(this._name, JSON.stringify(this._storage)); 112 | } 113 | catch (exception) { 114 | console.log( 115 | "There wasn't enough space to store '" + name + "' with value '" + value + "' in the local storage." 116 | ); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /resources/js/modules/custom/configuration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import OrientationCollection from "../lib/chart/orientation-collection"; 9 | import {LAYOUT_LEFTRIGHT} from "../lib/constants"; 10 | 11 | /** 12 | * This class handles the configuration of the application. 13 | * 14 | * @author Rico Sonntag 15 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 16 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 17 | */ 18 | export default class Configuration 19 | { 20 | /** 21 | * Constructor. 22 | * 23 | * @param {string[]} labels 24 | * @param {number} generations 25 | * @param {string} treeLayout 26 | * @param {boolean} openNewTabOnClick 27 | * @param {boolean} showAlternativeName 28 | * @param {boolean} rtl 29 | * @param {number} direction 30 | */ 31 | constructor( 32 | labels, 33 | generations = 4, 34 | treeLayout = LAYOUT_LEFTRIGHT, 35 | openNewTabOnClick = true, 36 | showAlternativeName = true, 37 | rtl = false, 38 | direction = 1 39 | ) { 40 | // The layout/orientation of the tree 41 | this._treeLayout = treeLayout; 42 | this._orientations = new OrientationCollection(); 43 | 44 | this._openNewTabOnClick = openNewTabOnClick; 45 | this._showAlternativeName = showAlternativeName; 46 | 47 | // 48 | this.duration = 750; 49 | 50 | // 51 | this.padding = 15; 52 | 53 | // Default number of generations to display 54 | this._generations = generations; 55 | 56 | // Left/Right padding of a text (used with truncation) 57 | this.textPadding = 8; 58 | 59 | // // Default font size, color and scaling 60 | this._fontSize = 14; 61 | this.fontColor = "rgb(0, 0, 0)"; 62 | 63 | // Duration of update animation if clicked on a person 64 | // this.updateDuration = 1250; 65 | 66 | this.rtl = rtl; 67 | this.labels = labels; 68 | 69 | // Direction is either 1 (forward) or -1 (backward) 70 | this.direction = direction; 71 | } 72 | 73 | /** 74 | * Returns the number of generations to display. 75 | * 76 | * @returns {number} 77 | */ 78 | get generations() 79 | { 80 | return this._generations; 81 | } 82 | 83 | /** 84 | * Sets the number of generations to display. 85 | * 86 | * @param {number} value The number of generations to display 87 | */ 88 | set generations(value) 89 | { 90 | this._generations = value; 91 | } 92 | 93 | /** 94 | * Returns the tree layout. 95 | * 96 | * @returns {string} 97 | */ 98 | get treeLayout() 99 | { 100 | return this._treeLayout; 101 | } 102 | 103 | /** 104 | * Sets the tree layout. 105 | * 106 | * @param {string} value Tree layout value 107 | */ 108 | set treeLayout(value) 109 | { 110 | this._treeLayout = value; 111 | } 112 | 113 | /** 114 | * Returns the current orientation. 115 | * 116 | * @returns {Orientation} 117 | */ 118 | get orientation() 119 | { 120 | return this._orientations.get()[this.treeLayout]; 121 | } 122 | 123 | /** 124 | * Returns TRUE or FALSE depending on whether to open the current individual's details page in a new tab. 125 | * 126 | * @returns {boolean} 127 | */ 128 | get openNewTabOnClick() 129 | { 130 | return this._openNewTabOnClick; 131 | } 132 | 133 | /** 134 | * Returns whether to show or hide the alternative name. 135 | * 136 | * @returns {boolean} 137 | */ 138 | get showAlternativeName() 139 | { 140 | return this._showAlternativeName; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /resources/js/modules/lib/tree/link-drawer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | /** 9 | * The class handles the creation of the tree. 10 | * 11 | * @author Rico Sonntag 12 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 13 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 14 | */ 15 | export default class LinkDrawer 16 | { 17 | /** 18 | * Constructor. 19 | * 20 | * @param {Svg} svg 21 | * @param {Configuration} configuration The configuration 22 | */ 23 | constructor(svg, configuration) 24 | { 25 | this._svg = svg; 26 | this._configuration = configuration; 27 | this._orientation = this._configuration.orientation; 28 | } 29 | 30 | /** 31 | * Draw the connecting lines. 32 | * 33 | * @param {Link[]} links Array of links 34 | * @param {Individual} source The root object 35 | * 36 | * @public 37 | */ 38 | drawLinks(links, source) 39 | { 40 | this._svg.visual 41 | .selectAll("path.link") 42 | .data(links) 43 | .join( 44 | enter => this.linkEnter(enter, source), 45 | update => this.linkUpdate(update), 46 | exit => this.linkExit(exit, source) 47 | ); 48 | } 49 | 50 | /** 51 | * Enter transition (new links). 52 | * 53 | * @param {Selection} enter 54 | * @param {Individual} source 55 | * 56 | * @private 57 | */ 58 | linkEnter(enter, source) 59 | { 60 | enter 61 | .append("path") 62 | .classed("link", true) 63 | .attr("d", link => this._orientation.elbow(link)) 64 | .call( 65 | g => g.transition() 66 | .duration(this._configuration.duration) 67 | .attr("opacity", 1) 68 | ); 69 | } 70 | 71 | /** 72 | * Update transition (existing links). 73 | * 74 | * @param {Selection} update 75 | * 76 | * @private 77 | */ 78 | linkUpdate(update) 79 | { 80 | // TODO Enable for transitions 81 | // update 82 | // .call( 83 | // g => g.transition() 84 | // // .duration(this._configuration.duration) 85 | // .attr("opacity", 1) 86 | // .attr("d", (link) => { 87 | // // link.source.x = source.x; 88 | // // link.source.y = source.y; 89 | // // 90 | // // if (link.target) { 91 | // // link.target.x = source.x; 92 | // // link.target.y = source.y; 93 | // // } 94 | // 95 | // return this._orientation.elbow(link); 96 | // }) 97 | // ); 98 | } 99 | 100 | /** 101 | * Exit transition (links to be removed). 102 | * 103 | * @param {Selection} exit 104 | * @param {Individual} source 105 | * 106 | * @private 107 | */ 108 | linkExit(exit, source) 109 | { 110 | // TODO Enable for transitions 111 | // exit 112 | // .call( 113 | // g => g.transition() 114 | // .duration(this._configuration.duration) 115 | // .attr("opacity", 0) 116 | // .attr("d", (link) => { 117 | // // link.source.x = source.x; 118 | // // link.source.y = source.y; 119 | // // 120 | // // if (link.target) { 121 | // // link.target.x = source.x; 122 | // // link.target.y = source.y; 123 | // // } 124 | // 125 | // return this._orientation.elbow(link); 126 | // }) 127 | // .remove() 128 | // ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/box/image.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import OrientationLeftRight from "../orientation/orientation-leftRight"; 9 | import OrientationRightLeft from "../orientation/orientation-rightLeft"; 10 | 11 | /** 12 | * The person image box container. 13 | * 14 | * @author Rico Sonntag 15 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 16 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 17 | */ 18 | export default class Image 19 | { 20 | /** 21 | * Constructor. 22 | * 23 | * @param {Orientation} orientation The current orientation 24 | * @param {number} cornerRadius The corner radius of the box 25 | */ 26 | constructor(orientation, cornerRadius) 27 | { 28 | this._orientation = orientation; 29 | this._cornerRadius = cornerRadius; 30 | 31 | this._imagePadding = 5; 32 | this._imageRadius = Math.min(40, (this._orientation.boxHeight / 2) - this._imagePadding); 33 | 34 | // Calculate values 35 | this._width = this.calculateImageWidth(); 36 | this._height = this.calculateImageHeight(); 37 | this._rx = this.calculateCornerRadius(); 38 | this._ry = this.calculateCornerRadius(); 39 | this._x = this.calculateX(); 40 | this._y = this.calculateY(); 41 | } 42 | 43 | /** 44 | * Returns the calculated X-coordinate. 45 | * 46 | * @returns {number} 47 | */ 48 | calculateX() 49 | { 50 | if ((this._orientation instanceof OrientationLeftRight) 51 | || (this._orientation instanceof OrientationRightLeft) 52 | ) { 53 | return this._orientation.isDocumentRtl 54 | ? (this._width - this._imagePadding) 55 | : (-(this._orientation.boxWidth - this._imagePadding) / 2) + this._imagePadding; 56 | } 57 | 58 | return -(this._orientation.boxWidth / 2) + (this._width / 2); 59 | } 60 | 61 | /** 62 | * Returns the calculated Y-coordinate. 63 | * 64 | * @returns {number} 65 | */ 66 | calculateY() 67 | { 68 | if ((this._orientation instanceof OrientationLeftRight) 69 | || (this._orientation instanceof OrientationRightLeft) 70 | ) { 71 | return -this._imageRadius; 72 | } 73 | 74 | return -((this._orientation.boxHeight - this._imagePadding) / 2) + this._imagePadding; 75 | } 76 | 77 | /** 78 | * Returns the calculated image width. 79 | * 80 | * @returns {number} 81 | */ 82 | calculateImageWidth() 83 | { 84 | return this._imageRadius * 2; 85 | } 86 | 87 | /** 88 | * Returns the calculated image height. 89 | * 90 | * @returns {number} 91 | */ 92 | calculateImageHeight() 93 | { 94 | return this._imageRadius * 2; 95 | } 96 | 97 | /** 98 | * Returns the calculated corner radius. 99 | * 100 | * @returns {number} 101 | */ 102 | calculateCornerRadius() 103 | { 104 | return this._cornerRadius - this._imagePadding; 105 | } 106 | 107 | /** 108 | * Returns the X-coordinate of the center of the image. 109 | * 110 | * @returns {number} 111 | */ 112 | get x() 113 | { 114 | return this._x; 115 | } 116 | 117 | /** 118 | * Returns the Y-coordinate of the center of the image. 119 | * 120 | * @returns {number} 121 | */ 122 | get y() 123 | { 124 | return this._y; 125 | } 126 | 127 | /** 128 | * Returns the horizontal corner radius of the image. 129 | * 130 | * @returns {number} 131 | */ 132 | get rx() 133 | { 134 | return this._rx; 135 | } 136 | 137 | /** 138 | * Returns the vertical corner radius of the image. 139 | * 140 | * @returns {number} 141 | */ 142 | get ry() 143 | { 144 | return this._ry; 145 | } 146 | 147 | /** 148 | * Returns the width of the image. 149 | * 150 | * @returns {number} 151 | */ 152 | get width() 153 | { 154 | return this._width; 155 | } 156 | 157 | /** 158 | * Returns the height of the image. 159 | * 160 | * @returns {number} 161 | */ 162 | get height() 163 | { 164 | return this._height; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magicsunday/webtrees-descendants-chart", 3 | "description": "This modules provides an SVG descendants chart for the [webtrees](https://www.webtrees.net) genealogy application.", 4 | "license": "GPL-3.0-or-later", 5 | "type": "webtrees-module", 6 | "keywords": [ 7 | "webtrees", 8 | "module", 9 | "descendant", 10 | "chart" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Rico Sonntag", 15 | "email": "mail@ricosonntag.de", 16 | "homepage": "https://ricosonntag.de", 17 | "role": "Developer" 18 | } 19 | ], 20 | "config": { 21 | "bin-dir": ".build/bin", 22 | "vendor-dir": ".build/vendor", 23 | "discard-changes": true, 24 | "sort-packages": true, 25 | "optimize-autoloader": true, 26 | "allow-plugins": { 27 | "magicsunday/webtrees-module-installer-plugin": true 28 | } 29 | }, 30 | "minimum-stability": "dev", 31 | "prefer-stable": true, 32 | "require": { 33 | "ext-dom": "*", 34 | "ext-json": "*", 35 | "fisharebest/webtrees": "~2.2.0 || dev-main", 36 | "magicsunday/webtrees-module-base": "^1.0", 37 | "magicsunday/webtrees-module-installer-plugin": "^1.3" 38 | }, 39 | "require-dev": { 40 | "friendsofphp/php-cs-fixer": "3.63.2", 41 | "overtrue/phplint": "^3.4 || ^9.0", 42 | "phpstan/phpstan": "^1.10", 43 | "phpstan/phpstan-strict-rules": "^1.5", 44 | "phpstan/phpstan-deprecation-rules": "^1.1", 45 | "rector/rector": "^1.0" 46 | }, 47 | "autoload": { 48 | "psr-4": { 49 | "MagicSunday\\Webtrees\\DescendantsChart\\": "src/" 50 | } 51 | }, 52 | "scripts": { 53 | "module:build": [ 54 | "### Remove any left over files", 55 | "rm -Rf webtrees-descendants-chart/", 56 | "### Checkout latest version of repository", 57 | "git archive --prefix=webtrees-descendants-chart/ HEAD --format=tar | tar -x", 58 | "### Install required components", 59 | "@composer require magicsunday/webtrees-module-base:^1.0", 60 | "### Copy base module to vendor directory", 61 | "mkdir -p webtrees-descendants-chart/vendor/magicsunday", 62 | "cp -r .build/vendor/magicsunday/webtrees-module-base webtrees-descendants-chart/vendor/magicsunday/webtrees-module-base", 63 | "### Remove all not required files from archive", 64 | "rm -rf webtrees-descendants-chart/.github", 65 | "rm -rf webtrees-descendants-chart/resources/js/modules", 66 | "rm -f webtrees-descendants-chart/.gitattributes", 67 | "rm -f webtrees-descendants-chart/.gitignore", 68 | "rm -f webtrees-descendants-chart/composer.json", 69 | "rm -f webtrees-descendants-chart/package.json", 70 | "rm -f webtrees-descendants-chart/rollup.config.js", 71 | "rm -f webtrees-descendants-chart/phpstan.neon", 72 | "rm -f webtrees-descendants-chart/phpstan-baseline.neon", 73 | "rm -f webtrees-descendants-chart/.php-cs-fixer.dist.php", 74 | "rm -f webtrees-descendants-chart/.phplint.yml", 75 | "rm -f webtrees-descendants-chart/rector.php", 76 | "### Create archive", 77 | "zip --quiet --recurse-paths --move -9 webtrees-descendants-chart.zip webtrees-descendants-chart" 78 | ], 79 | "ci:test:php:lint": [ 80 | "phplint" 81 | ], 82 | "ci:test:php:phpstan": [ 83 | "phpstan analyze" 84 | ], 85 | "ci:test:php:phpstan:baseline": [ 86 | "phpstan analyze --generate-baseline phpstan-baseline.neon --allow-empty-baseline" 87 | ], 88 | "ci:test:php:rector": [ 89 | "rector process --config rector.php --dry-run" 90 | ], 91 | "ci:cgl": [ 92 | "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix --diff --verbose" 93 | ], 94 | "ci:rector": [ 95 | "rector process --config rector.php" 96 | ], 97 | "ci:test": [ 98 | "@ci:test:php:lint", 99 | "@ci:test:php:phpstan", 100 | "@ci:test:php:rector", 101 | "@ci:cgl --dry-run" 102 | ], 103 | "module:check": [ 104 | "@ci:test" 105 | ] 106 | }, 107 | "scripts-descriptions": { 108 | "module:build": "Create a distribution file (webtrees-descendants-chart.zip)", 109 | "module:check": "Run various static analysis tools" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /resources/css/descendants-chart.css: -------------------------------------------------------------------------------- 1 | .wt-ajax-load:empty { 2 | min-height: 50dvh; 3 | } 4 | 5 | .webtrees-descendants-fullscreen-container::backdrop { 6 | background-color: var(--bs-body-bg); 7 | } 8 | 9 | .webtrees-descendants-fullscreen-container { 10 | position: relative; 11 | } 12 | 13 | /* Button toolbar */ 14 | .webtrees-descendants-fullscreen-container .btn-toolbar { 15 | margin-top: 1rem; 16 | margin-bottom: 1rem; 17 | } 18 | 19 | .webtrees-descendants-fullscreen-container .btn-chart { 20 | color: var(--bs-btn-bg); 21 | background: rgb(245, 245, 245); 22 | border: 1px solid rgb(210, 210, 210); 23 | width: 40px; 24 | height: 32px; 25 | border-radius: 30%; 26 | box-sizing: border-box; 27 | cursor: pointer; 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: center; 31 | text-align: center; 32 | position: relative; 33 | } 34 | 35 | .webtrees-descendants-fullscreen-container .btn-primary:hover { 36 | background: rgb(245, 245, 245); 37 | } 38 | 39 | .webtrees-descendants-fullscreen-container .btn-chart:hover { 40 | color: var(--link-color-hover); 41 | } 42 | 43 | .webtrees-descendants-fullscreen-container .btn-chart .icon { 44 | position: absolute; 45 | top: 50%; 46 | left: 50%; 47 | transform: translate(-50%, -50%); 48 | } 49 | 50 | .webtrees-descendants-fullscreen-container .btn-chart .icon .svg-inline--fa { 51 | height: 1.5em; 52 | vertical-align: -0.375em; 53 | } 54 | 55 | .webtrees-descendants-fullscreen-container .btn-fullscreen span:nth-child(2) { 56 | display: none; 57 | } 58 | 59 | /* Form */ 60 | .form-element-description { 61 | -webkit-box-decoration-break: clone; 62 | box-decoration-break: clone; 63 | } 64 | 65 | #webtrees-descendants-chart-form .row { 66 | margin-left: 0; 67 | margin-right: 0; 68 | } 69 | 70 | /* SVG */ 71 | .webtrees-descendants-chart-container { 72 | position: relative; 73 | font-size: unset; 74 | display: flex; 75 | flex: auto; 76 | } 77 | 78 | .webtrees-descendants-chart-container svg { 79 | display: block; 80 | cursor: grab; 81 | } 82 | 83 | .webtrees-descendants-chart-container svg:active { 84 | cursor: grabbing; 85 | } 86 | 87 | .webtrees-descendants-chart-container svg .person { 88 | cursor: pointer; 89 | } 90 | 91 | .webtrees-descendants-chart-container svg rect.background { 92 | fill: none; 93 | pointer-events: all; 94 | } 95 | 96 | .webtrees-descendants-chart-container div.overlay { 97 | position: absolute; 98 | top: 0; 99 | left: 0; 100 | text-align: center; 101 | width: 100%; 102 | height: 100%; 103 | margin: 0; 104 | padding: 0; 105 | border: 0; 106 | font: 10px sans-serif; 107 | pointer-events: none; 108 | transition: opacity ease-in-out; 109 | transition-duration: 0s; 110 | backdrop-filter: blur(5px); 111 | } 112 | 113 | @supports (-webkit-backdrop-filter: none) { 114 | .webtrees-descendants-chart-container div.overlay { 115 | -webkit-backdrop-filter: blur(1em); 116 | } 117 | } 118 | 119 | .webtrees-descendants-chart-container div.overlay .tooltip { 120 | font-size: 22px; 121 | color: #5a6268; 122 | position: relative; 123 | margin: 0; 124 | top: 50%; 125 | transform: translateY(-50%); 126 | opacity: 1; 127 | text-align: center; 128 | } 129 | 130 | @supports not ((-webkit-backdrop-filter: none) or (backdrop-filter: none)) { 131 | .webtrees-descendants-chart-container div.overlay { 132 | background: rgba(0, 0, 0, 0.5); 133 | } 134 | 135 | .webtrees-descendants-chart-container div.overlay .tooltip { 136 | color: white; 137 | } 138 | } 139 | 140 | /* Fullscreen */ 141 | [fullscreen] .webtrees-descendants-fullscreen-container .wt-page-content { 142 | padding: 0; 143 | } 144 | 145 | [fullscreen] .webtrees-descendants-fullscreen-container .btn-toolbar { 146 | position: absolute; 147 | top: 0; 148 | right: 0; 149 | z-index: 10; 150 | margin-top: 0.5rem; 151 | margin-right: 0.5rem; 152 | } 153 | 154 | [fullscreen] .webtrees-descendants-fullscreen-container .btn-fullscreen span:nth-child(1) { 155 | display: none; 156 | } 157 | 158 | [fullscreen] .webtrees-descendants-fullscreen-container .btn-fullscreen span:nth-child(2) { 159 | display: inline-block; 160 | } 161 | 162 | [fullscreen] .webtrees-descendants-fullscreen-container #exportPNG { 163 | display: none; 164 | } 165 | 166 | [fullscreen] .webtrees-descendants-fullscreen-container #exportSVG { 167 | display: none; 168 | } 169 | 170 | [fullscreen] .webtrees-descendants-chart-container { 171 | min-height: 100dvh; 172 | max-height: 100dvh; 173 | } 174 | -------------------------------------------------------------------------------- /resources/js/modules/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import * as d3 from "./lib/d3"; 9 | import Configuration from "./custom/configuration"; 10 | import Chart from "./lib/chart"; 11 | 12 | /** 13 | * The application class. 14 | * 15 | * @author Rico Sonntag 16 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 17 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 18 | */ 19 | export class DescendantsChart 20 | { 21 | /** 22 | * Constructor. 23 | * 24 | * @param {string} selector The CSS selector of the HTML element used to assign the chart too 25 | * @param {object} options A list of options passed from outside to the application 26 | * 27 | * @param {string[]} options.labels 28 | * @param {boolean} options.rtl 29 | * @param {number} options.generations 30 | * @param {string} options.treeLayout 31 | * @param {boolean} options.openNewTabOnClick 32 | * @param {boolean} options.showAlternativeName 33 | * @param {string[]} options.cssFiles 34 | * @param {Data[]} options.data 35 | */ 36 | constructor(selector, options) 37 | { 38 | this._selector = selector; 39 | this._parent = d3.select(this._selector); 40 | 41 | // Set up configuration 42 | this._configuration = new Configuration( 43 | options.labels, 44 | options.generations, 45 | options.treeLayout, 46 | options.openNewTabOnClick, 47 | options.showAlternativeName, 48 | options.rtl 49 | ); 50 | 51 | this._cssFiles = options.cssFiles; 52 | 53 | // Set up chart instance 54 | this._chart = new Chart(this._parent, this._configuration); 55 | 56 | this.init(); 57 | this.draw(options.data); 58 | } 59 | 60 | /** 61 | * Returns the configuration object. 62 | * 63 | * @returns {Configuration} 64 | */ 65 | get configuration() 66 | { 67 | return this._configuration; 68 | } 69 | 70 | /** 71 | * @private 72 | */ 73 | init() 74 | { 75 | // Bind click event on center button 76 | d3.select("#centerButton") 77 | .on("click", () => this._chart.center()); 78 | 79 | // Bind click event on export as PNG button 80 | d3.select("#exportPNG") 81 | .on("click", () => this.exportPNG()); 82 | 83 | // Bind click event on export as SVG button 84 | d3.select("#exportSVG") 85 | .on("click", () => this.exportSVG()); 86 | 87 | this.addEventListeners(); 88 | } 89 | 90 | /** 91 | * Add event listeners. 92 | */ 93 | addEventListeners() 94 | { 95 | // Listen for fullscreen change event 96 | document.addEventListener( 97 | "fullscreenchange", 98 | () => { 99 | if (document.fullscreenElement) { 100 | // Add attribute to the body element to indicate fullscreen state 101 | document.body.setAttribute("fullscreen", ""); 102 | } else { 103 | document.body.removeAttribute("fullscreen"); 104 | } 105 | 106 | this._chart.updateViewBox(); 107 | } 108 | ); 109 | 110 | // Listen for orientation change event 111 | screen.orientation.addEventListener( 112 | "change", 113 | () => { 114 | this._chart.updateViewBox(); 115 | }); 116 | } 117 | 118 | /** 119 | * Updates the chart. 120 | * 121 | * @param {string} url The update url 122 | */ 123 | update(url) 124 | { 125 | this._chart.update(url); 126 | } 127 | 128 | /** 129 | * Draws the chart. 130 | * 131 | * @param {object} data The JSON encoded chart data 132 | */ 133 | draw(data) 134 | { 135 | this._chart.data = data; 136 | this._chart.draw(); 137 | } 138 | 139 | /** 140 | * Exports the chart as PNG image and triggers a download. 141 | * 142 | * @private 143 | */ 144 | exportPNG() 145 | { 146 | this._chart.svg 147 | .export('png') 148 | .svgToImage(this._chart.svg, "descendants-chart.png"); 149 | } 150 | 151 | /** 152 | * Exports the chart as SVG image and triggers a download. 153 | * 154 | * @private 155 | */ 156 | exportSVG() 157 | { 158 | this._chart.svg 159 | .export('svg') 160 | .svgToImage( 161 | this._chart.svg, 162 | this._cssFiles, 163 | "webtrees-descendants-chart-container", 164 | "descendants-chart.svg" 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /resources/js/modules/lib/tree/elbow/vertical.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import * as d3 from "../../d3"; 9 | 10 | /** 11 | * Returns the path to draw the vertical connecting lines between the profile 12 | * boxes for Top/Bottom and Bottom/Top layout. 13 | * 14 | * @param {Link} link The link object 15 | * @param {Orientation} orientation The current orientation 16 | * 17 | * @returns {string} 18 | */ 19 | export default function(link, orientation) 20 | { 21 | const halfXOffset = orientation.xOffset / 2; 22 | const halfYOffset = orientation.yOffset / 2; 23 | 24 | let sourceX = link.source.x, 25 | sourceY = link.source.y; 26 | 27 | if ((typeof link.spouse !== "undefined") && (link.source.data.family === 0)) { 28 | // For the first family, the link to the child nodes begins between 29 | // the individual and the first spouse. 30 | sourceX -= (link.source.x - link.spouse.x) / 2; 31 | sourceY -= getFirstSpouseLinkOffset(link, orientation); 32 | } else { 33 | // For each additional family, the link to the child nodes begins at the additional spouse. 34 | sourceY += (orientation.boxHeight / 2) * orientation.direction; 35 | } 36 | 37 | // No spouse assigned to source node 38 | if (link.source.data.data === null) { 39 | sourceX -= (orientation.boxWidth / 2) + (halfXOffset / 2); 40 | sourceY += (orientation.boxHeight / 2) * orientation.direction; 41 | } 42 | 43 | if (link.target !== null) { 44 | let targetX = link.target.x, 45 | targetY = link.target.y - (orientation.direction * ((orientation.boxHeight / 2) + halfYOffset)); 46 | 47 | const path = d3.path(); 48 | 49 | // The line from source/spouse to target 50 | path.moveTo(sourceX, sourceY); 51 | path.lineTo(sourceX, targetY); 52 | path.lineTo(targetX, targetY); 53 | path.lineTo(targetX, targetY + (orientation.direction * halfYOffset)); 54 | 55 | return path.toString(); 56 | } 57 | 58 | return createLinksBetweenSpouses(link, orientation); 59 | } 60 | 61 | /** 62 | * Returns the path needed to draw the lines between each spouse. 63 | * 64 | * @param {Link} link The link object 65 | * @param {Orientation} orientation The current orientation 66 | * 67 | * @returns {string} 68 | */ 69 | function createLinksBetweenSpouses(link, orientation) 70 | { 71 | const path = d3.path(); 72 | 73 | // The distance from the line to the node. Causes the line to stop or begin just before the node, 74 | // instead of going straight to the node, so that the connection to another spouse is clearer. 75 | const lineStartOffset = 2; 76 | 77 | // Precomputed half width of box 78 | const boxWidthHalf = orientation.boxWidth / 2; 79 | 80 | let sourceY = link.source.y; 81 | 82 | // Handle multiple spouses 83 | if (link.spouse.data.spouses.length >= 0) { 84 | sourceY -= getFirstSpouseLinkOffset(link, orientation); 85 | } 86 | 87 | // Add a link between first spouse and source 88 | if (link.coords === null) { 89 | path.moveTo(link.spouse.x + boxWidthHalf, sourceY); 90 | path.lineTo(link.source.x - boxWidthHalf, sourceY); 91 | } 92 | 93 | // Append lines between the source and all spouses 94 | if (link.coords && (link.coords.length > 0)) { 95 | for (let i = 0; i < link.coords.length; ++i) { 96 | let startX = link.spouse.x + boxWidthHalf; 97 | let endX = link.coords[i].x - boxWidthHalf; 98 | 99 | if (i > 0) { 100 | startX = link.coords[i - 1].x + boxWidthHalf; 101 | } 102 | 103 | let startPosOffset = ((i > 0) ? lineStartOffset : 0); 104 | let endPosOffset = (((i + 1) <= link.coords.length) ? lineStartOffset : 0); 105 | 106 | path.moveTo(startX + startPosOffset, sourceY); 107 | path.lineTo(endX - endPosOffset, sourceY); 108 | } 109 | 110 | // Add last part from previous spouse to actual spouse 111 | path.moveTo( 112 | link.coords[link.coords.length - 1].x + boxWidthHalf + lineStartOffset, 113 | sourceY 114 | ); 115 | 116 | path.lineTo( 117 | link.source.x - boxWidthHalf, 118 | sourceY 119 | ); 120 | } 121 | 122 | return path.toString(); 123 | } 124 | 125 | /** 126 | * Calculates the offset for the coordinate of the first spouse. 127 | * 128 | * @param {Link} link The link object 129 | * @param {Orientation} orientation The current orientation 130 | * 131 | * @returns {number} 132 | */ 133 | function getFirstSpouseLinkOffset(link, orientation) 134 | { 135 | // The distance between the connecting lines when there are multiple spouses 136 | const spouseLineOffset = 5; 137 | 138 | return (link.source.data.family - Math.ceil(link.spouse.data.spouses.length / 2)) 139 | * orientation.direction 140 | * spouseLineOffset; 141 | } 142 | -------------------------------------------------------------------------------- /resources/js/modules/lib/tree/elbow/horizontal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import * as d3 from "../../d3"; 9 | 10 | /** 11 | * Returns the path to draw the horizontal connecting lines between the profile 12 | * boxes for Left/Right and Right/Left layout. 13 | * 14 | * @param {Link} link The link object 15 | * @param {Orientation} orientation The current orientation 16 | * 17 | * @returns {string} 18 | * 19 | * Curved edges => https://observablehq.com/@bumbeishvili/curved-edges-horizontal-d3-v3-v4-v5-v6 20 | */ 21 | export default function(link, orientation) 22 | { 23 | const halfXOffset = orientation.xOffset / 2; 24 | const halfYOffset = orientation.yOffset / 2; 25 | 26 | let sourceX = link.source.x, 27 | sourceY = link.source.y; 28 | 29 | if ((typeof link.spouse !== "undefined") && (link.source.data.family === 0)) { 30 | // For the first family, the link to the child nodes begins between 31 | // the individual and the first spouse. 32 | sourceX -= getFirstSpouseLinkOffset(link, orientation); 33 | sourceY -= (link.source.y - link.spouse.y) / 2; 34 | } else { 35 | // For each additional family, the link to the child nodes begins at the additional spouse. 36 | sourceX += (orientation.boxWidth / 2) * orientation.direction; 37 | } 38 | 39 | // No spouse assigned to source node 40 | if (link.source.data.data === null) { 41 | sourceX += (orientation.boxWidth / 2) * orientation.direction; 42 | sourceY -= (orientation.boxHeight / 2) + (halfYOffset / 2); 43 | } 44 | 45 | if (link.target !== null) { 46 | let targetX = link.target.x - (orientation.direction * ((orientation.boxWidth / 2) + halfXOffset)), 47 | targetY = link.target.y; 48 | 49 | const path = d3.path(); 50 | 51 | // The line from source/spouse to target 52 | path.moveTo(sourceX, sourceY); 53 | path.lineTo(targetX, sourceY); 54 | path.lineTo(targetX, targetY); 55 | path.lineTo(targetX + (orientation.direction * halfXOffset), targetY); 56 | 57 | return path.toString(); 58 | } 59 | 60 | return createLinksBetweenSpouses(link, orientation); 61 | } 62 | 63 | /** 64 | * Returns the path needed to draw the lines between each spouse. 65 | * 66 | * @param {Link} link The link object 67 | * @param {Orientation} orientation The current orientation 68 | * 69 | * @returns {string} 70 | */ 71 | function createLinksBetweenSpouses(link, orientation) 72 | { 73 | const path = d3.path(); 74 | 75 | // The distance from the line to the node. Causes the line to stop or begin just before the node, 76 | // instead of going straight to the node, so that the connection to another spouse is clearer. 77 | const lineStartOffset = 2; 78 | 79 | // Precomputed half height of box 80 | const boxHeightHalf = orientation.boxHeight / 2; 81 | 82 | let sourceX = link.source.x; 83 | 84 | // Handle multiple spouses 85 | if (link.spouse.data.spouses.length >= 0) { 86 | sourceX -= getFirstSpouseLinkOffset(link, orientation); 87 | } 88 | 89 | // Add a link between first spouse and source 90 | if (link.coords === null) { 91 | path.moveTo(sourceX, link.spouse.y + boxHeightHalf); 92 | path.lineTo(sourceX, link.source.y - boxHeightHalf); 93 | } 94 | 95 | // Append lines between the source and all spouses 96 | if (link.coords && (link.coords.length > 0)) { 97 | for (let i = 0; i < link.coords.length; ++i) { 98 | let startY = link.spouse.y + boxHeightHalf; 99 | let endY = link.coords[i].y - boxHeightHalf; 100 | 101 | if (i > 0) { 102 | startY = link.coords[i - 1].y + boxHeightHalf; 103 | } 104 | 105 | let startPosOffset = ((i > 0) ? lineStartOffset : 0); 106 | let endPosOffset = (((i + 1) <= link.coords.length) ? lineStartOffset : 0); 107 | 108 | path.moveTo(sourceX, startY + startPosOffset); 109 | path.lineTo(sourceX, endY - endPosOffset); 110 | } 111 | 112 | // Add last part from previous spouse to actual spouse 113 | path.moveTo( 114 | sourceX, 115 | link.coords[link.coords.length - 1].y + boxHeightHalf + lineStartOffset 116 | ); 117 | 118 | path.lineTo( 119 | sourceX, 120 | link.source.y - boxHeightHalf 121 | ); 122 | } 123 | 124 | return path.toString(); 125 | } 126 | 127 | /** 128 | * Calculates the offset for the coordinate of the first spouse. 129 | * 130 | * @param {Link} link The link object 131 | * @param {Orientation} orientation The current orientation 132 | * 133 | * @returns {number} 134 | */ 135 | function getFirstSpouseLinkOffset(link, orientation) 136 | { 137 | // The distance between the connecting lines when there are multiple spouses 138 | const spouseLineOffset = 5; 139 | 140 | return (link.source.data.family - Math.ceil(link.spouse.data.spouses.length / 2)) 141 | * orientation.direction 142 | * spouseLineOffset; 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest version](https://img.shields.io/github/v/release/magicsunday/webtrees-descendants-chart?sort=semver)](https://github.com/magicsunday/webtrees-descendants-chart/releases/latest) 2 | [![License](https://img.shields.io/github/license/magicsunday/webtrees-descendants-chart)](https://github.com/magicsunday/webtrees-descendants-chart/blob/main/LICENSE) 3 | [![CI](https://github.com/magicsunday/webtrees-descendants-chart/actions/workflows/ci.yml/badge.svg)](https://github.com/magicsunday/webtrees-descendants-chart/actions/workflows/ci.yml) 4 | 5 | 6 | 7 | * [Descendants chart](#descendants-chart) 8 | * [Installation](#installation) 9 | * [Manual installation](#manual-installation) 10 | * [Using Composer](#using-composer) 11 | * [Latest version](#latest-version) 12 | * [Using Git](#using-git) 13 | * [Configuration](#configuration) 14 | * [Usage](#usage) 15 | * [Development](#development) 16 | * [Run tests](#run-tests) 17 | 18 | 19 | 20 | # Descendants chart 21 | This module provides an SVG descendant chart for the [webtrees](https://www.webtrees.net) genealogical application. 22 | It is capable of displaying up to 25 generations of descendants from an individual. 23 | 24 | **But beware, if you select too many generations, it may take a while and even slow down your system significantly.** 25 | 26 | In addition to the descendants, the respective spouses are also displayed for a person. The display can be 27 | deactivated via the configuration form so that only the direct descendants are displayed. 28 | 29 | ![descendants-chart-4-generations](assets/descendants-chart-4-generations.png) 30 | 31 | *Fig. 1: A four generations descendants chart with spouses (drawn top to bottom)* 32 | 33 | 34 | ## Installation 35 | Requires webtrees 2.2. 36 | 37 | There are several ways to install the module. The method using [composer](#using-composer) is suitable 38 | for experienced users, as a developer you can also use [git](#using-git) to get a copy of the repository. For all other users, 39 | however, manual installation is recommended. 40 | 41 | ### Manual installation 42 | To manually install the module, perform the following steps: 43 | 44 | 1. Download the [latest release](https://github.com/magicsunday/webtrees-descendants-chart/releases/latest) of the module. 45 | 2. Upload the downloaded file to your web server. 46 | 3. Unzip the package into your ``modules_v4`` directory. 47 | 4. Rename the folder to ``webtrees-descendants-chart`` 48 | 49 | If everything was successful, you should see a subdirectory ``webtrees-descendants-chart`` with the unpacked content 50 | in the ``modules_v4`` directory. 51 | 52 | Then follow the steps described in [configuration](#configuration) and [usage](#usage). 53 | 54 | 55 | ### Using Composer 56 | Typically, to install with [composer](https://getcomposer.org/), just run the following command from the command line, 57 | from the root of your Webtrees installation. 58 | 59 | ```shell 60 | composer require magicsunday/webtrees-descendants-chart --update-no-dev 61 | ``` 62 | 63 | The module will automatically install into the ``modules_v4`` directory of your webtrees installation. 64 | To make this possible, the "magicsunday/webtrees-module-base" package is used. Approval within Composer 65 | may be required here to authorize the execution of the package. 66 | 67 | To remove the module run: 68 | ```shell 69 | composer remove magicsunday/webtrees-descendants-chart --update-no-dev 70 | ``` 71 | 72 | Then follow the steps described in [configuration](#configuration) and [usage](#usage). 73 | 74 | #### Latest version 75 | If you are using the development version of Webtrees (main branch), you may also need to install the development 76 | version of the module. For this, please use the following command: 77 | ```shell 78 | composer require magicsunday/webtrees-descendants-chart:dev-main --update-no-dev 79 | ``` 80 | 81 | 82 | ### Using Git 83 | If you are using ``git``, you could also clone the current main branch directly into your ``modules_v4`` directory 84 | by calling: 85 | 86 | ```shell 87 | git clone https://github.com/magicsunday/webtrees-descendants-chart.git modules_v4/webtrees-descendants-chart 88 | ``` 89 | 90 | Then follow the steps described in [configuration](#configuration) and [usage](#usage). 91 | 92 | 93 | ## Configuration 94 | Go to the control panel (admin section) of your installation and scroll down to the ``Modules`` section. Click 95 | on ``Charts`` (in subsection Genealogy). Enable the ``Descendants chart`` custom module (optionally disable the original 96 | installed descendant chart module) and save your settings. 97 | 98 | ![Control panel - Module administration](assets/control-panel-modules.png) 99 | 100 | *Fig. 2: Control panel - Module administration* 101 | 102 | ## Usage 103 | At the charts' menu, you will find a new link called `Descendants chart`. Use the provided configuration options 104 | to adjust the layout of the charts according to your needs. 105 | 106 | Furthermore, it is possible to export the generated tree diagram as an SVG or PNG image 107 | in order to be able to use it elsewhere. 108 | 109 | 110 | ## Development 111 | To build/update the javascript, run the following commands: 112 | 113 | ```shell 114 | nvm install node 115 | npm install 116 | npm run prepare 117 | ``` 118 | 119 | ### Run tests 120 | ```shell 121 | composer update 122 | 123 | composer ci:test 124 | composer ci:test:php:phpstan 125 | composer ci:test:php:lint 126 | composer ci:test:php:rector 127 | ``` 128 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/svg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import Defs from "./svg/defs"; 9 | import Zoom from "./svg/zoom"; 10 | import ExportFactory from "./svg/export-factory"; 11 | 12 | /** 13 | * SVG class 14 | * 15 | * @author Rico Sonntag 16 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 17 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 18 | */ 19 | export default class Svg 20 | { 21 | /** 22 | * Constructor. 23 | * 24 | * @param {Selection} parent The selected D3 parent element container 25 | * @param {Configuration} configuration The application configuration 26 | */ 27 | constructor(parent, configuration) 28 | { 29 | // Create the element 30 | this._element = parent.append("svg"); 31 | this._defs = new Defs(this._element); 32 | 33 | this._visual = null; 34 | this._zoom = null; 35 | this._configuration = configuration; 36 | 37 | this.init(); 38 | } 39 | 40 | /** 41 | * Returns the SVG definition instance. 42 | * 43 | * @returns {Defs} 44 | */ 45 | get defs() 46 | { 47 | return this._defs; 48 | } 49 | 50 | /** 51 | * Returns the SVG definition instance. 52 | * 53 | * @returns {Zoom} 54 | */ 55 | get zoom() 56 | { 57 | return this._zoom; 58 | } 59 | 60 | /** 61 | * 62 | * 63 | * @returns {Selection} 64 | */ 65 | get visual() 66 | { 67 | return this._visual; 68 | } 69 | 70 | /** 71 | * Initialize the element. 72 | * 73 | * @private 74 | */ 75 | init() 76 | { 77 | // Add SVG element 78 | this._element 79 | .attr("width", "100%") 80 | .attr("height", "100%") 81 | .attr("text-rendering", "optimizeLegibility") 82 | .attr("text-anchor", "middle") 83 | .attr("xmlns:xlink", "https://www.w3.org/1999/xlink"); 84 | 85 | // new Filter(this._defs.get()); 86 | } 87 | 88 | /** 89 | * Initialize the element events. 90 | * 91 | * @param {Overlay} overlay 92 | */ 93 | initEvents(overlay) 94 | { 95 | this._element 96 | .on("contextmenu", (event) => event.preventDefault()) 97 | .on("wheel", (event) => { 98 | if (!event.ctrlKey) { 99 | overlay.show( 100 | this._configuration.labels.zoom, 101 | 300, 102 | () => { 103 | overlay.hide(200, 600); 104 | } 105 | ); 106 | } 107 | }) 108 | .on("touchend", (event) => { 109 | if (event.touches.length < 2) { 110 | overlay.hide(0, 600); 111 | } 112 | }) 113 | .on("touchmove", (event) => { 114 | if (event.touches.length >= 2) { 115 | // Hide tooltip on more than two fingers 116 | overlay.hide(); 117 | } else { 118 | // Show tooltip if less than two fingers are used 119 | overlay.show(this._configuration.labels.move); 120 | } 121 | }) 122 | .on("click", (event) => this.doStopPropagation(event), true); 123 | 124 | if (this._configuration.rtl) { 125 | this._element.classed("rtl", true); 126 | } 127 | 128 | // Add a group 129 | this._visual = this._element.append("g"); 130 | 131 | // Add zoom 132 | this._zoom = new Zoom(this._visual); 133 | this._element.call(this._zoom.get()); 134 | } 135 | 136 | /** 137 | * Prevent default click and stop propagation. 138 | * 139 | * @param {Event} event 140 | * 141 | * @private 142 | */ 143 | doStopPropagation(event) 144 | { 145 | if (event.defaultPrevented) { 146 | event.stopPropagation(); 147 | } 148 | } 149 | 150 | /** 151 | * Exports the chart as PNG image and triggers a download. 152 | * 153 | * @param {string} type The export file type (either "png" or "svg") 154 | * 155 | * @returns {PngExport|SvgExport} 156 | */ 157 | export(type ) 158 | { 159 | const factory = new ExportFactory(); 160 | return factory.createExport(type); 161 | } 162 | 163 | /** 164 | * @returns {Node} 165 | */ 166 | node() 167 | { 168 | return this._element.node(); 169 | } 170 | 171 | /** 172 | * @param {function|string|null} select 173 | * 174 | * @returns {Selection} 175 | */ 176 | selectAll(select) 177 | { 178 | return this._element.selectAll(select); 179 | } 180 | 181 | /** 182 | * @param {string} name 183 | * 184 | * @returns {string|this} 185 | */ 186 | style(name) 187 | { 188 | return this._element.style(...arguments); 189 | } 190 | 191 | /** 192 | * @param {string} name 193 | * 194 | * @returns {string|this} 195 | */ 196 | attr(name) 197 | { 198 | return this._element.attr(...arguments); 199 | } 200 | 201 | /** 202 | * @returns {Transition} 203 | */ 204 | transition() 205 | { 206 | return this._element.transition(); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /resources/js/modules/lib/chart/svg/export/png.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the package magicsunday/webtrees-descendants-chart. 3 | * 4 | * For the full copyright and license information, please read the 5 | * LICENSE file distributed with this source code. 6 | */ 7 | 8 | import Export from "../export"; 9 | 10 | /** 11 | * Export the chart as PNG image. 12 | * 13 | * @author Rico Sonntag 14 | * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 15 | * @link https://github.com/magicsunday/webtrees-descendants-chart/ 16 | */ 17 | export default class PngExport extends Export 18 | { 19 | /** 20 | * Copies recursively all the styles from the list of container elements from the source 21 | * to the destination node. 22 | * 23 | * @param {Element} sourceNode 24 | * @param {Element} destinationNode 25 | */ 26 | copyStylesInline(sourceNode, destinationNode) 27 | { 28 | let containerElements = ["svg", "g", "text", "textPath"]; 29 | 30 | for (let i = 0; i < destinationNode.children.length; ++i) { 31 | let element = destinationNode.children[i]; 32 | 33 | if (containerElements.indexOf(element.tagName) !== -1) { 34 | this.copyStylesInline(sourceNode.children[i], element); 35 | continue; 36 | } 37 | 38 | let computedStyle = window.getComputedStyle(sourceNode.children[i]); 39 | 40 | for (let j = 0; j < computedStyle.length; ++j) { 41 | element.style.setProperty(computedStyle[j], computedStyle.getPropertyValue(computedStyle[j])); 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Returns the view-box of the SVG. Mainly used to apply a padding around the chart. 48 | * 49 | * @param {SVGGraphicsElement} svg The SVG element 50 | * 51 | * @returns {number[]} 52 | */ 53 | calculateViewBox(svg) 54 | { 55 | // Get bounding box 56 | const boundingBox = svg.getBBox(); 57 | const padding = 50; // Padding on each side 58 | 59 | // Return calculated view box 60 | return [ 61 | boundingBox.x - padding, 62 | boundingBox.y - padding, 63 | boundingBox.width + (padding * 2), 64 | boundingBox.height + (padding * 2) 65 | ]; 66 | } 67 | 68 | /** 69 | * 70 | * @param {number} width 71 | * @param {number} height 72 | * 73 | * @returns {HTMLCanvasElement} 74 | */ 75 | createCanvas(width, height) 76 | { 77 | let canvas = document.createElement("canvas"); 78 | canvas.width = width; 79 | canvas.height = height; 80 | 81 | return canvas; 82 | } 83 | 84 | /** 85 | * Converts the given SVG into a PNG image. Resolves to the PNG data URL. 86 | * 87 | * @param {SVGGraphicsElement} svg The SVG element 88 | * @param {number} width The width of the image 89 | * @param {number} height The height of the image 90 | * 91 | * @returns {Promise} 92 | */ 93 | convertToDataUrl(svg, width, height) 94 | { 95 | return new Promise(resolve => { 96 | let data = (new XMLSerializer()).serializeToString(svg); 97 | let DOMURL = window.URL || window.webkitURL || window; 98 | let svgBlob = new Blob([ data ], { type: "image/svg+xml;charset=utf-8" }); 99 | let url = DOMURL.createObjectURL(svgBlob); 100 | let img = new Image(); 101 | 102 | img.onload = () => { 103 | let canvas = this.createCanvas(width, height); 104 | let ctx = canvas.getContext("2d"); 105 | 106 | ctx.fillStyle = "rgb(255,255,255)"; 107 | ctx.fillRect(0, 0, canvas.width, canvas.height); 108 | ctx.drawImage(img, 0, 0); 109 | 110 | DOMURL.revokeObjectURL(url); 111 | 112 | let imgURI = canvas 113 | .toDataURL("image/png") 114 | .replace("image/png", "image/octet-stream"); 115 | 116 | resolve(imgURI); 117 | }; 118 | 119 | img.src = url; 120 | }); 121 | } 122 | 123 | /** 124 | * Clones the SVG element. 125 | * 126 | * @param {SVGGraphicsElement} svg 127 | * 128 | * @returns {Promise} 129 | */ 130 | cloneSvg(svg) 131 | { 132 | return new Promise(resolve => { 133 | let newSvg = svg.cloneNode(true); 134 | 135 | resolve(newSvg); 136 | }) 137 | } 138 | 139 | /** 140 | * Saves the given SVG as PNG image file. 141 | * 142 | * @param {Svg} svg The source SVG object 143 | * @param {string} fileName The file name 144 | */ 145 | svgToImage(svg, fileName) 146 | { 147 | // 300 DPI (good quality for printing) / 96 DPI (common browser) 148 | //let scale = 300 / dpi(); 149 | 150 | // Paper sizes (width, height) in pixel at 300 DPI/PPI 151 | const paperSize = { 152 | 'A3': [4960, 3508], 153 | 'A4': [3508, 2480], 154 | 'A5': [2480, 1748] 155 | }; 156 | 157 | this.cloneSvg(svg.node()) 158 | .then(newSvg => { 159 | this.copyStylesInline(svg.node(), newSvg); 160 | 161 | const viewBox = this.calculateViewBox(svg.node()); 162 | const width = Math.max(paperSize['A3'][0], viewBox[2]); 163 | const height = Math.max(paperSize['A3'][1], viewBox[3]); 164 | 165 | newSvg.setAttribute("width", "" + width); 166 | newSvg.setAttribute("height", "" + height); 167 | newSvg.setAttribute("viewBox", "" + viewBox); 168 | 169 | return this.convertToDataUrl(newSvg, width, height); 170 | }) 171 | .then(imgURI => this.triggerDownload(imgURI, fileName)) 172 | .catch(() => { 173 | console.log("Failed to save chart as PNG image"); 174 | }); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /resources/lang/en-US/messages.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: Descendants chart\n" 4 | "POT-Creation-Date: 2024-12-22 12:28+0100\n" 5 | "PO-Revision-Date: 2024-12-22 12:55+0100\n" 6 | "Last-Translator: Rico Sonntag \n" 7 | "Language-Team: \n" 8 | "Language: en_US\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 13 | "X-Generator: Poedit 3.5\n" 14 | "X-Poedit-Basepath: ../../../..\n" 15 | "X-Poedit-KeywordsList: translate\n" 16 | "X-Poedit-SearchPath-0: webtrees-descendants-chart/module.php\n" 17 | "X-Poedit-SearchPath-1: webtrees-descendants-chart/src\n" 18 | "X-Poedit-SearchPath-2: webtrees-descendants-chart/resources/views\n" 19 | "X-Poedit-SearchPath-3: webtrees-module-base/src\n" 20 | 21 | #: webtrees-descendants-chart/src/Configuration.php:173 22 | #: webtrees-descendants-chart/src/Configuration.php:181 23 | msgid "left" 24 | msgstr "" 25 | 26 | #: webtrees-descendants-chart/src/Configuration.php:174 27 | #: webtrees-descendants-chart/src/Configuration.php:182 28 | msgid "right" 29 | msgstr "" 30 | 31 | #: webtrees-descendants-chart/src/Configuration.php:175 32 | #: webtrees-descendants-chart/src/Configuration.php:183 33 | msgid "up" 34 | msgstr "" 35 | 36 | #: webtrees-descendants-chart/src/Configuration.php:176 37 | #: webtrees-descendants-chart/src/Configuration.php:184 38 | msgid "down" 39 | msgstr "" 40 | 41 | #: webtrees-descendants-chart/src/Module.php:131 42 | #: webtrees-descendants-chart/src/Module.php:247 43 | msgid "Descendants chart" 44 | msgstr "" 45 | 46 | #: webtrees-descendants-chart/src/Module.php:141 47 | msgid "An overview of an individual’s descendants." 48 | msgstr "" 49 | 50 | #: webtrees-descendants-chart/src/Module.php:244 51 | #: webtrees-descendants-chart/src/Traits/ModuleChartTrait.php:41 52 | #, php-format 53 | msgid "Descendants chart of %s" 54 | msgstr "" 55 | 56 | #: webtrees-descendants-chart/src/Module.php:260 57 | msgid "Use Ctrl + scroll to zoom in the view" 58 | msgstr "" 59 | 60 | #: webtrees-descendants-chart/src/Module.php:261 61 | msgid "Move the view with two fingers" 62 | msgstr "" 63 | 64 | #: webtrees-descendants-chart/src/Traits/ModuleConfigTrait.php:95 65 | #, php-format 66 | msgid "The preferences for the module “%s” have been updated." 67 | msgstr "" 68 | 69 | #: webtrees-module-base/src/Processor/DateProcessor.php:129 70 | #, php-format 71 | msgid "Born: %s" 72 | msgstr "" 73 | 74 | #: webtrees-module-base/src/Processor/DateProcessor.php:133 75 | #, php-format 76 | msgid "Died: %s" 77 | msgstr "" 78 | 79 | #: webtrees-module-base/src/Processor/DateProcessor.php:137 80 | msgid "Deceased" 81 | msgstr "" 82 | 83 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/center-button.phtml:18 84 | msgid "Re center" 85 | msgstr "" 86 | 87 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/fullscreen-button.phtml:18 88 | msgid "Switch between full screen mode and normal view" 89 | msgstr "" 90 | 91 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/help-button.phtml:17 92 | msgid "Show help" 93 | msgstr "" 94 | 95 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/png-export-button.phtml:18 96 | msgid "Export as PNG" 97 | msgstr "" 98 | 99 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/svg-export-button.phtml:18 100 | msgid "Export as SVG" 101 | msgstr "" 102 | 103 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:25 104 | msgid "Control panel" 105 | msgstr "" 106 | 107 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:25 108 | msgid "Modules" 109 | msgstr "" 110 | 111 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:44 112 | msgid "General" 113 | msgstr "" 114 | 115 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:50 116 | msgid "Hide the SVG export button" 117 | msgstr "" 118 | 119 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:60 120 | msgid "Hide the PNG export button" 121 | msgstr "" 122 | 123 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:73 124 | msgid "save" 125 | msgstr "" 126 | 127 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:78 128 | msgid "cancel" 129 | msgstr "" 130 | 131 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/generations.phtml:22 132 | msgid "Generations" 133 | msgstr "" 134 | 135 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:23 136 | msgid "Layout" 137 | msgstr "" 138 | 139 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:29 140 | msgid "Hide the spouses associated with an individual" 141 | msgstr "" 142 | 143 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:39 144 | msgid "Show married names" 145 | msgstr "" 146 | 147 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:45 148 | msgid "" 149 | "Shows the married name of the partner if this has a separate name entry of " 150 | "the type \"MARRIED\" or \"_MARNM\"." 151 | msgstr "" 152 | 153 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:53 154 | msgid "Open individual in new browser window/tab" 155 | msgstr "" 156 | 157 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:59 158 | msgid "" 159 | "Open the current individual's detail page in a new browser window/tab when " 160 | "it's left-clicked, otherwise the current window/tab is used." 161 | msgstr "" 162 | 163 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:67 164 | msgid "Show alternative name of individual" 165 | msgstr "" 166 | 167 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:73 168 | msgid "" 169 | "Displays a person's alternate name, if available. This simultaneously " 170 | "enlarges the individuals' respective boxes to create the necessary space for " 171 | "the display." 172 | msgstr "" 173 | 174 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/orientation.phtml:22 175 | msgid "Orientation" 176 | msgstr "" 177 | 178 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:39 179 | msgid "Individual" 180 | msgstr "" 181 | 182 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:64 183 | msgid "view" 184 | msgstr "" 185 | 186 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:75 187 | msgid "Show more options" 188 | msgstr "" 189 | 190 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:76 191 | msgid "Hide more options" 192 | msgstr "" 193 | -------------------------------------------------------------------------------- /resources/views/modules/descendants-chart/page.phtml: -------------------------------------------------------------------------------- 1 | 30 | 31 |
32 |

33 | 34 |
35 | 36 | 37 |
38 | 41 |
42 | 'xref', 45 | 'individual' => $individual, 46 | 'tree' => $tree, 47 | 'required' => true, 48 | ]) 49 | ?> 50 |
51 |
52 | 53 | $configuration]) ?> 54 | $configuration]) ?> 55 | 56 |
57 | $configuration, 'moduleName' => $moduleName]) ?> 58 |
59 | 60 |
61 |
62 |
63 |
64 | 65 |
66 | 67 |
68 | 78 |
79 |
80 |
81 |
82 |
83 | 84 |
85 | $configuration, 'moduleName' => $moduleName]) ?> 86 | 87 |
88 |
89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 179 | 180 | -------------------------------------------------------------------------------- /resources/lang/sv/messages.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: Descendants chart\n" 4 | "POT-Creation-Date: 2024-12-22 12:29+0100\n" 5 | "PO-Revision-Date: 2024-12-22 12:56+0100\n" 6 | "Last-Translator: Rico Sonntag \n" 7 | "Language-Team: \n" 8 | "Language: sv_SE\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 13 | "X-Generator: Poedit 3.5\n" 14 | "X-Poedit-Basepath: ../../../..\n" 15 | "X-Poedit-KeywordsList: translate\n" 16 | "X-Poedit-SearchPath-0: webtrees-descendants-chart/module.php\n" 17 | "X-Poedit-SearchPath-1: webtrees-descendants-chart/src\n" 18 | "X-Poedit-SearchPath-2: webtrees-descendants-chart/resources/views\n" 19 | "X-Poedit-SearchPath-3: webtrees-module-base/src\n" 20 | 21 | #: webtrees-descendants-chart/src/Configuration.php:173 22 | #: webtrees-descendants-chart/src/Configuration.php:181 23 | msgid "left" 24 | msgstr "vänster" 25 | 26 | #: webtrees-descendants-chart/src/Configuration.php:174 27 | #: webtrees-descendants-chart/src/Configuration.php:182 28 | msgid "right" 29 | msgstr "höger" 30 | 31 | #: webtrees-descendants-chart/src/Configuration.php:175 32 | #: webtrees-descendants-chart/src/Configuration.php:183 33 | msgid "up" 34 | msgstr "upp" 35 | 36 | #: webtrees-descendants-chart/src/Configuration.php:176 37 | #: webtrees-descendants-chart/src/Configuration.php:184 38 | msgid "down" 39 | msgstr "ner" 40 | 41 | #: webtrees-descendants-chart/src/Module.php:131 42 | #: webtrees-descendants-chart/src/Module.php:247 43 | msgid "Descendants chart" 44 | msgstr "Ättlingar" 45 | 46 | #: webtrees-descendants-chart/src/Module.php:141 47 | msgid "An overview of an individual’s descendants." 48 | msgstr "En översikt över en persons ättlingar." 49 | 50 | #: webtrees-descendants-chart/src/Module.php:244 51 | #: webtrees-descendants-chart/src/Traits/ModuleChartTrait.php:41 52 | #, php-format 53 | msgid "Descendants chart of %s" 54 | msgstr "Ättlingar till %s" 55 | 56 | #: webtrees-descendants-chart/src/Module.php:260 57 | msgid "Use Ctrl + scroll to zoom in the view" 58 | msgstr "Använd CTRL-scroll för att zooma i vyn" 59 | 60 | #: webtrees-descendants-chart/src/Module.php:261 61 | msgid "Move the view with two fingers" 62 | msgstr "Flytta vyn med två fingrar" 63 | 64 | #: webtrees-descendants-chart/src/Traits/ModuleConfigTrait.php:95 65 | #, php-format 66 | msgid "The preferences for the module “%s” have been updated." 67 | msgstr "" 68 | 69 | #: webtrees-module-base/src/Processor/DateProcessor.php:129 70 | #, php-format 71 | msgid "Born: %s" 72 | msgstr "Född: %s" 73 | 74 | #: webtrees-module-base/src/Processor/DateProcessor.php:133 75 | #, php-format 76 | msgid "Died: %s" 77 | msgstr "Död: %s" 78 | 79 | #: webtrees-module-base/src/Processor/DateProcessor.php:137 80 | msgid "Deceased" 81 | msgstr "Avliden" 82 | 83 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/center-button.phtml:18 84 | msgid "Re center" 85 | msgstr "Centrera" 86 | 87 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/fullscreen-button.phtml:18 88 | msgid "Switch between full screen mode and normal view" 89 | msgstr "" 90 | 91 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/help-button.phtml:17 92 | msgid "Show help" 93 | msgstr "" 94 | 95 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/png-export-button.phtml:18 96 | msgid "Export as PNG" 97 | msgstr "Exportera som PNG" 98 | 99 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/svg-export-button.phtml:18 100 | msgid "Export as SVG" 101 | msgstr "Exportera som SVG" 102 | 103 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:25 104 | msgid "Control panel" 105 | msgstr "" 106 | 107 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:25 108 | msgid "Modules" 109 | msgstr "" 110 | 111 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:44 112 | msgid "General" 113 | msgstr "" 114 | 115 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:50 116 | msgid "Hide the SVG export button" 117 | msgstr "" 118 | 119 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:60 120 | msgid "Hide the PNG export button" 121 | msgstr "" 122 | 123 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:73 124 | msgid "save" 125 | msgstr "" 126 | 127 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:78 128 | msgid "cancel" 129 | msgstr "" 130 | 131 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/generations.phtml:22 132 | msgid "Generations" 133 | msgstr "Generationer" 134 | 135 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:23 136 | msgid "Layout" 137 | msgstr "" 138 | 139 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:29 140 | msgid "Hide the spouses associated with an individual" 141 | msgstr "" 142 | 143 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:39 144 | msgid "Show married names" 145 | msgstr "" 146 | 147 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:45 148 | msgid "" 149 | "Shows the married name of the partner if this has a separate name entry of " 150 | "the type \"MARRIED\" or \"_MARNM\"." 151 | msgstr "" 152 | 153 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:53 154 | msgid "Open individual in new browser window/tab" 155 | msgstr "" 156 | 157 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:59 158 | msgid "" 159 | "Open the current individual's detail page in a new browser window/tab when " 160 | "it's left-clicked, otherwise the current window/tab is used." 161 | msgstr "" 162 | 163 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:67 164 | msgid "Show alternative name of individual" 165 | msgstr "" 166 | 167 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:73 168 | msgid "" 169 | "Displays a person's alternate name, if available. This simultaneously " 170 | "enlarges the individuals' respective boxes to create the necessary space for " 171 | "the display." 172 | msgstr "" 173 | 174 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/orientation.phtml:22 175 | msgid "Orientation" 176 | msgstr "Orientering" 177 | 178 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:39 179 | msgid "Individual" 180 | msgstr "Individ" 181 | 182 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:64 183 | msgid "view" 184 | msgstr "visa" 185 | 186 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:75 187 | msgid "Show more options" 188 | msgstr "" 189 | 190 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:76 191 | msgid "Hide more options" 192 | msgstr "" 193 | -------------------------------------------------------------------------------- /resources/lang/da/messages.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: Descendants chart\n" 4 | "POT-Creation-Date: 2024-12-22 12:28+0100\n" 5 | "PO-Revision-Date: 2024-12-22 12:54+0100\n" 6 | "Last-Translator: Rico Sonntag \n" 7 | "Language-Team: \n" 8 | "Language: da\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 13 | "X-Generator: Poedit 3.5\n" 14 | "X-Poedit-Basepath: ../../../..\n" 15 | "X-Poedit-KeywordsList: translate\n" 16 | "X-Poedit-SearchPath-0: webtrees-descendants-chart/module.php\n" 17 | "X-Poedit-SearchPath-1: webtrees-descendants-chart/src\n" 18 | "X-Poedit-SearchPath-2: webtrees-descendants-chart/resources/views\n" 19 | "X-Poedit-SearchPath-3: webtrees-module-base/src\n" 20 | 21 | #: webtrees-descendants-chart/src/Configuration.php:173 22 | #: webtrees-descendants-chart/src/Configuration.php:181 23 | msgid "left" 24 | msgstr "venstre" 25 | 26 | #: webtrees-descendants-chart/src/Configuration.php:174 27 | #: webtrees-descendants-chart/src/Configuration.php:182 28 | msgid "right" 29 | msgstr "højre" 30 | 31 | #: webtrees-descendants-chart/src/Configuration.php:175 32 | #: webtrees-descendants-chart/src/Configuration.php:183 33 | msgid "up" 34 | msgstr "op" 35 | 36 | #: webtrees-descendants-chart/src/Configuration.php:176 37 | #: webtrees-descendants-chart/src/Configuration.php:184 38 | msgid "down" 39 | msgstr "ned" 40 | 41 | #: webtrees-descendants-chart/src/Module.php:131 42 | #: webtrees-descendants-chart/src/Module.php:247 43 | msgid "Descendants chart" 44 | msgstr "Efterkommere" 45 | 46 | #: webtrees-descendants-chart/src/Module.php:141 47 | msgid "An overview of an individual’s descendants." 48 | msgstr "En oversigt over en persons efterkommere." 49 | 50 | #: webtrees-descendants-chart/src/Module.php:244 51 | #: webtrees-descendants-chart/src/Traits/ModuleChartTrait.php:41 52 | #, php-format 53 | msgid "Descendants chart of %s" 54 | msgstr "Graf over efterkommere af %s" 55 | 56 | #: webtrees-descendants-chart/src/Module.php:260 57 | msgid "Use Ctrl + scroll to zoom in the view" 58 | msgstr "Brug Ctrl + scroll for at zoome i visningen" 59 | 60 | #: webtrees-descendants-chart/src/Module.php:261 61 | msgid "Move the view with two fingers" 62 | msgstr "Flyt visningen med to fingre" 63 | 64 | #: webtrees-descendants-chart/src/Traits/ModuleConfigTrait.php:95 65 | #, php-format 66 | msgid "The preferences for the module “%s” have been updated." 67 | msgstr "" 68 | 69 | #: webtrees-module-base/src/Processor/DateProcessor.php:129 70 | #, php-format 71 | msgid "Born: %s" 72 | msgstr "Født: %s" 73 | 74 | #: webtrees-module-base/src/Processor/DateProcessor.php:133 75 | #, php-format 76 | msgid "Died: %s" 77 | msgstr "Død: %s" 78 | 79 | #: webtrees-module-base/src/Processor/DateProcessor.php:137 80 | msgid "Deceased" 81 | msgstr "Død" 82 | 83 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/center-button.phtml:18 84 | msgid "Re center" 85 | msgstr "Gen-centrer" 86 | 87 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/fullscreen-button.phtml:18 88 | msgid "Switch between full screen mode and normal view" 89 | msgstr "" 90 | 91 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/help-button.phtml:17 92 | msgid "Show help" 93 | msgstr "" 94 | 95 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/png-export-button.phtml:18 96 | msgid "Export as PNG" 97 | msgstr "Eksporter som PNG" 98 | 99 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/svg-export-button.phtml:18 100 | msgid "Export as SVG" 101 | msgstr "Eksporter som SVG" 102 | 103 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:25 104 | msgid "Control panel" 105 | msgstr "" 106 | 107 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:25 108 | msgid "Modules" 109 | msgstr "" 110 | 111 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:44 112 | msgid "General" 113 | msgstr "" 114 | 115 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:50 116 | msgid "Hide the SVG export button" 117 | msgstr "" 118 | 119 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:60 120 | msgid "Hide the PNG export button" 121 | msgstr "" 122 | 123 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:73 124 | msgid "save" 125 | msgstr "" 126 | 127 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:78 128 | msgid "cancel" 129 | msgstr "" 130 | 131 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/generations.phtml:22 132 | msgid "Generations" 133 | msgstr "Generationer" 134 | 135 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:23 136 | msgid "Layout" 137 | msgstr "" 138 | 139 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:29 140 | msgid "Hide the spouses associated with an individual" 141 | msgstr "" 142 | 143 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:39 144 | msgid "Show married names" 145 | msgstr "" 146 | 147 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:45 148 | msgid "" 149 | "Shows the married name of the partner if this has a separate name entry of " 150 | "the type \"MARRIED\" or \"_MARNM\"." 151 | msgstr "" 152 | 153 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:53 154 | msgid "Open individual in new browser window/tab" 155 | msgstr "" 156 | 157 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:59 158 | msgid "" 159 | "Open the current individual's detail page in a new browser window/tab when " 160 | "it's left-clicked, otherwise the current window/tab is used." 161 | msgstr "" 162 | 163 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:67 164 | msgid "Show alternative name of individual" 165 | msgstr "" 166 | 167 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:73 168 | msgid "" 169 | "Displays a person's alternate name, if available. This simultaneously " 170 | "enlarges the individuals' respective boxes to create the necessary space for " 171 | "the display." 172 | msgstr "" 173 | 174 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/orientation.phtml:22 175 | msgid "Orientation" 176 | msgstr "Orientering" 177 | 178 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:39 179 | msgid "Individual" 180 | msgstr "Person" 181 | 182 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:64 183 | msgid "view" 184 | msgstr "vis" 185 | 186 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:75 187 | msgid "Show more options" 188 | msgstr "" 189 | 190 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:76 191 | msgid "Hide more options" 192 | msgstr "" 193 | -------------------------------------------------------------------------------- /resources/lang/ru/messages.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: Descendants chart\n" 4 | "POT-Creation-Date: 2024-12-22 12:29+0100\n" 5 | "PO-Revision-Date: 2024-12-22 12:56+0100\n" 6 | "Last-Translator: Rico Sonntag \n" 7 | "Language-Team: \n" 8 | "Language: ru\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 13 | "X-Generator: Poedit 3.5\n" 14 | "X-Poedit-Basepath: ../../../..\n" 15 | "X-Poedit-KeywordsList: translate\n" 16 | "X-Poedit-SearchPath-0: webtrees-descendants-chart/module.php\n" 17 | "X-Poedit-SearchPath-1: webtrees-descendants-chart/src\n" 18 | "X-Poedit-SearchPath-2: webtrees-descendants-chart/resources/views\n" 19 | "X-Poedit-SearchPath-3: webtrees-module-base/src\n" 20 | 21 | #: webtrees-descendants-chart/src/Configuration.php:173 22 | #: webtrees-descendants-chart/src/Configuration.php:181 23 | msgid "left" 24 | msgstr "влево" 25 | 26 | #: webtrees-descendants-chart/src/Configuration.php:174 27 | #: webtrees-descendants-chart/src/Configuration.php:182 28 | msgid "right" 29 | msgstr "вправо" 30 | 31 | #: webtrees-descendants-chart/src/Configuration.php:175 32 | #: webtrees-descendants-chart/src/Configuration.php:183 33 | msgid "up" 34 | msgstr "вверх" 35 | 36 | #: webtrees-descendants-chart/src/Configuration.php:176 37 | #: webtrees-descendants-chart/src/Configuration.php:184 38 | msgid "down" 39 | msgstr "вниз" 40 | 41 | #: webtrees-descendants-chart/src/Module.php:131 42 | #: webtrees-descendants-chart/src/Module.php:247 43 | msgid "Descendants chart" 44 | msgstr "Потомки (новое)" 45 | 46 | #: webtrees-descendants-chart/src/Module.php:141 47 | msgid "An overview of an individual’s descendants." 48 | msgstr "Диаграмма потомков в виде дерева." 49 | 50 | #: webtrees-descendants-chart/src/Module.php:244 51 | #: webtrees-descendants-chart/src/Traits/ModuleChartTrait.php:41 52 | #, php-format 53 | msgid "Descendants chart of %s" 54 | msgstr "Диаграмма потомков у %s" 55 | 56 | #: webtrees-descendants-chart/src/Module.php:260 57 | msgid "Use Ctrl + scroll to zoom in the view" 58 | msgstr "Используйте Ctrl + scroll, чтобы увеличить" 59 | 60 | #: webtrees-descendants-chart/src/Module.php:261 61 | msgid "Move the view with two fingers" 62 | msgstr "Измените вид двумя пальцами" 63 | 64 | #: webtrees-descendants-chart/src/Traits/ModuleConfigTrait.php:95 65 | #, php-format 66 | msgid "The preferences for the module “%s” have been updated." 67 | msgstr "" 68 | 69 | #: webtrees-module-base/src/Processor/DateProcessor.php:129 70 | #, php-format 71 | msgid "Born: %s" 72 | msgstr "Рождение: %s" 73 | 74 | #: webtrees-module-base/src/Processor/DateProcessor.php:133 75 | #, php-format 76 | msgid "Died: %s" 77 | msgstr "Смерть: %s" 78 | 79 | #: webtrees-module-base/src/Processor/DateProcessor.php:137 80 | msgid "Deceased" 81 | msgstr "Умерший" 82 | 83 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/center-button.phtml:18 84 | msgid "Re center" 85 | msgstr "Центрировать" 86 | 87 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/fullscreen-button.phtml:18 88 | msgid "Switch between full screen mode and normal view" 89 | msgstr "" 90 | 91 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/help-button.phtml:17 92 | msgid "Show help" 93 | msgstr "" 94 | 95 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/png-export-button.phtml:18 96 | msgid "Export as PNG" 97 | msgstr "Экспорт в PNG" 98 | 99 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/svg-export-button.phtml:18 100 | msgid "Export as SVG" 101 | msgstr "Экспорт в SVG" 102 | 103 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:25 104 | msgid "Control panel" 105 | msgstr "" 106 | 107 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:25 108 | msgid "Modules" 109 | msgstr "" 110 | 111 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:44 112 | msgid "General" 113 | msgstr "" 114 | 115 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:50 116 | msgid "Hide the SVG export button" 117 | msgstr "" 118 | 119 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:60 120 | msgid "Hide the PNG export button" 121 | msgstr "" 122 | 123 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:73 124 | msgid "save" 125 | msgstr "" 126 | 127 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:78 128 | msgid "cancel" 129 | msgstr "" 130 | 131 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/generations.phtml:22 132 | msgid "Generations" 133 | msgstr "Поколения" 134 | 135 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:23 136 | msgid "Layout" 137 | msgstr "Ansicht" 138 | 139 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:29 140 | msgid "Hide the spouses associated with an individual" 141 | msgstr "" 142 | 143 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:39 144 | msgid "Show married names" 145 | msgstr "" 146 | 147 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:45 148 | msgid "" 149 | "Shows the married name of the partner if this has a separate name entry of " 150 | "the type \"MARRIED\" or \"_MARNM\"." 151 | msgstr "" 152 | 153 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:53 154 | msgid "Open individual in new browser window/tab" 155 | msgstr "" 156 | 157 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:59 158 | msgid "" 159 | "Open the current individual's detail page in a new browser window/tab when " 160 | "it's left-clicked, otherwise the current window/tab is used." 161 | msgstr "" 162 | 163 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:67 164 | msgid "Show alternative name of individual" 165 | msgstr "" 166 | 167 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:73 168 | msgid "" 169 | "Displays a person's alternate name, if available. This simultaneously " 170 | "enlarges the individuals' respective boxes to create the necessary space for " 171 | "the display." 172 | msgstr "" 173 | 174 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/orientation.phtml:22 175 | msgid "Orientation" 176 | msgstr "Ориентация" 177 | 178 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:39 179 | msgid "Individual" 180 | msgstr "Человек" 181 | 182 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:64 183 | msgid "view" 184 | msgstr "обновить" 185 | 186 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:75 187 | msgid "Show more options" 188 | msgstr "" 189 | 190 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:76 191 | msgid "Hide more options" 192 | msgstr "" 193 | -------------------------------------------------------------------------------- /resources/lang/es/messages.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: Descendants chart\n" 4 | "POT-Creation-Date: 2024-12-22 12:28+0100\n" 5 | "PO-Revision-Date: 2024-12-22 12:55+0100\n" 6 | "Last-Translator: Rico Sonntag \n" 7 | "Language-Team: \n" 8 | "Language: es\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 13 | "X-Generator: Poedit 3.5\n" 14 | "X-Poedit-Basepath: ../../../..\n" 15 | "X-Poedit-KeywordsList: translate\n" 16 | "X-Poedit-SearchPath-0: webtrees-descendants-chart/module.php\n" 17 | "X-Poedit-SearchPath-1: webtrees-descendants-chart/src\n" 18 | "X-Poedit-SearchPath-2: webtrees-descendants-chart/resources/views\n" 19 | "X-Poedit-SearchPath-3: webtrees-module-base/src\n" 20 | 21 | #: webtrees-descendants-chart/src/Configuration.php:173 22 | #: webtrees-descendants-chart/src/Configuration.php:181 23 | msgid "left" 24 | msgstr "izquierda" 25 | 26 | #: webtrees-descendants-chart/src/Configuration.php:174 27 | #: webtrees-descendants-chart/src/Configuration.php:182 28 | msgid "right" 29 | msgstr "derecha" 30 | 31 | #: webtrees-descendants-chart/src/Configuration.php:175 32 | #: webtrees-descendants-chart/src/Configuration.php:183 33 | msgid "up" 34 | msgstr "arriba" 35 | 36 | #: webtrees-descendants-chart/src/Configuration.php:176 37 | #: webtrees-descendants-chart/src/Configuration.php:184 38 | msgid "down" 39 | msgstr "abajo" 40 | 41 | #: webtrees-descendants-chart/src/Module.php:131 42 | #: webtrees-descendants-chart/src/Module.php:247 43 | msgid "Descendants chart" 44 | msgstr "Árbol de descendientes" 45 | 46 | #: webtrees-descendants-chart/src/Module.php:141 47 | msgid "An overview of an individual’s descendants." 48 | msgstr "Vista individual de descendientes." 49 | 50 | #: webtrees-descendants-chart/src/Module.php:244 51 | #: webtrees-descendants-chart/src/Traits/ModuleChartTrait.php:41 52 | #, php-format 53 | msgid "Descendants chart of %s" 54 | msgstr "Árbol de descendientes de %s" 55 | 56 | #: webtrees-descendants-chart/src/Module.php:260 57 | msgid "Use Ctrl + scroll to zoom in the view" 58 | msgstr "Use Ctrl + scroll para acercar la vista" 59 | 60 | #: webtrees-descendants-chart/src/Module.php:261 61 | msgid "Move the view with two fingers" 62 | msgstr "Mueve la vista con dos dedos" 63 | 64 | #: webtrees-descendants-chart/src/Traits/ModuleConfigTrait.php:95 65 | #, php-format 66 | msgid "The preferences for the module “%s” have been updated." 67 | msgstr "" 68 | 69 | #: webtrees-module-base/src/Processor/DateProcessor.php:129 70 | #, php-format 71 | msgid "Born: %s" 72 | msgstr "Nació: %s" 73 | 74 | #: webtrees-module-base/src/Processor/DateProcessor.php:133 75 | #, php-format 76 | msgid "Died: %s" 77 | msgstr "Murio: %s" 78 | 79 | #: webtrees-module-base/src/Processor/DateProcessor.php:137 80 | msgid "Deceased" 81 | msgstr "Fallecido" 82 | 83 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/center-button.phtml:18 84 | msgid "Re center" 85 | msgstr "Volver a centrar" 86 | 87 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/fullscreen-button.phtml:18 88 | msgid "Switch between full screen mode and normal view" 89 | msgstr "" 90 | 91 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/help-button.phtml:17 92 | msgid "Show help" 93 | msgstr "" 94 | 95 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/png-export-button.phtml:18 96 | msgid "Export as PNG" 97 | msgstr "Exportar como PNG" 98 | 99 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/svg-export-button.phtml:18 100 | msgid "Export as SVG" 101 | msgstr "Exportar como SVG" 102 | 103 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:25 104 | msgid "Control panel" 105 | msgstr "" 106 | 107 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:25 108 | msgid "Modules" 109 | msgstr "" 110 | 111 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:44 112 | msgid "General" 113 | msgstr "" 114 | 115 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:50 116 | msgid "Hide the SVG export button" 117 | msgstr "" 118 | 119 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:60 120 | msgid "Hide the PNG export button" 121 | msgstr "" 122 | 123 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:73 124 | msgid "save" 125 | msgstr "" 126 | 127 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:78 128 | msgid "cancel" 129 | msgstr "" 130 | 131 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/generations.phtml:22 132 | msgid "Generations" 133 | msgstr "Generaciones" 134 | 135 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:23 136 | msgid "Layout" 137 | msgstr "" 138 | 139 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:29 140 | msgid "Hide the spouses associated with an individual" 141 | msgstr "" 142 | 143 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:39 144 | msgid "Show married names" 145 | msgstr "" 146 | 147 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:45 148 | msgid "" 149 | "Shows the married name of the partner if this has a separate name entry of " 150 | "the type \"MARRIED\" or \"_MARNM\"." 151 | msgstr "" 152 | 153 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:53 154 | msgid "Open individual in new browser window/tab" 155 | msgstr "" 156 | 157 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:59 158 | msgid "" 159 | "Open the current individual's detail page in a new browser window/tab when " 160 | "it's left-clicked, otherwise the current window/tab is used." 161 | msgstr "" 162 | 163 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:67 164 | msgid "Show alternative name of individual" 165 | msgstr "" 166 | 167 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:73 168 | msgid "" 169 | "Displays a person's alternate name, if available. This simultaneously " 170 | "enlarges the individuals' respective boxes to create the necessary space for " 171 | "the display." 172 | msgstr "" 173 | 174 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/orientation.phtml:22 175 | msgid "Orientation" 176 | msgstr "Orientación" 177 | 178 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:39 179 | msgid "Individual" 180 | msgstr "Individuo" 181 | 182 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:64 183 | msgid "view" 184 | msgstr "ver" 185 | 186 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:75 187 | msgid "Show more options" 188 | msgstr "" 189 | 190 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:76 191 | msgid "Hide more options" 192 | msgstr "" 193 | -------------------------------------------------------------------------------- /resources/lang/fr/messages.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: Descendants chart\n" 4 | "POT-Creation-Date: 2024-12-22 12:28+0100\n" 5 | "PO-Revision-Date: 2024-12-22 12:55+0100\n" 6 | "Last-Translator: Rico Sonntag \n" 7 | "Language-Team: \n" 8 | "Language: fr\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 13 | "X-Generator: Poedit 3.5\n" 14 | "X-Poedit-Basepath: ../../../..\n" 15 | "X-Poedit-KeywordsList: translate\n" 16 | "X-Poedit-SearchPath-0: webtrees-descendants-chart/module.php\n" 17 | "X-Poedit-SearchPath-1: webtrees-descendants-chart/src\n" 18 | "X-Poedit-SearchPath-2: webtrees-descendants-chart/resources/views\n" 19 | "X-Poedit-SearchPath-3: webtrees-module-base/src\n" 20 | 21 | #: webtrees-descendants-chart/src/Configuration.php:173 22 | #: webtrees-descendants-chart/src/Configuration.php:181 23 | msgid "left" 24 | msgstr "gauche" 25 | 26 | #: webtrees-descendants-chart/src/Configuration.php:174 27 | #: webtrees-descendants-chart/src/Configuration.php:182 28 | msgid "right" 29 | msgstr "droite" 30 | 31 | #: webtrees-descendants-chart/src/Configuration.php:175 32 | #: webtrees-descendants-chart/src/Configuration.php:183 33 | msgid "up" 34 | msgstr "haut" 35 | 36 | #: webtrees-descendants-chart/src/Configuration.php:176 37 | #: webtrees-descendants-chart/src/Configuration.php:184 38 | msgid "down" 39 | msgstr "bas" 40 | 41 | #: webtrees-descendants-chart/src/Module.php:131 42 | #: webtrees-descendants-chart/src/Module.php:247 43 | msgid "Descendants chart" 44 | msgstr "Arbre de descendance" 45 | 46 | #: webtrees-descendants-chart/src/Module.php:141 47 | msgid "An overview of an individual’s descendants." 48 | msgstr "Un aperçu des descendants d’un individu." 49 | 50 | #: webtrees-descendants-chart/src/Module.php:244 51 | #: webtrees-descendants-chart/src/Traits/ModuleChartTrait.php:41 52 | #, php-format 53 | msgid "Descendants chart of %s" 54 | msgstr "Arbre de descendance de %s" 55 | 56 | #: webtrees-descendants-chart/src/Module.php:260 57 | msgid "Use Ctrl + scroll to zoom in the view" 58 | msgstr "Utilisez Ctrl + molette pour zoomer dans la vue" 59 | 60 | #: webtrees-descendants-chart/src/Module.php:261 61 | msgid "Move the view with two fingers" 62 | msgstr "Déplacez la vue avec deux doigts" 63 | 64 | #: webtrees-descendants-chart/src/Traits/ModuleConfigTrait.php:95 65 | #, php-format 66 | msgid "The preferences for the module “%s” have been updated." 67 | msgstr "" 68 | 69 | #: webtrees-module-base/src/Processor/DateProcessor.php:129 70 | #, php-format 71 | msgid "Born: %s" 72 | msgstr "Naissance: %s" 73 | 74 | #: webtrees-module-base/src/Processor/DateProcessor.php:133 75 | #, php-format 76 | msgid "Died: %s" 77 | msgstr "Décès: %s" 78 | 79 | #: webtrees-module-base/src/Processor/DateProcessor.php:137 80 | msgid "Deceased" 81 | msgstr "Décédé" 82 | 83 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/center-button.phtml:18 84 | msgid "Re center" 85 | msgstr "Re centrer" 86 | 87 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/fullscreen-button.phtml:18 88 | msgid "Switch between full screen mode and normal view" 89 | msgstr "" 90 | 91 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/help-button.phtml:17 92 | msgid "Show help" 93 | msgstr "" 94 | 95 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/png-export-button.phtml:18 96 | msgid "Export as PNG" 97 | msgstr "Exporter en PNG" 98 | 99 | #: webtrees-descendants-chart/resources/views/modules/components/buttonbar/svg-export-button.phtml:18 100 | msgid "Export as SVG" 101 | msgstr "Exporter en SVG" 102 | 103 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:25 104 | msgid "Control panel" 105 | msgstr "" 106 | 107 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:25 108 | msgid "Modules" 109 | msgstr "" 110 | 111 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:44 112 | msgid "General" 113 | msgstr "" 114 | 115 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:50 116 | msgid "Hide the SVG export button" 117 | msgstr "" 118 | 119 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:60 120 | msgid "Hide the PNG export button" 121 | msgstr "" 122 | 123 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:73 124 | msgid "save" 125 | msgstr "" 126 | 127 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/config.phtml:78 128 | msgid "cancel" 129 | msgstr "" 130 | 131 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/generations.phtml:22 132 | msgid "Generations" 133 | msgstr "Générations" 134 | 135 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:23 136 | msgid "Layout" 137 | msgstr "" 138 | 139 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:29 140 | msgid "Hide the spouses associated with an individual" 141 | msgstr "" 142 | 143 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:39 144 | msgid "Show married names" 145 | msgstr "" 146 | 147 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:45 148 | msgid "" 149 | "Shows the married name of the partner if this has a separate name entry of " 150 | "the type \"MARRIED\" or \"_MARNM\"." 151 | msgstr "" 152 | 153 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:53 154 | msgid "Open individual in new browser window/tab" 155 | msgstr "" 156 | 157 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:59 158 | msgid "" 159 | "Open the current individual's detail page in a new browser window/tab when " 160 | "it's left-clicked, otherwise the current window/tab is used." 161 | msgstr "" 162 | 163 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:67 164 | msgid "Show alternative name of individual" 165 | msgstr "" 166 | 167 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/layout.phtml:73 168 | msgid "" 169 | "Displays a person's alternate name, if available. This simultaneously " 170 | "enlarges the individuals' respective boxes to create the necessary space for " 171 | "the display." 172 | msgstr "" 173 | 174 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/form/orientation.phtml:22 175 | msgid "Orientation" 176 | msgstr "Orientation" 177 | 178 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:39 179 | msgid "Individual" 180 | msgstr "Individu" 181 | 182 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:64 183 | msgid "view" 184 | msgstr "Afficher" 185 | 186 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:75 187 | msgid "Show more options" 188 | msgstr "" 189 | 190 | #: webtrees-descendants-chart/resources/views/modules/descendants-chart/page.phtml:76 191 | msgid "Hide more options" 192 | msgstr "" 193 | --------------------------------------------------------------------------------